"use strict"; const gvlSnapshotEmpty = document.getElementById("gvl-snapshot-empty"); const gvlSnapshotContent = document.getElementById("gvl-snapshot-content"); const gvlSnapshotList = document.getElementById("gvl-snapshot-list"); const gvlSnapshotSummary = document.getElementById("gvl-snapshot-summary"); const gvlTechnicalFields = document.getElementById("gvl-technical-fields"); const gvlDebugData = document.getElementById("gvl-debug-data"); const gvlFetchOfficialButton = document.getElementById( "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 gvlRebuildNormalizedButton = document.getElementById( "gvl-rebuild-normalized-button" ); const gvlRebuildNormalizedStatus = document.getElementById( "gvl-rebuild-normalized-status" ); const gvlVendorOverviewEmpty = document.getElementById( "gvl-vendor-overview-empty" ); const gvlVendorOverviewDetails = document.getElementById( "gvl-vendor-overview-details" ); const gvlVendorOverviewSummary = document.getElementById( "gvl-vendor-overview-summary" ); const gvlVendorOverviewContent = document.getElementById( "gvl-vendor-overview-content" ); const gvlVendorOverviewList = document.getElementById( "gvl-vendor-overview-list" ); const gvlVendorDetailForm = document.getElementById("gvl-vendor-detail-form"); const gvlVendorIdInput = document.getElementById("gvl-vendor-id-input"); const gvlVendorDetailButton = document.getElementById( "gvl-vendor-detail-button" ); const gvlVendorDetailStatus = document.getElementById( "gvl-vendor-detail-status" ); const gvlVendorDetailResult = document.getElementById( "gvl-vendor-detail-result" ); let gvlSnapshots = []; let selectedSnapshotSha256 = null; let selectedSnapshotSummary = null; let selectedSnapshotVendors = []; document.addEventListener("DOMContentLoaded", async () => { gvlFetchOfficialButton.addEventListener("click", async () => { await fetchOfficialGvl(); }); gvlRevisionEvidenceExportButton.addEventListener("click", async () => { await exportSelectedGvlRevisionEvidenceJsonFile(); }); gvlRevisionEvidenceVerifyInput.addEventListener("change", async () => { await verifyGvlRevisionEvidenceJsonFile(); }); gvlRevisionEvidenceImportInput.addEventListener("change", async () => { await importGvlRevisionEvidenceJsonFile(); }); gvlRebuildNormalizedButton.addEventListener("click", async () => { await rebuildSelectedGvlSnapshotNormalizedData(); }); gvlVendorDetailForm.addEventListener("submit", async (event) => { event.preventDefault(); await renderGvlVendorDetail(); }); await renderGvlSnapshots(); }); async function fetchOfficialGvl() { gvlFetchOfficialButton.disabled = true; renderFetchStatus("GVL wird aus Web geladen..."); try { const result = await browser.runtime.sendMessage({ type: "fetch_official_gvl" }); if (result?.error === "gvl_revision_evidence_conflict") { renderFetchStatus(buildGvlEvidenceConflictMessage(result)); return; } if (!result?.success) { throw new Error(result?.error ?? "official_gvl_fetch_failed"); } renderFetchStatus(buildGvlSyncStatusMessage(result)); await renderGvlSnapshots(); await renderSelectedGvlSnapshotSummary(); } catch (error) { renderFetchStatus("GVL-Referenz aus Web konnte nicht geladen werden."); console.warn("VG-Observe manual official GVL fetch failed", error); } finally { 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) { if (result?.syncStatus === "new_gvl_revision_stored_and_normalized") { return "GVL-Referenz aus Web geladen, neue Revision gespeichert und normalisiert."; } if (result?.syncStatus === "known_gvl_rebuilt_from_local_evidence") { return "GVL-Referenz aus Web geprüft; bekannte Revision aus lokaler Evidence neu aufgebaut."; } if (result?.syncStatus === "current_and_locally_available") { return "GVL-Referenz aus Web geprüft; aktuelle Revision ist lokal vollständig verfügbar."; } return result?.alreadyKnown ? "GVL-Referenz aus Web geprüft; Revision ist lokal verfügbar." : "GVL-Referenz aus Web geladen."; } function renderFetchStatus(message) { gvlFetchStatus.textContent = message; } function renderGvlEvidenceTransportStatus(message, statusKind = "neutral") { gvlEvidenceTransportStatus.textContent = message; gvlEvidenceTransportStatus.className = [ "fetch-status", "evidence-status", `is-${statusKind}` ].join(" "); } function renderRebuildStatus(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); 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) { const snapshotSha256 = exportContainer?.metadata?.snapshotSha256 ?? null; if (!snapshotSha256) { return; } const result = await browser.runtime.sendMessage({ type: "mark_gvl_revision_evidence_vault_copy", payload: { snapshotSha256 } }); 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 ); 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() { const snapshot = findGvlSnapshot(selectedSnapshotSha256); if (!snapshot) { renderRebuildStatus("Keine Vendorliste ausgewählt."); return; } gvlRebuildNormalizedButton.disabled = true; renderRebuildStatus("Lokale Referenzdaten werden neu normalisiert..."); try { const result = await browser.runtime.sendMessage({ type: "rebuild_gvl_snapshot_normalized_data", payload: { sha256: snapshot.sha256 } }); if (!result?.success) { throw new Error( result?.error ?? "rebuild_gvl_snapshot_normalized_data_failed" ); } await renderSelectedGvlSnapshotSummary(); renderRebuildStatus(buildRebuildSuccessMessage(result)); if (gvlVendorIdInput.value) { await renderGvlVendorDetail(); } } catch (error) { renderRebuildStatus("Lokaler Referenz-Neuaufbau fehlgeschlagen."); console.warn("VG-Observe GVL normalized rebuild failed", error); } finally { gvlRebuildNormalizedButton.disabled = !doesSnapshotNeedNormalizedRebuild(selectedSnapshotSummary); } } function buildRebuildSuccessMessage(result) { const counts = result.counts ?? {}; return [ "Lokale Referenzdaten neu aufgebaut.", `Vendoren: ${formatCount(counts.vendorCount)}`, `Beziehungen: ${formatCount(counts.vendorRelationshipCount)}` ].join(" "); } async function renderGvlVendorDetail() { const vendorId = Number(gvlVendorIdInput.value); clearGvlVendorDetail(); if (!Number.isInteger(vendorId) || vendorId <= 0) { renderGvlVendorDetailStatus("Bitte eine gültige Vendor-ID eingeben."); return; } gvlVendorDetailButton.disabled = true; renderGvlVendorDetailStatus("Vendor wird geladen..."); try { const result = await browser.runtime.sendMessage({ type: "get_gvl_vendor_detail", payload: { vendorId } }); if (!result?.success) { throw new Error(result?.error ?? "get_gvl_vendor_detail_failed"); } renderGvlVendorDetailStatus("Vendor geladen."); renderGvlVendorDetailResult(result.vendorDetail ?? {}); } catch (error) { renderGvlVendorDetailStatus("Vendor konnte nicht geladen werden."); console.warn("VG-Observe GVL vendor detail failed", error); } finally { gvlVendorDetailButton.disabled = false; } } function clearGvlVendorDetail() { gvlVendorDetailResult.textContent = ""; } function renderGvlVendorDetailStatus(message) { gvlVendorDetailStatus.textContent = message; } function renderGvlVendorDetailResult(detail) { const vendor = detail.vendor ?? {}; const snapshot = detail.snapshot ?? {}; const rawEvidence = detail.rawEvidence ?? {}; const gvlInfo = detail.gvlInfo ?? {}; gvlVendorDetailResult.textContent = ""; gvlVendorDetailResult.append( buildVendorDetailPanel(vendor, snapshot, rawEvidence, gvlInfo) ); } function buildVendorDetailPanel(vendor, snapshot, rawEvidence, gvlInfo) { const details = document.createElement("details"); const summary = document.createElement("summary"); 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)], [ "Legitimate-Interest-Claim-URLs", formatMultilineValue(urlRows.legitimateInterest) ], [ "Device-Storage-Disclosure-URL", formatNullable( 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)], ["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)], ["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)], ["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)], ["Fetched At", formatNullable(snapshot.fetchedAt)], ["Snapshot createdAt", formatNullable(snapshot.createdAt)] ]), buildKeyValueSection("Raw-GVL-Evidence", [ ["Source URL", formatNullable(rawEvidence.sourceUrl)], ["Fetched At", formatNullable(rawEvidence.fetchedAt)], ["HTTP Status", formatNullable(rawEvidence.httpStatus)], ["Content-Type", formatNullable(rawEvidence.contentType)], ["hasRawBody", formatNullable(rawEvidence.hasRawBody)] ]), buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor), 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) { const section = document.createElement("section"); const heading = document.createElement("h3"); const table = document.createElement("table"); const body = document.createElement("tbody"); section.className = "vendor-detail-section"; heading.textContent = title; table.className = "summary-table"; for (const [label, value] of rows) { const row = document.createElement("tr"); const labelCell = document.createElement("th"); const valueCell = document.createElement("td"); labelCell.scope = "row"; labelCell.textContent = label; valueCell.textContent = value; row.append(labelCell, valueCell); body.append(row); } table.append(body); section.append(heading, table); return section; } function buildJsonDetails(title, value) { const details = document.createElement("details"); const summary = document.createElement("summary"); const valuePre = document.createElement("pre"); details.className = "technical-details"; summary.textContent = title; valuePre.textContent = JSON.stringify(value ?? null, null, 2); details.append(summary, valuePre); 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() { try { const result = await browser.runtime.sendMessage({ type: "list_gvl_snapshots" }); if (!result?.success) { throw new Error(result?.error ?? "list_gvl_snapshots_failed"); } gvlSnapshots = result.gvlSnapshots ?? []; if (gvlSnapshots.length === 0) { renderNoGvlSnapshots(); return; } gvlSnapshotEmpty.hidden = true; gvlSnapshotContent.hidden = false; if (!findGvlSnapshot(selectedSnapshotSha256)) { selectedSnapshotSha256 = gvlSnapshots[0]?.sha256 ?? null; } renderGvlSnapshotList(); await renderSelectedGvlSnapshotSummary(); } catch (error) { gvlSnapshotEmpty.hidden = false; gvlSnapshotContent.hidden = true; gvlSnapshotEmpty.textContent = "Gespeicherte GVL-Referenzen konnten nicht geladen werden."; console.warn("VG-Observe GVL snapshot list failed", error); } } function renderNoGvlSnapshots() { gvlSnapshotList.textContent = ""; clearGvlSnapshotSummary(); clearGvlVendorOverview(); gvlSnapshotEmpty.hidden = false; gvlSnapshotContent.hidden = true; gvlSnapshotEmpty.textContent = "Keine gespeicherten GVL-Referenzrevisionen vorhanden."; } function renderGvlSnapshotList() { gvlSnapshotList.textContent = ""; for (const snapshot of gvlSnapshots) { const row = document.createElement("tr"); const isSelected = snapshot?.sha256 === selectedSnapshotSha256; row.className = isSelected ? "is-selected" : ""; row.tabIndex = 0; row.setAttribute("role", "button"); row.setAttribute("aria-pressed", isSelected ? "true" : "false"); row.addEventListener("click", async () => { await selectGvlSnapshot(snapshot?.sha256 ?? null); }); row.addEventListener("keydown", async (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); await selectGvlSnapshot(snapshot?.sha256 ?? null); } }); 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, shortenSha256(snapshot?.sha256), "sha-cell"); appendListCell(row, formatNullable(snapshot?.sourceUrl), "url-cell"); gvlSnapshotList.append(row); } } function appendListCell(row, value, className) { const cell = document.createElement("td"); if (className) { cell.className = className; } cell.textContent = value; row.append(cell); } async function selectGvlSnapshot(sha256) { selectedSnapshotSha256 = sha256; renderGvlSnapshotList(); renderRebuildStatus(""); await renderSelectedGvlSnapshotSummary(); } async function renderSelectedGvlSnapshotSummary() { const snapshot = findGvlSnapshot(selectedSnapshotSha256); clearGvlSnapshotSummary(); clearGvlVendorOverview(); if (!snapshot) { updateRebuildActionState(null); return; } try { const result = await browser.runtime.sendMessage({ type: "get_gvl_snapshot_summary", payload: { sha256: snapshot.sha256 } }); if (!result?.success) { throw new Error(result?.error ?? "get_gvl_snapshot_summary_failed"); } selectedSnapshotSummary = result.summary ?? {}; renderSummaryTable(selectedSnapshotSummary); updateRebuildActionState(selectedSnapshotSummary); await renderVendorOverviewForSelectedSnapshot(); } catch (error) { selectedSnapshotSummary = null; updateRebuildActionState(null); gvlSnapshotSummary.textContent = "Zusammenfassung dieser GVL-Revision konnte nicht geladen werden."; console.warn("VG-Observe GVL snapshot summary failed", error); } } async function renderVendorOverviewForSelectedSnapshot() { const snapshot = findGvlSnapshot(selectedSnapshotSha256); clearGvlVendorOverview(); if (!snapshot) { return; } try { const result = await browser.runtime.sendMessage({ type: "list_gvl_vendors_for_snapshot", payload: { sha256: snapshot.sha256 } }); if (!result?.success) { throw new Error(result?.error ?? "list_gvl_vendors_for_snapshot_failed"); } selectedSnapshotVendors = result.vendors ?? []; updateVendorOverviewSummary(); renderVendorOverview(); } catch (error) { gvlVendorOverviewEmpty.hidden = false; gvlVendorOverviewEmpty.textContent = "Vendoren-Referenz konnte nicht geladen werden."; console.warn("VG-Observe GVL vendor overview failed", error); } } function renderVendorOverview() { gvlVendorOverviewList.textContent = ""; if (!selectedSnapshotVendors.length) { gvlVendorOverviewEmpty.hidden = false; gvlVendorOverviewContent.hidden = true; gvlVendorOverviewEmpty.textContent = "Keine normalisierten Vendor-Referenzen für diese GVL-Revision vorhanden."; return; } gvlVendorOverviewEmpty.hidden = true; gvlVendorOverviewContent.hidden = false; for (const vendor of selectedSnapshotVendors) { const row = document.createElement("tr"); row.tabIndex = 0; row.setAttribute("role", "button"); row.addEventListener("click", async () => { await selectVendorFromOverview(vendor.vendorId); }); row.addEventListener("keydown", async (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); await selectVendorFromOverview(vendor.vendorId); } }); appendListCell(row, formatNullable(vendor.vendorId), "numeric"); appendListCell(row, formatNullable(vendor.name)); appendListCell(row, formatNullable(vendor.deletedDate)); gvlVendorOverviewList.append(row); } } async function selectVendorFromOverview(vendorId) { if (vendorId === null || vendorId === undefined) { return; } gvlVendorIdInput.value = String(vendorId); await renderGvlVendorDetail(); } function clearGvlVendorOverview() { gvlVendorOverviewList.textContent = ""; selectedSnapshotVendors = []; gvlVendorOverviewEmpty.hidden = true; gvlVendorOverviewContent.hidden = true; gvlVendorOverviewDetails.open = false; updateVendorOverviewSummary(); } function updateVendorOverviewSummary() { const count = selectedSnapshotVendors.length; gvlVendorOverviewSummary.textContent = count ? `Vendoren-Referenz anzeigen (${count})` : "Vendoren-Referenz anzeigen"; } function updateRebuildActionState(summary) { const needsRebuild = doesSnapshotNeedNormalizedRebuild(summary); gvlRebuildNormalizedButton.disabled = !needsRebuild; if (!summary) { renderRebuildStatus(""); return; } if (needsRebuild) { renderRebuildStatus("Reparatur möglich: normalisierte Referenzdaten fehlen."); return; } renderRebuildStatus("Normalisierte lokale Referenzdaten sind verfügbar."); } function doesSnapshotNeedNormalizedRebuild(summary) { if (!summary) { return false; } const expectedVendorCount = Number(summary.snapshotVendorCount ?? 0); const normalizedVendorCount = Number(summary.normalizedVendorCount ?? 0); const normalizedRelationshipCount = Number( summary.normalizedVendorRelationshipCount ?? 0 ); if (expectedVendorCount > 0 && normalizedVendorCount < expectedVendorCount) { return true; } return normalizedVendorCount === 0 || normalizedRelationshipCount === 0; } function renderSummaryTable(summary) { const table = document.createElement("table"); const body = document.createElement("tbody"); const rows = [ ["Vendorlisten-Version", formatNullable(summary.vendorListVersion)], ["Herkunft", formatGvlProvenanceMarker(summary.provenance)], ["Vault-Kopie", formatGvlVaultMarker(summary.vaultCopyAvailable)], ["Workspace-Schutz", formatGvlProtectionMarker(summary)], ["Abrufzeitpunkt", formatNullable(summary.fetchedAt)], ["Quelle", formatNullable(summary.sourceUrl)], ["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)], [ "Normalisierte Vendoren", formatCount(summary.normalizedVendorCount) ], ["Anzahl Zwecke/Purposes", formatCount(summary.purposeCount)], ["Anzahl Special Purposes", formatCount(summary.specialPurposeCount)], ["Anzahl Features", formatCount(summary.featureCount)], ["Anzahl Special Features", formatCount(summary.specialFeatureCount)], ["Anzahl Datenkategorien", formatCount(summary.dataCategoryCount)], [ "Anzahl Vendor-Beziehungen", formatCount(summary.vendorRelationshipCount) ] ]; table.className = "summary-table"; for (const [label, value] of rows) { const row = document.createElement("tr"); const labelCell = document.createElement("th"); const valueCell = document.createElement("td"); labelCell.scope = "row"; labelCell.textContent = label; valueCell.textContent = value; row.append(labelCell, valueCell); body.append(row); } table.append(body); gvlSnapshotSummary.append(table); gvlTechnicalFields.textContent = JSON.stringify( summary.technicalFields ?? {}, null, 2 ); gvlDebugData.textContent = JSON.stringify( { sha256: summary.sha256 ?? null, eventType: summary.eventType ?? null, eventCapturedAt: summary.eventCapturedAt ?? null, diagnostics: summary.diagnostics ?? null }, null, 2 ); } function clearGvlSnapshotSummary() { gvlSnapshotSummary.textContent = ""; gvlTechnicalFields.textContent = ""; gvlDebugData.textContent = ""; selectedSnapshotSummary = null; } function findGvlSnapshot(sha256) { if (!sha256) { return null; } return ( gvlSnapshots.find((snapshot) => { return snapshot?.sha256 === sha256; }) ?? null ); } function formatNullable(value) { if (value === null || value === undefined || value === "") { return "-"; } return String(value); } function formatJsonValue(value) { if (value === null || value === undefined || value === "") { return "-"; } return JSON.stringify(value); } function formatCount(value) { if (value === null || value === undefined) { return "0"; } return String(value); } function shortenSha256(value) { if (!value) { return "-"; } 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"); }