Scheduled Events
A built-in cron replacement. Sysop-only, Admin → Scheduled Events
(/admin/events/). Maintenance jobs run inside the same process as
the web app, on a one-minute tick, with results visible right in the
admin UI — no need to grep syslog to find out if last night's VACUUM
actually ran.
How it works
A background thread wakes every 60 seconds, loads every row with
Enabled checked, and fires any whose schedule says it's due. Each
row records last_run_at, last_status (ok/fail),
last_duration_ms, and the first 4KB of captured output — all shown
in the admin list. Editing or disabling a row takes effect on the
next tick (within a minute); no restart needed.
There's no per-event timeout enforced by the scheduler itself — a
handler that hangs blocks the next tick's events. Handlers that shell
out to something that could run long (like shell, below) enforce
their own timeout instead.
Creating an event
- Name — free text, shown in the admin list only.
- Handler — picks the built-in function that runs (see table
below). Each option shows its description and expected params once
selected. - Schedule — one of four kinds:
- Daily — pick a time (
HH:MM, UTC). - Hourly — pick a minute-of-hour (0–59).
- Weekly — pick a day (Mon–Sun) + time, UTC.
- Every N min ("interval") — repeats N minutes after the
previous run finished, not wall-clock-aligned. - Params (JSON object) — handler-specific arguments. Must parse
as a JSON object, even if empty ({}) — a bare string or path is
rejected. A handler taking one argument calledcommandwants
{"command": "/path/to/script.sh"}, not just the path. - Enabled — unchecked rows never fire but stay in the list.
There's also a Run now button per row for testing without waiting
on the schedule — same code path either way, so output shown after a
manual run matches exactly what a scheduled fire would produce.
All schedule times are UTC, not the server's local timezone or
any caller's timezone — keep that in mind when picking a "quiet
hours" slot.
Built-in handlers
| Handler key | Label | Params | Notes |
|---|---|---|---|
noop |
No-op (test) | none | Does nothing; returns ok. Use to confirm the scheduler thread is alive. |
tw2_maint |
Trade Wars 2002 maint | none | Runs TW2002's headless daily maintenance (Cabal move, inactive-player sweep). |
db_vacuum |
SQLite VACUUM | none | Reclaims free pages, defragments, refreshes planner stats. Skips (returns ok) on non-SQLite backends — Postgres autovacuums itself. |
log_rotate |
Rotate large logs | max_mb (default 50) |
Any logs/*.log over the threshold is renamed to .1 and a fresh empty file takes its place. |
security_check |
Security update check | none | Scans apt list --upgradable and the venv's pip list --outdated, tags Ubuntu -security rows, writes a report consumed by Admin → Security. Always returns ok even on a non-Ubuntu box, so a missing apt doesn't permanently red-flag the row. |
hub_generate_nodelist |
ANotherNetwork: generate nodelist | none | Publishes the ANotherNetwork nodelist into the ANN.FILES.NODELIST file area, replacing the prior copy. Only meaningful on the install designated as the ANotherNetwork hub (REGISTRY_MODE_ENABLED) — elsewhere it still runs harmlessly, publishing a nodelist with just the hub entry. |
sync_wall_inbound |
InterBBS Wall: import inbound posts | none | Materializes new inbound InterBBS Wall echomail into local Wall posts. Auto-created when InterBBS Wall is enabled — you won't add this by hand. |
sync_lastcallers_inbound |
InterBBS Last Callers: import inbound entries | none | Materializes new inbound echomail into local Last Callers entries. Auto-created when InterBBS Last Callers sharing is enabled. |
sync_scores_inbound |
InterBBS Game Scores: import inbound scores | none | Materializes new inbound echomail into local game high scores. Auto-created when InterBBS Score Sharing is enabled. |
shell |
Shell command | command (required), timeout (default 60s) |
Runs an arbitrary command as the service user. No sudo — anything needing root privileges silently fails at that step. Output is captured as raw bytes and decoded with errors='replace', so non-UTF8 output (e.g. CP437 from a DOS program) doesn't crash the handler. |
The three sync_*_inbound handlers aren't something you'd pick from a
blank slate — they appear already-created and enabled the moment you
turn on the matching InterBBS feature (Wall, Last Callers, or Game
Score sharing) on a network. Listed here so they aren't a mystery if
you spot one.
shell handler — worked example
Running a nightly TradeWars external-events script via dosemu2:
Params (JSON object):
{"command": "/opt/anetbbs/doors/dos/tw/event.sh", "timeout": 120}
event.sh:
#!/bin/bash
export TERM=linux
cd /opt/anetbbs/doors/dos/tw
/usr/libexec/dosemu2/dosemu2.bin -td EXTERN.EXE
export TERM=linux and -td (dumb-terminal mode) are required
because the scheduler runs the command with no controlling tty — a
normal door launch gets a real PTY from door_runner.py, but a
scheduled shell command doesn't. Without these, dosemu2 fails with
ERROR: TERM environment variable needs set. before it ever reaches
your DOS program.
Default events on a fresh install
Seeded once (idempotent — re-running the seeder skips handler keys
that already have a row):
| Name | Schedule | Handler |
|---|---|---|
| TW2 daily maintenance | Daily 03:30 UTC | tw2_maint |
| Weekly SQLite VACUUM | Weekly, Sunday 04:15 UTC | db_vacuum |
| Rotate oversize logs | Daily 04:45 UTC | log_rotate (max_mb: 50) |
| Daily security update check | Daily 04:00 UTC | security_check |
Any of these can be disabled, deleted, or rescheduled — they're
normal rows, not special-cased.
On installs with REGISTRY_MODE_ENABLED=true (the designated
ANotherNetwork hub), a fifth event is also seeded:
| Name | Schedule | Handler |
|---|---|---|
| ANotherNetwork: weekly nodelist | Weekly, Sunday 05:00 UTC | hub_generate_nodelist |
This one is skipped on regular (non-hub) installs — there's nothing
useful for it to publish there.
Troubleshooting
- "Params (JSON object)" rejects what I typed — it must parse as
a JSON object./path/to/script.shisn't valid JSON; wrap it as
{"command": "/path/to/script.sh"}. - dosemu2 commands fail with
TERM environment variable needs set— see the worked example above; exportTERMand pass
-tdin your script. - An event always fails the same way — click Run now and
read the captured output; it's the same stdout/stderr you'd get
running the command by hand over SSH, just minus your interactive
shell's environment (PATH, TERM, etc. may differ — set them
explicitly in the script if needed).