Restructure request explorer into grouped evidence view

Dieser Commit ist enthalten in:
2026-05-27 20:04:27 +02:00
Ursprung 770c7587b9
Commit aedb818d1c
3 geänderte Dateien mit 397 neuen und 63 gelöschten Zeilen
@@ -164,6 +164,46 @@ th {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.request-groups {
display: grid;
gap: 16px;
}
.request-context-card {
padding: 14px;
border: 1px solid #334155;
border-radius: 4px;
background: #172033;
}
.request-context-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
margin: 0 0 12px;
}
.request-context-summary div {
min-width: 0;
padding: 9px 10px;
border: 1px solid #334155;
border-radius: 4px;
background: #1f2937;
}
.request-context-summary dt {
margin: 0 0 5px;
font-size: 12px;
color: #cbd5e1;
}
.request-context-summary dd {
margin: 0;
font-size: 13px;
font-weight: 700;
overflow-wrap: anywhere;
}
.request-detail { .request-detail {
margin-top: 18px; margin-top: 18px;
} }
@@ -222,7 +262,17 @@ th {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.request-context-summary {
grid-template-columns: 1fr;
}
.inspector-table th { .inspector-table th {
width: 150px; width: 150px;
} }
} }
@media (min-width: 641px) and (max-width: 980px) {
.request-context-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
+1 -16
Datei anzeigen
@@ -32,22 +32,7 @@
Keine beobachteten Requests vorhanden. Keine beobachteten Requests vorhanden.
</p> </p>
<div id="request-content" hidden> <div id="request-content" hidden>
<div class="request-list-wrap"> <div id="request-groups" class="request-groups"></div>
<table class="request-list">
<thead>
<tr>
<th scope="col">Letzte Beobachtung</th>
<th scope="col">Empf&auml;nger-Origin</th>
<th scope="col">Pfad</th>
<th scope="col">Third-Party</th>
<th scope="col">Consent-Parameter</th>
<th scope="col">Consent-Korrelation</th>
<th scope="col">SeenCount</th>
</tr>
</thead>
<tbody id="request-list"></tbody>
</table>
</div>
<section class="request-detail" aria-labelledby="request-detail-title"> <section class="request-detail" aria-labelledby="request-detail-title">
<h2 id="request-detail-title">Ausgew&auml;hlter Request</h2> <h2 id="request-detail-title">Ausgew&auml;hlter Request</h2>
+311 -12
Datei anzeigen
@@ -4,7 +4,7 @@ const requestLoadedCount = document.getElementById("request-loaded-count");
const requestLatestSeen = document.getElementById("request-latest-seen"); const requestLatestSeen = document.getElementById("request-latest-seen");
const requestEmpty = document.getElementById("request-empty"); const requestEmpty = document.getElementById("request-empty");
const requestContent = document.getElementById("request-content"); const requestContent = document.getElementById("request-content");
const requestList = document.getElementById("request-list"); const requestGroups = document.getElementById("request-groups");
const requestDetailSummary = document.getElementById("request-detail-summary"); const requestDetailSummary = document.getElementById("request-detail-summary");
const requestDetailJson = document.getElementById("request-detail-json"); const requestDetailJson = document.getElementById("request-detail-json");
const requestDetailCorrelation = document.getElementById( const requestDetailCorrelation = document.getElementById(
@@ -18,6 +18,7 @@ const requestDetailTechnicalFields = document.getElementById(
); );
let observedRequests = []; let observedRequests = [];
let observedRequestGroups = [];
let selectedRequestFingerprint = null; let selectedRequestFingerprint = null;
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
@@ -35,6 +36,7 @@ async function renderObservedRequests() {
} }
observedRequests = result.observedRequests ?? []; observedRequests = result.observedRequests ?? [];
observedRequestGroups = buildObservedRequestGroups(observedRequests);
updateOverview(); updateOverview();
if (observedRequests.length === 0) { if (observedRequests.length === 0) {
@@ -50,10 +52,11 @@ async function renderObservedRequests() {
observedRequests[0]?.requestFingerprint ?? null; observedRequests[0]?.requestFingerprint ?? null;
} }
renderRequestList(); renderRequestGroups();
renderSelectedRequest(); renderSelectedRequest();
} catch (error) { } catch (error) {
observedRequests = []; observedRequests = [];
observedRequestGroups = [];
updateOverview(); updateOverview();
requestEmpty.hidden = false; requestEmpty.hidden = false;
requestContent.hidden = true; requestContent.hidden = true;
@@ -72,17 +75,102 @@ function updateOverview() {
function renderNoObservedRequests() { function renderNoObservedRequests() {
selectedRequestFingerprint = null; selectedRequestFingerprint = null;
requestList.textContent = ""; requestGroups.textContent = "";
clearRequestDetails(); clearRequestDetails();
requestEmpty.hidden = false; requestEmpty.hidden = false;
requestContent.hidden = true; requestContent.hidden = true;
requestEmpty.textContent = "Keine beobachteten Requests vorhanden."; requestEmpty.textContent = "Keine beobachteten Requests vorhanden.";
} }
function renderRequestList() { function renderRequestGroups() {
requestList.textContent = ""; requestGroups.textContent = "";
for (const observedRequest of observedRequests) { for (const group of observedRequestGroups) {
requestGroups.append(buildRequestGroupElement(group));
}
}
function buildRequestGroupElement(group) {
const section = document.createElement("section");
const title = document.createElement("h3");
const context = document.createElement("dl");
const tableWrap = document.createElement("div");
const table = document.createElement("table");
const head = document.createElement("thead");
const headRow = document.createElement("tr");
const body = document.createElement("tbody");
section.className = "request-context-card";
title.textContent = "Technisch beobachteter Vorgang";
context.className = "request-context-summary";
tableWrap.className = "request-list-wrap";
table.className = "request-list";
appendDefinition(context, "Seiten-/Dokumentkontext", group.contextLabel);
appendDefinition(context, "Tab / Frame", group.tabFrameLabel);
appendDefinition(context, "Zeitraum", group.periodLabel);
appendDefinition(context, "Requests", String(group.requests.length));
appendDefinition(
context,
"Empfaenger-Origins",
String(group.recipientOriginCount)
);
appendDefinition(
context,
"Third-Party-Requests",
String(group.thirdPartyCount)
);
appendDefinition(
context,
"Requests mit Consent-Parametern",
String(group.consentParamCount)
);
appendDefinition(
context,
"Requests mit Consent-Korrelation",
String(group.consentCorrelationCount)
);
[
"Letzte Beobachtung",
"Empfaenger-Origin",
"Pfad",
"Third-Party",
"Consent-Parameter",
"Consent-Korrelation",
"SeenCount"
].forEach((label) => {
const cell = document.createElement("th");
cell.scope = "col";
cell.textContent = label;
headRow.append(cell);
});
for (const observedRequest of group.requests) {
body.append(buildRequestRow(observedRequest));
}
head.append(headRow);
table.append(head, body);
tableWrap.append(table);
section.append(title, context, tableWrap);
return section;
}
function appendDefinition(list, label, value) {
const item = document.createElement("div");
const term = document.createElement("dt");
const description = document.createElement("dd");
term.textContent = label;
description.textContent = value;
item.append(term, description);
list.append(item);
}
function buildRequestRow(observedRequest) {
const row = document.createElement("tr"); const row = document.createElement("tr");
const isSelected = const isSelected =
observedRequest?.requestFingerprint === selectedRequestFingerprint; observedRequest?.requestFingerprint === selectedRequestFingerprint;
@@ -119,8 +207,7 @@ function renderRequestList() {
appendListCell(row, formatYesNo(hasConsentCorrelation(observedRequest))); appendListCell(row, formatYesNo(hasConsentCorrelation(observedRequest)));
appendListCell(row, formatNullable(observedRequest?.seenCount), "numeric"); appendListCell(row, formatNullable(observedRequest?.seenCount), "numeric");
requestList.append(row); return row;
}
} }
function appendListCell(row, value, className) { function appendListCell(row, value, className) {
@@ -136,7 +223,7 @@ function appendListCell(row, value, className) {
function selectObservedRequest(requestFingerprint) { function selectObservedRequest(requestFingerprint) {
selectedRequestFingerprint = requestFingerprint; selectedRequestFingerprint = requestFingerprint;
renderRequestList(); renderRequestGroups();
renderSelectedRequest(); renderSelectedRequest();
} }
@@ -152,6 +239,10 @@ function renderSelectedRequest() {
renderSummaryTable([ renderSummaryTable([
["Letzte Beobachtung", formatNullable(observedRequest?.lastSeenAt)], ["Letzte Beobachtung", formatNullable(observedRequest?.lastSeenAt)],
["Erste Beobachtung", formatNullable(observedRequest?.firstSeenAt)], ["Erste Beobachtung", formatNullable(observedRequest?.firstSeenAt)],
["Beobachtet um", formatNullable(getObservedAt(observedRequest))],
["Seiten-/Dokumentkontext", getPageContextLabel(observedRequest)],
["Tab-ID", formatUnknown(getTabId(observedRequest))],
["Frame-ID", formatUnknown(getFrameId(observedRequest))],
["Empfaenger-Origin", formatNullable(observedRequest?.request?.origin)], ["Empfaenger-Origin", formatNullable(observedRequest?.request?.origin)],
["Pfad", formatNullable(observedRequest?.request?.pathname)], ["Pfad", formatNullable(observedRequest?.request?.pathname)],
["Methode", formatNullable(observedRequest?.request?.method)], ["Methode", formatNullable(observedRequest?.request?.method)],
@@ -251,18 +342,226 @@ function buildTechnicalFieldExtract(observedRequest) {
schemaVersion: observedRequest?.schemaVersion ?? null, schemaVersion: observedRequest?.schemaVersion ?? null,
recordedAt: observedRequest?.recordedAt ?? null, recordedAt: observedRequest?.recordedAt ?? null,
recordingSource: observedRequest?.recordingSource ?? null, recordingSource: observedRequest?.recordingSource ?? null,
observedAt: observedRequest?.observedAt ?? null,
firstSeenAt: observedRequest?.firstSeenAt ?? null, firstSeenAt: observedRequest?.firstSeenAt ?? null,
lastSeenAt: observedRequest?.lastSeenAt ?? null, lastSeenAt: observedRequest?.lastSeenAt ?? null,
seenCount: observedRequest?.seenCount ?? null, seenCount: observedRequest?.seenCount ?? null,
requestFingerprint: observedRequest?.requestFingerprint ?? null,
requestFingerprintSource: observedRequest?.requestFingerprintSource ?? null,
context: observedRequest?.context ?? null, context: observedRequest?.context ?? null,
request: observedRequest?.request ?? null, request: observedRequest?.request ?? null,
consentParams: observedRequest?.consentParams ?? null consentParams: observedRequest?.consentParams ?? null,
correlation: observedRequest?.correlation ?? null
}; };
} }
function buildObservedRequestGroups(requests) {
const groupsByKey = new Map();
for (const observedRequest of requests) {
const key = buildObservedRequestGroupKey(observedRequest);
let group = groupsByKey.get(key);
if (!group) {
group = {
key,
contextLabel: getPageContextLabel(observedRequest),
tabFrameLabel: buildTabFrameLabel(observedRequest),
requests: []
};
groupsByKey.set(key, group);
}
group.requests.push(observedRequest);
}
return Array.from(groupsByKey.values())
.map(enrichObservedRequestGroup)
.sort((left, right) => {
return compareTimestampValues(right.lastSeenAt, left.lastSeenAt);
});
}
function buildObservedRequestGroupKey(observedRequest) {
return [
`tab:${formatKeyValue(getTabId(observedRequest))}`,
`frame:${formatKeyValue(getFrameId(observedRequest))}`,
`page:${getPageContextValue(observedRequest) ?? "unknown"}`
].join("|");
}
function enrichObservedRequestGroup(group) {
const sortedRequests = [...group.requests].sort((left, right) => {
return compareTimestampValues(getObservedAt(left), getObservedAt(right));
});
const recipientOrigins = new Set(
sortedRequests
.map((observedRequest) => observedRequest?.request?.origin ?? null)
.filter((origin) => origin !== null && origin !== undefined && origin !== "")
);
let firstSeenAt = null;
let lastSeenAt = null;
for (const observedRequest of sortedRequests) {
firstSeenAt = pickTimestamp(firstSeenAt, observedRequest?.firstSeenAt, "min");
lastSeenAt = pickTimestamp(lastSeenAt, observedRequest?.lastSeenAt, "max");
}
return {
...group,
requests: sortedRequests,
firstSeenAt,
lastSeenAt,
periodLabel: buildPeriodLabel(firstSeenAt, lastSeenAt),
recipientOriginCount: recipientOrigins.size,
thirdPartyCount: sortedRequests.filter((observedRequest) => {
return observedRequest?.request?.thirdParty === true;
}).length,
consentParamCount: sortedRequests.filter(hasConsentParams).length,
consentCorrelationCount: sortedRequests.filter(hasConsentCorrelation).length
};
}
function getPageContextLabel(observedRequest) {
return formatUnknown(getPageContextValue(observedRequest));
}
function getPageContextValue(observedRequest) {
return (
getFirstPresentValue(
observedRequest,
[
"pageUrl",
"documentUrl",
"initiator",
"sourceUrl",
"context.pageUrl",
"context.documentUrl",
"context.initiator",
"context.sourceUrl",
"request.pageUrl",
"request.documentUrl",
"request.initiator",
"request.sourceUrl"
]
) ?? null
);
}
function getTabId(observedRequest) {
return getFirstPresentValue(observedRequest, ["tabId", "context.tabId"]);
}
function getFrameId(observedRequest) {
return getFirstPresentValue(observedRequest, ["frameId", "context.frameId"]);
}
function getObservedAt(observedRequest) {
return getFirstPresentValue(observedRequest, [
"observedAt",
"firstSeenAt",
"lastSeenAt"
]);
}
function getFirstPresentValue(source, paths) {
for (const path of paths) {
const value = getPathValue(source, path);
if (value !== null && value !== undefined && value !== "") {
return value;
}
}
return null;
}
function getPathValue(source, path) {
return path.split(".").reduce((value, key) => {
if (value === null || value === undefined || typeof value !== "object") {
return undefined;
}
return value[key];
}, source);
}
function buildTabFrameLabel(observedRequest) {
return `Tab ${formatUnknown(getTabId(observedRequest))} / Frame ${formatUnknown(
getFrameId(observedRequest)
)}`;
}
function pickTimestamp(current, candidate, direction) {
if (!current) {
return candidate ?? null;
}
if (!candidate) {
return current;
}
const comparison = compareTimestampValues(candidate, current);
if (direction === "min") {
return comparison < 0 ? candidate : current;
}
return comparison > 0 ? candidate : current;
}
function compareTimestampValues(left, right) {
const leftTime = Date.parse(left ?? "");
const rightTime = Date.parse(right ?? "");
if (Number.isNaN(leftTime) && Number.isNaN(rightTime)) {
return 0;
}
if (Number.isNaN(leftTime)) {
return -1;
}
if (Number.isNaN(rightTime)) {
return 1;
}
return leftTime - rightTime;
}
function buildPeriodLabel(firstSeenAt, lastSeenAt) {
if (!firstSeenAt && !lastSeenAt) {
return "unbekannt";
}
if (firstSeenAt === lastSeenAt || !lastSeenAt) {
return formatUnknown(firstSeenAt);
}
if (!firstSeenAt) {
return formatUnknown(lastSeenAt);
}
return `${firstSeenAt} bis ${lastSeenAt}`;
}
function formatKeyValue(value) {
return value === null || value === undefined || value === ""
? "unknown"
: value;
}
function formatUnknown(value) {
if (value === null || value === undefined || value === "") {
return "unbekannt";
}
return String(value);
}
function formatNullable(value) { function formatNullable(value) {
if (value === null || value === undefined || value === "") { if (value === null || value === undefined || value === "") {
return "-"; return "unbekannt";
} }
return String(value); return String(value);
@@ -281,5 +580,5 @@ function formatThirdParty(value) {
} }
function formatYesNo(value) { function formatYesNo(value) {
return value ? "ja" : "nein"; return value ? "vorhanden" : "nicht vorhanden";
} }