milestone 33: contract closure — samples, file-authority enumeration, dead code, bundle ceiling
Gates / frontend (push) Successful in 1m34s
Gates / test (push) Successful in 2m3s
Gates / test-aarch64 (push) Failing after 3h13m33s
Gates / package (push) Successful in 5m20s
Gates / container (push) Successful in 15s
CI / gates (push) Failing after 6h30m45s
Gates / frontend (push) Successful in 1m34s
Gates / test (push) Successful in 2m3s
Gates / test-aarch64 (push) Failing after 3h13m33s
Gates / package (push) Successful in 5m20s
Gates / container (push) Successful in 15s
CI / gates (push) Failing after 6h30m45s
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && node scripts/assert-css-layers.mjs && node scripts/stamp-dist.mjs",
|
||||
"build": "vite build && node scripts/assert-css-layers.mjs && node scripts/assert-bundle-size.mjs && node scripts/stamp-dist.mjs",
|
||||
"typecheck": "tsc -b",
|
||||
"lint": "oxlint src vite.config.ts",
|
||||
"format": "prettier --write .",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
// The admin bundle is embedded in the server binary and served to a household
|
||||
// LAN, so an accidental dependency or a stray asset landing in dist is a
|
||||
// regression nobody would otherwise notice: every other gate passes with a
|
||||
// bundle twice this size. One number, total bytes of dist/assets — not per
|
||||
// chunk, not gzipped — because the failure being caught is "something big
|
||||
// arrived", not chunk shape.
|
||||
//
|
||||
// This runs from admin/ as part of `npm run build`, before stamp-dist: a failed
|
||||
// size check must not leave a fresh .src-hash beside an oversized bundle that a
|
||||
// later Zig build would accept as current.
|
||||
|
||||
import { readdirSync, statSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const BUDGET_BYTES = 800_000;
|
||||
|
||||
const distDir = join(dirname(dirname(fileURLToPath(import.meta.url))), "dist", "assets");
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(distDir, { withFileTypes: true });
|
||||
} catch (err) {
|
||||
console.error(`assert-bundle-size: cannot read admin/dist/assets: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => ({ name: entry.name, bytes: statSync(join(distDir, entry.name)).size }))
|
||||
.sort((a, b) => b.bytes - a.bytes);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error("assert-bundle-size: no files in admin/dist/assets — did the build emit anything?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const total = files.reduce((sum, file) => sum + file.bytes, 0);
|
||||
const format = (bytes) => bytes.toLocaleString("en-US");
|
||||
|
||||
if (total > BUDGET_BYTES) {
|
||||
console.error(
|
||||
`assert-bundle-size: admin/dist/assets is ${format(total)} bytes, over the ${format(BUDGET_BYTES)} byte budget.`,
|
||||
);
|
||||
console.error("Largest chunks:");
|
||||
for (const file of files.slice(0, 5)) console.error(` ${format(file.bytes).padStart(9)} ${file.name}`);
|
||||
console.error("Drop what arrived, or raise the budget in this script with the reason in the changelog.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`admin/dist/assets is ${format(total)} bytes across ${files.length} files, ` +
|
||||
`${format(BUDGET_BYTES - total)} under the ${format(BUDGET_BYTES)} byte budget`,
|
||||
);
|
||||
@@ -5,7 +5,7 @@ import { AuthProvider } from "@/auth/store";
|
||||
import { createQueryClient } from "@/lib/queryClient";
|
||||
import { createAppRouter } from "@/routes";
|
||||
import type { QueryDetail } from "@/lib/types";
|
||||
import { provenance } from "@/features/queries/provenanceFixture";
|
||||
import { provenance } from "@/features/provenance/provenanceFixture";
|
||||
import { health } from "@/lib/healthFixture";
|
||||
|
||||
function detail(id: number, sections: Parameters<typeof provenance>[0] = {}): QueryDetail {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createQueryClient } from "@/lib/queryClient";
|
||||
import { createAppRouter } from "@/routes";
|
||||
import { health } from "@/lib/healthFixture";
|
||||
import type { Client, Coverage, QueriesPage, QueryRow } from "@/lib/types";
|
||||
import { queryRow } from "@/features/queries/provenanceFixture";
|
||||
import { queryRow } from "@/features/provenance/provenanceFixture";
|
||||
|
||||
function client(id: number, ip: string, name: string, learnedName: string): Client {
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,7 @@ import InlineError from "@/lib/InlineError";
|
||||
import { queriesInfiniteQuery } from "@/lib/queries";
|
||||
import type { QueryRow } from "@/lib/types";
|
||||
import { useClientNames } from "@/features/clients/clientNames";
|
||||
import { summarizeRow } from "@/features/queries/querySummary";
|
||||
import { summarizeRow } from "@/features/provenance/querySummary";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
import { ActivityCells, ActivityTableHead, activityDomainLink } from "./cells";
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AuthProvider } from "@/auth/store";
|
||||
import { createQueryClient } from "@/lib/queryClient";
|
||||
import { createAppRouter } from "@/routes";
|
||||
import type { Client } from "@/lib/types";
|
||||
import { provenance, queryRow } from "@/features/queries/provenanceFixture";
|
||||
import { provenance, queryRow } from "@/features/provenance/provenanceFixture";
|
||||
import { health } from "@/lib/healthFixture";
|
||||
import { FakeEventSource } from "./fakeEventSource";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { useClientNames } from "@/features/clients/clientNames";
|
||||
import { summarizeEvent } from "@/features/queries/querySummary";
|
||||
import { summarizeEvent } from "@/features/provenance/querySummary";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
import { ActivityCells, ActivityTableHead, activityDomainLink } from "./cells";
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
qclassName,
|
||||
rcodeName,
|
||||
routeKindLabel,
|
||||
} from "@/features/queries/provenanceCopy";
|
||||
import { qtypeName } from "@/features/queries/qtype";
|
||||
} from "@/features/provenance/provenanceCopy";
|
||||
import { qtypeName } from "@/features/provenance/qtype";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type { QueryRow } from "@/lib/types";
|
||||
import type { ClientNames } from "@/features/clients/clientNames";
|
||||
import { queryRow } from "@/features/queries/provenanceFixture";
|
||||
import { summarizeRow, type QuerySummary } from "@/features/queries/querySummary";
|
||||
import { queryRow } from "@/features/provenance/provenanceFixture";
|
||||
import { summarizeRow, type QuerySummary } from "@/features/provenance/querySummary";
|
||||
import { ACTIVITY_COLUMNS, ActivityCells, ActivityTableHead, resultLabel, routeLabel } from "./cells";
|
||||
|
||||
const noNames: ClientNames = new Map();
|
||||
|
||||
@@ -17,9 +17,9 @@ import * as stylex from "@stylexjs/stylex";
|
||||
import { formatMicros, formatTime } from "@/lib/format";
|
||||
import type { RouteKind } from "@/lib/types";
|
||||
import { ClientName, type ClientNames } from "@/features/clients/clientNames";
|
||||
import { rcodeShortName } from "@/features/queries/provenanceCopy";
|
||||
import { qtypeName } from "@/features/queries/qtype";
|
||||
import type { QuerySummary } from "@/features/queries/querySummary";
|
||||
import { rcodeShortName } from "@/features/provenance/provenanceCopy";
|
||||
import { qtypeName } from "@/features/provenance/qtype";
|
||||
import type { QuerySummary } from "@/features/provenance/querySummary";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { diagnosticsBounds, relatedBounds, RELATED_WINDOW_SECONDS } from "./relatedLinks";
|
||||
import { queriesFilterOf, validateActivitySearch } from "./search";
|
||||
|
||||
const TS = 1_700_000_000;
|
||||
|
||||
test("an unbounded origin falls back to the window either side of the query", () => {
|
||||
expect(relatedBounds(TS, { since: undefined, until: undefined })).toEqual({
|
||||
since: TS - RELATED_WINDOW_SECONDS,
|
||||
until: TS + RELATED_WINDOW_SECONDS,
|
||||
});
|
||||
});
|
||||
|
||||
test("a bounded origin carries both of its bounds through unchanged", () => {
|
||||
expect(relatedBounds(TS, { since: 1, until: 2 })).toEqual({ since: 1, until: 2 });
|
||||
});
|
||||
|
||||
test("a half-bounded origin keeps its half and falls back on the other", () => {
|
||||
expect(relatedBounds(TS, { since: 1, until: undefined })).toEqual({
|
||||
since: 1,
|
||||
until: TS + RELATED_WINDOW_SECONDS,
|
||||
});
|
||||
expect(relatedBounds(TS, { since: undefined, until: 2 })).toEqual({
|
||||
since: TS - RELATED_WINDOW_SECONDS,
|
||||
until: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("an origin bound of zero is a bound, not a missing one", () => {
|
||||
expect(relatedBounds(TS, { since: 0, until: 0 })).toEqual({ since: 0, until: 0 });
|
||||
});
|
||||
|
||||
test("the diagnostics window is the fixed window either side of the query, never inherited", () => {
|
||||
expect(diagnosticsBounds(TS)).toEqual({ since: TS - RELATED_WINDOW_SECONDS, until: TS + RELATED_WINDOW_SECONDS });
|
||||
expect(diagnosticsBounds(0)).toEqual({ since: -RELATED_WINDOW_SECONDS, until: RELATED_WINDOW_SECONDS });
|
||||
});
|
||||
|
||||
/**
|
||||
* The admin half of the server's `since <= ts < until` window contract
|
||||
* (queries_repo.zig): what a related link emits has to survive the validation
|
||||
* the Activity route puts every search through, or the link would silently open
|
||||
* a wider window than it named. Inclusion at the edges is the server's property
|
||||
* and is tested there; this pins that the bounds arrive intact.
|
||||
*/
|
||||
test("bounds emitted by a related link round-trip through the Activity search to the same filter", () => {
|
||||
const origin = { since: undefined, until: TS + 3_600 };
|
||||
const emitted = { mode: "history", domain: "ads.example.com", ...relatedBounds(TS, origin) };
|
||||
|
||||
const applied = validateActivitySearch(emitted);
|
||||
|
||||
expect(applied.mode).toBe("history");
|
||||
expect(applied.since).toBe(emitted.since);
|
||||
expect(applied.until).toBe(emitted.until);
|
||||
expect(queriesFilterOf(applied)).toEqual({
|
||||
domain: "ads.example.com",
|
||||
since: TS - RELATED_WINDOW_SECONDS,
|
||||
until: TS + 3_600,
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Provenance, QueryRow } from "@/lib/types";
|
||||
import { provenance, queryRow } from "@/features/queries/provenanceFixture";
|
||||
import { provenance, queryRow } from "@/features/provenance/provenanceFixture";
|
||||
import { RING_CAPACITY, mergeGap, pushRow, summaryOf, type LiveRow } from "./ringBuffer";
|
||||
|
||||
function streamed(key: number, ts: number, domain: string, sections: Parameters<typeof provenance>[0] = {}): LiveRow {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LiveQueryEvent, QueryRow } from "@/lib/types";
|
||||
import { summarizeEvent, summarizeRow, type QuerySummary } from "@/features/queries/querySummary";
|
||||
import { summarizeEvent, summarizeRow, type QuerySummary } from "@/features/provenance/querySummary";
|
||||
|
||||
/**
|
||||
* A row in the live buffer. `key` is a client-side monotonic counter, because
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import type { QueriesPage, QueryRow } from "@/lib/types";
|
||||
import { provenance, queryRow } from "@/features/queries/provenanceFixture";
|
||||
import { provenance, queryRow } from "@/features/provenance/provenanceFixture";
|
||||
import { summaryOf, type LiveRow } from "./ringBuffer";
|
||||
import { FakeEventSource } from "./fakeEventSource";
|
||||
import { CAP_ERROR_THRESHOLD, useLiveQueries } from "./useLiveQueries";
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as stylex from "@stylexjs/stylex";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import CoverageNotice from "@/lib/CoverageNotice";
|
||||
import InlineError from "@/lib/InlineError";
|
||||
import { qtypeName } from "@/features/queries/qtype";
|
||||
import { qtypeName } from "@/features/provenance/qtype";
|
||||
import type { Period, StatsRoutes, StatsTypes } from "@/lib/types";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
|
||||
@@ -184,8 +184,6 @@ export const deleteBlocklist = (id: number): Promise<void> => request(`/api/bloc
|
||||
export const listRules = async (): Promise<Rule[]> => (await request<{ rules: Rule[] }>("/api/rules")).rules;
|
||||
export const createRule = (input: RuleInput): Promise<RuleEcho> =>
|
||||
request("/api/rules", { method: "POST", body: input });
|
||||
export const updateRule = (id: number, input: RuleInput): Promise<RuleEcho> =>
|
||||
request(`/api/rules/${id}`, { method: "PUT", body: input });
|
||||
export const deleteRule = (id: number): Promise<void> => request(`/api/rules/${id}`, { method: "DELETE" });
|
||||
|
||||
// Local records
|
||||
|
||||
@@ -148,6 +148,21 @@ export const sample_create_blocklist: BlocklistEcho = {
|
||||
url: "https://lists.example/ads.txt",
|
||||
};
|
||||
|
||||
export const sample_get_blocklist: Blocklist = {
|
||||
checksum: null,
|
||||
domain_count: 0,
|
||||
enabled: false,
|
||||
exception_count: 0,
|
||||
id: 0,
|
||||
is_suggested: false,
|
||||
last_updated: null,
|
||||
name: "ads",
|
||||
skipped_regex_count: 0,
|
||||
skipped_unsupported_count: 0,
|
||||
url: "https://lists.example/ads.txt",
|
||||
wildcard_count: 0,
|
||||
};
|
||||
|
||||
export const sample_list_blocklists: { blocklists: Blocklist[] } = {
|
||||
blocklists: [
|
||||
{
|
||||
@@ -210,6 +225,12 @@ export const sample_create_group: Group = {
|
||||
safe_search: false,
|
||||
};
|
||||
|
||||
export const sample_get_group: Group = {
|
||||
id: 0,
|
||||
name: "kids",
|
||||
safe_search: false,
|
||||
};
|
||||
|
||||
export const sample_update_group: Group = {
|
||||
id: 0,
|
||||
name: "teens",
|
||||
@@ -232,6 +253,16 @@ export const sample_create_rule: RuleEcho = {
|
||||
pattern: "ads.example",
|
||||
};
|
||||
|
||||
export const sample_get_rule: Rule = {
|
||||
action: "block",
|
||||
created_at: 0,
|
||||
group: "default",
|
||||
group_id: 0,
|
||||
id: 0,
|
||||
kind: "exact",
|
||||
pattern: "ads.example",
|
||||
};
|
||||
|
||||
export const sample_list_rules: { rules: Rule[] } = {
|
||||
rules: [
|
||||
{
|
||||
@@ -274,6 +305,14 @@ export const sample_create_local_record: LocalRecord = {
|
||||
value: "192.168.1.10",
|
||||
};
|
||||
|
||||
export const sample_get_local_record: LocalRecord = {
|
||||
id: 0,
|
||||
name: "nas.lan",
|
||||
rtype: "A",
|
||||
ttl: 0,
|
||||
value: "192.168.1.10",
|
||||
};
|
||||
|
||||
export const sample_list_local_records: { local_records: LocalRecord[] } = {
|
||||
local_records: [
|
||||
{
|
||||
@@ -300,6 +339,12 @@ export const sample_create_forward_zone: ForwardZone = {
|
||||
zone: "lan",
|
||||
};
|
||||
|
||||
export const sample_get_forward_zone: ForwardZone = {
|
||||
id: 0,
|
||||
resolver: "udp://10.0.0.1:53",
|
||||
zone: "lan",
|
||||
};
|
||||
|
||||
export const sample_list_forward_zones: { forward_zones: ForwardZone[] } = {
|
||||
forward_zones: [
|
||||
{
|
||||
@@ -332,6 +377,18 @@ export const sample_list_clients: { clients: Client[] } = {
|
||||
],
|
||||
};
|
||||
|
||||
export const sample_get_client: Client = {
|
||||
first_seen: 0,
|
||||
group: "default",
|
||||
group_id: 0,
|
||||
hand_edited: false,
|
||||
id: 0,
|
||||
ip: "192.168.1.50",
|
||||
last_seen: 0,
|
||||
learned_name: "",
|
||||
name: "laptop",
|
||||
};
|
||||
|
||||
export const sample_update_client: Client = {
|
||||
first_seen: 0,
|
||||
group: "default",
|
||||
@@ -389,6 +446,14 @@ export const sample_create_upstream: UpstreamEcho = {
|
||||
url: "https://dns2.example/dns-query",
|
||||
};
|
||||
|
||||
export const sample_get_upstream: Upstream = {
|
||||
enabled: true,
|
||||
id: 0,
|
||||
priority: 0,
|
||||
tls_name: "",
|
||||
url: "https://dns.example/dns-query",
|
||||
};
|
||||
|
||||
export const sample_update_upstream: UpstreamEcho = {
|
||||
enabled: true,
|
||||
id: 0,
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface LogoutResponse {
|
||||
|
||||
/**
|
||||
* The three closed enums `src/storage/provenance.zig` stores, as values rather
|
||||
* than bare types: the copy maps in `features/queries/provenanceCopy.ts` have to
|
||||
* than bare types: the copy maps in `features/provenance/provenanceCopy.ts` have to
|
||||
* be proven exhaustive at runtime as well as by `tsc`, exactly as
|
||||
* `DIAGNOSTIC_CODES` below.
|
||||
*/
|
||||
@@ -320,7 +320,7 @@ export interface StatsTimeseries {
|
||||
|
||||
/**
|
||||
* One DNS type's share of the window. `qtype` is the numeric code as logged:
|
||||
* naming it is the admin's job (`features/queries/qtype.ts`), and a row whose
|
||||
* naming it is the admin's job (`features/provenance/qtype.ts`), and a row whose
|
||||
* type was never recorded keeps its own `null` group rather than disappearing.
|
||||
*/
|
||||
export interface StatsTypeRow {
|
||||
@@ -396,10 +396,6 @@ export interface GroupInput {
|
||||
safe_search?: boolean;
|
||||
}
|
||||
|
||||
export interface GroupSources {
|
||||
source_ids: number[];
|
||||
}
|
||||
|
||||
export interface Blocklist {
|
||||
id: number;
|
||||
url: string;
|
||||
|
||||
Reference in New Issue
Block a user