The audit log, the server log and the admin quota view were three pages (the quota one a Stats tab),
each with its own filters, and the background runs had no list at all. Now one page ("Napló" /
"Activity") reads all four, each still its own table on the server: a shared bar (text, a period of
1 h to 90 days defaulting to 7, an account) and each tab's own filters, the whole view in the URL.
The old page ids open their tab.
Server side: the audit log and the new run list (`/api/admin/runs`) filter and page like the server
log, through one keyset helper (`routes/_keyset`) the log now shares; an audit row keeps the request
it was made in (migration 0111), so its log lines are a pivot away, as a run's are (`lrun`). The
quota breakdown takes since/until and one account. The Scheduler rows get a history button to their
job's runs.
Also fixed on the way: re-saving the value in force on the Configuration page staged its override
without committing it (the commit sat inside the audit branch), so the save was lost.
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
// The activity page's view in the address bar: a link carries the tab and every filter, a hand-edited
|
|
// value the API would refuse degrades to "no filter", a filter at its default leaves no key behind,
|
|
// and a link to a page this one replaced opens that page's tab.
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
ACTIVITY_KEYS,
|
|
activityParamsToState,
|
|
activityParamUpdates,
|
|
DEFAULT_ACTIVITY_STATE,
|
|
sinceOf,
|
|
type ActivityUrlState,
|
|
} from "./activityUrlState";
|
|
|
|
const params = (updates: Record<string, string | null>) =>
|
|
new URLSearchParams(Object.entries(updates).filter((e): e is [string, string] => e[1] !== null));
|
|
|
|
describe("activityUrlState", () => {
|
|
it("round-trips the tab and every filter", () => {
|
|
const state: ActivityUrlState = {
|
|
tab: "admin",
|
|
window: "30d",
|
|
user: "none",
|
|
q: "100% done",
|
|
job: "channel_backfill",
|
|
status: "interrupted",
|
|
action: "config_set",
|
|
};
|
|
expect(activityParamsToState(params(activityParamUpdates(state)))).toEqual(state);
|
|
});
|
|
|
|
it("leaves no key for a filter at its default, and opens on the runs of the last 7 days", () => {
|
|
expect(
|
|
Object.values(activityParamUpdates(DEFAULT_ACTIVITY_STATE)).every((v) => v === null),
|
|
).toBe(true);
|
|
expect([DEFAULT_ACTIVITY_STATE.tab, DEFAULT_ACTIVITY_STATE.window]).toEqual(["runs", "7d"]);
|
|
});
|
|
|
|
it("drops a value the API would refuse instead of sending it", () => {
|
|
const state = activityParamsToState(
|
|
new URLSearchParams(
|
|
"at=everything&aw=1y&au=me&rjob=Not-A-Job&rst=finished&aact=DROP%20TABLE",
|
|
),
|
|
);
|
|
expect(state).toEqual(DEFAULT_ACTIVITY_STATE);
|
|
});
|
|
|
|
it("opens the tab of the page a link still names", () => {
|
|
expect(activityParamsToState(new URLSearchParams("page=audit")).tab).toBe("admin");
|
|
expect(activityParamsToState(new URLSearchParams("page=logs")).tab).toBe("logs");
|
|
// A tab the link names wins over the retired page's.
|
|
expect(activityParamsToState(new URLSearchParams("page=logs&at=quota")).tab).toBe("quota");
|
|
});
|
|
|
|
it("clears its own keys and the server log's on a page change", () => {
|
|
expect(ACTIVITY_KEYS).toEqual(expect.arrayContaining(["at", "aw", "au", "aq", "rjob", "lreq"]));
|
|
});
|
|
|
|
it("starts the window that many hours back", () => {
|
|
const now = Date.UTC(2026, 9, 18, 12);
|
|
expect(sinceOf("24h", now)).toBe("2026-10-17T12:00:00.000Z");
|
|
expect(sinceOf("90d", now)).toBe("2026-07-20T12:00:00.000Z");
|
|
});
|
|
});
|