Improve GVL explorer vendor evidence workflow

Dieser Commit ist enthalten in:
2026-06-08 22:53:01 +02:00
Ursprung 990da710c1
Commit 3fd40348b5
4 geänderte Dateien mit 854 neuen und 21 gelöschten Zeilen
+340 -12
Datei anzeigen
@@ -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",
+60
Datei anzeigen
@@ -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;
}
}
+56 -1
Datei anzeigen
@@ -22,7 +22,7 @@
<h2 id="snapshot-list-title">Gespeicherte Vendorlisten</h2>
<div class="fetch-actions">
<button id="gvl-fetch-official-button" type="button">
Offizielle Vendorliste jetzt abrufen
GVL-Referenzbasis synchronisieren
</button>
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
Bereit
@@ -48,9 +48,40 @@
<section class="snapshot-summary" aria-labelledby="snapshot-summary-title">
<h2 id="snapshot-summary-title">Ausgewählte Vendorliste</h2>
<div class="rebuild-actions">
<button id="gvl-rebuild-normalized-button" type="button" disabled>
Lokale GVL-Daten neu aufbauen (Reparatur)
</button>
<span
id="gvl-rebuild-normalized-status"
class="fetch-status"
aria-live="polite"
></span>
</div>
<div id="gvl-snapshot-summary"></div>
</section>
<details id="gvl-vendor-overview-details" class="vendor-overview">
<summary id="gvl-vendor-overview-summary">
Vendoren-Übersicht anzeigen
</summary>
<p id="gvl-vendor-overview-empty" class="empty-state" hidden>
Keine normalisierten Vendoren für diese Vendorliste vorhanden.
</p>
<div id="gvl-vendor-overview-content" class="snapshot-list-wrap" hidden>
<table class="snapshot-list">
<thead>
<tr>
<th scope="col">Vendor-ID</th>
<th scope="col">Name</th>
<th scope="col">Deleted Date</th>
</tr>
</thead>
<tbody id="gvl-vendor-overview-list"></tbody>
</table>
</div>
</details>
<details class="technical-details">
<summary>Technische Feldnamen</summary>
<pre id="gvl-technical-fields"></pre>
@@ -61,6 +92,30 @@
</details>
</div>
</section>
<section class="panel" aria-labelledby="vendor-detail-title">
<h2 id="vendor-detail-title">Lokaler Vendor-Nachweis</h2>
<form id="gvl-vendor-detail-form" class="vendor-detail-form">
<label for="gvl-vendor-id-input">Vendor-ID</label>
<input
id="gvl-vendor-id-input"
type="number"
min="1"
step="1"
inputmode="numeric"
placeholder="977"
>
<button id="gvl-vendor-detail-button" type="submit">
Vendor anzeigen
</button>
</form>
<div
id="gvl-vendor-detail-status"
class="fetch-status"
aria-live="polite"
></div>
<div id="gvl-vendor-detail-result" class="vendor-detail-result"></div>
</section>
</main>
<script src="gvl-explorer.js"></script>
+398 -8
Datei anzeigen
@@ -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";