@osqd/bothandlerjs 0.4.0 → 0.5.0
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/dist/cli.cjs +177 -21
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +177 -21
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/dashboard/client/feed.d.ts +16 -0
- package/dist/dashboard/client/format.d.ts +18 -0
- package/dist/dashboard/client/pager.d.ts +32 -0
- package/dist/dashboard/client/store.d.ts +60 -0
- package/dist/dashboard/client.generated.d.ts +1 -1
- package/dist/detectors/crawler-verification.d.ts +34 -1
- package/dist/detectors/index.d.ts +4 -1
- package/dist/detectors/known-bots.d.ts +49 -1
- package/dist/element/index.cjs +345 -39
- package/dist/element/index.cjs.map +1 -1
- package/dist/element/index.js +345 -39
- package/dist/element/index.js.map +1 -1
- package/dist/index.cjs +177 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +177 -21
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +1 -1
- package/docs/detection/signatures.md +36 -0
- package/docs/operations/dashboard.md +26 -0
- package/package.json +1 -1
package/dist/element/index.cjs
CHANGED
|
@@ -403,19 +403,28 @@ function clearFeed() {
|
|
|
403
403
|
state.actor = void 0;
|
|
404
404
|
state.bufferedWhilePaused = 0;
|
|
405
405
|
state.laggedDrops = 0;
|
|
406
|
+
resetPaging();
|
|
406
407
|
}
|
|
407
408
|
function setSearch(value) {
|
|
408
409
|
state.search = value;
|
|
409
410
|
state.terms = parseQuery(value);
|
|
411
|
+
resetPaging();
|
|
412
|
+
}
|
|
413
|
+
function resetPaging() {
|
|
414
|
+
state.feedPage = 0;
|
|
415
|
+
state.feedFrozen = void 0;
|
|
410
416
|
}
|
|
411
417
|
function textOf(row) {
|
|
412
418
|
if (row.text === void 0) row.text = searchableText(row.entry);
|
|
413
419
|
return row.text;
|
|
414
420
|
}
|
|
421
|
+
function sortRows() {
|
|
422
|
+
state.rows.sort((a, b) => a.entry.at - b.entry.at);
|
|
423
|
+
}
|
|
415
424
|
function matches(row) {
|
|
416
425
|
return matchesFilter(state.filter, row.entry) && matchesQuery(state.terms, row.entry, textOf(row));
|
|
417
426
|
}
|
|
418
|
-
function
|
|
427
|
+
function matchingRows(limit = Number.POSITIVE_INFINITY) {
|
|
419
428
|
const shown = [];
|
|
420
429
|
for (let i = state.rows.length - 1; i >= 0 && shown.length < limit; i--) {
|
|
421
430
|
const row = state.rows[i];
|
|
@@ -423,6 +432,22 @@ function visibleRows(limit = FEED_LIMIT) {
|
|
|
423
432
|
}
|
|
424
433
|
return shown;
|
|
425
434
|
}
|
|
435
|
+
function feedPage(size) {
|
|
436
|
+
const all = state.feedPage === 0 || state.feedFrozen === void 0 ? matchingRows() : state.feedFrozen;
|
|
437
|
+
const pages = Math.max(1, Math.ceil(all.length / size));
|
|
438
|
+
const page = Math.min(Math.max(0, state.feedPage), pages - 1);
|
|
439
|
+
if (page !== state.feedPage) state.feedPage = page;
|
|
440
|
+
return { rows: all.slice(page * size, page * size + size), page, pages, total: all.length };
|
|
441
|
+
}
|
|
442
|
+
function goToFeedPage(page) {
|
|
443
|
+
const next = Math.max(0, page);
|
|
444
|
+
if (next === 0) {
|
|
445
|
+
state.feedFrozen = void 0;
|
|
446
|
+
} else if (state.feedFrozen === void 0) {
|
|
447
|
+
state.feedFrozen = matchingRows();
|
|
448
|
+
}
|
|
449
|
+
state.feedPage = next;
|
|
450
|
+
}
|
|
426
451
|
function matchingCount() {
|
|
427
452
|
let count = 0;
|
|
428
453
|
for (const row of state.rows) if (matches(row)) count++;
|
|
@@ -462,14 +487,13 @@ function aggregate(rows) {
|
|
|
462
487
|
}
|
|
463
488
|
return totals;
|
|
464
489
|
}
|
|
465
|
-
var MAX_ROWS,
|
|
490
|
+
var MAX_ROWS, state;
|
|
466
491
|
var init_store = __esm({
|
|
467
492
|
"src/dashboard/client/store.ts"() {
|
|
468
493
|
"use strict";
|
|
469
494
|
init_query();
|
|
470
495
|
init_outcome();
|
|
471
496
|
MAX_ROWS = 1e3;
|
|
472
|
-
FEED_LIMIT = 300;
|
|
473
497
|
state = {
|
|
474
498
|
rows: [],
|
|
475
499
|
byId: /* @__PURE__ */ new Map(),
|
|
@@ -491,7 +515,13 @@ var init_store = __esm({
|
|
|
491
515
|
editorMode: "gui",
|
|
492
516
|
guardDirty: false,
|
|
493
517
|
bufferedWhilePaused: 0,
|
|
494
|
-
laggedDrops: 0
|
|
518
|
+
laggedDrops: 0,
|
|
519
|
+
feedPage: 0,
|
|
520
|
+
feedFrozen: void 0,
|
|
521
|
+
actorsPage: 0,
|
|
522
|
+
feedPageSize: 50,
|
|
523
|
+
actorsPageSize: 25,
|
|
524
|
+
caughtUp: 0
|
|
495
525
|
};
|
|
496
526
|
}
|
|
497
527
|
});
|
|
@@ -546,7 +576,18 @@ function rangeLabel(milliseconds) {
|
|
|
546
576
|
return minutes < 90 ? `${minutes} min` : `${Math.round(minutes / 60)}h`;
|
|
547
577
|
}
|
|
548
578
|
function clockTime(at) {
|
|
549
|
-
|
|
579
|
+
const when = new Date(at);
|
|
580
|
+
return `${pad(when.getHours())}:${pad(when.getMinutes())}:${pad(when.getSeconds())}`;
|
|
581
|
+
}
|
|
582
|
+
function clockDate(at) {
|
|
583
|
+
const when = new Date(at);
|
|
584
|
+
return `${pad(when.getDate())}-${pad(when.getMonth() + 1)}-${when.getFullYear()}`;
|
|
585
|
+
}
|
|
586
|
+
function clockStamp(at) {
|
|
587
|
+
return `${clockDate(at)} ${clockTime(at)}`;
|
|
588
|
+
}
|
|
589
|
+
function pad(value) {
|
|
590
|
+
return String(value).padStart(2, "0");
|
|
550
591
|
}
|
|
551
592
|
function windowLabel(count, oldestAt2, now) {
|
|
552
593
|
if (count === 0) return "this window \xB7 empty";
|
|
@@ -561,6 +602,68 @@ var init_format = __esm({
|
|
|
561
602
|
}
|
|
562
603
|
});
|
|
563
604
|
|
|
605
|
+
// src/dashboard/client/pager.ts
|
|
606
|
+
function renderPager(host, model, options) {
|
|
607
|
+
const signature = JSON.stringify([model.page, model.from, model.to, model.total, model.atStart, model.atEnd, model.held ?? "", options.withSize, model.size?.current]);
|
|
608
|
+
if (painted.get(host) === signature && host.childElementCount > 0) return;
|
|
609
|
+
painted.set(host, signature);
|
|
610
|
+
clear(host);
|
|
611
|
+
const steps = [
|
|
612
|
+
{ to: model.page - 1, glyph: "\u2039", label: "Previous page", disabled: model.atStart },
|
|
613
|
+
{ to: model.page + 1, glyph: "\u203A", label: "Next page", disabled: model.atEnd }
|
|
614
|
+
];
|
|
615
|
+
const [previous, next] = steps;
|
|
616
|
+
host.appendChild(stepButton(previous, model));
|
|
617
|
+
const range = model.total === void 0 ? `${n(model.from)}\u2013${n(model.to)}` : `${n(model.from)}\u2013${n(model.to)} of ${n(model.total)}`;
|
|
618
|
+
const where = el("span", "where", range);
|
|
619
|
+
where.setAttribute("aria-live", "polite");
|
|
620
|
+
host.appendChild(where);
|
|
621
|
+
host.appendChild(stepButton(next, model));
|
|
622
|
+
if (model.held !== void 0) {
|
|
623
|
+
const held = el("span", "held", model.held);
|
|
624
|
+
held.title = "New requests are still arriving and are still counted. They appear when you return to the first page.";
|
|
625
|
+
host.appendChild(held);
|
|
626
|
+
}
|
|
627
|
+
if (options.withSize && model.size !== void 0) {
|
|
628
|
+
const size = model.size;
|
|
629
|
+
const label2 = el("label", "size");
|
|
630
|
+
label2.appendChild(document.createTextNode("Per page"));
|
|
631
|
+
const select2 = document.createElement("select");
|
|
632
|
+
for (const choice of size.choices) {
|
|
633
|
+
const option = document.createElement("option");
|
|
634
|
+
option.value = String(choice);
|
|
635
|
+
option.textContent = String(choice);
|
|
636
|
+
option.selected = choice === size.current;
|
|
637
|
+
select2.appendChild(option);
|
|
638
|
+
}
|
|
639
|
+
select2.addEventListener("change", () => {
|
|
640
|
+
const chosen = Number(select2.value);
|
|
641
|
+
if (Number.isFinite(chosen) && chosen > 0) size.set(chosen);
|
|
642
|
+
});
|
|
643
|
+
label2.appendChild(select2);
|
|
644
|
+
host.appendChild(label2);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function stepButton(step, model) {
|
|
648
|
+
const button = el("button", "step", step.glyph);
|
|
649
|
+
const element = button;
|
|
650
|
+
element.type = "button";
|
|
651
|
+
element.disabled = step.disabled;
|
|
652
|
+
button.setAttribute("aria-label", step.label);
|
|
653
|
+
button.title = step.label;
|
|
654
|
+
button.addEventListener("click", () => model.go(Math.max(0, step.to)));
|
|
655
|
+
return button;
|
|
656
|
+
}
|
|
657
|
+
var painted;
|
|
658
|
+
var init_pager = __esm({
|
|
659
|
+
"src/dashboard/client/pager.ts"() {
|
|
660
|
+
"use strict";
|
|
661
|
+
init_dom();
|
|
662
|
+
init_format();
|
|
663
|
+
painted = /* @__PURE__ */ new WeakMap();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
564
667
|
// src/dashboard/client/replay.ts
|
|
565
668
|
function replayLine(entry) {
|
|
566
669
|
const headers = {};
|
|
@@ -751,7 +854,7 @@ function drawActor() {
|
|
|
751
854
|
["Engine sees", stats !== void 0 ? `${n(stats.requests)} requests, ${n(stats.distinctPaths)} distinct paths` : "\u2014"],
|
|
752
855
|
["Prior confirmations", stats !== void 0 ? n(stats.priorConfirmations) : "\u2014"],
|
|
753
856
|
["Holds clearance", stats !== void 0 ? stats.cleared ? "yes" : "no" : "\u2014"],
|
|
754
|
-
["First seen", stats !== void 0 ?
|
|
857
|
+
["First seen", stats !== void 0 ? clockStamp(stats.firstSeen) : "\u2014"],
|
|
755
858
|
["Mean gap", gaps.length > 0 ? `${Math.round(meanGap)}ms over ${n(gaps.length)} gaps` : "one request only"]
|
|
756
859
|
];
|
|
757
860
|
for (const [key, value] of rows) {
|
|
@@ -1830,6 +1933,13 @@ var init_policy = __esm({
|
|
|
1830
1933
|
|
|
1831
1934
|
// src/dashboard/client/feed.ts
|
|
1832
1935
|
function initFeed() {
|
|
1936
|
+
const loadThem = byId("feed-load-skipped");
|
|
1937
|
+
loadThem.addEventListener("click", () => {
|
|
1938
|
+
loadThem.disabled = true;
|
|
1939
|
+
void loadSkipped().finally(() => {
|
|
1940
|
+
loadThem.disabled = false;
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1833
1943
|
const filters = $("filters");
|
|
1834
1944
|
for (const [name, label2] of FILTERS) {
|
|
1835
1945
|
const button = el("button", null, label2);
|
|
@@ -1837,6 +1947,7 @@ function initFeed() {
|
|
|
1837
1947
|
button.dataset["filter"] = name;
|
|
1838
1948
|
button.addEventListener("click", () => {
|
|
1839
1949
|
state.filter = name;
|
|
1950
|
+
resetPaging();
|
|
1840
1951
|
for (const other of Array.from(filters.children)) other.setAttribute("aria-pressed", "false");
|
|
1841
1952
|
button.setAttribute("aria-pressed", "true");
|
|
1842
1953
|
app.syncUrl();
|
|
@@ -1853,7 +1964,7 @@ function initFeed() {
|
|
|
1853
1964
|
const exportShown = byId("feed-export");
|
|
1854
1965
|
exportShown.hidden = !SECTIONS.evidence;
|
|
1855
1966
|
exportShown.addEventListener("click", () => {
|
|
1856
|
-
const rows =
|
|
1967
|
+
const rows = matchingRows();
|
|
1857
1968
|
if (rows.length === 0) {
|
|
1858
1969
|
toast("warn", "Nothing to export", "No request in the window matches this filter.");
|
|
1859
1970
|
return;
|
|
@@ -1871,9 +1982,65 @@ function reflectFilterButtons() {
|
|
|
1871
1982
|
const search = byId("search");
|
|
1872
1983
|
if (search.value !== state.search) search.value = state.search;
|
|
1873
1984
|
}
|
|
1985
|
+
async function loadSkipped() {
|
|
1986
|
+
const before = state.rows.length;
|
|
1987
|
+
try {
|
|
1988
|
+
const body = await getJson("/api/feed");
|
|
1989
|
+
for (const entry of body.entries) ingest(entry);
|
|
1990
|
+
sortRows();
|
|
1991
|
+
} catch {
|
|
1992
|
+
toast("bad", "Could not load them", "The dashboard did not answer. The entries are still in the window; try again.");
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
state.caughtUp = (state.snapshot?.skipped ?? 0) + state.laggedDrops;
|
|
1996
|
+
const added = state.rows.length - before;
|
|
1997
|
+
resetPaging();
|
|
1998
|
+
app.drawNow();
|
|
1999
|
+
toast(
|
|
2000
|
+
added > 0 ? "ok" : "warn",
|
|
2001
|
+
added > 0 ? `Loaded ${n(added)}` : "Nothing left to load",
|
|
2002
|
+
added > 0 ? "They are in the feed now, in the order they happened." : "The window no longer holds them; the ring had already rotated past."
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
function drawPager(paged) {
|
|
2006
|
+
const size = state.feedPageSize;
|
|
2007
|
+
const hidden = paged.pages <= 1;
|
|
2008
|
+
const model = {
|
|
2009
|
+
page: paged.page,
|
|
2010
|
+
from: paged.page * size + 1,
|
|
2011
|
+
to: Math.min(paged.total, (paged.page + 1) * size),
|
|
2012
|
+
total: paged.total,
|
|
2013
|
+
atStart: paged.page === 0,
|
|
2014
|
+
atEnd: paged.page >= paged.pages - 1,
|
|
2015
|
+
...paged.page > 0 ? { held: "held while you read" } : {},
|
|
2016
|
+
go: (page) => {
|
|
2017
|
+
goToFeedPage(page);
|
|
2018
|
+
app.drawNow();
|
|
2019
|
+
},
|
|
2020
|
+
size: {
|
|
2021
|
+
current: size,
|
|
2022
|
+
choices: FEED_PAGE_SIZES,
|
|
2023
|
+
set: (next) => {
|
|
2024
|
+
state.feedPageSize = next;
|
|
2025
|
+
resetPaging();
|
|
2026
|
+
app.drawNow();
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
for (const [id, withSize] of [
|
|
2031
|
+
["feed-pager-top", true],
|
|
2032
|
+
["feed-pager", false]
|
|
2033
|
+
]) {
|
|
2034
|
+
const host = $(id);
|
|
2035
|
+
host.hidden = hidden;
|
|
2036
|
+
if (hidden) clear(host);
|
|
2037
|
+
else renderPager(host, model, { withSize });
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
1874
2040
|
function drawFeed() {
|
|
1875
2041
|
const body = byId("rows");
|
|
1876
|
-
const
|
|
2042
|
+
const paged = feedPage(state.feedPageSize);
|
|
2043
|
+
const shown = paged.rows;
|
|
1877
2044
|
let index = 0;
|
|
1878
2045
|
const place = (node) => {
|
|
1879
2046
|
const current = body.childNodes[index] ?? null;
|
|
@@ -1904,11 +2071,13 @@ function drawFeed() {
|
|
|
1904
2071
|
const total = state.rows.length;
|
|
1905
2072
|
const matching = matchingCount();
|
|
1906
2073
|
$("empty").hidden = total > 0;
|
|
1907
|
-
$("feed-count").textContent = matching === total ? `${n(total)} in this window` : `${n(matching)} of ${n(total)}
|
|
1908
|
-
|
|
2074
|
+
$("feed-count").textContent = matching === total ? `${n(total)} in this window` : `${n(matching)} of ${n(total)}`;
|
|
2075
|
+
drawPager(paged);
|
|
2076
|
+
const skipped = Math.max(0, (state.snapshot?.skipped ?? 0) + state.laggedDrops - state.caughtUp);
|
|
1909
2077
|
const note = $("feed-skipped");
|
|
1910
2078
|
note.hidden = skipped === 0;
|
|
1911
2079
|
note.textContent = `${n(skipped)} not streamed`;
|
|
2080
|
+
byId("feed-load-skipped").hidden = skipped === 0;
|
|
1912
2081
|
note.title = state.laggedDrops > 0 ? `${n(state.laggedDrops)} were skipped because this connection could not keep up, and the rest by the rate cap. All of them are still in the window, the preview and the export.` : "Entries the rate cap kept off this stream. They are still in the window, the preview and the export \u2014 raise maxEventsPerSecond to see them live.";
|
|
1913
2082
|
}
|
|
1914
2083
|
function resetFeedCache() {
|
|
@@ -2115,12 +2284,14 @@ function showText(text) {
|
|
|
2115
2284
|
selection.addRange(range);
|
|
2116
2285
|
}
|
|
2117
2286
|
}
|
|
2118
|
-
var FILTERS, rendered;
|
|
2287
|
+
var FILTERS, rendered, FEED_PAGE_SIZES;
|
|
2119
2288
|
var init_feed = __esm({
|
|
2120
2289
|
"src/dashboard/client/feed.ts"() {
|
|
2121
2290
|
"use strict";
|
|
2122
2291
|
init_dom();
|
|
2123
2292
|
init_store();
|
|
2293
|
+
init_api();
|
|
2294
|
+
init_pager();
|
|
2124
2295
|
init_boot();
|
|
2125
2296
|
init_app();
|
|
2126
2297
|
init_format();
|
|
@@ -2139,6 +2310,7 @@ var init_feed = __esm({
|
|
|
2139
2310
|
["allow", "Served"]
|
|
2140
2311
|
];
|
|
2141
2312
|
rendered = /* @__PURE__ */ new Map();
|
|
2313
|
+
FEED_PAGE_SIZES = [25, 50, 100, 200];
|
|
2142
2314
|
}
|
|
2143
2315
|
});
|
|
2144
2316
|
|
|
@@ -2243,27 +2415,34 @@ function drawTiles(force = false) {
|
|
|
2243
2415
|
const total = metrics.requests;
|
|
2244
2416
|
const unremarkable = metrics.verdicts.unknown + metrics.verdicts.human;
|
|
2245
2417
|
const provenBotCount = provenBots(metrics.verdicts);
|
|
2418
|
+
const actors = SECTIONS.registry ? { tab: "actors", label: "Actors" } : void 0;
|
|
2246
2419
|
const tiles = [
|
|
2247
|
-
["", n(total), "Requests", "since start"],
|
|
2248
|
-
["proven", n(provenBotCount), "Proven bots", `${pct(provenBotCount, total)} of traffic
|
|
2249
|
-
["warn", n(metrics.verdicts["suspected-bot"]), "Suspected", "never denied
|
|
2250
|
-
["", n(unremarkable), "Unremarkable", `${pct(unremarkable, total)} of traffic
|
|
2251
|
-
["warn", n(metrics.downgrades), "Guard stops", metrics.downgrades > 0 ? "
|
|
2252
|
-
["crit", n(denied), "Denied", `${pct(denied, total)} of traffic
|
|
2420
|
+
["", n(total), "Requests", "since start", void 0],
|
|
2421
|
+
["proven", n(provenBotCount), "Proven bots", `${pct(provenBotCount, total)} of traffic`, void 0],
|
|
2422
|
+
["warn", n(metrics.verdicts["suspected-bot"]), "Suspected", "never denied alone", void 0],
|
|
2423
|
+
["", n(unremarkable), "Unremarkable", `${pct(unremarkable, total)} of traffic`, void 0],
|
|
2424
|
+
["warn", n(metrics.downgrades), "Guard stops", metrics.downgrades > 0 ? "a rule over-reached" : "no rule overreached", void 0],
|
|
2425
|
+
["crit", n(denied), "Denied", `${pct(denied, total)} of traffic`, void 0],
|
|
2253
2426
|
[
|
|
2254
2427
|
"",
|
|
2255
2428
|
n(mitigated),
|
|
2256
2429
|
"Mitigated",
|
|
2257
|
-
metrics.challenges.issued > 0 ? `${n(metrics.challenges.solved)} of ${n(metrics.challenges.issued)} challenges solved` : "challenged
|
|
2430
|
+
metrics.challenges.issued > 0 ? `${n(metrics.challenges.solved)} of ${n(metrics.challenges.issued)} challenges solved` : "challenged or limited",
|
|
2431
|
+
void 0
|
|
2258
2432
|
],
|
|
2259
|
-
["good", n(served), "Served", `${pct(served, total)} of traffic
|
|
2260
|
-
["", n(metrics.actorsTracked), "Actors tracked", "in the registry now"]
|
|
2433
|
+
["good", n(served), "Served", `${pct(served, total)} of traffic`, void 0],
|
|
2434
|
+
["", n(metrics.actorsTracked), "Actors tracked", "in the registry now", actors]
|
|
2261
2435
|
];
|
|
2262
|
-
for (const [kind, value, key, sub] of tiles) {
|
|
2263
|
-
const tile = el("div", `tile ${kind}`);
|
|
2436
|
+
for (const [kind, value, key, sub, goes] of tiles) {
|
|
2437
|
+
const tile = goes === void 0 ? el("div", `tile ${kind}`) : el("button", `tile ${kind} go`);
|
|
2264
2438
|
tile.appendChild(el("div", "v tnum", value));
|
|
2265
2439
|
tile.appendChild(el("div", "k", key));
|
|
2266
2440
|
tile.appendChild(el("div", "s", sub));
|
|
2441
|
+
if (goes !== void 0) {
|
|
2442
|
+
tile.type = "button";
|
|
2443
|
+
tile.appendChild(el("span", "sr-only", `. Show the ${goes.label} screen`));
|
|
2444
|
+
tile.addEventListener("click", () => app.showTab(goes.tab, { replace: false }));
|
|
2445
|
+
}
|
|
2267
2446
|
box.appendChild(tile);
|
|
2268
2447
|
}
|
|
2269
2448
|
}
|
|
@@ -2525,6 +2704,7 @@ var init_panels = __esm({
|
|
|
2525
2704
|
init_bars();
|
|
2526
2705
|
init_format();
|
|
2527
2706
|
init_outcome();
|
|
2707
|
+
init_app();
|
|
2528
2708
|
}
|
|
2529
2709
|
});
|
|
2530
2710
|
|
|
@@ -2627,7 +2807,7 @@ function drawTraffic() {
|
|
|
2627
2807
|
svg.appendChild(svgEl("line", { x1: x, x2: x, y1: 0, y2: markerRow + plot, stroke: markColour, "stroke-width": 1.5, "stroke-dasharray": "3 2", opacity: 0.85 }));
|
|
2628
2808
|
const dot = svgEl("circle", { cx: x, cy: 3, r: 3, fill: markColour });
|
|
2629
2809
|
const title = svgEl("title");
|
|
2630
|
-
title.textContent = `${
|
|
2810
|
+
title.textContent = `${clockTime(change.at)} \xB7 ${change.kind}: ${change.summary}${change.by === void 0 ? "" : ` (by ${change.by})`}`;
|
|
2631
2811
|
dot.appendChild(title);
|
|
2632
2812
|
svg.appendChild(dot);
|
|
2633
2813
|
}
|
|
@@ -2646,7 +2826,7 @@ function drawTraffic() {
|
|
|
2646
2826
|
hover.setAttribute("x", String(index * step));
|
|
2647
2827
|
hover.setAttribute("width", String(step));
|
|
2648
2828
|
clear(tip);
|
|
2649
|
-
tip.appendChild(el("div", "t", `${
|
|
2829
|
+
tip.appendChild(el("div", "t", `${clockTime(bucket.at)} \xB7 ${Math.round(state.rangeMs / BUCKETS / 1e3)}s`));
|
|
2650
2830
|
tip.appendChild(tipRow("Served", n(bucket.served), s1));
|
|
2651
2831
|
tip.appendChild(tipRow("Mitigated", n(bucket.mitigated), s2));
|
|
2652
2832
|
tip.appendChild(tipRow("Denied", n(bucket.denied), crit));
|
|
@@ -2662,7 +2842,7 @@ function drawTraffic() {
|
|
|
2662
2842
|
const mitigated = buckets.reduce((sum, bucket) => sum + bucket.mitigated, 0);
|
|
2663
2843
|
const denied = buckets.reduce((sum, bucket) => sum + bucket.denied, 0);
|
|
2664
2844
|
const busiest = buckets.reduce((best, bucket) => bucket.total > best.total ? bucket : best, buckets[0] ?? { at: now, total: 0, served: 0, mitigated: 0, denied: 0 });
|
|
2665
|
-
$("traffic-alt").textContent = `Traffic over the last ${rangeLabel(state.rangeMs)}: ${n(total)} requests \u2014 ${n(served)} served, ${n(mitigated)} mitigated, ${n(denied)} denied. ` + (total === 0 ? "No traffic in this range." : `Busiest ${Math.round(state.rangeMs / BUCKETS / 1e3)}-second interval: ${n(busiest.total)} requests at ${
|
|
2845
|
+
$("traffic-alt").textContent = `Traffic over the last ${rangeLabel(state.rangeMs)}: ${n(total)} requests \u2014 ${n(served)} served, ${n(mitigated)} mitigated, ${n(denied)} denied. ` + (total === 0 ? "No traffic in this range." : `Busiest ${Math.round(state.rangeMs / BUCKETS / 1e3)}-second interval: ${n(busiest.total)} requests at ${clockTime(busiest.at)}.`) + (changes.length === 0 ? "" : ` ${n(changes.length)} runtime change${changes.length === 1 ? "" : "s"} in this range: ${changes.map((change) => `${change.kind}, ${change.summary}`).join("; ")}.`);
|
|
2666
2846
|
const legend = $("traffic-legend");
|
|
2667
2847
|
clear(legend);
|
|
2668
2848
|
legend.appendChild(el("span", null, `${n(total)} requests in the last ${rangeLabel(state.rangeMs)} \xB7`));
|
|
@@ -2865,7 +3045,7 @@ var init_charts = __esm({
|
|
|
2865
3045
|
async function loadActors() {
|
|
2866
3046
|
if (!SECTIONS.registry) return;
|
|
2867
3047
|
try {
|
|
2868
|
-
const body = await getJson(
|
|
3048
|
+
const body = await getJson(`/api/actors?limit=${state.actorsPageSize}&offset=${state.actorsPage * state.actorsPageSize}`);
|
|
2869
3049
|
state.actors = body.actors;
|
|
2870
3050
|
state.actorsTracked = body.tracked;
|
|
2871
3051
|
drawActors();
|
|
@@ -2882,6 +3062,41 @@ function trackActors() {
|
|
|
2882
3062
|
void loadActors();
|
|
2883
3063
|
}, 4e3);
|
|
2884
3064
|
}
|
|
3065
|
+
function drawActorsPager(full) {
|
|
3066
|
+
const page = state.actorsPage;
|
|
3067
|
+
const hidden = page === 0 && !full;
|
|
3068
|
+
const from = page * state.actorsPageSize + 1;
|
|
3069
|
+
const model = {
|
|
3070
|
+
page,
|
|
3071
|
+
from,
|
|
3072
|
+
to: from + state.actors.length - 1,
|
|
3073
|
+
total: state.actorsTracked,
|
|
3074
|
+
atStart: page === 0,
|
|
3075
|
+
atEnd: !full,
|
|
3076
|
+
go: (next) => {
|
|
3077
|
+
state.actorsPage = Math.max(0, next);
|
|
3078
|
+
void loadActors();
|
|
3079
|
+
},
|
|
3080
|
+
size: {
|
|
3081
|
+
current: state.actorsPageSize,
|
|
3082
|
+
choices: ACTORS_PAGE_SIZES,
|
|
3083
|
+
set: (next) => {
|
|
3084
|
+
state.actorsPageSize = next;
|
|
3085
|
+
state.actorsPage = 0;
|
|
3086
|
+
void loadActors();
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3090
|
+
for (const [id, withSize] of [
|
|
3091
|
+
["actors-pager-top", true],
|
|
3092
|
+
["actors-pager", false]
|
|
3093
|
+
]) {
|
|
3094
|
+
const host = $(id);
|
|
3095
|
+
host.hidden = hidden;
|
|
3096
|
+
if (hidden) clear(host);
|
|
3097
|
+
else renderPager(host, model, { withSize });
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
2885
3100
|
function drawActors() {
|
|
2886
3101
|
if (!SECTIONS.registry) return;
|
|
2887
3102
|
if (isConfirming()) return;
|
|
@@ -2889,6 +3104,7 @@ function drawActors() {
|
|
|
2889
3104
|
clear(body);
|
|
2890
3105
|
const actors = state.actors;
|
|
2891
3106
|
$("actors-count").textContent = `${n(actors.length)} shown \xB7 ${n(state.actorsTracked)} tracked`;
|
|
3107
|
+
drawActorsPager(actors.length === state.actorsPageSize);
|
|
2892
3108
|
byId("actors-empty").hidden = actors.length > 0;
|
|
2893
3109
|
for (const actor of actors) {
|
|
2894
3110
|
const row = el("tr");
|
|
@@ -2907,7 +3123,7 @@ function drawActors() {
|
|
|
2907
3123
|
const tags = [];
|
|
2908
3124
|
if (actor.cleared) tags.push("cleared as human");
|
|
2909
3125
|
if (actor.distinctUserAgents > 1) tags.push(`${actor.distinctUserAgents} User-Agents`);
|
|
2910
|
-
tags.push(`first seen ${
|
|
3126
|
+
tags.push(`first seen ${clockStamp(actor.firstSeen)}`);
|
|
2911
3127
|
stateCell.appendChild(el("div", "tagline", tags.join(" \xB7 ")));
|
|
2912
3128
|
row.appendChild(stateCell);
|
|
2913
3129
|
const actions = el("td", "acts");
|
|
@@ -2926,7 +3142,7 @@ function drawActors() {
|
|
|
2926
3142
|
body.appendChild(row);
|
|
2927
3143
|
}
|
|
2928
3144
|
}
|
|
2929
|
-
var timer;
|
|
3145
|
+
var timer, ACTORS_PAGE_SIZES;
|
|
2930
3146
|
var init_registry = __esm({
|
|
2931
3147
|
"src/dashboard/client/registry.ts"() {
|
|
2932
3148
|
"use strict";
|
|
@@ -2937,6 +3153,8 @@ var init_registry = __esm({
|
|
|
2937
3153
|
init_format();
|
|
2938
3154
|
init_api();
|
|
2939
3155
|
init_store();
|
|
3156
|
+
init_pager();
|
|
3157
|
+
ACTORS_PAGE_SIZES = [25, 50, 100, 200];
|
|
2940
3158
|
}
|
|
2941
3159
|
});
|
|
2942
3160
|
|
|
@@ -3338,9 +3556,26 @@ function start() {
|
|
|
3338
3556
|
setSearch(url.search);
|
|
3339
3557
|
if (SECTIONS.feed) reflectFilterButtons();
|
|
3340
3558
|
showTab(url.tab, { replace: true });
|
|
3341
|
-
|
|
3559
|
+
suspendScrollAnchoring();
|
|
3560
|
+
void loadInitialSnapshot().finally(settleScrollAnchoring);
|
|
3342
3561
|
connectStream();
|
|
3343
3562
|
}
|
|
3563
|
+
function htmlElement() {
|
|
3564
|
+
const root2 = rootNode();
|
|
3565
|
+
return root2 instanceof Document ? root2.documentElement : void 0;
|
|
3566
|
+
}
|
|
3567
|
+
function suspendScrollAnchoring() {
|
|
3568
|
+
htmlElement()?.classList.add("settling");
|
|
3569
|
+
}
|
|
3570
|
+
function settleScrollAnchoring() {
|
|
3571
|
+
const html = htmlElement();
|
|
3572
|
+
if (html === void 0) return;
|
|
3573
|
+
requestAnimationFrame(() => {
|
|
3574
|
+
requestAnimationFrame(() => {
|
|
3575
|
+
html.classList.remove("settling");
|
|
3576
|
+
});
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3344
3579
|
var TABS, available, FIRST, pending;
|
|
3345
3580
|
var init_client = __esm({
|
|
3346
3581
|
"src/dashboard/client/index.ts"() {
|
|
@@ -3551,6 +3786,8 @@ var DASHBOARD_CSS = String.raw`/* ----------------------------------------------
|
|
|
3551
3786
|
|
|
3552
3787
|
* { box-sizing: border-box; }
|
|
3553
3788
|
html, body { height: 100%; }
|
|
3789
|
+
/* Restored by the client once the first render has settled. See the note on .tiles. */
|
|
3790
|
+
html.settling { overflow-anchor: none; }
|
|
3554
3791
|
body {
|
|
3555
3792
|
margin: 0; background: var(--page); color: var(--ink);
|
|
3556
3793
|
font: 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
@@ -3643,14 +3880,68 @@ button[disabled] { opacity: .5; cursor: default; }
|
|
|
3643
3880
|
/* --- layout ------------------------------------------------------------- */
|
|
3644
3881
|
main { padding: 18px 20px 64px; max-width: 1680px; margin: 0 auto; }
|
|
3645
3882
|
.stack { display: grid; gap: 16px; }
|
|
3646
|
-
|
|
3883
|
+
/* Everything above the feed is drawn by script once the first snapshot arrives, which
|
|
3884
|
+
inserts a block of content above what is already laid out. The browser's scroll
|
|
3885
|
+
anchoring compensates for that by scrolling down by its height — so on any window narrow
|
|
3886
|
+
enough for the counter row to wrap, the dashboard opened with its own counters already
|
|
3887
|
+
off the top of the screen, every time.
|
|
3888
|
+
|
|
3889
|
+
Anchoring is switched off for the first render and switched back on once it has settled
|
|
3890
|
+
(see settleScrollAnchoring in the client). It is not simply left off: the feed puts new requests at
|
|
3891
|
+
the top, and anchoring is exactly what keeps somebody's place while they read a screen
|
|
3892
|
+
that grows above them. */
|
|
3893
|
+
.tiles { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(136px, 1fr)); }
|
|
3647
3894
|
.tile {
|
|
3648
|
-
background: var(--surface); border: 1px solid var(--line); border-radius:
|
|
3649
|
-
padding: 12px
|
|
3650
|
-
}
|
|
3651
|
-
.tile .v { font-size:
|
|
3652
|
-
.tile .k { font-size:
|
|
3653
|
-
|
|
3895
|
+
background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
|
|
3896
|
+
padding: 8px 12px 9px; box-shadow: var(--shadow);
|
|
3897
|
+
}
|
|
3898
|
+
.tile .v { font-size: 21px; font-weight: 620; letter-spacing: -.025em; line-height: 1.15; }
|
|
3899
|
+
.tile .k { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .055em; margin-top: 2px; font-weight: 560; }
|
|
3900
|
+
/* One line, always. These are grid items, so the tallest sets the height of all nine —
|
|
3901
|
+
two captions wrapping to a second line was costing every tile forty pixels of nothing.
|
|
3902
|
+
The full text stays available on hover rather than being cut from the page. */
|
|
3903
|
+
.tile .s { font-size: 11px; color: var(--muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3904
|
+
/* A tile that navigates is a real button, so it arrives carrying the browser's own
|
|
3905
|
+
font, centring and chrome. Reset to match its inert neighbours exactly, then given
|
|
3906
|
+
back the one thing a div must not have: something that says it can be pressed. */
|
|
3907
|
+
button.tile {
|
|
3908
|
+
font: inherit; color: inherit; text-align: left; width: 100%; display: block;
|
|
3909
|
+
cursor: pointer; appearance: none; transition: border-color .12s, box-shadow .12s;
|
|
3910
|
+
}
|
|
3911
|
+
button.tile:hover { border-color: var(--focus); }
|
|
3912
|
+
/* The badge's companion: fetches the entries the stream skipped. Sits inline with the
|
|
3913
|
+
heading, so it is styled to read as part of the sentence rather than as a form control. */
|
|
3914
|
+
.load-skipped {
|
|
3915
|
+
font: inherit; margin-left: 6px; padding: 1px 8px; border-radius: 6px; cursor: pointer;
|
|
3916
|
+
border: 1px solid var(--warn-text); background: transparent; color: var(--warn-text);
|
|
3917
|
+
}
|
|
3918
|
+
.load-skipped:hover { background: color-mix(in srgb, var(--warn-text) 12%, transparent); }
|
|
3919
|
+
.load-skipped:disabled { opacity: .5; cursor: default; }
|
|
3920
|
+
|
|
3921
|
+
/* Prev/next under a table. Quiet: it is navigation for a list, not an action on it. */
|
|
3922
|
+
.pager {
|
|
3923
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
3924
|
+
padding: 9px 2px 2px; font-size: 12px; color: var(--muted);
|
|
3925
|
+
}
|
|
3926
|
+
.pager button {
|
|
3927
|
+
font: inherit; padding: 4px 11px; border-radius: 7px;
|
|
3928
|
+
border: 1px solid var(--line); background: var(--surface); color: var(--ink); cursor: pointer;
|
|
3929
|
+
}
|
|
3930
|
+
.pager button:hover:not(:disabled) { border-color: var(--focus); }
|
|
3931
|
+
.pager button:disabled { opacity: .45; cursor: default; }
|
|
3932
|
+
.pager .where { font-variant-numeric: tabular-nums; }
|
|
3933
|
+
.pager.pager-top { padding: 2px 2px 9px; border-bottom: 1px solid var(--line); margin-bottom: 9px; }
|
|
3934
|
+
/* The feed's upper pager rides in the toolbar rather than owning a row of its own, which
|
|
3935
|
+
was thirty-six pixels of mostly empty rule above every screenful of requests. */
|
|
3936
|
+
.pager.pager-inline { padding: 0; margin-left: auto; gap: 8px; }
|
|
3937
|
+
.pager.pager-inline .size { margin-left: 0; }
|
|
3938
|
+
.pager .step { min-width: 30px; font-size: 15px; line-height: 1; padding: 3px 8px 5px; }
|
|
3939
|
+
.pager .size { display: flex; align-items: center; gap: 6px; margin-left: auto; }
|
|
3940
|
+
.pager .size select {
|
|
3941
|
+
font: inherit; padding: 3px 6px; border-radius: 6px;
|
|
3942
|
+
border: 1px solid var(--line); background: var(--surface); color: var(--ink);
|
|
3943
|
+
}
|
|
3944
|
+
.pager .held { color: var(--warn-text); }
|
|
3654
3945
|
.tile.good .v { color: var(--good-text); }
|
|
3655
3946
|
.tile.warn .v { color: var(--warn-text); }
|
|
3656
3947
|
.tile.crit .v { color: var(--crit-text); }
|
|
@@ -3665,7 +3956,7 @@ main { padding: 18px 20px 64px; max-width: 1680px; margin: 0 auto; }
|
|
|
3665
3956
|
the page header, where they were always meant to. */
|
|
3666
3957
|
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); overflow: clip; }
|
|
3667
3958
|
.panel > h2 {
|
|
3668
|
-
margin: 0; padding:
|
|
3959
|
+
margin: 0; padding: 9px 14px; font-size: 11.5px; font-weight: 620; color: var(--muted);
|
|
3669
3960
|
text-transform: uppercase; letter-spacing: .055em; border-bottom: 1px solid var(--line);
|
|
3670
3961
|
display: flex; align-items: center; gap: 10px;
|
|
3671
3962
|
}
|
|
@@ -3677,6 +3968,17 @@ main { padding: 18px 20px 64px; max-width: 1680px; margin: 0 auto; }
|
|
|
3677
3968
|
Embedded in a 320px sidebar on a 1280px screen the media query never fired, the second
|
|
3678
3969
|
column held its 280px minimum, and the feed was squeezed to twenty-two pixels. */
|
|
3679
3970
|
.stack { container: dash / inline-size; }
|
|
3971
|
+
/* A grid item's min-width is auto, so a panel wrapping a wide table refuses to shrink and
|
|
3972
|
+
pushes the whole document sideways instead — which is what the Actors screen did under
|
|
3973
|
+
about 600px, where the table is widest and the viewport narrowest. The .two grid already
|
|
3974
|
+
says minmax(0, ...) for this reason; .stack needs the same permission. With it the panel
|
|
3975
|
+
shrinks and the scroller inside it does its job. */
|
|
3976
|
+
.stack > * { min-width: 0; }
|
|
3977
|
+
/* Numeric columns shrink to their contents, so the width goes to the two columns that
|
|
3978
|
+
carry text — the actor and what is known about it. Six counters of one or two digits
|
|
3979
|
+
were each taking about a hundred pixels while the State column was squeezed against the
|
|
3980
|
+
buttons. Same trick the feed's time column already uses. */
|
|
3981
|
+
#view-actors th.num, #view-actors td.num { width: 1%; white-space: nowrap; }
|
|
3680
3982
|
.two { display: grid; gap: 16px; grid-template-columns: minmax(0, 1.9fr) minmax(280px, 1fr); align-items: start; }
|
|
3681
3983
|
.grid3 { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
|
3682
3984
|
@container dash (max-width: 1080px) { .two { grid-template-columns: minmax(0, 1fr); } }
|
|
@@ -3756,7 +4058,7 @@ thead th {
|
|
|
3756
4058
|
text-align: left; font-size: 11px; font-weight: 600; color: var(--muted);
|
|
3757
4059
|
text-transform: uppercase; letter-spacing: .05em; padding: 9px 15px; border-bottom: 1px solid var(--line);
|
|
3758
4060
|
}
|
|
3759
|
-
tbody td { padding:
|
|
4061
|
+
tbody td { padding: 7px 14px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
|
|
3760
4062
|
/* Narrow, quiet, and never the reason a row wraps: the time is for scanning down, not
|
|
3761
4063
|
for reading across. */
|
|
3762
4064
|
tbody td.when { color: var(--muted); font-size: 11.5px; white-space: nowrap; width: 1%; padding-right: 4px; }
|
|
@@ -4136,7 +4438,7 @@ var DASHBOARD_MARKUP = String.raw`
|
|
|
4136
4438
|
|
|
4137
4439
|
<div class="two">
|
|
4138
4440
|
<section class="panel feed-panel">
|
|
4139
|
-
<h2>Requests <span class="sub" id="feed-count"></span><span class="sub win" id="feed-window"></span><span class="sub warn-text" id="feed-skipped" hidden></span></h2>
|
|
4441
|
+
<h2>Requests <span class="sub" id="feed-count"></span><span class="sub win" id="feed-window"></span><span class="sub warn-text" id="feed-skipped" hidden></span><button class="sub load-skipped" id="feed-load-skipped" type="button" hidden>Load them</button></h2>
|
|
4140
4442
|
<div class="toolbar">
|
|
4141
4443
|
<div class="filters" id="filters"></div>
|
|
4142
4444
|
<div class="search">
|
|
@@ -4144,6 +4446,7 @@ var DASHBOARD_MARKUP = String.raw`
|
|
|
4144
4446
|
<kbd aria-hidden="true">/</kbd>
|
|
4145
4447
|
</div>
|
|
4146
4448
|
<button id="feed-export" title="Download every request matching this filter as replay JSONL">Export</button>
|
|
4449
|
+
<div class="pager pager-inline" id="feed-pager-top" hidden></div>
|
|
4147
4450
|
</div>
|
|
4148
4451
|
<div class="feed-scroll">
|
|
4149
4452
|
<table>
|
|
@@ -4155,6 +4458,7 @@ var DASHBOARD_MARKUP = String.raw`
|
|
|
4155
4458
|
<tbody id="rows"></tbody>
|
|
4156
4459
|
</table>
|
|
4157
4460
|
</div>
|
|
4461
|
+
<div class="pager" id="feed-pager" hidden></div>
|
|
4158
4462
|
<div class="empty" id="empty">Nothing assessed yet. Send some traffic through the handler and it appears here within a moment.</div>
|
|
4159
4463
|
</section>
|
|
4160
4464
|
|
|
@@ -4203,6 +4507,7 @@ var DASHBOARD_MARKUP = String.raw`
|
|
|
4203
4507
|
<div class="note" id="actors-note">Everyone the engine is currently remembering, busiest first — a far larger
|
|
4204
4508
|
population than the feed's ring, which holds requests rather than clients. This is
|
|
4205
4509
|
what <code>cadence</code>, <code>crawl-breadth</code> and <code>rate-anomaly</code> are reading.</div>
|
|
4510
|
+
<div class="pager pager-top" id="actors-pager-top" hidden></div>
|
|
4206
4511
|
<div class="feed-scroll">
|
|
4207
4512
|
<table>
|
|
4208
4513
|
<thead>
|
|
@@ -4214,6 +4519,7 @@ var DASHBOARD_MARKUP = String.raw`
|
|
|
4214
4519
|
<tbody id="actor-rows"></tbody>
|
|
4215
4520
|
</table>
|
|
4216
4521
|
</div>
|
|
4522
|
+
<div class="pager" id="actors-pager" hidden></div>
|
|
4217
4523
|
<div class="empty" id="actors-empty" hidden>Nothing in the registry yet.</div>
|
|
4218
4524
|
</section>
|
|
4219
4525
|
</div>
|