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();
}
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();
}
-13
Datei anzeigen
@@ -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
];
+5 -25
Datei anzeigen
@@ -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) => {
+67 -7
Datei anzeigen
@@ -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(),
+10 -9
Datei anzeigen
@@ -81,15 +81,6 @@
<strong>Analyse-Vorbereitung</strong>
<span>Datenbestände und vorbereitete Prüffelder, keine Engine.</span>
</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>
</section>
@@ -127,6 +118,16 @@
</dl>
</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>
<script src="dashboard.js"></script>
@@ -113,6 +113,10 @@ p {
background: #172033;
}
.protected-revisions {
white-space: pre-line;
}
button {
width: fit-content;
max-width: 100%;
+11 -2
Datei anzeigen
@@ -31,7 +31,7 @@
Consent-Daten, Request-Beobachtungen und Analyse-Daten bleiben
unberührt.
</p>
<button id="purge-gvl-reference-data-button" type="button">
<button id="purge-gvl-reference-data-button" type="button" disabled>
GVL-Referenzdaten bereinigen
</button>
</div>
@@ -40,7 +40,16 @@
class="segment-status"
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>
</article>
+89 -39
Datei anzeigen
@@ -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.`;
}
function buildGvlReferenceDataPurgeSuccessMessage(result) {
const clearedCount = Object.values(result.clearedStores ?? {}).reduce(
(total, count) => total + Number(count ?? 0),
0
);
return `GVL-Referenzdaten bereinigt: ${clearedCount} Records entfernt.`;
return "Bereinigung abgeschlossen.";
}
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(", ")}`
: "";
}
+12 -3
Datei anzeigen
@@ -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"