diff --git a/src/background.js b/src/background.js
index 6ab280f..3010638 100644
--- a/src/background.js
+++ b/src/background.js
@@ -73,6 +73,18 @@ async function handleVendorGetMessage(message, sender) {
return handleGetGvlSnapshotSummaryMessage(message);
}
+ if (message.type === "rebuild_gvl_snapshot_normalized_data") {
+ return handleRebuildGvlSnapshotNormalizedDataMessage(message);
+ }
+
+ if (message.type === "list_gvl_vendors_for_snapshot") {
+ return handleListGvlVendorsForSnapshotMessage(message);
+ }
+
+ if (message.type === "get_gvl_vendor_detail") {
+ return handleGetGvlVendorDetailMessage(message);
+ }
+
if (message.type === "get_latest_consent_state") {
return handleGetLatestConsentStateMessage();
}
@@ -280,12 +292,15 @@ async function handleGetGvlSnapshotSummaryMessage(message) {
eventType: event?.eventType ?? null,
eventCapturedAt: event?.capturedAt ?? null,
vendorCount: snapshot.vendorCount ?? counts.vendorCount,
+ snapshotVendorCount: snapshot.vendorCount ?? null,
+ normalizedVendorCount: counts.vendorCount,
purposeCount: snapshot.purposeCount ?? counts.purposeCount,
specialPurposeCount: counts.specialPurposeCount,
featureCount: counts.featureCount,
specialFeatureCount: counts.specialFeatureCount,
dataCategoryCount: counts.dataCategoryCount,
vendorRelationshipCount: counts.vendorRelationshipCount,
+ normalizedVendorRelationshipCount: counts.vendorRelationshipCount,
technicalFields: {
snapshotStore: "gvl_snapshots",
vendorListVersion: "vendorListVersion",
@@ -300,6 +315,267 @@ async function handleGetGvlSnapshotSummaryMessage(message) {
};
}
+async function handleRebuildGvlSnapshotNormalizedDataMessage(message) {
+ const db = await openVendorGetDb();
+ const payload = message?.payload ?? {};
+ const snapshot = await getGvlSnapshotByIdentifier(db, {
+ sha256: payload.sha256 ?? null,
+ vendorListVersion: payload.vendorListVersion ?? null
+ });
+
+ if (!snapshot) {
+ return {
+ success: false,
+ error: "gvl_snapshot_not_found"
+ };
+ }
+
+ const normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
+ snapshot
+ );
+ const counts = await countGvlNormalizedRecordsForVersion(
+ db,
+ snapshot.vendorListVersion ?? null
+ );
+
+ return {
+ success: true,
+ snapshotSha256: snapshot.sha256 ?? null,
+ vendorListVersion: snapshot.vendorListVersion ?? null,
+ normalizationSummary,
+ counts
+ };
+}
+
+async function handleListGvlVendorsForSnapshotMessage(message) {
+ const db = await openVendorGetDb();
+ const payload = message?.payload ?? {};
+ const snapshot = await getGvlSnapshotByIdentifier(db, {
+ sha256: payload.sha256 ?? null,
+ vendorListVersion: payload.vendorListVersion ?? null
+ });
+
+ if (!snapshot) {
+ return {
+ success: false,
+ error: "gvl_snapshot_not_found"
+ };
+ }
+
+ const vendors = await listGvlVendorsForSnapshot(db, snapshot);
+
+ return {
+ success: true,
+ snapshotSha256: snapshot.sha256 ?? null,
+ vendorListVersion: snapshot.vendorListVersion ?? null,
+ vendors
+ };
+}
+
+function listGvlVendorsForSnapshot(db, snapshot) {
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
+ const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
+ const vendorListVersionIndex = vendorsStore.index("vendorListVersion");
+ const cursorRequest = vendorListVersionIndex.openCursor(
+ IDBKeyRange.only(snapshot.vendorListVersion)
+ );
+ const vendors = [];
+
+ cursorRequest.onerror = () => reject(cursorRequest.error);
+ cursorRequest.onsuccess = () => {
+ const cursor = cursorRequest.result;
+
+ if (!cursor) {
+ return;
+ }
+
+ if (cursor.value?.snapshotSha256 === snapshot.sha256) {
+ vendors.push({
+ vendorId: cursor.value.vendorId ?? null,
+ name: cursor.value.name ?? null,
+ deletedDate: cursor.value.deletedDate ?? null,
+ snapshotSha256: cursor.value.snapshotSha256 ?? null
+ });
+ }
+
+ cursor.continue();
+ };
+
+ tx.onerror = () => reject(tx.error);
+ tx.onabort = () => reject(tx.error);
+ tx.oncomplete = () => {
+ resolve(
+ vendors.sort((left, right) => {
+ return (
+ toComparableNumber(left.vendorId) -
+ toComparableNumber(right.vendorId)
+ );
+ })
+ );
+ };
+ });
+}
+
+async function handleGetGvlVendorDetailMessage(message) {
+ const db = await openVendorGetDb();
+ const vendorId = parseGvlVendorDetailId(message?.payload?.vendorId);
+
+ if (vendorId === null) {
+ return {
+ success: false,
+ error: "invalid_vendor_id"
+ };
+ }
+
+ const vendorRecord = await getLatestGvlVendorByVendorId(db, vendorId);
+
+ if (!vendorRecord) {
+ return {
+ success: false,
+ error: "gvl_vendor_not_found"
+ };
+ }
+
+ const snapshotSha256 = vendorRecord.snapshotSha256 ?? null;
+ const snapshot = snapshotSha256
+ ? await getGvlSnapshotBySha256(db, snapshotSha256)
+ : null;
+ const rawGvlSha256 = snapshot?.rawGvlSha256 ?? null;
+ const rawEvidence = rawGvlSha256
+ ? await getGvlRawEvidenceBySha256(db, rawGvlSha256)
+ : null;
+
+ return {
+ success: true,
+ vendorDetail: {
+ vendor: vendorRecord,
+ snapshot: buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256),
+ rawEvidence: buildGvlVendorDetailRawEvidenceSummary(
+ rawEvidence,
+ rawGvlSha256
+ )
+ }
+ };
+}
+
+function parseGvlVendorDetailId(value) {
+ const vendorId = Number(value);
+
+ if (!Number.isInteger(vendorId) || vendorId <= 0) {
+ return null;
+ }
+
+ return vendorId;
+}
+
+function getLatestGvlVendorByVendorId(db, vendorId) {
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
+ const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
+ const vendorIdIndex = vendorsStore.index("vendorId");
+ const cursorRequest = vendorIdIndex.openCursor(IDBKeyRange.only(vendorId));
+ const vendors = [];
+
+ cursorRequest.onerror = () => reject(cursorRequest.error);
+ cursorRequest.onsuccess = () => {
+ const cursor = cursorRequest.result;
+
+ if (!cursor) {
+ return;
+ }
+
+ vendors.push(cursor.value);
+ cursor.continue();
+ };
+
+ tx.onerror = () => reject(tx.error);
+ tx.onabort = () => reject(tx.error);
+ tx.oncomplete = () => {
+ resolve(sortGvlVendorRecordsNewestFirst(vendors)[0] ?? null);
+ };
+ });
+}
+
+function sortGvlVendorRecordsNewestFirst(vendorRecords) {
+ return vendorRecords.slice().sort((left, right) => {
+ const fetchedAtComparison =
+ toComparableTime(right.snapshotFetchedAt) -
+ toComparableTime(left.snapshotFetchedAt);
+
+ if (fetchedAtComparison !== 0) {
+ return fetchedAtComparison;
+ }
+
+ return (
+ toComparableNumber(right.vendorListVersion) -
+ toComparableNumber(left.vendorListVersion)
+ );
+ });
+}
+
+function getGvlRawEvidenceBySha256(db, rawGvlSha256) {
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction([VENDORGET_STORE_NAMES.gvlRawEvidence], "readonly");
+ const rawEvidenceStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlRawEvidence);
+ const getRequest = rawEvidenceStore.get(rawGvlSha256);
+ let rawEvidenceOrNull = null;
+
+ getRequest.onerror = () => reject(getRequest.error);
+ getRequest.onsuccess = () => {
+ rawEvidenceOrNull = getRequest.result ?? null;
+ };
+
+ tx.onerror = () => reject(tx.error);
+ tx.onabort = () => reject(tx.error);
+ tx.oncomplete = () => resolve(rawEvidenceOrNull);
+ });
+}
+
+function buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256) {
+ if (!snapshot) {
+ return {
+ snapshotSha256: snapshotSha256 ?? null,
+ rawGvlSha256: null,
+ vendorListVersion: null,
+ tcfPolicyVersion: null,
+ fetchedAt: null,
+ createdAt: null
+ };
+ }
+
+ return {
+ snapshotSha256: snapshot.sha256 ?? snapshotSha256 ?? null,
+ rawGvlSha256: snapshot.rawGvlSha256 ?? null,
+ vendorListVersion: snapshot.vendorListVersion ?? null,
+ tcfPolicyVersion: snapshot.tcfPolicyVersion ?? null,
+ fetchedAt: snapshot.fetchedAt ?? null,
+ createdAt: snapshot.createdAt ?? null
+ };
+}
+
+function buildGvlVendorDetailRawEvidenceSummary(rawEvidence, rawGvlSha256) {
+ if (!rawEvidence) {
+ return {
+ rawGvlSha256: rawGvlSha256 ?? null,
+ sourceUrl: null,
+ fetchedAt: null,
+ httpStatus: null,
+ contentType: null,
+ hasRawBody: false
+ };
+ }
+
+ return {
+ rawGvlSha256: rawEvidence.rawGvlSha256 ?? rawGvlSha256 ?? null,
+ sourceUrl: rawEvidence.sourceUrl ?? null,
+ fetchedAt: rawEvidence.fetchedAt ?? null,
+ httpStatus: rawEvidence.httpStatus ?? null,
+ contentType: rawEvidence.contentType ?? null,
+ hasRawBody: typeof rawEvidence.rawBody === "string"
+ };
+}
+
async function handleGetLatestConsentStateMessage() {
const db = await openVendorGetDb();
const latestStateOrNull = await getLatestConsentState(db);
@@ -631,21 +907,55 @@ async function handleFetchOfficialGvlMessage() {
}
const db = await openVendorGetDb();
- const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
- sourceUrl: OFFICIAL_IAB_GVL_URL,
- fetchedAt: new Date().toISOString(),
- rawGvlSha256: rawGvlSha256,
- diagnostics: {
- ingestionSource: "official_iab_fetch",
- responseStatus: responseStatus
- }
- });
+ const currentVendorListVersion = rawJson.vendorListVersion ?? null;
+ const existingSnapshot = await getGvlSnapshotByVendorListVersion(
+ db,
+ currentVendorListVersion
+ );
+ const ingestResult = existingSnapshot
+ ? null
+ : await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
+ sourceUrl: OFFICIAL_IAB_GVL_URL,
+ fetchedAt: new Date().toISOString(),
+ rawGvlSha256: rawGvlSha256,
+ diagnostics: {
+ ingestionSource: "official_iab_fetch",
+ responseStatus: responseStatus
+ }
+ });
+ const snapshot = existingSnapshot ?? ingestResult.snapshot;
+ const completeness = await getGvlSnapshotNormalizedCompleteness(
+ db,
+ snapshot
+ );
+ let normalizationSummary = null;
+ let syncStatus = "current_and_locally_available";
+
+ if (!existingSnapshot) {
+ normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
+ snapshot
+ );
+ syncStatus = "new_gvl_revision_stored_and_normalized";
+ } else if (!completeness.complete) {
+ normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
+ existingSnapshot
+ );
+ syncStatus = "known_gvl_rebuilt_from_local_evidence";
+ }
+
+ const counts = await countGvlNormalizedRecordsForVersion(
+ db,
+ snapshot.vendorListVersion ?? null
+ );
return {
success: true,
- alreadyKnown: result.alreadyKnown,
- vendorListVersion: result.snapshot.vendorListVersion,
- sha256: result.snapshot.sha256
+ alreadyKnown: Boolean(existingSnapshot),
+ syncStatus,
+ vendorListVersion: snapshot.vendorListVersion,
+ sha256: snapshot.sha256,
+ normalizationSummary,
+ counts
};
} catch (error) {
console.warn("VG-Observe official GVL fetch failed", error);
@@ -657,6 +967,24 @@ async function handleFetchOfficialGvlMessage() {
}
}
+async function getGvlSnapshotNormalizedCompleteness(db, snapshot) {
+ const counts = await countGvlNormalizedRecordsForVersion(
+ db,
+ snapshot?.vendorListVersion ?? null
+ );
+ const expectedVendorCount = Number(snapshot?.vendorCount ?? 0);
+ const hasVendors =
+ expectedVendorCount > 0
+ ? counts.vendorCount >= expectedVendorCount
+ : counts.vendorCount > 0;
+ const hasVendorRelationships = counts.vendorRelationshipCount > 0;
+
+ return {
+ complete: hasVendors && hasVendorRelationships,
+ counts
+ };
+}
+
async function fetchOfficialGvlJson() {
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
method: "GET",
diff --git a/src/gvl-explorer/gvl-explorer.css b/src/gvl-explorer/gvl-explorer.css
index bb0650b..a485568 100644
--- a/src/gvl-explorer/gvl-explorer.css
+++ b/src/gvl-explorer/gvl-explorer.css
@@ -76,12 +76,57 @@ p {
margin-bottom: 12px;
}
+.rebuild-actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
.fetch-status {
min-height: 18px;
font-size: 13px;
color: #cbd5e1;
}
+.vendor-detail-form {
+ display: grid;
+ grid-template-columns: auto minmax(120px, 180px) auto;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.vendor-overview {
+ margin-top: 18px;
+ margin-bottom: 12px;
+ font-size: 13px;
+ color: #cbd5e1;
+}
+
+.vendor-overview summary {
+ cursor: pointer;
+ font-weight: 700;
+}
+
+.vendor-detail-form label {
+ font-size: 13px;
+ font-weight: 700;
+ color: #cbd5e1;
+}
+
+.vendor-detail-form input {
+ min-width: 0;
+ padding: 8px 10px;
+ border: 1px solid #475569;
+ border-radius: 4px;
+ font: inherit;
+ font-size: 13px;
+ color: #e5edf5;
+ background: #0f172a;
+}
+
button {
padding: 8px 10px;
border: 1px solid #475569;
@@ -160,6 +205,17 @@ th {
overflow-wrap: anywhere;
}
+.vendor-detail-result {
+ display: grid;
+ gap: 14px;
+ margin-top: 14px;
+}
+
+.vendor-detail-section h3 {
+ margin: 0 0 8px;
+ font-size: 13px;
+}
+
.empty-state {
padding: 10px 12px;
border: 1px solid #334155;
@@ -198,4 +254,8 @@ th {
.summary-table th {
width: 160px;
}
+
+ .vendor-detail-form {
+ grid-template-columns: 1fr;
+ }
}
diff --git a/src/gvl-explorer/gvl-explorer.html b/src/gvl-explorer/gvl-explorer.html
index b275d2b..47f93f1 100644
--- a/src/gvl-explorer/gvl-explorer.html
+++ b/src/gvl-explorer/gvl-explorer.html
@@ -22,7 +22,7 @@
Gespeicherte Vendorlisten
Bereit
@@ -48,9 +48,40 @@
Ausgewählte Vendorliste
+
+
+
+
+
+
+ Vendoren-Übersicht anzeigen
+
+
+ Keine normalisierten Vendoren für diese Vendorliste vorhanden.
+
+
+
+
+
+ | Vendor-ID |
+ Name |
+ Deleted Date |
+
+
+
+
+
+
+
Technische Feldnamen
@@ -61,6 +92,30 @@
+
+
+ Lokaler Vendor-Nachweis
+
+
+
+
diff --git a/src/gvl-explorer/gvl-explorer.js b/src/gvl-explorer/gvl-explorer.js
index e930e81..8978f68 100644
--- a/src/gvl-explorer/gvl-explorer.js
+++ b/src/gvl-explorer/gvl-explorer.js
@@ -10,21 +10,64 @@ const gvlFetchOfficialButton = document.getElementById(
"gvl-fetch-official-button"
);
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();
});
+ gvlRebuildNormalizedButton.addEventListener("click", async () => {
+ await rebuildSelectedGvlSnapshotNormalizedData();
+ });
+
+ gvlVendorDetailForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ await renderGvlVendorDetail();
+ });
+
await renderGvlSnapshots();
});
async function fetchOfficialGvl() {
gvlFetchOfficialButton.disabled = true;
- renderFetchStatus("Vendorliste wird abgerufen...");
+ renderFetchStatus("GVL-Referenzbasis wird synchronisiert...");
try {
const result = await browser.runtime.sendMessage({
@@ -35,26 +78,221 @@ async function fetchOfficialGvl() {
throw new Error(result?.error ?? "official_gvl_fetch_failed");
}
- renderFetchStatus(
- result.alreadyKnown
- ? "Vendorliste bereits bekannt."
- : "Vendorliste abgerufen."
- );
+ renderFetchStatus(buildGvlSyncStatusMessage(result));
await renderGvlSnapshots();
await renderSelectedGvlSnapshotSummary();
} catch (error) {
- renderFetchStatus("Vendorliste konnte nicht abgerufen werden.");
+ renderFetchStatus("GVL-Referenzbasis konnte nicht synchronisiert werden.");
console.warn("VG-Observe manual official GVL fetch failed", error);
} finally {
gvlFetchOfficialButton.disabled = false;
}
}
+function buildGvlSyncStatusMessage(result) {
+ if (result?.syncStatus === "new_gvl_revision_stored_and_normalized") {
+ return "Neue GVL-Revision gespeichert und normalisiert.";
+ }
+
+ if (result?.syncStatus === "known_gvl_rebuilt_from_local_evidence") {
+ return "Bekannte GVL aus lokaler Evidence neu aufgebaut.";
+ }
+
+ if (result?.syncStatus === "current_and_locally_available") {
+ return "Offizielle GVL ist aktuell und lokal vollständig verfügbar.";
+ }
+
+ return result?.alreadyKnown
+ ? "Offizielle GVL ist lokal verfügbar."
+ : "GVL-Referenzbasis synchronisiert.";
+}
+
function renderFetchStatus(message) {
gvlFetchStatus.textContent = message;
}
+function renderRebuildStatus(message) {
+ gvlRebuildNormalizedStatus.textContent = message;
+}
+
+async function rebuildSelectedGvlSnapshotNormalizedData() {
+ const snapshot = findGvlSnapshot(selectedSnapshotSha256);
+
+ if (!snapshot) {
+ renderRebuildStatus("Keine Vendorliste ausgewählt.");
+ return;
+ }
+
+ gvlRebuildNormalizedButton.disabled = true;
+ renderRebuildStatus("Lokale Evidence wird 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 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 GVL-Daten 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 ?? {};
+
+ gvlVendorDetailResult.textContent = "";
+ gvlVendorDetailResult.append(
+ buildKeyValueSection("Normalisierte Vendor-Felder", [
+ ["Vendor-ID", formatNullable(vendor.vendorId)],
+ ["Name", formatNullable(vendor.name)],
+ ["Policy URL", formatNullable(vendor.policyUrl)],
+ ["Deleted Date", formatNullable(vendor.deletedDate)],
+ ["Uses Cookies", formatNullable(vendor.usesCookies)],
+ ["Cookie Max Age Seconds", formatNullable(vendor.cookieMaxAgeSeconds)],
+ ["Uses Non-Cookie Access", formatNullable(vendor.usesNonCookieAccess)],
+ [
+ "Device Storage Disclosure URL",
+ formatNullable(vendor.deviceStorageDisclosureUrl)
+ ],
+ ["Domains", formatJsonValue(vendor.domains)],
+ ["Snapshot SHA256", formatNullable(vendor.snapshotSha256)]
+ ]),
+ buildKeyValueSection("Snapshot-Herkunft", [
+ ["Snapshot SHA256", formatNullable(snapshot.snapshotSha256)],
+ ["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)],
+ ["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)],
+ ["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)],
+ ["Abrufzeitpunkt", formatNullable(snapshot.fetchedAt)],
+ ["Snapshot erstellt", formatNullable(snapshot.createdAt)]
+ ]),
+ buildKeyValueSection("Raw-GVL-Evidence", [
+ ["Raw-GVL SHA256", formatNullable(rawEvidence.rawGvlSha256)],
+ ["Quelle", formatNullable(rawEvidence.sourceUrl)],
+ ["Abrufzeitpunkt", formatNullable(rawEvidence.fetchedAt)],
+ ["HTTP Status", formatNullable(rawEvidence.httpStatus)],
+ ["Content-Type", formatNullable(rawEvidence.contentType)],
+ ["Raw Body vorhanden", formatNullable(rawEvidence.hasRawBody)]
+ ]),
+ buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor),
+ buildJsonDetails("Vollständiger gvl_vendors-Datensatz", vendor)
+ );
+}
+
+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;
+}
+
async function renderGvlSnapshots() {
try {
const result = await browser.runtime.sendMessage({
@@ -93,6 +331,7 @@ async function renderGvlSnapshots() {
function renderNoGvlSnapshots() {
gvlSnapshotList.textContent = "";
clearGvlSnapshotSummary();
+ clearGvlVendorOverview();
gvlSnapshotEmpty.hidden = false;
gvlSnapshotContent.hidden = true;
gvlSnapshotEmpty.textContent =
@@ -145,6 +384,7 @@ function appendListCell(row, value, className) {
async function selectGvlSnapshot(sha256) {
selectedSnapshotSha256 = sha256;
renderGvlSnapshotList();
+ renderRebuildStatus("");
await renderSelectedGvlSnapshotSummary();
}
@@ -152,8 +392,10 @@ async function renderSelectedGvlSnapshotSummary() {
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
clearGvlSnapshotSummary();
+ clearGvlVendorOverview();
if (!snapshot) {
+ updateRebuildActionState(null);
return;
}
@@ -169,14 +411,149 @@ async function renderSelectedGvlSnapshotSummary() {
throw new Error(result?.error ?? "get_gvl_snapshot_summary_failed");
}
- renderSummaryTable(result.summary ?? {});
+ selectedSnapshotSummary = result.summary ?? {};
+ renderSummaryTable(selectedSnapshotSummary);
+ updateRebuildActionState(selectedSnapshotSummary);
+ await renderVendorOverviewForSelectedSnapshot();
} catch (error) {
+ selectedSnapshotSummary = null;
+ updateRebuildActionState(null);
gvlSnapshotSummary.textContent =
"Zusammenfassung dieser Vendorliste 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-Übersicht 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 Vendoren für diese Vendorliste 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-Übersicht anzeigen (${count})`
+ : "Vendoren-Übersicht anzeigen";
+}
+
+function updateRebuildActionState(summary) {
+ const needsRebuild = doesSnapshotNeedNormalizedRebuild(summary);
+
+ gvlRebuildNormalizedButton.disabled = !needsRebuild;
+
+ if (!summary) {
+ renderRebuildStatus("");
+ return;
+ }
+
+ if (needsRebuild) {
+ renderRebuildStatus("Reparatur möglich: normalisierte lokale Daten fehlen.");
+ return;
+ }
+
+ renderRebuildStatus("Normalisierte lokale GVL-Daten 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");
@@ -185,6 +562,10 @@ function renderSummaryTable(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)],
@@ -234,6 +615,7 @@ function clearGvlSnapshotSummary() {
gvlSnapshotSummary.textContent = "";
gvlTechnicalFields.textContent = "";
gvlDebugData.textContent = "";
+ selectedSnapshotSummary = null;
}
function findGvlSnapshot(sha256) {
@@ -256,6 +638,14 @@ function formatNullable(value) {
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";