0.2.66 - End Credits improvements / Gateway: 0.1.47 - End credits redesign

This commit is contained in:
ponzischeme89
2026-08-15 22:26:17 +12:00
parent e528d04b43
commit 4bc4b075ec
28 changed files with 643 additions and 211 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,7 +13,7 @@
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='16' fill='%2352b54b'/%3E%3Ctext x='16' y='23' font-family='system-ui,sans-serif' font-size='19' font-weight='800' text-anchor='middle' fill='%2306240a'%3EM%3C/text%3E%3C/svg%3E"
/>
<script type="module" crossorigin src="/admin/assets/index-Ax7UuMTz.js"></script>
<script type="module" crossorigin src="/admin/assets/index-B-5op1DD.js"></script>
<link rel="modulepreload" crossorigin href="/admin/assets/router-D9WH5XEU.js">
<link rel="stylesheet" crossorigin href="/admin/assets/index-DSlxKU2t.css">
</head>
+2
View File
@@ -355,6 +355,8 @@ export interface ScheduledTask {
description: string;
group: string;
intervalSeconds: number;
/** The cadence declared in code. Differs from intervalSeconds only when overridden. */
defaultIntervalSeconds: number;
enabled: boolean;
running: boolean;
nextRun?: string;
+107
View File
@@ -28,6 +28,48 @@ import type { Tone } from '../lib/format';
const IDLE_POLL_MS = 20_000;
const BUSY_POLL_MS = 3_000;
/* The cadences an operator may choose from.
*
* A fixed list rather than a free-text duration, because the useful range here spans three
* orders of magnitude and the two ways to get it wrong are both silent: a number typed in
* the wrong unit, and a cadence so tight the job never finishes before it is due again. The
* floor matches the scheduler's own it clamps anything under a minute so the console
* cannot offer a value the server would quietly change underneath it.
*
* Zero is absent on purpose. The API reads it as "restore the declared cadence" rather than
* as "never", so a "run by hand only" entry here would appear to do nothing on any task that
* declares an interval. That is a server-side limitation and it belongs in the server, not
* in a control that lies about it. */
const CADENCE_CHOICES: { value: number; label: string }[] = [
{ value: 60, label: 'Every minute' },
{ value: 300, label: 'Every 5 minutes' },
{ value: 600, label: 'Every 10 minutes' },
{ value: 900, label: 'Every 15 minutes' },
{ value: 1_800, label: 'Every 30 minutes' },
{ value: 3_600, label: 'Hourly' },
{ value: 10_800, label: 'Every 3 hours' },
{ value: 21_600, label: 'Every 6 hours' },
{ value: 43_200, label: 'Every 12 hours' },
{ value: 86_400, label: 'Daily' },
{ value: 604_800, label: 'Weekly' },
];
/* The choices for one task: the presets, plus its own declared cadence and whatever it is
* currently set to if either falls outside the list.
*
* Adding them rather than snapping to the nearest preset is what stops the control being
* destructive to look at a task declaring 45 minutes must not silently become hourly
* because somebody opened the page and the select had to show *something*. */
function cadenceChoices(task: ScheduledTask): { value: number; label: string }[] {
const choices = [...CADENCE_CHOICES];
for (const seconds of [task.defaultIntervalSeconds, task.intervalSeconds]) {
if (seconds > 0 && !choices.some((choice) => choice.value === seconds)) {
choices.push({ value: seconds, label: interval(seconds).replace(/^every /, 'Every ') });
}
}
return choices.sort((a, b) => a.value - b.value);
}
function statusTone(status: TaskRun['status']): Tone {
if (status === 'failed') return 'bad';
if (status === 'running') return 'info';
@@ -65,7 +107,32 @@ export function TasksPage() {
await reload();
});
// Named setCadence rather than setInterval so it cannot shadow the global of that
// name inside this component, which is a trap for anything added here later.
const setCadence = (task: ScheduledTask, intervalSeconds: number) =>
run(`${task.id}:interval`, async () => {
await wrap(
() => api.put(`/admin/api/tasks/${encodeURIComponent(task.id)}`, { intervalSeconds }),
`${task.name} now runs ${interval(intervalSeconds)}.`,
);
await reload();
});
// Sending zero is how the API is told to forget an override, so this is a separate call
// from the select rather than an option inside it — see CADENCE_CHOICES.
const resetCadence = (task: ScheduledTask) =>
run(`${task.id}:interval`, async () => {
await wrap(
() => api.put(`/admin/api/tasks/${encodeURIComponent(task.id)}`, { intervalSeconds: 0 }),
`${task.name} back to its default cadence.`,
);
await reload();
});
const failures = tasks.filter((task) => task.lastRun?.status === 'failed').length;
const retimed = tasks.filter(
(task) => task.defaultIntervalSeconds > 0 && task.intervalSeconds !== task.defaultIntervalSeconds,
).length;
const disabled = tasks.filter((task) => !task.enabled).length;
const groups = data?.groups ?? [];
@@ -105,6 +172,15 @@ export function TasksPage() {
icon: 'power',
tone: disabled > 0 ? 'warn' : undefined,
},
// Worth a tile of its own: a retimed task is the most likely explanation for
// "why has this not run", and it is invisible on a page that only prints the
// cadence currently in force.
{
label: 'Retimed',
value: num(retimed),
icon: 'clock',
tone: retimed > 0 ? 'note' : undefined,
},
]}
/>
@@ -133,6 +209,10 @@ export function TasksPage() {
{task.name}{' '}
{task.running ? <Tag tone="info">running</Tag> : null}
{!task.enabled ? <Tag tone="warn">off</Tag> : null}
{task.defaultIntervalSeconds > 0 &&
task.intervalSeconds !== task.defaultIntervalSeconds ? (
<Tag tone="note">retimed</Tag>
) : null}
</b>
<p>{task.description}</p>
<p className="quiet">
@@ -161,6 +241,33 @@ export function TasksPage() {
) : (
<Tag>never run</Tag>
)}
{/* Disabled while a run is in flight: changing the cadence
reschedules from now, and doing that underneath a running job
is how one run silently becomes two. */}
<select
aria-label={`How often ${task.name} runs`}
value={task.intervalSeconds}
disabled={busy === `${task.id}:interval` || task.running}
onChange={(event) => void setCadence(task, Number(event.target.value))}
>
{cadenceChoices(task).map((choice) => (
<option key={choice.value} value={choice.value}>
{choice.label}
{choice.value === task.defaultIntervalSeconds ? ' (default)' : ''}
</option>
))}
</select>
{task.defaultIntervalSeconds > 0 &&
task.intervalSeconds !== task.defaultIntervalSeconds ? (
<Button
size="sm"
icon="refresh"
busy={busy === `${task.id}:interval`}
onClick={() => void resetCadence(task)}
>
Default
</Button>
) : null}
<Toggle
label=""
checked={task.enabled}