Add lightweight observed request explorer

Dieser Commit ist enthalten in:
2026-05-27 18:31:44 +02:00
Ursprung 31001b9610
Commit 71541ef187
5 geänderte Dateien mit 641 neuen und 0 gelöschten Zeilen
+46
Datei anzeigen
@@ -81,6 +81,10 @@ async function handleVendorGetMessage(message, sender) {
return handleListRecentConsentStatesMessage();
}
if (message.type === "list_recent_observed_requests") {
return handleListRecentObservedRequestsMessage();
}
if (message.type === "purge_unlocked_evidence_records") {
return handlePurgeUnlockedEvidenceRecordsMessage();
}
@@ -316,6 +320,16 @@ async function handleListRecentConsentStatesMessage() {
};
}
async function handleListRecentObservedRequestsMessage() {
const db = await openVendorGetDb();
const observedRequests = await listRecentObservedRequests(db, 50);
return {
success: true,
observedRequests
};
}
function getLatestConsentState(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction(["consent_states"], "readonly");
@@ -368,6 +382,38 @@ function listRecentConsentStates(db, limit) {
});
}
function listRecentObservedRequests(db, limit) {
return new Promise((resolve, reject) => {
const observedRequests = [];
const tx = db.transaction(
[VENDORGET_STORE_NAMES.observedRequests],
"readonly"
);
const requestsStore = tx.objectStore(VENDORGET_STORE_NAMES.observedRequests);
const lastSeenAtIndex = requestsStore.index("lastSeenAt");
const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor || observedRequests.length >= limit) {
return;
}
observedRequests.push(cursor.value);
if (observedRequests.length < limit) {
cursor.continue();
}
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(observedRequests);
});
}
function listRecentGvlSnapshots(db, limit) {
return new Promise((resolve, reject) => {
const snapshots = [];
+3
Datei anzeigen
@@ -102,6 +102,9 @@
<a class="button-link" href="../gvl-explorer/gvl-explorer.html">
GVL-Explorer öffnen
</a>
<a class="button-link" href="../request-explorer/request-explorer.html">
Request-Explorer öffnen
</a>
<a class="button-link" href="../analysis-dashboard/analysis-dashboard.html">
Request-/Empfänger-Analyse öffnen
</a>
+228
Datei anzeigen
@@ -0,0 +1,228 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
font-family: Arial, sans-serif;
color: #e5edf5;
background: #111827;
}
.explorer {
width: min(1180px, 100%);
margin: 0 auto;
padding: 24px;
}
.explorer-header {
display: grid;
gap: 8px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #334155;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: 24px;
font-weight: 700;
}
h2 {
margin-bottom: 12px;
font-size: 15px;
font-weight: 700;
}
h3 {
margin-top: 14px;
margin-bottom: 8px;
font-size: 13px;
color: #cbd5e1;
}
p {
max-width: 760px;
font-size: 13px;
line-height: 1.5;
color: #cbd5e1;
}
.back-link {
width: fit-content;
color: #bfdbfe;
font-size: 13px;
}
.panel {
margin-bottom: 22px;
padding-bottom: 20px;
border-bottom: 1px solid #334155;
}
.section-help {
margin-bottom: 4px;
}
.request-overview {
display: grid;
grid-template-columns: repeat(2, minmax(0, 220px));
gap: 10px;
margin: 8px 0 0;
}
.request-overview div {
min-width: 0;
padding: 10px 12px;
border: 1px solid #334155;
border-radius: 4px;
background: #1f2937;
}
.request-overview dt {
margin: 0 0 6px;
font-size: 12px;
color: #cbd5e1;
}
.request-overview dd {
margin: 0;
font-size: 13px;
font-weight: 700;
overflow-wrap: anywhere;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
background: #1f2937;
}
th,
td {
padding: 10px 12px;
border: 1px solid #334155;
text-align: left;
}
th {
color: #cbd5e1;
font-weight: 700;
background: #182231;
}
.request-list-wrap {
width: 100%;
overflow-x: auto;
border: 1px solid #334155;
border-radius: 4px;
background: #1f2937;
}
.request-list {
min-width: 980px;
border: 0;
}
.request-list th,
.request-list td {
padding: 8px 10px;
vertical-align: top;
}
.request-list tbody tr {
cursor: pointer;
}
.request-list tbody tr:hover,
.request-list tbody tr:focus {
outline: 0;
background: #263449;
}
.request-list tbody tr.is-selected {
background: #1e3a5f;
box-shadow: inset 3px 0 0 #60a5fa;
}
.request-list .numeric {
text-align: right;
}
.request-list .path-cell,
.request-list .origin-cell {
max-width: 260px;
overflow-wrap: anywhere;
}
.request-detail {
margin-top: 18px;
}
.inspector-table th,
.inspector-table td {
vertical-align: top;
}
.inspector-table th {
width: 230px;
}
.inspector-table .inspector-value {
overflow-wrap: anywhere;
word-break: break-word;
user-select: text;
}
.empty-state {
padding: 10px 12px;
border: 1px solid #334155;
border-radius: 4px;
background: #1f2937;
}
.inspector-details {
margin-top: 12px;
font-size: 13px;
color: #cbd5e1;
}
.inspector-details summary {
cursor: pointer;
}
.inspector-details pre {
max-height: 260px;
margin: 10px 0 0;
padding: 10px 12px;
overflow: auto;
border: 1px solid #334155;
border-radius: 4px;
color: #e5edf5;
background: #0f172a;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
@media (max-width: 640px) {
.explorer {
padding: 16px;
}
.request-overview {
grid-template-columns: 1fr;
}
.inspector-table th {
width: 150px;
}
}
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VG-Observe Request-Explorer</title>
<link rel="stylesheet" href="request-explorer.css">
</head>
<body>
<main class="explorer">
<header class="explorer-header">
<a class="back-link" href="../dashboard/dashboard.html">Zur&uuml;ck zum Dashboard</a>
<h1>Request-Explorer</h1>
<p class="section-help">
Diese Ansicht zeigt zuletzt technisch beobachtete Requests.
</p>
<dl class="request-overview" aria-label="Geladene Request-Uebersicht">
<div>
<dt>Geladene Eintr&auml;ge</dt>
<dd id="request-loaded-count">-</dd>
</div>
<div>
<dt>Zuletzt beobachtet</dt>
<dd id="request-latest-seen">-</dd>
</div>
</dl>
</header>
<section class="panel" aria-labelledby="request-list-title">
<h2 id="request-list-title">Zuletzt beobachtete Requests</h2>
<p id="request-empty" class="empty-state" hidden>
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&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">
<h2 id="request-detail-title">Ausgew&auml;hlter Request</h2>
<div id="request-detail-summary"></div>
</section>
<details class="inspector-details" open>
<summary>Vollst&auml;ndiger JSON-Rohdatenauszug</summary>
<pre id="request-detail-json"></pre>
</details>
<details class="inspector-details">
<summary>correlation</summary>
<pre id="request-detail-correlation"></pre>
</details>
<details class="inspector-details">
<summary>requestFingerprintSource</summary>
<pre id="request-detail-fingerprint-source"></pre>
</details>
<details class="inspector-details">
<summary>Technische Rohfelder</summary>
<pre id="request-detail-technical-fields"></pre>
</details>
</div>
</section>
</main>
<script src="request-explorer.js"></script>
</body>
</html>
+285
Datei anzeigen
@@ -0,0 +1,285 @@
"use strict";
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 requestDetailSummary = document.getElementById("request-detail-summary");
const requestDetailJson = document.getElementById("request-detail-json");
const requestDetailCorrelation = document.getElementById(
"request-detail-correlation"
);
const requestDetailFingerprintSource = document.getElementById(
"request-detail-fingerprint-source"
);
const requestDetailTechnicalFields = document.getElementById(
"request-detail-technical-fields"
);
let observedRequests = [];
let selectedRequestFingerprint = null;
document.addEventListener("DOMContentLoaded", async () => {
await renderObservedRequests();
});
async function renderObservedRequests() {
try {
const result = await browser.runtime.sendMessage({
type: "list_recent_observed_requests"
});
if (!result?.success) {
throw new Error(result?.error ?? "list_recent_observed_requests_failed");
}
observedRequests = result.observedRequests ?? [];
updateOverview();
if (observedRequests.length === 0) {
renderNoObservedRequests();
return;
}
requestEmpty.hidden = true;
requestContent.hidden = false;
if (!findObservedRequest(selectedRequestFingerprint)) {
selectedRequestFingerprint =
observedRequests[0]?.requestFingerprint ?? null;
}
renderRequestList();
renderSelectedRequest();
} catch (error) {
observedRequests = [];
updateOverview();
requestEmpty.hidden = false;
requestContent.hidden = true;
requestEmpty.textContent =
"Beobachtete Requests konnten nicht geladen werden.";
console.warn("VG-Observe request list failed", error);
}
}
function updateOverview() {
requestLoadedCount.textContent = String(observedRequests.length);
requestLatestSeen.textContent = formatNullable(
observedRequests[0]?.lastSeenAt ?? null
);
}
function renderNoObservedRequests() {
selectedRequestFingerprint = null;
requestList.textContent = "";
clearRequestDetails();
requestEmpty.hidden = false;
requestContent.hidden = true;
requestEmpty.textContent = "Keine beobachteten Requests vorhanden.";
}
function renderRequestList() {
requestList.textContent = "";
for (const observedRequest of observedRequests) {
const row = document.createElement("tr");
const isSelected =
observedRequest?.requestFingerprint === selectedRequestFingerprint;
row.className = isSelected ? "is-selected" : "";
row.tabIndex = 0;
row.setAttribute("role", "button");
row.setAttribute("aria-pressed", isSelected ? "true" : "false");
row.addEventListener("click", () => {
selectObservedRequest(observedRequest?.requestFingerprint ?? null);
});
row.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectObservedRequest(observedRequest?.requestFingerprint ?? null);
}
});
appendListCell(row, formatNullable(observedRequest?.lastSeenAt));
appendListCell(
row,
formatNullable(observedRequest?.request?.origin),
"origin-cell"
);
appendListCell(
row,
formatNullable(observedRequest?.request?.pathname),
"path-cell"
);
appendListCell(row, formatThirdParty(observedRequest?.request?.thirdParty));
appendListCell(row, formatYesNo(hasConsentParams(observedRequest)));
appendListCell(row, formatYesNo(hasConsentCorrelation(observedRequest)));
appendListCell(row, formatNullable(observedRequest?.seenCount), "numeric");
requestList.append(row);
}
}
function appendListCell(row, value, className) {
const cell = document.createElement("td");
if (className) {
cell.className = className;
}
cell.textContent = value;
row.append(cell);
}
function selectObservedRequest(requestFingerprint) {
selectedRequestFingerprint = requestFingerprint;
renderRequestList();
renderSelectedRequest();
}
function renderSelectedRequest() {
const observedRequest = findObservedRequest(selectedRequestFingerprint);
clearRequestDetails();
if (!observedRequest) {
return;
}
renderSummaryTable([
["Letzte Beobachtung", formatNullable(observedRequest?.lastSeenAt)],
["Erste Beobachtung", formatNullable(observedRequest?.firstSeenAt)],
["Empfaenger-Origin", formatNullable(observedRequest?.request?.origin)],
["Pfad", formatNullable(observedRequest?.request?.pathname)],
["Methode", formatNullable(observedRequest?.request?.method)],
["Request-Typ", formatNullable(observedRequest?.request?.type)],
["Third-Party", formatThirdParty(observedRequest?.request?.thirdParty)],
["Consent-Parameter vorhanden", formatYesNo(hasConsentParams(observedRequest))],
[
"Consent-Korrelation vorhanden",
formatYesNo(hasConsentCorrelation(observedRequest))
],
["SeenCount", formatNullable(observedRequest?.seenCount)],
[
"Request-Fingerprint",
formatNullable(observedRequest?.requestFingerprint)
]
]);
requestDetailJson.textContent = JSON.stringify(observedRequest, null, 2);
requestDetailCorrelation.textContent = JSON.stringify(
observedRequest?.correlation ?? null,
null,
2
);
requestDetailFingerprintSource.textContent = JSON.stringify(
observedRequest?.requestFingerprintSource ?? null,
null,
2
);
requestDetailTechnicalFields.textContent = JSON.stringify(
buildTechnicalFieldExtract(observedRequest),
null,
2
);
}
function renderSummaryTable(rows) {
const table = document.createElement("table");
const body = document.createElement("tbody");
table.className = "inspector-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.className = "inspector-value";
valueCell.textContent = value;
row.append(labelCell, valueCell);
body.append(row);
}
table.append(body);
requestDetailSummary.append(table);
}
function clearRequestDetails() {
requestDetailSummary.textContent = "";
requestDetailJson.textContent = "";
requestDetailCorrelation.textContent = "";
requestDetailFingerprintSource.textContent = "";
requestDetailTechnicalFields.textContent = "";
}
function findObservedRequest(requestFingerprint) {
if (!requestFingerprint) {
return null;
}
return (
observedRequests.find((observedRequest) => {
return observedRequest?.requestFingerprint === requestFingerprint;
}) ?? null
);
}
function hasConsentParams(observedRequest) {
const consentParams = observedRequest?.consentParams;
if (!consentParams || typeof consentParams !== "object") {
return false;
}
return Object.values(consentParams).some((value) => {
return value !== null && value !== undefined && value !== "";
});
}
function hasConsentCorrelation(observedRequest) {
return Boolean(observedRequest?.correlation?.stateFingerprint);
}
function buildTechnicalFieldExtract(observedRequest) {
return {
schemaVersion: observedRequest?.schemaVersion ?? null,
recordedAt: observedRequest?.recordedAt ?? null,
recordingSource: observedRequest?.recordingSource ?? null,
firstSeenAt: observedRequest?.firstSeenAt ?? null,
lastSeenAt: observedRequest?.lastSeenAt ?? null,
seenCount: observedRequest?.seenCount ?? null,
context: observedRequest?.context ?? null,
request: observedRequest?.request ?? null,
consentParams: observedRequest?.consentParams ?? null
};
}
function formatNullable(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
return String(value);
}
function formatThirdParty(value) {
if (value === true) {
return "ja";
}
if (value === false) {
return "nein";
}
return "unbekannt";
}
function formatYesNo(value) {
return value ? "ja" : "nein";
}