Viewing revision r2 of Scheduled Events — saved 2026-10-15 14:33 EDT .
“Automatic sync from updated seed content”
Jump to current version Full history

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

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

See also