Consolidate GVL deletion workflow and document purge semantics

Dieser Commit ist enthalten in:
2026-06-10 23:39:07 +02:00
Ursprung 61a20c424c
Commit 8d923ba962
10 geänderte Dateien mit 230 neuen und 111 gelöschten Zeilen
+28
Datei anzeigen
@@ -0,0 +1,28 @@
### GVL-Purge-Summary und Snapshot-Events
Bei der Untersuchung der GVL-Bereinigung wurde festgestellt, dass die Purge-Summary je nach Herkunft einer GVL-Revision unterschiedliche Gesamtzahlen ausweisen kann.
Beobachtung:
* Vault-GVL importiert → Purge-Summary: `13593 Records`
* Web-GVL geladen, passende Vault-GVL verifiziert → Purge-Summary: `13594 Records`
Die Differenz von genau einem Record stellt keinen Zählfehler dar.
Ursache:
Der Web-Ingest einer offiziellen GVL erzeugt zusätzlich einen Eintrag im Store `gvl_snapshot_events`. Dabei handelt es sich um ein Ereignis des lokalen Beobachtungsverlaufs, beispielsweise `gvl_snapshot_ingested`.
Der Import einer GVL-Revision aus einem Vault-Paket rekonstruiert dagegen die Revision selbst, importiert jedoch keine Snapshot-Event-Historie und erzeugt auch kein entsprechendes Ereignis.
Die globale Purge-Summary zählt `gvl_snapshot_events` mit. Daher entsteht im Web-Pfad gegenüber dem Vault-Pfad ein zusätzlicher gelöschter Record.
Fachliche Einordnung:
Die unterschiedliche Gesamtzahl ist erwartetes Verhalten und Ausdruck unterschiedlicher Provenienz:
* Web-GVL: dokumentierter lokaler Ingest-Vorgang.
* Vault-GVL: rekonstruiertes Evidenzobjekt ohne lokale Beobachtungshistorie.
Die Purge-Summary ist daher nicht nur ein Maß für die Größe einer Revision, sondern kann auch Unterschiede im Entstehungskontext der lokalen Evidence widerspiegeln.
+2 -11
Datei anzeigen
@@ -125,10 +125,6 @@ async function handleVendorGetMessage(message, sender) {
return handlePurgeUnlockedEvidenceRecordsMessage(); return handlePurgeUnlockedEvidenceRecordsMessage();
} }
if (message.type === "purge_gvl_reference_data") {
return handlePurgeGvlReferenceDataMessage();
}
if (message.type === "delete_all_evidence_database") { if (message.type === "delete_all_evidence_database") {
return handleDeleteAllEvidenceDatabaseMessage(); return handleDeleteAllEvidenceDatabaseMessage();
} }
@@ -302,7 +298,8 @@ async function handleMarkGvlRevisionEvidenceVaultCopyMessage(message) {
return { return {
success: true, success: true,
mark: await markVendorGetGvlRevisionEvidenceVaultCopy( mark: await markVendorGetGvlRevisionEvidenceVaultCopy(
message?.payload?.snapshotSha256 ?? null message?.payload?.snapshotSha256 ?? null,
message?.payload?.verification ?? null
) )
}; };
} catch (error) { } catch (error) {
@@ -1229,12 +1226,6 @@ async function handlePurgeUnlockedEvidenceRecordsMessage() {
return purgeUnlockedEvidenceRecords(db); return purgeUnlockedEvidenceRecords(db);
} }
async function handlePurgeGvlReferenceDataMessage() {
const db = await openVendorGetDb();
return purgeGvlReferenceData(db);
}
function handleDeleteAllEvidenceDatabaseMessage() { function handleDeleteAllEvidenceDatabaseMessage() {
return deleteVendorGetDatabase(); return deleteVendorGetDatabase();
} }
-13
Datei anzeigen
@@ -33,16 +33,3 @@ const VENDORGET_EVIDENCE_STORE_NAMES = [
VENDORGET_STORE_NAMES.gvlDataCategories, VENDORGET_STORE_NAMES.gvlDataCategories,
VENDORGET_STORE_NAMES.gvlVendorRelationships VENDORGET_STORE_NAMES.gvlVendorRelationships
]; ];
const VENDORGET_GVL_REFERENCE_STORE_NAMES = [
VENDORGET_STORE_NAMES.gvlRawEvidence,
VENDORGET_STORE_NAMES.gvlSnapshots,
VENDORGET_STORE_NAMES.gvlSnapshotEvents,
VENDORGET_STORE_NAMES.gvlVendors,
VENDORGET_STORE_NAMES.gvlPurposes,
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
VENDORGET_STORE_NAMES.gvlFeatures,
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
VENDORGET_STORE_NAMES.gvlDataCategories,
VENDORGET_STORE_NAMES.gvlVendorRelationships
];
+5 -25
Datei anzeigen
@@ -89,31 +89,11 @@ async function purgeUnlockedEvidenceRecords(db) {
}); });
} }
function purgeGvlReferenceData(db) { // TODO: GVL-Datenpflege darf nicht storeweise per clear() erfolgen.
return new Promise((resolve, reject) => { // Loeschbar ist nur eine GVL-Revision, wenn ihre zugehoerigen Raw-, Snapshot-,
const clearedStores = {}; // Event- und normalisierten Records identifiziert sind, ihr Schutzstatus
const tx = db.transaction(VENDORGET_GVL_REFERENCE_STORE_NAMES, "readwrite"); // vollstaendig bewertet wurde und eine vorhandene Vault-/Workspace-Schutzlogik
// die Loeschung erlaubt.
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => {
resolve({
success: true,
clearedStores
});
};
for (const storeName of VENDORGET_GVL_REFERENCE_STORE_NAMES) {
const objectStore = tx.objectStore(storeName);
const countRequest = objectStore.count();
countRequest.onsuccess = () => {
clearedStores[storeName] = countRequest.result;
objectStore.clear();
};
}
});
}
function buildGvlWorkspaceProtectionIndex(db) { function buildGvlWorkspaceProtectionIndex(db) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
+67 -7
Datei anzeigen
@@ -287,14 +287,21 @@ async function importVendorGetGvlRevisionEvidenceJson(exportContainer) {
}; };
} }
async function markVendorGetGvlRevisionEvidenceVaultCopy(snapshotSha256) { async function markVendorGetGvlRevisionEvidenceVaultCopy(
snapshotSha256,
verification = null
) {
if (!snapshotSha256) { if (!snapshotSha256) {
throw new Error("missing_snapshot_sha256"); throw new Error("missing_snapshot_sha256");
} }
const db = await openVendorGetDb(); const db = await openVendorGetDb();
return markGvlRevisionEvidenceVaultCopyAvailable(db, snapshotSha256); return markGvlRevisionEvidenceVaultCopyAvailable(
db,
snapshotSha256,
verification
);
} }
function getGvlEvidenceRecordByKey(db, storeName, key) { function getGvlEvidenceRecordByKey(db, storeName, key) {
@@ -632,9 +639,16 @@ function formatGvlEvidenceProvenance(values) {
return "web"; return "web";
} }
function markGvlRevisionEvidenceVaultCopyAvailable(db, snapshotSha256) { function markGvlRevisionEvidenceVaultCopyAvailable(
return updateGvlRevisionEvidenceRecords(db, snapshotSha256, (record) => db,
markGvlEvidenceRecordVaultCopyAvailable(record) snapshotSha256,
verification = null
) {
return updateGvlRevisionEvidenceRecords(
db,
snapshotSha256,
(record) => markGvlEvidenceRecordVaultCopyAvailable(record),
verification
); );
} }
@@ -652,7 +666,12 @@ function markGvlRevisionEvidenceProvenance(db, snapshotSha256, provenance) {
); );
} }
function updateGvlRevisionEvidenceRecords(db, snapshotSha256, updateRecord) { function updateGvlRevisionEvidenceRecords(
db,
snapshotSha256,
updateRecord,
verification = null
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction( const tx = db.transaction(
[VENDORGET_STORE_NAMES.gvlSnapshots, VENDORGET_STORE_NAMES.gvlRawEvidence], [VENDORGET_STORE_NAMES.gvlSnapshots, VENDORGET_STORE_NAMES.gvlRawEvidence],
@@ -665,7 +684,8 @@ function updateGvlRevisionEvidenceRecords(db, snapshotSha256, updateRecord) {
snapshotMarked: false, snapshotMarked: false,
rawEvidenceMarked: false, rawEvidenceMarked: false,
snapshotSha256, snapshotSha256,
rawGvlSha256: null rawGvlSha256: null,
skippedReason: null
}; };
snapshotRequest.onerror = () => reject(snapshotRequest.error); snapshotRequest.onerror = () => reject(snapshotRequest.error);
@@ -673,6 +693,12 @@ function updateGvlRevisionEvidenceRecords(db, snapshotSha256, updateRecord) {
const snapshot = snapshotRequest.result ?? null; const snapshot = snapshotRequest.result ?? null;
if (!snapshot) { if (!snapshot) {
result.skippedReason = "gvl_snapshot_not_found";
return;
}
if (!doesGvlRevisionEvidenceMatchVerification(snapshot, verification)) {
result.skippedReason = "gvl_revision_evidence_verification_mismatch";
return; return;
} }
@@ -708,6 +734,40 @@ function updateGvlRevisionEvidenceRecords(db, snapshotSha256, updateRecord) {
}); });
} }
function doesGvlRevisionEvidenceMatchVerification(snapshot, verification) {
if (!verification) {
return true;
}
if (verification.valid !== true) {
return false;
}
if (
verification.snapshotSha256 &&
snapshot.sha256 !== verification.snapshotSha256
) {
return false;
}
if (
verification.vendorListVersion !== null &&
verification.vendorListVersion !== undefined &&
snapshot.vendorListVersion !== verification.vendorListVersion
) {
return false;
}
if (
verification.rawGvlSha256 &&
snapshot.rawGvlSha256 !== verification.rawGvlSha256
) {
return false;
}
return true;
}
function formatGvlEvidenceUtcCompact(date) { function formatGvlEvidenceUtcCompact(date) {
return [ return [
date.getUTCFullYear(), date.getUTCFullYear(),
+10 -9
Datei anzeigen
@@ -81,15 +81,6 @@
<strong>Analyse-Vorbereitung</strong> <strong>Analyse-Vorbereitung</strong>
<span>Datenbestände und vorbereitete Prüffelder, keine Engine.</span> <span>Datenbestände und vorbereitete Prüffelder, keine Engine.</span>
</a> </a>
<a class="workspace-link workspace-placeholder" href="../data-maintenance/data-maintenance.html">
<strong>Datenpflege</strong>
<span>
Gezielte Verwaltung lokaler Datenbestände. Löschen,
Wiederherstellen und Exportieren erfolgen künftig segmentbezogen.
Vorgesehene Segmente sind GVL-Referenzdaten der Browser-DB,
Consent-Daten, Analyse-Daten und weitere künftige Datenbereiche.
</span>
</a>
</div> </div>
</section> </section>
@@ -127,6 +118,16 @@
</dl> </dl>
</section> </section>
<section class="panel" aria-labelledby="data-maintenance-title">
<h2 id="data-maintenance-title">Datenpflege</h2>
<div class="workspace-actions">
<a class="workspace-link workspace-placeholder" href="../data-maintenance/data-maintenance.html">
<strong>Datenpflege</strong>
<span>Verwaltung lokaler Datenbestände.</span>
</a>
</div>
</section>
</main> </main>
<script src="dashboard.js"></script> <script src="dashboard.js"></script>
@@ -113,6 +113,10 @@ p {
background: #172033; background: #172033;
} }
.protected-revisions {
white-space: pre-line;
}
button { button {
width: fit-content; width: fit-content;
max-width: 100%; max-width: 100%;
+11 -2
Datei anzeigen
@@ -31,7 +31,7 @@
Consent-Daten, Request-Beobachtungen und Analyse-Daten bleiben Consent-Daten, Request-Beobachtungen und Analyse-Daten bleiben
unberührt. unberührt.
</p> </p>
<button id="purge-gvl-reference-data-button" type="button"> <button id="purge-gvl-reference-data-button" type="button" disabled>
GVL-Referenzdaten bereinigen GVL-Referenzdaten bereinigen
</button> </button>
</div> </div>
@@ -40,7 +40,16 @@
class="segment-status" class="segment-status"
aria-live="polite" aria-live="polite"
> >
Bereit.
</p>
<p
id="gvl-reference-protected-revisions"
class="protected-revisions"
aria-live="polite"
hidden
></p>
<p id="gvl-reference-maintenance-message">
Keine GVL-Revisionen.
</p> </p>
</article> </article>
+91 -41
Datei anzeigen
@@ -1,69 +1,119 @@
"use strict"; "use strict";
const purgeGvlReferenceDataButton = document.getElementById(
"purge-gvl-reference-data-button"
);
const gvlReferenceMaintenanceStatus = document.getElementById( const gvlReferenceMaintenanceStatus = document.getElementById(
"gvl-reference-maintenance-status" "gvl-reference-maintenance-status"
); );
const gvlReferenceProtectedRevisions = document.getElementById(
"gvl-reference-protected-revisions"
);
const gvlReferenceMaintenanceMessage = document.getElementById(
"gvl-reference-maintenance-message"
);
const purgeGvlReferenceDataButton = document.getElementById(
"purge-gvl-reference-data-button"
);
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
purgeGvlReferenceDataButton?.addEventListener("click", async () => { purgeGvlReferenceDataButton.addEventListener("click", async () => {
await purgeGvlReferenceData(); await purgeUnlockedEvidenceRecords();
}); });
await renderGvlReferenceMaintenanceStatus();
}); });
async function purgeGvlReferenceData() { async function renderGvlReferenceMaintenanceStatus() {
if (!confirm(buildGvlReferenceDataPurgeConfirmationText())) { try {
renderGvlReferenceMaintenanceStatus("Abgebrochen."); const result = await browser.runtime.sendMessage({
type: "list_gvl_snapshots"
});
if (!result?.success) {
throw new Error(result?.error ?? "list_gvl_snapshots_failed");
}
renderGvlReferenceSnapshotStatus(result.gvlSnapshots ?? []);
} catch (error) {
renderGvlReferenceStatus("–", true, "Status nicht verfügbar.");
renderProtectedRevisions([]);
console.warn("VG-Observe GVL maintenance status failed", error);
}
}
function renderGvlReferenceSnapshotStatus(snapshots) {
if (!snapshots.length) {
renderGvlReferenceStatus("–", true, "Keine GVL-Revisionen.");
renderProtectedRevisions([]);
return;
}
const protectedRevisions = snapshots
.filter((snapshot) => snapshot.workspaceDeleteProtected === true)
.map((snapshot) => snapshot.vendorListVersion)
.filter((vendorListVersion) => vendorListVersion !== null)
.filter((vendorListVersion) => vendorListVersion !== undefined);
const allRevisionsDeleteAllowed = snapshots.every((snapshot) => {
return snapshot.workspaceDeleteAllowed === true;
});
if (allRevisionsDeleteAllowed) {
renderGvlReferenceStatus("🔓", false, "GVL-Revisionen löschbar.");
} else if (protectedRevisions.length) {
renderGvlReferenceStatus("🔒", true, "GVL-Revisionen geschützt.");
} else {
renderGvlReferenceStatus("–", true, "Status nicht verfügbar.");
}
renderProtectedRevisions(protectedRevisions);
}
function renderGvlReferenceStatus(statusSymbol, buttonDisabled, message) {
gvlReferenceMaintenanceStatus.textContent = statusSymbol;
purgeGvlReferenceDataButton.disabled = buttonDisabled;
gvlReferenceMaintenanceMessage.textContent = message;
}
async function purgeUnlockedEvidenceRecords() {
if (
!confirm(
"Ungesperrte Evidence-Daten mit bestehender Schutzlogik bereinigen?"
)
) {
return; return;
} }
purgeGvlReferenceDataButton.disabled = true; purgeGvlReferenceDataButton.disabled = true;
renderGvlReferenceMaintenanceStatus("GVL-Referenzdaten werden bereinigt..."); gvlReferenceMaintenanceMessage.textContent = "Bereinigung läuft...";
try { try {
const result = await browser.runtime.sendMessage({ const result = await browser.runtime.sendMessage({
type: "purge_gvl_reference_data" type: "purge_unlocked_evidence_records"
}); });
if (!result?.success) { if (!result?.success) {
throw new Error(result?.error ?? "purge_gvl_reference_data_failed"); throw new Error(result?.error ?? "purge_unlocked_evidence_records_failed");
} }
renderGvlReferenceMaintenanceStatus( await renderGvlReferenceMaintenanceStatus();
buildGvlReferenceDataPurgeSuccessMessage(result) gvlReferenceMaintenanceMessage.textContent = buildPurgeSuccessMessage(result);
);
} catch (error) { } catch (error) {
renderGvlReferenceMaintenanceStatus( await renderGvlReferenceMaintenanceStatus();
"GVL-Referenzdaten konnten nicht bereinigt werden." gvlReferenceMaintenanceMessage.textContent = "Bereinigung fehlgeschlagen.";
); console.warn("VG-Observe protected purge failed", error);
console.warn("VG-Observe GVL reference data purge failed", error);
} finally {
purgeGvlReferenceDataButton.disabled = false;
} }
} }
function buildGvlReferenceDataPurgeConfirmationText() { function buildPurgeSuccessMessage(result) {
return [ if (Number.isFinite(result.deletedCount)) {
"GVL-Referenzdaten bereinigen?", return `Bereinigung abgeschlossen: ${result.deletedCount} Records.`;
"", }
"Betroffen: GVL-Referenzdaten der Browser-DB.",
"Nicht betroffen: Consent-Daten, Request-Beobachtungen, Analyse-Daten.", return "Bereinigung abgeschlossen.";
"",
"Diese Aktion entfernt lokale GVL-Referenzdaten aus der Browser-Datenbank."
].join("\n");
} }
function buildGvlReferenceDataPurgeSuccessMessage(result) { function renderProtectedRevisions(vendorListVersions) {
const clearedCount = Object.values(result.clearedStores ?? {}).reduce( gvlReferenceProtectedRevisions.hidden = vendorListVersions.length === 0;
(total, count) => total + Number(count ?? 0), gvlReferenceProtectedRevisions.textContent = vendorListVersions.length
0 ? `Geschützt: ${vendorListVersions
); .map((vendorListVersion) => String(vendorListVersion))
.join(", ")}`
return `GVL-Referenzdaten bereinigt: ${clearedCount} Records entfernt.`; : "";
}
function renderGvlReferenceMaintenanceStatus(message) {
gvlReferenceMaintenanceStatus.textContent = message;
} }
+12 -3
Datei anzeigen
@@ -215,7 +215,7 @@ async function exportSelectedGvlRevisionEvidenceJsonFile() {
} }
downloadGvlRevisionEvidenceJsonExport(result.export); downloadGvlRevisionEvidenceJsonExport(result.export);
await markGvlRevisionEvidenceVaultCopy(result.export); await markGvlRevisionEvidenceVaultCopy(result.export, verification);
await renderGvlSnapshots(); await renderGvlSnapshots();
renderGvlEvidenceTransportStatus( renderGvlEvidenceTransportStatus(
[ [
@@ -271,7 +271,10 @@ function downloadGvlRevisionEvidenceJsonExport(exportContainer) {
setTimeout(() => URL.revokeObjectURL(url), 0); setTimeout(() => URL.revokeObjectURL(url), 0);
} }
async function markGvlRevisionEvidenceVaultCopy(exportContainer) { async function markGvlRevisionEvidenceVaultCopy(
exportContainer,
verification = null
) {
const snapshotSha256 = exportContainer?.metadata?.snapshotSha256 ?? null; const snapshotSha256 = exportContainer?.metadata?.snapshotSha256 ?? null;
if (!snapshotSha256) { if (!snapshotSha256) {
@@ -281,7 +284,8 @@ async function markGvlRevisionEvidenceVaultCopy(exportContainer) {
const result = await browser.runtime.sendMessage({ const result = await browser.runtime.sendMessage({
type: "mark_gvl_revision_evidence_vault_copy", type: "mark_gvl_revision_evidence_vault_copy",
payload: { payload: {
snapshotSha256 snapshotSha256,
verification
} }
}); });
@@ -338,6 +342,11 @@ async function verifyGvlRevisionEvidenceJsonFile() {
exportContainer exportContainer
); );
if (verification.valid) {
await markGvlRevisionEvidenceVaultCopy(exportContainer, verification);
await renderGvlSnapshots();
}
renderGvlEvidenceTransportStatus( renderGvlEvidenceTransportStatus(
buildGvlRevisionEvidenceVerificationMessage(verification), buildGvlRevisionEvidenceVerificationMessage(verification),
verification.valid ? "success" : "error" verification.valid ? "success" : "error"