@analitikisler/ai-tag-debugger 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +34 -18
- package/README.tr.md +346 -0
- package/package.json +1 -1
- package/src/audit.js +1 -1
- package/src/brand.js +14 -0
- package/src/browser.js +21 -5
- package/src/i18n.js +68 -2
- package/src/mcp.js +2 -2
- package/src/parsers.js +7 -0
- package/src/report.js +446 -286
package/src/report.js
CHANGED
|
@@ -3,6 +3,10 @@ import { groupFindings, summarize } from "./checks.js";
|
|
|
3
3
|
import { messages } from "./i18n.js";
|
|
4
4
|
import { parseTrackingRequest, providerOf, PROVIDERS } from "./parsers.js";
|
|
5
5
|
import { decodeGa4Item, isEventParam, paramTip } from "./glossary.js";
|
|
6
|
+
import { LOGOS } from "./brand.js";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
6
10
|
|
|
7
11
|
const EXPLAIN_SYSTEM = `You are a senior digital analytics consultant reviewing an automated tracking audit. You receive findings from deterministic checks, the measurement plan, the hits captured on each step of each journey, and optionally a Google Tag Manager container export.
|
|
8
12
|
|
|
@@ -15,7 +19,9 @@ When a GTM container is provided, name the specific tags, triggers or variables
|
|
|
15
19
|
Then write the summary:
|
|
16
20
|
- headline: one sentence naming what matters most for data quality and ad spend;
|
|
17
21
|
- points: the main problems, most important first, each one sentence with its severity;
|
|
18
|
-
- also_check: things you can see in the captured hits that the findings list does not cover (for example a conversion that fires on page load, or a cookie banner click counted as a conversion). Only include what the captured data shows; leave the list empty otherwise
|
|
22
|
+
- also_check: things you can see in the captured hits that the findings list does not cover (for example a conversion that fires on page load, or a cookie banner click counted as a conversion). Only include what the captured data shows; leave the list empty otherwise.
|
|
23
|
+
|
|
24
|
+
Formatting: in the headline, points, also_check, impact, likely_cause and fix_steps, wrap numbers and counts in double asterisks (for example sent **2** times, at **+70.4s**) and event, parameter, tag and trigger names in backticks (for example \`begin_checkout\`). Use no other formatting: no headings, lists, links or HTML.`;
|
|
19
25
|
|
|
20
26
|
const SEVERITIES = ["broken", "risk", "warning", "info"];
|
|
21
27
|
const EXPLAIN_SCHEMA = {
|
|
@@ -240,17 +246,6 @@ function callsForHit(run, hit) {
|
|
|
240
246
|
return twins.length > 1 && calls.length === twins.length ? [calls[twins.indexOf(hit)]] : calls;
|
|
241
247
|
}
|
|
242
248
|
|
|
243
|
-
/** "4 planned events matched the plan: ga4:page_view, meta:AddToCart." as a lead line and event chips. */
|
|
244
|
-
function planMatchHtml(detail) {
|
|
245
|
-
const at = detail.indexOf(": ");
|
|
246
|
-
if (at < 0) return escapeHtml(detail);
|
|
247
|
-
const chips = detail.slice(at + 2).replace(/\.$/, "").split(", ").map((key) => {
|
|
248
|
-
const i = key.indexOf(":");
|
|
249
|
-
const p = i > 0 ? key.slice(0, i) : "";
|
|
250
|
-
return `<span class="ptag${p ? ` p-${escapeHtml(p)}` : ""}">${p ? `${escapeHtml(platformLabel(p))} <b>${escapeHtml(key.slice(i + 1))}</b>` : `<b>${escapeHtml(key)}</b>`}</span>`;
|
|
251
|
-
});
|
|
252
|
-
return `<span>${escapeHtml(detail.slice(0, at))}</span><span class="chips">${chips.join("")}</span>`;
|
|
253
|
-
}
|
|
254
249
|
|
|
255
250
|
const icon = {
|
|
256
251
|
chevron: '<svg class="chev" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>',
|
|
@@ -435,12 +430,43 @@ ${itemsBlock(m, items)}<details class="raw"><summary>${icon.chevron} ${m.ui.rawR
|
|
|
435
430
|
|
|
436
431
|
const stepAnchor = (ri, si) => `j${ri}-s${si}`;
|
|
437
432
|
const skippedText = (counts) => Object.entries(counts ?? {}).sort((a, b) => b[1] - a[1]);
|
|
433
|
+
const sortPlatforms = (keys) => [...keys].sort((a, b) => platformRank(a) - platformRank(b) || platformLabel(a).localeCompare(platformLabel(b)));
|
|
434
|
+
const problemCount = (findings, run) => findings.filter((f) => f.severity in SEVERITY_RANK && f.journey === run.journeyId && f.viewport === run.viewport).length;
|
|
435
|
+
|
|
436
|
+
// AI text marks numbers with **x** and names with `x`. Only those two marks become HTML; everything else is escaped.
|
|
437
|
+
const fmt = (s) => escapeHtml(s).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`(.+?)`/g, "<code>$1</code>");
|
|
438
|
+
// Finding texts quote event and parameter names ("add_to_cart"); show those as code.
|
|
439
|
+
const detailHtml = (s) => escapeHtml(s).replace(/"([^&]+?)"/g, "<code>$1</code>");
|
|
440
|
+
|
|
441
|
+
// Lucide icons.
|
|
442
|
+
const ic = (paths, size = 16) => `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
443
|
+
const ICON = {
|
|
444
|
+
broken: '<path d="m15 9-6 6"/><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="m9 9 6 6"/>',
|
|
445
|
+
warning: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
446
|
+
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
|
447
|
+
impact: '<polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/>',
|
|
448
|
+
cause: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
449
|
+
evidence: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>',
|
|
450
|
+
fix: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
|
451
|
+
where: '<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
|
|
452
|
+
};
|
|
453
|
+
const sevIcon = (sev) => ICON[sev === "broken" ? "broken" : sev === "info" ? "info" : "warning"];
|
|
454
|
+
const sec = (name, label) => `<div class="sec-h">${ic(ICON[name], 14)}<span>${label}</span></div>`;
|
|
455
|
+
const badge = (sev, text) => `<span class="badge ${sev}">${escapeHtml(text)}</span>`;
|
|
456
|
+
/** A logo with its light and dark versions, switched by the viewer's color scheme. */
|
|
457
|
+
const logo = (key, alt, height, cls) => `<picture class="${cls}"><source media="(prefers-color-scheme: dark)" srcset="${LOGOS[`${key}-dark`]}"><img src="${LOGOS[`${key}-light`]}" alt="${alt}" height="${height}"></picture>`;
|
|
458
|
+
|
|
459
|
+
/** The journey and step a finding's place points at. Steps are named by label; when labels repeat, prefer the step where the finding's event fired. */
|
|
460
|
+
function whereTarget(runs, w, f) {
|
|
461
|
+
const ri = runs.findIndex((r) => r.journeyId === w.journey && r.viewport === w.viewport);
|
|
462
|
+
if (ri < 0) return null;
|
|
463
|
+
const candidates = w.step ? runs[ri].steps.filter((s) => s.index > 0 && s.label === w.step) : [];
|
|
464
|
+
const step = candidates.find((s) => runs[ri].hits.some((h) => h.step === s.index && h.platform === f.platform && h.name === f.event)) ?? candidates[0];
|
|
465
|
+
return { ri, step };
|
|
466
|
+
}
|
|
438
467
|
|
|
439
|
-
function
|
|
468
|
+
function stepDetail(m, lang, findings, run, ri, step, isDefault) {
|
|
440
469
|
const hits = run.hits.filter((h) => h.step === step.index);
|
|
441
|
-
const flagged = hits.some((h) => hitFlag(findings, run, step, h));
|
|
442
|
-
const groups = [...new Set(hits.map((h) => h.platform))].sort((a, b) => platformRank(a) - platformRank(b) || platformLabel(a).localeCompare(platformLabel(b)));
|
|
443
|
-
const counts = groups.map((p) => `<span class="ptag p-${safeKey(p)}">${escapeHtml(platformLabel(p))} <b>${hits.filter((h) => h.platform === p).length}</b></span>`).join("");
|
|
444
470
|
const id = `s${ri}-${step.index}`;
|
|
445
471
|
const sortedHits = [...hits].sort((a, b) => platformRank(a.platform) - platformRank(b.platform) || a.t - b.t);
|
|
446
472
|
const eventRows = sortedHits.map((h) => ({ key: safeKey(h.platform), label: platformLabel(h.platform), html: eventRow(m, findings, run, step, h) }));
|
|
@@ -449,91 +475,181 @@ function stepHtml(m, lang, findings, run, ri, step) {
|
|
|
449
475
|
const skipped = skippedText(run.skipped?.[step.index]);
|
|
450
476
|
const skippedN = skipped.reduce((a, [, n]) => a + n, 0);
|
|
451
477
|
const tab = (n, label, count) => `<input type="radio" name="${id}" id="${id}-${n}"${n === 1 ? " checked" : ""}><label for="${id}-${n}">${label} <span class="n">${count}</span></label>`;
|
|
452
|
-
return `<
|
|
453
|
-
<
|
|
454
|
-
<div class="
|
|
455
|
-
<div class="panel">${eventRows.length ? filtered(m, `${id}e`, eventRows, eventRows.map((r) => r.html).join("")) : `<p class="
|
|
456
|
-
<div class="panel">${pushes.length ? `<ul class="evlist">${pushes.map((e) => pushRow(m, run, e)).join("")}</ul>` : `<p class="
|
|
457
|
-
<div class="panel">${reqRows.length ? filtered(m, `${id}n`, reqRows, reqRows.map((r) => r.html).join("")) : `<p class="
|
|
458
|
-
</div></
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
478
|
+
return `<section class="sdetail${isDefault ? " default" : ""}" id="${stepAnchor(ri, step.index)}" aria-label="${escapeHtml(step.label)}">
|
|
479
|
+
<header class="shead"><span class="sidx">${step.index}</span><div><h3>${escapeHtml(step.label)}</h3><div class="muted xs">${escapeHtml(m.ui.stepMeta(seconds(run, step.t), hits.length, pushes.length, skippedN))}${step.forced ? ` · ${m.ui.clickedThrough}` : ""}</div></div></header>
|
|
480
|
+
<div class="tabs seg">${tab(1, m.ui.tabEvents, eventRows.length)}${tab(2, "dataLayer", pushes.length)}${tab(3, m.ui.tabNetwork, reqRows.length)}
|
|
481
|
+
<div class="panel">${eventRows.length ? filtered(m, `${id}e`, eventRows, eventRows.map((r) => r.html).join("")) : `<p class="empty">${m.ui.noHits}</p>`}</div>
|
|
482
|
+
<div class="panel">${pushes.length ? `<ul class="evlist">${pushes.map((e) => pushRow(m, run, e)).join("")}</ul>` : `<p class="empty">${m.ui.noPushes}</p>`}</div>
|
|
483
|
+
<div class="panel">${reqRows.length ? filtered(m, `${id}n`, reqRows, reqRows.map((r) => r.html).join("")) : `<p class="empty">${m.ui.noRequests}</p>`}${skippedN ? `<p class="muted xs scope-note">${escapeHtml(m.ui.skippedInStep(skipped.map(([v, n]) => `${v} ${n}`).join(", "), skippedN))}</p>` : ""}</div>
|
|
484
|
+
</div></section>`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** One journey: a sticky step list on the left and the selected step on the right. It opens on the first step with a problem. */
|
|
488
|
+
function journeyPanel(m, lang, findings, run, ri, flagged) {
|
|
489
|
+
const steps = run.steps.filter((s) => s.index > 0);
|
|
490
|
+
const def = steps.find((s) => flagged.has(stepAnchor(ri, s.index))) ?? steps[0];
|
|
491
|
+
const nav = steps
|
|
492
|
+
.map((s) => {
|
|
493
|
+
const hits = run.hits.filter((h) => h.step === s.index);
|
|
494
|
+
const counts = sortPlatforms(new Set(hits.map((h) => h.platform))).map((p) => `<span class="dot p-${safeKey(p)}"></span>${hits.filter((h) => h.platform === p).length}`);
|
|
495
|
+
const a = stepAnchor(ri, s.index);
|
|
496
|
+
return `<li><a href="#${a}" data-s="${a}"${s === def ? ' class="def"' : ""}><span class="sidx">${s.index}</span><span class="sl">${escapeHtml(s.label)}</span>${flagged.has(a) ? `<span class="dot-flag" title="${m.ui.hasProblems}"></span>` : "<span></span>"}<span class="sc">${counts.join(" ") || '<span class="muted">–</span>'}</span></a></li>`;
|
|
466
497
|
})
|
|
467
498
|
.join("");
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
499
|
+
const body = steps.length
|
|
500
|
+
? `<div class="jgrid2"><nav class="stepnav" aria-label="${m.ui.stepsNav}"><ol>${nav}</ol></nav><div class="sdetails">${steps.map((s) => stepDetail(m, lang, findings, run, ri, s, s === def)).join("\n")}</div></div>`
|
|
501
|
+
: `<p class="empty">${m.ui.noSteps}</p>`;
|
|
502
|
+
return `<article class="jpanel" id="j${ri}"><header class="jhead"><div><h2>${escapeHtml(run.journeyId)} <span class="muted">· ${escapeHtml(run.viewport)}</span></h2>${run.note ? `<p class="muted xs">${escapeHtml(run.note)}</p>` : ""}</div>${run.completed ? badge("ok", m.ui.completed) : badge("warning", m.ui.notCompleted)}</header>
|
|
503
|
+
${body}</article>`;
|
|
473
504
|
}
|
|
474
505
|
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const step = candidates.find((s) => runs[ri].hits.some((h) => h.step === s.index && h.platform === f.platform && h.name === f.event)) ?? candidates[0];
|
|
480
|
-
const href = ri < 0 ? null : step ? `#${stepAnchor(ri, step.index)}` : `#j${ri}`;
|
|
481
|
-
const text = `<b>${escapeHtml(w.journey)}</b> · ${escapeHtml(w.viewport)}${w.step ? `<span class="muted"> · ${escapeHtml(w.step)}</span>` : ""}`;
|
|
482
|
-
return href ? `<li><a href="${href}">${text}</a></li>` : `<li>${text}</li>`;
|
|
506
|
+
function journeysTab(m, lang, findings, runs, flagged) {
|
|
507
|
+
if (!runs.length) return `<p class="empty">${m.ui.noSteps}</p>`;
|
|
508
|
+
const tabs = runs.map((r, ri) => `<a href="#j${ri}">${escapeHtml(r.journeyId)} <span class="muted">· ${escapeHtml(r.viewport)}</span><span class="n">${problemCount(findings, r)}</span></a>`).join("");
|
|
509
|
+
return `<nav class="jtabs" aria-label="${m.ui.chooseJourney}">${tabs}</nav>\n${runs.map((r, ri) => journeyPanel(m, lang, findings, r, ri, flagged)).join("\n")}`;
|
|
483
510
|
}
|
|
484
511
|
|
|
485
512
|
/** Evidence as a table when it is a JSON object (captured parameters), otherwise as text. */
|
|
486
513
|
function evidenceHtml(evidence) {
|
|
487
514
|
try {
|
|
488
515
|
const v = JSON.parse(evidence);
|
|
489
|
-
if (v && typeof v === "object" && !Array.isArray(v)) return
|
|
516
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return `<table class="ptable ev-table"><tbody>${Object.entries(v).map(([k, x]) => `<tr><th>${escapeHtml(k)}</th><td>${escapeHtml(cell(x))}</td></tr>`).join("")}</tbody></table>`;
|
|
490
517
|
} catch { /* plain text */ }
|
|
491
518
|
return `<pre>${escapeHtml(evidence)}</pre>`;
|
|
492
519
|
}
|
|
493
520
|
|
|
494
|
-
function
|
|
521
|
+
function findingCard(m, runs, f, open) {
|
|
495
522
|
const e = f.explanation;
|
|
496
523
|
const steps = fixSteps(e);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
524
|
+
const journeysN = new Set(f.where.map((w) => `${w.journey}|${w.viewport}`)).size;
|
|
525
|
+
const stepsN = f.where.filter((w) => w.step).length;
|
|
526
|
+
const evidence = f.evidence
|
|
527
|
+
? evidenceHtml(f.evidence)
|
|
528
|
+
: f.check === "missing_event" && f.platform
|
|
529
|
+
? `<table class="ptable ev-table"><tbody><tr><th>${escapeHtml(m.ui.requestTo(platformLabel(f.platform)))}</th><td><span class="missing">${m.ui.missing}</span></td></tr></tbody></table>`
|
|
530
|
+
: "";
|
|
531
|
+
const chips = f.where
|
|
532
|
+
.map((w) => {
|
|
533
|
+
const t = whereTarget(runs, w, f);
|
|
534
|
+
const inner = `${t?.step ? `<span class="chip-step">${t.step.index}</span>` : ""}${escapeHtml(w.journey)} · ${escapeHtml(w.viewport)}${w.step ? ` <span class="muted">· ${escapeHtml(w.step)}</span>` : ""}`;
|
|
535
|
+
return t ? `<a href="#${t.step ? stepAnchor(t.ri, t.step.index) : `j${t.ri}`}" class="chip">${inner}</a>` : `<span class="chip">${inner}</span>`;
|
|
536
|
+
})
|
|
537
|
+
.join("");
|
|
538
|
+
return `<li class="finding sev-${f.severity}" data-sev="${f.severity}"><details${open ? " open" : ""}><summary>
|
|
539
|
+
<span class="sev-icon">${ic(sevIcon(f.severity), 18)}</span>
|
|
540
|
+
<span class="fhead"><span class="ftitle">${detailHtml(f.detail)}</span><span class="fmeta">${f.platform ? `<span class="ptag p-${safeKey(f.platform)}">${escapeHtml(platformLabel(f.platform))}</span>` : ""}<span>${escapeHtml(m.ui.spread(journeysN, stepsN))}</span></span></span>
|
|
541
|
+
${badge(f.severity, m.severity[f.severity])}${icon.chevron}</summary>
|
|
542
|
+
<div class="fbody">${e?.impact ? `<div class="impact">${ic(ICON.impact, 16)}<div><span class="impact-l">${m.ui.impact}</span>${fmt(e.impact)}</div></div>` : ""}
|
|
543
|
+
<div class="fcols"><div class="fcol">${sec("cause", m.ui.likelyCause)}${e?.likely_cause ? `<p>${fmt(e.likely_cause)}</p>` : `<p class="muted">${m.ui.noExplanation}</p>`}${evidence ? `${sec("evidence", m.ui.evidence)}${evidence}` : ""}</div>
|
|
544
|
+
<div class="fcol">${steps.length ? `${sec("fix", m.ui.fixSteps)}<ol class="fix-steps">${steps.map((t) => `<li><span>${fmt(t)}</span></li>`).join("")}</ol>` : ""}${sec("where", m.ui.where)}<div class="where">${chips}</div></div></div></div></details></li>`;
|
|
501
545
|
}
|
|
502
546
|
|
|
503
|
-
|
|
547
|
+
const FINDING_SEVERITIES = ["broken", "risk", "warning", "info"];
|
|
548
|
+
const SEV_COLOR = { broken: "var(--danger-dot)", risk: "var(--risk)", warning: "var(--warn-dot)", info: "var(--accent)" };
|
|
549
|
+
|
|
550
|
+
function findingsTab(m, runs, rows) {
|
|
504
551
|
const problems = rows.filter((f) => f.severity !== "ok");
|
|
505
|
-
if (!problems.length) return `<p class="
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
552
|
+
if (!problems.length) return `<p class="empty">${m.ui.noProblems}</p>`;
|
|
553
|
+
const n = Object.fromEntries(FINDING_SEVERITIES.map((s) => [s, problems.filter((f) => f.severity === s).length]));
|
|
554
|
+
const present = FINDING_SEVERITIES.filter((s) => n[s]);
|
|
555
|
+
const radios = ["all", ...present].map((s) => `<input type="radio" name="fs" id="fs-${s}" class="fsr"${s === "all" ? " checked" : ""}>`).join("");
|
|
556
|
+
const bar = present.map((s) => `<span style="flex:${n[s]};background:${SEV_COLOR[s]}"></span>`).join("");
|
|
557
|
+
const legend = present.map((s) => `<span><i style="background:${SEV_COLOR[s]}"></i>${m.severity[s]} <strong>${n[s]}</strong></span>`).join("");
|
|
558
|
+
const labels = [`<label for="fs-all">${m.ui.all} <span class="n">${problems.length}</span></label>`, ...present.map((s) => `<label for="fs-${s}">${m.severity[s]} <span class="n">${n[s]}</span></label>`)].join("");
|
|
559
|
+
const sorted = FINDING_SEVERITIES.flatMap((s) => problems.filter((f) => f.severity === s));
|
|
560
|
+
return `<div class="filter-host">${radios}
|
|
561
|
+
<div class="sevbar" aria-hidden="true">${bar}</div><div class="sevlegend">${legend}</div>
|
|
562
|
+
<div class="toolbar"><div class="sevfilter" role="group" aria-label="${m.ui.filterBy}">${labels}</div><span class="muted xs">${m.ui.findingHint}</span></div>
|
|
563
|
+
<ul class="flist">${sorted.map((f, i) => findingCard(m, runs, f, i === 0 && f.severity !== "info")).join("")}</ul></div>`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function summaryCard(m, explained, problems) {
|
|
567
|
+
const s = summaryParts(explained?.summary);
|
|
568
|
+
let body;
|
|
569
|
+
if (!s) body = `<p class="muted">${problems ? m.ui.noSummary : m.ui.noProblems}</p>`;
|
|
570
|
+
else if (s.text) body = `<p class="summary-text">${fmt(s.text)}</p>`;
|
|
571
|
+
else {
|
|
572
|
+
body = `${s.headline ? `<p class="headline">${fmt(s.headline)}</p>` : ""}${s.points.length ? `<ul class="summary-list">${s.points.map((p) => `<li>${badge(p.severity, m.severity[p.severity])}<span>${fmt(p.text)}</span></li>`).join("")}</ul>` : ""}${s.also_check.length ? `<div><div class="label">${m.ui.alsoCheck}</div><ul class="also">${s.also_check.map((t) => `<li>${fmt(t)}</li>`).join("")}</ul></div>` : ""}`;
|
|
573
|
+
}
|
|
574
|
+
return `<div class="card"><div class="card-h"><div class="card-t">${m.ui.summary}</div><div class="card-d">${m.ui.summaryLead}</div></div><div class="card-c stack">${body}${problems ? `<p><a href="#findings">${m.ui.seeAllFindings}</a></p>` : ""}</div></div>`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The providers that were checked: from the runs, or for older captures the platforms seen. */
|
|
578
|
+
function checkedProviders(runs) {
|
|
579
|
+
const chosen = new Set(runs.flatMap((r) => r.providers ?? []));
|
|
580
|
+
if (!chosen.size) for (const r of runs) for (const h of r.hits) chosen.add(h.platform);
|
|
581
|
+
return sortPlatforms(chosen);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function skippedTotals(runs) {
|
|
585
|
+
const totals = {};
|
|
586
|
+
for (const r of runs) for (const counts of Object.values(r.skipped ?? {})) for (const [v, n] of Object.entries(counts)) totals[v] = (totals[v] ?? 0) + n;
|
|
587
|
+
return skippedText(totals);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const callProvider = (c) => safeKey(c.provider ?? providerOf(c.url)?.key ?? c.vendor);
|
|
591
|
+
|
|
592
|
+
function overviewTab(m, findings, rows, counts, explained, runs) {
|
|
593
|
+
const problems = rows.filter((f) => f.severity !== "ok").length;
|
|
594
|
+
const metric = (sev) => `<div class="card metric ${sev}"><div class="k">${m.severity[sev]}</div><div class="v">${counts[sev]}</div></div>`;
|
|
595
|
+
const journeyRows = runs.map((r, ri) => {
|
|
596
|
+
const n = problemCount(findings, r);
|
|
597
|
+
return `<tr><td><a href="#j${ri}">${escapeHtml(r.journeyId)} <span class="muted">· ${escapeHtml(r.viewport)}</span></a></td><td class="num">${r.steps.filter((s) => s.index > 0).length}</td><td class="num${n ? " bad" : ""}">${n}</td></tr>`;
|
|
598
|
+
});
|
|
599
|
+
const skipped = skippedTotals(runs);
|
|
600
|
+
const skippedN = skipped.reduce((a, [, x]) => a + x, 0);
|
|
601
|
+
const provs = checkedProviders(runs);
|
|
602
|
+
return `<div class="stack">
|
|
603
|
+
<div class="metrics">${["broken", "risk", "warning", "ok"].map(metric).join("")}</div>
|
|
604
|
+
<div class="ov-grid">${summaryCard(m, explained, problems)}
|
|
605
|
+
<div class="stack">
|
|
606
|
+
${runs.length ? `<div class="card"><div class="card-h"><div class="card-t">${m.ui.tabJourneys}</div></div><div class="card-c tight"><div class="tscroll"><table class="dt"><thead><tr><th>${m.ui.journey}</th><th>${m.ui.stepsCol}</th><th>${m.ui.problemsCol}</th></tr></thead><tbody>${journeyRows.join("")}</tbody></table></div></div></div>` : ""}
|
|
607
|
+
${provs.length ? `<div class="card"><div class="card-h"><div class="card-t">${m.ui.scopeCard}</div></div><div class="card-c"><div class="scope">${provs.map((k) => `<span class="ptag p-${safeKey(k)}">${escapeHtml(platformLabel(k))}</span>`).join("")}</div>${skippedN ? `<p class="muted xs scope-skipped">${escapeHtml(m.ui.skippedLine(skipped.map(([v]) => v).join(", "), skippedN))}</p>` : ""}</div></div>` : ""}
|
|
608
|
+
</div></div></div>`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Each planned event × each journey: sent, not sent, or not expected there (or its provider not checked). */
|
|
612
|
+
function coverageCard(m, plan, runs) {
|
|
613
|
+
const head = `<div class="card-h"><div class="card-t">${m.ui.planCoverage}</div><div class="card-d">${m.ui.planCoverageLead}</div></div>`;
|
|
614
|
+
if (!plan?.events?.length || !runs.length) return `<div class="card">${head}<div class="card-c"><p class="muted">${m.ui.noPlan}</p></div></div>`;
|
|
615
|
+
const events = plan.events.map((e) => ({ ...e, platform: String(e.platform ?? "").toLowerCase() }));
|
|
616
|
+
const checks = (r, e) => !r.providers?.length || r.providers.includes(e.platform);
|
|
617
|
+
const body = events
|
|
618
|
+
.map((e) => {
|
|
619
|
+
const cells = runs.map((r) => {
|
|
620
|
+
if (!checks(r, e) || (e.journeys && !e.journeys.includes(r.journeyId))) return '<td class="c o">–</td>';
|
|
621
|
+
return r.hits.some((h) => h.platform === e.platform && h.name === e.name) ? '<td class="c y">✓</td>' : '<td class="c x">✗</td>';
|
|
515
622
|
});
|
|
516
|
-
|
|
623
|
+
const unchecked = !runs.some((r) => checks(r, e));
|
|
624
|
+
return `<tr><td><span class="ptag p-${safeKey(e.platform)}">${escapeHtml(platformLabel(e.platform))}</span> <code>${escapeHtml(e.name)}</code>${unchecked ? ` ${badge("muted", m.ui.notChecked)}` : ""}</td>${cells.join("")}</tr>`;
|
|
517
625
|
})
|
|
518
626
|
.join("");
|
|
627
|
+
return `<div class="card">${head}<div class="card-c"><div class="tscroll"><table class="dt cov"><thead><tr><th>${m.ui.event}</th>${runs.map((r) => `<th>${escapeHtml(r.journeyId)} · ${escapeHtml(r.viewport)}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div><p class="muted xs legend">${m.ui.coverageLegend}</p></div></div>`;
|
|
519
628
|
}
|
|
520
629
|
|
|
521
|
-
function
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
630
|
+
function providersCard(m, runs) {
|
|
631
|
+
const provs = checkedProviders(runs);
|
|
632
|
+
const skipped = skippedTotals(runs);
|
|
633
|
+
if (!provs.length && !skipped.length) return "";
|
|
634
|
+
const calls = runs.flatMap((r) => r.network ?? []);
|
|
635
|
+
const rows = [
|
|
636
|
+
...provs.map((k) => [platformLabel(k), badge("ok", m.ui.checked), calls.filter((c) => callProvider(c) === k).length]),
|
|
637
|
+
...skipped.map(([v, n]) => [v, badge("muted", m.ui.outOfScopeStatus), n]),
|
|
638
|
+
];
|
|
639
|
+
return `<div class="card"><div class="card-h"><div class="card-t">${m.ui.providers}</div></div><div class="card-c tight"><div class="tscroll"><table class="dt"><thead><tr><th>${m.ui.provider}</th><th>${m.ui.status}</th><th>${m.ui.requestsCol}</th></tr></thead><tbody>${rows.map(([n, s, c]) => `<tr><td>${escapeHtml(n)}</td><td>${s}</td><td class="num">${c}</td></tr>`).join("")}</tbody></table></div></div></div>`;
|
|
526
640
|
}
|
|
527
641
|
|
|
528
|
-
/**
|
|
529
|
-
function
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
642
|
+
/** Steps a problem finding points at; they get a dot in the step list, and the journey opens on the first one. */
|
|
643
|
+
function flaggedSteps(runs, rows) {
|
|
644
|
+
const out = new Set();
|
|
645
|
+
for (const f of rows) {
|
|
646
|
+
if (!(f.severity in SEVERITY_RANK)) continue;
|
|
647
|
+
for (const w of f.where) {
|
|
648
|
+
const t = whereTarget(runs, w, f);
|
|
649
|
+
if (t?.step) out.add(stepAnchor(t.ri, t.step.index));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return out;
|
|
537
653
|
}
|
|
538
654
|
|
|
539
655
|
/** Filter rules for the platform keys in this report: picking a source hides the other rows. */
|
|
@@ -541,254 +657,298 @@ function filterCss(runs) {
|
|
|
541
657
|
const keys = new Set(PLATFORM_ORDER);
|
|
542
658
|
for (const r of runs) {
|
|
543
659
|
for (const h of r.hits) keys.add(safeKey(h.platform));
|
|
544
|
-
for (const c of r.network ?? []) keys.add(
|
|
660
|
+
for (const c of r.network ?? []) keys.add(callProvider(c));
|
|
545
661
|
}
|
|
546
662
|
return `${[...keys].map((k) => `.filter>.f-${k}:checked~.evlist>li:not([data-p="${k}"])`).join(",")}{display:none}`;
|
|
547
663
|
}
|
|
548
664
|
|
|
665
|
+
// The tabs, journeys and steps are switched by the URL hash (:target and :has()), so links work across tabs and every view can be shared.
|
|
666
|
+
function navCss(runs) {
|
|
667
|
+
const top = ["overview", "findings", "journeys", "scope"].map((t) => `body:has(#${t}:target,#${t} :target) .toptabs a[href="#${t}"]`).join(",");
|
|
668
|
+
const out = [`${top}{color:var(--fg);border-bottom-color:var(--accent)}`];
|
|
669
|
+
if (runs.length) {
|
|
670
|
+
out.push(`${runs.map((_, ri) => `body:has(#j${ri}:target,#j${ri} :target) .jtabs a[href="#j${ri}"]`).join(",")}{background:var(--bg);color:var(--fg);box-shadow:var(--shadow-sm)}`);
|
|
671
|
+
const steps = runs.flatMap((r, ri) => r.steps.filter((s) => s.index > 0).map((s) => `body:has(#${stepAnchor(ri, s.index)}:target) a[data-s="${stepAnchor(ri, s.index)}"]`));
|
|
672
|
+
if (steps.length) out.push(`${steps.join(",")}{background:var(--accent-bg);color:var(--fg)}`);
|
|
673
|
+
}
|
|
674
|
+
const sev = ["all", ...FINDING_SEVERITIES];
|
|
675
|
+
out.push(`${sev.map((s) => `#fs-${s}:checked~.toolbar label[for=fs-${s}]`).join(",")}{background:var(--bg);color:var(--fg);box-shadow:var(--shadow-sm)}`);
|
|
676
|
+
out.push(`${sev.map((s) => `#fs-${s}:focus-visible~.toolbar label[for=fs-${s}]`).join(",")}{outline:2px solid var(--accent)}`);
|
|
677
|
+
out.push(`${FINDING_SEVERITIES.map((s) => `#fs-${s}:checked~.flist>li:not([data-sev=${s}])`).join(",")}{display:none}`);
|
|
678
|
+
return out.join("\n");
|
|
679
|
+
}
|
|
680
|
+
|
|
549
681
|
const CSS = `
|
|
550
682
|
:root{
|
|
551
|
-
--bg:#
|
|
552
|
-
--accent:#4F8EF7;--accent-
|
|
553
|
-
--danger:#C5221F;--danger-
|
|
554
|
-
--
|
|
555
|
-
--
|
|
683
|
+
--bg:#FFFFFF;--page:#F7F9FC;--fg:#0A0E1A;--muted:#5A6480;--muted-bg:#F1F4F9;--border:#E3E7EF;--input:#E3E7EF;
|
|
684
|
+
--accent:#4F8EF7;--accent-fg:#2F6FDB;--accent-bg:#EBF1FE;--orange:#FF6D33;
|
|
685
|
+
--danger:#C5221F;--danger-bg:#FCE8E6;--warn:#A16207;--warn-bg:#FEF9C3;--warn-dot:#EAB308;--risk:#C2410C;--risk-bg:#FFEDD5;--danger-dot:#DC2626;--ok:#137333;--ok-bg:#E6F4EA;
|
|
686
|
+
--shadow-sm:0 1px 2px rgba(10,14,26,.06);--radius:6px;
|
|
687
|
+
--font-sans:'Google Sans',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;--font-mono:'Google Sans Code',ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
556
688
|
@media (prefers-color-scheme:dark){:root{
|
|
557
|
-
--bg:#0A0E1A;--
|
|
558
|
-
--accent
|
|
559
|
-
--danger:#F28B82;--danger-soft:#3A1F1D;--warn:#FDBA74;--warn-soft:#33241A;--ok:#81C995;--ok-soft:#16301F}}
|
|
689
|
+
--bg:#141927;--page:#0A0E1A;--fg:#F0F4FF;--muted:#8892AA;--muted-bg:#182035;--border:#1E2640;--input:#26304D;
|
|
690
|
+
--accent-fg:#7EAAF9;--accent-bg:#1A2F5C;--danger:#F28B82;--danger-bg:#3A1F1D;--warn:#FACC15;--warn-bg:#332B0A;--warn-dot:#FACC15;--risk:#FB923C;--risk-bg:#3A2312;--danger-dot:#F87171;--ok:#81C995;--ok-bg:#16301F;--shadow-sm:none}}
|
|
560
691
|
*{box-sizing:border-box}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
.
|
|
572
|
-
.
|
|
573
|
-
.
|
|
574
|
-
.
|
|
575
|
-
.
|
|
576
|
-
.
|
|
577
|
-
.
|
|
578
|
-
.
|
|
579
|
-
.
|
|
580
|
-
.
|
|
581
|
-
.
|
|
582
|
-
.
|
|
583
|
-
.
|
|
584
|
-
.
|
|
585
|
-
.
|
|
586
|
-
|
|
587
|
-
.
|
|
588
|
-
.
|
|
589
|
-
|
|
590
|
-
.
|
|
591
|
-
.
|
|
592
|
-
.
|
|
593
|
-
.
|
|
594
|
-
.
|
|
595
|
-
.
|
|
596
|
-
.
|
|
597
|
-
.
|
|
598
|
-
.
|
|
599
|
-
.
|
|
600
|
-
.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
.
|
|
604
|
-
.
|
|
605
|
-
.
|
|
606
|
-
.
|
|
607
|
-
.
|
|
608
|
-
summary{
|
|
609
|
-
|
|
610
|
-
.
|
|
611
|
-
.
|
|
612
|
-
.
|
|
613
|
-
.
|
|
614
|
-
.
|
|
615
|
-
.
|
|
616
|
-
.
|
|
617
|
-
.
|
|
618
|
-
.
|
|
619
|
-
.
|
|
620
|
-
.
|
|
621
|
-
.
|
|
622
|
-
.
|
|
623
|
-
.
|
|
624
|
-
.
|
|
625
|
-
.
|
|
626
|
-
.
|
|
627
|
-
.
|
|
628
|
-
.
|
|
629
|
-
.
|
|
630
|
-
.
|
|
631
|
-
.
|
|
632
|
-
.
|
|
633
|
-
.
|
|
634
|
-
.
|
|
635
|
-
.
|
|
636
|
-
.
|
|
637
|
-
.
|
|
638
|
-
.
|
|
639
|
-
.
|
|
640
|
-
.
|
|
641
|
-
.
|
|
642
|
-
.
|
|
643
|
-
.
|
|
644
|
-
.
|
|
645
|
-
.
|
|
646
|
-
.
|
|
647
|
-
.
|
|
648
|
-
.
|
|
649
|
-
.
|
|
650
|
-
.
|
|
651
|
-
.
|
|
652
|
-
.
|
|
653
|
-
.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
.
|
|
657
|
-
.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
.
|
|
662
|
-
.
|
|
663
|
-
.
|
|
664
|
-
.
|
|
665
|
-
.
|
|
666
|
-
.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
.
|
|
673
|
-
.
|
|
674
|
-
.
|
|
675
|
-
.
|
|
676
|
-
.
|
|
677
|
-
.
|
|
678
|
-
.
|
|
679
|
-
.
|
|
680
|
-
.
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
.
|
|
687
|
-
.
|
|
688
|
-
.
|
|
689
|
-
.
|
|
690
|
-
.
|
|
691
|
-
.
|
|
692
|
-
.
|
|
693
|
-
.
|
|
694
|
-
.
|
|
695
|
-
.
|
|
696
|
-
.
|
|
697
|
-
.
|
|
698
|
-
.
|
|
699
|
-
.
|
|
700
|
-
.
|
|
701
|
-
.
|
|
702
|
-
.
|
|
703
|
-
.fix-steps{margin:0;padding-left:20px;display:grid;gap:6px;font-size:.9375rem}
|
|
704
|
-
.sevgroup{margin:24px 0 8px;font:500 .75rem var(--font-mono);text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
|
|
705
|
-
.sevgroup+.findings{margin-top:0}
|
|
706
|
-
.step-body.v2{padding:0;overflow:visible}
|
|
707
|
-
.steptabs{border-top:0;border-radius:var(--radius-lg)}
|
|
708
|
-
.steptabs>label{font-size:.875rem;padding:12px 16px}
|
|
709
|
-
.steptabs>.panel{padding:12px}
|
|
692
|
+
html{scroll-padding-top:64px}
|
|
693
|
+
body{margin:0;min-height:100vh;display:flex;flex-direction:column;background:var(--page);color:var(--fg);font:400 14px/1.6 var(--font-sans);-webkit-font-smoothing:antialiased}
|
|
694
|
+
a{color:var(--accent-fg);text-decoration:none}
|
|
695
|
+
h1,h2,h3,h4,h5,h6{margin:0;font-weight:500;line-height:1.3}
|
|
696
|
+
h1{font-size:24px;letter-spacing:-.02em;overflow-wrap:anywhere}h2,h3{font-size:14px}
|
|
697
|
+
p{margin:0}
|
|
698
|
+
strong{font-weight:600;color:var(--fg)}
|
|
699
|
+
code,.mono,.time,pre{font-family:var(--font-mono)}
|
|
700
|
+
code{font-size:12px;background:var(--muted-bg);padding:1px 5px;border-radius:4px;overflow-wrap:anywhere}
|
|
701
|
+
.muted{color:var(--muted)}.xs{font-size:12px}
|
|
702
|
+
.label{font-size:12px;color:var(--muted);margin:0 0 6px}
|
|
703
|
+
.wrap{width:100%;max-width:1120px;margin:0 auto;padding:0 16px}
|
|
704
|
+
.topbar{background:var(--bg);border-bottom:1px solid var(--border)}
|
|
705
|
+
.topbar .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;height:56px}
|
|
706
|
+
.product-logo img{display:block;height:26px;width:auto}
|
|
707
|
+
.byline{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}.byline:hover{opacity:.85}
|
|
708
|
+
.logo-h img{display:block;height:18px;width:auto}
|
|
709
|
+
.title{margin-top:28px}
|
|
710
|
+
.eyebrow{font:500 12px var(--font-mono);color:var(--accent-fg);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
|
|
711
|
+
.title .meta{margin-top:4px;font-size:12px;color:var(--muted)}
|
|
712
|
+
.toptabs{position:sticky;top:0;z-index:10;background:var(--page);border-bottom:1px solid var(--border);margin-top:16px}
|
|
713
|
+
.toptabs .wrap{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none}
|
|
714
|
+
.toptabs a{display:inline-flex;align-items:center;gap:6px;padding:12px 10px;color:var(--muted);font-weight:500;white-space:nowrap;border-bottom:2px solid transparent;margin-bottom:-1px}
|
|
715
|
+
.toptabs a:hover{color:var(--fg)}
|
|
716
|
+
.n{font:500 12px/1.4 var(--font-mono);background:var(--muted-bg);color:var(--muted);padding:0 6px;border-radius:4px}
|
|
717
|
+
body:not(:has(:target)) .toptabs a[href="#overview"]{color:var(--fg);border-bottom-color:var(--accent)}
|
|
718
|
+
.tabpanel{display:none;padding:24px 0 64px}
|
|
719
|
+
.tabpanel:target,.tabpanel:has(:target){display:block}
|
|
720
|
+
body:not(:has(.tabpanel:target,.tabpanel :target)) #overview{display:block}
|
|
721
|
+
.card{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm)}
|
|
722
|
+
.card-h{padding:16px 20px 0}.card-c{padding:16px 20px 20px}.card-c.tight{padding:8px}
|
|
723
|
+
.card-t{font-weight:500}.card-d{font-size:12px;color:var(--muted);margin-top:2px}
|
|
724
|
+
.badge{display:inline-flex;align-items:center;font:500 12px/1 var(--font-sans);padding:4px 8px;border-radius:4px;white-space:nowrap;background:var(--muted-bg);color:var(--muted)}
|
|
725
|
+
.badge.broken{background:var(--danger-bg);color:var(--danger)}.badge.risk{background:var(--risk-bg);color:var(--risk)}.badge.warning{background:var(--warn-bg);color:var(--warn)}.badge.ok{background:var(--ok-bg);color:var(--ok)}.badge.info{background:var(--accent-bg);color:var(--accent-fg)}
|
|
726
|
+
.stack{display:grid;gap:16px}
|
|
727
|
+
.metrics{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}
|
|
728
|
+
.metric{padding:16px;border-top:3px solid var(--border)}
|
|
729
|
+
.metric .k{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
730
|
+
.metric .k::before{content:"";width:8px;height:8px;border-radius:2px;background:var(--muted)}
|
|
731
|
+
.metric.broken{border-top-color:var(--danger-dot)}.metric.broken .k::before{background:var(--danger-dot)}.metric.broken .v{color:var(--danger)}
|
|
732
|
+
.metric.risk{border-top-color:var(--risk)}.metric.risk .k::before{background:var(--risk)}.metric.risk .v{color:var(--risk)}
|
|
733
|
+
.metric.warning{border-top-color:var(--warn-dot)}.metric.warning .k::before{background:var(--warn-dot)}.metric.warning .v{color:var(--warn)}
|
|
734
|
+
.metric.ok{border-top-color:var(--ok)}.metric.ok .k::before{background:var(--ok)}
|
|
735
|
+
.metric .v{font:600 24px/1.3 var(--font-mono);margin-top:4px}
|
|
736
|
+
.headline{font-weight:500}
|
|
737
|
+
.summary-list{list-style:none;margin:0;padding:0;display:grid;gap:10px}
|
|
738
|
+
.summary-list li{display:grid;grid-template-columns:72px 1fr;gap:8px;align-items:baseline}
|
|
739
|
+
.summary-list .badge{justify-self:start}
|
|
740
|
+
.also{margin:0;padding-left:18px;display:grid;gap:6px}
|
|
741
|
+
.scope{display:flex;flex-wrap:wrap;gap:6px 8px;align-items:center;font-size:12px;color:var(--muted)}
|
|
742
|
+
.scope-skipped,.legend{margin-top:8px}
|
|
743
|
+
table.dt{width:100%;border-collapse:collapse}
|
|
744
|
+
table.dt th,table.dt td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:middle}
|
|
745
|
+
table.dt th{font-weight:500;color:var(--muted);font-size:12px}
|
|
746
|
+
table.dt tr:last-child td{border-bottom:0}
|
|
747
|
+
table.dt td.num{font-family:var(--font-mono)}
|
|
748
|
+
table.dt td.num.bad{color:var(--danger);font-weight:600}
|
|
749
|
+
table.dt a{color:var(--fg)}table.dt a:hover{color:var(--accent-fg)}
|
|
750
|
+
.tscroll{overflow-x:auto}
|
|
751
|
+
.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between;margin-bottom:12px}
|
|
752
|
+
.sevfilter{display:inline-flex;flex-wrap:wrap;gap:2px;padding:3px;background:var(--muted-bg);border-radius:var(--radius)}
|
|
753
|
+
.fsr{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
|
754
|
+
.sevfilter label{padding:4px 10px;border-radius:4px;font-weight:500;font-size:12px;color:var(--muted);cursor:pointer}
|
|
755
|
+
.filter-host{position:relative}
|
|
756
|
+
.flist{list-style:none;margin:0;padding:0;display:grid;gap:8px}
|
|
757
|
+
.finding{--sev:var(--warn-dot);--sev-fg:var(--warn);--sev-bg:var(--warn-bg);background:var(--bg);border:1px solid var(--border);border-left:4px solid var(--sev);border-radius:var(--radius);box-shadow:var(--shadow-sm)}
|
|
758
|
+
.finding.sev-broken{--sev:var(--danger-dot);--sev-fg:var(--danger);--sev-bg:var(--danger-bg)}
|
|
759
|
+
.finding.sev-risk{--sev:var(--risk);--sev-fg:var(--risk);--sev-bg:var(--risk-bg)}
|
|
760
|
+
.finding.sev-info{--sev:var(--accent);--sev-fg:var(--accent-fg);--sev-bg:var(--accent-bg)}
|
|
761
|
+
.finding>details>summary{display:flex;align-items:center;gap:12px;padding:14px 16px}
|
|
762
|
+
.finding>details[open]>summary{background:linear-gradient(90deg,var(--sev-bg),transparent 70%)}
|
|
763
|
+
.sev-icon{display:inline-grid;place-items:center;width:32px;height:32px;border-radius:var(--radius);background:var(--sev-bg);color:var(--sev-fg);flex:none}
|
|
764
|
+
.fhead{flex:1;min-width:0;display:grid;gap:4px}
|
|
765
|
+
.ftitle{color:var(--fg);overflow-wrap:anywhere}
|
|
766
|
+
.fmeta{display:flex;flex-wrap:wrap;gap:4px 12px;font-size:12px;color:var(--muted)}
|
|
767
|
+
.finding summary .chev{margin-left:4px}
|
|
768
|
+
.fbody{padding:16px;border-top:1px solid var(--border)}
|
|
769
|
+
.impact{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;background:var(--sev-bg);color:var(--fg);border-radius:var(--radius)}
|
|
770
|
+
.impact svg{color:var(--sev-fg);flex:none;margin-top:3px}
|
|
771
|
+
.impact-l{display:block;font-size:12px;color:var(--sev-fg);margin-bottom:2px}
|
|
772
|
+
.fcols{display:grid;gap:8px 32px;margin-top:4px}
|
|
773
|
+
.fcol{min-width:0}
|
|
774
|
+
.sec-h{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin:16px 0 8px}
|
|
775
|
+
.sec-h svg{color:var(--accent-fg)}
|
|
776
|
+
.ev-table{border:1px solid var(--border);border-radius:var(--radius);border-collapse:separate;border-spacing:0;overflow:hidden}
|
|
777
|
+
.ev-table th{background:var(--muted-bg)}
|
|
778
|
+
.missing{display:inline-block;padding:0 6px;border-radius:4px;background:var(--danger-bg);color:var(--danger)}
|
|
779
|
+
.fix-steps{list-style:none;margin:0;padding:0;display:grid;gap:10px;counter-reset:fx}
|
|
780
|
+
.fix-steps li{display:grid;grid-template-columns:22px 1fr;gap:10px;counter-increment:fx}
|
|
781
|
+
.fix-steps li::before{content:counter(fx);display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--accent-bg);color:var(--accent-fg);font:500 12px var(--font-mono)}
|
|
782
|
+
.where{display:flex;flex-wrap:wrap;gap:6px}
|
|
783
|
+
.chip{display:inline-flex;align-items:center;gap:8px;padding:4px 10px 4px 4px;border:1px solid var(--border);border-radius:var(--radius);color:var(--fg);font-size:12px;background:var(--bg)}
|
|
784
|
+
span.chip{padding-left:10px}
|
|
785
|
+
a.chip:hover{border-color:var(--accent);color:var(--fg)}
|
|
786
|
+
.chip-step{display:inline-grid;place-items:center;min-width:20px;height:20px;border-radius:4px;background:var(--accent);color:#fff;font:500 12px var(--font-mono)}
|
|
787
|
+
.sevbar{display:flex;height:8px;border-radius:4px;overflow:hidden;gap:2px;margin:0 0 6px}
|
|
788
|
+
.sevbar span{display:block}
|
|
789
|
+
.sevlegend{display:flex;flex-wrap:wrap;gap:4px 16px;font-size:12px;color:var(--muted);margin-bottom:16px}
|
|
790
|
+
.sevlegend i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px}
|
|
791
|
+
body>main{flex:1 0 auto;width:100%}
|
|
792
|
+
.footer{border-top:1px solid var(--border);background:var(--bg);margin-top:32px;flex:none}
|
|
793
|
+
.footer .wrap{display:grid;gap:16px;padding:32px 16px}
|
|
794
|
+
.footer p{font-size:12px;color:var(--muted)}
|
|
795
|
+
.footer a{color:var(--fg)}.footer a:hover{color:var(--accent-fg)}
|
|
796
|
+
.footer .f-links{display:flex;flex-wrap:wrap;gap:8px 20px;font-size:12px;margin-top:6px}
|
|
797
|
+
.footer .f-meta{font:12px var(--font-mono);color:var(--muted)}
|
|
798
|
+
.logo-link{display:inline-flex}.logo-s img{display:block;height:56px;width:auto}
|
|
799
|
+
.f-atd{display:inline-flex;align-items:center;gap:6px;vertical-align:-3px}.f-atd img{height:16px;width:auto}
|
|
800
|
+
.jtabs{display:inline-flex;flex-wrap:wrap;gap:2px;padding:3px;background:var(--muted-bg);border-radius:var(--radius);margin-bottom:16px}
|
|
801
|
+
.jtabs a{display:inline-flex;align-items:center;gap:8px;padding:5px 10px;border-radius:4px;color:var(--muted);font-weight:500}
|
|
802
|
+
.jtabs a:hover{color:var(--fg)}
|
|
803
|
+
#journeys:not(:has(.jpanel:target,.jpanel :target)) .jtabs a[href="#j0"]{background:var(--bg);color:var(--fg);box-shadow:var(--shadow-sm)}
|
|
804
|
+
.jpanel{display:none}
|
|
805
|
+
.jpanel:target,.jpanel:has(:target){display:block}
|
|
806
|
+
#journeys:not(:has(.jpanel:target,.jpanel :target)) .jpanel:first-of-type{display:block}
|
|
807
|
+
.jhead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:16px}
|
|
808
|
+
.jgrid2{display:grid;gap:16px;align-items:start}
|
|
809
|
+
.stepnav{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:6px}
|
|
810
|
+
.stepnav ol{list-style:none;margin:0;padding:0;display:grid;gap:2px}
|
|
811
|
+
.stepnav a{display:grid;grid-template-columns:24px minmax(0,1fr) auto auto;gap:6px;align-items:center;padding:7px 8px;border-radius:4px;color:var(--muted)}
|
|
812
|
+
.stepnav a:hover{background:var(--muted-bg);color:var(--fg)}
|
|
813
|
+
.jpanel:not(:has(.sdetail:target)) .stepnav a.def{background:var(--accent-bg);color:var(--fg)}
|
|
814
|
+
.sidx{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:4px;border:1px solid var(--border);font:500 12px var(--font-mono);color:var(--muted);background:var(--bg)}
|
|
815
|
+
.sl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:inherit}
|
|
816
|
+
.dot-flag{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--danger-dot)}
|
|
817
|
+
.sc{font:12px var(--font-mono);color:var(--muted);white-space:nowrap;display:inline-flex;align-items:center;gap:3px}
|
|
818
|
+
.sdetail{display:none;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm);scroll-margin-top:64px;min-width:0}
|
|
819
|
+
.sdetail:target{display:block}
|
|
820
|
+
.jpanel:not(:has(.sdetail:target)) .sdetail.default{display:block}
|
|
821
|
+
.shead{display:flex;gap:12px;align-items:flex-start;padding:16px}
|
|
822
|
+
.shead h3{overflow-wrap:anywhere}
|
|
823
|
+
.shead .sidx{background:var(--accent);border-color:var(--accent);color:#fff;width:26px;height:26px;flex:none}
|
|
824
|
+
.empty{color:var(--muted);padding:8px 0}
|
|
825
|
+
.tabs.seg{display:grid;grid-template-columns:repeat(3,auto) 1fr;padding:0 16px 16px}
|
|
826
|
+
.tabs.seg>input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
|
827
|
+
.tabs.seg>label{padding:6px 12px;font-weight:500;color:var(--muted);cursor:pointer;background:var(--muted-bg);white-space:nowrap;display:inline-flex;gap:6px;align-items:center}
|
|
828
|
+
.tabs.seg>label:nth-of-type(1){border-radius:var(--radius) 0 0 var(--radius)}
|
|
829
|
+
.tabs.seg>label:nth-of-type(3){border-radius:0 var(--radius) var(--radius) 0}
|
|
830
|
+
.tabs.seg>input:checked+label{background:var(--bg);color:var(--fg);box-shadow:inset 0 0 0 1px var(--border)}
|
|
831
|
+
.tabs.seg>input:focus-visible+label{outline:2px solid var(--accent)}
|
|
832
|
+
.tabs.seg>.panel{display:none;grid-column:1/-1;padding-top:12px;min-width:0}
|
|
833
|
+
.tabs.seg>input:nth-of-type(1):checked~.panel:nth-of-type(1),.tabs.seg>input:nth-of-type(2):checked~.panel:nth-of-type(2),.tabs.seg>input:nth-of-type(3):checked~.panel:nth-of-type(3){display:block}
|
|
710
834
|
.filter{display:flex;flex-wrap:wrap;gap:6px;position:relative}
|
|
711
835
|
.filter>input{position:absolute;opacity:0;width:1px;height:1px;pointer-events:none}
|
|
712
|
-
.filter>label{display:inline-flex;align-items:center;gap:6px;padding:
|
|
713
|
-
.filter>label:hover{
|
|
714
|
-
.filter>input:checked+label{
|
|
836
|
+
.filter>label{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border:1px solid var(--input);border-radius:var(--radius);font-size:12px;cursor:pointer;color:var(--muted);background:var(--bg)}
|
|
837
|
+
.filter>label:hover{color:var(--fg)}
|
|
838
|
+
.filter>input:checked+label{background:var(--fg);border-color:var(--fg);color:var(--bg)}
|
|
839
|
+
.filter>input:checked+label .n{background:transparent;color:inherit}
|
|
715
840
|
.filter>input:focus-visible+label{outline:2px solid var(--accent);outline-offset:2px}
|
|
716
|
-
.filter
|
|
717
|
-
.dot{width:8px;height:8px;border-radius:
|
|
718
|
-
.dot.p-ga4{background:var(--orange)}.dot.p-meta{background:var(--accent)}.dot.p-google_ads{background:var(--ok)}.dot.p-tiktok{background:var(--
|
|
719
|
-
.evlist{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:var(--radius
|
|
841
|
+
.filter>label:last-of-type{margin-bottom:4px}
|
|
842
|
+
.dot{display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--muted)}
|
|
843
|
+
.dot.p-ga4,.ptag.p-ga4::before{background:var(--orange)}.dot.p-meta,.ptag.p-meta::before{background:var(--accent)}.dot.p-google_ads,.ptag.p-google_ads::before{background:var(--ok)}.dot.p-tiktok,.ptag.p-tiktok::before{background:var(--fg)}
|
|
844
|
+
.evlist{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:var(--radius);flex-basis:100%;min-width:0}
|
|
720
845
|
.evlist>li+li{border-top:1px solid var(--border)}
|
|
846
|
+
summary{cursor:pointer;list-style:none}summary::-webkit-details-marker{display:none}
|
|
847
|
+
summary:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
848
|
+
.chev{flex:none;color:var(--muted);transition:transform .15s ease}details[open]>summary>.chev{transform:rotate(90deg)}
|
|
849
|
+
.event>summary{display:flex;align-items:center;gap:10px;padding:8px 12px;flex-wrap:wrap}
|
|
850
|
+
.event>summary:hover{background:var(--muted-bg)}
|
|
851
|
+
.event[class*=flag-]>summary .ev::after{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--warn-dot);margin-left:8px;vertical-align:2px}
|
|
852
|
+
.event.flag-broken>summary .ev::after{background:var(--danger-dot)}.event.flag-risk>summary .ev::after{background:var(--risk)}
|
|
853
|
+
.event .tag{display:none}
|
|
854
|
+
.ev{font:12px var(--font-mono);background:none;padding:0;overflow-wrap:anywhere}
|
|
855
|
+
.event .meta{font-size:12px;color:var(--muted)}
|
|
856
|
+
.event .time,.time{font-size:12px;color:var(--muted);margin-left:auto}
|
|
857
|
+
.ptag{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
858
|
+
.ptag::before{content:"";width:8px;height:8px;border-radius:2px;background:var(--muted)}
|
|
721
859
|
.event-body{padding:4px 12px 16px}
|
|
722
|
-
.flagnote{margin:4px 0 12px;padding:8px 12px;border-radius:var(--radius
|
|
723
|
-
.flagnote.broken{background:var(--danger-
|
|
724
|
-
.
|
|
725
|
-
.
|
|
860
|
+
.flagnote{margin:4px 0 12px;padding:8px 12px;border-radius:var(--radius);background:var(--warn-bg);color:var(--warn)}
|
|
861
|
+
.flagnote.broken{background:var(--danger-bg);color:var(--danger)}.flagnote.risk{background:var(--risk-bg);color:var(--risk)}
|
|
862
|
+
.event-body h5{font-size:12px;color:var(--muted);font-weight:400;margin:12px 0 6px}
|
|
863
|
+
.event-body h5:first-child{margin-top:4px}
|
|
864
|
+
h5 .n{margin-left:4px}
|
|
865
|
+
.ebgrid{display:grid;gap:12px}
|
|
866
|
+
.ebgrid>*{min-width:0}
|
|
867
|
+
.ptable{width:100%;border-collapse:collapse;font:12px/1.5 var(--font-mono)}
|
|
726
868
|
.ptable th,.ptable td{text-align:left;vertical-align:top;padding:5px 8px;border-bottom:1px solid var(--border)}
|
|
727
869
|
.ptable th{color:var(--muted);font-weight:400;white-space:nowrap;width:1%;min-width:120px}
|
|
728
870
|
.glossary th{min-width:170px}
|
|
729
871
|
.ptable td{overflow-wrap:anywhere}
|
|
730
872
|
.ptable tr:last-child>*{border-bottom:0}
|
|
731
|
-
details.ctx>summary .n,h5 .n{font:500 .6875rem var(--font-mono);background:var(--surface-2);padding:1px 5px;border-radius:var(--radius-sm)}
|
|
732
|
-
h5 .n{margin-left:4px}
|
|
733
873
|
.consents{display:flex;flex-wrap:wrap;gap:6px}
|
|
734
|
-
.
|
|
735
|
-
.
|
|
736
|
-
.
|
|
737
|
-
.
|
|
738
|
-
.
|
|
739
|
-
.
|
|
740
|
-
.
|
|
741
|
-
.
|
|
874
|
+
.consent{font-size:12px;padding:2px 6px;border-radius:4px}.consent.granted{background:var(--ok-bg);color:var(--ok)}.consent.denied{background:var(--warn-bg);color:var(--warn)}
|
|
875
|
+
.ebgrid aside{background:var(--muted-bg);border-radius:var(--radius);padding:10px 12px;align-self:start}
|
|
876
|
+
.ebgrid aside h5{margin-top:0!important}
|
|
877
|
+
.srclist{list-style:none;margin:0;padding:0;display:grid;gap:8px;font-size:12px}
|
|
878
|
+
.srclist li{display:grid;gap:2px;overflow-wrap:anywhere}.srclist .lbl{font-size:12px;color:var(--muted)}
|
|
879
|
+
.method{font:500 12px var(--font-mono);padding:0 5px;border-radius:4px;background:var(--accent-bg);color:var(--accent-fg)}
|
|
880
|
+
.status{color:var(--ok);font-family:var(--font-mono);font-size:12px}
|
|
881
|
+
.small{font-size:12px}
|
|
882
|
+
.item{border:1px solid var(--border);border-radius:var(--radius);padding:12px;margin-bottom:8px}
|
|
883
|
+
.item-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 10px;margin-bottom:10px}
|
|
884
|
+
.item-head b{font-weight:500}
|
|
885
|
+
.item-idx{font:12px var(--font-mono);color:var(--muted)}
|
|
886
|
+
.item-price{margin-left:auto;font-size:12px}
|
|
742
887
|
.grid4{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 16px;margin:0}
|
|
743
|
-
.grid4 dt{font
|
|
744
|
-
h6{margin:12px 0 6px;font:
|
|
888
|
+
.grid4 dt{font:12px var(--font-mono);color:var(--muted);overflow-wrap:anywhere}.grid4 dd{margin:0;font:12px var(--font-mono);overflow-wrap:anywhere}
|
|
889
|
+
h6{margin:12px 0 6px;font-size:12px;font-weight:400;color:var(--muted)}
|
|
745
890
|
.pk{margin-right:4px}
|
|
746
|
-
.q{position:relative;display:inline-grid;place-items:center;width:15px;height:15px;border:1px solid var(--
|
|
747
|
-
.q:hover,.q:focus{border-color:var(--accent);color:var(--accent-
|
|
748
|
-
.q:hover::after,.q:focus::after{content:attr(data-tip);position:absolute;left:0;top:calc(100% + 6px);z-index:5;width:260px;padding:8px 10px;background:var(--
|
|
891
|
+
.q{position:relative;display:inline-grid;place-items:center;width:15px;height:15px;border:1px solid var(--input);border-radius:50%;font:500 10px var(--font-sans);color:var(--muted);cursor:help;vertical-align:1px}
|
|
892
|
+
.q:hover,.q:focus{border-color:var(--accent);color:var(--accent-fg);outline:none}
|
|
893
|
+
.q:hover::after,.q:focus::after{content:attr(data-tip);position:absolute;left:0;top:calc(100% + 6px);z-index:5;width:260px;padding:8px 10px;background:var(--fg);color:var(--bg);font:400 12px/1.45 var(--font-sans);border-radius:var(--radius);white-space:normal;text-align:left}
|
|
749
894
|
tr.pr td{color:var(--muted)}
|
|
750
|
-
details.raw{margin-top:
|
|
751
|
-
|
|
895
|
+
details.raw{margin-top:10px}details.raw>summary{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
896
|
+
pre{margin:8px 0 0;font-size:12px;line-height:1.5;background:var(--muted-bg);border-radius:var(--radius);padding:10px 12px;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
897
|
+
.scope-note{margin-top:10px}
|
|
898
|
+
.cov td.c{text-align:center;font-family:var(--font-mono)}
|
|
899
|
+
.cov .y{color:var(--ok)}.cov .x{color:var(--danger);font-weight:600}.cov .o{color:var(--muted)}
|
|
752
900
|
@media (min-width:768px){
|
|
753
|
-
.
|
|
754
|
-
.
|
|
755
|
-
.
|
|
901
|
+
.wrap{padding:0 24px}
|
|
902
|
+
.metrics{grid-template-columns:repeat(4,1fr)}
|
|
903
|
+
.ov-grid{display:grid;grid-template-columns:2fr 1fr;gap:16px;align-items:start}
|
|
904
|
+
.fcols{grid-template-columns:1fr 1fr}
|
|
905
|
+
.footer .wrap{grid-template-columns:auto 1fr auto;align-items:center;padding:32px 24px}
|
|
906
|
+
.footer .wrap.plain{grid-template-columns:1fr auto}
|
|
907
|
+
.jgrid2{grid-template-columns:260px minmax(0,1fr)}
|
|
908
|
+
.stepnav{position:sticky;top:60px}
|
|
756
909
|
.ebgrid{grid-template-columns:2fr 1fr}
|
|
757
910
|
.grid4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
|
758
|
-
.
|
|
759
|
-
.fbody{padding:0 20px 20px 48px}
|
|
911
|
+
.event-body{padding:4px 16px 16px 38px}
|
|
760
912
|
}
|
|
761
|
-
@media
|
|
913
|
+
@media (max-width:767px){.ov-grid{display:grid;gap:16px}}
|
|
914
|
+
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
915
|
+
@media print{.toptabs,.jtabs,.stepnav,.filter>label,.filter>input,.tabs.seg>label,.sevfilter{display:none}.tabpanel,.jpanel,.sdetail,.tabs.seg>.panel{display:block!important}.jpanel{margin-bottom:32px}.sdetail{margin-bottom:16px;break-inside:avoid-page}details>*{display:block}}
|
|
762
916
|
`;
|
|
763
917
|
|
|
764
|
-
export function renderHtml({ title, generatedAt, findings, explained, runs, lang = "en", branding = true }) {
|
|
918
|
+
export function renderHtml({ title, generatedAt, findings, explained, runs, plan = null, lang = "en", branding = true }) {
|
|
765
919
|
const m = messages(lang);
|
|
766
920
|
const rows = attachExplanations(findings, explained);
|
|
767
921
|
const counts = summarize(rows);
|
|
768
|
-
const
|
|
769
|
-
const
|
|
922
|
+
const problems = rows.filter((f) => f.severity !== "ok").length;
|
|
923
|
+
const flagged = flaggedSteps(runs, rows);
|
|
924
|
+
const atd = `<a class="f-atd" href="https://github.com/analitikisler/ai-tag-debugger" rel="noopener noreferrer"><img src="${LOGOS["atd-icon"]}" alt="" height="16">AI Tag Debugger</a>`;
|
|
925
|
+
const footer = branding
|
|
926
|
+
? `<div class="wrap"><a href="https://analitikisler.com" class="logo-link" aria-label="analitikisler.com" rel="noopener noreferrer">${logo("stacked", "Analitik İşler", 56, "logo-s")}</a>
|
|
927
|
+
<div><p>${m.ui.credit(atd)}</p><div class="f-links"><a href="https://analitikisler.com" rel="noopener noreferrer">analitikisler.com</a><a href="https://github.com/analitikisler/ai-tag-debugger" rel="noopener noreferrer">GitHub</a></div></div>
|
|
928
|
+
<div class="f-meta">${escapeHtml(generatedAt)} · v${VERSION}</div></div>`
|
|
929
|
+
: `<div class="wrap plain"><div class="f-meta">AI Tag Debugger</div><div class="f-meta">${escapeHtml(generatedAt)} · v${VERSION}</div></div>`;
|
|
770
930
|
|
|
771
931
|
return `<!doctype html>
|
|
772
932
|
<html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
773
|
-
<title>${escapeHtml(title)}</title>
|
|
774
|
-
<style>${CSS}${filterCss(runs)}
|
|
775
|
-
|
|
776
|
-
<
|
|
933
|
+
<title>${escapeHtml(title)}</title><link rel="icon" type="image/png" href="${LOGOS.favicon}">
|
|
934
|
+
<style>${CSS}${filterCss(runs)}
|
|
935
|
+
${navCss(runs)}</style></head><body>
|
|
936
|
+
<div class="topbar"><div class="wrap"><span class="product-logo">${logo("atd-lockup", "AI Tag Debugger", 26, "logo-atd")}</span>${branding ? `<a href="https://analitikisler.com" class="byline" rel="noopener noreferrer"><span>${m.ui.by}</span>${logo("horizontal", "Analitik İşler", 18, "logo-h")}</a>` : ""}</div></div>
|
|
937
|
+
<header class="wrap title">
|
|
777
938
|
<div class="eyebrow">${m.ui.eyebrow}</div>
|
|
778
939
|
<h1>${escapeHtml(title)}</h1>
|
|
779
|
-
<div class="
|
|
940
|
+
<div class="meta">${escapeHtml(generatedAt)} · ${escapeHtml(m.ui.headerMeta(runs.length, checkedProviders(runs).length))} · ai-tag-debugger ${VERSION}</div>
|
|
780
941
|
</header>
|
|
781
|
-
<
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
<section
|
|
785
|
-
${
|
|
786
|
-
|
|
787
|
-
<section
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
</main></body></html>`;
|
|
942
|
+
<nav class="toptabs" aria-label="${m.ui.sections}"><div class="wrap">
|
|
943
|
+
<a href="#overview">${m.ui.tabOverview}</a><a href="#findings">${m.ui.findings} <span class="n">${problems}</span></a><a href="#journeys">${m.ui.tabJourneys} <span class="n">${runs.length}</span></a><a href="#scope">${m.ui.tabScope}</a></div></nav>
|
|
944
|
+
<main class="wrap">
|
|
945
|
+
<section class="tabpanel" id="overview" aria-label="${m.ui.tabOverview}">${overviewTab(m, findings, rows, counts, explained, runs)}</section>
|
|
946
|
+
<section class="tabpanel" id="findings" aria-label="${m.ui.findings}">${findingsTab(m, runs, rows)}</section>
|
|
947
|
+
<section class="tabpanel" id="journeys" aria-label="${m.ui.tabJourneys}">${journeysTab(m, lang, findings, runs, flagged)}</section>
|
|
948
|
+
<section class="tabpanel" id="scope" aria-label="${m.ui.tabScope}"><div class="stack">${coverageCard(m, plan, runs)}${providersCard(m, runs)}</div></section>
|
|
949
|
+
</main>
|
|
950
|
+
<footer class="footer">${footer}</footer>
|
|
951
|
+
</body></html>`;
|
|
792
952
|
}
|
|
793
953
|
|
|
794
954
|
const csvCell = (v) => {
|