- Bereit
+
+ Explorer
+
+ Historische Consent-Zustände und technische Belege werden in einer
+ eigenen Ansicht geöffnet.
+
+
diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js
index 38c9a36..89d403e 100644
--- a/src/dashboard/dashboard.js
+++ b/src/dashboard/dashboard.js
@@ -1,33 +1,20 @@
"use strict";
-const EVIDENCE_MAINTENANCE_SOURCE = "dashboard";
-const EVIDENCE_MAINTENANCE_HEARTBEAT_MS = 5 * 1000;
-
const dashboardStatus = document.getElementById("dashboard-status");
-const totalCount = document.getElementById("total-count");
-const lockedCount = document.getElementById("locked-count");
-const unlockedCount = document.getElementById("unlocked-count");
-const maintenanceWriteSuspend = document.getElementById(
- "maintenance-write-suspend"
+const officialGvlLocalVersion = document.getElementById(
+ "official-gvl-local-version"
);
-const maintenanceSource = document.getElementById("maintenance-source");
-const maintenanceHeartbeat = document.getElementById("maintenance-heartbeat");
-const maintenanceExpires = document.getElementById("maintenance-expires");
-const lockAllButton = document.getElementById("lock-all-button");
-const unlockAllButton = document.getElementById("unlock-all-button");
-const gvlFetchOfficialButton = document.getElementById(
- "gvl-fetch-official-button"
+const officialGvlLastCheck = document.getElementById("official-gvl-last-check");
+const officialGvlLastRealCheck = document.getElementById(
+ "official-gvl-last-real-check"
+);
+const officialGvlNextAllowedCheck = document.getElementById(
+ "official-gvl-next-allowed-check"
+);
+const officialGvlResult = document.getElementById("official-gvl-result");
+const officialGvlVersionChange = document.getElementById(
+ "official-gvl-version-change"
);
-const gvlImportButton = document.getElementById("gvl-import-button");
-const evidenceDeleteButton = document.getElementById("evidence-delete-button");
-const adminStatus = document.getElementById("admin-status");
-const gvlImportFileInput = document.createElement("input");
-
-gvlImportFileInput.type = "file";
-gvlImportFileInput.accept = ".json,application/json";
-gvlImportFileInput.hidden = true;
-
-let evidenceMaintenanceHeartbeatId = null;
const storeCells = {
consent_states: document.getElementById("store-consent-states"),
@@ -38,127 +25,10 @@ const storeCells = {
};
document.addEventListener("DOMContentLoaded", async () => {
- document.body.appendChild(gvlImportFileInput);
-
- await startEvidenceMaintenanceMode();
- evidenceMaintenanceHeartbeatId = setInterval(() => {
- void refreshEvidenceMaintenanceMode();
- }, EVIDENCE_MAINTENANCE_HEARTBEAT_MS);
-
- lockAllButton.addEventListener("click", async () => {
- await handleLockAllClick();
- });
-
- unlockAllButton.addEventListener("click", async () => {
- await handleUnlockAllClick();
- });
-
- gvlFetchOfficialButton.addEventListener("click", async () => {
- await fetchOfficialGvl();
- });
-
- gvlImportButton.addEventListener("click", () => {
- gvlImportFileInput.value = "";
- gvlImportFileInput.click();
- });
-
- gvlImportFileInput.addEventListener("change", async () => {
- const file = gvlImportFileInput.files?.[0] ?? null;
-
- if (!file) {
- return;
- }
-
- await importGvlFile(file);
- });
-
- evidenceDeleteButton.addEventListener("click", async () => {
- await handleEvidenceDeleteClick();
- });
-
await renderEvidenceStatus();
+ await renderOfficialGvlStatus();
});
-window.addEventListener("beforeunload", () => {
- endEvidenceMaintenanceMode();
-});
-
-window.addEventListener("pagehide", () => {
- endEvidenceMaintenanceMode();
-});
-
-async function startEvidenceMaintenanceMode() {
- try {
- const status = await sendEvidenceMaintenanceMessage(
- "start_evidence_maintenance_session"
- );
-
- renderEvidenceMaintenanceStatus(status);
- } catch (error) {
- renderEvidenceMaintenanceUnavailable();
- console.warn("VendorGet-IV maintenance start failed", error);
- }
-}
-
-async function refreshEvidenceMaintenanceMode() {
- try {
- const status = await sendEvidenceMaintenanceMessage(
- "refresh_evidence_maintenance_session"
- );
-
- renderEvidenceMaintenanceStatus(status);
- } catch (error) {
- renderEvidenceMaintenanceUnavailable();
- console.warn("VendorGet-IV maintenance heartbeat failed", error);
- }
-}
-
-function endEvidenceMaintenanceMode() {
- if (evidenceMaintenanceHeartbeatId !== null) {
- clearInterval(evidenceMaintenanceHeartbeatId);
- evidenceMaintenanceHeartbeatId = null;
- }
-
- void sendEvidenceMaintenanceMessage("end_evidence_maintenance_session").catch(
- (error) => {
- console.warn("VendorGet-IV maintenance end failed", error);
- }
- );
-}
-
-async function sendEvidenceMaintenanceMessage(type) {
- const status = await browser.runtime.sendMessage({
- type: type,
- payload: {
- source: EVIDENCE_MAINTENANCE_SOURCE
- }
- });
-
- if (!status?.success) {
- throw new Error(status?.error ?? `${type}_failed`);
- }
-
- return status;
-}
-
-function renderEvidenceMaintenanceStatus(status) {
- maintenanceWriteSuspend.textContent = status.evidenceWriteSuspended
- ? "aktiv"
- : "inaktiv";
- maintenanceSource.textContent = status.source ?? "-";
- maintenanceHeartbeat.textContent = formatMaintenanceTimestamp(
- status.lastHeartbeatAt
- );
- maintenanceExpires.textContent = formatMaintenanceTimestamp(status.expiresAt);
-}
-
-function renderEvidenceMaintenanceUnavailable() {
- maintenanceWriteSuspend.textContent = "unbekannt";
- maintenanceSource.textContent = "-";
- maintenanceHeartbeat.textContent = "-";
- maintenanceExpires.textContent = "-";
-}
-
async function renderEvidenceStatus() {
try {
const status = await browser.runtime.sendMessage({
@@ -169,10 +39,6 @@ async function renderEvidenceStatus() {
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
}
- totalCount.textContent = String(status.totalCount);
- lockedCount.textContent = String(status.lockedCount);
- unlockedCount.textContent = String(status.unlockedCount);
-
renderStoreCounts(status.storeCounts ?? {});
renderStatusMessage("Evidence status loaded");
} catch (error) {
@@ -187,250 +53,94 @@ function renderStoreCounts(storeCounts) {
}
}
-async function fetchOfficialGvl() {
- gvlFetchOfficialButton.disabled = true;
- renderAdminStatus("Fetching official IAB GVL...");
-
+async function renderOfficialGvlStatus() {
try {
const result = await browser.runtime.sendMessage({
- type: "fetch_official_gvl"
+ type: "get_latest_gvl_update_status"
});
if (!result?.success) {
- throw new Error(result?.error ?? "official_gvl_fetch_failed");
+ throw new Error(result?.error ?? "get_latest_gvl_update_status_failed");
}
- await renderEvidenceStatus();
- renderAdminStatus(
- "Fetched successfully - " +
- `${result.alreadyKnown ? "already known" : "newly stored"} - ` +
- `vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
- `sha256 ${shortenSha256(result.sha256)}`
+ const status = result.status ?? {};
+
+ officialGvlLocalVersion.textContent = formatNullable(
+ status.latestLocalVendorListVersion ?? status.currentVendorListVersion
);
+ officialGvlLastCheck.textContent = formatNullable(status.checkedAt);
+ officialGvlLastRealCheck.textContent = formatNullable(
+ status.lastAutoGvlCheckAt
+ );
+ officialGvlNextAllowedCheck.textContent = formatNullable(
+ status.nextAllowedAutoCheckAt
+ );
+ officialGvlResult.textContent = formatGvlUpdateResult(status);
+ officialGvlVersionChange.textContent = formatGvlVersionChange(status);
} catch (error) {
- renderAdminStatus("Official GVL fetch failed");
- console.warn("VendorGet-IV official GVL fetch failed", error);
- } finally {
- gvlFetchOfficialButton.disabled = false;
+ officialGvlLocalVersion.textContent = "-";
+ officialGvlLastCheck.textContent = "-";
+ officialGvlLastRealCheck.textContent = "-";
+ officialGvlNextAllowedCheck.textContent = "-";
+ officialGvlResult.textContent = "Auto-Check fehlgeschlagen";
+ officialGvlVersionChange.textContent = "-";
+ console.warn("VendorGet-IV official GVL status failed", error);
}
}
-async function importGvlFile(file) {
- gvlImportButton.disabled = true;
- renderAdminStatus("Import läuft...");
+function formatGvlUpdateResult(status) {
+ const result = status?.result ?? null;
- try {
- const fileContent = await readFileAsText(file);
- const rawJson = JSON.parse(fileContent);
-
- if (!isGvlImportCandidate(rawJson)) {
- throw new Error("invalid_gvl_json");
- }
-
- const result = await browser.runtime.sendMessage({
- type: "gvl_import_json",
- payload: {
- rawJson: rawJson,
- sourceUrl: "local-file-import"
- }
- });
-
- if (!result?.success) {
- throw new Error(result?.error ?? "gvl_import_failed");
- }
-
- await renderEvidenceStatus();
- renderAdminStatus(
- `${result.alreadyKnown ? "already known" : "imported"} - ` +
- `vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
- `sha256 ${shortenSha256(result.sha256)}`
- );
- } catch (error) {
- renderAdminStatus("Import fehlgeschlagen");
- console.warn("VendorGet-IV GVL import failed", error);
- } finally {
- gvlImportButton.disabled = false;
- }
-}
-
-function readFileAsText(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
-
- reader.onerror = () => reject(reader.error);
- reader.onload = () => resolve(reader.result);
- reader.readAsText(file);
- });
-}
-
-function isGvlImportCandidate(value) {
- return (
- value &&
- typeof value === "object" &&
- !Array.isArray(value) &&
- value.vendorListVersion !== undefined &&
- value.vendors &&
- typeof value.vendors === "object" &&
- !Array.isArray(value.vendors)
- );
-}
-
-async function handleEvidenceDeleteClick() {
- evidenceDeleteButton.disabled = true;
-
- try {
- const status = await getEvidenceRetentionStatus();
-
- const confirmed = confirm(
- "Alle lokal gespeicherten VG-IV-Evidenzdaten wirklich löschen?"
- );
-
- if (!confirmed) {
- renderAdminStatus("Löschung abgebrochen");
- return;
- }
-
- if (status.lockedCount === 0) {
- await deleteAllEvidenceDatabase();
- await renderEvidenceStatus();
- renderAdminStatus("Evidenzdaten gelöscht");
- return;
- }
-
- const deleteLockedRecords = confirm(
- `Achtung: ${status.lockedCount} Datensätze wurden als ` +
- "DSGVO-/DSAR-relevant markiert. Sollen auch diese Datensätze " +
- "wirklich gelöscht werden?"
- );
-
- if (deleteLockedRecords) {
- await deleteAllEvidenceDatabase();
- await renderEvidenceStatus();
- renderAdminStatus("Evidenzdaten gelöscht");
- return;
- }
-
- 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 renderEvidenceStatus();
- renderAdminStatus(
- `${result.deletedCount} Datensätze gelöscht, ` +
- `${result.keptLockedCount} gesperrte Datensätze behalten`
- );
- } catch (error) {
- renderAdminStatus("Löschung fehlgeschlagen");
- console.warn("VendorGet-IV evidence delete failed", error);
- } finally {
- evidenceDeleteButton.disabled = false;
- }
-}
-
-async function getEvidenceRetentionStatus() {
- const status = await browser.runtime.sendMessage({
- type: "get_evidence_retention_status"
- });
-
- if (!status?.success) {
- throw new Error(status?.error ?? "get_evidence_retention_status_failed");
+ if (result === "stored") {
+ return "Neue offizielle Vendorliste gespeichert";
}
- return status;
-}
-
-async function deleteAllEvidenceDatabase() {
- const result = await browser.runtime.sendMessage({
- type: "delete_all_evidence_database"
- });
-
- if (!result?.success) {
- throw new Error(result?.error ?? "delete_all_evidence_database_failed");
- }
-}
-
-async function handleLockAllClick() {
- const confirmed = confirm(
- "Alle vorhandenen VG-IV-Evidenzdatensätze als DSGVO-/DSAR-relevant markieren?"
- );
-
- if (!confirmed) {
- renderStatusMessage("Record lock update cancelled");
- return;
+ if (result === "no_change") {
+ return "Keine neuere offizielle Vendorliste gefunden";
}
- await runRecordLockAction({
- type: "lock_all_evidence_records",
- payload: {
- reason: "dsar_used",
- note: null
- }
- });
-}
-
-async function handleUnlockAllClick() {
- const confirmed = confirm(
- "Alle VG-IV-Evidenzsperren wirklich entfernen?"
- );
-
- if (!confirmed) {
- renderStatusMessage("Record lock update cancelled");
- return;
+ if (result === "already_known") {
+ return "Offizielle Vendorliste war bereits lokal bekannt";
}
- await runRecordLockAction({
- type: "unlock_all_evidence_records"
- });
-}
-
-async function runRecordLockAction(message) {
- setRecordLockButtonsDisabled(true);
-
- try {
- const result = await browser.runtime.sendMessage(message);
-
- if (!result?.success) {
- throw new Error(result?.error ?? `${message.type}_failed`);
- }
-
- await renderEvidenceStatus();
- } catch (error) {
- renderStatusMessage("Record lock update failed");
- console.warn("VendorGet-IV dashboard record lock update failed", error);
- } finally {
- setRecordLockButtonsDisabled(false);
+ if (result === "error") {
+ return "Auto-Check fehlgeschlagen";
}
+
+ if (result === "throttled") {
+ return "Übersprungen wegen 24h-Throttling";
+ }
+
+ if (result === "started") {
+ return "Auto-Check läuft";
+ }
+
+ if (result === "not_checked_since_background_start") {
+ return "Noch kein Auto-Check seit Background-Start";
+ }
+
+ return formatNullable(status?.message ?? result);
}
-function setRecordLockButtonsDisabled(disabled) {
- lockAllButton.disabled = disabled;
- unlockAllButton.disabled = disabled;
+function formatGvlVersionChange(status) {
+ const previousVersion = formatNullable(status?.previousVendorListVersion);
+ const currentVersion = formatNullable(status?.currentVendorListVersion);
+
+ if (previousVersion === "-" && currentVersion === "-") {
+ return "-";
+ }
+
+ return `${previousVersion} -> ${currentVersion}`;
}
function renderStatusMessage(message) {
dashboardStatus.textContent = message;
}
-function renderAdminStatus(message) {
- adminStatus.textContent = message;
-}
-
-function shortenSha256(value) {
- if (!value) {
- return "n/a";
- }
-
- return `${value.slice(0, 12)}...`;
-}
-
-function formatMaintenanceTimestamp(value) {
- if (!value) {
+function formatNullable(value) {
+ if (value === null || value === undefined || value === "") {
return "-";
}
- return value;
+ return String(value);
}
diff --git a/src/gvl-explorer/gvl-explorer.css b/src/gvl-explorer/gvl-explorer.css
new file mode 100644
index 0000000..bb0650b
--- /dev/null
+++ b/src/gvl-explorer/gvl-explorer.css
@@ -0,0 +1,201 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ font-family: Arial, sans-serif;
+ color: #e5edf5;
+ background: #111827;
+}
+
+.explorer {
+ width: min(1080px, 100%);
+ margin: 0 auto;
+ padding: 24px;
+}
+
+.explorer-header {
+ display: grid;
+ gap: 8px;
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #334155;
+}
+
+h1,
+h2,
+p {
+ margin: 0;
+}
+
+h1 {
+ font-size: 24px;
+ font-weight: 700;
+}
+
+h2 {
+ margin-bottom: 12px;
+ font-size: 15px;
+ font-weight: 700;
+}
+
+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;
+}
+
+.snapshot-list-wrap {
+ width: 100%;
+ overflow-x: auto;
+ border: 1px solid #334155;
+ border-radius: 4px;
+ background: #1f2937;
+}
+
+.fetch-actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.fetch-status {
+ min-height: 18px;
+ font-size: 13px;
+ color: #cbd5e1;
+}
+
+button {
+ 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;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+ background: #1f2937;
+}
+
+th,
+td {
+ padding: 10px 12px;
+ border: 1px solid #334155;
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ color: #cbd5e1;
+ font-weight: 700;
+ background: #182231;
+}
+
+.snapshot-list {
+ min-width: 820px;
+ border: 0;
+}
+
+.snapshot-list tbody tr {
+ cursor: pointer;
+}
+
+.snapshot-list tbody tr:hover,
+.snapshot-list tbody tr:focus {
+ outline: 0;
+ background: #263449;
+}
+
+.snapshot-list tbody tr.is-selected {
+ background: #1e3a5f;
+ box-shadow: inset 3px 0 0 #60a5fa;
+}
+
+.snapshot-list .numeric {
+ text-align: right;
+}
+
+.snapshot-list .sha-cell,
+.snapshot-list .url-cell {
+ overflow-wrap: anywhere;
+}
+
+.snapshot-summary {
+ margin-top: 18px;
+}
+
+.summary-table th {
+ width: 260px;
+}
+
+.summary-table td {
+ font-weight: 700;
+ overflow-wrap: anywhere;
+}
+
+.empty-state {
+ padding: 10px 12px;
+ border: 1px solid #334155;
+ border-radius: 4px;
+ background: #1f2937;
+}
+
+.technical-details {
+ margin-top: 12px;
+ font-size: 13px;
+ color: #cbd5e1;
+}
+
+.technical-details summary {
+ cursor: pointer;
+}
+
+.technical-details pre {
+ max-height: 220px;
+ margin: 10px 0 0;
+ padding: 10px 12px;
+ overflow: auto;
+ border: 1px solid #334155;
+ border-radius: 4px;
+ color: #e5edf5;
+ background: #0f172a;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
+@media (max-width: 640px) {
+ .explorer {
+ padding: 16px;
+ }
+
+ .summary-table th {
+ width: 160px;
+ }
+}
diff --git a/src/gvl-explorer/gvl-explorer.html b/src/gvl-explorer/gvl-explorer.html
new file mode 100644
index 0000000..b275d2b
--- /dev/null
+++ b/src/gvl-explorer/gvl-explorer.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
VG-Observe GVL-Explorer
+
+
+
+
+
+
+
+ Gespeicherte Vendorlisten
+
+
+ Offizielle Vendorliste jetzt abrufen
+
+
+ Bereit
+
+
+
+ Keine gespeicherten offiziellen Vendorlisten vorhanden.
+
+
+
+
+
+
+ Vendorlisten-Version
+ Abrufzeitpunkt
+ SHA256
+ Quelle
+
+
+
+
+
+
+
+ Ausgewählte Vendorliste
+
+
+
+
+ Technische Feldnamen
+
+
+
+ SHA256 und Debugdaten
+
+
+
+
+
+
+
+
+
diff --git a/src/gvl-explorer/gvl-explorer.js b/src/gvl-explorer/gvl-explorer.js
new file mode 100644
index 0000000..e930e81
--- /dev/null
+++ b/src/gvl-explorer/gvl-explorer.js
@@ -0,0 +1,273 @@
+"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 gvlFetchStatus = document.getElementById("gvl-fetch-status");
+
+let gvlSnapshots = [];
+let selectedSnapshotSha256 = null;
+
+document.addEventListener("DOMContentLoaded", async () => {
+ gvlFetchOfficialButton.addEventListener("click", async () => {
+ await fetchOfficialGvl();
+ });
+
+ await renderGvlSnapshots();
+});
+
+async function fetchOfficialGvl() {
+ gvlFetchOfficialButton.disabled = true;
+ renderFetchStatus("Vendorliste wird abgerufen...");
+
+ try {
+ const result = await browser.runtime.sendMessage({
+ type: "fetch_official_gvl"
+ });
+
+ if (!result?.success) {
+ throw new Error(result?.error ?? "official_gvl_fetch_failed");
+ }
+
+ renderFetchStatus(
+ result.alreadyKnown
+ ? "Vendorliste bereits bekannt."
+ : "Vendorliste abgerufen."
+ );
+
+ await renderGvlSnapshots();
+ await renderSelectedGvlSnapshotSummary();
+ } catch (error) {
+ renderFetchStatus("Vendorliste konnte nicht abgerufen werden.");
+ console.warn("VG-Observe manual official GVL fetch failed", error);
+ } finally {
+ gvlFetchOfficialButton.disabled = false;
+ }
+}
+
+function renderFetchStatus(message) {
+ gvlFetchStatus.textContent = message;
+}
+
+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 Vendorlisten konnten nicht geladen werden.";
+ console.warn("VG-Observe GVL snapshot list failed", error);
+ }
+}
+
+function renderNoGvlSnapshots() {
+ gvlSnapshotList.textContent = "";
+ clearGvlSnapshotSummary();
+ gvlSnapshotEmpty.hidden = false;
+ gvlSnapshotContent.hidden = true;
+ gvlSnapshotEmpty.textContent =
+ "Keine gespeicherten offiziellen Vendorlisten 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, 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();
+ await renderSelectedGvlSnapshotSummary();
+}
+
+async function renderSelectedGvlSnapshotSummary() {
+ const snapshot = findGvlSnapshot(selectedSnapshotSha256);
+
+ clearGvlSnapshotSummary();
+
+ if (!snapshot) {
+ 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");
+ }
+
+ renderSummaryTable(result.summary ?? {});
+ } catch (error) {
+ gvlSnapshotSummary.textContent =
+ "Zusammenfassung dieser Vendorliste konnte nicht geladen werden.";
+ console.warn("VG-Observe GVL snapshot summary failed", error);
+ }
+}
+
+function renderSummaryTable(summary) {
+ const table = document.createElement("table");
+ const body = document.createElement("tbody");
+ const rows = [
+ ["Vendorlisten-Version", formatNullable(summary.vendorListVersion)],
+ ["Abrufzeitpunkt", formatNullable(summary.fetchedAt)],
+ ["Quelle", formatNullable(summary.sourceUrl)],
+ ["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)],
+ ["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 = "";
+}
+
+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 formatCount(value) {
+ if (value === null || value === undefined) {
+ return "0";
+ }
+
+ return String(value);
+}
+
+function shortenSha256(value) {
+ if (!value) {
+ return "-";
+ }
+
+ return `${String(value).slice(0, 12)}...`;
+}