v0.16.0
Changed
defaultsmodule split.src/routines/defaults.rsis now a module (defaults/); each
built-in routine lives in its own file (update_moadim.rs,the_1_percent.rs). Pure
refactor — no behaviour change.
Added
- Group-by dimension for the Cron Jobs table. A new GROUP BY selector in the
Cron Jobs toolbar lets operators partition the flat job list into labelled sections
by Handler, Machine, or Status (enabled/disabled). Within each group the
active column sort still applies; groups are ordered alphabetically for a stable
layout.None(the default) preserves the existing flat-list behaviour. Backed by
a puregroup_jobs()/group_key()function covered by 16 new host-only tests.
Follows the first-class grouping pattern in Airflow's DAG list, GitHub Actions
workflow runs, and Temporal namespace views — orthogonal to filtering so operators
can filter and group simultaneously. Pure frontend — no backend change.
Closes #714. - Dedicated LAST FIRE column in the Routines table. The most-recent trigger
timestamp is now shown in its own LAST FIRE column directly beside NEXT RUN,
matching the side-by-side "last run / next run" pattern standard in Airflow, Temporal,
and Kubernetes CronJob dashboards. A ↻ prefix marks manual triggers; ⏱ marks
scheduled fires; routines that have never been triggered show—. The trigger data
was already returned by the API — it was previously buried as a sub-line under the
UPDATED cell where it was easy to miss. Pure frontend — no backend change.
Closes #660. Closes #688. - Schedule fire preview on Cron Jobs and Routines pages. Every schedule cell now has a
▸ fires toggle button. Clicking it expands an inline panel listing the next 10 scheduled
fire times for that job or routine (absolute time + relative countdown per entry); clicking
again collapses it. Implements the per-job forward-projection pattern used by Cronitor,
BetterStack, and Cloud Scheduler — operators can verify an expression after editing or check
whether a job falls inside a maintenance window without guessing from the human description.
Pure frontend:next_fires(schedule, now, n)iterates the existing croner iterator and
collects up tondatetimes; no backend change. Closes #704. - Calendar view for the Cron Jobs page. The Cron Jobs page gains a CALENDAR
view alongside the existing LIST and DAY views, matching the three-view layout
of the Routines page. Operators can browse a 6-week monthly grid showing how
many times each enabled job fires per day, with prev/next/today navigation.
Calendar grid helpers (WEEKDAYS,CAL_MONTHS,GRID_CELLS,MAX_OCCURRENCES,
month_start,occurrences_per_day) are extracted fromroutines.rsinto the
sharedschedulemodule so both pages share the same implementation. - Global routine lock — UI banner and REST API. The Routines page shows an amber banner
when a global lock is active, listing which sentinel(s) are present (SHARED / LOCAL) with an
UNLOCK ALL button that removes both viaDELETE /api/v1/routines/lock?scope=all. Three
new REST endpoints expose lock management:GET /routines/lock(status),POST /routines/lock
(create sentinel; scope=shared|local),DELETE /routines/lock(remove; scope=shared|local|all). - Global routine lock. Create
~/.config/moadim/.lock(committed, shared via git) or
~/.config/moadim/.local.lock(gitignored, machine-local) to pause all routine scheduling
and manual triggers without touching individual routineenabledstates. Removing the file(s)
restores prior state. Three new MCP tools —get_lock_status,lock_routines,
unlock_routines— manage the sentinels and immediately re-sync the crontab. Blocked triggers
return HTTP 423 Locked. - Bulk actions for the Routines page. Each routine row now has a leading selection
checkbox; a header checkbox toggles "all visible selected ↔ none" (respects the active
filter so hidden rows are never touched). When at least one routine is selected, a
floating bulk-action bar appears with ENABLE, DISABLE, and DELETE actions plus
a CLEAR affordance. Bulk enable/disable firesPATCH /routines/{id}for each
selected routine and surfaces a single summary toast. Bulk delete shows a confirmation
dialog and removes viaDELETE /routines/{id}. Selection is automatically pruned on
reload so stale IDs never carry over. Pure frontend — no backend change. Closes #676. - Token Trim default routine. A new built-in weekly routine (Sundays 07:00) that audits
routine prompts for redundancy, verbosity, dead scaffolding, and duplication, then opens one
PR per week that reduces LLM token consumption without degrading output quality. - Light/dark theme toggle. A ☀/🌙 button in the header switches between the dark
terminal aesthetic and a clean light palette. The choice persists tolocalStorage
undermoadim.themeand is applied flash-free via an inline<head>script before
the first paint. The⌘Kcommand palette gains a "Toggle Theme" entry so
keyboard-first operators never need to reach for the mouse. All colours are pure CSS
custom-property overrides — no per-component changes. Closes #664. - Sortable column headers for the Cron Jobs table. Clicking any column header
(ID, HANDLER, NEXT RUN, ENABLED, UPDATED) sorts the table by that field; clicking
again reverses direction. An arrow indicator shows the active sort column and
direction. Sort state lives in component memory (no URL pollution) and resets to the
server's natural order on page reload. Pure client-side — no backend change.
Closes #657, #669. - NEXT RUN countdown column in the Routines table. The Routines table gains a
live NEXT RUN column (absolute fire time + relative countdown + due-soon accent)
matching the already-shipped column on the Cron Jobs page, so operators see per-routine
next-fire times at a glance without navigating to the Overview. Disabled routines show
paused; invalid or exhausted schedules show—; countdowns turn green inside the
1-hour due-soon window. A 30 s background tick keeps countdowns live between data
fetches. Pure client-side computation from the existingschedulefield — no backend
change. Closes #653. - Cross-filterable KPI tiles + DueSoon facet for Routines page. The Routines
page's static stat cards are replaced by clickable<button>tiles with
aria-pressed; clicking ENABLED, DISABLED, or DUE SOON applies that status
filter to the list, and clicking the active tile clears it. A newDueSoon
status facet selects routines whose next scheduled fire lands within the next
hour (same 1-hour window used by the Cron Jobs page). A live 30-secondnow
tick keeps the DueSoon count current between data fetches. The STATUS dropdown
in the filter bar gains a "Due soon" option. The/key shortcut focuses the
search box when the user is not already typing in a field. Closes #652. - Enhanced log viewer. The per-job and per-routine log panel gains line numbers,
a keyword search bar with match highlighting and navigation arrows, and an
auto-tail toggle that keeps the viewport pinned to the last line as new output
arrives. Closes #646. - "The 1 Percent" built-in default routine. A new daemon-managed default that fires
daily at 08:00 and audits the user's automation portfolio across six dimensions (coverage
gaps, redundancy, dead weight, prompt quality, schedule hygiene, machine targeting). Each
run it picks the single highest-impact improvement and opens a pull request on the routines
repository. If the routines folder is not a git repository the routine self-disables via
update_routine. Closes #640. - Fleet schedule heatmap. A new HEATMAP page (
/heatmap) renders a forward-looking
7-day × 24-hour fire-density grid that aggregates the next week's schedule of every
enabled cron job and routine into one color-coded matrix, so an operator can see
fleet-wide busy windows, scheduling collisions, and open time slots at a glance.
Three toggle buttons filter the grid to ALL / CRON / ROUTINES, and the current day
and hour are highlighted. The grid auto-refreshes every 30 s and the "now" column
advances every minute. Pure host-testable aggregation math; no backend change.
Closes #625. - Live auto-refresh for the cron-jobs & routines tables. Each list's action row
gains a Grafana/Datadog-style refresh-interval selector (Offdefault,5s,15s,
30s,60s) plus an "updated Ns ago" freshness cue, so an operator can keep the data
current on a cadence they choose instead of reloading the SPA. The choice persists to
localStorageunder a shared key, so it is consistent across both pages and survives
navigation and reload;Offpreserves the historical load-once behaviour (no background
traffic until opted in). Re-fetches use the existingGET /api/v1/cron-jobs/
GET /api/v1/routinesendpoints — no backend change. Closes #618. - Operations overview landing page. The root
/route now serves a single-pane
OVERVIEW summary that aggregates both cron jobs and routines, so an operator sees
whole-system state at a glance: five cross-entity KPI tiles (SCHEDULED,ENABLED,
DUE SOON,DISABLED,NEXT RUNwith a live countdown) and an UPCOMING RUNS table
of the next 8 fires across every enabled job and routine, each taggedCRON/ROUTINE.
Closes #606. NEXT RUNcolumn andDUE SOONKPI tile. The scheduled-jobs table gains a
NEXT RUNcolumn showing the absolute next fire time plus a relative countdown
(in 5m,in 2h 10m,tomorrow 09:00); disabled jobs readpausedand the countdown
turns red once a fire lands inside the due-soon window. A newDUE SOONKPI tile counts
enabled jobs firing within the next hour, and a 30 s tick keeps countdowns live without
a manual reload. Closes #597.- Faceted filter toolbar for the Routines page. The single repository-URL
substring filter is replaced with a multi-facet toolbar matching the Cron Jobs
page (Airflow / GitHub Actions / Buildkite best practice: free-text + facets +
live result count). New facets: full-text search across title, agent, schedule,
schedule description, and repository URLs; status (All / Enabled / Disabled /
Dormant); agent (Any / claude / codex / …); machine (Any / Unassigned / specific).
A live "Showing N of M" count updates with each keystroke, a CLEAR button appears
when any filter is active, and the empty state distinguishes "no routines yet"
from "no matches — clear filters". Pure filter logic is extracted to free
functions with 31 new host-side unit tests. Closes #642.
Changed
- Enabled the
clippy::redundant_closure_for_method_callslint and fixed the
violations, replacing closures that only forward their receiver to a method
(|e| e.ok(),|s| s.to_string(),|p| p.into_inner()) with the method
path itself (Result::ok,ToString::to_string,
std::sync::PoisonError::into_inner). No behavior change. - Pinned the
AppErrorHTTP response body contract: tests now assert that
every variant serializes to{"error": <message>}, not just the right status
code, so the JSON error envelope clients parse can't silently regress. Tests
only; no behavior change. - Marked every public path builder in
paths(jobs_dir,routine_toml_path,
pid_file,moadim_home, …)#[must_use]. These functions are pure and the
returnedPathBufis the whole point of calling them, so discarding it is
always a mistake; the attribute lets clippy flag such a call at compile time
instead of letting it silently no-op. No behavior change.
Fixed
-
The max-runtime watchdog now runs on its own 30s cadence instead of riding the
hourly cleanup sweep, so a hung run is force-killed within ~30s of its
effective_max_runtime_secsrather than surviving up to ~1h past its bound. A
sub-hourmax_runtime_secs(or a sub-hour cron interval) is now actually
enforceable. TTL-reaping of finished workbenches stays on the hourly sweep.
(#436) -
Removed a duplicate
.logo { font-weight }declaration inui/index.htmlleft by
the concurrent merge of #595 and #596; identical rendering, cleaner CSS. Closes #599.