ANetBBS Changelog
Current release: v1.0.39 (August 2026). This file covers v1.0.0
onward, which follows standard semantic versioning — patch releases are
v1.0.1, v1.0.2, and so on. The full internal beta build-number
history (v1.0a1.1 through v1.0b2.239) that got the project to this
release is preserved in
CHANGELOG-beta.md.
v1.0.24 — PETSCII MRC chat client; colored PETSCII menus (August 2026)
New: MRC chat is now available on PETSCII (C64/128) sessions. Previously MRC was never offered to PETSCII users at all — not gated, just never built. anetbbs/features/mrc_chat.py's MRCChat class turned out to already have a complete, working plain-scroll fallback rendering mode built in and self-guarded throughout (_emit(), _draw_status_line(), etc. all already check if not self._split_screen:), just never reachable by a real terminal because the ANSI split-screen setup (CPR terminal-size probing, DECSTBM scroll regions, cursor-addressed draws) always ran first and silently failed on a real C64. New anetbbs/features/mrc_chat_petscii.py::PetsciiMRCChat subclasses MRCChat and overrides exactly three methods to force that plain-scroll mode instead — everything else (the bridge websocket connection, JSON protocol, ping/pong keepalive, slash commands) is inherited unchanged. Available from both the built-in PETSCII menu and sysop-built custom PetsciiMenu trees.
PETSCII menus can show real color now. The ANSI-to-PETSCII color translation added previously was already fully wired through session.write() — menu screens just never embedded any color codes to translate. New anetbbs/features/petscii_theme.py (the PETSCII counterpart to the ANSI side's ansi_ui.py) adds a colored reverse-video header bar and colored menu hotkeys to both the built-in menu and sysop-built custom menus, which also gain a per-menu color picker in the admin UI.
v1.0.23 — "Who's online" now shows every simultaneous connection per user (August 2026)
Fixed a real bug: a user logged in via both web and SSH at once only ever showed up once in "who's online." Root cause: UserSession.user_id was unique=True — a hard one-row-per-user constraint — so a second simultaneous connection's presence write found and overwrote the first connection's row instead of getting its own. Removed the constraint and gave each connection its own identity (session_key, not user_id): a fresh UUID per terminal session (anetbbs/core/presence.py::SessionPresence), and one stashed in the signed session cookie per browser session (anetbbs/web_app.py::track_user_session()). /who/, the terminal Who's Online screen, the admin control panel, and the sysop whoison console command all now correctly show one row per connection instead of one row per user.
Bounded the table's growth now that it's no longer implicitly capped at one row per user. A clean disconnect (terminal SessionPresence.disconnect(), web logout) now deletes its own row outright instead of just marking it stale. For connections that never get a clean disconnect (dropped carrier, killed process, browser closed), a new scheduled-maintenance handler, cleanup_stale_sessions, deletes anything untouched for more than a day — auto-seeded on every install, not just fresh ones.
Fixed two related bugs found in the same audit: three "N users online" counters (the site-wide navbar badge, the admin dashboard, and the terminal sysop stats screen) were counting raw connection rows rather than distinct users, which would have double-counted anyone with two connections open; and profile.py's is_user_online() picked an arbitrary session row with no ordering, which could report a genuinely-active user as offline if an older, stale row for the same account happened to be checked instead.
v1.0.22 — Terminal echomail-reply network bug, live presence detail, activity log drill-down, echomail admin logging, calendar/board polish (August 2026)
Fixed a real bug: replying to an echomail message from the terminal never actually reached the network. read_echo_area()'s inline reply composer (anetbbs/features/bbs_ui.py) was a fourth local-compose write path into EchomailMessage that never got the toss_message() fix the other three composers (the dedicated Compose Echomail menu item, the web composer, and PETSCII's composer) already had — a terminal reply sat in the local DB, visible on read-back, but was never queued into any downstream node's hold queue at all. This is the exact bug Jerry hit replying to a test message on a real network. Also fixed: none of the three terminal/PETSCII composers set tear_line/origin_line (the FTN * Origin: footer), even though the web composer always has — all three now read ECHOMAIL_TEAR_LINE/ECHOMAIL_ORIGIN_LINE from config like the web route does.
"Who's on" now shows real detail instead of being frozen at "main" all session. Root cause: SessionPresence.set_page() — the exact method built for this — was hardcoded to fire exactly once, at login, and never stored on the session for anything else to call again. Now updated from menu_engine.py's central action dispatch (every top-level menu action: games, chat, boards, files, echo, pm, ...) plus finer detail from games.py (which door) and mrc_chat.py (which room) — both the terminal Who's Online command and the web /who/ page benefit, along with the sysop's NodeSpy panel.
New per-session activity log with a real drill-down. The caller log now has a "View Activity" link per session, showing a full chronological timeline — login, menu actions, doors played/exited, MRC chat sessions with duration, logout — built on the existing (but nearly unused) UserActivity audit table rather than a new system. Also fixed a bug found along the way: CallerLog.duration_seconds was declared on the model and shown in two admin templates but never actually written anywhere — every row showed 0s; both web and terminal sessions now record real duration on logout.
Echomail admin logging got substantially more detail, all things Jerry specifically asked for after going through the hub over the weekend: poll logs are now filterable by downstream node, not just network; the AreaFix log gained a from-address (node) filter; and node detail pages gained a full File Area Subscriptions card — view what file areas a node is subscribed to and add/remove them, mirroring the existing message-area subscription UI, which didn't exist for file areas at all before this.
Calendar: a sysop can now delete past events from the main calendar view, not just upcoming ones — the delete button simply never rendered for the "Recent past events" list.
Message boards: added a "Recent Activity" sort option (web ?sort=activity, terminal A hotkey) alongside the existing sysop-configured manual order, so recently-active boards are easy to find instead of requiring a scan of every category.
v1.0.21 — Critical fix: unbounded memory/CPU leak in the terminal service; PETSCII ANSI color translation (August 2026)
Critical live fix: anetbbs.service (telnet/SSH/rlogin/PETSCII/FTP)
leaked memory and CPU without bound — observed growing to 19.8GB RAM
and 99.7% CPU after ~7 hours uptime with only a couple of concurrent
sessions, causing severe lag and dropped MRC chat connections. Root
cause: anetbbs/features/bbs_ui.py's _app() helper built a brand-new
Flask app and registered a brand-new SQLAlchemy engine/connection pool
on every single call, never disposed — and anetbbs/core/session.py's
sysop-kick watchdog calls it every 5 seconds for the entire lifetime of
every logged-in session (one of ~150 call sites across that module).
Over hours, with multiple concurrent sessions, that's tens of thousands
of leaked engines. Same root shape as a BinkP per-connection database
leak fixed earlier in this project's history — that fix was never
generalized to this helper. Fixed by caching the Flask app instead of
rebuilding it per call: reusing one shared app across many
app_context() pushes is the normal, correct Flask usage pattern (it's
exactly what the web/gunicorn process already does for every concurrent
web request) — building a fresh one on every call was the actual
anomaly. Found via a live user report ("I keep getting disconnected
from MRC" plus general terminal lag) traced in real time through
systemctl status/journalctl output showing RAM climbing while the
report was being investigated; hotfixed directly to the live server
ahead of this packaged release given the severity.
PETSCII (Commodore 64/128) sessions now get real translated colors
instead of having ANSI color codes stripped outright.
anetbbs/features/petscii_codec.py gained ansi_to_petscii(),
translating ANSI SGR color codes into real C64 color control bytes —
verified against Synchronet's own open-source PETSCII terminal
implementation (src/sbbs3/petscii_term.cpp) rather than invented from
scratch; every color byte matches theirs exactly. Replicates the same
reverse-video trick Synchronet uses for combined foreground+background
colors, since C64 text mode has no independent per-character background
color (only one foreground color per cell plus a whole-cell reverse
flag). Non-color ANSI sequences (cursor moves, erase, etc.) are still
dropped, same as before this change — PETSCII still can't honor
arbitrary cursor addressing from ANSI content.
Audited the rest of the codebase for the same leak shape and fixed
three more call sites that copy-pasted it, in
anetbbs/games/door_runner.py (_write_msgbase_ini_override(),
_cleanup_session_safe(), play_door_game_telnet() — the latter two
hit on every door game launch and exit) and
anetbbs/features/games.py (show_door_menu() and its game-launch
path). Unlike bbs_ui.py, these callers' own tests rely on getting a
genuinely fresh Flask app per call (to point SQLALCHEMY_DATABASE_URI
at a different temp DB per test case), so a shared-cached-app fix
wasn't an option here — instead added
anetbbs/features/db_scope.py::transient_app_context(), a small
context manager that disposes the fresh app's SQLAlchemy engine on
exit, modeled on anetbbs/echomail/binkp_server.py's existing
_new_app()/_dispose_app_engine() pattern (which already handled
this correctly and was left untouched).
v1.0.20 — anetbbs-cfg: standalone terminal admin tool (August 2026)
New: anetbbs-cfg, a full-screen curses terminal admin tool in the
spirit of Synchronet's SCFG / Mystic's mystic -cfg — a standalone
console command, independent of the web admin and of whether the
network services are even running. Run it with python -m anetbbs.cfg
from a checkout, or anetbbs-cfg once installed; it uses the same
create_app()/database as the web and BBS processes, so changes show up
immediately everywhere.
First version shipped 5 sections (Boards, Echomail Networks/Areas, File
Areas, Users & Security, System Settings); expanded to 16 total
sections for near-full web-admin parity, per Jerry's priority order:
- Boards & Message Areas — add/edit/delete/reorder, access levels
- Echomail Networks & Areas — pick a network, drill into its echo
areas; BinkP host/port/passwords, AreaFix password, poll interval - Echomail Hub — AreaFix log, poll log, QWK node request approve/
deny (mirrors the web admin's exact packet-id validation + random-
password credential generation, not a loose reimplementation) - File Areas — tag, storage path, upload permission, access levels
- File Bulletins — metadata (title/order/active/access) for files
dropped into FILE_BULLETINS_DIR, auto-synced from disk on view - Users & Security — search/edit users, one-time password reset, IP
bans, word filters, login auto-ban thresholds, registration attempt log - Games — door games (full field set: DOS/DOSBox/dosemu, Mystic,
Synchronet, rlogin, telnet, web), categories, active session monitor
with disconnect/clear-stale - Image Galleries — add/edit/remove gallery collections (JSON-config
backed, same store the web admin uses) - BBS Menus / PETSCII Menus — two-level menu/item editors for
the telnet/SSH/rlogin and C64/128 terminal menu trees - Scheduled Events — cron-style task config, JSON schedule/params
validated on save, [R]un Now - Graffiti Wall — post moderation (delete/restore/clear-all)
- Login Modules — logon/logoff action config (wall, ANSI screen,
file bulletins, shell command, native/Python doors) - Last Callers — read-only login log
- Backups — browse/delete
update.sh's pre-update snapshots - System / Network Settings — a grouped
.enveditor (server ports,
application settings, logging, BinkP, files/FTP, games, echomail,
NUV), preserving comments and untouched keys on save
Advanced/rarely-touched fields and a few genuinely risky operations stay
web-admin-only, flagged in the tool itself rather than silently missing:
ANSI board/menu banner screens, BinkP TLS/CRAM-MD5/packet password,
file-area network reassignment, IP whitelist, and — deliberately —
backup restore (goes through a privileged sudoers-gated helper
script and can overwrite a live .env/database; browsing and deleting
old backups is still available here) and InterBBS Wall/Last-Callers
sharing settings (each is a combined .env write + echomail-area
provisioning step in one web route).
Fixed during Pi3 testing: launching anetbbs-cfg on a live install
started a second full copy of the entire BBS background service
stack (echomail poller, RSS poller, MSP/SYSTAT listeners, the ANetBBS
directory refresher, the metrics sampler, the scheduled-events runner)
alongside the already-running anetbbs-web process — double-polling
echomail, double-firing scheduled events, extra CPU/network contention
on top of the real service — just to open a local config screen.
create_app() only ever gated these behind TESTING; extended the
existing ANETBBS_SCHEMA_MIGRATE_ONLY one-shot-CLI flag (already used
by update.sh's schema-migration step) to also skip all of them.
Then found the real cause of anetbbs-cfg still taking ~10 seconds
to start on a Pi3, plus the eventlet deprecation warning and an
exit-time RuntimeError: greenlet is being finalized crash report:
profiling showed create_app() — built for the full web server — pulls
in eventlet (+ monkey-patches stdlib socket/threading/ssl), flask-
socketio, and flask-migrate, then registers 76 web blueprints and
compiles their werkzeug URL-routing tables, none of which a local
config screen needs. anetbbs-cfg now uses a new, much smaller
anetbbs.cfg.db_bootstrap.create_minimal_app() instead — a bare Flask
app with just db bound to it, skipping web_app.py (and eventlet)
entirely. Cuts measured startup from ~3.6s to ~1.1s on a dev machine;
proportionally larger on a Pi3. Since eventlet is never imported at
all now, both the deprecation warning and the eventlet/greenlet
shutdown crash are structurally gone, not suppressed.
Built on a small reusable curses widget layer (anetbbs/cfg/ui.py) with
zero new dependencies (stdlib curses only) — a scrollable list editor,
a field-driven form (turns a set of model columns into a screen with no
per-section layout code), and confirm/message modals. The .env parser
round-trips a file byte-for-byte on a no-op edit and only ever rewrites
the keys actually changed.
v1.0.19 — Web UI performance pass: message rendering, boards, echomail, wiki, file areas (August 2026)
The trigger: a single echomail message with a large ANSI-art body was taking 30+ seconds to load. Root cause was an O(n²) bug in reflow_hard_wrapped_body() (the code that rejoins hard-wrapped FTN message lines) — for a long run of qualifying lines with no blank-line/art/list breaks, it folded the whole run into one ever-growing string and then re-scanned that entire string on every single line join, twice over (once for a trailing-word regex, once for an art-detection regex). Confirmed 32.7s on a synthetic body shaped like the real report; bounding both re-scans to a small fixed window instead of the whole string dropped that to 0.23s — about 140x faster, with all existing tests still passing.
That led to a broader pass across the web UI looking for the same class of bug and other real request-path cost:
- The "Toggle Markdown view" button on every message-read page (echomail/netmail/boards/PM) used to render the entire body through python-markdown + bleach unconditionally into a hidden div on every page load, even though almost nobody ever opens it — now deferred to a small on-demand endpoint that only renders on first click.
- Wiki page rendering had the identical O(n²) shape in its own placeholder-restore step (fenced/inline-code protection) — 3.13s → 0.02s on a comparable synthetic page.
_linkify()'s URL-substitution loop re-scanned the whole rendered output once per matched link — rewritten as a single pass; a 3,200-link body now renders in a fraction of the time.- The public message-board index ran 3 separate count queries per board (unread/post/reply) — hit by every visitor, including anonymous ones — collapsed to a small, fixed number of grouped queries regardless of board count.
- The echomail network-chooser page reloaded the user's entire read-status history from scratch once per network shown — now one indexed join query for the whole page.
- Viewing a board thread issued one DB query per reply in the tree — now one query per tree depth, so a thread with hundreds of replies at one depth costs 2 queries instead of hundreds.
- The file-areas index page ran a full TIC-DB-query + archive-extraction scan per area just to show a count and total size — replaced with a lightweight directory scan that skips all the per-file description work the index page never needed.
- The wiki's "Wanted pages" / "Orphaned pages" utility pages re-scanned every page's full body on every single visit — now cached (and kept in sync) whenever a page is saved, with pre-existing rows self-healing on first view.
Also fixed a long-standing drift found along the way: anetbbs/__init__.py's __version__, setup.py's version=, and FILE_ID.DIZ had been stuck at 1.0.9 since that release — VERSION, RELEASE.md, README.md, and this changelog were correctly bumped every release since, but those three files were missed for 9 releases running. Back in sync as of this one.
v1.0.18 — AFK warning + matrix-rain screensaver; MRC wide-terminal sizing fix (August 2026)
AFK warning + screensaver for the terminal client. New AFK_WARNING_SECONDS setting (.env, default 0 = off), mirroring a real Mystic Pascal AFK script Jerry pointed at as a reference. After that many seconds of no keystrokes at any menu prompt, the caller sees a live countdown warning ("You've been idle a while..."); if nobody responds, a generated matrix-rain screensaver takes over the screen. A keystroke at either stage cancels/dismisses it — consumed, not passed through as a real menu selection — and returns to exactly where the caller was (prompt redrawn, plus any already-typed partial line for read_line). If the sysop also has IDLE_TIMEOUT_SECONDS set and nobody ever comes back, the existing hard idle-disconnect still fires afterward, unchanged.
Scoped deliberately narrow: only read_key()/read_line()/read_key_arrow() (the actual menu-navigation primitives) can trigger this. read_raw() is also called directly by several other features (door games' own poll loops, IRC/telnet bridges, the ANSI editor, dialout) with their own timeout/retry semantics and broad exception handlers that would have silently misinterpreted an AFK interruption as "the game/door ended" rather than "resume where you were" — a real risk found auditing every read_raw() call site before wiring this up. Those are completely unaffected; only the three intended entry points opt in via a new allow_afk parameter.
AFK_WARNING_SECONDS is also now a real Admin → Settings field, right alongside its sibling IDLE_TIMEOUT_SECONDS — no SSH/.env hand-editing needed. Both are read the same way (a direct per-session environment read, not routed through Flask's live-reloadable config) so a service restart is still needed after changing either one, same as IDLE_TIMEOUT_SECONDS already required.
Real fix from the first live test: dismissing the screensaver showed "Welcome back!" and a bare Choice: prompt with no menu — the caller had to press another key before the actual menu reappeared. read_key() only had the trailing prompt text to redraw with; the real menu content had been drawn by the caller beforehand and the screensaver's own screen-clear wiped it. Fixed with a new optional on_afk_redraw hook — menu_engine.py's central menu loop (used for most of the BBS's screens) now passes its own full redraw routine, so one keystroke both wakes the session and redraws the menu it's serving.
MRC wide-terminal sizing. The Mystic MRC screen recreation always rendered the 80-column layout regardless of actual detected terminal size — load_theme_layout() always supported loading wider bundled .ini variants (132x36, 160x59, etc.), it just was never wired up. New best_fit_mode() picks the largest variant that fits the caller's real terminal size (e.g. a 132x37 terminal now gets the 132x36 layout, with the nick-list sidebar, instead of silently falling back to the 80-col default).
v1.0.17 — Doc fixes: door_mystic_mps still described the disproven -x flag (August 2026)
A full audit of everything added since GA turned up one real doc bug: docs/14-door-games.md and docs/17-development.md still described door_mystic_mps launching via mystic -x and never mentioned the mandatory -u/-p credentials — stale ever since the actual code was fixed to use the real flag (-y<script>; Mystic has no anonymous/no-login mode for scripts at all). Both docs now match what the code has actually done for a while. No code changes.
v1.0.16 — Mystic MRC themes: full Mystic-style chat screen in the terminal (August 2026)
Added support for a set of MRC chat themes inspired by StackFault's (The Bottomless Abyss / Phenom Productions) Mystic BBS MRC client, plus an optional backend to run that real client directly. Full credit to StackFault for the original client and its five bundled themes — see docs/27-mrc-chat.md's Credit section and mrc/mystic_client/vendor/PROVENANCE.md.
- Five new
/set paletteoptions (terminal) and matching web theme choices —original,minimal,bitchx,2leet4u,least. The terminal recreates the real Mystic screen layout (border art, room/topic/nick-list/latency/buffer/input positions, all sourced from the vendored theme files) rather than just swapping colors. Palette choice now persists per-handle across reconnects (default:original). - Real round-trip MRC latency shown in both terminal and web clients, replacing the old local-loopback ping/pong number.
- Room topic now shows immediately on joining a room instead of a delayed, inconsistent pop-up.
- New
mrc_backend: "mystic"option (default remainsnative) runs the real vendoredmrc_client.pyclient as a subprocess against a synthetic Mystic directory ANetBBS builds automatically — no real Mystic install or account needed. door_mystic_mps(Mystic Pascal door scripts) gained ARM64 install support and admin-form fixes for running standalone.mps/.mpxscripts with real credentials.
v1.0.13 — LORD (and every dorkit.js door) fixed after "sits stale, never loads" (August 2026)
LORD — Legend of the Red Dragon — stopped loading entirely: the intro screen never appeared and the door looked completely frozen. Root cause: Queue.prototype.poll() in the Node.js compat shim never flushed buffered terminal output before its wait loop, unlike every other blocking-read call site in the shim. LORD's whole "draw a screen, then wait for a keypress" flow runs through dorkit.js's waitkey() → poll() — not the code path that was already covered — so the intro art and every prompt sat buffered indefinitely while the door correctly polled for input in the background the whole time, with the player staring at a blank screen with no idea a key was expected. This affects every door built on the shared dorkit.js library, not just LORD. Confirmed fixed against the real vendored door end-to-end.
Also this round:
- Door menu sort_order is now truly flat/global across the whole menu (terminal and web), not just within each game's own category. Categories used to always render grouped together in their own separate sort_order, so a sysop numbering every door 1-N as one flat list (the natural way to think about it) could add a new door with a "should be last" number and see it land in the middle instead, if its category happened to sort earlier. Category headers/sections now fall wherever the category naturally changes while walking that flat order.
- Minesweeper's title bar rendered a garbled ←5C before the real title text. console.right/left/up/down (and cursor_right/left/up/down) were string-concatenating a raw fractional cursor-move count straight into the ANSI escape sequence — Minesweeper's title-bar centering math produces a non-integer for odd-width text, and a fractional CSI parameter isn't legal, so terminals abort the sequence mid-parse and print the tail literally. Now rounds before it hits the wire.
- The door-menu submenu screen (introduced in v1.0.12) now supports the same file-based ANSI art override as the top-level Door Games list — drop door_games_<category-slug>.ans into data/text/menus/ to replace the generated layout for that category's second-level menu. See the slot-names reference on /docs/04-ansi-screens for the full naming convention.
v1.0.12 — Door menu sections (drill-down categories) (August 2026)
A game category can now be marked as a submenu section (Admin → Games → Categories → edit a category → "Show as a submenu section") instead of always listing its games inline — useful once a category has enough doors to run off the bottom of a real terminal screen. A section shows as one selectable line in both the terminal and web door menus; picking it opens a second screen listing just that category's games. Off by default — existing categories/installs render exactly as before until a sysop opts one in. (PETSCII's Games menu intentionally never listed real doors at all, so there's nothing to change there.)
Also fixed along the way:
- console.charset was missing from the Node.js compat shim's console object, crashing any door that reads it via the real vendored modopts.js (surfaced running Minesweeper) — now returns "CP437", matching ANetBBS's encoding throughout.
- The InterBBS Score-Sharing Area dropdown (Admin → Games → edit a door_synchronet game) is now grouped by network (<optgroup> per network) instead of one flat alphabetical list — DOVE-Net's areas no longer get buried in a large FidoNet arealist.
- Adding a game whose auto-filled slug collided with an existing one (e.g. typing "Minesweeper" when the built-in browser minigame already owns slug minesweeper) crashed with a raw 500 instead of a friendly "slug already in use" message.
v1.0.11 — Minesweeper InterBBS DOVE-Net score sharing (real MsgBase support) (August 2026)
Synchronet's own official Minesweeper door (bundled in v1.0.9) has a real, built-in feature to share game wins across BBSes via Synchronet's MsgBase message-base API — previously unimplemented in the Node.js compat shim (any door calling new MsgBase(...) would ReferenceError). Now real:
- New per-game admin setting, "InterBBS Score-Sharing Area" (Admin → Games → edit Minesweeper) — pick any real configured echo area, e.g. DOVE-Net's "Synchronet Data" conference (2013), to enable score sharing. Leave unset and the feature stays off, same as before.
- A win posts a JSON-encoded report to that area, and other BBSes' win reports read back the same way and merge into the door's own winners list — real
MsgBaseopen/save_msg/get_index/get_msg_header/get_msg_bodycalls, backed by a new Python bridge (msgbase_bridge.py) that reaches ANetBBS's actual echomail data (EchoArea/EchomailMessage), not a stub. - Uses the door's own documented
ctrl/modopts.iniconfig path ([minesweeper]→sub = <area-tag>), auto-written before every launch — no changes needed to the door itself.
v1.0.10 — File Bulletins: configurable .txt/.ans bulletin viewer (August 2026)
A new logon/logoff-style module for file-based bulletins — distinct from the existing DB-authored Bulletins feature. Drop .txt, .asc, or .ans files into data/text/bulletins/ and they're auto-registered (inactive until enabled). Sysops manage them from Admin → Bulletins → Files: set a title, sort order, and minimum access level, and toggle visibility per file. Users browse a lightbar list and read through the same CP437/ANSI-aware ANView pipeline used elsewhere in the BBS — real file bytes, not DB text, so CP437/ANSI decoding is correct for genuine art bulletins (the kind door games often drop for scores/news). Wired into the LoginModule system as a new file_bulletin module type, attachable to logon/logoff sequences like any other module.
v1.0.9 — Synchronet door game support (17 games tested) + MRC ping/latency display (August 2026)
ANetBBS's Node.js compat shim (synchronet_compat.py) can now run real, unmodified Synchronet .js door games, including ones using Synchronet's real JSON-RPC "JSON DB" protocol (port 10088) for shared, cross-BBS game state and scoreboards. Confirmed working end-to-end against real live JSON-RPC servers (including real cross-BBS data — existing scores, levels, and player history from other real BBSes already using these games):
- Chicken Delivery — real-time delivery arcade
- Bubble Boggle — word-search puzzle
- Synchronetris — real-time multiplayer Tetris-style
- Jeopardized — trivia game show, live rankings
- Gooble Gooble — real-time Pac-Man-style chase
- Synkroban — Sokoban warehouse puzzle
- Star Trek — real-time space combat arcade
- Fat Fish — fishing simulation
- Dice Warz ][ — territory-conquest strategy (Risk-like)
- Maze Race — real-time multiplayer maze racing
- Thirstyville — café-owner economic simulation
- Good Time Trivia — trivia with multiple categories
- Lemons — "Lemmings"-style puzzle
- Star Stocks — galactic investment strategy
- DrugLord — "Dope Wars"-style economic sim
- Uber Blox — block-clearing puzzle
- Minesweeper — Synchronet's own official Minesweeper (by Digital Man), classic minefield-clearing puzzle with personal-best tracking — the one door in this list that doesn't use JSON-RPC
These games are not bundled in the release — they're real, free, open-source software from their own original authors, not ANetBBS's to redistribute. See docs/26-synchronet-json-rpc-doors.md for download links and setup instructions for each one.
MRC: The terminal client's status bar now shows real ping/latency instead of a clock (per-message timestamps already show the time on every line, so the status-bar clock was redundant). Turned out the latency widget already existed in the code but was silently broken — a wire-protocol field-name mismatch meant it never received a valid round-trip time, so only the clock ever showed. Fixed the mismatch and removed the now-redundant clock. The web UI now shows the same live latency figure next to the room topic in its status bar (previously only in the sidebar, which still also shows it).
v1.0.8 — Poll log dedup guard could block a network's polls forever (August 2026)
Found live, right after the public release announcement: DOVE-Net (a QWK network) had simply stopped appearing in echomail poll activity, with no error anywhere — just silence for over a day.
Root cause: _do_poll()'s concurrent-poll dedup guard (anetbbs/echomail/poller.py, added in an earlier audit to stop a sysop's manual "Poll Now" from racing the scheduled poller's own tick for the same network) treats any EchomailPollLog row still at status='running' as proof a poll is genuinely in progress, and skips starting a new one. That's correct for a poll that's actually still running — but a poll interrupted mid-flight (a service restart landing while a session was still open, which is exactly what happens during any update.sh run) leaves its row stuck at 'running' forever, since no exit path ever gets a chance to run and flip it. Every subsequent poll attempt for that network then silently self-skips, permanently, with nothing logged anywhere a sysop would think to look — confirmed live: DOVE-Net's last poll log row was status='running', started_at over a day in the past, and nothing after it at all.
Fixed: a 'running' row older than 30 minutes (_STALE_RUNNING_POLL_MINUTES) is now treated as abandoned rather than as a lock — it's flipped to 'error' with a note explaining why, and the new poll proceeds normally instead of skipping forever. A genuinely recent 'running' row still blocks a second concurrent attempt exactly as before. 2 new tests (stale-row recovery, and a sanity check that a fresh row still blocks normally) alongside the 3 existing dedup-guard tests, all passing.
This is the kind of bug an automated update can trigger on any network, not just QWK — any BinkP network poll interrupted by a service restart mid-session would hit the same silent-forever-skip. Sysops on v1.0.6/v1.0.7 whose polling has quietly gone silent for a network should check Admin → Echomail → Poll Log for an old 'running' row for that network; the fix here is automatic once updated, no manual DB edit needed.