Restructure VG-Observe into focused explorer views
Dieser Commit ist enthalten in:
@@ -0,0 +1,123 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-dashboard {
|
||||||
|
width: min(1040px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-header {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e5edf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 760px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
width: fit-content;
|
||||||
|
color: #bfdbfe;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-status {
|
||||||
|
min-height: 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid,
|
||||||
|
.area-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.area-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid div,
|
||||||
|
.area-grid article {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid dt {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid dd {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.analysis-dashboard {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid,
|
||||||
|
.area-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VG-Observe Analyse-Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="analysis-dashboard.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="analysis-dashboard">
|
||||||
|
<header class="analysis-header">
|
||||||
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||||
|
<h1>Analyse-Dashboard</h1>
|
||||||
|
<p>
|
||||||
|
Diese Ansicht bereitet technische Prüfungen zwischen
|
||||||
|
Consent-Zuständen, Vendorlisten und beobachteten Requests vor.
|
||||||
|
Aktuell werden nur vorhandene Datenbestände und vorbereitete
|
||||||
|
Analysebereiche angezeigt.
|
||||||
|
</p>
|
||||||
|
<div id="analysis-status" class="analysis-status" aria-live="polite">
|
||||||
|
Lade Datenbestände
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="panel" aria-labelledby="summary-title">
|
||||||
|
<h2 id="summary-title">Datenbestände</h2>
|
||||||
|
<dl class="status-grid">
|
||||||
|
<div>
|
||||||
|
<dt>Anzahl Consent States</dt>
|
||||||
|
<dd id="summary-consent-states">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Anzahl Observed Requests</dt>
|
||||||
|
<dd id="summary-observed-requests">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Lokal gespeicherte Vendorlisten</dt>
|
||||||
|
<dd id="summary-gvl-snapshots">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Lokal aktuelle Vendorlisten-Version</dt>
|
||||||
|
<dd id="summary-current-gvl-version">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Analyse-Engine</dt>
|
||||||
|
<dd>noch nicht aktiv</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" aria-labelledby="areas-title">
|
||||||
|
<h2 id="areas-title">Vorbereitete Analysebereiche</h2>
|
||||||
|
<div class="area-grid">
|
||||||
|
<article>
|
||||||
|
<h3>Consent ↔ Vendorliste</h3>
|
||||||
|
<p>Analyse noch nicht ausgeführt.</p>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h3>Consent ↔ beobachtete Requests</h3>
|
||||||
|
<p>Analyse noch nicht ausgeführt.</p>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h3>Request-Hosts ↔ bekannte Vendoren</h3>
|
||||||
|
<p>Keine erkennbare Zuordnung berechnet. Analyse noch nicht ausgeführt.</p>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h3>Potenziell erklärungsbedürftige technische Diskrepanzen</h3>
|
||||||
|
<p>Keine Bewertung vorgenommen. Analyse noch nicht ausgeführt.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="analysis-dashboard.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const analysisStatus = document.getElementById("analysis-status");
|
||||||
|
const summaryConsentStates = document.getElementById("summary-consent-states");
|
||||||
|
const summaryObservedRequests = document.getElementById(
|
||||||
|
"summary-observed-requests"
|
||||||
|
);
|
||||||
|
const summaryGvlSnapshots = document.getElementById("summary-gvl-snapshots");
|
||||||
|
const summaryCurrentGvlVersion = document.getElementById(
|
||||||
|
"summary-current-gvl-version"
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
await renderAnalysisSummary();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function renderAnalysisSummary() {
|
||||||
|
try {
|
||||||
|
const [evidenceStatus, gvlStatus] = await Promise.all([
|
||||||
|
getEvidenceRetentionStatus(),
|
||||||
|
getLatestGvlUpdateStatus()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const storeCounts = evidenceStatus.storeCounts ?? {};
|
||||||
|
const status = gvlStatus.status ?? {};
|
||||||
|
|
||||||
|
summaryConsentStates.textContent = String(storeCounts.consent_states ?? 0);
|
||||||
|
summaryObservedRequests.textContent = String(
|
||||||
|
storeCounts.observed_requests ?? 0
|
||||||
|
);
|
||||||
|
summaryGvlSnapshots.textContent = String(storeCounts.gvl_snapshots ?? 0);
|
||||||
|
summaryCurrentGvlVersion.textContent = formatNullable(
|
||||||
|
status.latestLocalVendorListVersion ?? status.currentVendorListVersion
|
||||||
|
);
|
||||||
|
analysisStatus.textContent = "Datenbestände geladen";
|
||||||
|
} catch (error) {
|
||||||
|
summaryConsentStates.textContent = "-";
|
||||||
|
summaryObservedRequests.textContent = "-";
|
||||||
|
summaryGvlSnapshots.textContent = "-";
|
||||||
|
summaryCurrentGvlVersion.textContent = "-";
|
||||||
|
analysisStatus.textContent = "Datenbestände konnten nicht geladen werden";
|
||||||
|
console.warn("VG-Observe analysis dashboard summary failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEvidenceRetentionStatus() {
|
||||||
|
const status = await browser.runtime.sendMessage({
|
||||||
|
type: "get_evidence_retention_status"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status?.success) {
|
||||||
|
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLatestGvlUpdateStatus() {
|
||||||
|
const status = await browser.runtime.sendMessage({
|
||||||
|
type: "get_latest_gvl_update_status"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status?.success) {
|
||||||
|
throw new Error(status?.error ?? "get_latest_gvl_update_status_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNullable(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
+860
-16
@@ -5,6 +5,13 @@ console.log("VG-Observe background loaded");
|
|||||||
const OFFICIAL_IAB_GVL_URL =
|
const OFFICIAL_IAB_GVL_URL =
|
||||||
"https://vendor-list.consensu.org/v3/vendor-list.json";
|
"https://vendor-list.consensu.org/v3/vendor-list.json";
|
||||||
const EVIDENCE_RECORDING_SOURCE = "vendorget_background_mirror";
|
const EVIDENCE_RECORDING_SOURCE = "vendorget_background_mirror";
|
||||||
|
const GVL_AUTO_UPDATE_SOURCE = "extension_startup";
|
||||||
|
const AUTO_GVL_CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const AUTO_GVL_CHECK_STORAGE_KEY = "vendorgetAutoGvlUpdateStatus";
|
||||||
|
|
||||||
|
let isAutoGvlCheckRunning = false;
|
||||||
|
let lastAutoGvlCheckStartedAt = null;
|
||||||
|
let latestGvlUpdateStatus = null;
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message, sender) =>
|
browser.runtime.onMessage.addListener((message, sender) =>
|
||||||
handleVendorGetMessage(message, sender)
|
handleVendorGetMessage(message, sender)
|
||||||
@@ -15,6 +22,8 @@ browser.webRequest.onBeforeRequest.addListener(
|
|||||||
{ urls: ["<all_urls>"] }
|
{ urls: ["<all_urls>"] }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void runStartupGvlAutoUpdateCheck();
|
||||||
|
|
||||||
async function handleVendorGetMessage(message, sender) {
|
async function handleVendorGetMessage(message, sender) {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return null;
|
return null;
|
||||||
@@ -48,6 +57,26 @@ async function handleVendorGetMessage(message, sender) {
|
|||||||
return handleGetEvidenceRetentionStatusMessage();
|
return handleGetEvidenceRetentionStatusMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "get_latest_gvl_update_status") {
|
||||||
|
return handleGetLatestGvlUpdateStatusMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "list_gvl_snapshots") {
|
||||||
|
return handleListGvlSnapshotsMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "get_gvl_snapshot_summary") {
|
||||||
|
return handleGetGvlSnapshotSummaryMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "get_latest_consent_state") {
|
||||||
|
return handleGetLatestConsentStateMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "list_recent_consent_states") {
|
||||||
|
return handleListRecentConsentStatesMessage();
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === "purge_unlocked_evidence_records") {
|
if (message.type === "purge_unlocked_evidence_records") {
|
||||||
return handlePurgeUnlockedEvidenceRecordsMessage();
|
return handlePurgeUnlockedEvidenceRecordsMessage();
|
||||||
}
|
}
|
||||||
@@ -140,6 +169,349 @@ async function handleGetEvidenceRetentionStatusMessage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGetLatestGvlUpdateStatusMessage() {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
|
||||||
|
|
||||||
|
if (latestGvlUpdateStatus) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
status: {
|
||||||
|
...latestGvlUpdateStatus,
|
||||||
|
latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttleState = await getAutoGvlCheckThrottleState();
|
||||||
|
const throttleDecision = shouldThrottleAutoGvlCheck(
|
||||||
|
throttleState?.lastAutoGvlCheckAt ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
status: {
|
||||||
|
checkedAt: null,
|
||||||
|
previousVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
previousSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null,
|
||||||
|
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
|
||||||
|
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
|
||||||
|
result: "not_checked_since_background_start",
|
||||||
|
message: "Noch kein automatischer GVL-Update-Check seit Background-Start."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleListGvlSnapshotsMessage() {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const snapshots = await listRecentGvlSnapshots(db, 25);
|
||||||
|
const snapshotsWithEvents = await Promise.all(
|
||||||
|
snapshots.map(async (snapshot) => {
|
||||||
|
const event = await getAnyGvlSnapshotEventBySha256(db, snapshot.sha256);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||||
|
sha256: snapshot.sha256 ?? null,
|
||||||
|
fetchedAt: snapshot.fetchedAt ?? null,
|
||||||
|
sourceUrl: snapshot.sourceUrl ?? null,
|
||||||
|
eventType: event?.eventType ?? null,
|
||||||
|
eventCapturedAt: event?.capturedAt ?? null
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
gvlSnapshots: snapshotsWithEvents
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetGvlSnapshotSummaryMessage(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 vendorListVersion = snapshot.vendorListVersion ?? null;
|
||||||
|
const event = await getAnyGvlSnapshotEventBySha256(db, snapshot.sha256);
|
||||||
|
const counts = await countGvlNormalizedRecordsForVersion(
|
||||||
|
db,
|
||||||
|
vendorListVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
summary: {
|
||||||
|
vendorListVersion,
|
||||||
|
sha256: snapshot.sha256 ?? null,
|
||||||
|
fetchedAt: snapshot.fetchedAt ?? null,
|
||||||
|
sourceUrl: snapshot.sourceUrl ?? null,
|
||||||
|
eventType: event?.eventType ?? null,
|
||||||
|
eventCapturedAt: event?.capturedAt ?? null,
|
||||||
|
vendorCount: snapshot.vendorCount ?? counts.vendorCount,
|
||||||
|
purposeCount: snapshot.purposeCount ?? counts.purposeCount,
|
||||||
|
specialPurposeCount: counts.specialPurposeCount,
|
||||||
|
featureCount: counts.featureCount,
|
||||||
|
specialFeatureCount: counts.specialFeatureCount,
|
||||||
|
dataCategoryCount: counts.dataCategoryCount,
|
||||||
|
vendorRelationshipCount: counts.vendorRelationshipCount,
|
||||||
|
technicalFields: {
|
||||||
|
snapshotStore: "gvl_snapshots",
|
||||||
|
vendorListVersion: "vendorListVersion",
|
||||||
|
sha256: "sha256",
|
||||||
|
fetchedAt: "fetchedAt",
|
||||||
|
sourceUrl: "sourceUrl"
|
||||||
|
},
|
||||||
|
diagnostics: {
|
||||||
|
eventDiagnostics: event?.diagnostics ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetLatestConsentStateMessage() {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const latestStateOrNull = await getLatestConsentState(db);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
consentState: latestStateOrNull
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleListRecentConsentStatesMessage() {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const consentStates = await listRecentConsentStates(db, 25);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
consentStates
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestConsentState(db) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(["consent_states"], "readonly");
|
||||||
|
const statesStore = tx.objectStore("consent_states");
|
||||||
|
const lastSeenAtIndex = statesStore.index("lastSeenAt");
|
||||||
|
const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
|
||||||
|
let latestStateOrNull = null;
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
latestStateOrNull = cursor.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(latestStateOrNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listRecentConsentStates(db, limit) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const consentStates = [];
|
||||||
|
const tx = db.transaction(["consent_states"], "readonly");
|
||||||
|
const statesStore = tx.objectStore("consent_states");
|
||||||
|
const lastSeenAtIndex = statesStore.index("lastSeenAt");
|
||||||
|
const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor || consentStates.length >= limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
consentStates.push(cursor.value);
|
||||||
|
|
||||||
|
if (consentStates.length < limit) {
|
||||||
|
cursor.continue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(consentStates);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listRecentGvlSnapshots(db, limit) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const snapshots = [];
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||||
|
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
|
||||||
|
const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
|
||||||
|
const cursorRequest = vendorListVersionIndex.openCursor(null, "prev");
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor || snapshots.length >= limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots.push(cursor.value);
|
||||||
|
|
||||||
|
if (snapshots.length < limit) {
|
||||||
|
cursor.continue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(snapshots);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnyGvlSnapshotEventBySha256(db, sha256) {
|
||||||
|
if (!sha256) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(
|
||||||
|
[VENDORGET_STORE_NAMES.gvlSnapshotEvents],
|
||||||
|
"readonly"
|
||||||
|
);
|
||||||
|
const eventsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshotEvents);
|
||||||
|
const sha256Index = eventsStore.index("sha256");
|
||||||
|
const getRequest = sha256Index.get(sha256);
|
||||||
|
let eventOrNull = null;
|
||||||
|
|
||||||
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
eventOrNull = getRequest.result ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(eventOrNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGvlSnapshotByIdentifier(db, { sha256, vendorListVersion }) {
|
||||||
|
if (sha256) {
|
||||||
|
return getGvlSnapshotBySha256(db, sha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vendorListVersion !== null && vendorListVersion !== undefined) {
|
||||||
|
return getGvlSnapshotByVendorListVersion(db, vendorListVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlSnapshotBySha256(db, sha256) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||||
|
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
|
||||||
|
const getRequest = snapshotsStore.get(sha256);
|
||||||
|
let snapshotOrNull = null;
|
||||||
|
|
||||||
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
snapshotOrNull = getRequest.result ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(snapshotOrNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlSnapshotByVendorListVersion(db, vendorListVersion) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||||
|
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
|
||||||
|
const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
|
||||||
|
const getRequest = vendorListVersionIndex.get(vendorListVersion);
|
||||||
|
let snapshotOrNull = null;
|
||||||
|
|
||||||
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
snapshotOrNull = getRequest.result ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(snapshotOrNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function countGvlNormalizedRecordsForVersion(db, vendorListVersion) {
|
||||||
|
if (vendorListVersion === null || vendorListVersion === undefined) {
|
||||||
|
return Promise.resolve({
|
||||||
|
vendorCount: 0,
|
||||||
|
purposeCount: 0,
|
||||||
|
specialPurposeCount: 0,
|
||||||
|
featureCount: 0,
|
||||||
|
specialFeatureCount: 0,
|
||||||
|
dataCategoryCount: 0,
|
||||||
|
vendorRelationshipCount: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const countDefinitions = [
|
||||||
|
["vendorCount", VENDORGET_STORE_NAMES.gvlVendors],
|
||||||
|
["purposeCount", VENDORGET_STORE_NAMES.gvlPurposes],
|
||||||
|
["specialPurposeCount", VENDORGET_STORE_NAMES.gvlSpecialPurposes],
|
||||||
|
["featureCount", VENDORGET_STORE_NAMES.gvlFeatures],
|
||||||
|
["specialFeatureCount", VENDORGET_STORE_NAMES.gvlSpecialFeatures],
|
||||||
|
["dataCategoryCount", VENDORGET_STORE_NAMES.gvlDataCategories],
|
||||||
|
[
|
||||||
|
"vendorRelationshipCount",
|
||||||
|
VENDORGET_STORE_NAMES.gvlVendorRelationships
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const counts = {};
|
||||||
|
const tx = db.transaction(
|
||||||
|
countDefinitions.map((definition) => definition[1]),
|
||||||
|
"readonly"
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(counts);
|
||||||
|
|
||||||
|
for (const [countName, storeName] of countDefinitions) {
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
const vendorListVersionIndex = store.index("vendorListVersion");
|
||||||
|
const countRequest = vendorListVersionIndex.count(
|
||||||
|
IDBKeyRange.only(vendorListVersion)
|
||||||
|
);
|
||||||
|
|
||||||
|
countRequest.onsuccess = () => {
|
||||||
|
counts[countName] = countRequest.result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePurgeUnlockedEvidenceRecordsMessage() {
|
async function handlePurgeUnlockedEvidenceRecordsMessage() {
|
||||||
const db = await openVendorGetDb();
|
const db = await openVendorGetDb();
|
||||||
|
|
||||||
@@ -190,26 +562,13 @@ function isGvlImportCandidate(value) {
|
|||||||
|
|
||||||
async function handleFetchOfficialGvlMessage() {
|
async function handleFetchOfficialGvlMessage() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
|
const { rawJson, responseStatus } = await fetchOfficialGvlJson();
|
||||||
method: "GET",
|
|
||||||
cache: "no-store"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "official_gvl_fetch_failed",
|
|
||||||
responseStatus: response.status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawJson = await response.json();
|
|
||||||
|
|
||||||
if (!isGvlImportCandidate(rawJson)) {
|
if (!isGvlImportCandidate(rawJson)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "invalid_gvl_json",
|
error: "invalid_gvl_json",
|
||||||
responseStatus: response.status
|
responseStatus: responseStatus
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +578,7 @@ async function handleFetchOfficialGvlMessage() {
|
|||||||
fetchedAt: new Date().toISOString(),
|
fetchedAt: new Date().toISOString(),
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
ingestionSource: "official_iab_fetch",
|
ingestionSource: "official_iab_fetch",
|
||||||
responseStatus: response.status
|
responseStatus: responseStatus
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,6 +598,491 @@ async function handleFetchOfficialGvlMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchOfficialGvlJson() {
|
||||||
|
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error("official_gvl_fetch_failed");
|
||||||
|
|
||||||
|
error.responseStatus = response.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawJson: await response.json(),
|
||||||
|
responseStatus: response.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runStartupGvlAutoUpdateCheck() {
|
||||||
|
if (isAutoGvlCheckRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoGvlCheckRunning = true;
|
||||||
|
|
||||||
|
const throttleState = await getAutoGvlCheckThrottleState();
|
||||||
|
const throttleDecision = shouldThrottleAutoGvlCheck(
|
||||||
|
throttleState?.lastAutoGvlCheckAt ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (throttleDecision.throttled) {
|
||||||
|
await handleThrottledStartupGvlAutoUpdateCheck(
|
||||||
|
throttleState,
|
||||||
|
throttleDecision
|
||||||
|
);
|
||||||
|
isAutoGvlCheckRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAutoGvlCheckStartedAt = new Date().toISOString();
|
||||||
|
await storeAutoGvlCheckThrottleState({
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
lastAutoGvlCheckResult: "started"
|
||||||
|
});
|
||||||
|
|
||||||
|
const startedStatus = {
|
||||||
|
checkedAt: lastAutoGvlCheckStartedAt,
|
||||||
|
previousVendorListVersion: null,
|
||||||
|
currentVendorListVersion: null,
|
||||||
|
previousSnapshotSha256: null,
|
||||||
|
currentSnapshotSha256: null,
|
||||||
|
latestLocalVendorListVersion: null,
|
||||||
|
latestLocalSnapshotSha256: null,
|
||||||
|
latestLocalFetchedAt: null,
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
|
||||||
|
lastAutoGvlCheckStartedAt
|
||||||
|
),
|
||||||
|
result: "started",
|
||||||
|
message: "Automatischer GVL-Update-Check gestartet."
|
||||||
|
};
|
||||||
|
|
||||||
|
latestGvlUpdateStatus = startedStatus;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const previousSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
|
||||||
|
const previousVendorListVersion =
|
||||||
|
previousSnapshot?.vendorListVersion ?? null;
|
||||||
|
const previousSnapshotSha256 = previousSnapshot?.sha256 ?? null;
|
||||||
|
const previousFetchedAt = previousSnapshot?.fetchedAt ?? null;
|
||||||
|
|
||||||
|
latestGvlUpdateStatus = {
|
||||||
|
checkedAt: lastAutoGvlCheckStartedAt,
|
||||||
|
previousVendorListVersion,
|
||||||
|
currentVendorListVersion: null,
|
||||||
|
previousSnapshotSha256,
|
||||||
|
currentSnapshotSha256: null,
|
||||||
|
latestLocalVendorListVersion: previousVendorListVersion,
|
||||||
|
latestLocalSnapshotSha256: previousSnapshotSha256,
|
||||||
|
latestLocalFetchedAt: previousFetchedAt,
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
|
||||||
|
lastAutoGvlCheckStartedAt
|
||||||
|
),
|
||||||
|
result: "started",
|
||||||
|
message: "Automatischer GVL-Update-Check gestartet."
|
||||||
|
};
|
||||||
|
|
||||||
|
await recordGvlAutoUpdateEvent(db, {
|
||||||
|
eventType: "gvl_auto_update_check_started",
|
||||||
|
checkedAt: lastAutoGvlCheckStartedAt,
|
||||||
|
previousSnapshot,
|
||||||
|
currentVendorListVersion: null,
|
||||||
|
currentSnapshotSha256: null,
|
||||||
|
result: "started"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { rawJson, responseStatus } = await fetchOfficialGvlJson();
|
||||||
|
|
||||||
|
if (!isGvlImportCandidate(rawJson)) {
|
||||||
|
throw new Error("invalid_gvl_json");
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVendorListVersion = rawJson.vendorListVersion ?? null;
|
||||||
|
const currentSnapshotSha256 =
|
||||||
|
await VendorGetGvlService.calculateGvlSnapshotSha256(rawJson);
|
||||||
|
const newVersionDetected = isNewerGvlVendorListVersion(
|
||||||
|
currentVendorListVersion,
|
||||||
|
previousVendorListVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
const ingestResult = await VendorGetGvlService.ingestGvlSnapshot(
|
||||||
|
db,
|
||||||
|
rawJson,
|
||||||
|
{
|
||||||
|
sourceUrl: OFFICIAL_IAB_GVL_URL,
|
||||||
|
fetchedAt: new Date().toISOString(),
|
||||||
|
diagnostics: {
|
||||||
|
ingestionSource: "official_iab_auto_update",
|
||||||
|
responseStatus: responseStatus,
|
||||||
|
updateCheckSource: GVL_AUTO_UPDATE_SOURCE,
|
||||||
|
checkedAt: lastAutoGvlCheckStartedAt,
|
||||||
|
previousVendorListVersion: previousVendorListVersion,
|
||||||
|
previousSnapshotSha256: previousSnapshotSha256,
|
||||||
|
previousFetchedAt: previousFetchedAt,
|
||||||
|
currentVendorListVersion: currentVendorListVersion,
|
||||||
|
currentSnapshotSha256: currentSnapshotSha256,
|
||||||
|
result: newVersionDetected ? "new_version_detected" : "no_change"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let normalizationSummary = null;
|
||||||
|
|
||||||
|
if (!ingestResult.alreadyKnown && newVersionDetected) {
|
||||||
|
normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
|
||||||
|
ingestResult.snapshot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = buildGvlAutoUpdateResult({
|
||||||
|
newVersionDetected,
|
||||||
|
alreadyKnown: ingestResult.alreadyKnown
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
checkedAt: lastAutoGvlCheckStartedAt,
|
||||||
|
previousVendorListVersion,
|
||||||
|
currentVendorListVersion,
|
||||||
|
previousSnapshotSha256,
|
||||||
|
currentSnapshotSha256,
|
||||||
|
latestLocalVendorListVersion: ingestResult.snapshot.vendorListVersion,
|
||||||
|
latestLocalSnapshotSha256: ingestResult.snapshot.sha256,
|
||||||
|
latestLocalFetchedAt: ingestResult.snapshot.fetchedAt ?? null,
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
|
||||||
|
lastAutoGvlCheckStartedAt
|
||||||
|
),
|
||||||
|
result,
|
||||||
|
message: buildGvlAutoUpdateMessage(result),
|
||||||
|
normalizationSummary
|
||||||
|
};
|
||||||
|
|
||||||
|
latestGvlUpdateStatus = status;
|
||||||
|
await storeAutoGvlCheckThrottleState({
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
lastAutoGvlCheckResult: result
|
||||||
|
});
|
||||||
|
|
||||||
|
await recordGvlAutoUpdateEvent(db, {
|
||||||
|
eventType: buildGvlAutoUpdateEventType(result),
|
||||||
|
checkedAt: lastAutoGvlCheckStartedAt,
|
||||||
|
previousSnapshot,
|
||||||
|
currentVendorListVersion,
|
||||||
|
currentSnapshotSha256,
|
||||||
|
currentSnapshot: ingestResult.snapshot,
|
||||||
|
result,
|
||||||
|
normalizationSummary
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("VG-Observe automatic official GVL update check failed", error);
|
||||||
|
|
||||||
|
const checkedAt = lastAutoGvlCheckStartedAt ?? new Date().toISOString();
|
||||||
|
const errorMessage = error?.message ?? String(error);
|
||||||
|
|
||||||
|
latestGvlUpdateStatus = {
|
||||||
|
checkedAt,
|
||||||
|
previousVendorListVersion: latestGvlUpdateStatus?.previousVendorListVersion ?? null,
|
||||||
|
currentVendorListVersion: latestGvlUpdateStatus?.currentVendorListVersion ?? null,
|
||||||
|
previousSnapshotSha256: latestGvlUpdateStatus?.previousSnapshotSha256 ?? null,
|
||||||
|
currentSnapshotSha256: latestGvlUpdateStatus?.currentSnapshotSha256 ?? null,
|
||||||
|
latestLocalVendorListVersion:
|
||||||
|
latestGvlUpdateStatus?.latestLocalVendorListVersion ?? null,
|
||||||
|
latestLocalSnapshotSha256:
|
||||||
|
latestGvlUpdateStatus?.latestLocalSnapshotSha256 ?? null,
|
||||||
|
latestLocalFetchedAt: latestGvlUpdateStatus?.latestLocalFetchedAt ?? null,
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
|
||||||
|
lastAutoGvlCheckStartedAt
|
||||||
|
),
|
||||||
|
result: "error",
|
||||||
|
message: "Auto-Check fehlgeschlagen",
|
||||||
|
error: errorMessage
|
||||||
|
};
|
||||||
|
await storeAutoGvlCheckThrottleState({
|
||||||
|
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
|
||||||
|
lastAutoGvlCheckResult: "error"
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const previousSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
|
||||||
|
|
||||||
|
await recordGvlAutoUpdateEvent(db, {
|
||||||
|
eventType: "gvl_auto_update_error",
|
||||||
|
checkedAt,
|
||||||
|
previousSnapshot,
|
||||||
|
currentVendorListVersion: null,
|
||||||
|
currentSnapshotSha256: null,
|
||||||
|
result: "error",
|
||||||
|
error: errorMessage
|
||||||
|
});
|
||||||
|
} catch (eventError) {
|
||||||
|
console.warn("VG-Observe automatic GVL error event failed", eventError);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isAutoGvlCheckRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleThrottledStartupGvlAutoUpdateCheck(
|
||||||
|
throttleState,
|
||||||
|
throttleDecision
|
||||||
|
) {
|
||||||
|
const checkedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
|
||||||
|
|
||||||
|
latestGvlUpdateStatus = {
|
||||||
|
checkedAt,
|
||||||
|
previousVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
previousSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null,
|
||||||
|
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
|
||||||
|
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
|
||||||
|
result: "throttled",
|
||||||
|
message: "Auto-Check wegen 24h-Throttling übersprungen."
|
||||||
|
};
|
||||||
|
|
||||||
|
await recordGvlAutoUpdateEvent(db, {
|
||||||
|
eventType: "gvl_auto_update_throttled",
|
||||||
|
checkedAt,
|
||||||
|
previousSnapshot: latestSnapshot,
|
||||||
|
currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
|
||||||
|
currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
|
||||||
|
result: "throttled",
|
||||||
|
throttleDiagnostics: {
|
||||||
|
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
|
||||||
|
throttleMs: AUTO_GVL_CHECK_THROTTLE_MS,
|
||||||
|
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("VG-Observe automatic GVL throttled event failed", error);
|
||||||
|
|
||||||
|
latestGvlUpdateStatus = {
|
||||||
|
checkedAt,
|
||||||
|
previousVendorListVersion: null,
|
||||||
|
currentVendorListVersion: null,
|
||||||
|
previousSnapshotSha256: null,
|
||||||
|
currentSnapshotSha256: null,
|
||||||
|
latestLocalVendorListVersion: null,
|
||||||
|
latestLocalSnapshotSha256: null,
|
||||||
|
latestLocalFetchedAt: null,
|
||||||
|
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
|
||||||
|
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
|
||||||
|
result: "throttled",
|
||||||
|
message:
|
||||||
|
"Auto-Check wegen 24h-Throttling übersprungen; Status-Event konnte nicht geschrieben werden."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAutoGvlCheckThrottleState() {
|
||||||
|
try {
|
||||||
|
const storedValue = await browser.storage.local.get(
|
||||||
|
AUTO_GVL_CHECK_STORAGE_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
return storedValue[AUTO_GVL_CHECK_STORAGE_KEY] ?? {};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("VG-Observe automatic GVL throttle state unavailable", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storeAutoGvlCheckThrottleState(state) {
|
||||||
|
try {
|
||||||
|
await browser.storage.local.set({
|
||||||
|
[AUTO_GVL_CHECK_STORAGE_KEY]: state
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("VG-Observe automatic GVL throttle state write failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldThrottleAutoGvlCheck(lastAutoGvlCheckAt) {
|
||||||
|
if (!lastAutoGvlCheckAt) {
|
||||||
|
return {
|
||||||
|
throttled: false,
|
||||||
|
nextAllowedAutoCheckAt: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCheckTime = Date.parse(lastAutoGvlCheckAt);
|
||||||
|
|
||||||
|
if (Number.isNaN(lastCheckTime)) {
|
||||||
|
return {
|
||||||
|
throttled: false,
|
||||||
|
nextAllowedAutoCheckAt: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAllowedTime = lastCheckTime + AUTO_GVL_CHECK_THROTTLE_MS;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
return {
|
||||||
|
throttled: now < nextAllowedTime,
|
||||||
|
nextAllowedAutoCheckAt: new Date(nextAllowedTime).toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextAllowedAutoGvlCheckAt(lastAutoGvlCheckAt) {
|
||||||
|
if (!lastAutoGvlCheckAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCheckTime = Date.parse(lastAutoGvlCheckAt);
|
||||||
|
|
||||||
|
if (Number.isNaN(lastCheckTime)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(lastCheckTime + AUTO_GVL_CHECK_THROTTLE_MS).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestGvlSnapshotByVendorListVersion(db) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
|
||||||
|
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
|
||||||
|
const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
|
||||||
|
const cursorRequest = vendorListVersionIndex.openCursor(null, "prev");
|
||||||
|
let latestSnapshotOrNull = null;
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
latestSnapshotOrNull = cursor.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(latestSnapshotOrNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNewerGvlVendorListVersion(currentVersion, previousVersion) {
|
||||||
|
if (currentVersion === null || currentVersion === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousVersion === null || previousVersion === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(currentVersion) > Number(previousVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function normalizeGvlSnapshotWithExistingPipeline(snapshot) {
|
||||||
|
return {
|
||||||
|
vendors: await normalizeGvlVendorsFromSnapshot(snapshot),
|
||||||
|
catalogs: await normalizeGvlCatalogsFromSnapshot(snapshot),
|
||||||
|
vendorRelationships:
|
||||||
|
await normalizeGvlVendorRelationshipsFromSnapshot(snapshot)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlAutoUpdateResult({ newVersionDetected, alreadyKnown }) {
|
||||||
|
if (newVersionDetected && !alreadyKnown) {
|
||||||
|
return "stored";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVersionDetected && alreadyKnown) {
|
||||||
|
return "already_known";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newVersionDetected && !alreadyKnown) {
|
||||||
|
return "stored";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "no_change";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlAutoUpdateEventType(result) {
|
||||||
|
if (result === "stored") {
|
||||||
|
return "gvl_auto_update_stored";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === "already_known") {
|
||||||
|
return "gvl_auto_update_detected";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "gvl_auto_update_no_change";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlAutoUpdateMessage(result) {
|
||||||
|
if (result === "stored") {
|
||||||
|
return "Neue offizielle IAB-Europe-Vendorliste gespeichert.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === "already_known") {
|
||||||
|
return "Offizielle IAB-Europe-Vendorliste war bereits lokal bekannt.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Keine neuere offizielle IAB-Europe-Vendorliste gefunden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordGvlAutoUpdateEvent(
|
||||||
|
db,
|
||||||
|
{
|
||||||
|
eventType,
|
||||||
|
checkedAt,
|
||||||
|
previousSnapshot,
|
||||||
|
currentVendorListVersion,
|
||||||
|
currentSnapshotSha256,
|
||||||
|
currentSnapshot,
|
||||||
|
result,
|
||||||
|
normalizationSummary,
|
||||||
|
error,
|
||||||
|
throttleDiagnostics
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return VendorGetGvlService.recordGvlSnapshotEvent(db, {
|
||||||
|
eventType,
|
||||||
|
capturedAt: checkedAt,
|
||||||
|
vendorListVersion:
|
||||||
|
currentVendorListVersion ?? previousSnapshot?.vendorListVersion ?? null,
|
||||||
|
sha256: currentSnapshotSha256 ?? previousSnapshot?.sha256 ?? null,
|
||||||
|
sourceUrl: OFFICIAL_IAB_GVL_URL,
|
||||||
|
diagnostics: {
|
||||||
|
updateCheckSource: GVL_AUTO_UPDATE_SOURCE,
|
||||||
|
checkedAt,
|
||||||
|
previousVendorListVersion: previousSnapshot?.vendorListVersion ?? null,
|
||||||
|
currentVendorListVersion: currentVendorListVersion ?? null,
|
||||||
|
previousSnapshotSha256: previousSnapshot?.sha256 ?? null,
|
||||||
|
currentSnapshotSha256: currentSnapshotSha256 ?? null,
|
||||||
|
previousFetchedAt: previousSnapshot?.fetchedAt ?? null,
|
||||||
|
currentFetchedAt: currentSnapshot?.fetchedAt ?? null,
|
||||||
|
result,
|
||||||
|
vendorCountBefore: previousSnapshot?.vendorCount ?? null,
|
||||||
|
vendorCountAfter: currentSnapshot?.vendorCount ?? null,
|
||||||
|
purposeCountBefore: previousSnapshot?.purposeCount ?? null,
|
||||||
|
purposeCountAfter: currentSnapshot?.purposeCount ?? null,
|
||||||
|
normalizationSummary: normalizationSummary ?? null,
|
||||||
|
error: error ?? null,
|
||||||
|
...(throttleDiagnostics ?? {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildConsentStateV1(rawCapture, sender, latestPingData) {
|
function buildConsentStateV1(rawCapture, sender, latestPingData) {
|
||||||
const decodedTcString = decodeTcStringCoreMetadata(rawCapture?.tcString ?? null);
|
const decodedTcString = decodeTcStringCoreMetadata(rawCapture?.tcString ?? null);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer {
|
||||||
|
width: min(1180px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer-header {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 760px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
width: fit-content;
|
||||||
|
color: #bfdbfe;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-help {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #182231;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list-wrap {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list {
|
||||||
|
min-width: 1180px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list th,
|
||||||
|
.consent-state-list td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list tbody tr:hover,
|
||||||
|
.consent-state-list tbody tr:focus {
|
||||||
|
outline: 0;
|
||||||
|
background: #263449;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list tbody tr.is-selected {
|
||||||
|
background: #1e3a5f;
|
||||||
|
box-shadow: inset 3px 0 0 #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list .numeric {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-state-list .url-cell {
|
||||||
|
max-width: 220px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consent-detail {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table th,
|
||||||
|
.inspector-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table th {
|
||||||
|
width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table th span,
|
||||||
|
.inspector-table th small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table th small {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table th .inspector-help {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table .inspector-value {
|
||||||
|
width: 190px;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 260px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e5edf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table .inspector-explanation {
|
||||||
|
width: auto;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table td {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-details {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-details pre {
|
||||||
|
max-height: 220px;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #0f172a;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.explorer {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table th {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-table .inspector-value,
|
||||||
|
.inspector-table .inspector-explanation {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VG-Observe Consent-Explorer</title>
|
||||||
|
<link rel="stylesheet" href="consent-explorer.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="explorer">
|
||||||
|
<header class="explorer-header">
|
||||||
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||||
|
<h1>Dokumentierte Consent-Zustände</h1>
|
||||||
|
<p class="section-help">
|
||||||
|
Diese Ansicht zeigt gespeicherte Consent-Zustände aus der lokalen
|
||||||
|
Beobachtungsdatenbank. Jeder Eintrag ist ein dokumentierter Zustand,
|
||||||
|
den VG-Observe während der Browser-Laufzeit beobachtet hat.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="panel" aria-labelledby="documented-consent-title">
|
||||||
|
<h2 id="documented-consent-title">Historische Consent-Zustände</h2>
|
||||||
|
<p id="documented-consent-empty" class="empty-state" hidden>
|
||||||
|
Keine dokumentierten Consent-Zustände vorhanden.
|
||||||
|
</p>
|
||||||
|
<div id="documented-consent-content" hidden>
|
||||||
|
<div class="consent-state-list-wrap">
|
||||||
|
<table class="consent-state-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Letzte Beobachtung</th>
|
||||||
|
<th scope="col">Erste Beobachtung</th>
|
||||||
|
<th scope="col">Beobachtungen</th>
|
||||||
|
<th scope="col">Seite</th>
|
||||||
|
<th scope="col">URL</th>
|
||||||
|
<th scope="col">Erlaubte Zwecke</th>
|
||||||
|
<th scope="col">Firmen mit Consent</th>
|
||||||
|
<th scope="col">Firmen mit berechtigtem Interesse</th>
|
||||||
|
<th scope="col">Gemeldete Vendorliste</th>
|
||||||
|
<th scope="col">Fingerprint</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="documented-consent-list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="consent-detail" aria-labelledby="consent-detail-title">
|
||||||
|
<h2 id="consent-detail-title">Ausgewählter Consent-Zustand</h2>
|
||||||
|
<div id="consent-detail-observation"></div>
|
||||||
|
<div id="consent-detail-basics"></div>
|
||||||
|
<div id="consent-detail-summary"></div>
|
||||||
|
<div id="consent-detail-publisher"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<details class="inspector-details">
|
||||||
|
<summary>Technische Feldpfade</summary>
|
||||||
|
<pre id="consent-detail-field-paths"></pre>
|
||||||
|
</details>
|
||||||
|
<details class="inspector-details">
|
||||||
|
<summary>Technische Rohstrings</summary>
|
||||||
|
<pre id="consent-detail-raw-strings"></pre>
|
||||||
|
</details>
|
||||||
|
<details class="inspector-details">
|
||||||
|
<summary>rawTcData / diagnostics</summary>
|
||||||
|
<pre id="consent-detail-diagnostics"></pre>
|
||||||
|
</details>
|
||||||
|
<details class="inspector-details">
|
||||||
|
<summary>Vollständiger JSON-Auszug des ausgewählten Consent-State</summary>
|
||||||
|
<pre id="consent-detail-json"></pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="consent-explorer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const documentedConsentEmpty = document.getElementById(
|
||||||
|
"documented-consent-empty"
|
||||||
|
);
|
||||||
|
const documentedConsentContent = document.getElementById(
|
||||||
|
"documented-consent-content"
|
||||||
|
);
|
||||||
|
const documentedConsentList = document.getElementById("documented-consent-list");
|
||||||
|
const consentDetailObservation = document.getElementById(
|
||||||
|
"consent-detail-observation"
|
||||||
|
);
|
||||||
|
const consentDetailBasics = document.getElementById("consent-detail-basics");
|
||||||
|
const consentDetailSummary = document.getElementById("consent-detail-summary");
|
||||||
|
const consentDetailPublisher = document.getElementById(
|
||||||
|
"consent-detail-publisher"
|
||||||
|
);
|
||||||
|
const consentDetailFieldPaths = document.getElementById(
|
||||||
|
"consent-detail-field-paths"
|
||||||
|
);
|
||||||
|
const consentDetailRawStrings = document.getElementById(
|
||||||
|
"consent-detail-raw-strings"
|
||||||
|
);
|
||||||
|
const consentDetailDiagnostics = document.getElementById(
|
||||||
|
"consent-detail-diagnostics"
|
||||||
|
);
|
||||||
|
const consentDetailJson = document.getElementById("consent-detail-json");
|
||||||
|
|
||||||
|
let documentedConsentStates = [];
|
||||||
|
let selectedConsentStateFingerprint = null;
|
||||||
|
|
||||||
|
const FIELD_EXPLANATIONS = {
|
||||||
|
firstSeenAt:
|
||||||
|
"Zeitpunkt, zu dem dieser Consent-Zustand erstmals lokal beobachtet wurde.",
|
||||||
|
lastSeenAt:
|
||||||
|
"Zeitpunkt, zu dem derselbe Consent-Zustand zuletzt wieder beobachtet wurde.",
|
||||||
|
seenCount: "Wie oft VG-Observe diesen identischen Zustand erkannt hat.",
|
||||||
|
"page.origin": "Webseite, auf der dieser Consent-Zustand beobachtet wurde.",
|
||||||
|
"page.url": "Vollständige beobachtete Seitenadresse, soweit verfügbar.",
|
||||||
|
stateFingerprint: "Technischer Wiedererkennungswert dieses Consent-Zustands.",
|
||||||
|
"consent.tcString":
|
||||||
|
"Technischer Consent-Nachweis. Für normale Nutzer meist nur als Rohbeleg relevant.",
|
||||||
|
"consent.addtlConsent":
|
||||||
|
"Zusätzlicher Google-Consent-String außerhalb des normalen TCF-Consent-Strings.",
|
||||||
|
"gvl.vendorListVersion":
|
||||||
|
"Vendorlisten-Version, die im beobachteten Consent-Kontext gemeldet wurde. Nicht automatisch die aktuellste IAB-Europe-Version.",
|
||||||
|
"cmp.tcfPolicyVersion":
|
||||||
|
"Version der TCF-Regelgrundlage, die im beobachteten Consent-Kontext gemeldet wurde.",
|
||||||
|
"cmp.gdprApplies":
|
||||||
|
"Angabe, ob das beobachtete Consent-System DSGVO-Anwendbarkeit gemeldet hat.",
|
||||||
|
"observation.eventStatus":
|
||||||
|
"Vom beobachteten TCF-Ereignis gemeldeter Status der Consent-Erfassung.",
|
||||||
|
"observation.cmpStatus":
|
||||||
|
"Vom beobachteten Consent-System gemeldeter Laufzeitstatus.",
|
||||||
|
"purposes.consents":
|
||||||
|
"Anzahl der Zwecke, für die Zustimmung gemeldet wurde.",
|
||||||
|
"purposes.legitimateInterests":
|
||||||
|
"Anzahl der Zwecke, für die berechtigtes Interesse gemeldet wurde.",
|
||||||
|
"vendors.consents":
|
||||||
|
"Anzahl der Firmen, für die Zustimmung gemeldet wurde.",
|
||||||
|
"vendors.legitimateInterests":
|
||||||
|
"Anzahl der Firmen, die sich laut beobachtetem Kontext auf berechtigtes Interesse stützen.",
|
||||||
|
specialFeatureOptins:
|
||||||
|
"Anzahl besonderer Funktionen, für die eine aktive Auswahl beobachtet wurde.",
|
||||||
|
"vendors.disclosedVendors":
|
||||||
|
"Anzahl der im beobachteten tcData-Kontext offengelegten Vendoren/Firmen.",
|
||||||
|
"publisher.consents":
|
||||||
|
"Anzahl der vom Webseitenbetreiber gemeldeten Publisher-Zustimmungen.",
|
||||||
|
"publisher.legitimateInterests":
|
||||||
|
"Anzahl der vom Webseitenbetreiber gemeldeten Publisher-Angaben zu berechtigtem Interesse.",
|
||||||
|
"publisher.restrictions":
|
||||||
|
"Technische Einschränkungen/Vorgaben des Webseitenbetreibers im TCF-Kontext. Für Nutzer nur eingeschränkt aussagekräftig."
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
await renderDocumentedConsentStates();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function renderDocumentedConsentStates() {
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "list_recent_consent_states"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(result?.error ?? "list_recent_consent_states_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
documentedConsentStates = result.consentStates ?? [];
|
||||||
|
|
||||||
|
if (documentedConsentStates.length === 0) {
|
||||||
|
selectedConsentStateFingerprint = null;
|
||||||
|
renderNoDocumentedConsentStates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
documentedConsentEmpty.hidden = true;
|
||||||
|
documentedConsentContent.hidden = false;
|
||||||
|
|
||||||
|
if (!findDocumentedConsentState(selectedConsentStateFingerprint)) {
|
||||||
|
selectedConsentStateFingerprint =
|
||||||
|
documentedConsentStates[0]?.stateFingerprint ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDocumentedConsentStateList();
|
||||||
|
renderSelectedConsentState();
|
||||||
|
} catch (error) {
|
||||||
|
documentedConsentEmpty.hidden = false;
|
||||||
|
documentedConsentContent.hidden = true;
|
||||||
|
documentedConsentEmpty.textContent =
|
||||||
|
"Dokumentierte Consent-Zustände konnten nicht geladen werden.";
|
||||||
|
console.warn("VendorGet-IV documented consent states failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNoDocumentedConsentStates() {
|
||||||
|
documentedConsentList.textContent = "";
|
||||||
|
clearConsentStateDetails();
|
||||||
|
documentedConsentEmpty.hidden = false;
|
||||||
|
documentedConsentContent.hidden = true;
|
||||||
|
documentedConsentEmpty.textContent =
|
||||||
|
"Keine dokumentierten Consent-Zustände vorhanden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDocumentedConsentStateList() {
|
||||||
|
documentedConsentList.textContent = "";
|
||||||
|
|
||||||
|
for (const consentState of documentedConsentStates) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
const isSelected =
|
||||||
|
consentState?.stateFingerprint === selectedConsentStateFingerprint;
|
||||||
|
|
||||||
|
row.className = isSelected ? "is-selected" : "";
|
||||||
|
row.tabIndex = 0;
|
||||||
|
row.setAttribute("role", "button");
|
||||||
|
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
||||||
|
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
selectDocumentedConsentState(consentState?.stateFingerprint ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
selectDocumentedConsentState(consentState?.stateFingerprint ?? null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
appendListCell(row, formatNullable(consentState?.lastSeenAt));
|
||||||
|
appendListCell(row, formatNullable(consentState?.firstSeenAt));
|
||||||
|
appendListCell(row, formatNullable(consentState?.seenCount), "numeric");
|
||||||
|
appendListCell(row, formatNullable(consentState?.page?.origin));
|
||||||
|
appendListCell(row, shortenLongString(consentState?.page?.url, 80), "url-cell");
|
||||||
|
appendListCell(
|
||||||
|
row,
|
||||||
|
String(countTruthyObjectValues(consentState?.purposes?.consents)),
|
||||||
|
"numeric"
|
||||||
|
);
|
||||||
|
appendListCell(
|
||||||
|
row,
|
||||||
|
String(countTruthyObjectValues(consentState?.vendors?.consents)),
|
||||||
|
"numeric"
|
||||||
|
);
|
||||||
|
appendListCell(
|
||||||
|
row,
|
||||||
|
String(countTruthyObjectValues(consentState?.vendors?.legitimateInterests)),
|
||||||
|
"numeric"
|
||||||
|
);
|
||||||
|
appendListCell(row, formatNullable(consentState?.gvl?.vendorListVersion));
|
||||||
|
appendListCell(row, shortenFingerprint(consentState?.stateFingerprint));
|
||||||
|
|
||||||
|
documentedConsentList.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendListCell(row, value, className) {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
|
||||||
|
if (className) {
|
||||||
|
cell.className = className;
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.textContent = value;
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDocumentedConsentState(stateFingerprint) {
|
||||||
|
selectedConsentStateFingerprint = stateFingerprint;
|
||||||
|
renderDocumentedConsentStateList();
|
||||||
|
renderSelectedConsentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedConsentState() {
|
||||||
|
const consentState = findDocumentedConsentState(selectedConsentStateFingerprint);
|
||||||
|
|
||||||
|
clearConsentStateDetails();
|
||||||
|
|
||||||
|
if (!consentState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDetailSection(consentDetailObservation, "Beobachtung", [
|
||||||
|
["Erste Beobachtung", formatNullable(consentState?.firstSeenAt), "firstSeenAt"],
|
||||||
|
["Letzte Beobachtung", formatNullable(consentState?.lastSeenAt), "lastSeenAt"],
|
||||||
|
["Anzahl Beobachtungen", formatNullable(consentState?.seenCount), "seenCount"],
|
||||||
|
["Seite / Origin", formatNullable(consentState?.page?.origin), "page.origin"],
|
||||||
|
["URL", formatLongString(consentState?.page?.url), "page.url"],
|
||||||
|
[
|
||||||
|
"State-Fingerprint",
|
||||||
|
formatLongString(consentState?.stateFingerprint),
|
||||||
|
"stateFingerprint"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderDetailSection(
|
||||||
|
consentDetailBasics,
|
||||||
|
"Gemeldete Consent-Grunddaten",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"Beobachteter TC-String / euconsent-v2",
|
||||||
|
summarizeRawString(consentState?.consent?.tcString),
|
||||||
|
"consent.tcString"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Beobachteter Google Additional Consent String",
|
||||||
|
summarizeRawString(consentState?.consent?.addtlConsent),
|
||||||
|
"consent.addtlConsent"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Vom Consent-System gemeldete Vendorlisten-Version",
|
||||||
|
formatNullable(consentState?.gvl?.vendorListVersion),
|
||||||
|
"gvl.vendorListVersion"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Vom Consent-System gemeldete TCF-Policy-Version",
|
||||||
|
formatNullable(consentState?.cmp?.tcfPolicyVersion),
|
||||||
|
"cmp.tcfPolicyVersion"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Vom Consent-System gemeldete DSGVO-Anwendbarkeit",
|
||||||
|
formatBoolean(consentState?.cmp?.gdprApplies),
|
||||||
|
"cmp.gdprApplies"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Beobachteter TCF Event Status",
|
||||||
|
formatNullable(consentState?.observation?.eventStatus),
|
||||||
|
"observation.eventStatus"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Beobachteter Consent-System-Status",
|
||||||
|
formatNullable(consentState?.observation?.cmpStatus),
|
||||||
|
"observation.cmpStatus"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
renderDetailSection(
|
||||||
|
consentDetailSummary,
|
||||||
|
"Zusammenfassung der Zwecke und Firmen",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"Anzahl Purposes mit aktivem Consent",
|
||||||
|
String(countTruthyObjectValues(consentState?.purposes?.consents)),
|
||||||
|
"purposes.consents"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Anzahl Purposes mit aktivem Legitimate Interest",
|
||||||
|
String(countTruthyObjectValues(consentState?.purposes?.legitimateInterests)),
|
||||||
|
"purposes.legitimateInterests"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Anzahl Vendoren/Firmen mit aktivem Consent",
|
||||||
|
String(countTruthyObjectValues(consentState?.vendors?.consents)),
|
||||||
|
"vendors.consents"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Anzahl Vendoren/Firmen mit aktivem Legitimate Interest",
|
||||||
|
String(countTruthyObjectValues(consentState?.vendors?.legitimateInterests)),
|
||||||
|
"vendors.legitimateInterests"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Anzahl aktivierter Special Features",
|
||||||
|
String(countTruthyObjectValues(consentState?.specialFeatureOptins)),
|
||||||
|
"specialFeatureOptins"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Anzahl offengelegter Vendoren/Firmen laut beobachtetem tcData-Kontext",
|
||||||
|
String(countTruthyObjectValues(consentState?.vendors?.disclosedVendors)),
|
||||||
|
"vendors.disclosedVendors"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
renderDetailSection(consentDetailPublisher, "Publisher-Angaben", [
|
||||||
|
[
|
||||||
|
"Publisher Consents: Anzahl aktiv",
|
||||||
|
String(countTruthyObjectValues(consentState?.publisher?.consents)),
|
||||||
|
"publisher.consents"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Publisher Legitimate Interests: Anzahl aktiv",
|
||||||
|
String(countTruthyObjectValues(consentState?.publisher?.legitimateInterests)),
|
||||||
|
"publisher.legitimateInterests"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Publisher Restrictions: Anzahl Einträge",
|
||||||
|
String(countObjectKeys(consentState?.publisher?.restrictions)),
|
||||||
|
"publisher.restrictions"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
consentDetailFieldPaths.textContent = JSON.stringify(
|
||||||
|
buildConsentStateFieldPathOverview(),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
consentDetailRawStrings.textContent = JSON.stringify(
|
||||||
|
{
|
||||||
|
tcString: consentState?.consent?.tcString ?? null,
|
||||||
|
addtlConsent: consentState?.consent?.addtlConsent ?? null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
consentDetailDiagnostics.textContent = JSON.stringify(
|
||||||
|
{
|
||||||
|
rawTcData: consentState?.rawTcData ?? null,
|
||||||
|
diagnostics: consentState?.diagnostics ?? null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
consentDetailJson.textContent = JSON.stringify(consentState, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetailSection(container, title, rows) {
|
||||||
|
const heading = document.createElement("h3");
|
||||||
|
const table = document.createElement("table");
|
||||||
|
const body = document.createElement("tbody");
|
||||||
|
|
||||||
|
heading.textContent = title;
|
||||||
|
table.className = "inspector-table";
|
||||||
|
|
||||||
|
for (const [label, value, technicalField, helpText] of rows) {
|
||||||
|
body.append(createInspectorRow(label, value, technicalField, helpText));
|
||||||
|
}
|
||||||
|
|
||||||
|
table.append(body);
|
||||||
|
container.append(heading, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInspectorRow(label, value, technicalField, helpText) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
const labelCell = document.createElement("th");
|
||||||
|
const valueCell = document.createElement("td");
|
||||||
|
const explanationCell = document.createElement("td");
|
||||||
|
const mainLabel = document.createElement("span");
|
||||||
|
const explanation = helpText ?? FIELD_EXPLANATIONS[technicalField] ?? "-";
|
||||||
|
|
||||||
|
labelCell.scope = "row";
|
||||||
|
mainLabel.textContent = label;
|
||||||
|
labelCell.append(mainLabel);
|
||||||
|
|
||||||
|
if (technicalField) {
|
||||||
|
const technicalFieldElement = document.createElement("small");
|
||||||
|
|
||||||
|
technicalFieldElement.className = "technical-field";
|
||||||
|
technicalFieldElement.textContent = `Technisches Feld: ${technicalField}`;
|
||||||
|
labelCell.append(technicalFieldElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helpText) {
|
||||||
|
const helpTextElement = document.createElement("small");
|
||||||
|
|
||||||
|
helpTextElement.className = "inspector-help";
|
||||||
|
helpTextElement.textContent = helpText;
|
||||||
|
labelCell.append(helpTextElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
valueCell.textContent = value;
|
||||||
|
valueCell.className = "inspector-value";
|
||||||
|
explanationCell.textContent = explanation;
|
||||||
|
explanationCell.className = "inspector-explanation";
|
||||||
|
|
||||||
|
row.append(labelCell, valueCell, explanationCell);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConsentStateDetails() {
|
||||||
|
consentDetailObservation.textContent = "";
|
||||||
|
consentDetailBasics.textContent = "";
|
||||||
|
consentDetailSummary.textContent = "";
|
||||||
|
consentDetailPublisher.textContent = "";
|
||||||
|
consentDetailFieldPaths.textContent = "";
|
||||||
|
consentDetailRawStrings.textContent = "";
|
||||||
|
consentDetailDiagnostics.textContent = "";
|
||||||
|
consentDetailJson.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDocumentedConsentState(stateFingerprint) {
|
||||||
|
if (!stateFingerprint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
documentedConsentStates.find((consentState) => {
|
||||||
|
return consentState?.stateFingerprint === stateFingerprint;
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConsentStateFieldPathOverview() {
|
||||||
|
return {
|
||||||
|
beobachtung: {
|
||||||
|
"Erste Beobachtung": "firstSeenAt",
|
||||||
|
"Letzte Beobachtung": "lastSeenAt",
|
||||||
|
"Anzahl Beobachtungen": "seenCount",
|
||||||
|
"Seite / Origin": "page.origin",
|
||||||
|
URL: "page.url",
|
||||||
|
"State-Fingerprint": "stateFingerprint"
|
||||||
|
},
|
||||||
|
grunddaten: {
|
||||||
|
"TC-String / euconsent-v2": "consent.tcString",
|
||||||
|
"Google Additional Consent String": "consent.addtlConsent",
|
||||||
|
Vendorliste: "gvl.vendorListVersion",
|
||||||
|
"TCF-Policy-Version": "cmp.tcfPolicyVersion",
|
||||||
|
"DSGVO-Anwendbarkeit": "cmp.gdprApplies",
|
||||||
|
"TCF Event Status": "observation.eventStatus",
|
||||||
|
"Consent-System-Status": "observation.cmpStatus"
|
||||||
|
},
|
||||||
|
zusammenfassung: {
|
||||||
|
"Purposes mit Consent": "purposes.consents",
|
||||||
|
"Purposes mit Legitimate Interest": "purposes.legitimateInterests",
|
||||||
|
"Vendoren/Firmen mit Consent": "vendors.consents",
|
||||||
|
"Vendoren/Firmen mit Legitimate Interest": "vendors.legitimateInterests",
|
||||||
|
"Special Features": "specialFeatureOptins",
|
||||||
|
"Offengelegte Vendoren/Firmen": "vendors.disclosedVendors"
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
"Publisher Consents": "publisher.consents",
|
||||||
|
"Publisher Legitimate Interests": "publisher.legitimateInterests",
|
||||||
|
"Publisher Restrictions": "publisher.restrictions"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenFingerprint(value) {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortenLongString(value, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenLongString(value, maxLength) {
|
||||||
|
const formattedValue = formatNullable(value);
|
||||||
|
|
||||||
|
if (formattedValue === "-" || formattedValue.length <= maxLength) {
|
||||||
|
return formattedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${formattedValue.slice(0, maxLength)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeRawString(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "nicht vorhanden";
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringValue = String(value);
|
||||||
|
const prefix = stringValue.slice(0, 32);
|
||||||
|
|
||||||
|
return `vorhanden, Länge ${stringValue.length}, Anfang: ${prefix}${
|
||||||
|
stringValue.length > prefix.length ? "..." : ""
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countTruthyObjectValues(obj) {
|
||||||
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(obj).filter(Boolean).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countObjectKeys(obj) {
|
||||||
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(obj).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNullable(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBoolean(value) {
|
||||||
|
if (value === true) {
|
||||||
|
return "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === false) {
|
||||||
|
return "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLongString(value) {
|
||||||
|
return formatNullable(value);
|
||||||
|
}
|
||||||
@@ -41,6 +41,13 @@ h2 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 720px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-status {
|
.dashboard-status {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -55,55 +62,24 @@ h2 {
|
|||||||
background: #182231;
|
background: #182231;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maintenance-status {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
max-width: 760px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #3f6f56;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #14251d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintenance-status strong {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #bbf7d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintenance-status dl {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintenance-status dt {
|
|
||||||
margin: 0 0 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintenance-status dd {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
border-bottom: 1px solid #334155;
|
border-bottom: 1px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid {
|
.section-help {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gvl-status-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid div {
|
.gvl-status-grid div {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
@@ -111,17 +87,18 @@ h2 {
|
|||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid dt {
|
.gvl-status-grid dt {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid dd {
|
.gvl-status-grid dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
word-break: break-word;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -150,29 +127,14 @@ th:last-child {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.explorer-actions {
|
||||||
max-width: 720px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retention-actions,
|
|
||||||
.admin-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-status {
|
.button-link,
|
||||||
min-height: 18px;
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid #475569;
|
border: 1px solid #475569;
|
||||||
@@ -183,6 +145,12 @@ button {
|
|||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
@@ -193,16 +161,11 @@ button:disabled {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid {
|
.gvl-status-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maintenance-status dl {
|
.explorer-actions {
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retention-actions,
|
|
||||||
.admin-actions {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,51 +14,11 @@
|
|||||||
Loading evidence status
|
Loading evidence status
|
||||||
</div>
|
</div>
|
||||||
<p class="dashboard-notice">
|
<p class="dashboard-notice">
|
||||||
Verwaltungsbereich: Lesen und manuelle Aktionen bleiben verfügbar.
|
Übersicht und Einstieg für VG-Observe. Detailansichten liegen in
|
||||||
VG-IV dokumentiert browserseitige Consent-/TCF-Zustände als
|
eigenen Explorern.
|
||||||
evidenzielle Spiegelung.
|
|
||||||
</p>
|
</p>
|
||||||
<section class="maintenance-status" aria-label="Verwaltungsmodus">
|
|
||||||
<strong>Verwaltungsmodus aktiv: Hintergrundaufzeichnung ist pausiert.</strong>
|
|
||||||
<dl>
|
|
||||||
<div>
|
|
||||||
<dt>Write Suspend</dt>
|
|
||||||
<dd id="maintenance-write-suspend">-</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Quelle</dt>
|
|
||||||
<dd id="maintenance-source">-</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Heartbeat</dt>
|
|
||||||
<dd id="maintenance-heartbeat">-</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Ablauf</dt>
|
|
||||||
<dd id="maintenance-expires">-</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="overview-title">
|
|
||||||
<h2 id="overview-title">Overview</h2>
|
|
||||||
<dl class="metric-grid">
|
|
||||||
<div>
|
|
||||||
<dt>Total Evidence Records</dt>
|
|
||||||
<dd id="total-count">-</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Locked Records</dt>
|
|
||||||
<dd id="locked-count">-</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Unlocked Records</dt>
|
|
||||||
<dd id="unlocked-count">-</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="stores-title">
|
<section class="panel" aria-labelledby="stores-title">
|
||||||
<h2 id="stores-title">Evidence Stores</h2>
|
<h2 id="stores-title">Evidence Stores</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -93,37 +53,58 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="retention-title">
|
<section class="panel" aria-labelledby="official-gvl-title">
|
||||||
<h2 id="retention-title">Retention Status</h2>
|
<h2 id="official-gvl-title">Offizielle Vendorliste</h2>
|
||||||
<p>
|
<p class="section-help">
|
||||||
Locked records are protected from partial purge. Full deletion still
|
Die aktuell offiziell abgerufene IAB-Europe-Vendorliste ist die
|
||||||
requires explicit confirmation.
|
Version, die VG-Observe direkt von der offiziellen IAB-Europe-Quelle
|
||||||
|
geladen hat. Sie ist getrennt von der Vendorliste, die in einem
|
||||||
|
konkreten Consent-Kontext gemeldet wurde.
|
||||||
</p>
|
</p>
|
||||||
<div class="retention-actions">
|
<dl class="gvl-status-grid">
|
||||||
<button id="lock-all-button" type="button">
|
<div>
|
||||||
Alle Evidenzen als DSGVO-/DSAR-relevant markieren
|
<dt>Lokal neueste gespeicherte Vendorlisten-Version</dt>
|
||||||
</button>
|
<dd id="official-gvl-local-version">-</dd>
|
||||||
<button id="unlock-all-button" type="button">
|
|
||||||
Alle Evidenz-Sperren entfernen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Letzter automatischer Update-Check</dt>
|
||||||
|
<dd id="official-gvl-last-check">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Letzter echter automatischer GVL-Check</dt>
|
||||||
|
<dd id="official-gvl-last-real-check">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Nächster erlaubter automatischer GVL-Check</dt>
|
||||||
|
<dd id="official-gvl-next-allowed-check">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Ergebnis des letzten Checks</dt>
|
||||||
|
<dd id="official-gvl-result">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Vorherige Version -> aktuelle Version</dt>
|
||||||
|
<dd id="official-gvl-version-change">-</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" aria-labelledby="administration-title">
|
<section class="panel" aria-labelledby="explorers-title">
|
||||||
<h2 id="administration-title">Administration</h2>
|
<h2 id="explorers-title">Explorer</h2>
|
||||||
<div class="admin-actions">
|
<p class="section-help">
|
||||||
<button id="gvl-fetch-official-button" type="button">
|
Historische Consent-Zustände und technische Belege werden in einer
|
||||||
Official IAB GVL Fetch
|
eigenen Ansicht geöffnet.
|
||||||
</button>
|
</p>
|
||||||
<button id="gvl-import-button" type="button">
|
<div class="explorer-actions">
|
||||||
GVL JSON importieren
|
<a class="button-link" href="../consent-explorer/consent-explorer.html">
|
||||||
</button>
|
Consent-Explorer öffnen
|
||||||
<button id="evidence-delete-button" type="button">
|
</a>
|
||||||
Evidence Delete
|
<a class="button-link" href="../gvl-explorer/gvl-explorer.html">
|
||||||
</button>
|
GVL-Explorer öffnen
|
||||||
</div>
|
</a>
|
||||||
<div id="admin-status" class="admin-status" aria-live="polite">
|
<a class="button-link" href="../analysis-dashboard/analysis-dashboard.html">
|
||||||
Bereit
|
Request-/Empfänger-Analyse öffnen
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+62
-352
@@ -1,33 +1,20 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const EVIDENCE_MAINTENANCE_SOURCE = "dashboard";
|
|
||||||
const EVIDENCE_MAINTENANCE_HEARTBEAT_MS = 5 * 1000;
|
|
||||||
|
|
||||||
const dashboardStatus = document.getElementById("dashboard-status");
|
const dashboardStatus = document.getElementById("dashboard-status");
|
||||||
const totalCount = document.getElementById("total-count");
|
const officialGvlLocalVersion = document.getElementById(
|
||||||
const lockedCount = document.getElementById("locked-count");
|
"official-gvl-local-version"
|
||||||
const unlockedCount = document.getElementById("unlocked-count");
|
|
||||||
const maintenanceWriteSuspend = document.getElementById(
|
|
||||||
"maintenance-write-suspend"
|
|
||||||
);
|
);
|
||||||
const maintenanceSource = document.getElementById("maintenance-source");
|
const officialGvlLastCheck = document.getElementById("official-gvl-last-check");
|
||||||
const maintenanceHeartbeat = document.getElementById("maintenance-heartbeat");
|
const officialGvlLastRealCheck = document.getElementById(
|
||||||
const maintenanceExpires = document.getElementById("maintenance-expires");
|
"official-gvl-last-real-check"
|
||||||
const lockAllButton = document.getElementById("lock-all-button");
|
);
|
||||||
const unlockAllButton = document.getElementById("unlock-all-button");
|
const officialGvlNextAllowedCheck = document.getElementById(
|
||||||
const gvlFetchOfficialButton = document.getElementById(
|
"official-gvl-next-allowed-check"
|
||||||
"gvl-fetch-official-button"
|
);
|
||||||
|
const officialGvlResult = document.getElementById("official-gvl-result");
|
||||||
|
const officialGvlVersionChange = document.getElementById(
|
||||||
|
"official-gvl-version-change"
|
||||||
);
|
);
|
||||||
const gvlImportButton = document.getElementById("gvl-import-button");
|
|
||||||
const evidenceDeleteButton = document.getElementById("evidence-delete-button");
|
|
||||||
const adminStatus = document.getElementById("admin-status");
|
|
||||||
const gvlImportFileInput = document.createElement("input");
|
|
||||||
|
|
||||||
gvlImportFileInput.type = "file";
|
|
||||||
gvlImportFileInput.accept = ".json,application/json";
|
|
||||||
gvlImportFileInput.hidden = true;
|
|
||||||
|
|
||||||
let evidenceMaintenanceHeartbeatId = null;
|
|
||||||
|
|
||||||
const storeCells = {
|
const storeCells = {
|
||||||
consent_states: document.getElementById("store-consent-states"),
|
consent_states: document.getElementById("store-consent-states"),
|
||||||
@@ -38,127 +25,10 @@ const storeCells = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
document.body.appendChild(gvlImportFileInput);
|
|
||||||
|
|
||||||
await startEvidenceMaintenanceMode();
|
|
||||||
evidenceMaintenanceHeartbeatId = setInterval(() => {
|
|
||||||
void refreshEvidenceMaintenanceMode();
|
|
||||||
}, EVIDENCE_MAINTENANCE_HEARTBEAT_MS);
|
|
||||||
|
|
||||||
lockAllButton.addEventListener("click", async () => {
|
|
||||||
await handleLockAllClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
unlockAllButton.addEventListener("click", async () => {
|
|
||||||
await handleUnlockAllClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
gvlFetchOfficialButton.addEventListener("click", async () => {
|
|
||||||
await fetchOfficialGvl();
|
|
||||||
});
|
|
||||||
|
|
||||||
gvlImportButton.addEventListener("click", () => {
|
|
||||||
gvlImportFileInput.value = "";
|
|
||||||
gvlImportFileInput.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
gvlImportFileInput.addEventListener("change", async () => {
|
|
||||||
const file = gvlImportFileInput.files?.[0] ?? null;
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await importGvlFile(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
evidenceDeleteButton.addEventListener("click", async () => {
|
|
||||||
await handleEvidenceDeleteClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
await renderEvidenceStatus();
|
await renderEvidenceStatus();
|
||||||
|
await renderOfficialGvlStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
|
||||||
endEvidenceMaintenanceMode();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("pagehide", () => {
|
|
||||||
endEvidenceMaintenanceMode();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function startEvidenceMaintenanceMode() {
|
|
||||||
try {
|
|
||||||
const status = await sendEvidenceMaintenanceMessage(
|
|
||||||
"start_evidence_maintenance_session"
|
|
||||||
);
|
|
||||||
|
|
||||||
renderEvidenceMaintenanceStatus(status);
|
|
||||||
} catch (error) {
|
|
||||||
renderEvidenceMaintenanceUnavailable();
|
|
||||||
console.warn("VendorGet-IV maintenance start failed", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshEvidenceMaintenanceMode() {
|
|
||||||
try {
|
|
||||||
const status = await sendEvidenceMaintenanceMessage(
|
|
||||||
"refresh_evidence_maintenance_session"
|
|
||||||
);
|
|
||||||
|
|
||||||
renderEvidenceMaintenanceStatus(status);
|
|
||||||
} catch (error) {
|
|
||||||
renderEvidenceMaintenanceUnavailable();
|
|
||||||
console.warn("VendorGet-IV maintenance heartbeat failed", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function endEvidenceMaintenanceMode() {
|
|
||||||
if (evidenceMaintenanceHeartbeatId !== null) {
|
|
||||||
clearInterval(evidenceMaintenanceHeartbeatId);
|
|
||||||
evidenceMaintenanceHeartbeatId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendEvidenceMaintenanceMessage("end_evidence_maintenance_session").catch(
|
|
||||||
(error) => {
|
|
||||||
console.warn("VendorGet-IV maintenance end failed", error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendEvidenceMaintenanceMessage(type) {
|
|
||||||
const status = await browser.runtime.sendMessage({
|
|
||||||
type: type,
|
|
||||||
payload: {
|
|
||||||
source: EVIDENCE_MAINTENANCE_SOURCE
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!status?.success) {
|
|
||||||
throw new Error(status?.error ?? `${type}_failed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEvidenceMaintenanceStatus(status) {
|
|
||||||
maintenanceWriteSuspend.textContent = status.evidenceWriteSuspended
|
|
||||||
? "aktiv"
|
|
||||||
: "inaktiv";
|
|
||||||
maintenanceSource.textContent = status.source ?? "-";
|
|
||||||
maintenanceHeartbeat.textContent = formatMaintenanceTimestamp(
|
|
||||||
status.lastHeartbeatAt
|
|
||||||
);
|
|
||||||
maintenanceExpires.textContent = formatMaintenanceTimestamp(status.expiresAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEvidenceMaintenanceUnavailable() {
|
|
||||||
maintenanceWriteSuspend.textContent = "unbekannt";
|
|
||||||
maintenanceSource.textContent = "-";
|
|
||||||
maintenanceHeartbeat.textContent = "-";
|
|
||||||
maintenanceExpires.textContent = "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderEvidenceStatus() {
|
async function renderEvidenceStatus() {
|
||||||
try {
|
try {
|
||||||
const status = await browser.runtime.sendMessage({
|
const status = await browser.runtime.sendMessage({
|
||||||
@@ -169,10 +39,6 @@ async function renderEvidenceStatus() {
|
|||||||
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
|
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
totalCount.textContent = String(status.totalCount);
|
|
||||||
lockedCount.textContent = String(status.lockedCount);
|
|
||||||
unlockedCount.textContent = String(status.unlockedCount);
|
|
||||||
|
|
||||||
renderStoreCounts(status.storeCounts ?? {});
|
renderStoreCounts(status.storeCounts ?? {});
|
||||||
renderStatusMessage("Evidence status loaded");
|
renderStatusMessage("Evidence status loaded");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -187,250 +53,94 @@ function renderStoreCounts(storeCounts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOfficialGvl() {
|
async function renderOfficialGvlStatus() {
|
||||||
gvlFetchOfficialButton.disabled = true;
|
|
||||||
renderAdminStatus("Fetching official IAB GVL...");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
type: "fetch_official_gvl"
|
type: "get_latest_gvl_update_status"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
throw new Error(result?.error ?? "official_gvl_fetch_failed");
|
throw new Error(result?.error ?? "get_latest_gvl_update_status_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
await renderEvidenceStatus();
|
const status = result.status ?? {};
|
||||||
renderAdminStatus(
|
|
||||||
"Fetched successfully - " +
|
officialGvlLocalVersion.textContent = formatNullable(
|
||||||
`${result.alreadyKnown ? "already known" : "newly stored"} - ` +
|
status.latestLocalVendorListVersion ?? status.currentVendorListVersion
|
||||||
`vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
|
|
||||||
`sha256 ${shortenSha256(result.sha256)}`
|
|
||||||
);
|
);
|
||||||
|
officialGvlLastCheck.textContent = formatNullable(status.checkedAt);
|
||||||
|
officialGvlLastRealCheck.textContent = formatNullable(
|
||||||
|
status.lastAutoGvlCheckAt
|
||||||
|
);
|
||||||
|
officialGvlNextAllowedCheck.textContent = formatNullable(
|
||||||
|
status.nextAllowedAutoCheckAt
|
||||||
|
);
|
||||||
|
officialGvlResult.textContent = formatGvlUpdateResult(status);
|
||||||
|
officialGvlVersionChange.textContent = formatGvlVersionChange(status);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderAdminStatus("Official GVL fetch failed");
|
officialGvlLocalVersion.textContent = "-";
|
||||||
console.warn("VendorGet-IV official GVL fetch failed", error);
|
officialGvlLastCheck.textContent = "-";
|
||||||
} finally {
|
officialGvlLastRealCheck.textContent = "-";
|
||||||
gvlFetchOfficialButton.disabled = false;
|
officialGvlNextAllowedCheck.textContent = "-";
|
||||||
|
officialGvlResult.textContent = "Auto-Check fehlgeschlagen";
|
||||||
|
officialGvlVersionChange.textContent = "-";
|
||||||
|
console.warn("VendorGet-IV official GVL status failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importGvlFile(file) {
|
function formatGvlUpdateResult(status) {
|
||||||
gvlImportButton.disabled = true;
|
const result = status?.result ?? null;
|
||||||
renderAdminStatus("Import läuft...");
|
|
||||||
|
|
||||||
try {
|
if (result === "stored") {
|
||||||
const fileContent = await readFileAsText(file);
|
return "Neue offizielle Vendorliste gespeichert";
|
||||||
const rawJson = JSON.parse(fileContent);
|
|
||||||
|
|
||||||
if (!isGvlImportCandidate(rawJson)) {
|
|
||||||
throw new Error("invalid_gvl_json");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await browser.runtime.sendMessage({
|
if (result === "no_change") {
|
||||||
type: "gvl_import_json",
|
return "Keine neuere offizielle Vendorliste gefunden";
|
||||||
payload: {
|
|
||||||
rawJson: rawJson,
|
|
||||||
sourceUrl: "local-file-import"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result?.success) {
|
|
||||||
throw new Error(result?.error ?? "gvl_import_failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await renderEvidenceStatus();
|
if (result === "already_known") {
|
||||||
renderAdminStatus(
|
return "Offizielle Vendorliste war bereits lokal bekannt";
|
||||||
`${result.alreadyKnown ? "already known" : "imported"} - ` +
|
|
||||||
`vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
|
|
||||||
`sha256 ${shortenSha256(result.sha256)}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
renderAdminStatus("Import fehlgeschlagen");
|
|
||||||
console.warn("VendorGet-IV GVL import failed", error);
|
|
||||||
} finally {
|
|
||||||
gvlImportButton.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFileAsText(file) {
|
if (result === "error") {
|
||||||
return new Promise((resolve, reject) => {
|
return "Auto-Check fehlgeschlagen";
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onerror = () => reject(reader.error);
|
|
||||||
reader.onload = () => resolve(reader.result);
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGvlImportCandidate(value) {
|
if (result === "throttled") {
|
||||||
return (
|
return "Übersprungen wegen 24h-Throttling";
|
||||||
value &&
|
|
||||||
typeof value === "object" &&
|
|
||||||
!Array.isArray(value) &&
|
|
||||||
value.vendorListVersion !== undefined &&
|
|
||||||
value.vendors &&
|
|
||||||
typeof value.vendors === "object" &&
|
|
||||||
!Array.isArray(value.vendors)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEvidenceDeleteClick() {
|
if (result === "started") {
|
||||||
evidenceDeleteButton.disabled = true;
|
return "Auto-Check läuft";
|
||||||
|
|
||||||
try {
|
|
||||||
const status = await getEvidenceRetentionStatus();
|
|
||||||
|
|
||||||
const confirmed = confirm(
|
|
||||||
"Alle lokal gespeicherten VG-IV-Evidenzdaten wirklich löschen?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
renderAdminStatus("Löschung abgebrochen");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.lockedCount === 0) {
|
if (result === "not_checked_since_background_start") {
|
||||||
await deleteAllEvidenceDatabase();
|
return "Noch kein Auto-Check seit Background-Start";
|
||||||
await renderEvidenceStatus();
|
|
||||||
renderAdminStatus("Evidenzdaten gelöscht");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteLockedRecords = confirm(
|
return formatNullable(status?.message ?? result);
|
||||||
`Achtung: ${status.lockedCount} Datensätze wurden als ` +
|
|
||||||
"DSGVO-/DSAR-relevant markiert. Sollen auch diese Datensätze " +
|
|
||||||
"wirklich gelöscht werden?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (deleteLockedRecords) {
|
|
||||||
await deleteAllEvidenceDatabase();
|
|
||||||
await renderEvidenceStatus();
|
|
||||||
renderAdminStatus("Evidenzdaten gelöscht");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await browser.runtime.sendMessage({
|
function formatGvlVersionChange(status) {
|
||||||
type: "purge_unlocked_evidence_records"
|
const previousVersion = formatNullable(status?.previousVendorListVersion);
|
||||||
});
|
const currentVersion = formatNullable(status?.currentVendorListVersion);
|
||||||
|
|
||||||
if (!result?.success) {
|
if (previousVersion === "-" && currentVersion === "-") {
|
||||||
throw new Error(result?.error ?? "purge_unlocked_evidence_records_failed");
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
await renderEvidenceStatus();
|
return `${previousVersion} -> ${currentVersion}`;
|
||||||
renderAdminStatus(
|
|
||||||
`${result.deletedCount} Datensätze gelöscht, ` +
|
|
||||||
`${result.keptLockedCount} gesperrte Datensätze behalten`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
renderAdminStatus("Löschung fehlgeschlagen");
|
|
||||||
console.warn("VendorGet-IV evidence delete failed", error);
|
|
||||||
} finally {
|
|
||||||
evidenceDeleteButton.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEvidenceRetentionStatus() {
|
|
||||||
const status = await browser.runtime.sendMessage({
|
|
||||||
type: "get_evidence_retention_status"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!status?.success) {
|
|
||||||
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteAllEvidenceDatabase() {
|
|
||||||
const result = await browser.runtime.sendMessage({
|
|
||||||
type: "delete_all_evidence_database"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result?.success) {
|
|
||||||
throw new Error(result?.error ?? "delete_all_evidence_database_failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLockAllClick() {
|
|
||||||
const confirmed = confirm(
|
|
||||||
"Alle vorhandenen VG-IV-Evidenzdatensätze als DSGVO-/DSAR-relevant markieren?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
renderStatusMessage("Record lock update cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await runRecordLockAction({
|
|
||||||
type: "lock_all_evidence_records",
|
|
||||||
payload: {
|
|
||||||
reason: "dsar_used",
|
|
||||||
note: null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUnlockAllClick() {
|
|
||||||
const confirmed = confirm(
|
|
||||||
"Alle VG-IV-Evidenzsperren wirklich entfernen?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
renderStatusMessage("Record lock update cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await runRecordLockAction({
|
|
||||||
type: "unlock_all_evidence_records"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runRecordLockAction(message) {
|
|
||||||
setRecordLockButtonsDisabled(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await browser.runtime.sendMessage(message);
|
|
||||||
|
|
||||||
if (!result?.success) {
|
|
||||||
throw new Error(result?.error ?? `${message.type}_failed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await renderEvidenceStatus();
|
|
||||||
} catch (error) {
|
|
||||||
renderStatusMessage("Record lock update failed");
|
|
||||||
console.warn("VendorGet-IV dashboard record lock update failed", error);
|
|
||||||
} finally {
|
|
||||||
setRecordLockButtonsDisabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRecordLockButtonsDisabled(disabled) {
|
|
||||||
lockAllButton.disabled = disabled;
|
|
||||||
unlockAllButton.disabled = disabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatusMessage(message) {
|
function renderStatusMessage(message) {
|
||||||
dashboardStatus.textContent = message;
|
dashboardStatus.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAdminStatus(message) {
|
function formatNullable(value) {
|
||||||
adminStatus.textContent = message;
|
if (value === null || value === undefined || value === "") {
|
||||||
}
|
|
||||||
|
|
||||||
function shortenSha256(value) {
|
|
||||||
if (!value) {
|
|
||||||
return "n/a";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${value.slice(0, 12)}...`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMaintenanceTimestamp(value) {
|
|
||||||
if (!value) {
|
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer {
|
||||||
|
width: min(1080px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer-header {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 760px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
width: fit-content;
|
||||||
|
color: #bfdbfe;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list-wrap {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fetch-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fetch-status {
|
||||||
|
min-height: 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #475569;
|
||||||
|
border-radius: 4px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #182231;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list {
|
||||||
|
min-width: 820px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list tbody tr:hover,
|
||||||
|
.snapshot-list tbody tr:focus {
|
||||||
|
outline: 0;
|
||||||
|
background: #263449;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list tbody tr.is-selected {
|
||||||
|
background: #1e3a5f;
|
||||||
|
box-shadow: inset 3px 0 0 #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list .numeric {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-list .sha-cell,
|
||||||
|
.snapshot-list .url-cell {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-summary {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-table th {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-table td {
|
||||||
|
font-weight: 700;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technical-details {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technical-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technical-details pre {
|
||||||
|
max-height: 220px;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #0f172a;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.explorer {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-table th {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VG-Observe GVL-Explorer</title>
|
||||||
|
<link rel="stylesheet" href="gvl-explorer.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="explorer">
|
||||||
|
<header class="explorer-header">
|
||||||
|
<a class="back-link" href="../dashboard/dashboard.html">Zurück zum Dashboard</a>
|
||||||
|
<h1>GVL-Explorer</h1>
|
||||||
|
<p class="section-help">
|
||||||
|
Diese Ansicht zeigt lokal gespeicherte offizielle
|
||||||
|
IAB-Europe-Vendorlisten. Sie dient dazu, historische
|
||||||
|
Vendorlisten-Versionen nachvollziehbar zu machen.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="panel" aria-labelledby="snapshot-list-title">
|
||||||
|
<h2 id="snapshot-list-title">Gespeicherte Vendorlisten</h2>
|
||||||
|
<div class="fetch-actions">
|
||||||
|
<button id="gvl-fetch-official-button" type="button">
|
||||||
|
Offizielle Vendorliste jetzt abrufen
|
||||||
|
</button>
|
||||||
|
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
||||||
|
Bereit
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p id="gvl-snapshot-empty" class="empty-state" hidden>
|
||||||
|
Keine gespeicherten offiziellen Vendorlisten vorhanden.
|
||||||
|
</p>
|
||||||
|
<div id="gvl-snapshot-content" hidden>
|
||||||
|
<div class="snapshot-list-wrap">
|
||||||
|
<table class="snapshot-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Vendorlisten-Version</th>
|
||||||
|
<th scope="col">Abrufzeitpunkt</th>
|
||||||
|
<th scope="col">SHA256</th>
|
||||||
|
<th scope="col">Quelle</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="gvl-snapshot-list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="snapshot-summary" aria-labelledby="snapshot-summary-title">
|
||||||
|
<h2 id="snapshot-summary-title">Ausgewählte Vendorliste</h2>
|
||||||
|
<div id="gvl-snapshot-summary"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<details class="technical-details">
|
||||||
|
<summary>Technische Feldnamen</summary>
|
||||||
|
<pre id="gvl-technical-fields"></pre>
|
||||||
|
</details>
|
||||||
|
<details class="technical-details">
|
||||||
|
<summary>SHA256 und Debugdaten</summary>
|
||||||
|
<pre id="gvl-debug-data"></pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="gvl-explorer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const gvlSnapshotEmpty = document.getElementById("gvl-snapshot-empty");
|
||||||
|
const gvlSnapshotContent = document.getElementById("gvl-snapshot-content");
|
||||||
|
const gvlSnapshotList = document.getElementById("gvl-snapshot-list");
|
||||||
|
const gvlSnapshotSummary = document.getElementById("gvl-snapshot-summary");
|
||||||
|
const gvlTechnicalFields = document.getElementById("gvl-technical-fields");
|
||||||
|
const gvlDebugData = document.getElementById("gvl-debug-data");
|
||||||
|
const gvlFetchOfficialButton = document.getElementById(
|
||||||
|
"gvl-fetch-official-button"
|
||||||
|
);
|
||||||
|
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
||||||
|
|
||||||
|
let gvlSnapshots = [];
|
||||||
|
let selectedSnapshotSha256 = null;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
gvlFetchOfficialButton.addEventListener("click", async () => {
|
||||||
|
await fetchOfficialGvl();
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderGvlSnapshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchOfficialGvl() {
|
||||||
|
gvlFetchOfficialButton.disabled = true;
|
||||||
|
renderFetchStatus("Vendorliste wird abgerufen...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "fetch_official_gvl"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(result?.error ?? "official_gvl_fetch_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFetchStatus(
|
||||||
|
result.alreadyKnown
|
||||||
|
? "Vendorliste bereits bekannt."
|
||||||
|
: "Vendorliste abgerufen."
|
||||||
|
);
|
||||||
|
|
||||||
|
await renderGvlSnapshots();
|
||||||
|
await renderSelectedGvlSnapshotSummary();
|
||||||
|
} catch (error) {
|
||||||
|
renderFetchStatus("Vendorliste konnte nicht abgerufen werden.");
|
||||||
|
console.warn("VG-Observe manual official GVL fetch failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlFetchOfficialButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFetchStatus(message) {
|
||||||
|
gvlFetchStatus.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderGvlSnapshots() {
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "list_gvl_snapshots"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(result?.error ?? "list_gvl_snapshots_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlSnapshots = result.gvlSnapshots ?? [];
|
||||||
|
|
||||||
|
if (gvlSnapshots.length === 0) {
|
||||||
|
renderNoGvlSnapshots();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlSnapshotEmpty.hidden = true;
|
||||||
|
gvlSnapshotContent.hidden = false;
|
||||||
|
|
||||||
|
if (!findGvlSnapshot(selectedSnapshotSha256)) {
|
||||||
|
selectedSnapshotSha256 = gvlSnapshots[0]?.sha256 ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGvlSnapshotList();
|
||||||
|
await renderSelectedGvlSnapshotSummary();
|
||||||
|
} catch (error) {
|
||||||
|
gvlSnapshotEmpty.hidden = false;
|
||||||
|
gvlSnapshotContent.hidden = true;
|
||||||
|
gvlSnapshotEmpty.textContent =
|
||||||
|
"Gespeicherte Vendorlisten konnten nicht geladen werden.";
|
||||||
|
console.warn("VG-Observe GVL snapshot list failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNoGvlSnapshots() {
|
||||||
|
gvlSnapshotList.textContent = "";
|
||||||
|
clearGvlSnapshotSummary();
|
||||||
|
gvlSnapshotEmpty.hidden = false;
|
||||||
|
gvlSnapshotContent.hidden = true;
|
||||||
|
gvlSnapshotEmpty.textContent =
|
||||||
|
"Keine gespeicherten offiziellen Vendorlisten vorhanden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGvlSnapshotList() {
|
||||||
|
gvlSnapshotList.textContent = "";
|
||||||
|
|
||||||
|
for (const snapshot of gvlSnapshots) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
const isSelected = snapshot?.sha256 === selectedSnapshotSha256;
|
||||||
|
|
||||||
|
row.className = isSelected ? "is-selected" : "";
|
||||||
|
row.tabIndex = 0;
|
||||||
|
row.setAttribute("role", "button");
|
||||||
|
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
||||||
|
|
||||||
|
row.addEventListener("click", async () => {
|
||||||
|
await selectGvlSnapshot(snapshot?.sha256 ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener("keydown", async (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
await selectGvlSnapshot(snapshot?.sha256 ?? null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
appendListCell(row, formatNullable(snapshot?.vendorListVersion), "numeric");
|
||||||
|
appendListCell(row, formatNullable(snapshot?.fetchedAt));
|
||||||
|
appendListCell(row, shortenSha256(snapshot?.sha256), "sha-cell");
|
||||||
|
appendListCell(row, formatNullable(snapshot?.sourceUrl), "url-cell");
|
||||||
|
|
||||||
|
gvlSnapshotList.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendListCell(row, value, className) {
|
||||||
|
const cell = document.createElement("td");
|
||||||
|
|
||||||
|
if (className) {
|
||||||
|
cell.className = className;
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.textContent = value;
|
||||||
|
row.append(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectGvlSnapshot(sha256) {
|
||||||
|
selectedSnapshotSha256 = sha256;
|
||||||
|
renderGvlSnapshotList();
|
||||||
|
await renderSelectedGvlSnapshotSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderSelectedGvlSnapshotSummary() {
|
||||||
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
|
clearGvlSnapshotSummary();
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "get_gvl_snapshot_summary",
|
||||||
|
payload: {
|
||||||
|
sha256: snapshot.sha256
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(result?.error ?? "get_gvl_snapshot_summary_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSummaryTable(result.summary ?? {});
|
||||||
|
} catch (error) {
|
||||||
|
gvlSnapshotSummary.textContent =
|
||||||
|
"Zusammenfassung dieser Vendorliste konnte nicht geladen werden.";
|
||||||
|
console.warn("VG-Observe GVL snapshot summary failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummaryTable(summary) {
|
||||||
|
const table = document.createElement("table");
|
||||||
|
const body = document.createElement("tbody");
|
||||||
|
const rows = [
|
||||||
|
["Vendorlisten-Version", formatNullable(summary.vendorListVersion)],
|
||||||
|
["Abrufzeitpunkt", formatNullable(summary.fetchedAt)],
|
||||||
|
["Quelle", formatNullable(summary.sourceUrl)],
|
||||||
|
["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)],
|
||||||
|
["Anzahl Zwecke/Purposes", formatCount(summary.purposeCount)],
|
||||||
|
["Anzahl Special Purposes", formatCount(summary.specialPurposeCount)],
|
||||||
|
["Anzahl Features", formatCount(summary.featureCount)],
|
||||||
|
["Anzahl Special Features", formatCount(summary.specialFeatureCount)],
|
||||||
|
["Anzahl Datenkategorien", formatCount(summary.dataCategoryCount)],
|
||||||
|
[
|
||||||
|
"Anzahl Vendor-Beziehungen",
|
||||||
|
formatCount(summary.vendorRelationshipCount)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
table.className = "summary-table";
|
||||||
|
|
||||||
|
for (const [label, value] of rows) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
const labelCell = document.createElement("th");
|
||||||
|
const valueCell = document.createElement("td");
|
||||||
|
|
||||||
|
labelCell.scope = "row";
|
||||||
|
labelCell.textContent = label;
|
||||||
|
valueCell.textContent = value;
|
||||||
|
row.append(labelCell, valueCell);
|
||||||
|
body.append(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.append(body);
|
||||||
|
gvlSnapshotSummary.append(table);
|
||||||
|
|
||||||
|
gvlTechnicalFields.textContent = JSON.stringify(
|
||||||
|
summary.technicalFields ?? {},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
gvlDebugData.textContent = JSON.stringify(
|
||||||
|
{
|
||||||
|
sha256: summary.sha256 ?? null,
|
||||||
|
eventType: summary.eventType ?? null,
|
||||||
|
eventCapturedAt: summary.eventCapturedAt ?? null,
|
||||||
|
diagnostics: summary.diagnostics ?? null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGvlSnapshotSummary() {
|
||||||
|
gvlSnapshotSummary.textContent = "";
|
||||||
|
gvlTechnicalFields.textContent = "";
|
||||||
|
gvlDebugData.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function findGvlSnapshot(sha256) {
|
||||||
|
if (!sha256) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
gvlSnapshots.find((snapshot) => {
|
||||||
|
return snapshot?.sha256 === sha256;
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNullable(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCount(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenSha256(value) {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${String(value).slice(0, 12)}...`;
|
||||||
|
}
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren