Commits vergleichen
13 Commits
08d5a6ccc2
..
master
| Autor | SHA1 | Datum | |
|---|---|---|---|
| 904787c115 | |||
| 1f10389016 | |||
| 3a174128e0 | |||
| e19eef67b2 | |||
| d68735ffc4 | |||
| 0defb37e63 | |||
| 8d923ba962 | |||
| 61a20c424c | |||
| d0dc1978e4 | |||
| b89addf362 | |||
| a3ca8019a1 | |||
| eb70f0ce81 | |||
| f8a23ba643 |
@@ -38,3 +38,6 @@ web-ext-artifacts/
|
|||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.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
|
# VG-Environment Codex Workflow
|
||||||
|
|
||||||
|
Vor Beginn fachlicher Arbeiten ist `docs/architecture/project-philosophy.md` als maßgebliche Beschreibung des Projektkerns zu berücksichtigen.
|
||||||
|
|
||||||
## Grundprinzip
|
## Grundprinzip
|
||||||
|
|
||||||
VG-Environment wird schrittweise modularisiert.
|
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
|
# 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
|
## Core purpose
|
||||||
|
|
||||||
VG-Environment is a local evidence and runtime reconstruction environment for consent-related browser activity.
|
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-constants.js",
|
||||||
"src/background/db/db-core.js",
|
"src/background/db/db-core.js",
|
||||||
"src/core/evidence-export-json.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-normalizer.js",
|
||||||
"src/background/gvl/gvl-vendor-relationship-normalizer.js",
|
"src/background/gvl/gvl-vendor-relationship-normalizer.js",
|
||||||
"src/background/gvl/gvl-catalog-normalizer.js",
|
"src/background/gvl/gvl-catalog-normalizer.js",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"src/background/gvl-service.js",
|
"src/background/gvl-service.js",
|
||||||
"src/core/binary-utils.js",
|
"src/core/binary-utils.js",
|
||||||
"src/core/tcf-core-metadata-decoder.js",
|
"src/core/tcf-core-metadata-decoder.js",
|
||||||
|
"src/core/consent-diff.js",
|
||||||
"src/background.js"
|
"src/background.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,19 +3,18 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="analysis-dashboard.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="analysis-dashboard">
|
<main class="analysis-dashboard">
|
||||||
<header class="analysis-header">
|
<header class="analysis-header">
|
||||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||||
<h1>Analyse-Dashboard</h1>
|
<h1>Analyse-Vorbereitung</h1>
|
||||||
<p>
|
<p>
|
||||||
Diese Ansicht bereitet technische Prüfungen zwischen
|
Diese Ansicht ist noch keine Analyse-Engine. Sie zeigt vorhandene
|
||||||
Consent-Zuständen, Vendorlisten und beobachteten Requests vor.
|
Datenbestände und vorbereitet strukturierte Prüffelder für spätere
|
||||||
Aktuell werden nur vorhandene Datenbestände und vorbereitete
|
Auswertung, ohne Bewertung oder Zuordnung zu berechnen.
|
||||||
Analysebereiche angezeigt.
|
|
||||||
</p>
|
</p>
|
||||||
<div id="analysis-status" class="analysis-status" aria-live="polite">
|
<div id="analysis-status" class="analysis-status" aria-live="polite">
|
||||||
Lade Datenbestände
|
Lade Datenbestände
|
||||||
@@ -34,11 +33,11 @@
|
|||||||
<dd id="summary-observed-requests">-</dd>
|
<dd id="summary-observed-requests">-</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Lokal gespeicherte Vendorlisten</dt>
|
<dt>Lokal gespeicherte GVL-Referenzen</dt>
|
||||||
<dd id="summary-gvl-snapshots">-</dd>
|
<dd id="summary-gvl-snapshots">-</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Lokal aktuelle Vendorlisten-Version</dt>
|
<dt>Lokal aktuelle GVL-Referenzversion</dt>
|
||||||
<dd id="summary-current-gvl-version">-</dd>
|
<dd id="summary-current-gvl-version">-</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -49,23 +48,23 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="areas-title">
|
<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">
|
<div class="area-grid">
|
||||||
<article>
|
<article>
|
||||||
<h3>Consent ↔ Vendorliste</h3>
|
<h3>Consent ↔ Vendorliste</h3>
|
||||||
<p>Analyse noch nicht ausgeführt.</p>
|
<p>Vorbereitung vorhanden, keine Analyse ausgeführt.</p>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h3>Consent ↔ beobachtete Requests</h3>
|
<h3>Consent ↔ beobachtete Requests</h3>
|
||||||
<p>Analyse noch nicht ausgeführt.</p>
|
<p>Vorbereitung vorhanden, keine Analyse ausgeführt.</p>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h3>Request-Hosts ↔ bekannte Vendoren</h3>
|
<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>
|
||||||
<article>
|
<article>
|
||||||
<h3>Potenziell erklärungsbedürftige technische Diskrepanzen</h3>
|
<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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+1543
-22
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
let keptLockedCount = 0;
|
let keptLockedCount = 0;
|
||||||
|
let keptGvlWorkspaceProtectedCount = 0;
|
||||||
const tx = db.transaction(VENDORGET_EVIDENCE_STORE_NAMES, "readwrite");
|
const tx = db.transaction(VENDORGET_EVIDENCE_STORE_NAMES, "readwrite");
|
||||||
|
|
||||||
tx.onerror = () => reject(tx.error);
|
tx.onerror = () => reject(tx.error);
|
||||||
@@ -41,7 +44,12 @@ function purgeUnlockedEvidenceRecords(db) {
|
|||||||
resolve({
|
resolve({
|
||||||
success: true,
|
success: true,
|
||||||
deletedCount,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isGvlWorkspaceProtectedRecord(
|
||||||
|
storeName,
|
||||||
|
cursor.value,
|
||||||
|
gvlWorkspaceProtection
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
keptGvlWorkspaceProtectedCount += 1;
|
||||||
|
cursor.continue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deletedCount += 1;
|
deletedCount += 1;
|
||||||
cursor.delete();
|
cursor.delete();
|
||||||
cursor.continue();
|
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) {
|
function countRecordsInStores(db, storeNames) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
|
|||||||
@@ -23,25 +23,32 @@ function storeGvlRawEvidenceIfNew(db, rawEvidence) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(["gvl_raw_evidence"], "readwrite");
|
const tx = db.transaction(["gvl_raw_evidence"], "readwrite");
|
||||||
const rawEvidenceStore = tx.objectStore("gvl_raw_evidence");
|
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;
|
let result = null;
|
||||||
|
|
||||||
getRequest.onerror = () => reject(getRequest.error);
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
|
||||||
getRequest.onsuccess = () => {
|
getRequest.onsuccess = () => {
|
||||||
if (getRequest.result) {
|
if (getRequest.result) {
|
||||||
|
rawEvidenceStore.put(
|
||||||
|
annotateGvlEvidenceRecordProvenance(getRequest.result, "web")
|
||||||
|
);
|
||||||
result = {
|
result = {
|
||||||
stored: false,
|
stored: false,
|
||||||
rawGvlSha256: rawEvidence.rawGvlSha256
|
rawGvlSha256: evidenceRecord.rawGvlSha256
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawEvidenceStore.add(rawEvidence);
|
rawEvidenceStore.add(evidenceRecord);
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
stored: true,
|
stored: true,
|
||||||
rawGvlSha256: rawEvidence.rawGvlSha256
|
rawGvlSha256: evidenceRecord.rawGvlSha256
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +65,7 @@ async function buildGvlSnapshotRecord(
|
|||||||
) {
|
) {
|
||||||
const gvlJson = normalizeGvlSnapshotValueForMetadata(rawJson);
|
const gvlJson = normalizeGvlSnapshotValueForMetadata(rawJson);
|
||||||
|
|
||||||
return {
|
return annotateGvlEvidenceRecordProvenance({
|
||||||
sha256: await calculateGvlSnapshotSha256(rawJson),
|
sha256: await calculateGvlSnapshotSha256(rawJson),
|
||||||
rawGvlSha256: rawGvlSha256 ?? null,
|
rawGvlSha256: rawGvlSha256 ?? null,
|
||||||
vendorListVersion: gvlJson?.vendorListVersion ?? null,
|
vendorListVersion: gvlJson?.vendorListVersion ?? null,
|
||||||
@@ -72,7 +79,7 @@ async function buildGvlSnapshotRecord(
|
|||||||
// Existing GVL snapshots already use createdAt as the local mirror timestamp;
|
// Existing GVL snapshots already use createdAt as the local mirror timestamp;
|
||||||
// keep that field instead of duplicating it as recordedAt.
|
// keep that field instead of duplicating it as recordedAt.
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
}, "web");
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeGvlSnapshotIfNew(db, snapshot) {
|
function storeGvlSnapshotIfNew(db, snapshot) {
|
||||||
@@ -86,6 +93,9 @@ function storeGvlSnapshotIfNew(db, snapshot) {
|
|||||||
|
|
||||||
getRequest.onsuccess = () => {
|
getRequest.onsuccess = () => {
|
||||||
if (getRequest.result) {
|
if (getRequest.result) {
|
||||||
|
snapshotsStore.put(
|
||||||
|
annotateGvlEvidenceRecordProvenance(getRequest.result, "web")
|
||||||
|
);
|
||||||
result = {
|
result = {
|
||||||
stored: false,
|
stored: false,
|
||||||
sha256: snapshot.sha256,
|
sha256: snapshot.sha256,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ body {
|
|||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
h4,
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,11 @@ h3 {
|
|||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -139,6 +145,165 @@ th {
|
|||||||
margin-top: 18px;
|
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 th,
|
||||||
.inspector-table td {
|
.inspector-table td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@@ -227,4 +392,12 @@ th {
|
|||||||
.inspector-table .inspector-explanation {
|
.inspector-table .inspector-explanation {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.finding-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finding-list div {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="consent-explorer.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="explorer">
|
<main class="explorer">
|
||||||
<header class="explorer-header">
|
<header class="explorer-header">
|
||||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||||
<h1>Dokumentierte Consent-Zustände</h1>
|
<h1>Consent untersuchen</h1>
|
||||||
<p class="section-help">
|
<p class="section-help">
|
||||||
Diese Ansicht zeigt gespeicherte Consent-Zustände aus der lokalen
|
Consent ist das zentrale Untersuchungsobjekt von VG-Observe. Diese
|
||||||
Beobachtungsdatenbank. Jeder Eintrag ist ein dokumentierter Zustand,
|
Ansicht zeigt lokal beobachtete Consent-Zustände und ihre technischen
|
||||||
den VG-Observe während der Browser-Laufzeit beobachtet hat.
|
Belege, ohne daraus eine Bewertung abzuleiten.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="documented-consent-title">
|
<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>
|
<p id="documented-consent-empty" class="empty-state" hidden>
|
||||||
Keine dokumentierten Consent-Zustände vorhanden.
|
Keine dokumentierten Consent-Zustände vorhanden.
|
||||||
</p>
|
</p>
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
<section class="consent-detail" aria-labelledby="consent-detail-title">
|
<section class="consent-detail" aria-labelledby="consent-detail-title">
|
||||||
<h2 id="consent-detail-title">Ausgewählter Consent-Zustand</h2>
|
<h2 id="consent-detail-title">Ausgewählter Consent-Zustand</h2>
|
||||||
<div id="consent-detail-observation"></div>
|
<div id="consent-detail-observation"></div>
|
||||||
|
<div id="consent-detail-diff"></div>
|
||||||
<div id="consent-detail-basics"></div>
|
<div id="consent-detail-basics"></div>
|
||||||
<div id="consent-detail-summary"></div>
|
<div id="consent-detail-summary"></div>
|
||||||
<div id="consent-detail-publisher"></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);
|
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) => {
|
window.addEventListener("VendorGetFromPage", async (event) => {
|
||||||
|
|
||||||
console.log("VendorGet message from page:", event.detail);
|
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({
|
await browser.runtime.sendMessage({
|
||||||
type: "vendorget_capture",
|
type: "vendorget_capture",
|
||||||
payload: event.detail
|
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;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explorer-actions {
|
.workspace-actions {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-link,
|
.workspace-link,
|
||||||
button {
|
button {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid #475569;
|
border: 1px solid #475569;
|
||||||
@@ -145,12 +145,27 @@ button {
|
|||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-link {
|
.workspace-link {
|
||||||
display: inline-flex;
|
display: grid;
|
||||||
align-items: center;
|
gap: 5px;
|
||||||
text-decoration: none;
|
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 {
|
button:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
@@ -165,7 +180,8 @@ button:disabled {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explorer-actions {
|
.workspace-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="dashboard.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="dashboard">
|
<main class="dashboard">
|
||||||
<header class="dashboard-header">
|
<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">
|
<div id="dashboard-status" class="dashboard-status" aria-live="polite">
|
||||||
Loading evidence status
|
Lade lokalen Beobachtungsstatus
|
||||||
</div>
|
</div>
|
||||||
<p class="dashboard-notice">
|
<p class="dashboard-notice">
|
||||||
Übersicht und Einstieg für VG-Observe. Detailansichten liegen in
|
Was wurde lokal beobachtet? Diese Übersicht zeigt den Workspace-
|
||||||
eigenen Explorern.
|
Bestand und führt zu den fachlichen Arbeitsansichten.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="stores-title">
|
<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>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -53,13 +57,38 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="official-gvl-title">
|
<section class="panel" aria-labelledby="workspaces-title">
|
||||||
<h2 id="official-gvl-title">Offizielle Vendorliste</h2>
|
<h2 id="workspaces-title">Arbeitsbereiche</h2>
|
||||||
<p class="section-help">
|
<p class="section-help">
|
||||||
Die aktuell offiziell abgerufene IAB-Europe-Vendorliste ist die
|
Oben verstehen, in der Mitte arbeiten, unten beweisen: Die
|
||||||
Version, die VG-Observe direkt von der offiziellen IAB-Europe-Quelle
|
Detailansichten trennen Untersuchung, technische Beobachtung,
|
||||||
geladen hat. Sie ist getrennt von der Vendorliste, die in einem
|
Referenz/Vault und Analyse-Vorbereitung.
|
||||||
konkreten Consent-Kontext gemeldet wurde.
|
</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>
|
</p>
|
||||||
<dl class="gvl-status-grid">
|
<dl class="gvl-status-grid">
|
||||||
<div>
|
<div>
|
||||||
@@ -89,27 +118,16 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="explorers-title">
|
<section class="panel" aria-labelledby="data-maintenance-title">
|
||||||
<h2 id="explorers-title">Explorer</h2>
|
<h2 id="data-maintenance-title">Datenpflege</h2>
|
||||||
<p class="section-help">
|
<div class="workspace-actions">
|
||||||
Historische Consent-Zustände und technische Belege werden in einer
|
<a class="workspace-link workspace-placeholder" href="../data-maintenance/data-maintenance.html">
|
||||||
eigenen Ansicht geöffnet.
|
<strong>Datenpflege</strong>
|
||||||
</p>
|
<span>Verwaltung lokaler Datenbestände.</span>
|
||||||
<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
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="dashboard.js"></script>
|
<script src="dashboard.js"></script>
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ async function renderEvidenceStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStoreCounts(status.storeCounts ?? {});
|
renderStoreCounts(status.storeCounts ?? {});
|
||||||
renderStatusMessage("Evidence status loaded");
|
renderStatusMessage("Lokaler Beobachtungsstatus geladen");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderStatusMessage("Evidence status could not be loaded");
|
renderStatusMessage("Lokaler Beobachtungsstatus konnte nicht geladen werden");
|
||||||
console.warn("VendorGet-IV dashboard status failed", error);
|
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(", ")}`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
@@ -90,6 +90,37 @@ p {
|
|||||||
color: #cbd5e1;
|
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 {
|
.vendor-detail-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(120px, 180px) auto;
|
grid-template-columns: auto minmax(120px, 180px) auto;
|
||||||
@@ -137,11 +168,40 @@ button {
|
|||||||
background: #1f2937;
|
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 {
|
button:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.65;
|
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 {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -164,7 +224,7 @@ th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-list {
|
.snapshot-list {
|
||||||
min-width: 820px;
|
min-width: 940px;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,11 +271,100 @@ th {
|
|||||||
margin-top: 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 {
|
.vendor-detail-section h3 {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 13px;
|
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 {
|
.empty-state {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
@@ -258,4 +407,8 @@ th {
|
|||||||
.vendor-detail-form {
|
.vendor-detail-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.definition-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,59 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="gvl-explorer.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="explorer">
|
<main class="explorer">
|
||||||
<header class="explorer-header">
|
<header class="explorer-header">
|
||||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||||
<h1>GVL-Explorer</h1>
|
<h1>GVL-Referenz/Vault</h1>
|
||||||
<p class="section-help">
|
<p class="section-help">
|
||||||
Diese Ansicht zeigt lokal gespeicherte offizielle
|
Die GVL ist Referenzbasis und Evidence-Quelle für reproduzierbare
|
||||||
IAB-Europe-Vendorlisten. Sie dient dazu, historische
|
Rekonstruktion. Sie ist nicht das untersuchte Consent-Ereignis
|
||||||
Vendorlisten-Versionen nachvollziehbar zu machen.
|
selbst; Import, Export und Verifikation sichern den Referenzbestand.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="snapshot-list-title">
|
<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">
|
<div class="fetch-actions">
|
||||||
<button id="gvl-fetch-official-button" type="button">
|
<button id="gvl-fetch-official-button" type="button">
|
||||||
GVL aus Web laden
|
GVL-Referenz aus Web laden
|
||||||
</button>
|
</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">
|
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
||||||
Bereit
|
Bereit
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="gvl-evidence-transport-status"
|
||||||
|
class="fetch-status"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
<p id="gvl-snapshot-empty" class="empty-state" hidden>
|
<p id="gvl-snapshot-empty" class="empty-state" hidden>
|
||||||
Keine gespeicherten offiziellen Vendorlisten vorhanden.
|
Keine gespeicherten GVL-Referenzrevisionen vorhanden.
|
||||||
</p>
|
</p>
|
||||||
<div id="gvl-snapshot-content" hidden>
|
<div id="gvl-snapshot-content" hidden>
|
||||||
<div class="snapshot-list-wrap">
|
<div class="snapshot-list-wrap">
|
||||||
@@ -37,6 +63,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Vendorlisten-Version</th>
|
<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">Abrufzeitpunkt</th>
|
||||||
<th scope="col">SHA256</th>
|
<th scope="col">SHA256</th>
|
||||||
<th scope="col">Quelle</th>
|
<th scope="col">Quelle</th>
|
||||||
@@ -47,10 +76,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="snapshot-summary" aria-labelledby="snapshot-summary-title">
|
<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">
|
<div class="rebuild-actions">
|
||||||
<button id="gvl-rebuild-normalized-button" type="button" disabled>
|
<button id="gvl-rebuild-normalized-button" type="button" disabled>
|
||||||
Lokale GVL-Daten neu aufbauen (Reparatur)
|
Lokale Referenzdaten neu aufbauen (Reparatur)
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
id="gvl-rebuild-normalized-status"
|
id="gvl-rebuild-normalized-status"
|
||||||
@@ -63,7 +92,7 @@
|
|||||||
|
|
||||||
<details id="gvl-vendor-overview-details" class="vendor-overview">
|
<details id="gvl-vendor-overview-details" class="vendor-overview">
|
||||||
<summary id="gvl-vendor-overview-summary">
|
<summary id="gvl-vendor-overview-summary">
|
||||||
Vendoren-Übersicht anzeigen
|
Vendoren-Referenz anzeigen
|
||||||
</summary>
|
</summary>
|
||||||
<p id="gvl-vendor-overview-empty" class="empty-state" hidden>
|
<p id="gvl-vendor-overview-empty" class="empty-state" hidden>
|
||||||
Keine normalisierten Vendoren für diese Vendorliste vorhanden.
|
Keine normalisierten Vendoren für diese Vendorliste vorhanden.
|
||||||
@@ -94,7 +123,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="vendor-detail-title">
|
<section class="panel" aria-labelledby="vendor-detail-title">
|
||||||
<h2 id="vendor-detail-title">Lokaler Vendor-Nachweis</h2>
|
<h2 id="vendor-detail-title">Lokaler Vendor-Referenznachweis</h2>
|
||||||
<form id="gvl-vendor-detail-form" class="vendor-detail-form">
|
<form id="gvl-vendor-detail-form" class="vendor-detail-form">
|
||||||
<label for="gvl-vendor-id-input">Vendor-ID</label>
|
<label for="gvl-vendor-id-input">Vendor-ID</label>
|
||||||
<input
|
<input
|
||||||
@@ -106,7 +135,7 @@
|
|||||||
placeholder="977"
|
placeholder="977"
|
||||||
>
|
>
|
||||||
<button id="gvl-vendor-detail-button" type="submit">
|
<button id="gvl-vendor-detail-button" type="submit">
|
||||||
Vendor anzeigen
|
Vendor-Referenz anzeigen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ const gvlDebugData = document.getElementById("gvl-debug-data");
|
|||||||
const gvlFetchOfficialButton = document.getElementById(
|
const gvlFetchOfficialButton = document.getElementById(
|
||||||
"gvl-fetch-official-button"
|
"gvl-fetch-official-button"
|
||||||
);
|
);
|
||||||
|
const gvlRevisionEvidenceExportButton = document.getElementById(
|
||||||
|
"gvl-revision-evidence-export-button"
|
||||||
|
);
|
||||||
|
const gvlRevisionEvidenceVerifyInput = document.getElementById(
|
||||||
|
"gvl-revision-evidence-verify-input"
|
||||||
|
);
|
||||||
|
const gvlRevisionEvidenceImportInput = document.getElementById(
|
||||||
|
"gvl-revision-evidence-import-input"
|
||||||
|
);
|
||||||
|
const gvlEvidenceTransportStatus = document.getElementById(
|
||||||
|
"gvl-evidence-transport-status"
|
||||||
|
);
|
||||||
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
||||||
const gvlRebuildNormalizedButton = document.getElementById(
|
const gvlRebuildNormalizedButton = document.getElementById(
|
||||||
"gvl-rebuild-normalized-button"
|
"gvl-rebuild-normalized-button"
|
||||||
@@ -53,6 +65,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
await fetchOfficialGvl();
|
await fetchOfficialGvl();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gvlRevisionEvidenceExportButton.addEventListener("click", async () => {
|
||||||
|
await exportSelectedGvlRevisionEvidenceJsonFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
gvlRevisionEvidenceVerifyInput.addEventListener("change", async () => {
|
||||||
|
await verifyGvlRevisionEvidenceJsonFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
gvlRevisionEvidenceImportInput.addEventListener("change", async () => {
|
||||||
|
await importGvlRevisionEvidenceJsonFile();
|
||||||
|
});
|
||||||
|
|
||||||
gvlRebuildNormalizedButton.addEventListener("click", async () => {
|
gvlRebuildNormalizedButton.addEventListener("click", async () => {
|
||||||
await rebuildSelectedGvlSnapshotNormalizedData();
|
await rebuildSelectedGvlSnapshotNormalizedData();
|
||||||
});
|
});
|
||||||
@@ -74,6 +98,11 @@ async function fetchOfficialGvl() {
|
|||||||
type: "fetch_official_gvl"
|
type: "fetch_official_gvl"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result?.error === "gvl_revision_evidence_conflict") {
|
||||||
|
renderFetchStatus(buildGvlEvidenceConflictMessage(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
throw new Error(result?.error ?? "official_gvl_fetch_failed");
|
throw new Error(result?.error ?? "official_gvl_fetch_failed");
|
||||||
}
|
}
|
||||||
@@ -83,39 +112,399 @@ async function fetchOfficialGvl() {
|
|||||||
await renderGvlSnapshots();
|
await renderGvlSnapshots();
|
||||||
await renderSelectedGvlSnapshotSummary();
|
await renderSelectedGvlSnapshotSummary();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderFetchStatus("GVL aus Web konnte nicht geladen werden.");
|
renderFetchStatus("GVL-Referenz aus Web konnte nicht geladen werden.");
|
||||||
console.warn("VG-Observe manual official GVL fetch failed", error);
|
console.warn("VG-Observe manual official GVL fetch failed", error);
|
||||||
} finally {
|
} finally {
|
||||||
gvlFetchOfficialButton.disabled = false;
|
gvlFetchOfficialButton.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGvlEvidenceConflictMessage(result) {
|
||||||
|
return [
|
||||||
|
"GVL-Web-Abruf abgebrochen: lokale Vault-Evidence weicht vom Live-Web ab.",
|
||||||
|
`Revision ${formatNullable(result?.vendorListVersion)}.`,
|
||||||
|
`Lokal ${shortenSha256(result?.existingSnapshotSha256)} / ${shortenSha256(
|
||||||
|
result?.existingRawGvlSha256
|
||||||
|
)}.`,
|
||||||
|
`Web ${shortenSha256(result?.fetchedSnapshotSha256)} / ${shortenSha256(
|
||||||
|
result?.fetchedRawGvlSha256
|
||||||
|
)}.`
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function buildGvlSyncStatusMessage(result) {
|
function buildGvlSyncStatusMessage(result) {
|
||||||
if (result?.syncStatus === "new_gvl_revision_stored_and_normalized") {
|
if (result?.syncStatus === "new_gvl_revision_stored_and_normalized") {
|
||||||
return "GVL aus Web geladen, neue Revision gespeichert und normalisiert.";
|
return "GVL-Referenz aus Web geladen, neue Revision gespeichert und normalisiert.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result?.syncStatus === "known_gvl_rebuilt_from_local_evidence") {
|
if (result?.syncStatus === "known_gvl_rebuilt_from_local_evidence") {
|
||||||
return "GVL aus Web geprüft; bekannte Revision aus lokaler Evidence neu aufgebaut.";
|
return "GVL-Referenz aus Web geprüft; bekannte Revision aus lokaler Evidence neu aufgebaut.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result?.syncStatus === "current_and_locally_available") {
|
if (result?.syncStatus === "current_and_locally_available") {
|
||||||
return "GVL aus Web geprüft; aktuelle Revision ist lokal vollständig verfügbar.";
|
return "GVL-Referenz aus Web geprüft; aktuelle Revision ist lokal vollständig verfügbar.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return result?.alreadyKnown
|
return result?.alreadyKnown
|
||||||
? "GVL aus Web geprüft; Revision ist lokal verfügbar."
|
? "GVL-Referenz aus Web geprüft; Revision ist lokal verfügbar."
|
||||||
: "GVL aus Web geladen.";
|
: "GVL-Referenz aus Web geladen.";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFetchStatus(message) {
|
function renderFetchStatus(message) {
|
||||||
gvlFetchStatus.textContent = message;
|
gvlFetchStatus.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderGvlEvidenceTransportStatus(message, statusKind = "neutral") {
|
||||||
|
gvlEvidenceTransportStatus.textContent = message;
|
||||||
|
gvlEvidenceTransportStatus.className = [
|
||||||
|
"fetch-status",
|
||||||
|
"evidence-status",
|
||||||
|
`is-${statusKind}`
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function renderRebuildStatus(message) {
|
function renderRebuildStatus(message) {
|
||||||
gvlRebuildNormalizedStatus.textContent = message;
|
gvlRebuildNormalizedStatus.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportSelectedGvlRevisionEvidenceJsonFile() {
|
||||||
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"Keine GVL-Revision ausgewählt.",
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlRevisionEvidenceExportButton.disabled = true;
|
||||||
|
renderGvlEvidenceTransportStatus("GVL-Revision-Export läuft...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "export_gvl_revision_evidence_json",
|
||||||
|
payload: {
|
||||||
|
snapshotSha256: snapshot.sha256
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success || !result.export) {
|
||||||
|
throw new Error(
|
||||||
|
result?.error ?? "export_gvl_revision_evidence_json_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Export wird intern verifiziert..."
|
||||||
|
);
|
||||||
|
|
||||||
|
const verification = await verifyGeneratedGvlRevisionEvidenceExport(
|
||||||
|
result.export
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verification.valid) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
[
|
||||||
|
"GVL-Revision-Export nicht erzeugt: interne Verifikation fehlgeschlagen.",
|
||||||
|
buildGvlRevisionEvidenceVerificationMessage(verification)
|
||||||
|
].join(" "),
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadGvlRevisionEvidenceJsonExport(result.export);
|
||||||
|
await markGvlRevisionEvidenceVaultCopy(result.export, verification);
|
||||||
|
await renderGvlSnapshots();
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
[
|
||||||
|
"GVL-Revision exportiert und intern verifiziert.",
|
||||||
|
buildGvlRevisionEvidenceExportSuccessMessage(result.export)
|
||||||
|
].join(" "),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Export fehlgeschlagen.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
console.warn("VG-Observe GVL revision evidence export failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlRevisionEvidenceExportButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyGeneratedGvlRevisionEvidenceExport(exportContainer) {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "verify_gvl_revision_evidence_json",
|
||||||
|
payload: {
|
||||||
|
export: exportContainer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success || !result.verification) {
|
||||||
|
throw new Error(
|
||||||
|
result?.error ?? "verify_gvl_revision_evidence_json_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.verification;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadGvlRevisionEvidenceJsonExport(exportContainer) {
|
||||||
|
const json = JSON.stringify(exportContainer, null, 2);
|
||||||
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
const metadata = exportContainer?.metadata ?? {};
|
||||||
|
const vendorListVersion = metadata.vendorListVersion ?? "unknown";
|
||||||
|
const exportedAtUtcCompact =
|
||||||
|
metadata.exportedAtUtcCompact ?? formatExportTimestampUtcCompact(new Date());
|
||||||
|
|
||||||
|
downloadLink.href = url;
|
||||||
|
downloadLink.download = `GVL-REV-${vendorListVersion}-${exportedAtUtcCompact}.json`;
|
||||||
|
document.body.append(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markGvlRevisionEvidenceVaultCopy(
|
||||||
|
exportContainer,
|
||||||
|
verification = null
|
||||||
|
) {
|
||||||
|
const snapshotSha256 = exportContainer?.metadata?.snapshotSha256 ?? null;
|
||||||
|
|
||||||
|
if (!snapshotSha256) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "mark_gvl_revision_evidence_vault_copy",
|
||||||
|
payload: {
|
||||||
|
snapshotSha256,
|
||||||
|
verification
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(
|
||||||
|
result?.error ?? "mark_gvl_revision_evidence_vault_copy_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlRevisionEvidenceExportSuccessMessage(exportContainer) {
|
||||||
|
const metadata = exportContainer?.metadata ?? {};
|
||||||
|
const recordCount = getGvlRevisionEvidenceNormalizedRecordCount(
|
||||||
|
exportContainer?.normalized
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
"GVL-Revision exportiert:",
|
||||||
|
`Vendorlisten-Version ${formatNullable(metadata.vendorListVersion)}`,
|
||||||
|
`Snapshot ${shortenSha256(metadata.snapshotSha256)}`,
|
||||||
|
`Raw-GVL ${shortenSha256(metadata.rawGvlSha256)}`,
|
||||||
|
`Payload ${shortenSha256(metadata.exportPayloadSha256)}`,
|
||||||
|
`${recordCount} normalisierte Records.`
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlRevisionEvidenceNormalizedRecordCount(normalized) {
|
||||||
|
return [
|
||||||
|
"gvl_vendors",
|
||||||
|
"gvl_purposes",
|
||||||
|
"gvl_special_purposes",
|
||||||
|
"gvl_features",
|
||||||
|
"gvl_special_features",
|
||||||
|
"gvl_data_categories",
|
||||||
|
"gvl_vendor_relationships"
|
||||||
|
].reduce((total, storeName) => {
|
||||||
|
return total + (normalized?.[storeName]?.length ?? 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyGvlRevisionEvidenceJsonFile() {
|
||||||
|
const file = gvlRevisionEvidenceVerifyInput.files?.[0] ?? null;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGvlRevisionEvidenceVerifyDisabled(true);
|
||||||
|
renderGvlEvidenceTransportStatus("GVL-Revision-Export wird verifiziert...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exportContainer = JSON.parse(await file.text());
|
||||||
|
const verification = await verifyGeneratedGvlRevisionEvidenceExport(
|
||||||
|
exportContainer
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verification.valid) {
|
||||||
|
await markGvlRevisionEvidenceVaultCopy(exportContainer, verification);
|
||||||
|
await renderGvlSnapshots();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
buildGvlRevisionEvidenceVerificationMessage(verification),
|
||||||
|
verification.valid ? "success" : "error"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Export ist nicht valide.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
console.warn("VG-Observe GVL revision evidence verification failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlRevisionEvidenceVerifyInput.value = "";
|
||||||
|
setGvlRevisionEvidenceVerifyDisabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGvlRevisionEvidenceVerifyDisabled(disabled) {
|
||||||
|
const importLabel = document.querySelector(
|
||||||
|
"label[for='gvl-revision-evidence-verify-input']"
|
||||||
|
);
|
||||||
|
|
||||||
|
gvlRevisionEvidenceVerifyInput.disabled = disabled;
|
||||||
|
importLabel?.classList.toggle("is-disabled", disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importGvlRevisionEvidenceJsonFile() {
|
||||||
|
const file = gvlRevisionEvidenceImportInput.files?.[0] ?? null;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGvlRevisionEvidenceImportDisabled(true);
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Evidence wird verifiziert..."
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exportContainer = JSON.parse(await file.text());
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "import_gvl_revision_evidence_json",
|
||||||
|
payload: {
|
||||||
|
export: exportContainer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
const verification = result?.verification ?? result?.import?.verification;
|
||||||
|
const message = verification
|
||||||
|
? buildGvlRevisionEvidenceVerificationMessage(verification)
|
||||||
|
: `Fehler: ${result?.error ?? "import_gvl_revision_evidence_failed"}.`;
|
||||||
|
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
`GVL-Revision-Evidence nicht valide. ${message}`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedSnapshotSha256 = result.import?.snapshotSha256 ?? null;
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
buildGvlRevisionEvidenceImportSuccessMessage(result.import),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
await renderGvlSnapshots();
|
||||||
|
|
||||||
|
if (gvlVendorIdInput.value) {
|
||||||
|
await renderGvlVendorDetail();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Evidence ist nicht valide.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
console.warn("VG-Observe GVL revision evidence import failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlRevisionEvidenceImportInput.value = "";
|
||||||
|
setGvlRevisionEvidenceImportDisabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGvlRevisionEvidenceImportDisabled(disabled) {
|
||||||
|
const importLabel = document.querySelector(
|
||||||
|
"label[for='gvl-revision-evidence-import-input']"
|
||||||
|
);
|
||||||
|
|
||||||
|
gvlRevisionEvidenceImportInput.disabled = disabled;
|
||||||
|
importLabel?.classList.toggle("is-disabled", disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlRevisionEvidenceImportSuccessMessage(importResult) {
|
||||||
|
return [
|
||||||
|
"GVL-Revision-Evidence erfolgreich importiert.",
|
||||||
|
`Vendorlisten-Version ${formatNullable(importResult?.vendorListVersion)}.`,
|
||||||
|
`Snapshot ${shortenSha256(importResult?.snapshotSha256)}.`,
|
||||||
|
`Raw-GVL ${shortenSha256(importResult?.rawGvlSha256)}.`,
|
||||||
|
formatGvlRevisionEvidenceImportCounts(importResult?.counts ?? {})
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGvlRevisionEvidenceImportCounts(counts) {
|
||||||
|
return [
|
||||||
|
["gvl_raw_evidence", counts.gvl_raw_evidence],
|
||||||
|
["gvl_snapshots", counts.gvl_snapshots],
|
||||||
|
["gvl_vendors", counts.gvl_vendors],
|
||||||
|
["gvl_purposes", counts.gvl_purposes],
|
||||||
|
["gvl_special_purposes", counts.gvl_special_purposes],
|
||||||
|
["gvl_features", counts.gvl_features],
|
||||||
|
["gvl_special_features", counts.gvl_special_features],
|
||||||
|
["gvl_data_categories", counts.gvl_data_categories],
|
||||||
|
["gvl_vendor_relationships", counts.gvl_vendor_relationships]
|
||||||
|
]
|
||||||
|
.map(([storeName, storeCounts]) => {
|
||||||
|
return `${storeName}: importiert ${Number(
|
||||||
|
storeCounts?.inserted ?? 0
|
||||||
|
)}, übersprungen ${Number(storeCounts?.skippedExisting ?? 0)}`;
|
||||||
|
})
|
||||||
|
.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlRevisionEvidenceVerificationMessage(verification) {
|
||||||
|
const validityLabel = verification.valid ? "valide" : "nicht valide";
|
||||||
|
const counts = verification.normalizedCounts ?? {};
|
||||||
|
const normalizedRecordCount = Object.values(counts).reduce((total, count) => {
|
||||||
|
return total + Number(count ?? 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
`Verifikation: ${validityLabel}.`,
|
||||||
|
`Vendorlisten-Version ${formatNullable(verification.vendorListVersion)}.`,
|
||||||
|
`Snapshot ${shortenSha256(verification.snapshotSha256)}.`,
|
||||||
|
`Raw-GVL ${shortenSha256(verification.rawGvlSha256)}.`,
|
||||||
|
`Payload ${shortenSha256(verification.exportPayloadSha256)}.`,
|
||||||
|
`Normalisierte Records: ${normalizedRecordCount} (${formatNormalizedCounts(
|
||||||
|
counts
|
||||||
|
)}).`,
|
||||||
|
verification.valid
|
||||||
|
? ""
|
||||||
|
: `Fehler: ${(verification.errors ?? []).join(", ")}.`
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNormalizedCounts(counts) {
|
||||||
|
return [
|
||||||
|
["Vendoren", counts.gvl_vendors],
|
||||||
|
["Purposes", counts.gvl_purposes],
|
||||||
|
["Special Purposes", counts.gvl_special_purposes],
|
||||||
|
["Features", counts.gvl_features],
|
||||||
|
["Special Features", counts.gvl_special_features],
|
||||||
|
["Data Categories", counts.gvl_data_categories],
|
||||||
|
["Vendor-Beziehungen", counts.gvl_vendor_relationships]
|
||||||
|
]
|
||||||
|
.map(([label, count]) => `${label}: ${Number(count ?? 0)}`)
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
async function rebuildSelectedGvlSnapshotNormalizedData() {
|
async function rebuildSelectedGvlSnapshotNormalizedData() {
|
||||||
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
@@ -125,7 +514,7 @@ async function rebuildSelectedGvlSnapshotNormalizedData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gvlRebuildNormalizedButton.disabled = true;
|
gvlRebuildNormalizedButton.disabled = true;
|
||||||
renderRebuildStatus("Lokale Evidence wird neu normalisiert...");
|
renderRebuildStatus("Lokale Referenzdaten werden neu normalisiert...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -148,7 +537,7 @@ async function rebuildSelectedGvlSnapshotNormalizedData() {
|
|||||||
await renderGvlVendorDetail();
|
await renderGvlVendorDetail();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderRebuildStatus("Lokaler Neuaufbau fehlgeschlagen.");
|
renderRebuildStatus("Lokaler Referenz-Neuaufbau fehlgeschlagen.");
|
||||||
console.warn("VG-Observe GVL normalized rebuild failed", error);
|
console.warn("VG-Observe GVL normalized rebuild failed", error);
|
||||||
} finally {
|
} finally {
|
||||||
gvlRebuildNormalizedButton.disabled =
|
gvlRebuildNormalizedButton.disabled =
|
||||||
@@ -160,7 +549,7 @@ function buildRebuildSuccessMessage(result) {
|
|||||||
const counts = result.counts ?? {};
|
const counts = result.counts ?? {};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"Lokale GVL-Daten neu aufgebaut.",
|
"Lokale Referenzdaten neu aufgebaut.",
|
||||||
`Vendoren: ${formatCount(counts.vendorCount)}`,
|
`Vendoren: ${formatCount(counts.vendorCount)}`,
|
||||||
`Beziehungen: ${formatCount(counts.vendorRelationshipCount)}`
|
`Beziehungen: ${formatCount(counts.vendorRelationshipCount)}`
|
||||||
].join(" ");
|
].join(" ");
|
||||||
@@ -213,43 +602,292 @@ function renderGvlVendorDetailResult(detail) {
|
|||||||
const vendor = detail.vendor ?? {};
|
const vendor = detail.vendor ?? {};
|
||||||
const snapshot = detail.snapshot ?? {};
|
const snapshot = detail.snapshot ?? {};
|
||||||
const rawEvidence = detail.rawEvidence ?? {};
|
const rawEvidence = detail.rawEvidence ?? {};
|
||||||
|
const gvlInfo = detail.gvlInfo ?? {};
|
||||||
|
|
||||||
gvlVendorDetailResult.textContent = "";
|
gvlVendorDetailResult.textContent = "";
|
||||||
gvlVendorDetailResult.append(
|
gvlVendorDetailResult.append(
|
||||||
buildKeyValueSection("Normalisierte Vendor-Felder", [
|
buildVendorDetailPanel(vendor, snapshot, rawEvidence, gvlInfo)
|
||||||
["Vendor-ID", formatNullable(vendor.vendorId)],
|
);
|
||||||
["Name", formatNullable(vendor.name)],
|
}
|
||||||
["Policy URL", formatNullable(vendor.policyUrl)],
|
|
||||||
["Deleted Date", formatNullable(vendor.deletedDate)],
|
function buildVendorDetailPanel(vendor, snapshot, rawEvidence, gvlInfo) {
|
||||||
["Uses Cookies", formatNullable(vendor.usesCookies)],
|
const details = document.createElement("details");
|
||||||
["Cookie Max Age Seconds", formatNullable(vendor.cookieMaxAgeSeconds)],
|
const summary = document.createElement("summary");
|
||||||
["Uses Non-Cookie Access", formatNullable(vendor.usesNonCookieAccess)],
|
const closeButton = document.createElement("button");
|
||||||
|
|
||||||
|
details.className = "vendor-detail-panel";
|
||||||
|
details.open = true;
|
||||||
|
summary.textContent = `Vendor-Details: ${formatNullable(
|
||||||
|
vendor.name
|
||||||
|
)} (ID ${formatNullable(vendor.vendorId)})`;
|
||||||
|
|
||||||
|
closeButton.type = "button";
|
||||||
|
closeButton.className = "secondary-action";
|
||||||
|
closeButton.textContent = "Vendor-Details schließen";
|
||||||
|
closeButton.addEventListener("click", () => {
|
||||||
|
details.open = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
details.append(
|
||||||
|
summary,
|
||||||
|
buildHumanReadableVendorCard(vendor),
|
||||||
|
buildGvlSubjectMatterDetails(gvlInfo),
|
||||||
|
buildTechnicalEvidenceDetails(vendor, snapshot, rawEvidence),
|
||||||
|
closeButton
|
||||||
|
);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHumanReadableVendorCard(vendor) {
|
||||||
|
const rawVendor = vendor.rawVendor ?? {};
|
||||||
|
const urlRows = getVendorUrlRows(rawVendor, vendor);
|
||||||
|
|
||||||
|
return buildKeyValueSection("Vendor-Karte", [
|
||||||
|
["Vendor-Name", formatNullable(vendor.name ?? rawVendor.name)],
|
||||||
|
["Vendor-ID", formatNullable(vendor.vendorId ?? rawVendor.id)],
|
||||||
|
["Status", formatVendorStatus(vendor.deletedDate ?? rawVendor.deletedDate)],
|
||||||
|
["Privacy-/Datenschutz-URLs", formatMultilineValue(urlRows.privacy)],
|
||||||
[
|
[
|
||||||
"Device Storage Disclosure URL",
|
"Legitimate-Interest-Claim-URLs",
|
||||||
formatNullable(vendor.deviceStorageDisclosureUrl)
|
formatMultilineValue(urlRows.legitimateInterest)
|
||||||
],
|
],
|
||||||
["Domains", formatJsonValue(vendor.domains)],
|
[
|
||||||
["Snapshot SHA256", formatNullable(vendor.snapshotSha256)]
|
"Device-Storage-Disclosure-URL",
|
||||||
]),
|
formatNullable(
|
||||||
buildKeyValueSection("Snapshot-Herkunft", [
|
vendor.deviceStorageDisclosureUrl ??
|
||||||
|
rawVendor.deviceStorageDisclosureUrl
|
||||||
|
)
|
||||||
|
],
|
||||||
|
["Nutzt Cookies", formatBooleanGerman(vendor.usesCookies)],
|
||||||
|
[
|
||||||
|
"Cookie Refresh",
|
||||||
|
formatBooleanGerman(rawVendor.cookieRefresh ?? vendor.cookieRefresh)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Cookie Max Age",
|
||||||
|
formatCookieMaxAge(vendor.cookieMaxAgeSeconds)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Non-Cookie Access",
|
||||||
|
formatBooleanGerman(vendor.usesNonCookieAccess)
|
||||||
|
],
|
||||||
|
["Domains", formatArrayValue(vendor.domains ?? rawVendor.domains)]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVendorUrlRows(rawVendor, vendor) {
|
||||||
|
const urls = Array.isArray(rawVendor.urls) ? rawVendor.urls : [];
|
||||||
|
const privacy = [];
|
||||||
|
const legitimateInterest = [];
|
||||||
|
|
||||||
|
urls.forEach((urlEntry) => {
|
||||||
|
if (!urlEntry || typeof urlEntry !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = urlEntry.langId ? `${urlEntry.langId}: ` : "";
|
||||||
|
|
||||||
|
appendUrlIfPresent(privacy, `${label}${urlEntry.privacy}`);
|
||||||
|
appendUrlIfPresent(
|
||||||
|
legitimateInterest,
|
||||||
|
`${label}${urlEntry.legIntClaim}`
|
||||||
|
);
|
||||||
|
appendUrlIfPresent(
|
||||||
|
legitimateInterest,
|
||||||
|
`${label}${urlEntry.legitimateInterestClaim}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
appendUrlIfPresent(privacy, vendor.policyUrl ?? rawVendor.policyUrl);
|
||||||
|
appendUrlIfPresent(
|
||||||
|
legitimateInterest,
|
||||||
|
vendor.legitimateInterestDisclosureUrl ??
|
||||||
|
rawVendor.legitimateInterestDisclosureUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
privacy: uniqueValues(privacy),
|
||||||
|
legitimateInterest: uniqueValues(legitimateInterest)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendUrlIfPresent(values, value) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlValue = String(value);
|
||||||
|
|
||||||
|
if (urlValue.endsWith("undefined") || urlValue.endsWith("null")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(urlValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueValues(values) {
|
||||||
|
return Array.from(new Set(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlSubjectMatterDetails(gvlInfo) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
|
||||||
|
details.className = "subject-details";
|
||||||
|
details.open = true;
|
||||||
|
summary.textContent = "GVL-Fachinformationen";
|
||||||
|
details.append(
|
||||||
|
summary,
|
||||||
|
buildCatalogListSection("Purposes", "Purpose", gvlInfo.purposes),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Legitimate-Interest-Purposes",
|
||||||
|
"Purpose",
|
||||||
|
gvlInfo.legIntPurposes
|
||||||
|
),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Flexible Purposes",
|
||||||
|
"Purpose",
|
||||||
|
gvlInfo.flexiblePurposes
|
||||||
|
),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Special Purposes",
|
||||||
|
"Special Purpose",
|
||||||
|
gvlInfo.specialPurposes
|
||||||
|
),
|
||||||
|
buildCatalogListSection("Features", "Feature", gvlInfo.features),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Special Features",
|
||||||
|
"Special Feature",
|
||||||
|
gvlInfo.specialFeatures
|
||||||
|
),
|
||||||
|
buildDataDeclarationSection(gvlInfo),
|
||||||
|
buildJsonDetails("Data Retention", gvlInfo.dataRetention),
|
||||||
|
buildJsonDetails("Overflow", gvlInfo.overflow)
|
||||||
|
);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCatalogListSection(title, entityLabel, items) {
|
||||||
|
const section = document.createElement("section");
|
||||||
|
const heading = document.createElement("h4");
|
||||||
|
const list = document.createElement("div");
|
||||||
|
const catalogItems = Array.isArray(items) ? items : [];
|
||||||
|
|
||||||
|
section.className = "gvl-catalog-section";
|
||||||
|
heading.textContent = title;
|
||||||
|
list.className = "catalog-list";
|
||||||
|
section.append(heading);
|
||||||
|
|
||||||
|
if (!catalogItems.length) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
|
||||||
|
empty.className = "muted-text";
|
||||||
|
empty.textContent = "Keine Angaben vorhanden.";
|
||||||
|
section.append(empty);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogItems.forEach((item) => {
|
||||||
|
list.append(buildCatalogItemDetails(entityLabel, item));
|
||||||
|
});
|
||||||
|
section.append(list);
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCatalogItemDetails(entityLabel, item) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
const rows = [
|
||||||
|
["ID", formatNullable(item?.id)],
|
||||||
|
["Name", formatNullable(item?.name)],
|
||||||
|
["Description", formatNullable(item?.description)],
|
||||||
|
["Description Legal", formatNullable(item?.descriptionLegal)],
|
||||||
|
["Illustrations", formatJsonValue(item?.illustrations)]
|
||||||
|
];
|
||||||
|
|
||||||
|
details.className = "catalog-item";
|
||||||
|
summary.textContent = formatCatalogItemSummary(entityLabel, item);
|
||||||
|
details.append(summary, buildDefinitionList(rows));
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCatalogItemSummary(entityLabel, item) {
|
||||||
|
const prefix = `${entityLabel} ${formatNullable(item?.id)}`;
|
||||||
|
|
||||||
|
if (!item?.name) {
|
||||||
|
return `${prefix} - Klartext lokal nicht verfügbar`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix} - ${item.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDataDeclarationSection(gvlInfo) {
|
||||||
|
const section = buildCatalogListSection(
|
||||||
|
"Data Declaration",
|
||||||
|
"Data Category",
|
||||||
|
gvlInfo.dataDeclaration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gvlInfo.dataCategoriesTextAvailable) {
|
||||||
|
const note = document.createElement("p");
|
||||||
|
|
||||||
|
note.className = "muted-text";
|
||||||
|
note.textContent = "Klartext derzeit nicht normalisiert verfügbar.";
|
||||||
|
section.prepend(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTechnicalEvidenceDetails(vendor, snapshot, rawEvidence) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
|
||||||
|
details.className = "technical-details";
|
||||||
|
summary.textContent = "Technischer Evidence-Nachweis";
|
||||||
|
details.append(
|
||||||
|
summary,
|
||||||
|
buildKeyValueSection("Snapshot und Raw-GVL", [
|
||||||
["Snapshot SHA256", formatNullable(snapshot.snapshotSha256)],
|
["Snapshot SHA256", formatNullable(snapshot.snapshotSha256)],
|
||||||
["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)],
|
["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)],
|
||||||
["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)],
|
["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)],
|
||||||
["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)],
|
["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)],
|
||||||
["Abrufzeitpunkt", formatNullable(snapshot.fetchedAt)],
|
["Fetched At", formatNullable(snapshot.fetchedAt)],
|
||||||
["Snapshot erstellt", formatNullable(snapshot.createdAt)]
|
["Snapshot createdAt", formatNullable(snapshot.createdAt)]
|
||||||
]),
|
]),
|
||||||
buildKeyValueSection("Raw-GVL-Evidence", [
|
buildKeyValueSection("Raw-GVL-Evidence", [
|
||||||
["Raw-GVL SHA256", formatNullable(rawEvidence.rawGvlSha256)],
|
["Source URL", formatNullable(rawEvidence.sourceUrl)],
|
||||||
["Quelle", formatNullable(rawEvidence.sourceUrl)],
|
["Fetched At", formatNullable(rawEvidence.fetchedAt)],
|
||||||
["Abrufzeitpunkt", formatNullable(rawEvidence.fetchedAt)],
|
|
||||||
["HTTP Status", formatNullable(rawEvidence.httpStatus)],
|
["HTTP Status", formatNullable(rawEvidence.httpStatus)],
|
||||||
["Content-Type", formatNullable(rawEvidence.contentType)],
|
["Content-Type", formatNullable(rawEvidence.contentType)],
|
||||||
["Raw Body vorhanden", formatNullable(rawEvidence.hasRawBody)]
|
["hasRawBody", formatNullable(rawEvidence.hasRawBody)]
|
||||||
]),
|
]),
|
||||||
buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor),
|
buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor),
|
||||||
buildJsonDetails("Vollständiger gvl_vendors-Datensatz", vendor)
|
buildJsonDetails("Vollständiger gvl_vendors-Datensatz", vendor)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefinitionList(rows) {
|
||||||
|
const list = document.createElement("dl");
|
||||||
|
|
||||||
|
list.className = "definition-list";
|
||||||
|
|
||||||
|
rows.forEach(([label, value]) => {
|
||||||
|
const term = document.createElement("dt");
|
||||||
|
const description = document.createElement("dd");
|
||||||
|
|
||||||
|
term.textContent = label;
|
||||||
|
description.textContent = value;
|
||||||
|
list.append(term, description);
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildKeyValueSection(title, rows) {
|
function buildKeyValueSection(title, rows) {
|
||||||
@@ -293,6 +931,79 @@ function buildJsonDetails(title, value) {
|
|||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatVendorStatus(deletedDate) {
|
||||||
|
if (deletedDate) {
|
||||||
|
return `Gelöscht seit ${deletedDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Aktiv";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBooleanGerman(value) {
|
||||||
|
if (value === true) {
|
||||||
|
return "ja";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === false) {
|
||||||
|
return "nein";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCookieMaxAge(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isFinite(seconds)) {
|
||||||
|
return formatNullable(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanReadable = formatDurationFromSeconds(seconds);
|
||||||
|
|
||||||
|
return humanReadable
|
||||||
|
? `${seconds} Sekunden (${humanReadable})`
|
||||||
|
: `${seconds} Sekunden`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDurationFromSeconds(seconds) {
|
||||||
|
const units = [
|
||||||
|
["Jahr", "Jahre", 365 * 24 * 60 * 60],
|
||||||
|
["Tag", "Tage", 24 * 60 * 60],
|
||||||
|
["Stunde", "Stunden", 60 * 60],
|
||||||
|
["Minute", "Minuten", 60]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [singular, plural, unitSeconds] of units) {
|
||||||
|
if (seconds >= unitSeconds && seconds % unitSeconds === 0) {
|
||||||
|
const amount = seconds / unitSeconds;
|
||||||
|
|
||||||
|
return `${amount} ${amount === 1 ? singular : plural}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatArrayValue(value) {
|
||||||
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMultilineValue(values) {
|
||||||
|
if (!Array.isArray(values) || !values.length) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async function renderGvlSnapshots() {
|
async function renderGvlSnapshots() {
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -323,7 +1034,7 @@ async function renderGvlSnapshots() {
|
|||||||
gvlSnapshotEmpty.hidden = false;
|
gvlSnapshotEmpty.hidden = false;
|
||||||
gvlSnapshotContent.hidden = true;
|
gvlSnapshotContent.hidden = true;
|
||||||
gvlSnapshotEmpty.textContent =
|
gvlSnapshotEmpty.textContent =
|
||||||
"Gespeicherte Vendorlisten konnten nicht geladen werden.";
|
"Gespeicherte GVL-Referenzen konnten nicht geladen werden.";
|
||||||
console.warn("VG-Observe GVL snapshot list failed", error);
|
console.warn("VG-Observe GVL snapshot list failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +1046,7 @@ function renderNoGvlSnapshots() {
|
|||||||
gvlSnapshotEmpty.hidden = false;
|
gvlSnapshotEmpty.hidden = false;
|
||||||
gvlSnapshotContent.hidden = true;
|
gvlSnapshotContent.hidden = true;
|
||||||
gvlSnapshotEmpty.textContent =
|
gvlSnapshotEmpty.textContent =
|
||||||
"Keine gespeicherten offiziellen Vendorlisten vorhanden.";
|
"Keine gespeicherten GVL-Referenzrevisionen vorhanden.";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGvlSnapshotList() {
|
function renderGvlSnapshotList() {
|
||||||
@@ -362,6 +1073,9 @@ function renderGvlSnapshotList() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
appendListCell(row, formatNullable(snapshot?.vendorListVersion), "numeric");
|
appendListCell(row, formatNullable(snapshot?.vendorListVersion), "numeric");
|
||||||
|
appendListCell(row, formatGvlProvenanceMarker(snapshot?.provenance));
|
||||||
|
appendListCell(row, formatGvlVaultMarker(snapshot?.vaultCopyAvailable));
|
||||||
|
appendListCell(row, formatGvlProtectionMarker(snapshot));
|
||||||
appendListCell(row, formatNullable(snapshot?.fetchedAt));
|
appendListCell(row, formatNullable(snapshot?.fetchedAt));
|
||||||
appendListCell(row, shortenSha256(snapshot?.sha256), "sha-cell");
|
appendListCell(row, shortenSha256(snapshot?.sha256), "sha-cell");
|
||||||
appendListCell(row, formatNullable(snapshot?.sourceUrl), "url-cell");
|
appendListCell(row, formatNullable(snapshot?.sourceUrl), "url-cell");
|
||||||
@@ -419,7 +1133,7 @@ async function renderSelectedGvlSnapshotSummary() {
|
|||||||
selectedSnapshotSummary = null;
|
selectedSnapshotSummary = null;
|
||||||
updateRebuildActionState(null);
|
updateRebuildActionState(null);
|
||||||
gvlSnapshotSummary.textContent =
|
gvlSnapshotSummary.textContent =
|
||||||
"Zusammenfassung dieser Vendorliste konnte nicht geladen werden.";
|
"Zusammenfassung dieser GVL-Revision konnte nicht geladen werden.";
|
||||||
console.warn("VG-Observe GVL snapshot summary failed", error);
|
console.warn("VG-Observe GVL snapshot summary failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,7 +1165,7 @@ async function renderVendorOverviewForSelectedSnapshot() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
gvlVendorOverviewEmpty.hidden = false;
|
gvlVendorOverviewEmpty.hidden = false;
|
||||||
gvlVendorOverviewEmpty.textContent =
|
gvlVendorOverviewEmpty.textContent =
|
||||||
"Vendoren-Übersicht konnte nicht geladen werden.";
|
"Vendoren-Referenz konnte nicht geladen werden.";
|
||||||
console.warn("VG-Observe GVL vendor overview failed", error);
|
console.warn("VG-Observe GVL vendor overview failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,7 +1177,7 @@ function renderVendorOverview() {
|
|||||||
gvlVendorOverviewEmpty.hidden = false;
|
gvlVendorOverviewEmpty.hidden = false;
|
||||||
gvlVendorOverviewContent.hidden = true;
|
gvlVendorOverviewContent.hidden = true;
|
||||||
gvlVendorOverviewEmpty.textContent =
|
gvlVendorOverviewEmpty.textContent =
|
||||||
"Keine normalisierten Vendoren für diese Vendorliste vorhanden.";
|
"Keine normalisierten Vendor-Referenzen für diese GVL-Revision vorhanden.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,8 +1228,8 @@ function updateVendorOverviewSummary() {
|
|||||||
const count = selectedSnapshotVendors.length;
|
const count = selectedSnapshotVendors.length;
|
||||||
|
|
||||||
gvlVendorOverviewSummary.textContent = count
|
gvlVendorOverviewSummary.textContent = count
|
||||||
? `Vendoren-Übersicht anzeigen (${count})`
|
? `Vendoren-Referenz anzeigen (${count})`
|
||||||
: "Vendoren-Übersicht anzeigen";
|
: "Vendoren-Referenz anzeigen";
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRebuildActionState(summary) {
|
function updateRebuildActionState(summary) {
|
||||||
@@ -529,11 +1243,11 @@ function updateRebuildActionState(summary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsRebuild) {
|
if (needsRebuild) {
|
||||||
renderRebuildStatus("Reparatur möglich: normalisierte lokale Daten fehlen.");
|
renderRebuildStatus("Reparatur möglich: normalisierte Referenzdaten fehlen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRebuildStatus("Normalisierte lokale GVL-Daten sind verfügbar.");
|
renderRebuildStatus("Normalisierte lokale Referenzdaten sind verfügbar.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function doesSnapshotNeedNormalizedRebuild(summary) {
|
function doesSnapshotNeedNormalizedRebuild(summary) {
|
||||||
@@ -559,6 +1273,9 @@ function renderSummaryTable(summary) {
|
|||||||
const body = document.createElement("tbody");
|
const body = document.createElement("tbody");
|
||||||
const rows = [
|
const rows = [
|
||||||
["Vendorlisten-Version", formatNullable(summary.vendorListVersion)],
|
["Vendorlisten-Version", formatNullable(summary.vendorListVersion)],
|
||||||
|
["Herkunft", formatGvlProvenanceMarker(summary.provenance)],
|
||||||
|
["Vault-Kopie", formatGvlVaultMarker(summary.vaultCopyAvailable)],
|
||||||
|
["Workspace-Schutz", formatGvlProtectionMarker(summary)],
|
||||||
["Abrufzeitpunkt", formatNullable(summary.fetchedAt)],
|
["Abrufzeitpunkt", formatNullable(summary.fetchedAt)],
|
||||||
["Quelle", formatNullable(summary.sourceUrl)],
|
["Quelle", formatNullable(summary.sourceUrl)],
|
||||||
["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)],
|
["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)],
|
||||||
@@ -661,3 +1378,38 @@ function shortenSha256(value) {
|
|||||||
|
|
||||||
return `${String(value).slice(0, 12)}...`;
|
return `${String(value).slice(0, 12)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatGvlProvenanceMarker(provenance) {
|
||||||
|
if (provenance === "web+vault") {
|
||||||
|
return "🌐💾";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provenance === "vault") {
|
||||||
|
return "💾";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "🌐";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGvlVaultMarker(vaultCopyAvailable) {
|
||||||
|
return vaultCopyAvailable ? "📦" : "❌";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGvlProtectionMarker(snapshot) {
|
||||||
|
return snapshot?.workspaceDeleteAllowed ? "🔓" : "🔒";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatExportTimestampUtcCompact(date) {
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
const month = padDatePart(date.getUTCMonth() + 1);
|
||||||
|
const day = padDatePart(date.getUTCDate());
|
||||||
|
const hours = padDatePart(date.getUTCHours());
|
||||||
|
const minutes = padDatePart(date.getUTCMinutes());
|
||||||
|
const seconds = padDatePart(date.getUTCSeconds());
|
||||||
|
|
||||||
|
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function padDatePart(value) {
|
||||||
|
return String(value).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
function cloneSerializable(value, seen) {
|
function cloneSerializable(value, seen) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
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 = {};
|
const result = {};
|
||||||
|
|
||||||
Object.keys(value).forEach(function (key) {
|
Object.keys(value).forEach(function (key) {
|
||||||
@@ -75,31 +71,8 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.__tcfapi("ping", 2, function (pingData, pingSuccess) {
|
function buildTcfEventCapture(tcData) {
|
||||||
|
return {
|
||||||
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 = {
|
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
|
|
||||||
@@ -133,8 +106,64 @@
|
|||||||
|
|
||||||
addtlConsent: tcData.addtlConsent,
|
addtlConsent: tcData.addtlConsent,
|
||||||
|
|
||||||
|
rawTcString: {
|
||||||
|
tcString: tcData.tcString,
|
||||||
|
addtlConsent: tcData.addtlConsent
|
||||||
|
},
|
||||||
|
|
||||||
rawTcData: cloneSerializable(tcData)
|
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);
|
console.log("VendorGet CONSENT CAPTURE:", capture);
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,42 @@ h1 {
|
|||||||
display: none;
|
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 {
|
.evidence-counts {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -176,6 +212,11 @@ button:disabled {
|
|||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondary-workflow {
|
||||||
|
color: #cbd5e1;
|
||||||
|
background: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-modal {
|
.confirm-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="popup.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="popup">
|
<main class="popup">
|
||||||
<h1>VendorGet-IV</h1>
|
<h1>VG-Observe</h1>
|
||||||
|
|
||||||
<section class="status" aria-label="Status">
|
<section class="status" aria-label="Status">
|
||||||
<div class="status-row">
|
<div class="status-row">
|
||||||
@@ -29,10 +29,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="evidence-retention" aria-label="Evidenzdaten">
|
<section
|
||||||
<h2>Evidenzdaten</h2>
|
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>
|
<div id="maintenance-warning" class="maintenance-warning" hidden>
|
||||||
Aufzeichnung pausiert: Dashboard geöffnet
|
Aufzeichnung pausiert: Observe-Dashboard geöffnet
|
||||||
</div>
|
</div>
|
||||||
<dl class="evidence-counts">
|
<dl class="evidence-counts">
|
||||||
<div>
|
<div>
|
||||||
@@ -61,15 +86,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<button id="evidence-dashboard-button" type="button">
|
<button id="evidence-dashboard-button" type="button">
|
||||||
Evidence Dashboard öffnen
|
Observe-Dashboard öffnen
|
||||||
</button>
|
</button>
|
||||||
<button id="evidence-export-json-button" type="button">
|
<button
|
||||||
|
id="evidence-export-json-button"
|
||||||
|
class="secondary-workflow"
|
||||||
|
type="button"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
Export Evidence JSON
|
Export Evidence JSON
|
||||||
</button>
|
</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
|
Ungesperrte Evidence-Daten löschen
|
||||||
</button>
|
</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">
|
<div id="evidence-retention-status" class="retention-status" aria-live="polite">
|
||||||
Status wird geladen
|
Status wird geladen
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+160
-1
@@ -8,6 +8,19 @@ const requestMonitoringStatus = document.getElementById(
|
|||||||
);
|
);
|
||||||
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
|
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
|
||||||
const consentCaptureStatus = document.getElementById("consent-capture-status");
|
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 maintenanceWarning = document.getElementById("maintenance-warning");
|
||||||
const evidenceLockedCount = document.getElementById("evidence-locked-count");
|
const evidenceLockedCount = document.getElementById("evidence-locked-count");
|
||||||
const evidenceDashboardButton = document.getElementById(
|
const evidenceDashboardButton = document.getElementById(
|
||||||
@@ -47,6 +60,7 @@ const evidenceStoreCountCells = {
|
|||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
await renderSettings();
|
await renderSettings();
|
||||||
|
await renderPreConsentCaptureStatus();
|
||||||
await renderEvidenceMaintenanceStatus();
|
await renderEvidenceMaintenanceStatus();
|
||||||
await renderEvidenceRetentionStatus();
|
await renderEvidenceRetentionStatus();
|
||||||
|
|
||||||
@@ -57,7 +71,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
"consentCaptureEnabled",
|
"consentCaptureEnabled",
|
||||||
consentCaptureToggle.checked
|
consentCaptureToggle.checked
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (consentCaptureToggle.checked) {
|
||||||
|
await probePreConsentCaptureForActiveTab();
|
||||||
|
}
|
||||||
|
|
||||||
await renderSettings();
|
await renderSettings();
|
||||||
|
await renderPreConsentCaptureStatus();
|
||||||
|
|
||||||
consentCaptureToggle.disabled = false;
|
consentCaptureToggle.disabled = false;
|
||||||
});
|
});
|
||||||
@@ -84,6 +104,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
evidencePurgeUnlockedButton.addEventListener("click", openPurgeConfirmModal);
|
evidencePurgeUnlockedButton.addEventListener("click", openPurgeConfirmModal);
|
||||||
evidencePurgeCancelButton.addEventListener("click", closePurgeConfirmModal);
|
evidencePurgeCancelButton.addEventListener("click", closePurgeConfirmModal);
|
||||||
evidencePurgeConfirmButton.addEventListener("click", purgeUnlockedEvidence);
|
evidencePurgeConfirmButton.addEventListener("click", purgeUnlockedEvidence);
|
||||||
|
preConsentCaptureStart.addEventListener("click", startPreConsentCapture);
|
||||||
|
preConsentCaptureDecline.addEventListener("click", declinePreConsentCapture);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderSettings() {
|
async function renderSettings() {
|
||||||
@@ -158,6 +180,137 @@ function renderEvidenceRetentionMessage(message) {
|
|||||||
evidenceRetentionStatus.textContent = 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() {
|
function openPurgeConfirmModal() {
|
||||||
evidencePurgeConfirmModal.hidden = false;
|
evidencePurgeConfirmModal.hidden = false;
|
||||||
evidencePurgeCancelButton.focus();
|
evidencePurgeCancelButton.focus();
|
||||||
@@ -194,7 +347,13 @@ async function purgeUnlockedEvidence() {
|
|||||||
|
|
||||||
function buildPurgeUnlockedSuccessMessage(result) {
|
function buildPurgeUnlockedSuccessMessage(result) {
|
||||||
if (Number.isFinite(result.deletedCount)) {
|
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";
|
return "Ungesperrte Evidence-Daten gelöscht";
|
||||||
|
|||||||
@@ -3,16 +3,18 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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">
|
<link rel="stylesheet" href="request-explorer.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="explorer">
|
<main class="explorer">
|
||||||
<header class="explorer-header">
|
<header class="explorer-header">
|
||||||
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Observe-Dashboard</a>
|
||||||
<h1>Request-Explorer</h1>
|
<h1>Requests prüfen</h1>
|
||||||
<p class="section-help">
|
<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>
|
</p>
|
||||||
<dl class="request-overview" aria-label="Geladene Request-Uebersicht">
|
<dl class="request-overview" aria-label="Geladene Request-Uebersicht">
|
||||||
<div>
|
<div>
|
||||||
@@ -27,7 +29,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="request-list-title">
|
<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>
|
<p id="request-empty" class="empty-state" hidden>
|
||||||
Keine beobachteten Requests vorhanden.
|
Keine beobachteten Requests vorhanden.
|
||||||
</p>
|
</p>
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
<div id="request-groups" class="request-groups"></div>
|
<div id="request-groups" class="request-groups"></div>
|
||||||
|
|
||||||
<section class="request-detail" aria-labelledby="request-detail-title">
|
<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>
|
<div id="request-detail-summary"></div>
|
||||||
</section>
|
</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