diff --git a/app/src/dev/seedRoutines.ts b/app/src/dev/seedRoutines.ts index 617e4447..7c5347e9 100644 --- a/app/src/dev/seedRoutines.ts +++ b/app/src/dev/seedRoutines.ts @@ -27,6 +27,7 @@ export function seedRoutines(fake: FakeBotloft, bots: { scout: BotId; analyst: B messageId: null, createdAt: fake.now - 2 * HOUR, finishedAt: fake.now - HOUR, + signal: null, ...run, }; }; @@ -62,4 +63,14 @@ export function seedRoutines(fake: FakeBotloft, bots: { scout: BotId; analyst: B }); backup.enabled = false; backup.nextRunAt = null; + + // Spec 20.13: runs when Analyst says the numbers are in. + const recheck = create(bots.scout, { + name: "Recheck the sources", + prompt: "The numbers changed: check that every claim in the report still has a source.", + schedule: { kind: "signal", name: "numbers-updated" }, + }); + lastRun(recheck, { + signal: { name: "numbers-updated", fromBotId: bots.analyst, note: null }, + }); } diff --git a/app/src/features/routines/MoreOptions.tsx b/app/src/features/routines/MoreOptions.tsx index 994fe16d..6801776a 100644 --- a/app/src/features/routines/MoreOptions.tsx +++ b/app/src/features/routines/MoreOptions.tsx @@ -1,6 +1,7 @@ // "More options" in the routine form (spec 20.9): the time zone, what to // do when the last run has not finished or the computer was off, and a -// cron expression for those who want one. +// cron expression for those who want one. A routine that waits for a +// signal (spec 20.13) has no time: only the overlap rule is left. import { ChevronRight } from "lucide-react"; import { useT } from "../../i18n"; @@ -18,6 +19,7 @@ export function MoreOptions({ }) { const d = useT().routines.dialog; const zones = allZones(); + const timed = form.frequency !== "signal" || form.useCron; return (
@@ -25,12 +27,14 @@ export function MoreOptions({ {d.more}
- change({ timezone: event.target.value })} - options={zones.map((zone) => ({ value: zone, label: zoneLabel(zone) }))} - /> + {timed && ( + change({ timezone: event.target.value })} + options={zones.map((zone) => ({ value: zone, label: zoneLabel(zone) }))} + /> + )}

{d.overlap}

change({ overlap })} />
-
-

{d.missed}

- change({ missed })} - /> -

{d.wakeNote}

-
- + {timed && ( +
+

{d.missed}

+ change({ missed })} + /> +

{d.wakeNote}

+
+ )} + {timed && ( + + )} {form.useCron && ( - + {error && (

diff --git a/app/src/features/routines/RoutineRequestCard.tsx b/app/src/features/routines/RoutineRequestCard.tsx index 59cc4b5c..5221c35f 100644 --- a/app/src/features/routines/RoutineRequestCard.tsx +++ b/app/src/features/routines/RoutineRequestCard.tsx @@ -132,7 +132,7 @@ export function RoutineRequestCard({ approval, bot }: { approval: ApprovalItem; rows={4} onChange={(event) => change({ prompt: event.target.value })} /> - +

{r.explain(runner.name)}

{error && ( diff --git a/app/src/features/routines/RoutineRow.tsx b/app/src/features/routines/RoutineRow.tsx index c461baf3..053ce9bc 100644 --- a/app/src/features/routines/RoutineRow.tsx +++ b/app/src/features/routines/RoutineRow.tsx @@ -73,9 +73,13 @@ export function RoutineRow({ {describeSchedule(routine.schedule, r.when)} {" · "} - {routine.enabled && routine.nextRunAt !== null - ? r.next(nextText(routine.nextRunAt, routine.timezone, r)) - : r.off} + {!routine.enabled + ? r.off + : routine.schedule.kind === "signal" + ? r.waitsSignal + : routine.nextRunAt !== null + ? r.next(nextText(routine.nextRunAt, routine.timezone, r)) + : r.off}

{routine.lastRun && } @@ -112,12 +116,18 @@ export function RoutineRow({ function LastRun({ run, words }: { run: RoutineRun; words: Messages["routines"]["last"] }) { const line = "mt-1 flex items-center gap-1.5 text-xs"; + // Who sent the signal it ran for (spec 20.13), while that bot is around. + const sender = useApp((state) => + run.signal?.fromBotId ? (state.bots[run.signal.fromBotId]?.name ?? null) : null, + ); + const from = sender && · {words.signalFrom(sender)}; switch (run.status) { case "queued": return (

{words.queued} + {from}

); case "done": @@ -125,6 +135,7 @@ function LastRun({ run, words }: { run: RoutineRun; words: Messages["routines"][

{words.done} + {from}

); case "failed": @@ -132,6 +143,7 @@ function LastRun({ run, words }: { run: RoutineRun; words: Messages["routines"][

{words.failed} + {from}

); case "skipped": { @@ -140,11 +152,14 @@ function LastRun({ run, words }: { run: RoutineRun; words: Messages["routines"][ ? words.missed(run.skippedCount) : run.reason === "bot_paused" ? words.bot_paused - : words.overlap; + : run.reason === "too_soon" + ? words.too_soon + : words.overlap; return (

{text} + {from}

); } diff --git a/app/src/features/routines/ScheduleFields.tsx b/app/src/features/routines/ScheduleFields.tsx index dc6930ce..6b9f3101 100644 --- a/app/src/features/routines/ScheduleFields.tsx +++ b/app/src/features/routines/ScheduleFields.tsx @@ -13,18 +13,23 @@ const control = export function ScheduleFields({ form, change, + bot, }: { form: RoutineForm; change(patch: Partial): void; + /** The bot's name, for the signal's hint. */ + bot: string; }) { const d = useT().routines.dialog; const timeId = useId(); const everyId = useId(); + const signalId = useId(); const frequencies: { value: Frequency; label: string }[] = [ { value: "daily", label: d.frequency.daily }, { value: "weekdays", label: d.frequency.weekdays }, { value: "days", label: d.frequency.days }, { value: "interval", label: d.frequency.interval }, + { value: "signal", label: d.frequency.signal }, ]; const toggleDay = (day: number) => @@ -65,7 +70,24 @@ export function ScheduleFields({ })} )} - {form.frequency === "interval" ? ( + {form.frequency === "signal" ? ( +
+
+ + change({ signal: event.target.value })} + className={`${control} w-56`} + /> +
+

{d.signalHint(bot)}

+
+ ) : form.frequency === "interval" ? (