Restructure request explorer into grouped evidence view
Dieser Commit ist enthalten in:
@@ -164,6 +164,46 @@ th {
|
||||
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 {
|
||||
margin-top: 18px;
|
||||
}
|
||||
@@ -222,7 +262,17 @@ th {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.request-context-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.inspector-table th {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 980px) {
|
||||
.request-context-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,22 +32,7 @@
|
||||
Keine beobachteten Requests vorhanden.
|
||||
</p>
|
||||
<div id="request-content" hidden>
|
||||
<div class="request-list-wrap">
|
||||
<table class="request-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Letzte Beobachtung</th>
|
||||
<th scope="col">Empfä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>
|
||||
<div id="request-groups" class="request-groups"></div>
|
||||
|
||||
<section class="request-detail" aria-labelledby="request-detail-title">
|
||||
<h2 id="request-detail-title">Ausgewählter Request</h2>
|
||||
|
||||
@@ -4,7 +4,7 @@ const requestLoadedCount = document.getElementById("request-loaded-count");
|
||||
const requestLatestSeen = document.getElementById("request-latest-seen");
|
||||
const requestEmpty = document.getElementById("request-empty");
|
||||
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 requestDetailJson = document.getElementById("request-detail-json");
|
||||
const requestDetailCorrelation = document.getElementById(
|
||||
@@ -18,6 +18,7 @@ const requestDetailTechnicalFields = document.getElementById(
|
||||
);
|
||||
|
||||
let observedRequests = [];
|
||||
let observedRequestGroups = [];
|
||||
let selectedRequestFingerprint = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
@@ -35,6 +36,7 @@ async function renderObservedRequests() {
|
||||
}
|
||||
|
||||
observedRequests = result.observedRequests ?? [];
|
||||
observedRequestGroups = buildObservedRequestGroups(observedRequests);
|
||||
updateOverview();
|
||||
|
||||
if (observedRequests.length === 0) {
|
||||
@@ -50,10 +52,11 @@ async function renderObservedRequests() {
|
||||
observedRequests[0]?.requestFingerprint ?? null;
|
||||
}
|
||||
|
||||
renderRequestList();
|
||||
renderRequestGroups();
|
||||
renderSelectedRequest();
|
||||
} catch (error) {
|
||||
observedRequests = [];
|
||||
observedRequestGroups = [];
|
||||
updateOverview();
|
||||
requestEmpty.hidden = false;
|
||||
requestContent.hidden = true;
|
||||
@@ -72,17 +75,102 @@ function updateOverview() {
|
||||
|
||||
function renderNoObservedRequests() {
|
||||
selectedRequestFingerprint = null;
|
||||
requestList.textContent = "";
|
||||
requestGroups.textContent = "";
|
||||
clearRequestDetails();
|
||||
requestEmpty.hidden = false;
|
||||
requestContent.hidden = true;
|
||||
requestEmpty.textContent = "Keine beobachteten Requests vorhanden.";
|
||||
}
|
||||
|
||||
function renderRequestList() {
|
||||
requestList.textContent = "";
|
||||
function renderRequestGroups() {
|
||||
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 isSelected =
|
||||
observedRequest?.requestFingerprint === selectedRequestFingerprint;
|
||||
@@ -119,8 +207,7 @@ function renderRequestList() {
|
||||
appendListCell(row, formatYesNo(hasConsentCorrelation(observedRequest)));
|
||||
appendListCell(row, formatNullable(observedRequest?.seenCount), "numeric");
|
||||
|
||||
requestList.append(row);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function appendListCell(row, value, className) {
|
||||
@@ -136,7 +223,7 @@ function appendListCell(row, value, className) {
|
||||
|
||||
function selectObservedRequest(requestFingerprint) {
|
||||
selectedRequestFingerprint = requestFingerprint;
|
||||
renderRequestList();
|
||||
renderRequestGroups();
|
||||
renderSelectedRequest();
|
||||
}
|
||||
|
||||
@@ -152,6 +239,10 @@ function renderSelectedRequest() {
|
||||
renderSummaryTable([
|
||||
["Letzte Beobachtung", formatNullable(observedRequest?.lastSeenAt)],
|
||||
["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)],
|
||||
["Pfad", formatNullable(observedRequest?.request?.pathname)],
|
||||
["Methode", formatNullable(observedRequest?.request?.method)],
|
||||
@@ -251,18 +342,226 @@ function buildTechnicalFieldExtract(observedRequest) {
|
||||
schemaVersion: observedRequest?.schemaVersion ?? null,
|
||||
recordedAt: observedRequest?.recordedAt ?? null,
|
||||
recordingSource: observedRequest?.recordingSource ?? null,
|
||||
observedAt: observedRequest?.observedAt ?? null,
|
||||
firstSeenAt: observedRequest?.firstSeenAt ?? null,
|
||||
lastSeenAt: observedRequest?.lastSeenAt ?? null,
|
||||
seenCount: observedRequest?.seenCount ?? null,
|
||||
requestFingerprint: observedRequest?.requestFingerprint ?? null,
|
||||
requestFingerprintSource: observedRequest?.requestFingerprintSource ?? null,
|
||||
context: observedRequest?.context ?? 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) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "-";
|
||||
return "unbekannt";
|
||||
}
|
||||
|
||||
return String(value);
|
||||
@@ -281,5 +580,5 @@ function formatThirdParty(value) {
|
||||
}
|
||||
|
||||
function formatYesNo(value) {
|
||||
return value ? "ja" : "nein";
|
||||
return value ? "vorhanden" : "nicht vorhanden";
|
||||
}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren