From 8d923ba9627f303d981733c66a755dca6353121b Mon Sep 17 00:00:00 2001 From: jensmohr Date: Wed, 10 Jun 2026 23:39:07 +0200 Subject: [PATCH] Consolidate GVL deletion workflow and document purge semantics --- docs/architecture/gvl-purge-notes.md | 28 +++++ src/background.js | 13 +- src/background/db/db-constants.js | 13 -- src/background/db/db-retention.js | 30 +---- src/core/gvl-evidence-json.js | 74 ++++++++++-- src/dashboard/dashboard.html | 19 +-- src/data-maintenance/data-maintenance.css | 4 + src/data-maintenance/data-maintenance.html | 13 +- src/data-maintenance/data-maintenance.js | 132 ++++++++++++++------- src/gvl-explorer/gvl-explorer.js | 15 ++- 10 files changed, 230 insertions(+), 111 deletions(-) create mode 100644 docs/architecture/gvl-purge-notes.md diff --git a/docs/architecture/gvl-purge-notes.md b/docs/architecture/gvl-purge-notes.md new file mode 100644 index 0000000..a8eb3a0 --- /dev/null +++ b/docs/architecture/gvl-purge-notes.md @@ -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. + diff --git a/src/background.js b/src/background.js index 8bd3791..9781a36 100644 --- a/src/background.js +++ b/src/background.js @@ -125,10 +125,6 @@ async function handleVendorGetMessage(message, sender) { return handlePurgeUnlockedEvidenceRecordsMessage(); } - if (message.type === "purge_gvl_reference_data") { - return handlePurgeGvlReferenceDataMessage(); - } - if (message.type === "delete_all_evidence_database") { return handleDeleteAllEvidenceDatabaseMessage(); } @@ -302,7 +298,8 @@ async function handleMarkGvlRevisionEvidenceVaultCopyMessage(message) { return { success: true, mark: await markVendorGetGvlRevisionEvidenceVaultCopy( - message?.payload?.snapshotSha256 ?? null + message?.payload?.snapshotSha256 ?? null, + message?.payload?.verification ?? null ) }; } catch (error) { @@ -1229,12 +1226,6 @@ async function handlePurgeUnlockedEvidenceRecordsMessage() { return purgeUnlockedEvidenceRecords(db); } -async function handlePurgeGvlReferenceDataMessage() { - const db = await openVendorGetDb(); - - return purgeGvlReferenceData(db); -} - function handleDeleteAllEvidenceDatabaseMessage() { return deleteVendorGetDatabase(); } diff --git a/src/background/db/db-constants.js b/src/background/db/db-constants.js index a2aebec..12dae18 100644 --- a/src/background/db/db-constants.js +++ b/src/background/db/db-constants.js @@ -33,16 +33,3 @@ const VENDORGET_EVIDENCE_STORE_NAMES = [ VENDORGET_STORE_NAMES.gvlDataCategories, 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 -]; diff --git a/src/background/db/db-retention.js b/src/background/db/db-retention.js index a561e0f..00fb037 100644 --- a/src/background/db/db-retention.js +++ b/src/background/db/db-retention.js @@ -89,31 +89,11 @@ async function purgeUnlockedEvidenceRecords(db) { }); } -function purgeGvlReferenceData(db) { - return new Promise((resolve, reject) => { - const clearedStores = {}; - const tx = db.transaction(VENDORGET_GVL_REFERENCE_STORE_NAMES, "readwrite"); - - 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(); - }; - } - }); -} +// TODO: GVL-Datenpflege darf nicht storeweise per clear() erfolgen. +// Loeschbar ist nur eine GVL-Revision, wenn ihre zugehoerigen Raw-, Snapshot-, +// Event- und normalisierten Records identifiziert sind, ihr Schutzstatus +// vollstaendig bewertet wurde und eine vorhandene Vault-/Workspace-Schutzlogik +// die Loeschung erlaubt. function buildGvlWorkspaceProtectionIndex(db) { return new Promise((resolve, reject) => { diff --git a/src/core/gvl-evidence-json.js b/src/core/gvl-evidence-json.js index 8a5ea83..c1e7277 100644 --- a/src/core/gvl-evidence-json.js +++ b/src/core/gvl-evidence-json.js @@ -287,14 +287,21 @@ async function importVendorGetGvlRevisionEvidenceJson(exportContainer) { }; } -async function markVendorGetGvlRevisionEvidenceVaultCopy(snapshotSha256) { +async function markVendorGetGvlRevisionEvidenceVaultCopy( + snapshotSha256, + verification = null +) { if (!snapshotSha256) { throw new Error("missing_snapshot_sha256"); } const db = await openVendorGetDb(); - return markGvlRevisionEvidenceVaultCopyAvailable(db, snapshotSha256); + return markGvlRevisionEvidenceVaultCopyAvailable( + db, + snapshotSha256, + verification + ); } function getGvlEvidenceRecordByKey(db, storeName, key) { @@ -632,9 +639,16 @@ function formatGvlEvidenceProvenance(values) { return "web"; } -function markGvlRevisionEvidenceVaultCopyAvailable(db, snapshotSha256) { - return updateGvlRevisionEvidenceRecords(db, snapshotSha256, (record) => - markGvlEvidenceRecordVaultCopyAvailable(record) +function markGvlRevisionEvidenceVaultCopyAvailable( + db, + 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) => { const tx = db.transaction( [VENDORGET_STORE_NAMES.gvlSnapshots, VENDORGET_STORE_NAMES.gvlRawEvidence], @@ -665,7 +684,8 @@ function updateGvlRevisionEvidenceRecords(db, snapshotSha256, updateRecord) { snapshotMarked: false, rawEvidenceMarked: false, snapshotSha256, - rawGvlSha256: null + rawGvlSha256: null, + skippedReason: null }; snapshotRequest.onerror = () => reject(snapshotRequest.error); @@ -673,6 +693,12 @@ function updateGvlRevisionEvidenceRecords(db, snapshotSha256, updateRecord) { const snapshot = snapshotRequest.result ?? null; if (!snapshot) { + result.skippedReason = "gvl_snapshot_not_found"; + return; + } + + if (!doesGvlRevisionEvidenceMatchVerification(snapshot, verification)) { + result.skippedReason = "gvl_revision_evidence_verification_mismatch"; 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) { return [ date.getUTCFullYear(), diff --git a/src/dashboard/dashboard.html b/src/dashboard/dashboard.html index 121a349..b27729f 100644 --- a/src/dashboard/dashboard.html +++ b/src/dashboard/dashboard.html @@ -81,15 +81,6 @@ Analyse-Vorbereitung Datenbestände und vorbereitete Prüffelder, keine Engine. - - Datenpflege - - 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. - - @@ -127,6 +118,16 @@ +
+

Datenpflege

+ +
+ diff --git a/src/data-maintenance/data-maintenance.css b/src/data-maintenance/data-maintenance.css index 54f738d..7b61bfd 100644 --- a/src/data-maintenance/data-maintenance.css +++ b/src/data-maintenance/data-maintenance.css @@ -113,6 +113,10 @@ p { background: #172033; } +.protected-revisions { + white-space: pre-line; +} + button { width: fit-content; max-width: 100%; diff --git a/src/data-maintenance/data-maintenance.html b/src/data-maintenance/data-maintenance.html index b0f8182..581548f 100644 --- a/src/data-maintenance/data-maintenance.html +++ b/src/data-maintenance/data-maintenance.html @@ -31,7 +31,7 @@ Consent-Daten, Request-Beobachtungen und Analyse-Daten bleiben unberührt.

- @@ -40,7 +40,16 @@ class="segment-status" aria-live="polite" > - Bereit. + – +

+ +

+ Keine GVL-Revisionen.

diff --git a/src/data-maintenance/data-maintenance.js b/src/data-maintenance/data-maintenance.js index 26fb5f1..1697e8a 100644 --- a/src/data-maintenance/data-maintenance.js +++ b/src/data-maintenance/data-maintenance.js @@ -1,69 +1,119 @@ "use strict"; -const purgeGvlReferenceDataButton = document.getElementById( - "purge-gvl-reference-data-button" -); const gvlReferenceMaintenanceStatus = document.getElementById( "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", () => { - purgeGvlReferenceDataButton?.addEventListener("click", async () => { - await purgeGvlReferenceData(); +document.addEventListener("DOMContentLoaded", async () => { + purgeGvlReferenceDataButton.addEventListener("click", async () => { + await purgeUnlockedEvidenceRecords(); }); + await renderGvlReferenceMaintenanceStatus(); }); -async function purgeGvlReferenceData() { - if (!confirm(buildGvlReferenceDataPurgeConfirmationText())) { - renderGvlReferenceMaintenanceStatus("Abgebrochen."); +async function renderGvlReferenceMaintenanceStatus() { + try { + 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; } purgeGvlReferenceDataButton.disabled = true; - renderGvlReferenceMaintenanceStatus("GVL-Referenzdaten werden bereinigt..."); + gvlReferenceMaintenanceMessage.textContent = "Bereinigung läuft..."; try { const result = await browser.runtime.sendMessage({ - type: "purge_gvl_reference_data" + type: "purge_unlocked_evidence_records" }); if (!result?.success) { - throw new Error(result?.error ?? "purge_gvl_reference_data_failed"); + throw new Error(result?.error ?? "purge_unlocked_evidence_records_failed"); } - renderGvlReferenceMaintenanceStatus( - buildGvlReferenceDataPurgeSuccessMessage(result) - ); + await renderGvlReferenceMaintenanceStatus(); + gvlReferenceMaintenanceMessage.textContent = buildPurgeSuccessMessage(result); } catch (error) { - renderGvlReferenceMaintenanceStatus( - "GVL-Referenzdaten konnten nicht bereinigt werden." - ); - console.warn("VG-Observe GVL reference data purge failed", error); - } finally { - purgeGvlReferenceDataButton.disabled = false; + await renderGvlReferenceMaintenanceStatus(); + gvlReferenceMaintenanceMessage.textContent = "Bereinigung fehlgeschlagen."; + console.warn("VG-Observe protected purge failed", error); } } -function buildGvlReferenceDataPurgeConfirmationText() { - return [ - "GVL-Referenzdaten bereinigen?", - "", - "Betroffen: GVL-Referenzdaten der Browser-DB.", - "Nicht betroffen: Consent-Daten, Request-Beobachtungen, Analyse-Daten.", - "", - "Diese Aktion entfernt lokale GVL-Referenzdaten aus der Browser-Datenbank." - ].join("\n"); +function buildPurgeSuccessMessage(result) { + if (Number.isFinite(result.deletedCount)) { + return `Bereinigung abgeschlossen: ${result.deletedCount} Records.`; + } + + return "Bereinigung abgeschlossen."; } -function buildGvlReferenceDataPurgeSuccessMessage(result) { - const clearedCount = Object.values(result.clearedStores ?? {}).reduce( - (total, count) => total + Number(count ?? 0), - 0 - ); - - return `GVL-Referenzdaten bereinigt: ${clearedCount} Records entfernt.`; -} - -function renderGvlReferenceMaintenanceStatus(message) { - gvlReferenceMaintenanceStatus.textContent = message; +function renderProtectedRevisions(vendorListVersions) { + gvlReferenceProtectedRevisions.hidden = vendorListVersions.length === 0; + gvlReferenceProtectedRevisions.textContent = vendorListVersions.length + ? `Geschützt: ${vendorListVersions + .map((vendorListVersion) => String(vendorListVersion)) + .join(", ")}` + : ""; } diff --git a/src/gvl-explorer/gvl-explorer.js b/src/gvl-explorer/gvl-explorer.js index 3282921..c6a59e9 100644 --- a/src/gvl-explorer/gvl-explorer.js +++ b/src/gvl-explorer/gvl-explorer.js @@ -215,7 +215,7 @@ async function exportSelectedGvlRevisionEvidenceJsonFile() { } downloadGvlRevisionEvidenceJsonExport(result.export); - await markGvlRevisionEvidenceVaultCopy(result.export); + await markGvlRevisionEvidenceVaultCopy(result.export, verification); await renderGvlSnapshots(); renderGvlEvidenceTransportStatus( [ @@ -271,7 +271,10 @@ function downloadGvlRevisionEvidenceJsonExport(exportContainer) { setTimeout(() => URL.revokeObjectURL(url), 0); } -async function markGvlRevisionEvidenceVaultCopy(exportContainer) { +async function markGvlRevisionEvidenceVaultCopy( + exportContainer, + verification = null +) { const snapshotSha256 = exportContainer?.metadata?.snapshotSha256 ?? null; if (!snapshotSha256) { @@ -281,7 +284,8 @@ async function markGvlRevisionEvidenceVaultCopy(exportContainer) { const result = await browser.runtime.sendMessage({ type: "mark_gvl_revision_evidence_vault_copy", payload: { - snapshotSha256 + snapshotSha256, + verification } }); @@ -338,6 +342,11 @@ async function verifyGvlRevisionEvidenceJsonFile() { exportContainer ); + if (verification.valid) { + await markGvlRevisionEvidenceVaultCopy(exportContainer, verification); + await renderGvlSnapshots(); + } + renderGvlEvidenceTransportStatus( buildGvlRevisionEvidenceVerificationMessage(verification), verification.valid ? "success" : "error"