0.2.66 - End Credits improvements / Gateway: 0.1.47 - End credits redesign
This commit is contained in:
-12
File diff suppressed because one or more lines are too long
+12
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user