admin: overview redesign, device scope, one formatting contract (milestone 39)
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s
The Overview page takes the decided visual language (specs/ui-visual-redesign.md): four centred totals with their Activity links, a smoothed area chart of total and blocked queries with point hover and a tooltip centred beside the point, a stacked client chart in eight distinct hues plus one Other band that is always a series, and a card row with the cache hit rate, the query types as a single-hue ramp ring, and the upstream breakdown. The count axis grows its margin with the widest grouped tick and draws whole-number ticks only. GET /api/overview takes a client parameter; the scoped read uses idx_query_log_ts and the cache keeps scoped slots. The device selector beside the period selector is URL state, so a scoped view is a link, and the tile links carry the scope into Activity. The route reduces a pasted IPv6 scope to the RFC 5952 spelling the logger stores, mapped addresses included, and drops anything that is not an address. A failed device list says so under the selector with a retry. All measured quantities go through admin/src/lib/format.ts: grouped counts, two-decimal percentages, one-decimal rates, durations as the two largest nonzero units. Identifiers, configured values and preset labels render as written; the module header states that scope. A sweep test refuses toFixed, toLocaleString, Intl.NumberFormat and padStart anywhere else. Chrome: one 4px radius from the metrics constants, shared Card with a prominent title and a one-line description on every panel, the settings form sections on the same card with a floated legend, the sidebar grouped into Monitoring and System with a status block (protection, queries per minute on Overview, uptime), keyboard-focusable table scroll wrappers, and the accent darkened to 5.43:1 on its wash. Not built: the spec's ranked-list primitive, which has no consumer and no API rows. Codex reviewed sessions B to D over five rounds (thirty-three findings fixed, thirteen rejected as non-quantities); the owner skipped a sixth round. Claude-Session: https://claude.ai/code/session_01VTgx3a1zz1R78o4K55kkwR
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
import { formatBytes, formatClock, formatDuration, formatMicros, formatTime } from "@/lib/format";
|
||||
import {
|
||||
formatBytes,
|
||||
formatClock,
|
||||
formatCount,
|
||||
formatDuration,
|
||||
formatMicros,
|
||||
formatPercent,
|
||||
formatRate,
|
||||
formatTime,
|
||||
} from "@/lib/format";
|
||||
|
||||
test("formatTime renders unix seconds in the given locale and zone", () => {
|
||||
// 2024-01-01T00:00:00Z; ICU emits U+202F before AM/PM in recent Node.
|
||||
expect(formatTime(1704067200, "en-US", "UTC").replace(/ /g, " ")).toBe("Jan 1, 2024, 12:00:00 AM");
|
||||
expect(formatTime(1704067200, "en-US", "UTC").replace(/\u202f/g, " ")).toBe("Jan 1, 2024, 12:00:00 AM");
|
||||
});
|
||||
|
||||
test("formatClock states the time of day alone, for a stamp read against now", () => {
|
||||
@@ -12,6 +21,33 @@ test("formatClock states the time of day alone, for a stamp read against now", (
|
||||
expect(formatClock(Date.UTC(2026, 0, 1, 14, 5) / 1000, "en-GB", "UTC")).not.toMatch(/2026/);
|
||||
});
|
||||
|
||||
test("formatCount groups thousands", () => {
|
||||
expect(formatCount(0)).toBe("0");
|
||||
expect(formatCount(999)).toBe("999");
|
||||
expect(formatCount(18432)).toBe("18,432");
|
||||
expect(formatCount(1_234_567)).toBe("1,234,567");
|
||||
});
|
||||
|
||||
test("formatPercent always carries two decimals, rounded half up", () => {
|
||||
expect(formatPercent(0)).toBe("0.00%");
|
||||
expect(formatPercent(1)).toBe("100.00%");
|
||||
expect(formatPercent(0.1544)).toBe("15.44%");
|
||||
expect(formatPercent(0.154)).toBe("15.40%");
|
||||
// A stored 15.435 sits just under the half; it still rounds up.
|
||||
expect(formatPercent(0.15435)).toBe("15.44%");
|
||||
expect(formatPercent(0.15434)).toBe("15.43%");
|
||||
expect(formatPercent(0.005)).toBe("0.50%");
|
||||
expect(formatPercent(0.00005)).toBe("0.01%");
|
||||
expect(formatPercent(0.00004)).toBe("0.00%");
|
||||
});
|
||||
|
||||
test("formatRate carries one decimal, rounded half up", () => {
|
||||
expect(formatRate(12.84)).toBe("12.8");
|
||||
expect(formatRate(12.85)).toBe("12.9");
|
||||
expect(formatRate(5)).toBe("5.0");
|
||||
expect(formatRate(0)).toBe("0.0");
|
||||
});
|
||||
|
||||
test("formatBytes humanizes with binary units", () => {
|
||||
expect(formatBytes(0)).toBe("0 B");
|
||||
expect(formatBytes(1023)).toBe("1023 B");
|
||||
@@ -22,21 +58,25 @@ test("formatBytes humanizes with binary units", () => {
|
||||
expect(formatBytes(2 * 1024 ** 4)).toBe("2.0 TiB");
|
||||
});
|
||||
|
||||
test("formatDuration steps up a unit at each boundary and truncates", () => {
|
||||
expect(formatDuration(0)).toBe("0s");
|
||||
expect(formatDuration(59)).toBe("59s");
|
||||
test("formatDuration is the two largest nonzero units, unpadded", () => {
|
||||
expect(formatDuration(45)).toBe("45s");
|
||||
expect(formatDuration(60)).toBe("1m");
|
||||
expect(formatDuration(3599)).toBe("59m");
|
||||
expect(formatDuration(725)).toBe("12m 5s");
|
||||
expect(formatDuration(3600)).toBe("1h");
|
||||
expect(formatDuration(10800)).toBe("3h");
|
||||
expect(formatDuration(86399)).toBe("23h");
|
||||
expect(formatDuration(86400)).toBe("1d");
|
||||
expect(formatDuration(400000)).toBe("4d");
|
||||
expect(formatDuration(3600 * 4 + 60 * 12 + 30)).toBe("4h 12m");
|
||||
expect(formatDuration(86400 * 6 + 3600 * 4)).toBe("6d 4h");
|
||||
// The two largest that are nonzero, whatever sits between them.
|
||||
expect(formatDuration(86400 * 6 + 5)).toBe("6d 5s");
|
||||
expect(formatDuration(86400 * 6)).toBe("6d");
|
||||
expect(formatDuration(86400 * 400)).toBe("400d");
|
||||
// Never "06d 4h" or "6d 04h".
|
||||
expect(formatDuration(86400 * 6 + 3600 * 4)).not.toMatch(/0\d/);
|
||||
});
|
||||
|
||||
test("formatDuration is never negative", () => {
|
||||
// Clock skew between the server's timestamps and the browser's clock.
|
||||
expect(formatDuration(-5)).toBe("0s");
|
||||
test("formatDuration reads under a second as such, skew included", () => {
|
||||
expect(formatDuration(0)).toBe("<1s");
|
||||
expect(formatDuration(0.9)).toBe("<1s");
|
||||
expect(formatDuration(-5)).toBe("<1s");
|
||||
});
|
||||
|
||||
test("formatMicros renders milliseconds with one decimal", () => {
|
||||
@@ -45,3 +85,27 @@ test("formatMicros renders milliseconds with one decimal", () => {
|
||||
expect(formatMicros(999)).toBe("1.0 ms");
|
||||
expect(formatMicros(2_500_000)).toBe("2500.0 ms");
|
||||
});
|
||||
|
||||
/**
|
||||
* The contract's teeth, as far as a text scan can bite: no source outside this
|
||||
* module calls a number-formatting API. A bare `{count}` in JSX is invisible to
|
||||
* this scan and is caught in review; the scan closes the door on the four ways
|
||||
* a competing rule would be written. `datetime-local` zero-padding in
|
||||
* `activity/datetime.ts` is a wire grammar, not a display rule, and is the one
|
||||
* allowed `padStart`. Identifiers, configured values and preset labels are not
|
||||
* quantities at all and are exempt by the header rule, so the scan never sees
|
||||
* them.
|
||||
*/
|
||||
test("no source file outside format.ts calls a number-formatting API", () => {
|
||||
const sources = import.meta.glob<string>("../**/*.{ts,tsx}", { query: "?raw", import: "default", eager: true });
|
||||
const offenders: string[] = [];
|
||||
for (const [path, text] of Object.entries(sources)) {
|
||||
if (/\.test\.tsx?$/.test(path) || /(^|\/)format\.ts$/.test(path)) continue;
|
||||
for (const pattern of [/\.toFixed\(/, /\.toLocaleString\(/, /Intl\.NumberFormat/]) {
|
||||
if (pattern.test(text)) offenders.push(`${path}: ${pattern.source}`);
|
||||
}
|
||||
if (/\.padStart\(/.test(text) && !path.endsWith("/activity/datetime.ts")) offenders.push(`${path}: padStart`);
|
||||
}
|
||||
expect(Object.keys(sources).length).toBeGreaterThan(50);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user