Commits vergleichen
2 Commits
| Autor | SHA1 | Datum | |
|---|---|---|---|
| 08d5a6ccc2 | |||
| 3fd40348b5 |
+341
-13
@@ -22,7 +22,7 @@ browser.webRequest.onBeforeRequest.addListener(
|
||||
{ urls: ["<all_urls>"] }
|
||||
);
|
||||
|
||||
void runStartupGvlAutoUpdateCheck();
|
||||
console.info("GVL auto update disabled; use manual sync");
|
||||
|
||||
async function handleVendorGetMessage(message, sender) {
|
||||
if (!message) {
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 aus Web laden
|
||||
</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>
|
||||
|
||||
@@ -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 wird aus Web geladen...");
|
||||
|
||||
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 aus Web konnte nicht geladen 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 "GVL aus Web geladen, neue Revision gespeichert und normalisiert.";
|
||||
}
|
||||
|
||||
if (result?.syncStatus === "known_gvl_rebuilt_from_local_evidence") {
|
||||
return "GVL aus Web geprüft; bekannte Revision aus lokaler Evidence neu aufgebaut.";
|
||||
}
|
||||
|
||||
if (result?.syncStatus === "current_and_locally_available") {
|
||||
return "GVL aus Web geprüft; aktuelle Revision ist lokal vollständig verfügbar.";
|
||||
}
|
||||
|
||||
return result?.alreadyKnown
|
||||
? "GVL aus Web geprüft; Revision ist lokal verfügbar."
|
||||
: "GVL aus Web geladen.";
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren