From 31001b96109acf420d3f0a729308e39d7402aee3 Mon Sep 17 00:00:00 2001 From: jensmohr Date: Wed, 27 May 2026 17:50:44 +0200 Subject: [PATCH] Add structured JSON evidence export workflow --- manifest.json | 1 + src/background.js | 11 +++++ src/core/evidence-export-json.js | 80 ++++++++++++++++++++++++++++++++ src/popup/popup.html | 4 ++ src/popup/popup.js | 75 ++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+) create mode 100644 src/core/evidence-export-json.js diff --git a/manifest.json b/manifest.json index c428a05..3af19ff 100644 --- a/manifest.json +++ b/manifest.json @@ -22,6 +22,7 @@ "src/modules/vg-observe/module.js", "src/background/db/db-constants.js", "src/background/db/db-core.js", + "src/core/evidence-export-json.js", "src/background/gvl/gvl-vendor-normalizer.js", "src/background/gvl/gvl-vendor-relationship-normalizer.js", "src/background/gvl/gvl-catalog-normalizer.js", diff --git a/src/background.js b/src/background.js index b30b01c..cdb2b8e 100644 --- a/src/background.js +++ b/src/background.js @@ -37,6 +37,10 @@ async function handleVendorGetMessage(message, sender) { return handleFetchOfficialGvlMessage(); } + if (message.type === "export_evidence_json") { + return handleExportEvidenceJsonMessage(); + } + if (message.type === "start_evidence_maintenance_session") { return startEvidenceMaintenanceSession(message?.payload?.source); } @@ -169,6 +173,13 @@ async function handleGetEvidenceRetentionStatusMessage() { }; } +async function handleExportEvidenceJsonMessage() { + return { + success: true, + export: await exportVendorGetEvidenceJson() + }; +} + async function handleGetLatestGvlUpdateStatusMessage() { const db = await openVendorGetDb(); const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db); diff --git a/src/core/evidence-export-json.js b/src/core/evidence-export-json.js new file mode 100644 index 0000000..98841d8 --- /dev/null +++ b/src/core/evidence-export-json.js @@ -0,0 +1,80 @@ +"use strict"; + +async function exportVendorGetEvidenceJson() { + const db = await openVendorGetDb(); + const storeNames = [...VENDORGET_EVIDENCE_STORE_NAMES]; + const stores = await exportVendorGetEvidenceStores(db, storeNames); + const summary = buildVendorGetEvidenceExportSummary(stores); + + return { + metadata: { + exportVersion: 1, + exportedAt: new Date().toISOString(), + project: "VG-Environment", + extension: "VendorGet", + dbName: db.name, + dbVersion: db.version, + stores: storeNames, + summary + }, + stores + }; +} + +function buildVendorGetEvidenceExportSummary(stores) { + const storeRecordCounts = {}; + let totalRecordCount = 0; + + for (const [storeName, storeExport] of Object.entries(stores)) { + const recordCount = storeExport.recordCount; + + storeRecordCounts[storeName] = recordCount; + totalRecordCount += recordCount; + } + + return { + totalRecordCount, + storeRecordCounts + }; +} + +function exportVendorGetEvidenceStores(db, storeNames) { + return new Promise((resolve, reject) => { + const stores = {}; + const tx = db.transaction(storeNames, "readonly"); + + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error); + tx.oncomplete = () => resolve(stores); + + for (const storeName of storeNames) { + const objectStore = tx.objectStore(storeName); + const storeExport = { + keyPath: objectStore.keyPath, + autoIncrement: objectStore.autoIncrement, + recordCount: 0, + records: [] + }; + + stores[storeName] = storeExport; + + const cursorRequest = objectStore.openCursor(); + + cursorRequest.onerror = () => reject(cursorRequest.error); + cursorRequest.onsuccess = () => { + const cursor = cursorRequest.result; + + if (!cursor) { + return; + } + + storeExport.records.push({ + key: cursor.key, + value: cursor.value + }); + storeExport.recordCount += 1; + cursor.continue(); + }; + } + }); +} diff --git a/src/popup/popup.html b/src/popup/popup.html index 16db664..f4aa9d5 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -63,6 +63,10 @@ + +
Status wird geladen
diff --git a/src/popup/popup.js b/src/popup/popup.js index 4eb8ffe..31f9834 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -13,6 +13,12 @@ const evidenceLockedCount = document.getElementById("evidence-locked-count"); const evidenceDashboardButton = document.getElementById( "evidence-dashboard-button" ); +const evidenceExportJsonButton = document.getElementById( + "evidence-export-json-button" +); +const evidenceExportJsonStatus = document.getElementById( + "evidence-export-json-status" +); const evidenceRetentionStatus = document.getElementById( "evidence-retention-status" ); @@ -61,6 +67,8 @@ document.addEventListener("DOMContentLoaded", async () => { url: browser.runtime.getURL("src/dashboard/dashboard.html") }); }); + + evidenceExportJsonButton.addEventListener("click", exportEvidenceJsonFile); }); async function renderSettings() { @@ -134,3 +142,70 @@ function formatStatusValue(value) { function renderEvidenceRetentionMessage(message) { evidenceRetentionStatus.textContent = message; } + +async function exportEvidenceJsonFile() { + evidenceExportJsonButton.disabled = true; + renderEvidenceExportJsonMessage("Export läuft…"); + + try { + const result = await browser.runtime.sendMessage({ + type: "export_evidence_json" + }); + + if (!result?.success || !result.export) { + throw new Error(result?.error ?? "export_evidence_json_failed"); + } + + downloadJsonExport(result.export); + renderEvidenceExportJsonMessage(buildExportSuccessMessage(result.export)); + } catch (error) { + renderEvidenceExportJsonMessage("Export fehlgeschlagen"); + console.warn("VendorGet-IV evidence JSON export failed", error); + } finally { + evidenceExportJsonButton.disabled = false; + } +} + +function downloadJsonExport(exportContainer) { + const json = JSON.stringify(exportContainer, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const downloadLink = document.createElement("a"); + + downloadLink.href = url; + downloadLink.download = `vendorget-evidence-export-${formatExportTimestamp( + new Date() + )}.json`; + document.body.append(downloadLink); + downloadLink.click(); + downloadLink.remove(); + + setTimeout(() => URL.revokeObjectURL(url), 0); +} + +function buildExportSuccessMessage(exportContainer) { + const storeCount = exportContainer?.metadata?.stores?.length ?? 0; + const totalRecordCount = + exportContainer?.metadata?.summary?.totalRecordCount ?? 0; + + return `Export gespeichert: ${storeCount} Stores, ${totalRecordCount} Records`; +} + +function formatExportTimestamp(date) { + const year = date.getFullYear(); + const month = padDatePart(date.getMonth() + 1); + const day = padDatePart(date.getDate()); + const hours = padDatePart(date.getHours()); + const minutes = padDatePart(date.getMinutes()); + const seconds = padDatePart(date.getSeconds()); + + return `${year}${month}${day}-${hours}${minutes}${seconds}`; +} + +function padDatePart(value) { + return String(value).padStart(2, "0"); +} + +function renderEvidenceExportJsonMessage(message) { + evidenceExportJsonStatus.textContent = message; +}