ANetBBS Changelog

Versions are internal build numbers. Public releases are tagged
separately. Current release: v1.0b2.58 (July 2026). Full release: August 1 2026.

v283.6 — Echomail message reader: color + paging (May 2026)

After picking an area under Echomail (E), the resulting message
list and message body were plain monochrome and dumped without
paging. Now matches the rest of the terminal UI:

  • Banner + footer wrap on both list and body screens
  • Colored columns on the message index (yellow #, white subject,
    green from, cyan date)
  • -- more (Enter) -- paging every 18 lines on both the index
    and the message body, so long posts don't scroll past on a
    24-row terminal
  • Same paging treatment in Compose Echomail (C) for the area-empty
    screen, prompts, and the queued-for-BinkP confirmation

v283.5 — Echomail (E) area list paging (May 2026)

The terminal main menu's Echomail (E) area listing scrolled past on
24-row terminals — compose_echomail (C) already paged every 18
lines with -- more (Enter) --, but list_echo_areas dumped
everything at once. Now uses the same paging.

v283.4 — Dark-theme: list-group + table row tints (May 2026)

Bootstrap's default .list-group-item and .table-warning/-info/...
row tints render light-on-light, which clashes with the dark theme.
The most visible offenders were:

  • The RSS Reader feed list and River feed (white cards, hard to read).
  • The Inter-BBS Instant Messages inbox unread-row highlight (cream
    yellow on white).
  • @mention autocomplete popup, leaderboards, stats, profile,
    oneliners, calendar, docs nav — all used .list-group-item.

base.html now overrides these classes to use the same dark palette
as the .card and .alert-* styles. No template changes needed —
all consumers benefit automatically.

v283.3 — NodeSpy kick audit-log fix (May 2026)

The kick endpoint in v283.2 raised
TypeError: 'action' is an invalid keyword argument for UserActivity
on every kick attempt — the audit-log column is activity_type, not
action. (The legacy v283 code had the same bug but never ran far
enough to trip it.) Now uses the correct field and also records
ip_address and service='web'.

v283.2 — NodeSpy kick: cross-process fix (May 2026)

The kick button in v283 / v283.1 appeared to work but didn't actually
disconnect anyone. Root cause: anetbbs-web (gunicorn) and
anetbbs-telnet are separate systemd services with separate Python
processes. The kick endpoint flipped a flag in the web process's
in-memory _NODES dict, but the active terminal session lived in the
telnet process — different memory, no effect.

Now the kick crosses the process boundary via the database:

  • NodeActivity gains two columns: kick_requested (bool) and
    kick_reason (string). Auto-sweep adds them on next gunicorn start.
  • /admin/control/nodespy/<slot>/kick simply updates those columns
    and commits.
  • A new _kick_watchdog task in BBSSession polls its own
    NodeActivity row every 5 seconds. When kick_requested is set, it
    writes the goodbye banner, closes the writer, and the session
    unwinds via the normal teardown path.
  • The watchdog is cancelled on session close, before
    _close_node_activity deletes the row.

Worst-case latency: ~5 seconds between kick click and disconnect.
Stuck/idle users are unaffected (the watchdog runs on its own timer,
not on user input).

v283 — NodeSpy kick (May 2026)

Sysop can now disconnect a stuck or misbehaving terminal user
straight from the NodeSpy panel:

  • Kick button in the per-row NodeSpy table (red door-arrow icon)
  • Kick form in the per-node detail page with an optional reason
    text field
  • New endpoint: POST /admin/control/nodespy/<slot>/kick (CSRF-
    protected, admin-only)
  • New function: multinode.kick_node(slot, reason) — pushes a 'kick'
    payload to the session's chat queue, writes a goodbye line to the
    user's terminal, then closes the underlying transport. The session's
    reader EOFs and the session cleans up naturally.
  • NodeEntry gained a session reference (passed by acquire_slot
    in core/session.py) so the kick handler has a handle to close.
  • Audit log entry in user_activity for every kick (action=kick_node,
    details = "slot N (username): reason"). So sysop kicks are
    traceable.

Different from a ban — this just drops the current connection. The
user can reconnect immediately. To prevent reconnect, sysop must
also add an IP ban under /admin/ip-bans/.

v282 — consolidation cut (May 2026)

Clean release after the v280-v281.3 hotfix series. No new behavior
beyond what the v281.3 patches already shipped — this is purely
a docs + memory + audit-pass roll-up so the next deploy carries
matching docs alongside the running code.

  • Docs updated: CHANGELOG, FEATURES, 14-door-games (door_rlogin
    worked example, dosemu removal note), 16-rss-reader.
  • Memory entries updated: project_status reflects current cursor
  • RSS + door_rlogin work; rlogin handshake quirk recorded at
    project_rlogin_format.md; LORD setup guidance at
    project_lord_setup.md; door-files-hands-off feedback at
    feedback_door_files.md.
  • Final audit: 50 blueprints, 319 routes, 189 templates parse,
    zero broken url_for() references, app boots clean. Pyflakes
    has no undefined-name / redefinition warnings — only intentional
    drains and load-bearing imports remain.

v1.0a — RSS reader (May 2026, internal v281)

Built-in RSS / Atom feed reader for both web and terminal. Sysop
manages feeds at /admin/rss/. Background poller refreshes every
30 minutes (configurable via RSS_POLL_INTERVAL env var). X-News
seeded by default so a fresh install has at least one feed populated.

New tables (auto-created by the lightweight migration sweep):
- rss_feeds — sysop-configured feeds
- rss_items — articles (deduped per-feed by GUID)
- rss_read_status — per-user read markers

New blueprints: rss at /rss/, rss_admin at /admin/rss/.

New file: anetbbs/rss/poller.py — background daemon thread.

New dependency: feedparser>=6.0 (handles RSS 2.0 / Atom / RSS 1.0).

Web UI: Tools → RSS Reader. Feed list with unread badges, "all
feeds" river view, paginated per-feed item lists, single-item full
content view, mark-as-read tracking per user.

Terminal UI: Main BBS Menu → R. Feed picker, river, paginated
item lists, single-item viewer with word-wrapped body. Same mark-read
state shared with the web UI.

Pre-seeded feed: https://x-bit.org/rss/rss.xml (X-News).

v281.1 — fixed Jinja name collision. Renamed template var from
unread to feed_unread because base.html does {% set unread = ... %}
for the PM badge counter, which shadowed our context variable inside
child templates and turned the dict into an int.

v281.2 — added RSS to the data-driven menu engine. v281 only
added it to the hardcoded BBSMenuUI.show_main() fallback; the
running terminal sessions use the BbsMenu table-driven menu, which
needed a new rss action_type registration + a R hotkey entry in
DEFAULT_MENUS (auto-backfilled to existing installs by
seed_default_menus).

v281.3 — typo: FG['ylw']FG['yel']. Crashed show_rss()
on first launch.

See docs/16-rss-reader.md for full feature docs.

v1.0a — A-Net Game Server / rlogin doors (May 2026, internal v280)

New game type: A-Net Game Server (rlogin). Lets BBS users
transparently rlogin into a remote Synchronet door game server
(or DoorParty, or any other rlogin-accepting BBS host). The user
sees one continuous session — no second login prompt.

Architecture:

  • New anetbbs/games/rlogin_bridge.py with RloginConnection
    same write/stop/bind_emit shape as DosBridge so it slots into
    the existing DoorSession machinery (send_input, terminate_session,
    cleanup all work without changes).
  • New launch_rlogin_session() in door_runner.py for the web flow.
  • New play_rlogin_telnet() in door_runner.py for the terminal
    flow. Both use the same RloginConnection.
  • Game type door_rlogin selectable in /admin/games/ with its own
    helper section in the form (server host:port + user template +
    password + optional terminal hint for direct-to-door launches).

Wire format: Synchronet's BBS-mode rlogin server uses INVERTED
field order vs RFC 1282 — the password goes in the client-user-name
slot (1st field) and the BBS username in the server-user-name slot
(2nd). Verified against game.a-net-online.lol on 2026-05-09. Memory
note saved at memory/project_rlogin_format.md.

dosemu removed. The half-finished door_dosemu game type was
yanked from the dropdown — drive-letter detection was wrong and the
admin form fields were missing. _build_dosemu_command stays in the
source tree as dead code in case someone wants to validate it
properly later.

v280.1–v280.4 — admin form polish + rename: dropdown renamed
"Synchronet xtrn / DoorParty" → "A-Net Game Server (rlogin)". Added
a dedicated form section so the executable_path / command_line_args
fields are visible when door_rlogin is selected (same fix pattern as
the gallery_admin disappearing-fields bug). Updated helper text with
working examples for game.a-net-online.lol direct-to-door
(xtrn=LORD408, xtrn=ASSASSIN, etc.).

v1.0a — door I/O safety + auto-recovery (May 2026, internal v279.1–v279.9)

A series of hotfixes after v279 wired up the DosBridge — fixing
real-world door issues that surfaced during testing.

  • v279.1: _build_dos_command now accepts token_ctx=None for
    validation-only callers (the terminal launcher previewed the
    command before allocating a bridge port; without this it crashed
    with "unexpected keyword argument").
  • v279.2: DOOR.SYS / DORINFO / DOOR32.SYS now identify as COM1:
  • 38400 baud + comm type 1 (FOSSIL) instead of COM0: + 0 + comm
    type 2. Most classic doors (LORD especially) interpret COM0/0 as
    "local console — bypass FOSSIL, write to BIOS only", which dropped
    all output before it could reach the bridge.
  • v279.3: bridge diagnostic logging — first 5 chunks logged
    verbatim, "0 bytes after 10s" warning if door isn't writing to
    FOSSIL, total chunk/byte count on close.
  • v279.4: terminal door_dos now uses the unified launch path
    (launch_door_game via play_door_game_telnet). The legacy
    play_dos_game_telnet had its own DOSBox-launching code that
    predated the bridge wiring and never got updated; terminal users
    were getting silent hangs.
  • v279.5: waitpid watcher thread. xvfb-run can leave Xvfb running
    past the door exit (Xvfb inherits the PTY slave_fd, keeping
    master_fd from EOFing), which means our PTY watcher never wakes
    to fire cleanup. The new watcher does os.waitpid(pid, 0) and
    triggers _cleanup_session when the door subprocess actually
    exits — independent of PTY EOF.
  • v279.6: bridge close → cleanup signal. When DOSBox closes its
    end of the TCP nullmodem (which it does on exit), the bridge fires
    a new on_close callback that runs _cleanup_session immediately.
    Faster than waitpid for the common case.
  • v279.7: real Ctrl+]q abort. The launch banner has been telling
    users "press Ctrl+] then q to abort" since forever, but the input
    pump just forwarded those bytes to the door. Now the pump
    actually watches for the sequence and triggers cleanup.
  • v279.8: idle-timeout watchdog (default 300s). If zero bytes
    flow in either direction for the timeout, the bridge force-closes
    and triggers cleanup. Catches stuck-door cases (LORD's exit-loop,
    infinite "press any key" prompts on hidden screens).
  • v279.9: idle timeout reduced to 60s default + made configurable
    via DOOR_IDLE_TIMEOUT. Also force-kills the door process group
    via SIGTERM (then SIGKILL 2s later) when timeout fires, instead of
    just trusting _cleanup_session to do it.

End result: stuck DOS doors always return to the BBS within 60
seconds, the BBS user's session never permanently hangs, orphan
DOSBox processes get reaped.

v1.0a — DOS door bridge wired up (May 2026, internal v279)

v279 — actual DOS door I/O. The previous DOSBox configs wrote
serial1=stdio, which DOSBox-staging 0.80+ silently downgraded to
dummy — meaning DOS door output and keystrokes never reached BBS
users. The whole DOS door path was end-to-end broken on modern DOSBox.

This release wires dos_bridge.py (which has been complete-but-unused
in the tree for a while) into the launch flow:

  • DosBridge picks a free TCP port (5000–5100) and listens.
  • DOSBox config is serial1=nullmodem server:127.0.0.1 port:NNNN transparent:1. DOSBox dials in at boot.
  • New DosBridge.bind_emit(emit_fn) reads bytes off the TCP socket and
    forwards them to the BBS user's terminal via socketio_emit_fn.
  • New DosBridge.write(data) accepts BBS-side keystrokes and pushes
    them onto the TCP socket so DOSBox's COM1 receives them.
  • DoorSession.write() routes through the bridge when present, so the
    rest of the BBS doesn't have to care which transport is in use.
  • DoorSession.close() also stops the bridge so the listener doesn't
    leak.
  • The PTY remains open on door_dos sessions but only as a process-exit
    watcher; door I/O no longer flows through it.
  • DOSBox config also stripped invalid output=null and autolock
    options that staging 0.82.x rejects.

End result: vanilla DOSBox 0.74-3, DOSBox-staging 0.82.x, and DOSBox-X
all work for door_dos games via TCP nullmodem. No special build
required.

v1.0a — door-runtime polish (May 2026, internal v277–v278)

Hot-fixes plus the dosemu2 path, deployed on top of the v276 audit pass.

v278:
- New door_dosemu game type — uses dosemu2 (-dumb stdio) instead of
DOSBox. Better latency, no FOSSIL gymnastics over TCP nullmodem,
apt-packaged via dosemu2 PPA on Ubuntu / native on Debian. Mounts
game dir + per-node temp dir + bundled FOSSIL driver dir, autoexec
loads BNU.COM on COM1 for parity with the DOSBox path so the same
LORDCFG / TWCFG settings work either way.
- Snap-confine detector in _build_dos_command: when dosbox /
dosbox-staging resolves to a /snap/ path, we reject it up front
with a clear "remove snap, install apt or AppImage instead" message.
Avoids the cryptic cap_dac_override not found error on first
launch from systemd.

v277:
- DOSBox per-node mount: the per-node temp dir is now mounted as drive
E: in addition to the game dir on C:. Sysops set LORDCFG's "Path to
dropfile" to E:\ and multi-node LORD / TradeWars / TQW just works
without overwriting each other's drop files.
- docs/14-door-games.md and docs/15-synchronet-compat.md published.

v1.0a — pre-release polish (May 2026, internal v195–v276)

Final shake-out before tagging the alpha.

New: Image galleries
- /gallery/ — paginated thumbnail grid + full-screen modal viewer.
Browser-native, lazy-loaded.
- /admin/galleries/ — full CRUD on collections (label/slug/path/sort/
active) plus per-gallery file management (drag-drop multi-upload,
click-to-delete, pagination).
- Storage: gallery-config.json at install root, auto-seeded on first
run from any DSR gifs / swim directories that exist. Excluded
from deploy so sysop edits survive rsync --delete.
- Standalone terminal viewer: /home/<user>/anet-gallery.sh (chafa /
img2sixel) — kept outside the install dir so deploys don't remove it.
- Galleries link lives under Tools in the top nav (kept off the
primary bar to avoid wrapping on smaller widths).
- New doc: 13-image-galleries.md.

Removed: DSR (Digital Showroom)
- The Synchronet sixel image-viewer door is no longer wired into the
games menu. The convert → .sixel pipeline returned rc=0 with no
output file when the door was launched from a gunicorn-spawned PTY
child (worked manually but not under the web service); root cause
not identified after extensive investigation. The web /gallery/ +
terminal anet-gallery.sh cover the same use-case at higher quality.
- DSR files remain on disk under doors/sbbs/dsr/ so a future
reinstatement is possible without re-extracting from the .zip.

Codebase audit
- Real bug found and fixed: current_app referenced in web/admin.py
but missing from the flask import line (would have NameError'd at the
affected admin routes).
- Real bug found and fixed: missing requests from setup.py
install_requires — webhooks.py imports it unconditionally so a fresh
pip install -e . would crash on first webhook dispatch.
- Real bug found and fixed: missing admin/time_budgets.html template
/admin/time-budgets would 500 on GET. Template added matching the
rest of the admin UI.
- Real bug found and fixed: 4 broken url_for() references that would
raise BuildError at render time (echomail.view_message,
boards.view_thread, pm.read_message, echomail.list_messages
endpoints renamed in earlier work but the saved-messages and
permalinks blueprints still pointed at the old names).
- Real bug found and fixed: 3 duplicate method definitions in
core/session.py (write / read_line / clear_screen) — Python
silently kept only the later definitions, the first ones were dead
code. Removed.
- Real bug found and fixed: 3 stray f''-prefixed strings without
placeholders in web/echomail_admin.py and web/irc_web.py.
- Real bug found and fixed: update.sh rsync was missing --exclude
for /doors/ and /gallery-config.json — closes the same hole that
caused the v195 deploy incident.
- Duplicate import json in echomail/tic.py removed.
- Duplicate from typing import Dict in core/session.py consolidated.
- Explicit __all__ added to anetbbs/core/__init__.py (silences
pyflakes false-positive on re-exports).
- Orphan templates removed: admin/callers.html (replaced by
admin/caller_log.html), chat/index.html (legacy local-chat,
replaced by MRC).
- ~80 unused imports trimmed across the package — pyflakes warning
count down from ~110 to 22, with the remaining warnings all
intentional (load-bearing SessionProtocol re-export, protocol-level
drain variables, dead global declarations).
- All 180 Jinja templates parse cleanly.
- All 308 url_for() references resolve to a registered endpoint.
- 79/81 SQLAlchemy models are queried in code; the two stragglers
(ChatMessage, MenuTranslation) are leftover scaffold tables that
stay in the schema for now to avoid alpha migration churn.
- Zero outstanding TODO/FIXME/XXX/HACK markers in our code
(matches in vendored Synchronet .js stubs are upstream's).
- New top-level FEATURES.md — single-page inventory
of every user-visible feature for the alpha.
- docs/00-overview.md updated with the gallery feature.
- docs/PORTS.md + deploy/README.md now document the Finger
service (port 79).

Error pages + nav coverage
- New branded 404.html / 403.html / 500.html templates wired up
via errorhandler() (previous behaviour fell through to Flask's
default monochrome error pages).
- Sysop nav: added Time Budgets link to the Admin → Users dropdown
(the /admin/time-budgets page was registered but unreachable from
the UI — only the URL bar got you there).

Config completeness
- .env.example expanded with previously-undocumented but code-used
settings: SYSOP_NAME, BBS_DOMAIN, BBS_PUBLIC_HOST, BBS_EMAIL,
BBS_LOCATION, BBS_NODES, IDLE_TIMEOUT_SECONDS,
FINGER_LISTEN_HOST/PORT, BINKP_LISTEN_HOST/PORT,
BINKP_OUR_ADDRESS, BINKP_SYSTEM_NAME, CLAMSCAN_PATH,
IRC_LOG_CHANNELS, NUV_ENABLED, RATIO_MIN.

Door-game runtime parity with Synchronet / Mystic

A real BBS gives each active node its own scratch directory and
substitutes shortcodes (Synchronet %f, Mystic %P etc.) in door
command lines. Both were missing; both shipped this session.

  • New anetbbs/games/node_paths.py — per-node temp dirs at
    <install>/data/temp/nodeN/ (1..BBS_NODES), auto-created on app
    boot. Owns the substitution table.
  • Synchronet %-token vocabulary supported in Game.executable_path,
    Game.working_directory, Game.command_line_args, and
    Game.drop_file_path: %a, %c, %d, %e, %f, %g, %h,
    %i, %j, %k, %l, %m, %n, %o, %p, %r, %s, %t,
    %u, %w, %y, %z, %!, %%.
  • Mystic %-token vocabulary supported in the same fields:
    %P (per-node temp dir w/ trailing /), %N, %M, %U, %T,
    %H, %R, %E, %S, %L. So %Pdoor32.sys correctly resolves
    to e.g. /data/temp/node3/door32.sys.
  • Game.drop_file_path is now %-token expanded (was only {node}).
  • New doc: docs/14-door-games.md — full token
    table, LORD walk-through, and the rest of the door-config story.
  • New doc: docs/15-synchronet-compat.md
    xtrn coverage survey, what stock Synchronet doors are likely to work
    through our Node shim vs. need a real jsexec.

Mystic .mps auto-compile
- New _ensure_mps_compiled() in door_runner.py — if mplc is on
the host, automatically compile .mps source → .mpx bytecode
before launch (only if source is newer than bytecode). Falls back to
an existing .mpx next to the source if mplc isn't installed.
- New MYSTIC_MPLC_PATH env var for explicit override.

Installer modes (test vs production)
- install.sh now starts with an "Install mode" prompt:
- production = real BBS facing the public internet — wants
nginx, SSL, a domain pointed at the box, privileged-port services
(Finger/MSP/SYSTAT) reachable from peers.
- test = local-only / behind NAT / no static IP / just trying it
out — auto-skips nginx, certbot, Finger/MSP/SYSTAT/BinkP. Gunicorn
binds 0.0.0.0:5000 directly. Sysop can flip individual flags later
without re-installing.
- New optional install step: download Mystic Linux freeware tarball,
extract mystic + mplc to /opt/mystic/, symlink into
/usr/local/bin/, write MYSTIC_MPLC_PATH to .env. Soft-fails
if the download URL is unreachable (no network, mysticbbs.com down).
- anetbbs-finger.service unit now installed by install.sh when
ENABLE_FINGER=y (was never deployed by the installer despite the
unit file existing in deploy/).
- Test-mode summary message at end of install spells out exactly what
was skipped and how to re-enable later.

Deploy / ops
- update.sh and the documented rsync recipe explicitly
--exclude=doors --exclude=gallery-config.json --exclude=data/ to
prevent the v195-era incident where a --delete deploy wiped the
doors tree (DSR, BotWars, RDQ3, the 6500-GIF library) from a
production install.

Misc
- permission denied regressions on the MRC bridge fixed by ensuring
service unit User=stingray matches actual file ownership on the
reference deployment (was getting accidentally chown'd to anetbbs).

v1.0a — alpha refresh (May 2026, internal v194)

Subsequent rolling internal builds (v165-v194) refine the alpha. Public
tarball at ANetBBS-v1.0a.tar.gz always tracks the latest internal cut.

Highlights since the v164 alpha:

  • MSP / Inter-BBS IM correctness — the previous wire format was a
    homegrown 5-field variant that Synchronet rejected silently and we
    mis-parsed in the inbound direction. v168 ships a real RFC 1312 MSP-2
    encoder/decoder (7 fields with a leading 'B'/'A' type byte), strips
    @host from inbound recipients, optionally answers acked-mode requests.
  • Synchronet @CODE@ substitution in our Synchronet stub layer's
    printfile@BBS@, @USER@, @SYSOP@, @TIME@, @DATE@,
    @SECURITY@, @CALLS@, @NODE@, etc. resolve from the BBS_*
    environment variables that door_runner.py exports.
  • Display-code preprocessor for ANSI screens and BbsMenu screens —
    Synchronet @-codes (@USER@, @BBS@, @TIME@, ...) and Mystic
    named codes (|UN, |BN, |DT, ...). Color-pipe codes like |07
    remain handled by the existing _pipe_to_ansi.
  • QWK polling improvements — Synchronet's 550 No QWK packet created (no new messages) is now treated as benign instead of a
    fatal poll error (was generating thousands of false ERROR lines per
    day). Poll interval clamped to ≥5 minutes to prevent a misconfigured
    DB from hammering remote hubs.
  • Web-stack hardeningKillMode=mixed + RestartSec=10 on the
    gunicorn unit so eventlet workers don't leak past the master and
    cause EADDRINUSE crash loops; _create_default_data os shadowing
    bug fixed; body keyword for PrivateMessage (was content);
    /help endpoint repaired.
  • Permissions — install.sh asserts ssh_host_key + sbbs_stubs/
  • doors/ permissions on every run (not only at first generation),
    with sbbs_stubs source files now shipped world-readable so future
    rsync deploys don't reset BotWars/RDQ3 to broken.
  • Terminal MRC client — extensive UX work:
    • anetmrc-style stationary status bar at the top (room, topic,
      mention badge, server latency)
    • DECSTBM scroll region for chat below it
    • HH:MM timestamp prefix on every chat line, ANSI-aware word-wrap
      with continuation indent
    • Tab-complete usernames (active list seeded from chat events,
      /who, /chatters)
    • New slash commands: /afk [msg], /back, /status,
      /roomconfig, /termsize, /whoon alias of /who
    • /info <id> now passes the BBS index to the server
    • /scroll removed (terminal-native scrollback works fine)
    • Removed the doubling-on-wrap bug (input is slide-windowed; chat
      uses ANSI-aware word wrap instead of native terminal wrap)
  • Terminal BBS UI — single-key hotkeys (no Enter required), screen
    clear before each menu, Q logoff confirms with Y/N, banner+colored
    rendering on Boards / Echomail / Bulletins / Who's Online / Sysop
    Tools / Profile / PM compose / IM compose / inboxes, paged area
    picker for compose-echomail, terminal IM reader ([I] and [J]).
  • Web nav reorg — Inter-BBS IM badge in the top bar (next to PM
    envelope and notification bell), Web Terminal moved from Chat
    dropdown to top of Tools dropdown.
  • Privacy — IP column on /who now hidden from non-admins.
  • CP437 mojibake recovery for ANSI screens stored as Latin-1 in
    the DB (encode→bytes→write-as-CP437 path so SyncTERM and modern
    terminals both render correctly).
  • Dialout default directory trimmed to a single seed entry
    (sysop adds the rest via /admin/dialout).
  • Door runner — door cleanup callback now runs inside a captured
    Flask app context (was leaking "Working outside of application
    context" errors when sessions ended).
  • Web service unit — config templated for /opt/anetbbs/; the
    install.sh rewrite handles INSTALL_DIR substitution properly.

v1.0a — first alpha (May 2026)

Bundles all internal versions through v164.

Major features:
- Web BBS (Flask + SocketIO) + telnet + SSH + rlogin front-ends
- FidoNet binkp echomail + netmail with full kludge support (MSGID, REPLY,
INTL, FMPT/TOPT, CHRS, PID, TZUTC), CRAM-MD5 BinkP auth, optional TLS
- DOVE-Net QWK echomail with REP packet outbound, CONTROL.DAT-driven
area auto-create, 1-on-1 QWK netmail compose
- Inter-BBS Instant Messaging via MSP (RFC 1312) on TCP/18 + SYSTAT/
ActiveUser on UDP/11, with the Synchronet sbbsimsg.lst directory
mirrored daily and a built-in BBS picker UI
- Terminal MRC chat (telnet/SSH/rlogin) — talks to the same local
websocket bridge that web users hit, so terminal and browser users
share the same hub identity and chat rooms. Full command set
(/identify /msg /me /join /list /who /motd /banners /topic /scroll /mentions /trust /broadcast …); ←/→ arrow keys cycle outgoing text
color; /identify mid-line password masking; @mention highlighting
with bell; outgoing 140-char cap with auto-split on word boundaries;
ROOMTOPIC banner + topic snippet baked into the prompt
- Local boards with voting, search, sticky/lock, threading, ANSI banners,
per-board moderators
- File bases with TIC ingest, outbound TIC hatching, FILE_ID.DIZ
auto-extract, per-area upload permissions
- Doors: DOSBox (auto-detect staging/x/vanilla), Mystic .mps and .mpy,
Synchronet .js (real jsexec when available, Node.js shim fallback),
native binaries — all with DOOR.SYS / DOOR32.SYS / DORINFO drop files
- Private messages, FTN netmail, CP437 + ANSI rendering everywhere
- AreaFix in/out with per-network admin queue, BadArea sysop-review queue
- Admin: rate limits on login/MSP/votes, random initial admin password
written to data/admin_password.txt, SECRET_KEY guard refusing dev
default in production, open-redirect-hardened login