Improve GVL explorer vendor evidence workflow
Dieser Commit ist enthalten in:
+340
-12
@@ -73,6 +73,18 @@ async function handleVendorGetMessage(message, sender) {
|
|||||||
return handleGetGvlSnapshotSummaryMessage(message);
|
return handleGetGvlSnapshotSummaryMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "rebuild_gvl_snapshot_normalized_data") {
|
||||||
|
return handleRebuildGvlSnapshotNormalizedDataMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "list_gvl_vendors_for_snapshot") {
|
||||||
|
return handleListGvlVendorsForSnapshotMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "get_gvl_vendor_detail") {
|
||||||
|
return handleGetGvlVendorDetailMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === "get_latest_consent_state") {
|
if (message.type === "get_latest_consent_state") {
|
||||||
return handleGetLatestConsentStateMessage();
|
return handleGetLatestConsentStateMessage();
|
||||||
}
|
}
|
||||||
@@ -280,12 +292,15 @@ async function handleGetGvlSnapshotSummaryMessage(message) {
|
|||||||
eventType: event?.eventType ?? null,
|
eventType: event?.eventType ?? null,
|
||||||
eventCapturedAt: event?.capturedAt ?? null,
|
eventCapturedAt: event?.capturedAt ?? null,
|
||||||
vendorCount: snapshot.vendorCount ?? counts.vendorCount,
|
vendorCount: snapshot.vendorCount ?? counts.vendorCount,
|
||||||
|
snapshotVendorCount: snapshot.vendorCount ?? null,
|
||||||
|
normalizedVendorCount: counts.vendorCount,
|
||||||
purposeCount: snapshot.purposeCount ?? counts.purposeCount,
|
purposeCount: snapshot.purposeCount ?? counts.purposeCount,
|
||||||
specialPurposeCount: counts.specialPurposeCount,
|
specialPurposeCount: counts.specialPurposeCount,
|
||||||
featureCount: counts.featureCount,
|
featureCount: counts.featureCount,
|
||||||
specialFeatureCount: counts.specialFeatureCount,
|
specialFeatureCount: counts.specialFeatureCount,
|
||||||
dataCategoryCount: counts.dataCategoryCount,
|
dataCategoryCount: counts.dataCategoryCount,
|
||||||
vendorRelationshipCount: counts.vendorRelationshipCount,
|
vendorRelationshipCount: counts.vendorRelationshipCount,
|
||||||
|
normalizedVendorRelationshipCount: counts.vendorRelationshipCount,
|
||||||
technicalFields: {
|
technicalFields: {
|
||||||
snapshotStore: "gvl_snapshots",
|
snapshotStore: "gvl_snapshots",
|
||||||
vendorListVersion: "vendorListVersion",
|
vendorListVersion: "vendorListVersion",
|
||||||
@@ -300,6 +315,267 @@ async function handleGetGvlSnapshotSummaryMessage(message) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRebuildGvlSnapshotNormalizedDataMessage(message) {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const payload = message?.payload ?? {};
|
||||||
|
const snapshot = await getGvlSnapshotByIdentifier(db, {
|
||||||
|
sha256: payload.sha256 ?? null,
|
||||||
|
vendorListVersion: payload.vendorListVersion ?? null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "gvl_snapshot_not_found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
const counts = await countGvlNormalizedRecordsForVersion(
|
||||||
|
db,
|
||||||
|
snapshot.vendorListVersion ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
snapshotSha256: snapshot.sha256 ?? null,
|
||||||
|
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||||
|
normalizationSummary,
|
||||||
|
counts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleListGvlVendorsForSnapshotMessage(message) {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const payload = message?.payload ?? {};
|
||||||
|
const snapshot = await getGvlSnapshotByIdentifier(db, {
|
||||||
|
sha256: payload.sha256 ?? null,
|
||||||
|
vendorListVersion: payload.vendorListVersion ?? null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "gvl_snapshot_not_found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const vendors = await listGvlVendorsForSnapshot(db, snapshot);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
snapshotSha256: snapshot.sha256 ?? null,
|
||||||
|
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||||
|
vendors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function listGvlVendorsForSnapshot(db, snapshot) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
|
||||||
|
const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
|
||||||
|
const vendorListVersionIndex = vendorsStore.index("vendorListVersion");
|
||||||
|
const cursorRequest = vendorListVersionIndex.openCursor(
|
||||||
|
IDBKeyRange.only(snapshot.vendorListVersion)
|
||||||
|
);
|
||||||
|
const vendors = [];
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor.value?.snapshotSha256 === snapshot.sha256) {
|
||||||
|
vendors.push({
|
||||||
|
vendorId: cursor.value.vendorId ?? null,
|
||||||
|
name: cursor.value.name ?? null,
|
||||||
|
deletedDate: cursor.value.deletedDate ?? null,
|
||||||
|
snapshotSha256: cursor.value.snapshotSha256 ?? null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => {
|
||||||
|
resolve(
|
||||||
|
vendors.sort((left, right) => {
|
||||||
|
return (
|
||||||
|
toComparableNumber(left.vendorId) -
|
||||||
|
toComparableNumber(right.vendorId)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetGvlVendorDetailMessage(message) {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const vendorId = parseGvlVendorDetailId(message?.payload?.vendorId);
|
||||||
|
|
||||||
|
if (vendorId === null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "invalid_vendor_id"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const vendorRecord = await getLatestGvlVendorByVendorId(db, vendorId);
|
||||||
|
|
||||||
|
if (!vendorRecord) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "gvl_vendor_not_found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshotSha256 = vendorRecord.snapshotSha256 ?? null;
|
||||||
|
const snapshot = snapshotSha256
|
||||||
|
? await getGvlSnapshotBySha256(db, snapshotSha256)
|
||||||
|
: null;
|
||||||
|
const rawGvlSha256 = snapshot?.rawGvlSha256 ?? null;
|
||||||
|
const rawEvidence = rawGvlSha256
|
||||||
|
? await getGvlRawEvidenceBySha256(db, rawGvlSha256)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
vendorDetail: {
|
||||||
|
vendor: vendorRecord,
|
||||||
|
snapshot: buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256),
|
||||||
|
rawEvidence: buildGvlVendorDetailRawEvidenceSummary(
|
||||||
|
rawEvidence,
|
||||||
|
rawGvlSha256
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGvlVendorDetailId(value) {
|
||||||
|
const vendorId = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isInteger(vendorId) || vendorId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestGvlVendorByVendorId(db, vendorId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
|
||||||
|
const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
|
||||||
|
const vendorIdIndex = vendorsStore.index("vendorId");
|
||||||
|
const cursorRequest = vendorIdIndex.openCursor(IDBKeyRange.only(vendorId));
|
||||||
|
const vendors = [];
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vendors.push(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => {
|
||||||
|
resolve(sortGvlVendorRecordsNewestFirst(vendors)[0] ?? null);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortGvlVendorRecordsNewestFirst(vendorRecords) {
|
||||||
|
return vendorRecords.slice().sort((left, right) => {
|
||||||
|
const fetchedAtComparison =
|
||||||
|
toComparableTime(right.snapshotFetchedAt) -
|
||||||
|
toComparableTime(left.snapshotFetchedAt);
|
||||||
|
|
||||||
|
if (fetchedAtComparison !== 0) {
|
||||||
|
return fetchedAtComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
toComparableNumber(right.vendorListVersion) -
|
||||||
|
toComparableNumber(left.vendorListVersion)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlRawEvidenceBySha256(db, rawGvlSha256) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlRawEvidence], "readonly");
|
||||||
|
const rawEvidenceStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlRawEvidence);
|
||||||
|
const getRequest = rawEvidenceStore.get(rawGvlSha256);
|
||||||
|
let rawEvidenceOrNull = null;
|
||||||
|
|
||||||
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
rawEvidenceOrNull = getRequest.result ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(rawEvidenceOrNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256) {
|
||||||
|
if (!snapshot) {
|
||||||
|
return {
|
||||||
|
snapshotSha256: snapshotSha256 ?? null,
|
||||||
|
rawGvlSha256: null,
|
||||||
|
vendorListVersion: null,
|
||||||
|
tcfPolicyVersion: null,
|
||||||
|
fetchedAt: null,
|
||||||
|
createdAt: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
snapshotSha256: snapshot.sha256 ?? snapshotSha256 ?? null,
|
||||||
|
rawGvlSha256: snapshot.rawGvlSha256 ?? null,
|
||||||
|
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||||
|
tcfPolicyVersion: snapshot.tcfPolicyVersion ?? null,
|
||||||
|
fetchedAt: snapshot.fetchedAt ?? null,
|
||||||
|
createdAt: snapshot.createdAt ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGvlVendorDetailRawEvidenceSummary(rawEvidence, rawGvlSha256) {
|
||||||
|
if (!rawEvidence) {
|
||||||
|
return {
|
||||||
|
rawGvlSha256: rawGvlSha256 ?? null,
|
||||||
|
sourceUrl: null,
|
||||||
|
fetchedAt: null,
|
||||||
|
httpStatus: null,
|
||||||
|
contentType: null,
|
||||||
|
hasRawBody: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawGvlSha256: rawEvidence.rawGvlSha256 ?? rawGvlSha256 ?? null,
|
||||||
|
sourceUrl: rawEvidence.sourceUrl ?? null,
|
||||||
|
fetchedAt: rawEvidence.fetchedAt ?? null,
|
||||||
|
httpStatus: rawEvidence.httpStatus ?? null,
|
||||||
|
contentType: rawEvidence.contentType ?? null,
|
||||||
|
hasRawBody: typeof rawEvidence.rawBody === "string"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function handleGetLatestConsentStateMessage() {
|
async function handleGetLatestConsentStateMessage() {
|
||||||
const db = await openVendorGetDb();
|
const db = await openVendorGetDb();
|
||||||
const latestStateOrNull = await getLatestConsentState(db);
|
const latestStateOrNull = await getLatestConsentState(db);
|
||||||
@@ -631,21 +907,55 @@ async function handleFetchOfficialGvlMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = await openVendorGetDb();
|
const db = await openVendorGetDb();
|
||||||
const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
|
const currentVendorListVersion = rawJson.vendorListVersion ?? null;
|
||||||
sourceUrl: OFFICIAL_IAB_GVL_URL,
|
const existingSnapshot = await getGvlSnapshotByVendorListVersion(
|
||||||
fetchedAt: new Date().toISOString(),
|
db,
|
||||||
rawGvlSha256: rawGvlSha256,
|
currentVendorListVersion
|
||||||
diagnostics: {
|
);
|
||||||
ingestionSource: "official_iab_fetch",
|
const ingestResult = existingSnapshot
|
||||||
responseStatus: responseStatus
|
? null
|
||||||
}
|
: await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
|
||||||
});
|
sourceUrl: OFFICIAL_IAB_GVL_URL,
|
||||||
|
fetchedAt: new Date().toISOString(),
|
||||||
|
rawGvlSha256: rawGvlSha256,
|
||||||
|
diagnostics: {
|
||||||
|
ingestionSource: "official_iab_fetch",
|
||||||
|
responseStatus: responseStatus
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const snapshot = existingSnapshot ?? ingestResult.snapshot;
|
||||||
|
const completeness = await getGvlSnapshotNormalizedCompleteness(
|
||||||
|
db,
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
let normalizationSummary = null;
|
||||||
|
let syncStatus = "current_and_locally_available";
|
||||||
|
|
||||||
|
if (!existingSnapshot) {
|
||||||
|
normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
|
||||||
|
snapshot
|
||||||
|
);
|
||||||
|
syncStatus = "new_gvl_revision_stored_and_normalized";
|
||||||
|
} else if (!completeness.complete) {
|
||||||
|
normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
|
||||||
|
existingSnapshot
|
||||||
|
);
|
||||||
|
syncStatus = "known_gvl_rebuilt_from_local_evidence";
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = await countGvlNormalizedRecordsForVersion(
|
||||||
|
db,
|
||||||
|
snapshot.vendorListVersion ?? null
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
alreadyKnown: result.alreadyKnown,
|
alreadyKnown: Boolean(existingSnapshot),
|
||||||
vendorListVersion: result.snapshot.vendorListVersion,
|
syncStatus,
|
||||||
sha256: result.snapshot.sha256
|
vendorListVersion: snapshot.vendorListVersion,
|
||||||
|
sha256: snapshot.sha256,
|
||||||
|
normalizationSummary,
|
||||||
|
counts
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("VG-Observe official GVL fetch failed", error);
|
console.warn("VG-Observe official GVL fetch failed", error);
|
||||||
@@ -657,6 +967,24 @@ async function handleFetchOfficialGvlMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getGvlSnapshotNormalizedCompleteness(db, snapshot) {
|
||||||
|
const counts = await countGvlNormalizedRecordsForVersion(
|
||||||
|
db,
|
||||||
|
snapshot?.vendorListVersion ?? null
|
||||||
|
);
|
||||||
|
const expectedVendorCount = Number(snapshot?.vendorCount ?? 0);
|
||||||
|
const hasVendors =
|
||||||
|
expectedVendorCount > 0
|
||||||
|
? counts.vendorCount >= expectedVendorCount
|
||||||
|
: counts.vendorCount > 0;
|
||||||
|
const hasVendorRelationships = counts.vendorRelationshipCount > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
complete: hasVendors && hasVendorRelationships,
|
||||||
|
counts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchOfficialGvlJson() {
|
async function fetchOfficialGvlJson() {
|
||||||
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
|
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -76,12 +76,57 @@ p {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rebuild-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.fetch-status {
|
.fetch-status {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vendor-detail-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(120px, 180px) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendor-overview {
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendor-overview summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendor-detail-form label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendor-detail-form input {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #475569;
|
||||||
|
border-radius: 4px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e5edf5;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid #475569;
|
border: 1px solid #475569;
|
||||||
@@ -160,6 +205,17 @@ th {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vendor-detail-result {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vendor-detail-section h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
@@ -198,4 +254,8 @@ th {
|
|||||||
.summary-table th {
|
.summary-table th {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vendor-detail-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<h2 id="snapshot-list-title">Gespeicherte Vendorlisten</h2>
|
<h2 id="snapshot-list-title">Gespeicherte Vendorlisten</h2>
|
||||||
<div class="fetch-actions">
|
<div class="fetch-actions">
|
||||||
<button id="gvl-fetch-official-button" type="button">
|
<button id="gvl-fetch-official-button" type="button">
|
||||||
Offizielle Vendorliste jetzt abrufen
|
GVL-Referenzbasis synchronisieren
|
||||||
</button>
|
</button>
|
||||||
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
<span id="gvl-fetch-status" class="fetch-status" aria-live="polite">
|
||||||
Bereit
|
Bereit
|
||||||
@@ -48,9 +48,40 @@
|
|||||||
|
|
||||||
<section class="snapshot-summary" aria-labelledby="snapshot-summary-title">
|
<section class="snapshot-summary" aria-labelledby="snapshot-summary-title">
|
||||||
<h2 id="snapshot-summary-title">Ausgewählte Vendorliste</h2>
|
<h2 id="snapshot-summary-title">Ausgewählte Vendorliste</h2>
|
||||||
|
<div class="rebuild-actions">
|
||||||
|
<button id="gvl-rebuild-normalized-button" type="button" disabled>
|
||||||
|
Lokale GVL-Daten neu aufbauen (Reparatur)
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
id="gvl-rebuild-normalized-status"
|
||||||
|
class="fetch-status"
|
||||||
|
aria-live="polite"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
<div id="gvl-snapshot-summary"></div>
|
<div id="gvl-snapshot-summary"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<details id="gvl-vendor-overview-details" class="vendor-overview">
|
||||||
|
<summary id="gvl-vendor-overview-summary">
|
||||||
|
Vendoren-Übersicht anzeigen
|
||||||
|
</summary>
|
||||||
|
<p id="gvl-vendor-overview-empty" class="empty-state" hidden>
|
||||||
|
Keine normalisierten Vendoren für diese Vendorliste vorhanden.
|
||||||
|
</p>
|
||||||
|
<div id="gvl-vendor-overview-content" class="snapshot-list-wrap" hidden>
|
||||||
|
<table class="snapshot-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Vendor-ID</th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Deleted Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="gvl-vendor-overview-list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details class="technical-details">
|
<details class="technical-details">
|
||||||
<summary>Technische Feldnamen</summary>
|
<summary>Technische Feldnamen</summary>
|
||||||
<pre id="gvl-technical-fields"></pre>
|
<pre id="gvl-technical-fields"></pre>
|
||||||
@@ -61,6 +92,30 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" aria-labelledby="vendor-detail-title">
|
||||||
|
<h2 id="vendor-detail-title">Lokaler Vendor-Nachweis</h2>
|
||||||
|
<form id="gvl-vendor-detail-form" class="vendor-detail-form">
|
||||||
|
<label for="gvl-vendor-id-input">Vendor-ID</label>
|
||||||
|
<input
|
||||||
|
id="gvl-vendor-id-input"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
inputmode="numeric"
|
||||||
|
placeholder="977"
|
||||||
|
>
|
||||||
|
<button id="gvl-vendor-detail-button" type="submit">
|
||||||
|
Vendor anzeigen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
id="gvl-vendor-detail-status"
|
||||||
|
class="fetch-status"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
<div id="gvl-vendor-detail-result" class="vendor-detail-result"></div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="gvl-explorer.js"></script>
|
<script src="gvl-explorer.js"></script>
|
||||||
|
|||||||
@@ -10,21 +10,64 @@ const gvlFetchOfficialButton = document.getElementById(
|
|||||||
"gvl-fetch-official-button"
|
"gvl-fetch-official-button"
|
||||||
);
|
);
|
||||||
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
const gvlFetchStatus = document.getElementById("gvl-fetch-status");
|
||||||
|
const gvlRebuildNormalizedButton = document.getElementById(
|
||||||
|
"gvl-rebuild-normalized-button"
|
||||||
|
);
|
||||||
|
const gvlRebuildNormalizedStatus = document.getElementById(
|
||||||
|
"gvl-rebuild-normalized-status"
|
||||||
|
);
|
||||||
|
const gvlVendorOverviewEmpty = document.getElementById(
|
||||||
|
"gvl-vendor-overview-empty"
|
||||||
|
);
|
||||||
|
const gvlVendorOverviewDetails = document.getElementById(
|
||||||
|
"gvl-vendor-overview-details"
|
||||||
|
);
|
||||||
|
const gvlVendorOverviewSummary = document.getElementById(
|
||||||
|
"gvl-vendor-overview-summary"
|
||||||
|
);
|
||||||
|
const gvlVendorOverviewContent = document.getElementById(
|
||||||
|
"gvl-vendor-overview-content"
|
||||||
|
);
|
||||||
|
const gvlVendorOverviewList = document.getElementById(
|
||||||
|
"gvl-vendor-overview-list"
|
||||||
|
);
|
||||||
|
const gvlVendorDetailForm = document.getElementById("gvl-vendor-detail-form");
|
||||||
|
const gvlVendorIdInput = document.getElementById("gvl-vendor-id-input");
|
||||||
|
const gvlVendorDetailButton = document.getElementById(
|
||||||
|
"gvl-vendor-detail-button"
|
||||||
|
);
|
||||||
|
const gvlVendorDetailStatus = document.getElementById(
|
||||||
|
"gvl-vendor-detail-status"
|
||||||
|
);
|
||||||
|
const gvlVendorDetailResult = document.getElementById(
|
||||||
|
"gvl-vendor-detail-result"
|
||||||
|
);
|
||||||
|
|
||||||
let gvlSnapshots = [];
|
let gvlSnapshots = [];
|
||||||
let selectedSnapshotSha256 = null;
|
let selectedSnapshotSha256 = null;
|
||||||
|
let selectedSnapshotSummary = null;
|
||||||
|
let selectedSnapshotVendors = [];
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
gvlFetchOfficialButton.addEventListener("click", async () => {
|
gvlFetchOfficialButton.addEventListener("click", async () => {
|
||||||
await fetchOfficialGvl();
|
await fetchOfficialGvl();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gvlRebuildNormalizedButton.addEventListener("click", async () => {
|
||||||
|
await rebuildSelectedGvlSnapshotNormalizedData();
|
||||||
|
});
|
||||||
|
|
||||||
|
gvlVendorDetailForm.addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await renderGvlVendorDetail();
|
||||||
|
});
|
||||||
|
|
||||||
await renderGvlSnapshots();
|
await renderGvlSnapshots();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchOfficialGvl() {
|
async function fetchOfficialGvl() {
|
||||||
gvlFetchOfficialButton.disabled = true;
|
gvlFetchOfficialButton.disabled = true;
|
||||||
renderFetchStatus("Vendorliste wird abgerufen...");
|
renderFetchStatus("GVL-Referenzbasis wird synchronisiert...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -35,26 +78,221 @@ async function fetchOfficialGvl() {
|
|||||||
throw new Error(result?.error ?? "official_gvl_fetch_failed");
|
throw new Error(result?.error ?? "official_gvl_fetch_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFetchStatus(
|
renderFetchStatus(buildGvlSyncStatusMessage(result));
|
||||||
result.alreadyKnown
|
|
||||||
? "Vendorliste bereits bekannt."
|
|
||||||
: "Vendorliste abgerufen."
|
|
||||||
);
|
|
||||||
|
|
||||||
await renderGvlSnapshots();
|
await renderGvlSnapshots();
|
||||||
await renderSelectedGvlSnapshotSummary();
|
await renderSelectedGvlSnapshotSummary();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderFetchStatus("Vendorliste konnte nicht abgerufen werden.");
|
renderFetchStatus("GVL-Referenzbasis konnte nicht synchronisiert werden.");
|
||||||
console.warn("VG-Observe manual official GVL fetch failed", error);
|
console.warn("VG-Observe manual official GVL fetch failed", error);
|
||||||
} finally {
|
} finally {
|
||||||
gvlFetchOfficialButton.disabled = false;
|
gvlFetchOfficialButton.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGvlSyncStatusMessage(result) {
|
||||||
|
if (result?.syncStatus === "new_gvl_revision_stored_and_normalized") {
|
||||||
|
return "Neue GVL-Revision gespeichert und normalisiert.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.syncStatus === "known_gvl_rebuilt_from_local_evidence") {
|
||||||
|
return "Bekannte GVL aus lokaler Evidence neu aufgebaut.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.syncStatus === "current_and_locally_available") {
|
||||||
|
return "Offizielle GVL ist aktuell und lokal vollständig verfügbar.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result?.alreadyKnown
|
||||||
|
? "Offizielle GVL ist lokal verfügbar."
|
||||||
|
: "GVL-Referenzbasis synchronisiert.";
|
||||||
|
}
|
||||||
|
|
||||||
function renderFetchStatus(message) {
|
function renderFetchStatus(message) {
|
||||||
gvlFetchStatus.textContent = message;
|
gvlFetchStatus.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRebuildStatus(message) {
|
||||||
|
gvlRebuildNormalizedStatus.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebuildSelectedGvlSnapshotNormalizedData() {
|
||||||
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
renderRebuildStatus("Keine Vendorliste ausgewählt.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlRebuildNormalizedButton.disabled = true;
|
||||||
|
renderRebuildStatus("Lokale Evidence wird neu normalisiert...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "rebuild_gvl_snapshot_normalized_data",
|
||||||
|
payload: {
|
||||||
|
sha256: snapshot.sha256
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(
|
||||||
|
result?.error ?? "rebuild_gvl_snapshot_normalized_data_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderSelectedGvlSnapshotSummary();
|
||||||
|
renderRebuildStatus(buildRebuildSuccessMessage(result));
|
||||||
|
|
||||||
|
if (gvlVendorIdInput.value) {
|
||||||
|
await renderGvlVendorDetail();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
renderRebuildStatus("Lokaler Neuaufbau fehlgeschlagen.");
|
||||||
|
console.warn("VG-Observe GVL normalized rebuild failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlRebuildNormalizedButton.disabled =
|
||||||
|
!doesSnapshotNeedNormalizedRebuild(selectedSnapshotSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRebuildSuccessMessage(result) {
|
||||||
|
const counts = result.counts ?? {};
|
||||||
|
|
||||||
|
return [
|
||||||
|
"Lokale GVL-Daten neu aufgebaut.",
|
||||||
|
`Vendoren: ${formatCount(counts.vendorCount)}`,
|
||||||
|
`Beziehungen: ${formatCount(counts.vendorRelationshipCount)}`
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderGvlVendorDetail() {
|
||||||
|
const vendorId = Number(gvlVendorIdInput.value);
|
||||||
|
|
||||||
|
clearGvlVendorDetail();
|
||||||
|
|
||||||
|
if (!Number.isInteger(vendorId) || vendorId <= 0) {
|
||||||
|
renderGvlVendorDetailStatus("Bitte eine gültige Vendor-ID eingeben.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlVendorDetailButton.disabled = true;
|
||||||
|
renderGvlVendorDetailStatus("Vendor wird geladen...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "get_gvl_vendor_detail",
|
||||||
|
payload: {
|
||||||
|
vendorId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(result?.error ?? "get_gvl_vendor_detail_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGvlVendorDetailStatus("Vendor geladen.");
|
||||||
|
renderGvlVendorDetailResult(result.vendorDetail ?? {});
|
||||||
|
} catch (error) {
|
||||||
|
renderGvlVendorDetailStatus("Vendor konnte nicht geladen werden.");
|
||||||
|
console.warn("VG-Observe GVL vendor detail failed", error);
|
||||||
|
} finally {
|
||||||
|
gvlVendorDetailButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGvlVendorDetail() {
|
||||||
|
gvlVendorDetailResult.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGvlVendorDetailStatus(message) {
|
||||||
|
gvlVendorDetailStatus.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGvlVendorDetailResult(detail) {
|
||||||
|
const vendor = detail.vendor ?? {};
|
||||||
|
const snapshot = detail.snapshot ?? {};
|
||||||
|
const rawEvidence = detail.rawEvidence ?? {};
|
||||||
|
|
||||||
|
gvlVendorDetailResult.textContent = "";
|
||||||
|
gvlVendorDetailResult.append(
|
||||||
|
buildKeyValueSection("Normalisierte Vendor-Felder", [
|
||||||
|
["Vendor-ID", formatNullable(vendor.vendorId)],
|
||||||
|
["Name", formatNullable(vendor.name)],
|
||||||
|
["Policy URL", formatNullable(vendor.policyUrl)],
|
||||||
|
["Deleted Date", formatNullable(vendor.deletedDate)],
|
||||||
|
["Uses Cookies", formatNullable(vendor.usesCookies)],
|
||||||
|
["Cookie Max Age Seconds", formatNullable(vendor.cookieMaxAgeSeconds)],
|
||||||
|
["Uses Non-Cookie Access", formatNullable(vendor.usesNonCookieAccess)],
|
||||||
|
[
|
||||||
|
"Device Storage Disclosure URL",
|
||||||
|
formatNullable(vendor.deviceStorageDisclosureUrl)
|
||||||
|
],
|
||||||
|
["Domains", formatJsonValue(vendor.domains)],
|
||||||
|
["Snapshot SHA256", formatNullable(vendor.snapshotSha256)]
|
||||||
|
]),
|
||||||
|
buildKeyValueSection("Snapshot-Herkunft", [
|
||||||
|
["Snapshot SHA256", formatNullable(snapshot.snapshotSha256)],
|
||||||
|
["Raw-GVL SHA256", formatNullable(snapshot.rawGvlSha256)],
|
||||||
|
["Vendorlisten-Version", formatNullable(snapshot.vendorListVersion)],
|
||||||
|
["TCF Policy Version", formatNullable(snapshot.tcfPolicyVersion)],
|
||||||
|
["Abrufzeitpunkt", formatNullable(snapshot.fetchedAt)],
|
||||||
|
["Snapshot erstellt", formatNullable(snapshot.createdAt)]
|
||||||
|
]),
|
||||||
|
buildKeyValueSection("Raw-GVL-Evidence", [
|
||||||
|
["Raw-GVL SHA256", formatNullable(rawEvidence.rawGvlSha256)],
|
||||||
|
["Quelle", formatNullable(rawEvidence.sourceUrl)],
|
||||||
|
["Abrufzeitpunkt", formatNullable(rawEvidence.fetchedAt)],
|
||||||
|
["HTTP Status", formatNullable(rawEvidence.httpStatus)],
|
||||||
|
["Content-Type", formatNullable(rawEvidence.contentType)],
|
||||||
|
["Raw Body vorhanden", formatNullable(rawEvidence.hasRawBody)]
|
||||||
|
]),
|
||||||
|
buildJsonDetails("Vollständiger rawVendor-Datensatz", vendor.rawVendor),
|
||||||
|
buildJsonDetails("Vollständiger gvl_vendors-Datensatz", vendor)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKeyValueSection(title, rows) {
|
||||||
|
const section = document.createElement("section");
|
||||||
|
const heading = document.createElement("h3");
|
||||||
|
const table = document.createElement("table");
|
||||||
|
const body = document.createElement("tbody");
|
||||||
|
|
||||||
|
section.className = "vendor-detail-section";
|
||||||
|
heading.textContent = title;
|
||||||
|
table.className = "summary-table";
|
||||||
|
|
||||||
|
for (const [label, value] of rows) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
const labelCell = document.createElement("th");
|
||||||
|
const valueCell = document.createElement("td");
|
||||||
|
|
||||||
|
labelCell.scope = "row";
|
||||||
|
labelCell.textContent = label;
|
||||||
|
valueCell.textContent = value;
|
||||||
|
row.append(labelCell, valueCell);
|
||||||
|
body.append(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.append(body);
|
||||||
|
section.append(heading, table);
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJsonDetails(title, value) {
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
const valuePre = document.createElement("pre");
|
||||||
|
|
||||||
|
details.className = "technical-details";
|
||||||
|
summary.textContent = title;
|
||||||
|
valuePre.textContent = JSON.stringify(value ?? null, null, 2);
|
||||||
|
details.append(summary, valuePre);
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
async function renderGvlSnapshots() {
|
async function renderGvlSnapshots() {
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -93,6 +331,7 @@ async function renderGvlSnapshots() {
|
|||||||
function renderNoGvlSnapshots() {
|
function renderNoGvlSnapshots() {
|
||||||
gvlSnapshotList.textContent = "";
|
gvlSnapshotList.textContent = "";
|
||||||
clearGvlSnapshotSummary();
|
clearGvlSnapshotSummary();
|
||||||
|
clearGvlVendorOverview();
|
||||||
gvlSnapshotEmpty.hidden = false;
|
gvlSnapshotEmpty.hidden = false;
|
||||||
gvlSnapshotContent.hidden = true;
|
gvlSnapshotContent.hidden = true;
|
||||||
gvlSnapshotEmpty.textContent =
|
gvlSnapshotEmpty.textContent =
|
||||||
@@ -145,6 +384,7 @@ function appendListCell(row, value, className) {
|
|||||||
async function selectGvlSnapshot(sha256) {
|
async function selectGvlSnapshot(sha256) {
|
||||||
selectedSnapshotSha256 = sha256;
|
selectedSnapshotSha256 = sha256;
|
||||||
renderGvlSnapshotList();
|
renderGvlSnapshotList();
|
||||||
|
renderRebuildStatus("");
|
||||||
await renderSelectedGvlSnapshotSummary();
|
await renderSelectedGvlSnapshotSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +392,10 @@ async function renderSelectedGvlSnapshotSummary() {
|
|||||||
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
clearGvlSnapshotSummary();
|
clearGvlSnapshotSummary();
|
||||||
|
clearGvlVendorOverview();
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
|
updateRebuildActionState(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,14 +411,149 @@ async function renderSelectedGvlSnapshotSummary() {
|
|||||||
throw new Error(result?.error ?? "get_gvl_snapshot_summary_failed");
|
throw new Error(result?.error ?? "get_gvl_snapshot_summary_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSummaryTable(result.summary ?? {});
|
selectedSnapshotSummary = result.summary ?? {};
|
||||||
|
renderSummaryTable(selectedSnapshotSummary);
|
||||||
|
updateRebuildActionState(selectedSnapshotSummary);
|
||||||
|
await renderVendorOverviewForSelectedSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
selectedSnapshotSummary = null;
|
||||||
|
updateRebuildActionState(null);
|
||||||
gvlSnapshotSummary.textContent =
|
gvlSnapshotSummary.textContent =
|
||||||
"Zusammenfassung dieser Vendorliste konnte nicht geladen werden.";
|
"Zusammenfassung dieser Vendorliste konnte nicht geladen werden.";
|
||||||
console.warn("VG-Observe GVL snapshot summary failed", error);
|
console.warn("VG-Observe GVL snapshot summary failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderVendorOverviewForSelectedSnapshot() {
|
||||||
|
const snapshot = findGvlSnapshot(selectedSnapshotSha256);
|
||||||
|
|
||||||
|
clearGvlVendorOverview();
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await browser.runtime.sendMessage({
|
||||||
|
type: "list_gvl_vendors_for_snapshot",
|
||||||
|
payload: {
|
||||||
|
sha256: snapshot.sha256
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(result?.error ?? "list_gvl_vendors_for_snapshot_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedSnapshotVendors = result.vendors ?? [];
|
||||||
|
updateVendorOverviewSummary();
|
||||||
|
renderVendorOverview();
|
||||||
|
} catch (error) {
|
||||||
|
gvlVendorOverviewEmpty.hidden = false;
|
||||||
|
gvlVendorOverviewEmpty.textContent =
|
||||||
|
"Vendoren-Übersicht konnte nicht geladen werden.";
|
||||||
|
console.warn("VG-Observe GVL vendor overview failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVendorOverview() {
|
||||||
|
gvlVendorOverviewList.textContent = "";
|
||||||
|
|
||||||
|
if (!selectedSnapshotVendors.length) {
|
||||||
|
gvlVendorOverviewEmpty.hidden = false;
|
||||||
|
gvlVendorOverviewContent.hidden = true;
|
||||||
|
gvlVendorOverviewEmpty.textContent =
|
||||||
|
"Keine normalisierten Vendoren für diese Vendorliste vorhanden.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlVendorOverviewEmpty.hidden = true;
|
||||||
|
gvlVendorOverviewContent.hidden = false;
|
||||||
|
|
||||||
|
for (const vendor of selectedSnapshotVendors) {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
|
||||||
|
row.tabIndex = 0;
|
||||||
|
row.setAttribute("role", "button");
|
||||||
|
row.addEventListener("click", async () => {
|
||||||
|
await selectVendorFromOverview(vendor.vendorId);
|
||||||
|
});
|
||||||
|
row.addEventListener("keydown", async (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
await selectVendorFromOverview(vendor.vendorId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
appendListCell(row, formatNullable(vendor.vendorId), "numeric");
|
||||||
|
appendListCell(row, formatNullable(vendor.name));
|
||||||
|
appendListCell(row, formatNullable(vendor.deletedDate));
|
||||||
|
gvlVendorOverviewList.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectVendorFromOverview(vendorId) {
|
||||||
|
if (vendorId === null || vendorId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gvlVendorIdInput.value = String(vendorId);
|
||||||
|
await renderGvlVendorDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGvlVendorOverview() {
|
||||||
|
gvlVendorOverviewList.textContent = "";
|
||||||
|
selectedSnapshotVendors = [];
|
||||||
|
gvlVendorOverviewEmpty.hidden = true;
|
||||||
|
gvlVendorOverviewContent.hidden = true;
|
||||||
|
gvlVendorOverviewDetails.open = false;
|
||||||
|
updateVendorOverviewSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVendorOverviewSummary() {
|
||||||
|
const count = selectedSnapshotVendors.length;
|
||||||
|
|
||||||
|
gvlVendorOverviewSummary.textContent = count
|
||||||
|
? `Vendoren-Übersicht anzeigen (${count})`
|
||||||
|
: "Vendoren-Übersicht anzeigen";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRebuildActionState(summary) {
|
||||||
|
const needsRebuild = doesSnapshotNeedNormalizedRebuild(summary);
|
||||||
|
|
||||||
|
gvlRebuildNormalizedButton.disabled = !needsRebuild;
|
||||||
|
|
||||||
|
if (!summary) {
|
||||||
|
renderRebuildStatus("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRebuild) {
|
||||||
|
renderRebuildStatus("Reparatur möglich: normalisierte lokale Daten fehlen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRebuildStatus("Normalisierte lokale GVL-Daten sind verfügbar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function doesSnapshotNeedNormalizedRebuild(summary) {
|
||||||
|
if (!summary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedVendorCount = Number(summary.snapshotVendorCount ?? 0);
|
||||||
|
const normalizedVendorCount = Number(summary.normalizedVendorCount ?? 0);
|
||||||
|
const normalizedRelationshipCount = Number(
|
||||||
|
summary.normalizedVendorRelationshipCount ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (expectedVendorCount > 0 && normalizedVendorCount < expectedVendorCount) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedVendorCount === 0 || normalizedRelationshipCount === 0;
|
||||||
|
}
|
||||||
|
|
||||||
function renderSummaryTable(summary) {
|
function renderSummaryTable(summary) {
|
||||||
const table = document.createElement("table");
|
const table = document.createElement("table");
|
||||||
const body = document.createElement("tbody");
|
const body = document.createElement("tbody");
|
||||||
@@ -185,6 +562,10 @@ function renderSummaryTable(summary) {
|
|||||||
["Abrufzeitpunkt", formatNullable(summary.fetchedAt)],
|
["Abrufzeitpunkt", formatNullable(summary.fetchedAt)],
|
||||||
["Quelle", formatNullable(summary.sourceUrl)],
|
["Quelle", formatNullable(summary.sourceUrl)],
|
||||||
["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)],
|
["Anzahl Firmen/Vendoren", formatCount(summary.vendorCount)],
|
||||||
|
[
|
||||||
|
"Normalisierte Vendoren",
|
||||||
|
formatCount(summary.normalizedVendorCount)
|
||||||
|
],
|
||||||
["Anzahl Zwecke/Purposes", formatCount(summary.purposeCount)],
|
["Anzahl Zwecke/Purposes", formatCount(summary.purposeCount)],
|
||||||
["Anzahl Special Purposes", formatCount(summary.specialPurposeCount)],
|
["Anzahl Special Purposes", formatCount(summary.specialPurposeCount)],
|
||||||
["Anzahl Features", formatCount(summary.featureCount)],
|
["Anzahl Features", formatCount(summary.featureCount)],
|
||||||
@@ -234,6 +615,7 @@ function clearGvlSnapshotSummary() {
|
|||||||
gvlSnapshotSummary.textContent = "";
|
gvlSnapshotSummary.textContent = "";
|
||||||
gvlTechnicalFields.textContent = "";
|
gvlTechnicalFields.textContent = "";
|
||||||
gvlDebugData.textContent = "";
|
gvlDebugData.textContent = "";
|
||||||
|
selectedSnapshotSummary = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findGvlSnapshot(sha256) {
|
function findGvlSnapshot(sha256) {
|
||||||
@@ -256,6 +638,14 @@ function formatNullable(value) {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatJsonValue(value) {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
function formatCount(value) {
|
function formatCount(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return "0";
|
return "0";
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren