Improve GVL explorer usability and evidence workflows
Dieser Commit ist enthalten in:
@@ -38,3 +38,6 @@ web-ext-artifacts/
|
|||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
# Local GVL evidence exports
|
||||||
|
data/GVL-Dumps/*.json
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"src/background/db/db-constants.js",
|
"src/background/db/db-constants.js",
|
||||||
"src/background/db/db-core.js",
|
"src/background/db/db-core.js",
|
||||||
"src/core/evidence-export-json.js",
|
"src/core/evidence-export-json.js",
|
||||||
|
"src/core/gvl-evidence-json.js",
|
||||||
"src/background/gvl/gvl-vendor-normalizer.js",
|
"src/background/gvl/gvl-vendor-normalizer.js",
|
||||||
"src/background/gvl/gvl-vendor-relationship-normalizer.js",
|
"src/background/gvl/gvl-vendor-relationship-normalizer.js",
|
||||||
"src/background/gvl/gvl-catalog-normalizer.js",
|
"src/background/gvl/gvl-catalog-normalizer.js",
|
||||||
|
|||||||
@@ -29,6 +29,22 @@ async function handleVendorGetMessage(message, sender) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "export_gvl_evidence_json") {
|
||||||
|
return handleExportGvlEvidenceJsonMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "export_gvl_revision_evidence_json") {
|
||||||
|
return handleExportGvlRevisionEvidenceJsonMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "verify_gvl_revision_evidence_json") {
|
||||||
|
return handleVerifyGvlRevisionEvidenceJsonMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "import_gvl_evidence_json") {
|
||||||
|
return handleImportGvlEvidenceJsonMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === "gvl_import_json") {
|
if (message.type === "gvl_import_json") {
|
||||||
return handleGvlImportJsonMessage(message);
|
return handleGvlImportJsonMessage(message);
|
||||||
}
|
}
|
||||||
@@ -196,6 +212,59 @@ async function handleExportEvidenceJsonMessage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleExportGvlEvidenceJsonMessage() {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
export: await exportVendorGetGvlEvidenceJson()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportGvlRevisionEvidenceJsonMessage(message) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
export: await exportVendorGetGvlRevisionEvidenceJson(
|
||||||
|
message?.payload?.snapshotSha256 ?? null
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error?.message ?? String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVerifyGvlRevisionEvidenceJsonMessage(message) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
verification: await verifyVendorGetGvlRevisionEvidenceJson(
|
||||||
|
message?.payload?.export ?? null
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error?.message ?? String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportGvlEvidenceJsonMessage(message) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
import: await importVendorGetGvlEvidenceJson(message?.payload?.export)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error?.message ?? String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleGetLatestGvlUpdateStatusMessage() {
|
async function handleGetLatestGvlUpdateStatusMessage() {
|
||||||
const db = await openVendorGetDb();
|
const db = await openVendorGetDb();
|
||||||
const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
|
const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
|
||||||
@@ -445,11 +514,13 @@ async function handleGetGvlVendorDetailMessage(message) {
|
|||||||
const rawEvidence = rawGvlSha256
|
const rawEvidence = rawGvlSha256
|
||||||
? await getGvlRawEvidenceBySha256(db, rawGvlSha256)
|
? await getGvlRawEvidenceBySha256(db, rawGvlSha256)
|
||||||
: null;
|
: null;
|
||||||
|
const gvlInfo = await getGvlVendorDetailGvlInfo(db, vendorRecord);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
vendorDetail: {
|
vendorDetail: {
|
||||||
vendor: vendorRecord,
|
vendor: vendorRecord,
|
||||||
|
gvlInfo,
|
||||||
snapshot: buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256),
|
snapshot: buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256),
|
||||||
rawEvidence: buildGvlVendorDetailRawEvidenceSummary(
|
rawEvidence: buildGvlVendorDetailRawEvidenceSummary(
|
||||||
rawEvidence,
|
rawEvidence,
|
||||||
@@ -459,6 +530,255 @@ async function handleGetGvlVendorDetailMessage(message) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getGvlVendorDetailGvlInfo(db, vendorRecord) {
|
||||||
|
const vendorListVersion = vendorRecord?.vendorListVersion ?? null;
|
||||||
|
const vendorId = vendorRecord?.vendorId ?? null;
|
||||||
|
|
||||||
|
if (vendorListVersion === null || vendorId === null) {
|
||||||
|
return buildEmptyGvlVendorDetailGvlInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationships = await getGvlVendorRelationshipsForVendor(
|
||||||
|
db,
|
||||||
|
vendorListVersion,
|
||||||
|
vendorId
|
||||||
|
);
|
||||||
|
const catalogs = await getGvlVendorDetailCatalogs(db, vendorListVersion);
|
||||||
|
const rawVendor = vendorRecord?.rawVendor ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
purposes: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.purposes,
|
||||||
|
relationships.purpose,
|
||||||
|
catalogs.purposes
|
||||||
|
),
|
||||||
|
legIntPurposes: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.legIntPurposes,
|
||||||
|
relationships.legIntPurpose,
|
||||||
|
catalogs.purposes
|
||||||
|
),
|
||||||
|
flexiblePurposes: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.flexiblePurposes,
|
||||||
|
relationships.flexiblePurpose,
|
||||||
|
catalogs.purposes
|
||||||
|
),
|
||||||
|
specialPurposes: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.specialPurposes,
|
||||||
|
relationships.specialPurpose,
|
||||||
|
catalogs.specialPurposes
|
||||||
|
),
|
||||||
|
features: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.features,
|
||||||
|
relationships.feature,
|
||||||
|
catalogs.features
|
||||||
|
),
|
||||||
|
specialFeatures: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.specialFeatures,
|
||||||
|
relationships.specialFeature,
|
||||||
|
catalogs.specialFeatures
|
||||||
|
),
|
||||||
|
dataDeclaration: buildGvlVendorDetailCatalogList(
|
||||||
|
rawVendor.dataDeclaration,
|
||||||
|
[],
|
||||||
|
catalogs.dataCategories
|
||||||
|
),
|
||||||
|
dataCategoriesTextAvailable: catalogs.dataCategories.size > 0,
|
||||||
|
dataRetention: rawVendor.dataRetention ?? null,
|
||||||
|
overflow: rawVendor.overflow ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEmptyGvlVendorDetailGvlInfo() {
|
||||||
|
return {
|
||||||
|
purposes: [],
|
||||||
|
legIntPurposes: [],
|
||||||
|
flexiblePurposes: [],
|
||||||
|
specialPurposes: [],
|
||||||
|
features: [],
|
||||||
|
specialFeatures: [],
|
||||||
|
dataDeclaration: [],
|
||||||
|
dataCategoriesTextAvailable: false,
|
||||||
|
dataRetention: null,
|
||||||
|
overflow: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlVendorRelationshipsForVendor(db, vendorListVersion, vendorId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const storeName = VENDORGET_STORE_NAMES.gvlVendorRelationships;
|
||||||
|
const tx = db.transaction([storeName], "readonly");
|
||||||
|
const relationshipsStore = tx.objectStore(storeName);
|
||||||
|
const vendorIdIndex = relationshipsStore.index("vendorId");
|
||||||
|
const cursorRequest = vendorIdIndex.openCursor(IDBKeyRange.only(vendorId));
|
||||||
|
const relationships = {};
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = cursor.value;
|
||||||
|
|
||||||
|
if (record?.vendorListVersion === vendorListVersion) {
|
||||||
|
const relationshipType = record.relationshipType ?? "unknown";
|
||||||
|
|
||||||
|
if (!relationships[relationshipType]) {
|
||||||
|
relationships[relationshipType] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
relationships[relationshipType].push(record.relatedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(relationships);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGvlVendorDetailCatalogs(db, vendorListVersion) {
|
||||||
|
const [
|
||||||
|
purposes,
|
||||||
|
specialPurposes,
|
||||||
|
features,
|
||||||
|
specialFeatures,
|
||||||
|
dataCategories
|
||||||
|
] = await Promise.all([
|
||||||
|
getGvlCatalogMapForVersion(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlPurposes,
|
||||||
|
vendorListVersion
|
||||||
|
),
|
||||||
|
getGvlCatalogMapForVersion(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
|
||||||
|
vendorListVersion
|
||||||
|
),
|
||||||
|
getGvlCatalogMapForVersion(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlFeatures,
|
||||||
|
vendorListVersion
|
||||||
|
),
|
||||||
|
getGvlCatalogMapForVersion(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
|
||||||
|
vendorListVersion
|
||||||
|
),
|
||||||
|
getGvlCatalogMapForVersion(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlDataCategories,
|
||||||
|
vendorListVersion
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
purposes,
|
||||||
|
specialPurposes,
|
||||||
|
features,
|
||||||
|
specialFeatures,
|
||||||
|
dataCategories
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlCatalogMapForVersion(db, storeName, vendorListVersion) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([storeName], "readonly");
|
||||||
|
const catalogStore = tx.objectStore(storeName);
|
||||||
|
const vendorListVersionIndex = catalogStore.index("vendorListVersion");
|
||||||
|
const cursorRequest = vendorListVersionIndex.openCursor(
|
||||||
|
IDBKeyRange.only(vendorListVersion)
|
||||||
|
);
|
||||||
|
const catalogMap = new Map();
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = cursor.value;
|
||||||
|
const catalogId = getGvlCatalogRecordId(record);
|
||||||
|
|
||||||
|
if (catalogId !== null) {
|
||||||
|
catalogMap.set(catalogId, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(catalogMap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlCatalogRecordId(record) {
|
||||||
|
const catalogId =
|
||||||
|
record?.catalogId ??
|
||||||
|
record?.purposeId ??
|
||||||
|
record?.specialPurposeId ??
|
||||||
|
record?.featureId ??
|
||||||
|
record?.specialFeatureId ??
|
||||||
|
record?.dataCategoryId ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (catalogId === null || catalogId === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericId = Number(catalogId);
|
||||||
|
|
||||||
|
return Number.isFinite(numericId) ? numericId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlVendorDetailCatalogList(rawIds, relationshipIds, catalogMap) {
|
||||||
|
const ids = mergeGvlVendorDetailIds(rawIds, relationshipIds);
|
||||||
|
|
||||||
|
return ids.map((id) => {
|
||||||
|
const catalogRecord = catalogMap.get(id) ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: catalogRecord?.name ?? null,
|
||||||
|
description: catalogRecord?.description ?? null,
|
||||||
|
descriptionLegal: catalogRecord?.descriptionLegal ?? null,
|
||||||
|
illustrations: catalogRecord?.illustrations ?? null,
|
||||||
|
rawCatalog: catalogRecord?.rawCatalog ?? null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeGvlVendorDetailIds(...sources) {
|
||||||
|
const ids = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
sources.forEach((source) => {
|
||||||
|
if (!Array.isArray(source)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.forEach((value) => {
|
||||||
|
const id = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isFinite(id) || seen.has(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(id);
|
||||||
|
ids.push(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
function parseGvlVendorDetailId(value) {
|
function parseGvlVendorDetailId(value) {
|
||||||
const vendorId = Number(value);
|
const vendorId = Number(value);
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,37 @@ p {
|
|||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.evidence-status {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-status.is-neutral {
|
||||||
|
color: #cbd5e1;
|
||||||
|
border-color: #334155;
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-status.is-success {
|
||||||
|
color: #bbf7d0;
|
||||||
|
border-color: #166534;
|
||||||
|
background: #052e16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-status.is-warning {
|
||||||
|
color: #fde68a;
|
||||||
|
border-color: #92400e;
|
||||||
|
background: #422006;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-status.is-error {
|
||||||
|
color: #fecaca;
|
||||||
|
border-color: #991b1b;
|
||||||
|
background: #450a0a;
|
||||||
|
}
|
||||||
|
|
||||||
.vendor-detail-form {
|
.vendor-detail-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(120px, 180px) auto;
|
grid-template-columns: auto minmax(120px, 180px) auto;
|
||||||
@@ -137,11 +168,40 @@ button {
|
|||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-action {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #475569;
|
||||||
|
border-radius: 4px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #1f2937;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-action.is-disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.65;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -211,11 +271,100 @@ th {
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vendor-detail-panel {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendor-detail-panel > summary,
|
||||||
|
.subject-details > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.vendor-detail-section h3 {
|
.vendor-detail-section h3 {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-table td {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-details {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gvl-catalog-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gvl-catalog-section h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e5edf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-item {
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #182231;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-item summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: #e5edf5;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(120px, 180px) 1fr;
|
||||||
|
gap: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-top: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-list dt,
|
||||||
|
.definition-list dd {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-list dt {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-list dd {
|
||||||
|
color: #e5edf5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-text {
|
||||||
|
max-width: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-action {
|
||||||
|
width: fit-content;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
@@ -258,4 +407,8 @@ th {
|
|||||||
.vendor-detail-form {
|
.vendor-detail-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.definition-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,27 @@
|
|||||||
<button id="gvl-fetch-official-button" type="button">
|
<button id="gvl-fetch-official-button" type="button">
|
||||||
GVL aus Web laden
|
GVL aus Web laden
|
||||||
</button>
|
</button>
|
||||||
|
<button id="gvl-revision-evidence-export-button" type="button">
|
||||||
|
Ausgewählte GVL-Revision exportieren
|
||||||
|
</button>
|
||||||
|
<label class="file-action" for="gvl-revision-evidence-verify-input">
|
||||||
|
GVL-Revision-Export verifizieren
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="gvl-revision-evidence-verify-input"
|
||||||
|
class="visually-hidden"
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
>
|
||||||
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
||||||
Bereit
|
Bereit
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="gvl-evidence-transport-status"
|
||||||
|
class="fetch-status"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
<p id="gvl-snapshot-empty" class="empty-state" hidden>
|
<p id="gvl-snapshot-empty" class="empty-state" hidden>
|
||||||
Keine gespeicherten offiziellen Vendorlisten vorhanden.
|
Keine gespeicherten offiziellen Vendorlisten vorhanden.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ const gvlDebugData = document.getElementById("gvl-debug-data");
|
|||||||
const gvlFetchOfficialButton = document.getElementById(
|
const gvlFetchOfficialButton = document.getElementById(
|
||||||
"gvl-fetch-official-button"
|
"gvl-fetch-official-button"
|
||||||
);
|
);
|
||||||
|
const gvlRevisionEvidenceExportButton = document.getElementById(
|
||||||
|
"gvl-revision-evidence-export-button"
|
||||||
|
);
|
||||||
|
const gvlRevisionEvidenceVerifyInput = document.getElementById(
|
||||||
|
"gvl-revision-evidence-verify-input"
|
||||||
|
);
|
||||||
|
const gvlEvidenceTransportStatus = document.getElementById(
|
||||||
|
"gvl-evidence-transport-status"
|
||||||
|
);
|
||||||
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
||||||
const gvlRebuildNormalizedButton = document.getElementById(
|
const gvlRebuildNormalizedButton = document.getElementById(
|
||||||
"gvl-rebuild-normalized-button"
|
"gvl-rebuild-normalized-button"
|
||||||
@@ -53,6 +62,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
await fetchOfficialGvl();
|
await fetchOfficialGvl();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gvlRevisionEvidenceExportButton.addEventListener("click", async () => {
|
||||||
|
await exportSelectedGvlRevisionEvidenceJsonFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
gvlRevisionEvidenceVerifyInput.addEventListener("change", async () => {
|
||||||
|
await verifyGvlRevisionEvidenceJsonFile();
|
||||||
|
});
|
||||||
|
|
||||||
gvlRebuildNormalizedButton.addEventListener("click", async () => {
|
gvlRebuildNormalizedButton.addEventListener("click", async () => {
|
||||||
await rebuildSelectedGvlSnapshotNormalizedData();
|
await rebuildSelectedGvlSnapshotNormalizedData();
|
||||||
});
|
});
|
||||||
@@ -112,10 +129,230 @@ function renderFetchStatus(message) {
|
|||||||
gvlFetchStatus.textContent = message;
|
gvlFetchStatus.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderGvlEvidenceTransportStatus(message, statusKind = "neutral") {
|
||||||
|
gvlEvidenceTransportStatus.textContent = message;
|
||||||
|
gvlEvidenceTransportStatus.className = [
|
||||||
|
"fetch-status",
|
||||||
|
"evidence-status",
|
||||||
|
`is-${statusKind}`
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function renderRebuildStatus(message) {
|
function renderRebuildStatus(message) {
|
||||||
gvlRebuildNormalizedStatus.textContent = message;
|
gvlRebuildNormalizedStatus.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportSelectedGvlRevisionEvidenceJsonFile() {
|
||||||
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"Keine GVL-Revision ausgewählt.",
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlRevisionEvidenceExportButton.disabled = true;
|
||||||
|
renderGvlEvidenceTransportStatus("GVL-Revision-Export läuft...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "export_gvl_revision_evidence_json",
|
||||||
|
payload: {
|
||||||
|
snapshotSha256: snapshot.sha256
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success || !result.export) {
|
||||||
|
throw new Error(
|
||||||
|
result?.error ?? "export_gvl_revision_evidence_json_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Export wird intern verifiziert..."
|
||||||
|
);
|
||||||
|
|
||||||
|
const verification = await verifyGeneratedGvlRevisionEvidenceExport(
|
||||||
|
result.export
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verification.valid) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
[
|
||||||
|
"GVL-Revision-Export nicht erzeugt: interne Verifikation fehlgeschlagen.",
|
||||||
|
buildGvlRevisionEvidenceVerificationMessage(verification)
|
||||||
|
].join(" "),
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadGvlRevisionEvidenceJsonExport(result.export);
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
[
|
||||||
|
"GVL-Revision exportiert und intern verifiziert.",
|
||||||
|
buildGvlRevisionEvidenceExportSuccessMessage(result.export)
|
||||||
|
].join(" "),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Export fehlgeschlagen.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
console.warn("VG-Observe GVL revision evidence export failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlRevisionEvidenceExportButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyGeneratedGvlRevisionEvidenceExport(exportContainer) {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "verify_gvl_revision_evidence_json",
|
||||||
|
payload: {
|
||||||
|
export: exportContainer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success || !result.verification) {
|
||||||
|
throw new Error(
|
||||||
|
result?.error ?? "verify_gvl_revision_evidence_json_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.verification;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadGvlRevisionEvidenceJsonExport(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");
|
||||||
|
const metadata = exportContainer?.metadata ?? {};
|
||||||
|
const vendorListVersion = metadata.vendorListVersion ?? "unknown";
|
||||||
|
const exportedAtUtcCompact =
|
||||||
|
metadata.exportedAtUtcCompact ?? formatExportTimestampUtcCompact(new Date());
|
||||||
|
|
||||||
|
downloadLink.href = url;
|
||||||
|
downloadLink.download = `GVL-REV-${vendorListVersion}-${exportedAtUtcCompact}.json`;
|
||||||
|
document.body.append(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlRevisionEvidenceExportSuccessMessage(exportContainer) {
|
||||||
|
const metadata = exportContainer?.metadata ?? {};
|
||||||
|
const recordCount = getGvlRevisionEvidenceNormalizedRecordCount(
|
||||||
|
exportContainer?.normalized
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
"GVL-Revision exportiert:",
|
||||||
|
`Vendorlisten-Version ${formatNullable(metadata.vendorListVersion)}`,
|
||||||
|
`Snapshot ${shortenSha256(metadata.snapshotSha256)}`,
|
||||||
|
`Raw-GVL ${shortenSha256(metadata.rawGvlSha256)}`,
|
||||||
|
`Payload ${shortenSha256(metadata.exportPayloadSha256)}`,
|
||||||
|
`${recordCount} normalisierte Records.`
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlRevisionEvidenceNormalizedRecordCount(normalized) {
|
||||||
|
return [
|
||||||
|
"gvl_vendors",
|
||||||
|
"gvl_purposes",
|
||||||
|
"gvl_special_purposes",
|
||||||
|
"gvl_features",
|
||||||
|
"gvl_special_features",
|
||||||
|
"gvl_data_categories",
|
||||||
|
"gvl_vendor_relationships"
|
||||||
|
].reduce((total, storeName) => {
|
||||||
|
return total + (normalized?.[storeName]?.length ?? 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyGvlRevisionEvidenceJsonFile() {
|
||||||
|
const file = gvlRevisionEvidenceVerifyInput.files?.[0] ?? null;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGvlRevisionEvidenceVerifyDisabled(true);
|
||||||
|
renderGvlEvidenceTransportStatus("GVL-Revision-Export wird verifiziert...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exportContainer = JSON.parse(await file.text());
|
||||||
|
const verification = await verifyGeneratedGvlRevisionEvidenceExport(
|
||||||
|
exportContainer
|
||||||
|
);
|
||||||
|
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
buildGvlRevisionEvidenceVerificationMessage(verification),
|
||||||
|
verification.valid ? "success" : "error"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
renderGvlEvidenceTransportStatus(
|
||||||
|
"GVL-Revision-Export ist nicht valide.",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
console.warn("VG-Observe GVL revision evidence verification failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlRevisionEvidenceVerifyInput.value = "";
|
||||||
|
setGvlRevisionEvidenceVerifyDisabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGvlRevisionEvidenceVerifyDisabled(disabled) {
|
||||||
|
const importLabel = document.querySelector(
|
||||||
|
"label[for='gvl-revision-evidence-verify-input']"
|
||||||
|
);
|
||||||
|
|
||||||
|
gvlRevisionEvidenceVerifyInput.disabled = disabled;
|
||||||
|
importLabel?.classList.toggle("is-disabled", disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlRevisionEvidenceVerificationMessage(verification) {
|
||||||
|
const validityLabel = verification.valid ? "valide" : "nicht valide";
|
||||||
|
const counts = verification.normalizedCounts ?? {};
|
||||||
|
const normalizedRecordCount = Object.values(counts).reduce((total, count) => {
|
||||||
|
return total + Number(count ?? 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
`Verifikation: ${validityLabel}.`,
|
||||||
|
`Vendorlisten-Version ${formatNullable(verification.vendorListVersion)}.`,
|
||||||
|
`Snapshot ${shortenSha256(verification.snapshotSha256)}.`,
|
||||||
|
`Raw-GVL ${shortenSha256(verification.rawGvlSha256)}.`,
|
||||||
|
`Payload ${shortenSha256(verification.exportPayloadSha256)}.`,
|
||||||
|
`Normalisierte Records: ${normalizedRecordCount} (${formatNormalizedCounts(
|
||||||
|
counts
|
||||||
|
)}).`,
|
||||||
|
verification.valid
|
||||||
|
? ""
|
||||||
|
: `Fehler: ${(verification.errors ?? []).join(", ")}.`
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNormalizedCounts(counts) {
|
||||||
|
return [
|
||||||
|
["Vendoren", counts.gvl_vendors],
|
||||||
|
["Purposes", counts.gvl_purposes],
|
||||||
|
["Special Purposes", counts.gvl_special_purposes],
|
||||||
|
["Features", counts.gvl_features],
|
||||||
|
["Special Features", counts.gvl_special_features],
|
||||||
|
["Data Categories", counts.gvl_data_categories],
|
||||||
|
["Vendor-Beziehungen", counts.gvl_vendor_relationships]
|
||||||
|
]
|
||||||
|
.map(([label, count]) => `${label}: ${Number(count ?? 0)}`)
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
async function rebuildSelectedGvlSnapshotNormalizedData() {
|
async function rebuildSelectedGvlSnapshotNormalizedData() {
|
||||||
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
@@ -213,43 +450,292 @@ function renderGvlVendorDetailResult(detail) {
|
|||||||
const vendor = detail.vendor ?? {};
|
const vendor = detail.vendor ?? {};
|
||||||
const snapshot = detail.snapshot ?? {};
|
const snapshot = detail.snapshot ?? {};
|
||||||
const rawEvidence = detail.rawEvidence ?? {};
|
const rawEvidence = detail.rawEvidence ?? {};
|
||||||
|
const gvlInfo = detail.gvlInfo ?? {};
|
||||||
|
|
||||||
gvlVendorDetailResult.textContent = "";
|
gvlVendorDetailResult.textContent = "";
|
||||||
gvlVendorDetailResult.append(
|
gvlVendorDetailResult.append(
|
||||||
buildKeyValueSection("Normalisierte Vendor-Felder", [
|
buildVendorDetailPanel(vendor, snapshot, rawEvidence, gvlInfo)
|
||||||
["Vendor-ID", formatNullable(vendor.vendorId)],
|
);
|
||||||
["Name", formatNullable(vendor.name)],
|
}
|
||||||
["Policy URL", formatNullable(vendor.policyUrl)],
|
|
||||||
["Deleted Date", formatNullable(vendor.deletedDate)],
|
function buildVendorDetailPanel(vendor, snapshot, rawEvidence, gvlInfo) {
|
||||||
["Uses Cookies", formatNullable(vendor.usesCookies)],
|
const details = document.createElement("details");
|
||||||
["Cookie Max Age Seconds", formatNullable(vendor.cookieMaxAgeSeconds)],
|
const summary = document.createElement("summary");
|
||||||
["Uses Non-Cookie Access", formatNullable(vendor.usesNonCookieAccess)],
|
const closeButton = document.createElement("button");
|
||||||
[
|
|
||||||
"Device Storage Disclosure URL",
|
details.className = "vendor-detail-panel";
|
||||||
formatNullable(vendor.deviceStorageDisclosureUrl)
|
details.open = true;
|
||||||
],
|
summary.textContent = `Vendor-Details: ${formatNullable(
|
||||||
["Domains", formatJsonValue(vendor.domains)],
|
vendor.name
|
||||||
["Snapshot SHA256", formatNullable(vendor.snapshotSha256)]
|
)} (ID ${formatNullable(vendor.vendorId)})`;
|
||||||
]),
|
|
||||||
buildKeyValueSection("Snapshot-Herkunft", [
|
closeButton.type = "button";
|
||||||
|
closeButton.className = "secondary-action";
|
||||||
|
closeButton.textContent = "Vendor-Details schließen";
|
||||||
|
closeButton.addEventListener("click", () => {
|
||||||
|
details.open = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
details.append(
|
||||||
|
summary,
|
||||||
|
buildHumanReadableVendorCard(vendor),
|
||||||
|
buildGvlSubjectMatterDetails(gvlInfo),
|
||||||
|
buildTechnicalEvidenceDetails(vendor, snapshot, rawEvidence),
|
||||||
|
closeButton
|
||||||
|
);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHumanReadableVendorCard(vendor) {
|
||||||
|
const rawVendor = vendor.rawVendor ?? {};
|
||||||
|
const urlRows = getVendorUrlRows(rawVendor, vendor);
|
||||||
|
|
||||||
|
return buildKeyValueSection("Vendor-Karte", [
|
||||||
|
["Vendor-Name", formatNullable(vendor.name ?? rawVendor.name)],
|
||||||
|
["Vendor-ID", formatNullable(vendor.vendorId ?? rawVendor.id)],
|
||||||
|
["Status", formatVendorStatus(vendor.deletedDate ?? rawVendor.deletedDate)],
|
||||||
|
["Privacy-/Datenschutz-URLs", formatMultilineValue(urlRows.privacy)],
|
||||||
|
[
|
||||||
|
"Legitimate-Interest-Claim-URLs",
|
||||||
|
formatMultilineValue(urlRows.legitimateInterest)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Device-Storage-Disclosure-URL",
|
||||||
|
formatNullable(
|
||||||
|
vendor.deviceStorageDisclosureUrl ??
|
||||||
|
rawVendor.deviceStorageDisclosureUrl
|
||||||
|
)
|
||||||
|
],
|
||||||
|
["Nutzt Cookies", formatBooleanGerman(vendor.usesCookies)],
|
||||||
|
[
|
||||||
|
"Cookie Refresh",
|
||||||
|
formatBooleanGerman(rawVendor.cookieRefresh ?? vendor.cookieRefresh)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Cookie Max Age",
|
||||||
|
formatCookieMaxAge(vendor.cookieMaxAgeSeconds)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Non-Cookie Access",
|
||||||
|
formatBooleanGerman(vendor.usesNonCookieAccess)
|
||||||
|
],
|
||||||
|
["Domains", formatArrayValue(vendor.domains ?? rawVendor.domains)]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVendorUrlRows(rawVendor, vendor) {
|
||||||
|
const urls = Array.isArray(rawVendor.urls) ? rawVendor.urls : [];
|
||||||
|
const privacy = [];
|
||||||
|
const legitimateInterest = [];
|
||||||
|
|
||||||
|
urls.forEach((urlEntry) => {
|
||||||
|
if (!urlEntry || typeof urlEntry !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = urlEntry.langId ? `${urlEntry.langId}: ` : "";
|
||||||
|
|
||||||
|
appendUrlIfPresent(privacy, `${label}${urlEntry.privacy}`);
|
||||||
|
appendUrlIfPresent(
|
||||||
|
legitimateInterest,
|
||||||
|
`${label}${urlEntry.legIntClaim}`
|
||||||
|
);
|
||||||
|
appendUrlIfPresent(
|
||||||
|
legitimateInterest,
|
||||||
|
`${label}${urlEntry.legitimateInterestClaim}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
appendUrlIfPresent(privacy, vendor.policyUrl ?? rawVendor.policyUrl);
|
||||||
|
appendUrlIfPresent(
|
||||||
|
legitimateInterest,
|
||||||
|
vendor.legitimateInterestDisclosureUrl ??
|
||||||
|
rawVendor.legitimateInterestDisclosureUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
privacy: uniqueValues(privacy),
|
||||||
|
legitimateInterest: uniqueValues(legitimateInterest)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendUrlIfPresent(values, value) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlValue = String(value);
|
||||||
|
|
||||||
|
if (urlValue.endsWith("undefined") || urlValue.endsWith("null")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(urlValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueValues(values) {
|
||||||
|
return Array.from(new Set(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlSubjectMatterDetails(gvlInfo) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
|
||||||
|
details.className = "subject-details";
|
||||||
|
details.open = true;
|
||||||
|
summary.textContent = "GVL-Fachinformationen";
|
||||||
|
details.append(
|
||||||
|
summary,
|
||||||
|
buildCatalogListSection("Purposes", "Purpose", gvlInfo.purposes),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Legitimate-Interest-Purposes",
|
||||||
|
"Purpose",
|
||||||
|
gvlInfo.legIntPurposes
|
||||||
|
),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Flexible Purposes",
|
||||||
|
"Purpose",
|
||||||
|
gvlInfo.flexiblePurposes
|
||||||
|
),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Special Purposes",
|
||||||
|
"Special Purpose",
|
||||||
|
gvlInfo.specialPurposes
|
||||||
|
),
|
||||||
|
buildCatalogListSection("Features", "Feature", gvlInfo.features),
|
||||||
|
buildCatalogListSection(
|
||||||
|
"Special Features",
|
||||||
|
"Special Feature",
|
||||||
|
gvlInfo.specialFeatures
|
||||||
|
),
|
||||||
|
buildDataDeclarationSection(gvlInfo),
|
||||||
|
buildJsonDetails("Data Retention", gvlInfo.dataRetention),
|
||||||
|
buildJsonDetails("Overflow", gvlInfo.overflow)
|
||||||
|
);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCatalogListSection(title, entityLabel, items) {
|
||||||
|
const section = document.createElement("section");
|
||||||
|
const heading = document.createElement("h4");
|
||||||
|
const list = document.createElement("div");
|
||||||
|
const catalogItems = Array.isArray(items) ? items : [];
|
||||||
|
|
||||||
|
section.className = "gvl-catalog-section";
|
||||||
|
heading.textContent = title;
|
||||||
|
list.className = "catalog-list";
|
||||||
|
section.append(heading);
|
||||||
|
|
||||||
|
if (!catalogItems.length) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
|
||||||
|
empty.className = "muted-text";
|
||||||
|
empty.textContent = "Keine Angaben vorhanden.";
|
||||||
|
section.append(empty);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogItems.forEach((item) => {
|
||||||
|
list.append(buildCatalogItemDetails(entityLabel, item));
|
||||||
|
});
|
||||||
|
section.append(list);
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCatalogItemDetails(entityLabel, item) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
const rows = [
|
||||||
|
["ID", formatNullable(item?.id)],
|
||||||
|
["Name", formatNullable(item?.name)],
|
||||||
|
["Description", formatNullable(item?.description)],
|
||||||
|
["Description Legal", formatNullable(item?.descriptionLegal)],
|
||||||
|
["Illustrations", formatJsonValue(item?.illustrations)]
|
||||||
|
];
|
||||||
|
|
||||||
|
details.className = "catalog-item";
|
||||||
|
summary.textContent = formatCatalogItemSummary(entityLabel, item);
|
||||||
|
details.append(summary, buildDefinitionList(rows));
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCatalogItemSummary(entityLabel, item) {
|
||||||
|
const prefix = `${entityLabel} ${formatNullable(item?.id)}`;
|
||||||
|
|
||||||
|
if (!item?.name) {
|
||||||
|
return `${prefix} - Klartext lokal nicht verfügbar`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix} - ${item.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDataDeclarationSection(gvlInfo) {
|
||||||
|
const section = buildCatalogListSection(
|
||||||
|
"Data Declaration",
|
||||||
|
"Data Category",
|
||||||
|
gvlInfo.dataDeclaration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gvlInfo.dataCategoriesTextAvailable) {
|
||||||
|
const note = document.createElement("p");
|
||||||
|
|
||||||
|
note.className = "muted-text";
|
||||||
|
note.textContent = "Klartext derzeit nicht normalisiert verfügbar.";
|
||||||
|
section.prepend(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTechnicalEvidenceDetails(vendor, snapshot, rawEvidence) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
|
||||||
|
details.className = "technical-details";
|
||||||
|
summary.textContent = "Technischer Evidence-Nachweis";
|
||||||
|
details.append(
|
||||||
|
summary,
|
||||||
|
buildKeyValueSection("Snapshot und Raw-GVL", [
|
||||||
["Snapshot SHA256", formatNullable(snapshot.snapshotSha256)],
|
["Snapshot SHA256", formatNullable(snapshot.snapshotSha256)],
|
||||||
["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)],
|
["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)],
|
||||||
["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)],
|
["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)],
|
||||||
["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)],
|
["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)],
|
||||||
["Abrufzeitpunkt", formatNullable(snapshot.fetchedAt)],
|
["Fetched At", formatNullable(snapshot.fetchedAt)],
|
||||||
["Snapshot erstellt", formatNullable(snapshot.createdAt)]
|
["Snapshot createdAt", formatNullable(snapshot.createdAt)]
|
||||||
]),
|
]),
|
||||||
buildKeyValueSection("Raw-GVL-Evidence", [
|
buildKeyValueSection("Raw-GVL-Evidence", [
|
||||||
["Raw-GVL SHA256", formatNullable(rawEvidence.rawGvlSha256)],
|
["Source URL", formatNullable(rawEvidence.sourceUrl)],
|
||||||
["Quelle", formatNullable(rawEvidence.sourceUrl)],
|
["Fetched At", formatNullable(rawEvidence.fetchedAt)],
|
||||||
["Abrufzeitpunkt", formatNullable(rawEvidence.fetchedAt)],
|
|
||||||
["HTTP Status", formatNullable(rawEvidence.httpStatus)],
|
["HTTP Status", formatNullable(rawEvidence.httpStatus)],
|
||||||
["Content-Type", formatNullable(rawEvidence.contentType)],
|
["Content-Type", formatNullable(rawEvidence.contentType)],
|
||||||
["Raw Body vorhanden", formatNullable(rawEvidence.hasRawBody)]
|
["hasRawBody", formatNullable(rawEvidence.hasRawBody)]
|
||||||
]),
|
]),
|
||||||
buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor),
|
buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor),
|
||||||
buildJsonDetails("Vollständiger gvl_vendors-Datensatz", vendor)
|
buildJsonDetails("Vollständiger gvl_vendors-Datensatz", vendor)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefinitionList(rows) {
|
||||||
|
const list = document.createElement("dl");
|
||||||
|
|
||||||
|
list.className = "definition-list";
|
||||||
|
|
||||||
|
rows.forEach(([label, value]) => {
|
||||||
|
const term = document.createElement("dt");
|
||||||
|
const description = document.createElement("dd");
|
||||||
|
|
||||||
|
term.textContent = label;
|
||||||
|
description.textContent = value;
|
||||||
|
list.append(term, description);
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildKeyValueSection(title, rows) {
|
function buildKeyValueSection(title, rows) {
|
||||||
@@ -293,6 +779,79 @@ function buildJsonDetails(title, value) {
|
|||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatVendorStatus(deletedDate) {
|
||||||
|
if (deletedDate) {
|
||||||
|
return `Gelöscht seit ${deletedDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Aktiv";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBooleanGerman(value) {
|
||||||
|
if (value === true) {
|
||||||
|
return "ja";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === false) {
|
||||||
|
return "nein";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCookieMaxAge(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isFinite(seconds)) {
|
||||||
|
return formatNullable(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanReadable = formatDurationFromSeconds(seconds);
|
||||||
|
|
||||||
|
return humanReadable
|
||||||
|
? `${seconds} Sekunden (${humanReadable})`
|
||||||
|
: `${seconds} Sekunden`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDurationFromSeconds(seconds) {
|
||||||
|
const units = [
|
||||||
|
["Jahr", "Jahre", 365 * 24 * 60 * 60],
|
||||||
|
["Tag", "Tage", 24 * 60 * 60],
|
||||||
|
["Stunde", "Stunden", 60 * 60],
|
||||||
|
["Minute", "Minuten", 60]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [singular, plural, unitSeconds] of units) {
|
||||||
|
if (seconds >= unitSeconds && seconds % unitSeconds === 0) {
|
||||||
|
const amount = seconds / unitSeconds;
|
||||||
|
|
||||||
|
return `${amount} ${amount === 1 ? singular : plural}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatArrayValue(value) {
|
||||||
|
if (!Array.isArray(value) || !value.length) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMultilineValue(values) {
|
||||||
|
if (!Array.isArray(values) || !values.length) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async function renderGvlSnapshots() {
|
async function renderGvlSnapshots() {
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -661,3 +1220,18 @@ function shortenSha256(value) {
|
|||||||
|
|
||||||
return `${String(value).slice(0, 12)}...`;
|
return `${String(value).slice(0, 12)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExportTimestampUtcCompact(date) {
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
const month = padDatePart(date.getUTCMonth() + 1);
|
||||||
|
const day = padDatePart(date.getUTCDate());
|
||||||
|
const hours = padDatePart(date.getUTCHours());
|
||||||
|
const minutes = padDatePart(date.getUTCMinutes());
|
||||||
|
const seconds = padDatePart(date.getUTCSeconds());
|
||||||
|
|
||||||
|
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function padDatePart(value) {
|
||||||
|
return String(value).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren