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. |
cleanup_stale_sessions |
Clean up stale online-presence rows | stale_days (default 1) |
Deletes UserSession rows untouched for stale_days — catches connections that never got a clean disconnect (dropped carrier, killed process). |
cleanup_stale_game_sessions |
Clean up stale game-center node slots | timeout_seconds (default 3600) |
Closes GameSession rows stuck at status='active' and releases their node slot, for a door-game process that crashed/was killed without a clean exit. |
cleanup_stale_registry_entries |
Federation registry: clean up unverified entries | stale_days (default 3) |
Deletes RegistryEntry rows that never completed email verification within stale_days. A fast no-op on any install that isn't REGISTRY_MODE_ENABLED. |
cleanup_stale_presence_events |
Clean up old login/logout alert events | stale_minutes (default 60) |
Deletes PresenceEvent rows (the real-time "X just logged in/out" delivery queue) older than stale_minutes. Meant to stay small and short-lived, unlike the other cleanup handlers. |
export_jam_message_bases |
Export echo areas to JAM message base format | output_dir (default DATA_DIR/jam_export) |
Regenerates a real .jhr/.jdt/.jdx JAM message base for every echo area with JAM export enabled, so classic JAM-API door games can read it. Read-only snapshot, rebuilt from scratch every run. |
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. |
sync_fsxnet_inbound |
fsxNet IBOL/IBLC: import inbound data | none | Materializes new FSX_DAT echomail into local Oneliners/Last Callers. Auto-created when fsxNet IBOL or IBLC 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. |
Sixteen handlers ship in total. The four 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, Game Score sharing, or fsxNet
IBOL/IBLC) on a network. The four cleanup_stale_* handlers are
auto-seeded on every install too, not just fresh ones. Listed here so
none of them are 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 |
| Daily security update check | Daily 04:00 UTC | security_check |
| Weekly SQLite VACUUM | Weekly, Sunday 04:15 UTC | db_vacuum |
| Rotate oversize logs | Daily 04:45 UTC | log_rotate (max_mb: 50) |
| Clean up stale online-presence rows | Daily 05:00 UTC | cleanup_stale_sessions (stale_days: 1) |
| Clean up stale game-center node slots | Daily 05:15 UTC | cleanup_stale_game_sessions (timeout_seconds: 3600) |
| Federation registry: clean up unverified entries | Daily 05:30 UTC | cleanup_stale_registry_entries (stale_days: 3) |
| Clean up old login/logout alert events | Every 15 min | cleanup_stale_presence_events (stale_minutes: 60) |
Any of these can be disabled, deleted, or rescheduled — they're
normal rows, not special-cased. The four cleanup_stale_* events are
auto-seeded on every install, not just fresh ones — each only inserts
if its own handler key isn't already present, so an existing install
picks up a newly-added cleanup job on its next app start.
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).