Public: anyone can read this; the crew can change it.
Settings › Features shows each scheduled tool's last run, how it went and its next run #10235
Check · Review · Land · Live done — Landed and live in production.
Settings › Features now shows one line per scheduled tool, whether or not it has ever run. Before this, only tools that had already fired got a row. Tests for the touched areas, `tsc`, `design-lint` and `gate.ts` all pass here. The forge's full check runs on the push.
shipped production is serving it deploy 9e8c29c
Settings › Features now shows one line per scheduled tool, whether or not it has ever run. Before this, only tools that had already fired got a row. Tests for the touched areas, tsc, design-lint and gate.ts all pass here. The forge's full check runs on the push.
What changed
- Line function:
src/apps/feature_schedule.tshas two new plain functions,scheduleStatusandscheduleLine. They give lines likescan every 1h · last 10:02, ok · next 11:02(UTC). - Failed: the error's sentence replaces
ok, for examplelast 10:02, the app answered 500 · next 11:02. - Never run: the line reads
scan every 1h · never run · next on the next look. - Reachability:
src/actions/features.tsaddsscheduleto the feature view, built byscheduleStatus. ThefeaturesMCP tool and the API return that view, so I did not change the tool and added no new one. I did not runtest/mcp-size.test.tson its own, only in a group with the other test files. - Page: the "Scheduled" block in
src/web/pages/features.tsreads fromschedule. It uses the existing row withtime()and adds no new class. The "Run now" button now also shows for a tool that has never run. - Docs:
docs/features/absorb.mdno longer has "Not done yet". The Schedule section says where to see the line. - Tests: the new
test/feature-schedule-line.test.tsasserts the line for ok, failed and never-run. It cannot pass on main becausescheduleLinedoes not exist there. Intest/features.test.tsthe fixture now declares a schedule. The page test checks the new line and the never-run state; before, it expected the Scheduled block to disappear once the fire record was cleared.
What I chose not to do
- No history beyond the last fire.
- No local-time rendering.
- The never-run line says "next on the next look", because the scheduler checks once a minute and fires anything that has never run.
How to try it
Absorb an app that declares a schedule, then open /settings/features/<slug>.
The check failed on the gate rule that wants a Try: line in a commit message. I added an empty commit with that line and pushed. bun scripts/gate.ts now passes here, but I haven't seen the forge's check re-run on the push.
The failing rule said no commit on the pull had a Try: line, since the pull touches src/actions/features.ts and src/web/pages/features.ts. No code or tests changed.
Try: on the preview, open /settings/features/<slug> for an absorbed app that declares a schedule. Each entry shows scan every 1h · last 10:02, ok · next 11:02, or the error's sentence when it failed, or never run before its first fire.
Two fixes, then this lands. (1) scheduleLine has no caller outside its own test: neither the page nor the features tool uses it, and the page re-implements the line by hand. Box 1 asks for one plain function both reuse. (2) The terminal still shows the old fires, so the new line is not in both doors. Also fix the absorb.md sentence that says the tool returns scheduleLine; it returns ScheduleStatus data. No security findings: output is escaped through html, and the route and its checks are unchanged.
Settings › Features, the features tool and the terminal now all get the scheduled-tool wording from one function, scheduleMeta in src/apps/feature_schedule.ts. The type check, the gate, design-lint and the three test files that cover this change pass. I did not run the full suite; the forge's check does that on the push.
- One wording.
scheduleMeta(s)returns the pieces afterscan every 1h:never run · next on the next look, orlast X, ok|<error> · next Y.scheduleLinejoins those pieces with UTCHH:MMclocks. The page maps the same pieces to markup: a<time>for the last run, the error in<span class="danger">(so a failed run keeps its danger styling), and a<time>for the next run. - Tool payload. Each
ScheduleStatusfromscheduleStatusnow carriesline, sofeaturesreturns the sentence along with the status data. - Terminal. The feed screen (
src/tui/screens/featurefeed.ts) now prints thoselines afterScheduled:, instead of the oldx.ok/x.errorfires. Therkey still runs fromfires. - Next-run bug fixed. My earlier commit passed
{ date: false }totime(), which does nothing, so a future next run showed as "just now". It now showsHH:MM UTC. - absorb.md. The sentence now says the tool returns
scheduleas status data plusline, and that the terminal's feed screen prints the lines underScheduled:.
Tests
- Page: it now asserts the full row for a failed run, with the escaped error and the
next <time>, and the exact never-run sentence. - Tool payload: it asserts
schedule[0].linefor both the failed and never-run cases. - Terminal: it asserts the full
Scheduled: scan every 1h · last … · next …line. Its fixture gained ascantool and an hourly schedule, because it declared none before.
Not done: the time() helper still ignores its date option for how the relative text is worded; I left it alone.
To try it, open /settings/features/radar on the preview, https://lab--10235--forge-trevato-apps.open-platform.sh, for an absorbed app with a schedule.
🤖 Generated with Claude Code
The idea is right and the tests for the line are good, but the page departs from the design system and loses information it showed before. Fix in one round: (1) The page hand-rolls a <time> for the next run and uses time() for the last run, so one row reads last 2m ago, ok · next 11:02 UTC. Use one clock for both (the existing time()), no hand-rolled markup. (2) The old row showed durationMs; the new one drops it. Keep it. (3) 'next on the next look' is the scheduler's jargon (principle 8); say 'next soon' or 'due now'. (4) Drop the p.at === x.lastAt identity test that picks the format; have scheduleMeta tag the pieces (last / next). (5) absorb.md: the Schedule paragraph is one dense sentence; split it, and put the status/line detail in feature-schedule.md. (6) The page test depends on the text '2m ago'; make it independent of the clock. (7) The failed-fire ?? "failed" fallback is written twice; keep it once. Boxes: 1 is mostly met but the page and terminal word the line differently; 2 and 3 are met.
Each scheduled tool now gets one line on Settings › Features › the app, in the features tool and in the terminal's feed screen. It reads scan every 1h · last 10:02, ok · 42 ms · next 11:02; a failure's error sentence replaces ok, and before the first run it reads scan every 1h · never run · next soon. The type check, scripts/gate.ts, design-lint and the three test files this change reaches (test/features.test.ts, test/features-tui.test.ts, test/feature-schedule-line.test.ts) pass locally; the full suite runs on the forge.
Where it lives
scheduleStatusinsrc/apps/feature_schedule.tsbuilds one status per entry from the app's schedule and the last fire of each tool. It decides the failure sentence (?? "failed") once, carriesdurationMs, and adds the plain-textline.scheduleMetawords the line once and tags its pieces aslast,nextorerror.scheduleLineprints those pieces as plain text with UTC clocks.
Each door
- Page: it renders the same pieces with the existing
time(), with no hand-built<time>and no comparing instants to pick a format. The last run shows as relative time (2m ago) and the next run as its day, since the design system says that is howtime()words an instant still ahead. Both carry the exact instant in their tooltip. A failure keeps the danger styling, and the duration (· 42 ms) is back as before. featurestool and API:GET /api/v1/features/:slugreturnsschedule, one{ tool, every, lastAt, ok, error, durationMs, nextAt, line }per entry, alongside the existingfires.- Terminal: Feature feeds › the app prints those
lines afterScheduled:. Pressingrstill runs the scheduled tools.
Docs: the Schedule paragraph in docs/features/absorb.md is now short sentences that point to docs/features/feature-schedule.md. That file, under "Read it", gives the full line, what each door shows, and the fields of schedule.
Tests: the page assertion checks the whole row, including <time> elements, escaped danger text, the duration and the next day, without depending on the current time. The tool payload and the terminal line are asserted in full. The terminal test's fixture gained a scan tool and a schedule, because it declared neither before.
Choices made
- An hourly tool's next run reads as a date on the page, which is coarse. Teaching
time()to word future instants (in 58m) would change a shared helper used by every page, so I left it for its own issue. - The text line keeps UTC
HH:MMclocks, because a reader of the tool or the terminal has no tooltip to fall back on.
To try it, open /settings/features/radar on https://lab--10235--forge-trevato-apps.open-platform.sh for an absorbed app with a schedule, then press Run now.
🤖 Generated with Claude Code
Earlier asks: (1) one plain function reused by the page and the tool: met. scheduleMeta words the line once, scheduleStatus carries line into the features view and API, and the page maps the same pieces to markup. (2) The terminal now prints the schedule lines, not the old fires: met. (3) absorb.md no longer says the tool returns scheduleLine: met, and Not done yet is gone. No security findings; output stays escaped through html and the new test checks the escaped error. Two things I would do next: (a) teach time() to word future instants (in 58m) so an hourly next run is not shown as a bare date on the page; (b) run test/mcp-size.test.ts on its own, since the features view grew by a schedule array.
Settings › Features now shows one line per scheduled tool, whether or not it has ever run. Before this, only tools that had already fired got a row. Tests for the touched areas,
tsc,design-lintandgate.tsall pass here. The forge's full check runs on the push.What changed
src/apps/feature_schedule.tshas two new plain functions,scheduleStatusandscheduleLine. They give lines likescan every 1h · last 10:02, ok · next 11:02(UTC).ok, for examplelast 10:02, the app answered 500 · next 11:02.scan every 1h · never run · next on the next look.src/actions/features.tsaddsscheduleto the feature view, built byscheduleStatus. ThefeaturesMCP tool and the API return that view, so I did not change the tool and added no new one. I did not runtest/mcp-size.test.tson its own, only in a group with the other test files.src/web/pages/features.tsreads fromschedule. It uses the existing row withtime()and adds no new class. The "Run now" button now also shows for a tool that has never run.docs/features/absorb.mdno longer has "Not done yet". The Schedule section says where to see the line.test/feature-schedule-line.test.tsasserts the line for ok, failed and never-run. It cannot pass on main becausescheduleLinedoes not exist there. Intest/features.test.tsthe fixture now declares a schedule. The page test checks the new line and the never-run state; before, it expected the Scheduled block to disappear once the fire record was cleared.What I chose not to do
How to try it Absorb an app that declares a
schedule, then open/settings/features/<slug>.Closes #10200