Initialize VG-Environment from stable VG-IV baseline

Dieser Commit ist enthalten in:
2026-05-21 19:58:08 +02:00
Commit a1a8147ae2
27 geänderte Dateien mit 3981 neuen und 0 gelöschten Zeilen
+208
Datei anzeigen
@@ -0,0 +1,208 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
font-family: Arial, sans-serif;
color: #e5edf5;
background: #111827;
}
.dashboard {
width: min(1040px, 100%);
margin: 0 auto;
padding: 24px;
}
.dashboard-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;
}
.dashboard-status {
min-height: 18px;
font-size: 13px;
color: #cbd5e1;
}
.dashboard-notice {
max-width: 760px;
padding: 10px 12px;
border: 1px solid #334155;
border-radius: 4px;
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 {
margin-bottom: 22px;
padding-bottom: 20px;
border-bottom: 1px solid #334155;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 0;
}
.metric-grid div {
min-width: 0;
padding: 12px;
border: 1px solid #334155;
border-radius: 4px;
background: #1f2937;
}
.metric-grid dt {
margin: 0 0 8px;
font-size: 12px;
color: #cbd5e1;
}
.metric-grid dd {
margin: 0;
font-size: 24px;
font-weight: 700;
word-break: break-word;
}
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;
}
td:last-child,
th:last-child {
width: 140px;
text-align: right;
}
p {
max-width: 720px;
font-size: 13px;
line-height: 1.5;
color: #cbd5e1;
}
.retention-actions,
.admin-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.admin-status {
min-height: 18px;
margin-top: 12px;
font-size: 13px;
line-height: 1.4;
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;
}
@media (max-width: 640px) {
.dashboard {
padding: 16px;
}
.metric-grid {
grid-template-columns: 1fr;
}
.maintenance-status dl {
grid-template-columns: 1fr 1fr;
}
.retention-actions,
.admin-actions {
display: grid;
}
}
+133
Datei anzeigen
@@ -0,0 +1,133 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VendorGet-IV Evidence Dashboard</title>
<link rel="stylesheet" href="dashboard.css">
</head>
<body>
<main class="dashboard">
<header class="dashboard-header">
<h1>VendorGet-IV Evidence Dashboard</h1>
<div id="dashboard-status" class="dashboard-status" aria-live="polite">
Loading evidence status
</div>
<p class="dashboard-notice">
Verwaltungsbereich: Lesen und manuelle Aktionen bleiben verfügbar.
VG-IV dokumentiert browserseitige Consent-/TCF-Zustände als
evidenzielle Spiegelung.
</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>
<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">
<h2 id="stores-title">Evidence Stores</h2>
<table>
<thead>
<tr>
<th scope="col">Store</th>
<th scope="col">Records</th>
</tr>
</thead>
<tbody>
<tr>
<td>Consent States</td>
<td id="store-consent-states">-</td>
</tr>
<tr>
<td>Consent Events</td>
<td id="store-consent-events">-</td>
</tr>
<tr>
<td>Observed Requests</td>
<td id="store-observed-requests">-</td>
</tr>
<tr>
<td>GVL Snapshots</td>
<td id="store-gvl-snapshots">-</td>
</tr>
<tr>
<td>GVL Snapshot Events</td>
<td id="store-gvl-snapshot-events">-</td>
</tr>
</tbody>
</table>
</section>
<section class="panel" aria-labelledby="retention-title">
<h2 id="retention-title">Retention Status</h2>
<p>
Locked records are protected from partial purge. Full deletion still
requires explicit confirmation.
</p>
<div class="retention-actions">
<button id="lock-all-button" type="button">
Alle Evidenzen als DSGVO-/DSAR-relevant markieren
</button>
<button id="unlock-all-button" type="button">
Alle Evidenz-Sperren entfernen
</button>
</div>
</section>
<section class="panel" aria-labelledby="administration-title">
<h2 id="administration-title">Administration</h2>
<div class="admin-actions">
<button id="gvl-fetch-official-button" type="button">
Official IAB GVL Fetch
</button>
<button id="gvl-import-button" type="button">
GVL JSON importieren
</button>
<button id="evidence-delete-button" type="button">
Evidence Delete
</button>
</div>
<div id="admin-status" class="admin-status" aria-live="polite">
Bereit
</div>
</section>
</main>
<script src="dashboard.js"></script>
</body>
</html>
+436
Datei anzeigen
@@ -0,0 +1,436 @@
"use strict";
const EVIDENCE_MAINTENANCE_SOURCE = "dashboard";
const EVIDENCE_MAINTENANCE_HEARTBEAT_MS = 5 * 1000;
const dashboardStatus = document.getElementById("dashboard-status");
const totalCount = document.getElementById("total-count");
const lockedCount = document.getElementById("locked-count");
const unlockedCount = document.getElementById("unlocked-count");
const maintenanceWriteSuspend = document.getElementById(
"maintenance-write-suspend"
);
const maintenanceSource = document.getElementById("maintenance-source");
const maintenanceHeartbeat = document.getElementById("maintenance-heartbeat");
const maintenanceExpires = document.getElementById("maintenance-expires");
const lockAllButton = document.getElementById("lock-all-button");
const unlockAllButton = document.getElementById("unlock-all-button");
const gvlFetchOfficialButton = document.getElementById(
"gvl-fetch-official-button"
);
const gvlImportButton = document.getElementById("gvl-import-button");
const evidenceDeleteButton = document.getElementById("evidence-delete-button");
const adminStatus = document.getElementById("admin-status");
const gvlImportFileInput = document.createElement("input");
gvlImportFileInput.type = "file";
gvlImportFileInput.accept = ".json,application/json";
gvlImportFileInput.hidden = true;
let evidenceMaintenanceHeartbeatId = null;
const storeCells = {
consent_states: document.getElementById("store-consent-states"),
consent_events: document.getElementById("store-consent-events"),
observed_requests: document.getElementById("store-observed-requests"),
gvl_snapshots: document.getElementById("store-gvl-snapshots"),
gvl_snapshot_events: document.getElementById("store-gvl-snapshot-events")
};
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();
});
window.addEventListener("beforeunload", () => {
endEvidenceMaintenanceMode();
});
window.addEventListener("pagehide", () => {
endEvidenceMaintenanceMode();
});
async function startEvidenceMaintenanceMode() {
try {
const status = await sendEvidenceMaintenanceMessage(
"start_evidence_maintenance_session"
);
renderEvidenceMaintenanceStatus(status);
} catch (error) {
renderEvidenceMaintenanceUnavailable();
console.warn("VendorGet-IV maintenance start failed", error);
}
}
async function refreshEvidenceMaintenanceMode() {
try {
const status = await sendEvidenceMaintenanceMessage(
"refresh_evidence_maintenance_session"
);
renderEvidenceMaintenanceStatus(status);
} catch (error) {
renderEvidenceMaintenanceUnavailable();
console.warn("VendorGet-IV maintenance heartbeat failed", error);
}
}
function endEvidenceMaintenanceMode() {
if (evidenceMaintenanceHeartbeatId !== null) {
clearInterval(evidenceMaintenanceHeartbeatId);
evidenceMaintenanceHeartbeatId = null;
}
void sendEvidenceMaintenanceMessage("end_evidence_maintenance_session").catch(
(error) => {
console.warn("VendorGet-IV maintenance end failed", error);
}
);
}
async function sendEvidenceMaintenanceMessage(type) {
const status = await browser.runtime.sendMessage({
type: type,
payload: {
source: EVIDENCE_MAINTENANCE_SOURCE
}
});
if (!status?.success) {
throw new Error(status?.error ?? `${type}_failed`);
}
return status;
}
function renderEvidenceMaintenanceStatus(status) {
maintenanceWriteSuspend.textContent = status.evidenceWriteSuspended
? "aktiv"
: "inaktiv";
maintenanceSource.textContent = status.source ?? "-";
maintenanceHeartbeat.textContent = formatMaintenanceTimestamp(
status.lastHeartbeatAt
);
maintenanceExpires.textContent = formatMaintenanceTimestamp(status.expiresAt);
}
function renderEvidenceMaintenanceUnavailable() {
maintenanceWriteSuspend.textContent = "unbekannt";
maintenanceSource.textContent = "-";
maintenanceHeartbeat.textContent = "-";
maintenanceExpires.textContent = "-";
}
async function renderEvidenceStatus() {
try {
const status = await browser.runtime.sendMessage({
type: "get_evidence_retention_status"
});
if (!status?.success) {
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
}
totalCount.textContent = String(status.totalCount);
lockedCount.textContent = String(status.lockedCount);
unlockedCount.textContent = String(status.unlockedCount);
renderStoreCounts(status.storeCounts ?? {});
renderStatusMessage("Evidence status loaded");
} catch (error) {
renderStatusMessage("Evidence status could not be loaded");
console.warn("VendorGet-IV dashboard status failed", error);
}
}
function renderStoreCounts(storeCounts) {
for (const [storeName, cell] of Object.entries(storeCells)) {
cell.textContent = String(storeCounts[storeName] ?? 0);
}
}
async function fetchOfficialGvl() {
gvlFetchOfficialButton.disabled = true;
renderAdminStatus("Fetching official IAB GVL...");
try {
const result = await browser.runtime.sendMessage({
type: "fetch_official_gvl"
});
if (!result?.success) {
throw new Error(result?.error ?? "official_gvl_fetch_failed");
}
await renderEvidenceStatus();
renderAdminStatus(
"Fetched successfully - " +
`${result.alreadyKnown ? "already known" : "newly stored"} - ` +
`vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
`sha256 ${shortenSha256(result.sha256)}`
);
} catch (error) {
renderAdminStatus("Official GVL fetch failed");
console.warn("VendorGet-IV official GVL fetch failed", error);
} finally {
gvlFetchOfficialButton.disabled = false;
}
}
async function importGvlFile(file) {
gvlImportButton.disabled = true;
renderAdminStatus("Import läuft...");
try {
const fileContent = await readFileAsText(file);
const rawJson = JSON.parse(fileContent);
if (!isGvlImportCandidate(rawJson)) {
throw new Error("invalid_gvl_json");
}
const result = await browser.runtime.sendMessage({
type: "gvl_import_json",
payload: {
rawJson: rawJson,
sourceUrl: "local-file-import"
}
});
if (!result?.success) {
throw new Error(result?.error ?? "gvl_import_failed");
}
await renderEvidenceStatus();
renderAdminStatus(
`${result.alreadyKnown ? "already known" : "imported"} - ` +
`vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
`sha256 ${shortenSha256(result.sha256)}`
);
} catch (error) {
renderAdminStatus("Import fehlgeschlagen");
console.warn("VendorGet-IV GVL import failed", error);
} finally {
gvlImportButton.disabled = false;
}
}
function readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error);
reader.onload = () => resolve(reader.result);
reader.readAsText(file);
});
}
function isGvlImportCandidate(value) {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
value.vendorListVersion !== undefined &&
value.vendors &&
typeof value.vendors === "object" &&
!Array.isArray(value.vendors)
);
}
async function handleEvidenceDeleteClick() {
evidenceDeleteButton.disabled = true;
try {
const status = await getEvidenceRetentionStatus();
const confirmed = confirm(
"Alle lokal gespeicherten VG-IV-Evidenzdaten wirklich löschen?"
);
if (!confirmed) {
renderAdminStatus("Löschung abgebrochen");
return;
}
if (status.lockedCount === 0) {
await deleteAllEvidenceDatabase();
await renderEvidenceStatus();
renderAdminStatus("Evidenzdaten gelöscht");
return;
}
const deleteLockedRecords = confirm(
`Achtung: ${status.lockedCount} Datensätze wurden als ` +
"DSGVO-/DSAR-relevant markiert. Sollen auch diese Datensätze " +
"wirklich gelöscht werden?"
);
if (deleteLockedRecords) {
await deleteAllEvidenceDatabase();
await renderEvidenceStatus();
renderAdminStatus("Evidenzdaten gelöscht");
return;
}
const result = await browser.runtime.sendMessage({
type: "purge_unlocked_evidence_records"
});
if (!result?.success) {
throw new Error(result?.error ?? "purge_unlocked_evidence_records_failed");
}
await renderEvidenceStatus();
renderAdminStatus(
`${result.deletedCount} Datensätze gelöscht, ` +
`${result.keptLockedCount} gesperrte Datensätze behalten`
);
} catch (error) {
renderAdminStatus("Löschung fehlgeschlagen");
console.warn("VendorGet-IV evidence delete failed", error);
} finally {
evidenceDeleteButton.disabled = false;
}
}
async function getEvidenceRetentionStatus() {
const status = await browser.runtime.sendMessage({
type: "get_evidence_retention_status"
});
if (!status?.success) {
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
}
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) {
dashboardStatus.textContent = message;
}
function renderAdminStatus(message) {
adminStatus.textContent = message;
}
function shortenSha256(value) {
if (!value) {
return "n/a";
}
return `${value.slice(0, 12)}...`;
}
function formatMaintenanceTimestamp(value) {
if (!value) {
return "-";
}
return value;
}