Commits vergleichen
15 Commits
990da710c1
..
master
| Autor | SHA1 | Datum | |
|---|---|---|---|
| 904787c115 | |||
| 1f10389016 | |||
| 3a174128e0 | |||
| e19eef67b2 | |||
| d68735ffc4 | |||
| 0defb37e63 | |||
| 8d923ba962 | |||
| 61a20c424c | |||
| d0dc1978e4 | |||
| b89addf362 | |||
| a3ca8019a1 | |||
| eb70f0ce81 | |||
| f8a23ba643 | |||
| 08d5a6ccc2 | |||
| 3fd40348b5 |
@@ -38,3 +38,6 @@ web-ext-artifacts/
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Local GVL evidence exports
|
||||
data/GVL-Dumps/*.json
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Background Message Map
|
||||
|
||||
Stand: aus der aktuellen Implementierung abgeleitet. Diese Datei beschreibt den beobachteten Ist-Zustand der Background-Ladefolge, Message-Routen und dauerhaften Datenänderungen.
|
||||
|
||||
## 1. Background-Lade- und Initialisierungspfad
|
||||
|
||||
Die Background-Scripts werden laut `manifest.json` in der folgenden Reihenfolge geladen. Die Dateien laufen nicht als ES-Module, sondern teilen sich den globalen Background-Kontext.
|
||||
|
||||
| Reihenfolge | Dateipfad | Aufgabe | Bereitgestellte globale Funktionen / Werte | Unmittelbar erkennbare Abhängigkeiten |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `src/core/module-registry.js` | Registriert lokale Moduldefinitionen. | `globalThis.VGCoreModuleRegistry.registerModule`, `getModules`, `getModuleById` | keine Projekt-Abhängigkeit |
|
||||
| 2 | `src/modules/vg-observe/module.js` | Definiert und registriert das VG-Observe-Modul. | `globalThis.VGObserveModule` | `VGCoreModuleRegistry` |
|
||||
| 3 | `src/background/db/db-constants.js` | Definiert DB-Name, DB-Version und Store-Namen. | `VENDORGET_DB_NAME`, `VENDORGET_DB_VERSION`, `VENDORGET_STORE_NAMES`, `VENDORGET_EVIDENCE_STORE_NAMES` | keine Projekt-Abhängigkeit |
|
||||
| 4 | `src/background/db/db-core.js` | Öffnet, migriert und löscht die IndexedDB. | `openVendorGetDb`, `closeVendorGetDb`, `deleteVendorGetDatabase`, `ensureGvlStores`, `ensureGvlRelationshipStores` | DB-Konstanten, `indexedDB` |
|
||||
| 5 | `src/core/evidence-export-json.js` | Exportiert alle Evidence-Stores als JSON-Container. | `exportVendorGetEvidenceJson` | `openVendorGetDb`, `VENDORGET_EVIDENCE_STORE_NAMES` |
|
||||
| 6 | `src/core/gvl-evidence-json.js` | Export, Verifikation, Import und Provenienzmarkierung von GVL-Evidence. | `exportVendorGetGvlEvidenceJson`, `exportVendorGetGvlRevisionEvidenceJson`, `verifyVendorGetGvlRevisionEvidenceJson`, `importVendorGetGvlRevisionEvidenceJson`, `markVendorGetGvlRevisionEvidenceVaultCopy`, `getGvlEvidenceProvenanceState`, `importVendorGetGvlEvidenceJson`, weitere GVL-Hilfsfunktionen | DB-Konstanten, `openVendorGetDb`, `sha256Hex` erst zur Laufzeit benötigt, `stableStringify`, `VendorGetGvlService` bei Verifikation |
|
||||
| 7 | `src/background/gvl/gvl-vendor-normalizer.js` | Normalisiert Vendors aus einem GVL-Snapshot. | `globalThis.normalizeGvlVendorsFromSnapshot`, `globalThis.normalizeLatestGvlSnapshotVendors` | `openVendorGetDb`, `VENDORGET_STORE_NAMES` |
|
||||
| 8 | `src/background/gvl/gvl-vendor-relationship-normalizer.js` | Normalisiert Vendor-Beziehungen zu Purposes, Features und Special Purposes/Features. | `globalThis.normalizeGvlVendorRelationshipsFromSnapshot`, `globalThis.normalizeLatestGvlVendorRelationships` | `openVendorGetDb`, `VENDORGET_STORE_NAMES` |
|
||||
| 9 | `src/background/gvl/gvl-catalog-normalizer.js` | Normalisiert GVL-Kataloge wie Purposes und Features. | `globalThis.normalizeGvlCatalogsFromSnapshot`, `globalThis.normalizeLatestGvlCatalogs` | `openVendorGetDb`, `VENDORGET_STORE_NAMES` |
|
||||
| 10 | `src/background/db/db-retention.js` | Zählt Evidence-Daten und löscht ungeschützte, nicht gesperrte Evidence-Records. | `countEvidenceRecords`, `countLockedEvidenceRecords`, `getEvidenceStoreCounts`, `purgeUnlockedEvidenceRecords` | `VENDORGET_EVIDENCE_STORE_NAMES`, `VENDORGET_STORE_NAMES`, `getGvlEvidenceProvenanceState` |
|
||||
| 11 | `src/background/db/db-record-locks.js` | Setzt oder entfernt Record-Locks auf allen Evidence-Stores. | `lockAllEvidenceRecords`, `unlockAllEvidenceRecords`, `updateAllEvidenceRecords` | `openVendorGetDb`, `VENDORGET_EVIDENCE_STORE_NAMES` |
|
||||
| 12 | `src/core/stable-serialize.js` | Stabile JSON-Serialisierung mit sortierten Objekt-Keys. | `stableStringify`, `sortObjectKeys` | keine Projekt-Abhängigkeit |
|
||||
| 13 | `src/background/utils.js` | SHA-256-Hilfsfunktion. | `sha256Hex` | `crypto.subtle`, `TextEncoder` |
|
||||
| 14 | `src/core/settings-storage.js` | Generische Storage-Helfer. | `getSettingsFromStorage`, `setSettingInStorage` | `browser.storage.local` |
|
||||
| 15 | `src/background/settings.js` | VG-Observe-Schalter für Consent-Capture und Request-Monitoring. | `getVendorGetSettings`, `setVendorGetSetting`, `isConsentCaptureEnabled`, `isRequestMonitoringEnabled` | Settings-Storage-Helfer |
|
||||
| 16 | `src/core/maintenance-guard.js` | Kurzlebige Maintenance-Session zum Suspendieren von Evidence-Schreibzugriffen. | `startEvidenceMaintenanceSession`, `refreshEvidenceMaintenanceSession`, `endEvidenceMaintenanceSession`, `isEvidenceWriteSuspended`, `getEvidenceMaintenanceStatus` | Zeitfunktionen |
|
||||
| 17 | `src/background/consent-memory.js` | Hält zuletzt beobachtete TCF-Pings und Consent-States im Speicher. | `rememberLatestTcfPing`, `getLatestTcfPing`, `rememberLatestConsentState`, `getLatestConsentStateForRequest` | keine persistente Abhängigkeit |
|
||||
| 18 | `src/background/request-fingerprint.js` | Baut stabile Fingerprint-Quellen für beobachtete Requests. | `buildObservedRequestFingerprintSource`, `buildObservedRequestFingerprint` | `URL`, `sha256Hex`, `stableStringify` |
|
||||
| 19 | `src/background/request-observer.js` | Beobachtet consentbezogene Requests über `webRequest`. | `handleObservedRequest` | Settings, Maintenance-Guard, Request-Fingerprint, Consent-Memory, `persistObservedRequest` aus `src/background.js` |
|
||||
| 20 | `src/background/gvl-service.js` | Hashing, Speicherung und Event-Erzeugung für GVL-Rohdaten und Snapshots. | `globalThis.VendorGetGvlService` mit `calculateGvlSnapshotSha256`, `calculateRawGvlSha256`, `storeGvlRawEvidenceIfNew`, `buildGvlSnapshotRecord`, `storeGvlSnapshotIfNew`, `recordGvlSnapshotEvent`, `ingestGvlSnapshot` | `stableStringify`, GVL-Provenienzfunktionen aus `src/core/gvl-evidence-json.js` |
|
||||
| 21 | `src/core/binary-utils.js` | Bit-Helfer für Base64URL- und TC-String-Decoding. | `base64UrlToBits`, `bitsToInt`, `bitsToBoolean` | `atob` |
|
||||
| 22 | `src/core/tcf-core-metadata-decoder.js` | Decodiert Core-Metadaten aus TC-Strings. | `decodeTcStringCoreMetadata`, `bitsToString` | Binary-Utils |
|
||||
| 23 | `src/background.js` | Zentraler Background-Router, Message-Handler, Consent-Persistenz, Request-Persistenz und GVL-Sync. | `handleVendorGetMessage`, Handlerfunktionen, Query-Helfer, `persistConsentState`, `persistObservedRequest` | Alle zuvor geladenen Komponenten, `browser.runtime.onMessage`, `browser.webRequest.onBeforeRequest`, `fetch` |
|
||||
|
||||
Bei Initialisierung registriert `src/background.js` `browser.runtime.onMessage` auf `handleVendorGetMessage` und `browser.webRequest.onBeforeRequest` auf `handleObservedRequest`. Der automatische GVL-Update-Pfad ist im aktuellen Code nicht aktiv gestartet; es wird lediglich geloggt: `GVL auto update disabled; use manual sync`.
|
||||
|
||||
## 2. Message-Katalog
|
||||
|
||||
| Message-Name | Zuständiger Handler | Zweck | Art des Zugriffs | Fachbereiche |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `export_gvl_evidence_json` | `handleExportGvlEvidenceJsonMessage` | Exportiert alle GVL-Evidence-Stores. | Lesen | GVL, Export |
|
||||
| `export_gvl_revision_evidence_json` | `handleExportGvlRevisionEvidenceJsonMessage` | Exportiert eine einzelne GVL-Revision inklusive Raw Evidence, Snapshot und normalisierten Records. | Lesen | GVL, Export |
|
||||
| `verify_gvl_revision_evidence_json` | `handleVerifyGvlRevisionEvidenceJsonMessage` | Prüft ein GVL-Revision-Evidence-Paket gegen Hashes, Metadaten und Konsistenzregeln. | Lesen | GVL, Import, Export |
|
||||
| `import_gvl_revision_evidence_json` | `handleImportGvlRevisionEvidenceJsonMessage` | Importiert eine verifizierte GVL-Revision aus einem Evidence-Paket. | Lesen und Schreiben | GVL, Import |
|
||||
| `mark_gvl_revision_evidence_vault_copy` | `handleMarkGvlRevisionEvidenceVaultCopyMessage` | Markiert Snapshot und Raw-GVL einer Revision als Vault-Kopie verfügbar. | Lesen und Schreiben | GVL, Export |
|
||||
| `import_gvl_evidence_json` | `handleImportGvlEvidenceJsonMessage` | Importiert einen älteren/gesamten GVL-Evidence-Exportcontainer. | Lesen und Schreiben | GVL, Import |
|
||||
| `gvl_import_json` | `handleGvlImportJsonMessage` | Importiert eine lokale GVL-JSON als Snapshot über die GVL-Service-Pipeline. | Lesen und Schreiben | GVL, Import |
|
||||
| `fetch_official_gvl` | `handleFetchOfficialGvlMessage` | Ruft die offizielle IAB-GVL ab, speichert Raw Evidence/Snapshot und normalisiert Daten. | Lesen und Schreiben | GVL |
|
||||
| `export_evidence_json` | `handleExportEvidenceJsonMessage` | Exportiert alle Evidence-Stores. | Lesen | Consent, Requests, GVL, Export |
|
||||
| `start_evidence_maintenance_session` | `startEvidenceMaintenanceSession` | Startet eine kurzlebige In-Memory-Maintenance-Session. | Schreiben | Settings, Purge |
|
||||
| `refresh_evidence_maintenance_session` | `refreshEvidenceMaintenanceSession` | Erneuert die Maintenance-Session. | Schreiben | Settings, Purge |
|
||||
| `end_evidence_maintenance_session` | `endEvidenceMaintenanceSession` | Beendet die Maintenance-Session für die angegebene Quelle. | Schreiben | Settings, Purge |
|
||||
| `get_evidence_maintenance_status` | `getEvidenceMaintenanceStatus` | Meldet, ob Evidence-Schreibzugriffe suspendiert sind. | Lesen | Settings, Dashboard |
|
||||
| `get_evidence_retention_status` | `handleGetEvidenceRetentionStatusMessage` | Zählt Evidence-Records, gesperrte Records und Store-Bestände. | Lesen | Consent, Requests, GVL, Purge, Dashboard |
|
||||
| `get_latest_gvl_update_status` | `handleGetLatestGvlUpdateStatusMessage` | Liefert den letzten bekannten GVL-Update-Status und lokale Snapshot-Infos. | Lesen | GVL, Dashboard |
|
||||
| `list_gvl_snapshots` | `handleListGvlSnapshotsMessage` | Listet aktuelle GVL-Snapshots mit Event- und Provenienzstatus. | Lesen | GVL, Dashboard |
|
||||
| `get_gvl_snapshot_summary` | `handleGetGvlSnapshotSummaryMessage` | Liefert Summary, Zählungen und Diagnostik für einen GVL-Snapshot. | Lesen | GVL, Dashboard |
|
||||
| `rebuild_gvl_snapshot_normalized_data` | `handleRebuildGvlSnapshotNormalizedDataMessage` | Baut normalisierte GVL-Daten für einen vorhandenen Snapshot neu auf. | Lesen und Schreiben | GVL |
|
||||
| `list_gvl_vendors_for_snapshot` | `handleListGvlVendorsForSnapshotMessage` | Listet normalisierte Vendoren eines Snapshots. | Lesen | GVL, Dashboard |
|
||||
| `get_gvl_vendor_detail` | `handleGetGvlVendorDetailMessage` | Liefert Vendor-Detaildaten inklusive Beziehungen, Katalogtexten, Snapshot und Raw-Evidence-Summary. | Lesen | GVL, Dashboard |
|
||||
| `get_latest_consent_state` | `handleGetLatestConsentStateMessage` | Liefert den zuletzt gesehenen Consent-State. | Lesen | Consent, Dashboard |
|
||||
| `list_recent_consent_states` | `handleListRecentConsentStatesMessage` | Listet die letzten Consent-States. | Lesen | Consent, Dashboard |
|
||||
| `list_recent_observed_requests` | `handleListRecentObservedRequestsMessage` | Listet zuletzt beobachtete Requests. | Lesen | Requests, Dashboard |
|
||||
| `purge_unlocked_evidence_records` | `handlePurgeUnlockedEvidenceRecordsMessage` | Löscht nicht gesperrte und nicht workspace-geschützte Evidence-Records. | Lesen und Schreiben | Consent, Requests, GVL, Purge |
|
||||
| `delete_all_evidence_database` | `handleDeleteAllEvidenceDatabaseMessage` | Löscht die komplette IndexedDB. | Schreiben | Consent, Requests, GVL, Purge |
|
||||
| `lock_all_evidence_records` | `lockAllEvidenceRecords` | Setzt Lock-Metadaten auf allen Evidence-Records. | Lesen und Schreiben | Consent, Requests, GVL, Purge |
|
||||
| `unlock_all_evidence_records` | `unlockAllEvidenceRecords` | Entfernt Lock-Metadaten auf allen Evidence-Records. | Lesen und Schreiben | Consent, Requests, GVL, Purge |
|
||||
| `vendorget_capture` mit `eventName: "tcf_ping"` | `handleVendorGetMessage` | Speichert den letzten TCF-Ping pro Tab nur im Background-Speicher. | Schreiben | Consent |
|
||||
| `vendorget_capture` mit `eventName: "consent_capture"` | `handleVendorGetMessage` und `persistConsentState` | Baut, fingerprintet und persistiert einen Consent-State sowie ein Consent-Event. | Lesen und Schreiben | Consent |
|
||||
|
||||
Zusätzlich existiert kein Runtime-Message-Typ für `setVendorGetSetting`; die Popup-UI ruft diese Funktion wegen gemeinsamer Script-Einbindung direkt aus dem Popup-Kontext auf.
|
||||
|
||||
## 3. Dauerhafte Datenänderungen
|
||||
|
||||
| Funktion | Betroffene IndexedDB-Stores / Storage | Zweck der Änderung | Bezug zu Evidence-Daten | Besondere Sensibilität |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `openVendorGetDb` / `ensureGvlStores` / `ensureGvlRelationshipStores` | alle IndexedDB-Stores bei Migration/Upgrade | Additive Anlage von Object Stores und Indizes. | Legt die Speicherstruktur für Evidence und GVL-Daten fest. | Sehr hoch: DB-Version, KeyPaths und Indizes bestimmen Lesbarkeit und Migration vorhandener Evidence. |
|
||||
| `deleteVendorGetDatabase` | gesamte IndexedDB `vendorget_iv` | Löscht die komplette lokale Datenbank. | Entfernt alle lokalen Evidence- und GVL-Daten. | Extrem hoch: vollständiger Datenverlust. |
|
||||
| `persistConsentState` | `consent_states`, `consent_events` | Fügt neue Consent-States hinzu oder aktualisiert bestehende Duplikate; schreibt immer ein Consent-Event. | Kern-Evidence für beobachtete Consent-Zustände. | Sehr hoch: Fingerprint, Zeitstempel, Raw-TC-Daten und Eventhistorie beeinflussen Nachvollziehbarkeit. |
|
||||
| `persistObservedRequest` | `observed_requests` | Fügt beobachtete consentbezogene Requests hinzu oder aktualisiert Wiederholungen. | Request-Evidence und Korrelation zu Consent-States. | Hoch: Fingerprint-Quelle, Seen-Zählung und Korrelation beeinflussen Rekonstruktion. |
|
||||
| `VendorGetGvlService.storeGvlRawEvidenceIfNew` | `gvl_raw_evidence` | Speichert Raw-GVL oder markiert vorhandene Raw-GVL mit Web-Provenienz. | Raw Evidence für GVL-Inhalte. | Sehr hoch: Raw Body und Hashes sind Grundlage späterer Verifikation. |
|
||||
| `VendorGetGvlService.storeGvlSnapshotIfNew` | `gvl_snapshots` | Speichert Snapshot oder ergänzt Web-Provenienz an vorhandenem Snapshot. | GVL-Snapshot-Evidence. | Sehr hoch: Snapshot-Hash, Vendorlisten-Version und Provenienz steuern Export/Purge. |
|
||||
| `VendorGetGvlService.recordGvlSnapshotEvent` | `gvl_snapshot_events` | Schreibt Ereignisse zu GVL-Ingest, Auto-Update, Fehlern oder Status. | Event-Evidence zum lokalen GVL-Verlauf. | Hoch: Ereignishistorie erklärt Entstehungskontext. |
|
||||
| `VendorGetGvlService.ingestGvlSnapshot` | `gvl_snapshots`, `gvl_snapshot_events` | Speichert Snapshot und erzeugt Event `gvl_snapshot_ingested` oder `gvl_snapshot_already_known`. | GVL-Evidence und lokale Beobachtungshistorie. | Sehr hoch: Erzeugt zusätzliche Evidence zum Ingest-Pfad. |
|
||||
| `importVendorGetGvlRevisionEvidenceJson` / `importGvlRevisionEvidenceStores` | `gvl_raw_evidence`, `gvl_snapshots`, normalisierte GVL-Stores | Importiert verifizierte GVL-Revisionsdaten; bestehende Raw/Snapshot-Records können Provenienz per `put` erhalten. | Rekonstruiert GVL-Evidence aus Vault-Paketen. | Sehr hoch: Import validiert und entscheidet zwischen Insert, Skip und Provenienzmerge. |
|
||||
| `importVendorGetGvlEvidenceJson` / `importGvlEvidenceStores` | `gvl_raw_evidence`, `gvl_snapshots`, normalisierte GVL-Stores | Importiert GVL-Evidence-Store-Arrays und überspringt vorhandene Keys. | GVL-Evidence-Import. | Hoch: Key-Ermittlung und Skip-Logik bestimmen Dublettenverhalten. |
|
||||
| `markVendorGetGvlRevisionEvidenceVaultCopy` / `markGvlRevisionEvidenceVaultCopyAvailable` / `updateGvlRevisionEvidenceRecords` | `gvl_snapshots`, `gvl_raw_evidence` | Markiert Vault-Kopie und Workspace-Löschbarkeit für Snapshot und Raw-GVL. | Verändert Evidence-Provenienz und Schutzstatus. | Sehr hoch: beeinflusst Purge-Schutz und Audit-Aussage zur Vault-Verfügbarkeit. |
|
||||
| `markGvlRevisionEvidenceWebSource` / `markGvlRevisionEvidenceProvenance` | `gvl_snapshots`, `gvl_raw_evidence` | Ergänzt Web-Provenienz an GVL-Revisionsrecords. | Verändert GVL-Provenienz. | Hoch: Provenienz beeinflusst Schutzstatus und Interpretation der Herkunft. |
|
||||
| `putGvlVendorRecords` | `gvl_vendors` | Schreibt normalisierte Vendor-Records per `put`. | Normalisierte Ableitung aus GVL-Snapshot. | Hoch: überschreibt normalisierte Sicht auf Vendor-Daten einer Revision. |
|
||||
| `putGvlVendorRelationshipRecords` | `gvl_vendor_relationships` | Schreibt normalisierte Vendor-Beziehungsrecords per `put`. | Normalisierte Ableitung aus GVL-Snapshot. | Hoch: beeinflusst Vendor-Detail- und Purpose/Feature-Nachvollziehbarkeit. |
|
||||
| `putGvlCatalogRecords` | `gvl_purposes`, `gvl_special_purposes`, `gvl_features`, `gvl_special_features` | Schreibt normalisierte Katalogrecords per `put`. | Normalisierte GVL-Katalogableitung. | Hoch: Katalogtexte und IDs sind Grundlage der Darstellung. |
|
||||
| `normalizeGvlSnapshotWithExistingPipeline` | normalisierte GVL-Stores | Führt Vendor-, Katalog- und Relationship-Normalisierung aus. | Erstellt/aktualisiert normalisierte Evidence-Ableitungen. | Hoch: gemeinsamer Einstieg für Rebuild und GVL-Fetch. |
|
||||
| `purgeUnlockedEvidenceRecords` | alle Stores in `VENDORGET_EVIDENCE_STORE_NAMES` | Löscht nicht gelockte und nicht workspace-geschützte Records per Cursor. | Entfernt lokale Evidence-Records selektiv. | Extrem hoch: Löschlogik hängt von Locks und GVL-Provenienzschutz ab. |
|
||||
| `lockAllEvidenceRecords` / `updateAllEvidenceRecords` | alle Stores in `VENDORGET_EVIDENCE_STORE_NAMES` | Setzt `bolRecordLock` und Lock-Metadaten auf allen Evidence-Records. | Schützt Evidence vor Purge. | Sehr hoch: Vollständiger Record-Durchlauf; Schutzstatus wird verändert. |
|
||||
| `unlockAllEvidenceRecords` / `updateAllEvidenceRecords` | alle Stores in `VENDORGET_EVIDENCE_STORE_NAMES` | Entfernt Lock-Metadaten und setzt Unlock-Zeit. | Macht Evidence wieder purgefähig, sofern kein anderer Schutz greift. | Sehr hoch: verändert Schutzstatus aller Evidence-Records. |
|
||||
| `storeAutoGvlCheckThrottleState` | `browser.storage.local` unter `vendorgetAutoGvlUpdateStatus` | Speichert Auto-GVL-Throttle-Status. | Kein IndexedDB-Evidence-Record, aber Status zur GVL-Automatik. | Mittel: beeinflusst Statusmeldungen und Auto-Check-Verhalten, aktuell nicht gestartet. |
|
||||
| `setSettingInStorage` / `setVendorGetSetting` | `browser.storage.local` unter `vendorgetSettings` | Speichert Consent-Capture- und Request-Monitoring-Schalter. | Steuert, ob neue Evidence geschrieben wird. | Hoch: Schalter bestimmen Beobachtungsumfang. |
|
||||
| `startEvidenceMaintenanceSession` / `refreshEvidenceMaintenanceSession` / `endEvidenceMaintenanceSession` | nur In-Memory | Setzt/erneuert/beendet temporäre Schreibsperre. | Verhindert während Maintenance neue Evidence-Schreibzugriffe. | Mittel bis hoch: nicht persistent, aber direkt wirksam auf Capture/Request-Writes. |
|
||||
|
||||
Hinweis zu Store-Abdeckung: `VENDORGET_EVIDENCE_STORE_NAMES` enthält `consent_states`, `consent_events`, `observed_requests`, `gvl_snapshots`, `gvl_snapshot_events`, `gvl_vendors`, `gvl_purposes`, `gvl_special_purposes`, `gvl_features`, `gvl_special_features`, `gvl_data_categories` und `gvl_vendor_relationships`. `gvl_raw_evidence` ist nicht in dieser allgemeinen Evidence-Store-Liste enthalten, wird aber in den GVL-spezifischen Evidence-Import/Export-Pfaden verwendet.
|
||||
|
||||
## 4. Sensible Bereiche
|
||||
|
||||
| Bereich | Betroffene Komponenten | Grund der Sensibilität | Mögliche Auswirkungen von Änderungen |
|
||||
| --- | --- | --- | --- |
|
||||
| IndexedDB-Schema und Store-Konstanten | `db-constants.js`, `db-core.js`, alle DB-Nutzer | KeyPaths, Store-Namen, Indizes und DB-Version sind Grundlage aller Evidence-Zugriffe. | Bestehende Evidence kann unlesbar, nicht migrierbar oder falsch gezählt werden. |
|
||||
| Consent-Capture-Persistenz | `background.js`, `tcf-core-metadata-decoder.js`, `stable-serialize.js`, `utils.js`, `consent-memory.js` | `stateFingerprint`, `rawTcData`, Zeitstempel und Eventtypen bilden die technische Consent-Evidence. | Duplikaterkennung, Rekonstruktion und Audit-Trail können verfälscht werden. |
|
||||
| Request-Beobachtung und Korrelation | `request-observer.js`, `request-fingerprint.js`, `consent-memory.js`, `background.js` | Requests werden dedupliziert, fingerprinted und optional mit dem letzten Consent-State korreliert. | Request-Evidence kann fehlen, zusammenfallen oder falschen Consent-Zuständen zugeordnet werden. |
|
||||
| GVL-Raw/Snapshot-Hashing | `gvl-service.js`, `gvl-evidence-json.js`, `stable-serialize.js` | Hashes sind Grundlage für Snapshot-Identität, Raw-Evidence-Verifikation und Exportpakete. | GVL-Revisionen könnten nicht mehr vergleichbar oder verifizierbar sein. |
|
||||
| GVL-Provenienz und Vault-Markierung | `gvl-evidence-json.js`, `db-retention.js`, `gvl-explorer.js` | `gvlEvidenceProvenance`, `vaultCopyAvailable` und `evidenceWorkspaceDeleteAllowed` steuern Schutzstatus und Exportstatus. | Purge kann zu viel oder zu wenig löschen; Vault-Aussagen können unzuverlässig werden. |
|
||||
| GVL-Normalisierung | `gvl-vendor-normalizer.js`, `gvl-vendor-relationship-normalizer.js`, `gvl-catalog-normalizer.js`, `background.js` | Normalisierte Stores sind abgeleitete Sicht auf eine konkrete GVL-Revision. | Vendor-Details, Katalogtexte und Zählungen können von Raw-Snapshot abweichen. |
|
||||
| Purge- und Lock-Logik | `db-retention.js`, `db-record-locks.js`, `background.js`, Popup/Data-Maintenance | Diese Pfade löschen oder schützen Evidence über viele Stores hinweg. | Datenverlust, ungewollte Schutzumgehung oder falsche Retention-Summaries. |
|
||||
| Export/Import-Pakete | `evidence-export-json.js`, `gvl-evidence-json.js`, Popup/GVL-Explorer | Exportformate bilden auditierbare Übergabecontainer. | Spätere Reimporte, Payload-Hashes und Nachvollziehbarkeit können brechen. |
|
||||
| Maintenance-Guard | `maintenance-guard.js`, `background.js`, `request-observer.js` | Temporäre Schreibsperre verhindert parallele Evidence-Writes während Wartung. | Während Wartung können Daten fehlen oder trotz Wartung geschrieben werden. |
|
||||
| Settings | `settings-storage.js`, `settings.js`, `popup.js` | Browser-Storage-Schalter steuern Consent-Capture und Request-Monitoring. | Beobachtungsumfang und Evidence-Erzeugung ändern sich ohne DB-Schemaänderung. |
|
||||
|
||||
## 5. Überwiegend lesende Bereiche
|
||||
|
||||
| Komponente | Aufgabe | Verwendete Datenquellen | Art des Zugriffs |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/core/evidence-export-json.js` | Baut einen vollständigen Evidence-Exportcontainer. | `VENDORGET_EVIDENCE_STORE_NAMES` über IndexedDB-Cursor | Lesen |
|
||||
| `exportVendorGetGvlEvidenceJson` / `readGvlEvidenceStores` in `src/core/gvl-evidence-json.js` | Exportiert GVL-Evidence-Stores. | GVL-Evidence-Stores über IndexedDB-Cursor | Lesen |
|
||||
| `exportVendorGetGvlRevisionEvidenceJson` in `src/core/gvl-evidence-json.js` | Exportiert eine einzelne GVL-Revision. | `gvl_snapshots`, `gvl_raw_evidence`, normalisierte GVL-Stores | Lesen |
|
||||
| `verifyVendorGetGvlRevisionEvidenceJson` in `src/core/gvl-evidence-json.js` | Prüft einen übergebenen Exportcontainer. | Import-/Exportobjekt im Speicher, Hashfunktionen | Lesen |
|
||||
| `handleGetEvidenceRetentionStatusMessage` und Zählfunktionen in `db-retention.js` | Liefert Gesamt-, Lock- und Store-Zählungen. | Evidence-Stores | Lesen |
|
||||
| `handleGetLatestGvlUpdateStatusMessage` | Liefert lokalen GVL-Status und letzten In-Memory-Update-Status. | `gvl_snapshots`, Browser-Storage-Throttle, In-Memory-Status | Lesen |
|
||||
| `handleListGvlSnapshotsMessage` | Listet GVL-Snapshots mit Event- und Provenienzstatus. | `gvl_snapshots`, `gvl_snapshot_events` | Lesen |
|
||||
| `handleGetGvlSnapshotSummaryMessage` | Baut Snapshot-Summary mit normalisierten Zählungen. | `gvl_snapshots`, `gvl_snapshot_events`, normalisierte GVL-Stores | Lesen |
|
||||
| `handleListGvlVendorsForSnapshotMessage` | Listet Vendoren einer Revision. | `gvl_vendors`, `gvl_snapshots` | Lesen |
|
||||
| `handleGetGvlVendorDetailMessage` | Baut Vendor-Detailansicht. | `gvl_vendors`, `gvl_vendor_relationships`, Katalogstores, `gvl_snapshots`, `gvl_raw_evidence` | Lesen |
|
||||
| `handleGetLatestConsentStateMessage` | Liefert neuesten Consent-State. | `consent_states` über `lastSeenAt` | Lesen |
|
||||
| `handleListRecentConsentStatesMessage` | Listet aktuelle Consent-States. | `consent_states` über `lastSeenAt` | Lesen |
|
||||
| `handleListRecentObservedRequestsMessage` | Listet aktuelle beobachtete Requests. | `observed_requests` über `lastSeenAt` | Lesen |
|
||||
| `src/dashboard/dashboard.js` | Zeigt Evidence-Bestand und GVL-Status. | Runtime-Messages `get_evidence_retention_status`, `get_latest_gvl_update_status` | Lesen |
|
||||
| `src/analysis-dashboard/analysis-dashboard.js` | Zeigt zusammenfassende Bestände und Status. | Runtime-Messages `get_evidence_retention_status`, `get_latest_gvl_update_status` | Lesen |
|
||||
| `src/consent-explorer/consent-explorer.js` | Stellt Consent-States dar. | Runtime-Message `list_recent_consent_states` | Lesen |
|
||||
| `src/request-explorer/request-explorer.js` | Stellt beobachtete Requests dar. | Runtime-Message `list_recent_observed_requests` | Lesen |
|
||||
| `src/gvl-explorer/gvl-explorer.js` | Stellt GVL-Snapshots, Vendoren und Revisionsexporte dar; enthält zusätzlich schreibende Aktionen für Fetch, Import, Rebuild und Vault-Markierung. | GVL-Runtime-Messages | Überwiegend Lesen, einzelne Schreiben |
|
||||
|
||||
## 6. Audit-Hinweis
|
||||
|
||||
Besonders kritisch sind DB-Schema, Consent-Persistenz, Request-Fingerprints, GVL-Hashing/Provenienz, Import/Export, Purge und Record-Locks.
|
||||
|
||||
Überwiegend lesend arbeiten Export-, Dashboard-, Explorer- und Summary-Pfade, solange sie keine Import-, Fetch-, Rebuild-, Markierungs- oder Purge-Aktion auslösen.
|
||||
|
||||
Bei späteren Änderungen verdienen `src/background.js`, `src/background/db/db-core.js`, `src/background/db/db-retention.js`, `src/background/db/db-record-locks.js`, `src/core/gvl-evidence-json.js` und `src/background/gvl-service.js` besondere Aufmerksamkeit.
|
||||
|
||||
Änderungen an Settings und Maintenance-Guard sind ebenfalls auditrelevant, weil sie steuern, ob Evidence überhaupt geschrieben wird.
|
||||
@@ -1,5 +1,7 @@
|
||||
# VG-Environment Codex Workflow
|
||||
|
||||
Vor Beginn fachlicher Arbeiten ist `docs/architecture/project-philosophy.md` als maßgebliche Beschreibung des Projektkerns zu berücksichtigen.
|
||||
|
||||
## Grundprinzip
|
||||
|
||||
VG-Environment wird schrittweise modularisiert.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# VG-Environment – Wesen und Kern des Projekts
|
||||
|
||||
VG-Environment ist kein GVL-Explorer.
|
||||
|
||||
VG-Environment ist kein Dashboard.
|
||||
|
||||
VG-Environment ist keine Analyse-Engine.
|
||||
|
||||
VG-Environment ist ein lokales, evidenzorientiertes Rekonstruktionswerkzeug zur Untersuchung der technisch beobachtbaren Wirkungen von Consent-Entscheidungen.
|
||||
|
||||
Die zentrale Frage des Projekts lautet:
|
||||
|
||||
> Was ist tatsächlich geschehen, nachdem ich dieser konkreten Entscheidung zugestimmt habe?
|
||||
|
||||
Zur Beantwortung dieser Frage trennt VG-Environment bewusst drei unterschiedliche Wirklichkeitsebenen.
|
||||
|
||||
## 1. Die Provider-Erzählung
|
||||
|
||||
Ein CMP präsentiert dem Nutzer eine Erklärung.
|
||||
|
||||
Der Nutzer trifft eine Entscheidung.
|
||||
|
||||
Aus dieser Entscheidung entsteht ein tcString.
|
||||
|
||||
Der tcString dokumentiert nicht die technische Realität. Er dokumentiert die Behauptung des Systems darüber, welche Zustimmungen, Ablehnungen und Berechtigungen vorliegen sollen.
|
||||
|
||||
Er ist die digitale Selbstbeschreibung des Consent-Vorgangs.
|
||||
|
||||
VG-Environment behandelt diese Beschreibung als Evidenzobjekt – nicht als Wahrheit.
|
||||
|
||||
## 2. Die technische Realität
|
||||
|
||||
Unabhängig von jeder Erklärung werden technische Folgehandlungen beobachtet.
|
||||
|
||||
Requests finden statt oder finden nicht statt.
|
||||
|
||||
Empfänger werden kontaktiert oder werden nicht kontaktiert.
|
||||
|
||||
Parameter werden übertragen oder werden nicht übertragen.
|
||||
|
||||
Diese Beobachtungen stellen die stärkste Form der Primärevidence des Projekts dar.
|
||||
|
||||
Sie benötigen keine GVL und keine Interpretation, um zu existieren.
|
||||
|
||||
Sie sind die dokumentierte technische Realität.
|
||||
|
||||
## 3. Das Regelwerk
|
||||
|
||||
Die GVL beschreibt nicht die technische Realität.
|
||||
|
||||
Sie erklärt auch nicht die Requests.
|
||||
|
||||
Sie beschreibt vielmehr, wie die Behauptungen des Providers innerhalb des TCF-Regelwerks einzuordnen gewesen wären.
|
||||
|
||||
Sie beantwortet Fragen wie:
|
||||
|
||||
- Welche Zwecke hatte ein Vendor angegeben?
|
||||
- Welche Eigenschaften waren einem Vendor zugeordnet?
|
||||
- Welche Beziehungen waren innerhalb des Regelwerks vorgesehen?
|
||||
|
||||
Die GVL ist daher Referenz- und Analysematerial.
|
||||
|
||||
Sie ist ein historisches Wörterbuch zur Bewertung der Provider-Erzählung.
|
||||
|
||||
Sie ist nicht Bestandteil des beobachteten Vorgangs selbst.
|
||||
|
||||
## Die eigentliche Rekonstruktion
|
||||
|
||||
VG-Environment bringt diese drei Ebenen nicht durcheinander.
|
||||
|
||||
Es hält sie bewusst getrennt.
|
||||
|
||||
Erst aus ihrer Gegenüberstellung entstehen die entscheidenden Fragen:
|
||||
|
||||
- Was wurde behauptet?
|
||||
- Was ist tatsächlich geschehen?
|
||||
- Entspricht die Behauptung dem eigenen Regelwerk?
|
||||
|
||||
Die Antworten darauf entstehen nicht durch versteckte Bewertungen, sondern durch nachvollziehbare, auditierbare Evidence-Ketten.
|
||||
|
||||
## Das Leitprinzip
|
||||
|
||||
VG-Environment entscheidet nicht, was richtig oder falsch ist.
|
||||
|
||||
VG-Environment dokumentiert.
|
||||
|
||||
Es rekonstruiert.
|
||||
|
||||
Es macht technische Realität sichtbar.
|
||||
|
||||
Es zeigt Herkunft, Zusammenhänge und Widersprüche transparent auf.
|
||||
|
||||
Die Bewertung bleibt dem Nutzer, Forschenden, Journalisten, Aufsichtsbehörden oder Gerichten überlassen.
|
||||
|
||||
## Der Kern des Projekts
|
||||
|
||||
> VG-Environment ist ein Werkzeug zur Rekonstruktion technischer Realität im Spannungsfeld zwischen Provider-Erzählung, beobachtbarer Handlung und Regelwerk.
|
||||
|
||||
Oder kürzer:
|
||||
|
||||
> Es zeigt nicht, was hätte geschehen sollen.
|
||||
>
|
||||
> Es zeigt, was behauptet wurde, was tatsächlich geschah und auf welcher Grundlage beides beurteilt werden kann.
|
||||
@@ -1,5 +1,8 @@
|
||||
# VG-Environment — Project Principles
|
||||
|
||||
Hinweis:
|
||||
Die grundlegende Zieldefinition und das Erkenntnisinteresse von VG-Environment sind in `docs/architecture/project-philosophy.md` beschrieben und bei allen Architektur-, Implementierungs- und Analyseentscheidungen vorrangig zu berücksichtigen.
|
||||
|
||||
## Core purpose
|
||||
|
||||
VG-Environment is a local evidence and runtime reconstruction environment for consent-related browser activity.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"src/background/db/db-constants.js",
|
||||
"src/background/db/db-core.js",
|
||||
"src/core/evidence-export-json.js",
|
||||
"src/core/gvl-evidence-json.js",
|
||||
"src/background/gvl/gvl-vendor-normalizer.js",
|
||||
"src/background/gvl/gvl-vendor-relationship-normalizer.js",
|
||||
"src/background/gvl/gvl-catalog-normalizer.js",
|
||||
@@ -39,6 +40,7 @@
|
||||
"src/background/gvl-service.js",
|
||||
"src/core/binary-utils.js",
|
||||
"src/core/tcf-core-metadata-decoder.js",
|
||||
"src/core/consent-diff.js",
|
||||
"src/background.js"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -3,19 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VG-Observe Analyse-Dashboard</title>
|
||||
<title>VG-Observe Analyse-Vorbereitung</title>
|
||||
<link rel="stylesheet" href="analysis-dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="analysis-dashboard">
|
||||
<header class="analysis-header">
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||
<h1>Analyse-Dashboard</h1>
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||
<h1>Analyse-Vorbereitung</h1>
|
||||
<p>
|
||||
Diese Ansicht bereitet technische Prüfungen zwischen
|
||||
Consent-Zuständen, Vendorlisten und beobachteten Requests vor.
|
||||
Aktuell werden nur vorhandene Datenbestände und vorbereitete
|
||||
Analysebereiche angezeigt.
|
||||
Diese Ansicht ist noch keine Analyse-Engine. Sie zeigt vorhandene
|
||||
Datenbestände und vorbereitet strukturierte Prüffelder für spätere
|
||||
Auswertung, ohne Bewertung oder Zuordnung zu berechnen.
|
||||
</p>
|
||||
<div id="analysis-status" class="analysis-status" aria-live="polite">
|
||||
Lade Datenbestände
|
||||
@@ -34,11 +33,11 @@
|
||||
<dd id="summary-observed-requests">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Lokal gespeicherte Vendorlisten</dt>
|
||||
<dt>Lokal gespeicherte GVL-Referenzen</dt>
|
||||
<dd id="summary-gvl-snapshots">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Lokal aktuelle Vendorlisten-Version</dt>
|
||||
<dt>Lokal aktuelle GVL-Referenzversion</dt>
|
||||
<dd id="summary-current-gvl-version">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
@@ -49,23 +48,23 @@
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="areas-title">
|
||||
<h2 id="areas-title">Vorbereitete Analysebereiche</h2>
|
||||
<h2 id="areas-title">Vorbereitete Prüffelder</h2>
|
||||
<div class="area-grid">
|
||||
<article>
|
||||
<h3>Consent ↔ Vendorliste</h3>
|
||||
<p>Analyse noch nicht ausgeführt.</p>
|
||||
<p>Vorbereitung vorhanden, keine Analyse ausgeführt.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Consent ↔ beobachtete Requests</h3>
|
||||
<p>Analyse noch nicht ausgeführt.</p>
|
||||
<p>Vorbereitung vorhanden, keine Analyse ausgeführt.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Request-Hosts ↔ bekannte Vendoren</h3>
|
||||
<p>Keine erkennbare Zuordnung berechnet. Analyse noch nicht ausgeführt.</p>
|
||||
<p>Keine Zuordnung berechnet. Vorbereitung vorhanden, keine Analyse ausgeführt.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Potenziell erklärungsbedürftige technische Diskrepanzen</h3>
|
||||
<p>Keine Bewertung vorgenommen. Analyse noch nicht ausgeführt.</p>
|
||||
<p>Keine Bewertung vorgenommen. Vorbereitung vorhanden, keine Analyse ausgeführt.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1874
-25
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -29,10 +29,13 @@ function getEvidenceStoreCounts(db) {
|
||||
});
|
||||
}
|
||||
|
||||
function purgeUnlockedEvidenceRecords(db) {
|
||||
async function purgeUnlockedEvidenceRecords(db) {
|
||||
const gvlWorkspaceProtection = await buildGvlWorkspaceProtectionIndex(db);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let deletedCount = 0;
|
||||
let keptLockedCount = 0;
|
||||
let keptGvlWorkspaceProtectedCount = 0;
|
||||
const tx = db.transaction(VENDORGET_EVIDENCE_STORE_NAMES, "readwrite");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
@@ -41,7 +44,12 @@ function purgeUnlockedEvidenceRecords(db) {
|
||||
resolve({
|
||||
success: true,
|
||||
deletedCount,
|
||||
keptLockedCount
|
||||
keptLockedCount,
|
||||
keptGvlWorkspaceProtectedCount,
|
||||
gvlWorkspaceProtectionNotice:
|
||||
keptGvlWorkspaceProtectedCount > 0
|
||||
? "Diese GVL-Evidence wurde noch nicht in den Vault exportiert."
|
||||
: null
|
||||
});
|
||||
};
|
||||
|
||||
@@ -61,6 +69,18 @@ function purgeUnlockedEvidenceRecords(db) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isGvlWorkspaceProtectedRecord(
|
||||
storeName,
|
||||
cursor.value,
|
||||
gvlWorkspaceProtection
|
||||
)
|
||||
) {
|
||||
keptGvlWorkspaceProtectedCount += 1;
|
||||
cursor.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
deletedCount += 1;
|
||||
cursor.delete();
|
||||
cursor.continue();
|
||||
@@ -69,6 +89,86 @@ function purgeUnlockedEvidenceRecords(db) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
const protectedSnapshotSha256 = new Set();
|
||||
const protectedRawGvlSha256 = new Set();
|
||||
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||
const cursorRequest = tx
|
||||
.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots)
|
||||
.openCursor();
|
||||
|
||||
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||
cursorRequest.onsuccess = () => {
|
||||
const cursor = cursorRequest.result;
|
||||
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = cursor.value;
|
||||
const provenanceState = getGvlEvidenceProvenanceState(snapshot);
|
||||
|
||||
if (provenanceState.workspaceDeleteProtected) {
|
||||
if (snapshot.sha256) {
|
||||
protectedSnapshotSha256.add(snapshot.sha256);
|
||||
}
|
||||
|
||||
if (snapshot.rawGvlSha256) {
|
||||
protectedRawGvlSha256.add(snapshot.rawGvlSha256);
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue();
|
||||
};
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => {
|
||||
resolve({
|
||||
protectedSnapshotSha256,
|
||||
protectedRawGvlSha256
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function isGvlWorkspaceProtectedRecord(storeName, record, protectionIndex) {
|
||||
if (!record || typeof record !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storeName === VENDORGET_STORE_NAMES.gvlSnapshots) {
|
||||
return protectionIndex.protectedSnapshotSha256.has(record.sha256);
|
||||
}
|
||||
|
||||
if (storeName === VENDORGET_STORE_NAMES.gvlRawEvidence) {
|
||||
return protectionIndex.protectedRawGvlSha256.has(record.rawGvlSha256);
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
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
|
||||
].includes(storeName)
|
||||
) {
|
||||
return protectionIndex.protectedSnapshotSha256.has(record.snapshotSha256);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function countRecordsInStores(db, storeNames) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let totalCount = 0;
|
||||
|
||||
@@ -23,25 +23,32 @@ function storeGvlRawEvidenceIfNew(db, rawEvidence) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(["gvl_raw_evidence"], "readwrite");
|
||||
const rawEvidenceStore = tx.objectStore("gvl_raw_evidence");
|
||||
const getRequest = rawEvidenceStore.get(rawEvidence.rawGvlSha256);
|
||||
const evidenceRecord = annotateGvlEvidenceRecordProvenance(
|
||||
rawEvidence,
|
||||
"web"
|
||||
);
|
||||
const getRequest = rawEvidenceStore.get(evidenceRecord.rawGvlSha256);
|
||||
let result = null;
|
||||
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
if (getRequest.result) {
|
||||
rawEvidenceStore.put(
|
||||
annotateGvlEvidenceRecordProvenance(getRequest.result, "web")
|
||||
);
|
||||
result = {
|
||||
stored: false,
|
||||
rawGvlSha256: rawEvidence.rawGvlSha256
|
||||
rawGvlSha256: evidenceRecord.rawGvlSha256
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
rawEvidenceStore.add(rawEvidence);
|
||||
rawEvidenceStore.add(evidenceRecord);
|
||||
|
||||
result = {
|
||||
stored: true,
|
||||
rawGvlSha256: rawEvidence.rawGvlSha256
|
||||
rawGvlSha256: evidenceRecord.rawGvlSha256
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,7 +65,7 @@ async function buildGvlSnapshotRecord(
|
||||
) {
|
||||
const gvlJson = normalizeGvlSnapshotValueForMetadata(rawJson);
|
||||
|
||||
return {
|
||||
return annotateGvlEvidenceRecordProvenance({
|
||||
sha256: await calculateGvlSnapshotSha256(rawJson),
|
||||
rawGvlSha256: rawGvlSha256 ?? null,
|
||||
vendorListVersion: gvlJson?.vendorListVersion ?? null,
|
||||
@@ -72,7 +79,7 @@ async function buildGvlSnapshotRecord(
|
||||
// Existing GVL snapshots already use createdAt as the local mirror timestamp;
|
||||
// keep that field instead of duplicating it as recordedAt.
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
}, "web");
|
||||
}
|
||||
|
||||
function storeGvlSnapshotIfNew(db, snapshot) {
|
||||
@@ -86,6 +93,9 @@ function storeGvlSnapshotIfNew(db, snapshot) {
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
if (getRequest.result) {
|
||||
snapshotsStore.put(
|
||||
annotateGvlEvidenceRecordProvenance(getRequest.result, "web")
|
||||
);
|
||||
result = {
|
||||
stored: false,
|
||||
sha256: snapshot.sha256,
|
||||
|
||||
@@ -27,6 +27,7 @@ body {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -49,6 +50,11 @@ h3 {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 760px;
|
||||
font-size: 13px;
|
||||
@@ -139,6 +145,165 @@ th {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.finding-overview {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.finding-note {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.finding-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
background: #334155;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.finding-list div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 220px) 1fr;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.finding-list dt {
|
||||
color: #cbd5e1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.finding-list dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #e5edf5;
|
||||
}
|
||||
|
||||
.consent-diff {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.compact-definition-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.compact-definition-list div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(150px, 220px) 1fr;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.compact-definition-list dt {
|
||||
color: #cbd5e1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.compact-definition-list dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.diff-note {
|
||||
max-width: none;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.consent-diff-primary {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.consent-diff-findings {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.consent-diff-findings li {
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
color: #e5edf5;
|
||||
background: #1f2937;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.consent-diff-finding-details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.consent-diff-finding-body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.consent-change-categories,
|
||||
.consent-change-category-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.consent-change-category {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.consent-change-category summary,
|
||||
.consent-change-collapsed-list summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.consent-change-list-block {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inline-id-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.inline-id-list li {
|
||||
padding: 3px 6px;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 3px;
|
||||
background: #0f172a;
|
||||
font-size: 12px;
|
||||
color: #e5edf5;
|
||||
}
|
||||
|
||||
.inspector-table th,
|
||||
.inspector-table td {
|
||||
vertical-align: top;
|
||||
@@ -227,4 +392,12 @@ th {
|
||||
.inspector-table .inspector-explanation {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.finding-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.finding-list div {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VG-Observe Consent-Explorer</title>
|
||||
<title>VG-Observe Consent untersuchen</title>
|
||||
<link rel="stylesheet" href="consent-explorer.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="explorer">
|
||||
<header class="explorer-header">
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||
<h1>Dokumentierte Consent-Zustände</h1>
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||
<h1>Consent untersuchen</h1>
|
||||
<p class="section-help">
|
||||
Diese Ansicht zeigt gespeicherte Consent-Zustände aus der lokalen
|
||||
Beobachtungsdatenbank. Jeder Eintrag ist ein dokumentierter Zustand,
|
||||
den VG-Observe während der Browser-Laufzeit beobachtet hat.
|
||||
Consent ist das zentrale Untersuchungsobjekt von VG-Observe. Diese
|
||||
Ansicht zeigt lokal beobachtete Consent-Zustände und ihre technischen
|
||||
Belege, ohne daraus eine Bewertung abzuleiten.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="panel" aria-labelledby="documented-consent-title">
|
||||
<h2 id="documented-consent-title">Historische Consent-Zustände</h2>
|
||||
<h2 id="documented-consent-title">Lokal beobachtete Consent-Zustände</h2>
|
||||
<p id="documented-consent-empty" class="empty-state" hidden>
|
||||
Keine dokumentierten Consent-Zustände vorhanden.
|
||||
</p>
|
||||
@@ -47,6 +47,7 @@
|
||||
<section class="consent-detail" aria-labelledby="consent-detail-title">
|
||||
<h2 id="consent-detail-title">Ausgewählter Consent-Zustand</h2>
|
||||
<div id="consent-detail-observation"></div>
|
||||
<div id="consent-detail-diff"></div>
|
||||
<div id="consent-detail-basics"></div>
|
||||
<div id="consent-detail-summary"></div>
|
||||
<div id="consent-detail-publisher"></div>
|
||||
|
||||
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -1,9 +1,87 @@
|
||||
console.log("VendorGet content listener loaded:", window.location.href);
|
||||
|
||||
let latestObservedPreConsentCapture = null;
|
||||
let latestObservedConsentCompleted = false;
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message?.type !== "probe_tcf_state") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return probeTcfState();
|
||||
});
|
||||
|
||||
function probeTcfState() {
|
||||
if (latestObservedConsentCompleted) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
capture: null,
|
||||
source: "content_completed"
|
||||
});
|
||||
}
|
||||
|
||||
if (latestObservedPreConsentCapture && !latestObservedConsentCompleted) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
capture: latestObservedPreConsentCapture,
|
||||
source: "content_memory"
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const requestId = [
|
||||
"tcf-probe",
|
||||
Date.now().toString(36),
|
||||
Math.random().toString(36).slice(2)
|
||||
].join("-");
|
||||
const timeoutId = setTimeout(() => {
|
||||
window.removeEventListener("VendorGetTcfProbeResponse", handleResponse);
|
||||
resolve({
|
||||
success: false,
|
||||
capture: null,
|
||||
error: "tcf_probe_timeout"
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
function handleResponse(event) {
|
||||
if (event?.detail?.requestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("VendorGetTcfProbeResponse", handleResponse);
|
||||
resolve({
|
||||
success: event.detail.success === true,
|
||||
capture: event.detail.capture ?? null,
|
||||
source: "tcf_get_tc_data"
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("VendorGetTcfProbeResponse", handleResponse);
|
||||
window.dispatchEvent(new CustomEvent("VendorGetTcfProbeRequest", {
|
||||
detail: {
|
||||
requestId
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("VendorGetFromPage", async (event) => {
|
||||
|
||||
console.log("VendorGet message from page:", event.detail);
|
||||
|
||||
if (
|
||||
event.detail?.eventName === "tcf_pre_consent_event" &&
|
||||
!latestObservedConsentCompleted
|
||||
) {
|
||||
latestObservedPreConsentCapture = event.detail.payload ?? null;
|
||||
}
|
||||
|
||||
if (event.detail?.eventName === "consent_capture") {
|
||||
latestObservedConsentCompleted = true;
|
||||
latestObservedPreConsentCapture = null;
|
||||
}
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: "vendorget_capture",
|
||||
payload: event.detail
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
"use strict";
|
||||
|
||||
var CONSENT_DIFF_RULE_ID = "consent_diff";
|
||||
var CONSENT_DIFF_RULE_VERSION = 1;
|
||||
|
||||
const CONSENT_DIFF_SCALAR_AXES = [
|
||||
"tcString",
|
||||
"addtlConsent",
|
||||
"vendorListVersion",
|
||||
"cmpId",
|
||||
"cmpVersion",
|
||||
"tcfPolicyVersion",
|
||||
"gdprApplies",
|
||||
"cmpStatus"
|
||||
];
|
||||
|
||||
const CONSENT_DIFF_OBJECT_AXES = [
|
||||
"purpose.consents",
|
||||
"vendor.consents",
|
||||
"specialFeatureOptins"
|
||||
];
|
||||
|
||||
function buildConsentDiff({
|
||||
captureSessionId,
|
||||
providerAnnouncement,
|
||||
consentExecution,
|
||||
derivedAt
|
||||
}) {
|
||||
const providerRaw = cloneConsentDiffValue(
|
||||
providerAnnouncement?.rawTcData ?? null
|
||||
);
|
||||
const executionRaw = cloneConsentDiffValue(consentExecution?.rawTcData ?? null);
|
||||
const providerWork = buildConsentDiffWorkCopy(
|
||||
providerAnnouncement,
|
||||
providerRaw
|
||||
);
|
||||
const executionWork = buildConsentDiffWorkCopy(consentExecution, executionRaw);
|
||||
const axes = {};
|
||||
|
||||
CONSENT_DIFF_SCALAR_AXES.forEach((axis) => {
|
||||
axes[axis] = compareConsentDiffScalarAxis(
|
||||
axis,
|
||||
providerWork,
|
||||
executionWork
|
||||
);
|
||||
});
|
||||
|
||||
CONSENT_DIFF_OBJECT_AXES.forEach((axis) => {
|
||||
axes[axis] = compareConsentDiffObjectAxis(
|
||||
axis,
|
||||
providerWork,
|
||||
executionWork
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
ruleId: CONSENT_DIFF_RULE_ID,
|
||||
ruleVersion: CONSENT_DIFF_RULE_VERSION,
|
||||
derivedAt,
|
||||
captureSessionId,
|
||||
sources: {
|
||||
providerAnnouncement: buildConsentDiffSourceReference(
|
||||
providerAnnouncement
|
||||
),
|
||||
consentExecution: buildConsentDiffSourceReference(consentExecution)
|
||||
},
|
||||
sourceSelection: {
|
||||
providerAnnouncement:
|
||||
"first_provider_announcement_for_capture_session_available_in_memory",
|
||||
consentExecution: "current_useractioncomplete_execution_event"
|
||||
},
|
||||
comparisonAxes: [
|
||||
...CONSENT_DIFF_SCALAR_AXES,
|
||||
...CONSENT_DIFF_OBJECT_AXES
|
||||
],
|
||||
axes,
|
||||
notes: buildConsentDiffNotes(providerAnnouncement, consentExecution)
|
||||
};
|
||||
}
|
||||
|
||||
function buildConsentDiffWorkCopy(eventRecord, rawTcData) {
|
||||
return {
|
||||
eventRecord: cloneConsentDiffValue(eventRecord ?? null),
|
||||
rawTcData: rawTcData ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function compareConsentDiffScalarAxis(axis, providerWork, executionWork) {
|
||||
const providerValue = readConsentDiffAxisValue(providerWork, axis);
|
||||
const executionValue = readConsentDiffAxisValue(executionWork, axis);
|
||||
|
||||
return {
|
||||
kind: "scalar",
|
||||
status: getConsentDiffScalarStatus(providerValue, executionValue),
|
||||
provider: buildConsentDiffAxisSide(providerValue),
|
||||
execution: buildConsentDiffAxisSide(executionValue)
|
||||
};
|
||||
}
|
||||
|
||||
function compareConsentDiffObjectAxis(axis, providerWork, executionWork) {
|
||||
const providerValue = readConsentDiffAxisValue(providerWork, axis);
|
||||
const executionValue = readConsentDiffAxisValue(executionWork, axis);
|
||||
const providerObject = isConsentDiffPlainObject(providerValue.value)
|
||||
? providerValue.value
|
||||
: null;
|
||||
const executionObject = isConsentDiffPlainObject(executionValue.value)
|
||||
? executionValue.value
|
||||
: null;
|
||||
const providerKeys = providerObject ? Object.keys(providerObject).sort() : [];
|
||||
const executionKeys = executionObject ? Object.keys(executionObject).sort() : [];
|
||||
const allKeys = Array.from(new Set([...providerKeys, ...executionKeys])).sort();
|
||||
const added = [];
|
||||
const removed = [];
|
||||
const changed = [];
|
||||
const unchanged = [];
|
||||
|
||||
allKeys.forEach((key) => {
|
||||
const inProvider = providerObject
|
||||
? Object.prototype.hasOwnProperty.call(providerObject, key)
|
||||
: false;
|
||||
const inExecution = executionObject
|
||||
? Object.prototype.hasOwnProperty.call(executionObject, key)
|
||||
: false;
|
||||
|
||||
if (inProvider && !inExecution) {
|
||||
removed.push({
|
||||
key,
|
||||
providerValue: cloneConsentDiffValue(providerObject[key])
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inProvider && inExecution) {
|
||||
added.push({
|
||||
key,
|
||||
executionValue: cloneConsentDiffValue(executionObject[key])
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
stableConsentDiffValue(providerObject[key]) ===
|
||||
stableConsentDiffValue(executionObject[key])
|
||||
) {
|
||||
unchanged.push(key);
|
||||
return;
|
||||
}
|
||||
|
||||
changed.push({
|
||||
key,
|
||||
providerValue: cloneConsentDiffValue(providerObject[key]),
|
||||
executionValue: cloneConsentDiffValue(executionObject[key])
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: "object",
|
||||
status: getConsentDiffObjectStatus(
|
||||
providerValue,
|
||||
executionValue,
|
||||
added,
|
||||
removed,
|
||||
changed
|
||||
),
|
||||
provider: {
|
||||
present: providerValue.present,
|
||||
sourcePath: providerValue.sourcePath,
|
||||
keys: providerKeys
|
||||
},
|
||||
execution: {
|
||||
present: executionValue.present,
|
||||
sourcePath: executionValue.sourcePath,
|
||||
keys: executionKeys
|
||||
},
|
||||
addedKeys: added,
|
||||
removedKeys: removed,
|
||||
changedKeys: changed,
|
||||
unchangedKeys: unchanged,
|
||||
counts: {
|
||||
providerKeys: providerKeys.length,
|
||||
executionKeys: executionKeys.length,
|
||||
added: added.length,
|
||||
removed: removed.length,
|
||||
changed: changed.length,
|
||||
unchanged: unchanged.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function readConsentDiffAxisValue(workCopy, axis) {
|
||||
const rawValue = readConsentDiffPath(workCopy.rawTcData, axis);
|
||||
|
||||
if (rawValue.present) {
|
||||
return {
|
||||
...rawValue,
|
||||
sourcePath: `rawTcData.${axis}`
|
||||
};
|
||||
}
|
||||
|
||||
const eventValue = readConsentDiffPath(workCopy.eventRecord, axis);
|
||||
|
||||
if (eventValue.present) {
|
||||
return {
|
||||
...eventValue,
|
||||
sourcePath: axis
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
present: false,
|
||||
value: undefined,
|
||||
sourcePath: null
|
||||
};
|
||||
}
|
||||
|
||||
function readConsentDiffPath(value, path) {
|
||||
if (value === null || value === undefined) {
|
||||
return {
|
||||
present: false,
|
||||
value: undefined
|
||||
};
|
||||
}
|
||||
|
||||
const parts = path.split(".");
|
||||
let current = value;
|
||||
|
||||
for (const part of parts) {
|
||||
if (
|
||||
current === null ||
|
||||
current === undefined ||
|
||||
!Object.prototype.hasOwnProperty.call(Object(current), part)
|
||||
) {
|
||||
return {
|
||||
present: false,
|
||||
value: undefined
|
||||
};
|
||||
}
|
||||
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
return {
|
||||
present: true,
|
||||
value: cloneConsentDiffValue(current)
|
||||
};
|
||||
}
|
||||
|
||||
function getConsentDiffScalarStatus(providerValue, executionValue) {
|
||||
if (!providerValue.present && !executionValue.present) {
|
||||
return "absent";
|
||||
}
|
||||
|
||||
if (providerValue.present && !executionValue.present) {
|
||||
return "removed";
|
||||
}
|
||||
|
||||
if (!providerValue.present && executionValue.present) {
|
||||
return "added";
|
||||
}
|
||||
|
||||
return stableConsentDiffValue(providerValue.value) ===
|
||||
stableConsentDiffValue(executionValue.value)
|
||||
? "unchanged"
|
||||
: "changed";
|
||||
}
|
||||
|
||||
function getConsentDiffObjectStatus(
|
||||
providerValue,
|
||||
executionValue,
|
||||
added,
|
||||
removed,
|
||||
changed
|
||||
) {
|
||||
if (!providerValue.present && !executionValue.present) {
|
||||
return "absent";
|
||||
}
|
||||
|
||||
if (providerValue.present && !executionValue.present) {
|
||||
return "removed";
|
||||
}
|
||||
|
||||
if (!providerValue.present && executionValue.present) {
|
||||
return "added";
|
||||
}
|
||||
|
||||
return added.length === 0 && removed.length === 0 && changed.length === 0
|
||||
? "unchanged"
|
||||
: "changed";
|
||||
}
|
||||
|
||||
function buildConsentDiffAxisSide(axisValue) {
|
||||
return {
|
||||
present: axisValue.present,
|
||||
sourcePath: axisValue.sourcePath,
|
||||
value: axisValue.present ? cloneConsentDiffValue(axisValue.value) : null
|
||||
};
|
||||
}
|
||||
|
||||
function buildConsentDiffSourceReference(eventRecord) {
|
||||
if (!eventRecord) {
|
||||
return {
|
||||
available: false,
|
||||
reason: "missing_source_event"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
store: "consent_events",
|
||||
id: eventRecord.id ?? null,
|
||||
eventType: eventRecord.eventType ?? null,
|
||||
rawEventName: eventRecord.rawEventName ?? null,
|
||||
capturedAt: eventRecord.capturedAt ?? null,
|
||||
recordedAt: eventRecord.recordedAt ?? null,
|
||||
captureSessionId: eventRecord.captureSessionId ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function buildConsentDiffNotes(providerAnnouncement, consentExecution) {
|
||||
const notes = [];
|
||||
|
||||
if (!providerAnnouncement) {
|
||||
notes.push("provider_announcement_source_missing");
|
||||
}
|
||||
|
||||
if (!consentExecution) {
|
||||
notes.push("consent_execution_source_missing");
|
||||
}
|
||||
|
||||
notes.push("primary_evidence_not_modified");
|
||||
notes.push("no_legal_assessment");
|
||||
notes.push("no_gvl_semantic_assessment");
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
function stableConsentDiffValue(value) {
|
||||
if (value === undefined) {
|
||||
return "[[undefined]]";
|
||||
}
|
||||
|
||||
if (value === null || typeof value !== "object") {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((item) => stableConsentDiffValue(item)).join(",")}]`;
|
||||
}
|
||||
|
||||
return `{${Object.keys(value)
|
||||
.sort()
|
||||
.map((key) => `${JSON.stringify(key)}:${stableConsentDiffValue(value[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
|
||||
function cloneConsentDiffValue(value) {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === null || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function isConsentDiffPlainObject(value) {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,956 @@
|
||||
"use strict";
|
||||
|
||||
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT =
|
||||
"vendorget-gvl-evidence-export";
|
||||
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
|
||||
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT =
|
||||
"vendorget-gvl-revision-evidence";
|
||||
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
|
||||
const VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND = "iab-gvl-revision";
|
||||
const VENDORGET_GVL_PROVENANCE_WEB = "web";
|
||||
const VENDORGET_GVL_PROVENANCE_VAULT = "vault";
|
||||
|
||||
const VENDORGET_GVL_EVIDENCE_STORE_NAMES = [
|
||||
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||
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
|
||||
];
|
||||
|
||||
const VENDORGET_GVL_EVIDENCE_ARRAY_NAMES = [
|
||||
"gvl_raw_evidence",
|
||||
"gvl_snapshots",
|
||||
"gvl_vendors",
|
||||
"gvl_purposes",
|
||||
"gvl_special_purposes",
|
||||
"gvl_features",
|
||||
"gvl_special_features",
|
||||
"gvl_data_categories",
|
||||
"gvl_vendor_relationships"
|
||||
];
|
||||
|
||||
const VENDORGET_GVL_NORMALIZED_STORE_NAMES = [
|
||||
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
|
||||
];
|
||||
|
||||
async function exportVendorGetGvlEvidenceJson() {
|
||||
const db = await openVendorGetDb();
|
||||
const exportedStores = await readGvlEvidenceStores(db);
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
exportFormat: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT,
|
||||
exportFormatVersion: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
dbName: db.name,
|
||||
dbVersion: db.version
|
||||
},
|
||||
...exportedStores
|
||||
};
|
||||
}
|
||||
|
||||
async function exportVendorGetGvlRevisionEvidenceJson(snapshotSha256) {
|
||||
if (!snapshotSha256) {
|
||||
throw new Error("missing_snapshot_sha256");
|
||||
}
|
||||
|
||||
const db = await openVendorGetDb();
|
||||
const snapshot = await getGvlEvidenceRecordByKey(
|
||||
db,
|
||||
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||
snapshotSha256
|
||||
);
|
||||
|
||||
if (!snapshot) {
|
||||
throw new Error("gvl_snapshot_not_found");
|
||||
}
|
||||
|
||||
const rawGvlSha256 = snapshot.rawGvlSha256 ?? null;
|
||||
const rawEvidence = rawGvlSha256
|
||||
? await getGvlEvidenceRecordByKey(
|
||||
db,
|
||||
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||
rawGvlSha256
|
||||
)
|
||||
: null;
|
||||
const normalized = await readGvlRevisionNormalizedRecords(db, snapshotSha256);
|
||||
const exportedAt = new Date();
|
||||
const exportedAtUtcCompact = formatGvlEvidenceUtcCompact(exportedAt);
|
||||
const payload = {
|
||||
metadata: {
|
||||
exportFormat: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT,
|
||||
exportFormatVersion: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION,
|
||||
exportedAt: exportedAt.toISOString(),
|
||||
exportedAtUtcCompact,
|
||||
dbName: db.name,
|
||||
dbVersion: db.version,
|
||||
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||
snapshotSha256: snapshot.sha256 ?? snapshot.snapshotSha256 ?? null,
|
||||
rawGvlSha256,
|
||||
contentKind: VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND
|
||||
},
|
||||
rawEvidence,
|
||||
snapshot,
|
||||
normalized
|
||||
};
|
||||
const exportPayloadSha256 = await calculateGvlEvidencePayloadSha256(payload);
|
||||
|
||||
return {
|
||||
...payload,
|
||||
metadata: {
|
||||
...payload.metadata,
|
||||
exportPayloadSha256
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyVendorGetGvlRevisionEvidenceJson(exportContainer) {
|
||||
const errors = [];
|
||||
const metadata = exportContainer?.metadata ?? {};
|
||||
const normalized = exportContainer?.normalized ?? {};
|
||||
const rawBody = exportContainer?.rawEvidence?.rawBody ?? null;
|
||||
const snapshot = exportContainer?.snapshot ?? null;
|
||||
const snapshotSha256 = metadata.snapshotSha256 ?? null;
|
||||
const rawGvlSha256 = metadata.rawGvlSha256 ?? null;
|
||||
|
||||
if (!exportContainer || typeof exportContainer !== "object") {
|
||||
errors.push("invalid_export_container");
|
||||
}
|
||||
|
||||
if (metadata.exportFormat !== VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT) {
|
||||
errors.push("invalid_export_format");
|
||||
}
|
||||
|
||||
if (
|
||||
metadata.exportFormatVersion !==
|
||||
VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION
|
||||
) {
|
||||
errors.push("unsupported_export_format_version");
|
||||
}
|
||||
|
||||
if (metadata.contentKind !== VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND) {
|
||||
errors.push("invalid_content_kind");
|
||||
}
|
||||
|
||||
if (metadata.vendorListVersion === null || metadata.vendorListVersion === undefined) {
|
||||
errors.push("missing_vendor_list_version");
|
||||
}
|
||||
|
||||
if (!snapshotSha256) {
|
||||
errors.push("missing_snapshot_sha256");
|
||||
}
|
||||
|
||||
if (!rawGvlSha256) {
|
||||
errors.push("missing_raw_gvl_sha256");
|
||||
}
|
||||
|
||||
if (typeof rawBody !== "string") {
|
||||
errors.push("missing_raw_body");
|
||||
} else if ((await VendorGetGvlService.calculateRawGvlSha256(rawBody)) !== rawGvlSha256) {
|
||||
errors.push("raw_body_sha256_mismatch");
|
||||
}
|
||||
|
||||
if (
|
||||
exportContainer?.rawEvidence &&
|
||||
exportContainer.rawEvidence.rawGvlSha256 !== rawGvlSha256
|
||||
) {
|
||||
errors.push("raw_evidence_sha256_mismatch");
|
||||
}
|
||||
|
||||
const snapshotRecordSha256 = snapshot?.sha256 ?? snapshot?.snapshotSha256 ?? null;
|
||||
|
||||
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
||||
errors.push("missing_snapshot");
|
||||
} else if (snapshotRecordSha256 !== snapshotSha256) {
|
||||
errors.push("snapshot_sha256_mismatch");
|
||||
} else if (snapshot.rawGvlSha256 !== rawGvlSha256) {
|
||||
errors.push("snapshot_raw_gvl_sha256_mismatch");
|
||||
} else if (snapshot.vendorListVersion !== metadata.vendorListVersion) {
|
||||
errors.push("snapshot_vendor_list_version_mismatch");
|
||||
} else if (
|
||||
(await VendorGetGvlService.calculateGvlSnapshotSha256(snapshot.rawJson)) !==
|
||||
snapshotSha256
|
||||
) {
|
||||
errors.push("snapshot_sha256_mismatch");
|
||||
}
|
||||
|
||||
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
|
||||
if (!Array.isArray(normalized[storeName])) {
|
||||
errors.push(`missing_normalized_store_${storeName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const seenKeys = new Set();
|
||||
|
||||
for (const record of normalized[storeName]) {
|
||||
if (record?.snapshotSha256 !== snapshotSha256) {
|
||||
errors.push(`normalized_record_snapshot_mismatch_${storeName}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (record?.vendorListVersion !== metadata.vendorListVersion) {
|
||||
errors.push(`normalized_record_vendor_list_version_mismatch_${storeName}`);
|
||||
break;
|
||||
}
|
||||
|
||||
const recordKey = getGvlEvidenceRecordKeyByStoreName(storeName, record);
|
||||
const recordKeySignature = JSON.stringify(recordKey);
|
||||
|
||||
if (recordKey === null) {
|
||||
errors.push(`normalized_record_missing_key_${storeName}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (seenKeys.has(recordKeySignature)) {
|
||||
errors.push(`normalized_record_duplicate_key_${storeName}`);
|
||||
break;
|
||||
}
|
||||
|
||||
seenKeys.add(recordKeySignature);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getGvlEvidenceRecordKeyByStoreName(
|
||||
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||
exportContainer?.rawEvidence
|
||||
) === null
|
||||
) {
|
||||
errors.push("raw_evidence_missing_key");
|
||||
}
|
||||
|
||||
if (
|
||||
getGvlEvidenceRecordKeyByStoreName(
|
||||
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||
snapshot
|
||||
) === null
|
||||
) {
|
||||
errors.push("snapshot_missing_key");
|
||||
}
|
||||
|
||||
if (!metadata.exportPayloadSha256) {
|
||||
errors.push("missing_export_payload_sha256");
|
||||
} else {
|
||||
const recalculatedPayloadSha256 =
|
||||
await calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(exportContainer);
|
||||
|
||||
if (recalculatedPayloadSha256 !== metadata.exportPayloadSha256) {
|
||||
errors.push("export_payload_sha256_mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
vendorListVersion: metadata.vendorListVersion ?? null,
|
||||
snapshotSha256,
|
||||
rawGvlSha256,
|
||||
exportPayloadSha256: metadata.exportPayloadSha256 ?? null,
|
||||
normalizedCounts: countGvlRevisionNormalizedRecords(normalized)
|
||||
};
|
||||
}
|
||||
|
||||
async function importVendorGetGvlRevisionEvidenceJson(exportContainer) {
|
||||
const verification =
|
||||
await verifyVendorGetGvlRevisionEvidenceJson(exportContainer);
|
||||
|
||||
if (!verification.valid) {
|
||||
return {
|
||||
imported: false,
|
||||
verification,
|
||||
counts: buildEmptyGvlRevisionEvidenceImportCounts()
|
||||
};
|
||||
}
|
||||
|
||||
const db = await openVendorGetDb();
|
||||
const counts = await importGvlRevisionEvidenceStores(db, exportContainer);
|
||||
|
||||
return {
|
||||
imported: true,
|
||||
importedAt: new Date().toISOString(),
|
||||
vendorListVersion: verification.vendorListVersion,
|
||||
snapshotSha256: verification.snapshotSha256,
|
||||
rawGvlSha256: verification.rawGvlSha256,
|
||||
verification,
|
||||
counts
|
||||
};
|
||||
}
|
||||
|
||||
async function markVendorGetGvlRevisionEvidenceVaultCopy(
|
||||
snapshotSha256,
|
||||
verification = null
|
||||
) {
|
||||
if (!snapshotSha256) {
|
||||
throw new Error("missing_snapshot_sha256");
|
||||
}
|
||||
|
||||
const db = await openVendorGetDb();
|
||||
|
||||
return markGvlRevisionEvidenceVaultCopyAvailable(
|
||||
db,
|
||||
snapshotSha256,
|
||||
verification
|
||||
);
|
||||
}
|
||||
|
||||
function getGvlEvidenceRecordByKey(db, storeName, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction([storeName], "readonly");
|
||||
const getRequest = tx.objectStore(storeName).get(key);
|
||||
let record = null;
|
||||
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
getRequest.onsuccess = () => {
|
||||
record = getRequest.result ?? null;
|
||||
};
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(record);
|
||||
});
|
||||
}
|
||||
|
||||
function readGvlRevisionNormalizedRecords(db, snapshotSha256) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const normalized = {};
|
||||
const tx = db.transaction(VENDORGET_GVL_NORMALIZED_STORE_NAMES, "readonly");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(normalized);
|
||||
|
||||
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
|
||||
const records = [];
|
||||
const cursorRequest = tx
|
||||
.objectStore(storeName)
|
||||
.index("snapshotSha256")
|
||||
.openCursor(IDBKeyRange.only(snapshotSha256));
|
||||
|
||||
normalized[storeName] = records;
|
||||
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||
cursorRequest.onsuccess = () => {
|
||||
const cursor = cursorRequest.result;
|
||||
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.push(cursor.value);
|
||||
cursor.continue();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function calculateGvlEvidencePayloadSha256(payload) {
|
||||
return sha256Hex(stableStringify(payload));
|
||||
}
|
||||
|
||||
async function calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(
|
||||
exportContainer
|
||||
) {
|
||||
const metadata = {
|
||||
...(exportContainer?.metadata ?? {})
|
||||
};
|
||||
|
||||
delete metadata.exportPayloadSha256;
|
||||
|
||||
return calculateGvlEvidencePayloadSha256({
|
||||
...exportContainer,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
function countGvlRevisionNormalizedRecords(normalized) {
|
||||
return Object.fromEntries(
|
||||
VENDORGET_GVL_NORMALIZED_STORE_NAMES.map((storeName) => [
|
||||
storeName,
|
||||
Array.isArray(normalized?.[storeName]) ? normalized[storeName].length : 0
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function importGvlRevisionEvidenceStores(db, exportContainer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const counts = buildEmptyGvlRevisionEvidenceImportCounts();
|
||||
const recordsByStoreName =
|
||||
buildGvlRevisionEvidenceImportRecordsByStoreName(exportContainer);
|
||||
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readwrite");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(counts);
|
||||
|
||||
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||
importGvlRevisionEvidenceStoreRecords(
|
||||
tx.objectStore(storeName),
|
||||
recordsByStoreName[storeName] ?? [],
|
||||
counts[storeName]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildGvlRevisionEvidenceImportRecordsByStoreName(exportContainer) {
|
||||
const rawEvidence = setGvlEvidenceRecordLocalProvenance(
|
||||
exportContainer.rawEvidence,
|
||||
VENDORGET_GVL_PROVENANCE_VAULT
|
||||
);
|
||||
const snapshot = setGvlEvidenceRecordLocalProvenance(
|
||||
exportContainer.snapshot,
|
||||
VENDORGET_GVL_PROVENANCE_VAULT
|
||||
);
|
||||
|
||||
return {
|
||||
[VENDORGET_STORE_NAMES.gvlRawEvidence]: [rawEvidence],
|
||||
[VENDORGET_STORE_NAMES.gvlSnapshots]: [snapshot],
|
||||
...Object.fromEntries(
|
||||
VENDORGET_GVL_NORMALIZED_STORE_NAMES.map((storeName) => [
|
||||
storeName,
|
||||
exportContainer.normalized?.[storeName] ?? []
|
||||
])
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function buildEmptyGvlRevisionEvidenceImportCounts() {
|
||||
return Object.fromEntries(
|
||||
VENDORGET_GVL_EVIDENCE_STORE_NAMES.map((storeName) => [
|
||||
storeName,
|
||||
{
|
||||
read: 0,
|
||||
inserted: 0,
|
||||
skippedExisting: 0,
|
||||
skippedInvalid: 0
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function importGvlRevisionEvidenceStoreRecords(objectStore, records, counts) {
|
||||
const seenKeys = new Set();
|
||||
|
||||
for (const record of records) {
|
||||
counts.read += 1;
|
||||
|
||||
const key = getGvlEvidenceRecordKey(objectStore, record);
|
||||
const keySignature = JSON.stringify(key);
|
||||
|
||||
if (key === null || seenKeys.has(keySignature)) {
|
||||
counts.skippedInvalid += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
seenKeys.add(keySignature);
|
||||
|
||||
const getRequest = objectStore.get(key);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
if (getRequest.result !== undefined) {
|
||||
mergeExistingGvlRevisionEvidenceProvenance(
|
||||
objectStore,
|
||||
getRequest.result,
|
||||
record
|
||||
);
|
||||
counts.skippedExisting += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const addRequest = objectStore.add(record);
|
||||
|
||||
addRequest.onsuccess = () => {
|
||||
counts.inserted += 1;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function mergeExistingGvlRevisionEvidenceProvenance(
|
||||
objectStore,
|
||||
existingRecord,
|
||||
importRecord
|
||||
) {
|
||||
if (
|
||||
objectStore.name !== VENDORGET_STORE_NAMES.gvlRawEvidence &&
|
||||
objectStore.name !== VENDORGET_STORE_NAMES.gvlSnapshots
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importedProvenance = normalizeGvlEvidenceProvenanceValues(importRecord);
|
||||
let updatedRecord = existingRecord;
|
||||
|
||||
for (const provenance of importedProvenance) {
|
||||
updatedRecord = annotateGvlEvidenceRecordProvenance(
|
||||
updatedRecord,
|
||||
provenance
|
||||
);
|
||||
}
|
||||
|
||||
objectStore.put(updatedRecord);
|
||||
}
|
||||
|
||||
function mergeGvlEvidenceProvenance(record, provenance) {
|
||||
const values = new Set();
|
||||
|
||||
for (const value of normalizeGvlEvidenceProvenanceValues(record)) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
if (provenance === VENDORGET_GVL_PROVENANCE_WEB) {
|
||||
values.add(VENDORGET_GVL_PROVENANCE_WEB);
|
||||
}
|
||||
|
||||
if (provenance === VENDORGET_GVL_PROVENANCE_VAULT) {
|
||||
values.add(VENDORGET_GVL_PROVENANCE_VAULT);
|
||||
}
|
||||
|
||||
return Array.from(values).sort(sortGvlEvidenceProvenanceValue);
|
||||
}
|
||||
|
||||
function annotateGvlEvidenceRecordProvenance(record, provenance) {
|
||||
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const provenanceValues = mergeGvlEvidenceProvenance(record, provenance);
|
||||
const vaultCopyAvailable =
|
||||
provenance === VENDORGET_GVL_PROVENANCE_VAULT ||
|
||||
provenanceValues.includes(VENDORGET_GVL_PROVENANCE_VAULT) ||
|
||||
record.vaultCopyAvailable === true;
|
||||
|
||||
return {
|
||||
...record,
|
||||
gvlEvidenceProvenance: provenanceValues,
|
||||
vaultCopyAvailable,
|
||||
evidenceWorkspaceDeleteAllowed: vaultCopyAvailable
|
||||
};
|
||||
}
|
||||
|
||||
function setGvlEvidenceRecordLocalProvenance(record, provenance) {
|
||||
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const provenanceValues =
|
||||
provenance === VENDORGET_GVL_PROVENANCE_VAULT
|
||||
? [VENDORGET_GVL_PROVENANCE_VAULT]
|
||||
: [VENDORGET_GVL_PROVENANCE_WEB];
|
||||
const vaultCopyAvailable = provenance === VENDORGET_GVL_PROVENANCE_VAULT;
|
||||
|
||||
return {
|
||||
...record,
|
||||
gvlEvidenceProvenance: provenanceValues,
|
||||
vaultCopyAvailable,
|
||||
evidenceWorkspaceDeleteAllowed: vaultCopyAvailable
|
||||
};
|
||||
}
|
||||
|
||||
function markGvlEvidenceRecordVaultCopyAvailable(record) {
|
||||
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||
return record;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
vaultCopyAvailable: true,
|
||||
evidenceWorkspaceDeleteAllowed: true
|
||||
};
|
||||
}
|
||||
|
||||
function getGvlEvidenceProvenanceState(record) {
|
||||
const provenanceValues = normalizeGvlEvidenceProvenanceValues(record);
|
||||
const containsWeb = provenanceValues.includes(VENDORGET_GVL_PROVENANCE_WEB);
|
||||
const containsVault = provenanceValues.includes(VENDORGET_GVL_PROVENANCE_VAULT);
|
||||
const vaultCopyAvailable = record?.vaultCopyAvailable === true || containsVault;
|
||||
|
||||
return {
|
||||
provenance: formatGvlEvidenceProvenance(provenanceValues),
|
||||
containsWeb,
|
||||
containsVault,
|
||||
vaultCopyAvailable,
|
||||
workspaceDeleteAllowed: vaultCopyAvailable,
|
||||
workspaceDeleteProtected: containsWeb && !vaultCopyAvailable
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGvlEvidenceProvenanceValues(record) {
|
||||
const values = new Set();
|
||||
const provenance = record?.gvlEvidenceProvenance ?? record?.provenance ?? null;
|
||||
|
||||
if (Array.isArray(provenance)) {
|
||||
provenance.forEach((value) => appendGvlEvidenceProvenanceValue(values, value));
|
||||
} else if (typeof provenance === "string") {
|
||||
provenance
|
||||
.split("+")
|
||||
.forEach((value) => appendGvlEvidenceProvenanceValue(values, value));
|
||||
}
|
||||
|
||||
if (!values.size && record?.sourceUrl) {
|
||||
values.add(VENDORGET_GVL_PROVENANCE_WEB);
|
||||
}
|
||||
|
||||
return Array.from(values).sort(sortGvlEvidenceProvenanceValue);
|
||||
}
|
||||
|
||||
function appendGvlEvidenceProvenanceValue(values, value) {
|
||||
if (value === VENDORGET_GVL_PROVENANCE_WEB) {
|
||||
values.add(VENDORGET_GVL_PROVENANCE_WEB);
|
||||
}
|
||||
|
||||
if (value === VENDORGET_GVL_PROVENANCE_VAULT) {
|
||||
values.add(VENDORGET_GVL_PROVENANCE_VAULT);
|
||||
}
|
||||
}
|
||||
|
||||
function sortGvlEvidenceProvenanceValue(left, right) {
|
||||
const order = {
|
||||
[VENDORGET_GVL_PROVENANCE_WEB]: 0,
|
||||
[VENDORGET_GVL_PROVENANCE_VAULT]: 1
|
||||
};
|
||||
|
||||
return (order[left] ?? 99) - (order[right] ?? 99);
|
||||
}
|
||||
|
||||
function formatGvlEvidenceProvenance(values) {
|
||||
const normalizedValues = Array.isArray(values) ? values : [];
|
||||
|
||||
if (
|
||||
normalizedValues.includes(VENDORGET_GVL_PROVENANCE_WEB) &&
|
||||
normalizedValues.includes(VENDORGET_GVL_PROVENANCE_VAULT)
|
||||
) {
|
||||
return "web+vault";
|
||||
}
|
||||
|
||||
if (normalizedValues.includes(VENDORGET_GVL_PROVENANCE_VAULT)) {
|
||||
return "vault";
|
||||
}
|
||||
|
||||
return "web";
|
||||
}
|
||||
|
||||
function markGvlRevisionEvidenceVaultCopyAvailable(
|
||||
db,
|
||||
snapshotSha256,
|
||||
verification = null
|
||||
) {
|
||||
return updateGvlRevisionEvidenceRecords(
|
||||
db,
|
||||
snapshotSha256,
|
||||
(record) => markGvlEvidenceRecordVaultCopyAvailable(record),
|
||||
verification
|
||||
);
|
||||
}
|
||||
|
||||
function markGvlRevisionEvidenceWebSource(db, snapshotSha256) {
|
||||
return markGvlRevisionEvidenceProvenance(
|
||||
db,
|
||||
snapshotSha256,
|
||||
VENDORGET_GVL_PROVENANCE_WEB
|
||||
);
|
||||
}
|
||||
|
||||
function markGvlRevisionEvidenceProvenance(db, snapshotSha256, provenance) {
|
||||
return updateGvlRevisionEvidenceRecords(db, snapshotSha256, (record) =>
|
||||
annotateGvlEvidenceRecordProvenance(record, provenance)
|
||||
);
|
||||
}
|
||||
|
||||
function updateGvlRevisionEvidenceRecords(
|
||||
db,
|
||||
snapshotSha256,
|
||||
updateRecord,
|
||||
verification = null
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(
|
||||
[VENDORGET_STORE_NAMES.gvlSnapshots, VENDORGET_STORE_NAMES.gvlRawEvidence],
|
||||
"readwrite"
|
||||
);
|
||||
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
|
||||
const rawEvidenceStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlRawEvidence);
|
||||
const snapshotRequest = snapshotsStore.get(snapshotSha256);
|
||||
let result = {
|
||||
snapshotMarked: false,
|
||||
rawEvidenceMarked: false,
|
||||
snapshotSha256,
|
||||
rawGvlSha256: null,
|
||||
skippedReason: null
|
||||
};
|
||||
|
||||
snapshotRequest.onerror = () => reject(snapshotRequest.error);
|
||||
snapshotRequest.onsuccess = () => {
|
||||
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;
|
||||
}
|
||||
|
||||
const updatedSnapshot = updateRecord(snapshot);
|
||||
|
||||
snapshotsStore.put(updatedSnapshot);
|
||||
result.snapshotMarked = true;
|
||||
result.rawGvlSha256 = updatedSnapshot.rawGvlSha256 ?? null;
|
||||
|
||||
if (!updatedSnapshot.rawGvlSha256) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawEvidenceRequest = rawEvidenceStore.get(updatedSnapshot.rawGvlSha256);
|
||||
|
||||
rawEvidenceRequest.onsuccess = () => {
|
||||
const rawEvidence = rawEvidenceRequest.result ?? null;
|
||||
|
||||
if (!rawEvidence) {
|
||||
return;
|
||||
}
|
||||
|
||||
rawEvidenceStore.put(
|
||||
updateRecord(rawEvidence)
|
||||
);
|
||||
result.rawEvidenceMarked = true;
|
||||
};
|
||||
};
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(result);
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
padGvlEvidenceDatePart(date.getUTCMonth() + 1),
|
||||
padGvlEvidenceDatePart(date.getUTCDate()),
|
||||
"T",
|
||||
padGvlEvidenceDatePart(date.getUTCHours()),
|
||||
padGvlEvidenceDatePart(date.getUTCMinutes()),
|
||||
padGvlEvidenceDatePart(date.getUTCSeconds()),
|
||||
"Z"
|
||||
].join("");
|
||||
}
|
||||
|
||||
function padGvlEvidenceDatePart(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function readGvlEvidenceStores(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const exportContainer = {};
|
||||
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readonly");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(exportContainer);
|
||||
|
||||
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||
const records = [];
|
||||
const cursorRequest = tx.objectStore(storeName).openCursor();
|
||||
|
||||
exportContainer[storeName] = records;
|
||||
|
||||
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||
cursorRequest.onsuccess = () => {
|
||||
const cursor = cursorRequest.result;
|
||||
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.push(cursor.value);
|
||||
cursor.continue();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function importVendorGetGvlEvidenceJson(importContainer) {
|
||||
validateVendorGetGvlEvidenceImport(importContainer);
|
||||
|
||||
const db = await openVendorGetDb();
|
||||
|
||||
return importGvlEvidenceStores(db, importContainer);
|
||||
}
|
||||
|
||||
function validateVendorGetGvlEvidenceImport(importContainer) {
|
||||
if (!importContainer || typeof importContainer !== "object") {
|
||||
throw new Error("invalid_gvl_evidence_export");
|
||||
}
|
||||
|
||||
const metadata = importContainer.metadata ?? {};
|
||||
|
||||
if (metadata.exportFormat !== VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT) {
|
||||
throw new Error("invalid_gvl_evidence_export_format");
|
||||
}
|
||||
|
||||
if (
|
||||
metadata.exportFormatVersion !==
|
||||
VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION
|
||||
) {
|
||||
throw new Error("unsupported_gvl_evidence_export_version");
|
||||
}
|
||||
|
||||
for (const arrayName of VENDORGET_GVL_EVIDENCE_ARRAY_NAMES) {
|
||||
if (!Array.isArray(importContainer[arrayName])) {
|
||||
throw new Error(`missing_gvl_evidence_store_${arrayName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importGvlEvidenceStores(db, importContainer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const counts = buildEmptyGvlEvidenceImportCounts();
|
||||
const seenKeysByStore = new Map();
|
||||
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readwrite");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => {
|
||||
resolve({
|
||||
importedAt: new Date().toISOString(),
|
||||
counts
|
||||
});
|
||||
};
|
||||
|
||||
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||
importGvlEvidenceStoreRecords(
|
||||
tx.objectStore(storeName),
|
||||
importContainer[storeName],
|
||||
counts[storeName],
|
||||
getSeenKeysForStore(seenKeysByStore, storeName)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildEmptyGvlEvidenceImportCounts() {
|
||||
return Object.fromEntries(
|
||||
VENDORGET_GVL_EVIDENCE_STORE_NAMES.map((storeName) => [
|
||||
storeName,
|
||||
{
|
||||
read: 0,
|
||||
inserted: 0,
|
||||
skippedExisting: 0,
|
||||
skippedInvalid: 0
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function getSeenKeysForStore(seenKeysByStore, storeName) {
|
||||
if (!seenKeysByStore.has(storeName)) {
|
||||
seenKeysByStore.set(storeName, new Set());
|
||||
}
|
||||
|
||||
return seenKeysByStore.get(storeName);
|
||||
}
|
||||
|
||||
function importGvlEvidenceStoreRecords(objectStore, records, counts, seenKeys) {
|
||||
for (const record of records) {
|
||||
counts.read += 1;
|
||||
|
||||
const key = getGvlEvidenceRecordKey(objectStore, record);
|
||||
const keySignature = JSON.stringify(key);
|
||||
|
||||
if (key === null || seenKeys.has(keySignature)) {
|
||||
counts.skippedInvalid += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
seenKeys.add(keySignature);
|
||||
|
||||
const getRequest = objectStore.get(key);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
if (getRequest.result !== undefined) {
|
||||
counts.skippedExisting += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const addRequest = objectStore.add(record);
|
||||
|
||||
addRequest.onsuccess = () => {
|
||||
counts.inserted += 1;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getGvlEvidenceRecordKey(objectStore, record) {
|
||||
return getGvlEvidenceRecordKeyByStoreName(
|
||||
objectStore.name,
|
||||
record,
|
||||
objectStore.keyPath
|
||||
);
|
||||
}
|
||||
|
||||
function getGvlEvidenceRecordKeyByStoreName(storeName, record, keyPath = "id") {
|
||||
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (storeName === VENDORGET_STORE_NAMES.gvlSnapshots) {
|
||||
return record.sha256 ?? record.snapshotSha256 ?? null;
|
||||
}
|
||||
|
||||
if (storeName === VENDORGET_STORE_NAMES.gvlRawEvidence) {
|
||||
return record.rawGvlSha256 ?? null;
|
||||
}
|
||||
|
||||
if (typeof keyPath !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return record[keyPath] ?? null;
|
||||
}
|
||||
@@ -127,14 +127,14 @@ th:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.explorer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.workspace-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.button-link,
|
||||
.workspace-link,
|
||||
button {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #475569;
|
||||
@@ -145,12 +145,27 @@ button {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.workspace-link {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.workspace-link strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.workspace-link span {
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.workspace-placeholder {
|
||||
color: #cbd5e1;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
@@ -165,7 +180,8 @@ button:disabled {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.explorer-actions {
|
||||
.workspace-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VendorGet-IV Evidence Dashboard</title>
|
||||
<title>VG-Observe Dashboard</title>
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="dashboard">
|
||||
<header class="dashboard-header">
|
||||
<h1>VendorGet-IV Evidence Dashboard</h1>
|
||||
<h1>VG-Observe Dashboard</h1>
|
||||
<div id="dashboard-status" class="dashboard-status" aria-live="polite">
|
||||
Loading evidence status
|
||||
Lade lokalen Beobachtungsstatus
|
||||
</div>
|
||||
<p class="dashboard-notice">
|
||||
Übersicht und Einstieg für VG-Observe. Detailansichten liegen in
|
||||
eigenen Explorern.
|
||||
Was wurde lokal beobachtet? Diese Übersicht zeigt den Workspace-
|
||||
Bestand und führt zu den fachlichen Arbeitsansichten.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="panel" aria-labelledby="stores-title">
|
||||
<h2 id="stores-title">Evidence Stores</h2>
|
||||
<h2 id="stores-title">Lokaler Evidence-Bestand</h2>
|
||||
<p class="section-help">
|
||||
Bestandsübersicht der lokal gespeicherten Beobachtungen und
|
||||
Referenzdaten. Die Zahlen sind Inventar, keine Bewertung.
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -53,13 +57,38 @@
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="official-gvl-title">
|
||||
<h2 id="official-gvl-title">Offizielle Vendorliste</h2>
|
||||
<section class="panel" aria-labelledby="workspaces-title">
|
||||
<h2 id="workspaces-title">Arbeitsbereiche</h2>
|
||||
<p class="section-help">
|
||||
Die aktuell offiziell abgerufene IAB-Europe-Vendorliste ist die
|
||||
Version, die VG-Observe direkt von der offiziellen IAB-Europe-Quelle
|
||||
geladen hat. Sie ist getrennt von der Vendorliste, die in einem
|
||||
konkreten Consent-Kontext gemeldet wurde.
|
||||
Oben verstehen, in der Mitte arbeiten, unten beweisen: Die
|
||||
Detailansichten trennen Untersuchung, technische Beobachtung,
|
||||
Referenz/Vault und Analyse-Vorbereitung.
|
||||
</p>
|
||||
<div class="workspace-actions">
|
||||
<a class="workspace-link" href="../consent-explorer/consent-explorer.html">
|
||||
<strong>Consent untersuchen</strong>
|
||||
<span>Zentrale Ansicht für dokumentierte Consent-Zustände.</span>
|
||||
</a>
|
||||
<a class="workspace-link" href="../request-explorer/request-explorer.html">
|
||||
<strong>Requests prüfen</strong>
|
||||
<span>Technisch beobachtete Browser-Requests ohne Bewertung.</span>
|
||||
</a>
|
||||
<a class="workspace-link" href="../gvl-explorer/gvl-explorer.html">
|
||||
<strong>GVL-Referenz/Vault</strong>
|
||||
<span>Vendorlisten, Revision-Evidence und Vault-Transport.</span>
|
||||
</a>
|
||||
<a class="workspace-link" href="../analysis-dashboard/analysis-dashboard.html">
|
||||
<strong>Analyse-Vorbereitung</strong>
|
||||
<span>Datenbestände und vorbereitete Prüffelder, keine Engine.</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="official-gvl-title">
|
||||
<h2 id="official-gvl-title">GVL-Referenzstatus</h2>
|
||||
<p class="section-help">
|
||||
Die GVL ist Referenzbasis für spätere Rekonstruktion. Dieser Status
|
||||
zeigt nur den lokalen Referenzbestand und letzte Update-Hinweise.
|
||||
</p>
|
||||
<dl class="gvl-status-grid">
|
||||
<div>
|
||||
@@ -89,27 +118,16 @@
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="explorers-title">
|
||||
<h2 id="explorers-title">Explorer</h2>
|
||||
<p class="section-help">
|
||||
Historische Consent-Zustände und technische Belege werden in einer
|
||||
eigenen Ansicht geöffnet.
|
||||
</p>
|
||||
<div class="explorer-actions">
|
||||
<a class="button-link" href="../consent-explorer/consent-explorer.html">
|
||||
Consent-Explorer öffnen
|
||||
</a>
|
||||
<a class="button-link" href="../gvl-explorer/gvl-explorer.html">
|
||||
GVL-Explorer öffnen
|
||||
</a>
|
||||
<a class="button-link" href="../request-explorer/request-explorer.html">
|
||||
Request-Explorer öffnen
|
||||
</a>
|
||||
<a class="button-link" href="../analysis-dashboard/analysis-dashboard.html">
|
||||
Request-/Empfänger-Analyse öffnen
|
||||
<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>
|
||||
|
||||
@@ -40,9 +40,9 @@ async function renderEvidenceStatus() {
|
||||
}
|
||||
|
||||
renderStoreCounts(status.storeCounts ?? {});
|
||||
renderStatusMessage("Evidence status loaded");
|
||||
renderStatusMessage("Lokaler Beobachtungsstatus geladen");
|
||||
} catch (error) {
|
||||
renderStatusMessage("Evidence status could not be loaded");
|
||||
renderStatusMessage("Lokaler Beobachtungsstatus konnte nicht geladen werden");
|
||||
console.warn("VendorGet-IV dashboard status failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #e5edf5;
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
width: min(1040px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.maintenance-header {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 13px;
|
||||
color: #e5edf5;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 13px;
|
||||
color: #e5edf5;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 760px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
width: fit-content;
|
||||
color: #bfdbfe;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 22px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.section-help {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.segment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.segment-card,
|
||||
.danger-action {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.segment-action {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 4px;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.segment-status {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
color: #cbd5e1;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.protected-revisions {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 4px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: #e5edf5;
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.danger-panel {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.danger-action {
|
||||
border-color: #7f1d1d;
|
||||
background: #1f1518;
|
||||
}
|
||||
|
||||
.danger-action .segment-status {
|
||||
border-color: #7f1d1d;
|
||||
color: #fecaca;
|
||||
background: #450a0a;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.maintenance {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.segment-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VG-Observe Datenpflege</title>
|
||||
<link rel="stylesheet" href="data-maintenance.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="maintenance">
|
||||
<header class="maintenance-header">
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||
<h1>Datenpflege</h1>
|
||||
<p class="section-help">
|
||||
Lokale Datenbestände verwalten und bereinigen.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="panel" aria-labelledby="segments-title">
|
||||
<h2 id="segments-title">Datenbereiche</h2>
|
||||
<div class="segment-grid">
|
||||
<article class="segment-card">
|
||||
<h3>GVL-Referenzdaten</h3>
|
||||
<p>
|
||||
Vendorlisten, Revisionen und GVL-Referenzbestände.
|
||||
</p>
|
||||
<div class="segment-action">
|
||||
<h4>GVL-Referenzdaten bereinigen</h4>
|
||||
<p>
|
||||
Entfernt lokale GVL-Referenzdaten aus der Browser-Datenbank.
|
||||
Consent-Daten, Request-Beobachtungen und Analyse-Daten bleiben
|
||||
unberührt.
|
||||
</p>
|
||||
<button id="purge-gvl-reference-data-button" type="button" disabled>
|
||||
GVL-Referenzdaten bereinigen
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
id="gvl-reference-maintenance-status"
|
||||
class="segment-status"
|
||||
aria-live="polite"
|
||||
>
|
||||
–
|
||||
</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>
|
||||
|
||||
<article class="segment-card">
|
||||
<h3>Consent-Daten</h3>
|
||||
<p>
|
||||
Consent-Zustände und Ereignisse.
|
||||
</p>
|
||||
<p class="segment-status">
|
||||
Noch nicht verfügbar.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="segment-card">
|
||||
<h3>Analyse-Daten</h3>
|
||||
<p>
|
||||
Analyse- und Arbeitsdaten.
|
||||
</p>
|
||||
<p class="segment-status">
|
||||
Noch nicht verfügbar.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="segment-card">
|
||||
<h3>Weitere Datenbereiche</h3>
|
||||
<p>
|
||||
Reserviert für zukünftige Erweiterungen.
|
||||
</p>
|
||||
<p class="segment-status">
|
||||
Noch nicht verfügbar.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel danger-panel" aria-labelledby="danger-title">
|
||||
<h2 id="danger-title">Gefahrenbereich</h2>
|
||||
<p class="section-help">
|
||||
Irreversible Aktionen für den gesamten lokalen Datenbestand.
|
||||
</p>
|
||||
<article class="danger-action" aria-label="Vorbereitete Gefahrenaktion">
|
||||
<h3>Gesamten lokalen Datenbestand löschen</h3>
|
||||
<p class="segment-status">Noch nicht verfügbar.</p>
|
||||
<p>
|
||||
Erfordert später eine ausdrückliche Sicherheitsbestätigung.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<script src="data-maintenance.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
"use strict";
|
||||
|
||||
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", async () => {
|
||||
purgeGvlReferenceDataButton.addEventListener("click", async () => {
|
||||
await purgeUnlockedEvidenceRecords();
|
||||
});
|
||||
await renderGvlReferenceMaintenanceStatus();
|
||||
});
|
||||
|
||||
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;
|
||||
gvlReferenceMaintenanceMessage.textContent = "Bereinigung läuft...";
|
||||
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({
|
||||
type: "purge_unlocked_evidence_records"
|
||||
});
|
||||
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error ?? "purge_unlocked_evidence_records_failed");
|
||||
}
|
||||
|
||||
await renderGvlReferenceMaintenanceStatus();
|
||||
gvlReferenceMaintenanceMessage.textContent = buildPurgeSuccessMessage(result);
|
||||
} catch (error) {
|
||||
await renderGvlReferenceMaintenanceStatus();
|
||||
gvlReferenceMaintenanceMessage.textContent = "Bereinigung fehlgeschlagen.";
|
||||
console.warn("VG-Observe protected purge failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function buildPurgeSuccessMessage(result) {
|
||||
if (Number.isFinite(result.deletedCount)) {
|
||||
return `Bereinigung abgeschlossen: ${result.deletedCount} Records.`;
|
||||
}
|
||||
|
||||
return "Bereinigung abgeschlossen.";
|
||||
}
|
||||
|
||||
function renderProtectedRevisions(vendorListVersions) {
|
||||
gvlReferenceProtectedRevisions.hidden = vendorListVersions.length === 0;
|
||||
gvlReferenceProtectedRevisions.textContent = vendorListVersions.length
|
||||
? `Geschützt: ${vendorListVersions
|
||||
.map((vendorListVersion) => String(vendorListVersion))
|
||||
.join(", ")}`
|
||||
: "";
|
||||
}
|
||||
@@ -76,12 +76,88 @@ p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rebuild-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fetch-status {
|
||||
min-height: 18px;
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.evidence-status {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.evidence-status.is-neutral {
|
||||
color: #cbd5e1;
|
||||
border-color: #334155;
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.evidence-status.is-success {
|
||||
color: #bbf7d0;
|
||||
border-color: #166534;
|
||||
background: #052e16;
|
||||
}
|
||||
|
||||
.evidence-status.is-warning {
|
||||
color: #fde68a;
|
||||
border-color: #92400e;
|
||||
background: #422006;
|
||||
}
|
||||
|
||||
.evidence-status.is-error {
|
||||
color: #fecaca;
|
||||
border-color: #991b1b;
|
||||
background: #450a0a;
|
||||
}
|
||||
|
||||
.vendor-detail-form {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(120px, 180px) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vendor-overview {
|
||||
margin-top: 18px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.vendor-overview summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vendor-detail-form label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.vendor-detail-form input {
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 4px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: #e5edf5;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #475569;
|
||||
@@ -92,11 +168,40 @@ button {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.file-action {
|
||||
display: inline-block;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 4px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: #e5edf5;
|
||||
background: #1f2937;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-action.is-disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -119,7 +224,7 @@ th {
|
||||
}
|
||||
|
||||
.snapshot-list {
|
||||
min-width: 820px;
|
||||
min-width: 940px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -160,6 +265,106 @@ th {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vendor-detail-result {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.vendor-detail-panel {
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.vendor-detail-panel > summary,
|
||||
.subject-details > summary {
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vendor-detail-section h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary-table td {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.subject-details {
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.gvl-catalog-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.gvl-catalog-section h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #e5edf5;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.catalog-item {
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
background: #182231;
|
||||
}
|
||||
|
||||
.catalog-item summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
color: #e5edf5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.definition-list {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 180px) 1fr;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.definition-list dt,
|
||||
.definition-list dd {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #334155;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.definition-list dt {
|
||||
color: #cbd5e1;
|
||||
font-weight: 700;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.definition-list dd {
|
||||
color: #e5edf5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
max-width: none;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.secondary-action {
|
||||
width: fit-content;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #334155;
|
||||
@@ -198,4 +403,12 @@ th {
|
||||
.summary-table th {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.vendor-detail-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.definition-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,59 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VG-Observe GVL-Explorer</title>
|
||||
<title>VG-Observe GVL-Referenz/Vault</title>
|
||||
<link rel="stylesheet" href="gvl-explorer.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="explorer">
|
||||
<header class="explorer-header">
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||
<h1>GVL-Explorer</h1>
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||
<h1>GVL-Referenz/Vault</h1>
|
||||
<p class="section-help">
|
||||
Diese Ansicht zeigt lokal gespeicherte offizielle
|
||||
IAB-Europe-Vendorlisten. Sie dient dazu, historische
|
||||
Vendorlisten-Versionen nachvollziehbar zu machen.
|
||||
Die GVL ist Referenzbasis und Evidence-Quelle für reproduzierbare
|
||||
Rekonstruktion. Sie ist nicht das untersuchte Consent-Ereignis
|
||||
selbst; Import, Export und Verifikation sichern den Referenzbestand.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="panel" aria-labelledby="snapshot-list-title">
|
||||
<h2 id="snapshot-list-title">Gespeicherte Vendorlisten</h2>
|
||||
<h2 id="snapshot-list-title">GVL-Revisionen im Workspace</h2>
|
||||
<div class="fetch-actions">
|
||||
<button id="gvl-fetch-official-button" type="button">
|
||||
Offizielle Vendorliste jetzt abrufen
|
||||
GVL-Referenz aus Web laden
|
||||
</button>
|
||||
<button id="gvl-revision-evidence-export-button" type="button">
|
||||
Ausgewählte Revision in den Vault exportieren
|
||||
</button>
|
||||
<label class="file-action" for="gvl-revision-evidence-verify-input">
|
||||
Vault-Paket verifizieren
|
||||
</label>
|
||||
<input
|
||||
id="gvl-revision-evidence-verify-input"
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
>
|
||||
<label class="file-action" for="gvl-revision-evidence-import-input">
|
||||
Revision aus Vault importieren
|
||||
</label>
|
||||
<input
|
||||
id="gvl-revision-evidence-import-input"
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
>
|
||||
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
||||
Bereit
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
id="gvl-evidence-transport-status"
|
||||
class="fetch-status"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
<p id="gvl-snapshot-empty" class="empty-state" hidden>
|
||||
Keine gespeicherten offiziellen Vendorlisten vorhanden.
|
||||
Keine gespeicherten GVL-Referenzrevisionen vorhanden.
|
||||
</p>
|
||||
<div id="gvl-snapshot-content" hidden>
|
||||
<div class="snapshot-list-wrap">
|
||||
@@ -37,6 +63,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Vendorlisten-Version</th>
|
||||
<th scope="col">Herkunft</th>
|
||||
<th scope="col">Vault</th>
|
||||
<th scope="col">Schutz</th>
|
||||
<th scope="col">Abrufzeitpunkt</th>
|
||||
<th scope="col">SHA256</th>
|
||||
<th scope="col">Quelle</th>
|
||||
@@ -47,10 +76,41 @@
|
||||
</div>
|
||||
|
||||
<section class="snapshot-summary" aria-labelledby="snapshot-summary-title">
|
||||
<h2 id="snapshot-summary-title">Ausgewählte Vendorliste</h2>
|
||||
<h2 id="snapshot-summary-title">Ausgewählte GVL-Revision</h2>
|
||||
<div class="rebuild-actions">
|
||||
<button id="gvl-rebuild-normalized-button" type="button" disabled>
|
||||
Lokale Referenzdaten neu aufbauen (Reparatur)
|
||||
</button>
|
||||
<span
|
||||
id="gvl-rebuild-normalized-status"
|
||||
class="fetch-status"
|
||||
aria-live="polite"
|
||||
></span>
|
||||
</div>
|
||||
<div id="gvl-snapshot-summary"></div>
|
||||
</section>
|
||||
|
||||
<details id="gvl-vendor-overview-details" class="vendor-overview">
|
||||
<summary id="gvl-vendor-overview-summary">
|
||||
Vendoren-Referenz anzeigen
|
||||
</summary>
|
||||
<p id="gvl-vendor-overview-empty" class="empty-state" hidden>
|
||||
Keine normalisierten Vendoren für diese Vendorliste vorhanden.
|
||||
</p>
|
||||
<div id="gvl-vendor-overview-content" class="snapshot-list-wrap" hidden>
|
||||
<table class="snapshot-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Vendor-ID</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Deleted Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="gvl-vendor-overview-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="technical-details">
|
||||
<summary>Technische Feldnamen</summary>
|
||||
<pre id="gvl-technical-fields"></pre>
|
||||
@@ -61,6 +121,30 @@
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="vendor-detail-title">
|
||||
<h2 id="vendor-detail-title">Lokaler Vendor-Referenznachweis</h2>
|
||||
<form id="gvl-vendor-detail-form" class="vendor-detail-form">
|
||||
<label for="gvl-vendor-id-input">Vendor-ID</label>
|
||||
<input
|
||||
id="gvl-vendor-id-input"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="977"
|
||||
>
|
||||
<button id="gvl-vendor-detail-button" type="submit">
|
||||
Vendor-Referenz anzeigen
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
id="gvl-vendor-detail-status"
|
||||
class="fetch-status"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
<div id="gvl-vendor-detail-result" class="vendor-detail-result"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="gvl-explorer.js"></script>
|
||||
|
||||
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -20,7 +20,7 @@
|
||||
|
||||
function cloneSerializable(value, seen) {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
@@ -50,10 +50,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
@@ -75,31 +71,8 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
window.__tcfapi("ping", 2, function (pingData, pingSuccess) {
|
||||
|
||||
console.log("VendorGet __tcfapi ping:", {
|
||||
success: pingSuccess,
|
||||
data: pingData
|
||||
});
|
||||
|
||||
emitToContentScript("tcf_ping", {
|
||||
success: pingSuccess,
|
||||
data: pingData
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
window.__tcfapi("addEventListener", 2, function (tcData, success) {
|
||||
|
||||
if (!success || !tcData) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("VendorGet raw event:", tcData);
|
||||
|
||||
if (tcData.eventStatus === "useractioncomplete") {
|
||||
|
||||
const capture = {
|
||||
function buildTcfEventCapture(tcData) {
|
||||
return {
|
||||
url: window.location.href,
|
||||
origin: window.location.origin,
|
||||
|
||||
@@ -133,8 +106,64 @@
|
||||
|
||||
addtlConsent: tcData.addtlConsent,
|
||||
|
||||
rawTcString: {
|
||||
tcString: tcData.tcString,
|
||||
addtlConsent: tcData.addtlConsent
|
||||
},
|
||||
|
||||
rawTcData: cloneSerializable(tcData)
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("VendorGetTcfProbeRequest", function (event) {
|
||||
const requestId = event?.detail?.requestId ?? null;
|
||||
|
||||
window.__tcfapi("getTCData", 2, function (tcData, success) {
|
||||
window.dispatchEvent(new CustomEvent("VendorGetTcfProbeResponse", {
|
||||
detail: {
|
||||
requestId: requestId,
|
||||
success: success,
|
||||
capture: success && tcData ? buildTcfEventCapture(tcData) : null
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
window.__tcfapi("ping", 2, function (pingData, pingSuccess) {
|
||||
|
||||
console.log("VendorGet __tcfapi ping:", {
|
||||
success: pingSuccess,
|
||||
data: pingData
|
||||
});
|
||||
|
||||
emitToContentScript("tcf_ping", {
|
||||
success: pingSuccess,
|
||||
data: pingData
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
window.__tcfapi("addEventListener", 2, function (tcData, success) {
|
||||
|
||||
if (!success || !tcData) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("VendorGet raw event:", tcData);
|
||||
|
||||
if (
|
||||
tcData.eventStatus === "cmpuishown" ||
|
||||
tcData.eventStatus === "tcloaded"
|
||||
) {
|
||||
emitToContentScript(
|
||||
"tcf_pre_consent_event",
|
||||
buildTcfEventCapture(tcData)
|
||||
);
|
||||
}
|
||||
|
||||
if (tcData.eventStatus === "useractioncomplete") {
|
||||
|
||||
const capture = buildTcfEventCapture(tcData);
|
||||
|
||||
console.log("VendorGet CONSENT CAPTURE:", capture);
|
||||
|
||||
|
||||
@@ -123,6 +123,42 @@ h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pre-consent-capture {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
border: 1px solid #6366f1;
|
||||
border-radius: 6px;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.pre-consent-capture[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pre-consent-capture h2 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pre-consent-capture p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.pre-consent-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pre-consent-actions button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.evidence-counts {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -176,6 +212,11 @@ button:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.secondary-workflow {
|
||||
color: #cbd5e1;
|
||||
background: #172033;
|
||||
}
|
||||
|
||||
.confirm-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VendorGet-IV</title>
|
||||
<title>VG-Observe Laufzeitsteuerung</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup">
|
||||
<h1>VendorGet-IV</h1>
|
||||
<h1>VG-Observe</h1>
|
||||
|
||||
<section class="status" aria-label="Status">
|
||||
<div class="status-row">
|
||||
@@ -29,10 +29,35 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="evidence-retention" aria-label="Evidenzdaten">
|
||||
<h2>Evidenzdaten</h2>
|
||||
<section
|
||||
id="pre-consent-capture"
|
||||
class="pre-consent-capture"
|
||||
aria-label="Pre-Consent-Erfassung"
|
||||
hidden
|
||||
>
|
||||
<h2>Consent-Vorgang erfassen?</h2>
|
||||
<p id="pre-consent-capture-summary">
|
||||
CMP-Vorgang erkannt.
|
||||
</p>
|
||||
<div class="pre-consent-actions">
|
||||
<button id="pre-consent-capture-decline" type="button">
|
||||
Nein
|
||||
</button>
|
||||
<button id="pre-consent-capture-start" type="button">
|
||||
Ja
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="pre-consent-capture-status"
|
||||
class="retention-status"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section class="evidence-retention" aria-label="Kurzstatus">
|
||||
<h2>Workspace-Kurzstatus</h2>
|
||||
<div id="maintenance-warning" class="maintenance-warning" hidden>
|
||||
Aufzeichnung pausiert: Dashboard geöffnet
|
||||
Aufzeichnung pausiert: Observe-Dashboard geöffnet
|
||||
</div>
|
||||
<dl class="evidence-counts">
|
||||
<div>
|
||||
@@ -61,15 +86,30 @@
|
||||
</div>
|
||||
</dl>
|
||||
<button id="evidence-dashboard-button" type="button">
|
||||
Evidence Dashboard öffnen
|
||||
Observe-Dashboard öffnen
|
||||
</button>
|
||||
<button id="evidence-export-json-button" type="button">
|
||||
<button
|
||||
id="evidence-export-json-button"
|
||||
class="secondary-workflow"
|
||||
type="button"
|
||||
hidden
|
||||
>
|
||||
Export Evidence JSON
|
||||
</button>
|
||||
<button id="evidence-purge-unlocked-button" type="button">
|
||||
<button
|
||||
id="evidence-purge-unlocked-button"
|
||||
class="secondary-workflow"
|
||||
type="button"
|
||||
hidden
|
||||
>
|
||||
Ungesperrte Evidence-Daten löschen
|
||||
</button>
|
||||
<div id="evidence-export-json-status" class="retention-status" aria-live="polite"></div>
|
||||
<div
|
||||
id="evidence-export-json-status"
|
||||
class="retention-status"
|
||||
aria-live="polite"
|
||||
hidden
|
||||
></div>
|
||||
<div id="evidence-retention-status" class="retention-status" aria-live="polite">
|
||||
Status wird geladen
|
||||
</div>
|
||||
|
||||
+160
-1
@@ -8,6 +8,19 @@ const requestMonitoringStatus = document.getElementById(
|
||||
);
|
||||
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
|
||||
const consentCaptureStatus = document.getElementById("consent-capture-status");
|
||||
const preConsentCapture = document.getElementById("pre-consent-capture");
|
||||
const preConsentCaptureSummary = document.getElementById(
|
||||
"pre-consent-capture-summary"
|
||||
);
|
||||
const preConsentCaptureStart = document.getElementById(
|
||||
"pre-consent-capture-start"
|
||||
);
|
||||
const preConsentCaptureDecline = document.getElementById(
|
||||
"pre-consent-capture-decline"
|
||||
);
|
||||
const preConsentCaptureStatus = document.getElementById(
|
||||
"pre-consent-capture-status"
|
||||
);
|
||||
const maintenanceWarning = document.getElementById("maintenance-warning");
|
||||
const evidenceLockedCount = document.getElementById("evidence-locked-count");
|
||||
const evidenceDashboardButton = document.getElementById(
|
||||
@@ -47,6 +60,7 @@ const evidenceStoreCountCells = {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await renderSettings();
|
||||
await renderPreConsentCaptureStatus();
|
||||
await renderEvidenceMaintenanceStatus();
|
||||
await renderEvidenceRetentionStatus();
|
||||
|
||||
@@ -57,7 +71,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
"consentCaptureEnabled",
|
||||
consentCaptureToggle.checked
|
||||
);
|
||||
|
||||
if (consentCaptureToggle.checked) {
|
||||
await probePreConsentCaptureForActiveTab();
|
||||
}
|
||||
|
||||
await renderSettings();
|
||||
await renderPreConsentCaptureStatus();
|
||||
|
||||
consentCaptureToggle.disabled = false;
|
||||
});
|
||||
@@ -84,6 +104,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
evidencePurgeUnlockedButton.addEventListener("click", openPurgeConfirmModal);
|
||||
evidencePurgeCancelButton.addEventListener("click", closePurgeConfirmModal);
|
||||
evidencePurgeConfirmButton.addEventListener("click", purgeUnlockedEvidence);
|
||||
preConsentCaptureStart.addEventListener("click", startPreConsentCapture);
|
||||
preConsentCaptureDecline.addEventListener("click", declinePreConsentCapture);
|
||||
});
|
||||
|
||||
async function renderSettings() {
|
||||
@@ -158,6 +180,137 @@ function renderEvidenceRetentionMessage(message) {
|
||||
evidenceRetentionStatus.textContent = message;
|
||||
}
|
||||
|
||||
async function renderPreConsentCaptureStatus() {
|
||||
try {
|
||||
const activeTab = await getActiveTab();
|
||||
|
||||
if (!activeTab?.id) {
|
||||
renderNoPreConsentCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await browser.runtime.sendMessage({
|
||||
type: "get_pre_consent_capture_status",
|
||||
payload: {
|
||||
tabId: activeTab.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error ?? "get_pre_consent_capture_status_failed");
|
||||
}
|
||||
|
||||
renderPreConsentCapture(result.capture);
|
||||
} catch (error) {
|
||||
renderNoPreConsentCapture();
|
||||
console.warn("VendorGet-IV pre-consent status failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function probePreConsentCaptureForActiveTab() {
|
||||
try {
|
||||
const activeTab = await getActiveTab();
|
||||
|
||||
if (!activeTab?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: "probe_pre_consent_capture_for_tab",
|
||||
payload: {
|
||||
tabId: activeTab.id
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("VendorGet-IV pre-consent probe failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreConsentCapture(capture) {
|
||||
if (!capture) {
|
||||
renderNoPreConsentCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
preConsentCapture.hidden = false;
|
||||
preConsentCaptureStart.hidden = capture.status !== "attention";
|
||||
preConsentCaptureDecline.hidden = capture.status !== "attention";
|
||||
|
||||
if (capture.status === "recording") {
|
||||
preConsentCaptureSummary.textContent = "Pre-Consent-Erfassung läuft.";
|
||||
preConsentCaptureStatus.textContent =
|
||||
"Provider-Announcement wurde gesichert; warte auf useractioncomplete.";
|
||||
return;
|
||||
}
|
||||
|
||||
preConsentCaptureSummary.textContent = buildPreConsentCaptureSummary(capture);
|
||||
preConsentCaptureStatus.textContent = "";
|
||||
}
|
||||
|
||||
function renderNoPreConsentCapture() {
|
||||
preConsentCapture.hidden = true;
|
||||
preConsentCaptureSummary.textContent = "";
|
||||
preConsentCaptureStatus.textContent = "";
|
||||
preConsentCaptureStart.disabled = false;
|
||||
preConsentCaptureDecline.disabled = false;
|
||||
}
|
||||
|
||||
function buildPreConsentCaptureSummary(capture) {
|
||||
const eventLabel = capture.firstEventStatus ?? "TCF-Event";
|
||||
const eventCount = Number(capture.eventCount ?? 0);
|
||||
|
||||
return `${eventLabel} erkannt, ${eventCount} Pre-Consent-Event(s) gepuffert.`;
|
||||
}
|
||||
|
||||
async function startPreConsentCapture() {
|
||||
await answerPreConsentCapture("start_pre_consent_capture");
|
||||
}
|
||||
|
||||
async function declinePreConsentCapture() {
|
||||
await answerPreConsentCapture("decline_pre_consent_capture");
|
||||
}
|
||||
|
||||
async function answerPreConsentCapture(messageType) {
|
||||
preConsentCaptureStart.disabled = true;
|
||||
preConsentCaptureDecline.disabled = true;
|
||||
|
||||
try {
|
||||
const activeTab = await getActiveTab();
|
||||
|
||||
if (!activeTab?.id) {
|
||||
throw new Error("active_tab_unavailable");
|
||||
}
|
||||
|
||||
const result = await browser.runtime.sendMessage({
|
||||
type: messageType,
|
||||
payload: {
|
||||
tabId: activeTab.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error ?? messageType);
|
||||
}
|
||||
|
||||
renderPreConsentCapture(result.capture);
|
||||
} catch (error) {
|
||||
preConsentCaptureStatus.textContent = "Aktion konnte nicht ausgeführt werden";
|
||||
console.warn("VendorGet-IV pre-consent action failed", error);
|
||||
} finally {
|
||||
preConsentCaptureStart.disabled = false;
|
||||
preConsentCaptureDecline.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveTab() {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
});
|
||||
|
||||
return tabs[0] ?? null;
|
||||
}
|
||||
|
||||
function openPurgeConfirmModal() {
|
||||
evidencePurgeConfirmModal.hidden = false;
|
||||
evidencePurgeCancelButton.focus();
|
||||
@@ -194,7 +347,13 @@ async function purgeUnlockedEvidence() {
|
||||
|
||||
function buildPurgeUnlockedSuccessMessage(result) {
|
||||
if (Number.isFinite(result.deletedCount)) {
|
||||
return `Ungesperrte Evidence-Daten gelöscht: ${result.deletedCount} Records`;
|
||||
const message = `Ungesperrte Evidence-Daten gelöscht: ${result.deletedCount} Records`;
|
||||
|
||||
if (Number(result.keptGvlWorkspaceProtectedCount ?? 0) > 0) {
|
||||
return `${message}. Diese GVL-Evidence wurde noch nicht in den Vault exportiert.`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
return "Ungesperrte Evidence-Daten gelöscht";
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>VG-Observe Request-Explorer</title>
|
||||
<title>VG-Observe Requests prüfen</title>
|
||||
<link rel="stylesheet" href="request-explorer.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="explorer">
|
||||
<header class="explorer-header">
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||
<h1>Request-Explorer</h1>
|
||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||
<h1>Requests prüfen</h1>
|
||||
<p class="section-help">
|
||||
Diese Ansicht zeigt zuletzt technisch beobachtete Requests.
|
||||
Diese Ansicht zeigt sichtbar beobachtete Browser-Requests aus der
|
||||
Laufzeitbeobachtung. Sie dokumentiert technische Vorgänge und nimmt
|
||||
keine rechtliche Bewertung vor.
|
||||
</p>
|
||||
<dl class="request-overview" aria-label="Geladene Request-Uebersicht">
|
||||
<div>
|
||||
@@ -27,7 +29,7 @@
|
||||
</header>
|
||||
|
||||
<section class="panel" aria-labelledby="request-list-title">
|
||||
<h2 id="request-list-title">Zuletzt beobachtete Requests</h2>
|
||||
<h2 id="request-list-title">Technisch beobachtete Requests</h2>
|
||||
<p id="request-empty" class="empty-state" hidden>
|
||||
Keine beobachteten Requests vorhanden.
|
||||
</p>
|
||||
@@ -35,7 +37,7 @@
|
||||
<div id="request-groups" class="request-groups"></div>
|
||||
|
||||
<section class="request-detail" aria-labelledby="request-detail-title">
|
||||
<h2 id="request-detail-title">Ausgewählter Request</h2>
|
||||
<h2 id="request-detail-title">Ausgewählter beobachteter Request</h2>
|
||||
<div id="request-detail-summary"></div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "VG-Consent",
|
||||
"path": ".."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"codex.projectRole": "Consent-Workspace",
|
||||
"codex.scope": [
|
||||
"Consent-Capture",
|
||||
"Consent-State-Rekonstruktion",
|
||||
"Consent-Events",
|
||||
"Consent-Explorer",
|
||||
"Evidence-Chain-Resolver",
|
||||
"lesende Request-Korrelation",
|
||||
"GVL nur als Referenz/Vault"
|
||||
],
|
||||
"codex.outOfScope": [
|
||||
"allgemeiner Cleanup",
|
||||
"GVL-Import",
|
||||
"GVL-Export",
|
||||
"GVL-Verify",
|
||||
"Dashboard-Refactoring",
|
||||
"Request-Explorer-Ausbau",
|
||||
"neue Datenquellen",
|
||||
"IndexedDB-Migrationen",
|
||||
"Analyse-Engine"
|
||||
],
|
||||
"codex.primaryQuestion": "Hilft diese Änderung dabei, die technisch beobachtbare Wirkung einer konkreten Consent-Entscheidung nachvollziehbar, reproduzierbar und evidenzorientiert zu rekonstruieren?"
|
||||
}
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren