ANetBBS Changelog

Current release: v1.0.28 (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.28 — PETSCII new-user registration fixes (August 2026)

The newuser welcome banner displayed as literal garbage on PETSCII — real bug found live on the Pi. _show_ansi_screen() writes raw CP437/ANSI bytes directly to the socket, bypassing write()'s petscii translation branch entirely — the exact same limitation already guarded against for the 'welcome' and 'goodbye' screen slots, just missed for 'newuser'. A real PETSCII session saw the sysop's newuser.ans banner as literal ESC[...m escape codes with case-inverted text instead of a rendered screen. Fixed with the same if self.term_mode != 'petscii' guard already used for the other two slots — "Registration successful!" (which already goes through write() correctly) still confirms the account was created; petscii users just don't get the customizable ANSI banner, the same tradeoff already accepted for 'welcome'/'goodbye'.

Security-question and newuser-questionnaire prompts broke mid-word on a 40-column PETSCII screen — another real bug from the same screenshots. These prompts were written as long unwrapped lines via session.write() and left to the terminal's own hardware auto-wrap, with no word-boundary awareness. New _prompt_width()/_wrap_text_lines() helpers in session.py (petscii_width-aware, falling back to window_size/80 for every other term_mode) now word-wrap the security-question list, the "Question N of 3" selection prompt, and the sysop-defined newuser questionnaire prompts.

v1.0.27 — ASCII MRC chat client; word-wrap fix for embedded newlines (August 2026)

New AsciiMRCChat client for term_mode == 'ascii' sessions. ascii has always been a real, selectable terminal mode, but it never had its own MRC client the way PETSCII now does — chat.py's ChatManager handed every session the full ANSI split-screen MRCChat regardless of mode. session.write() strips every ANSI escape sequence outright for ascii sessions, so that split-screen mode's DECSTBM scroll-region setup, CPR terminal-size probe, and cursor-addressed status/input/ticker draws were all silently dropped — a real, structural bug (ascii+MRC had no usable layout at all), not just a missing feature. AsciiMRCChat (anetbbs/features/mrc_chat_ascii.py) is the same plain-scroll-mode override pattern already proven by PetsciiMRCChat, simplified since ASCII has no case-inversion, no color-byte translation, and no special DEL key — just standard \x7f/\x08 backspace. ChatManager.__init__ now picks AsciiMRCChat for term_mode == 'ascii' and MRCChat for everything else.

_word_wrap() fix for embedded newlines — real bug found live on the Pi. A multi-line MOTD/banner from the MRC bridge arrives as one string with its own intentional \n line breaks. The word-wrap tokenizer (shared by both MRCChat's ANSI split-screen _emit() and PetsciiMRCChat/AsciiMRCChat's plain-scroll _emit()) only charged an embedded \n 1 column against its width budget, but the terminal itself resets to column 0 there — so the algorithm's internal column count and the real cursor position diverged, leaving whatever word came right after the newline in the source text stranded alone at the left margin (seen live at 40 columns: "at", "!list", "or", and a URL each appearing as isolated fragments). Fixed by treating \n/\r\n in the input as hard breaks, word-wrapped independently, before the normal width-based reflow runs.

v1.0.26 — PETSCII MRC chat: real word-wrap instead of raw terminal auto-wrap (August 2026)

Another real bug found live-testing on the Pi, worse at 40 columns than 80. PetsciiMRCChat._emit() was just writing each message's raw text and letting the terminal's own hardware auto-wrap break it wherever the physical column happened to land — no word-boundary awareness, so long messages could split mid-word and continuation text had no relationship to the original line. Fixed by reusing MRCChat's own _word_wrap() helper (the same one the ANSI split-screen client already uses) so messages wrap cleanly at word boundaries regardless of screen width, written as a single atomic write (still serialized against incoming/outgoing keystrokes via the shared lock from the previous release).

v1.0.25 — PETSCII MRC chat: password masking, AFK interruption, message-splicing fixes (August 2026)

Three real bugs found live-testing v1.0.24's new PETSCII MRC client on the Pi, all traced to the same root cause. PetsciiMRCChat._read_chat_line() originally delegated to the generic session.read_line() for simplicity — that turned out to be wrong three ways:

  1. /identify <password> echoed the password in the clear, unmasked. read_line() has no masking; the real per-keystroke masking logic lives in the ANSI client's raw input loop, which the PETSCII override bypassed entirely.
  2. The AFK warning/screensaver could interrupt an active chat session. read_line() always opts into AFK tracking internally — MRC is specifically designed to never go through that path at all.
  3. An incoming message arriving mid-keystroke spliced into the line being typed, corrupting the display (the message actually sent was still correct — this was a rendering race, not data corruption). ANSI split-screen mode avoids this because incoming messages and the input line are drawn to separate cursor-addressed regions; plain-scroll mode has no such separation, so both now share the same lock the ANSI client already uses for this.

Fixed by replacing the delegated read_line() call with a proper PETSCII-safe character-by-character input loop — reading raw off the connection (matching the ANSI client's own approach, which also sidesteps AFK), reimplementing password masking at the single-character level, and serializing with incoming-message writes via the shared input lock. Also fixed _term_columns never being set for PETSCII sessions (stuck at its 80-column default), which affected a couple of width calculations.

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 .env editor (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 palette options (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 remains native) runs the real vendored mrc_client.py client 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/.mpx scripts 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.