ANetBBS Changelog — Beta History (pre-v1.0.0)
Archived history of every internal beta build number (v1.0a1.1 through
v1.0b2.239) from before ANetBBS's v1.0.0 full release in August 2026.
Preserved as-is — a real record of what changed at each step during
development — but split out of the main CHANGELOG.md
once it passed 6,000 lines, to keep that file scannable for the
v1.0.0-onward history that actually matters day to day. Newest-first,
same as the main changelog.
v1.0b2.103 — MRC bridge: fixed a hard identify-wall blocking all chat by default (July 2026)
- FIX (significant): reported live as "I still have to identify every single time" / "blocked from chatting entirely until identified" — every MRC caller, on every install, was silently blocked from sending or (in some paths) seeing chat at all until they ran
/identify <pass>against a registered account on the upstream hub, every single connect. Root-caused against the real reference C client (anetmrc_v1.3.9/src/helper_protocol.c): it joins the room unconditionally right after the handshake and never waits on identify —/identifyis purely optional "MRC Trust" for a registered handle, never a requirement to participate, and casual/unregistered handles chat freely on the real network. The bridge's ownidentify_required_modegate defaulted toTruewith no documented way to discover or disable it (not even inconfig.example.json), and even flipping it off alone didn't help — a second, equally undocumented flag (post_identify_auto_join, also defaulting off) additionally had to be set before a caller was ever actually placed in the room. Default changed to match the reference client's real behavior:identify_required_modenow defaults toFalse, and joining a room completes immediately, no/identifyneeded, for every existing install without any config change. An admin who deliberately wants the old strict, identify-required behavior can still opt in viaconfig.json; that path is unchanged and fully covered by tests. - 9 new tests, including one that builds a real
BridgeAppfrom a config file with noidentify_required_modekey (matching every existing install's actual config.json) and asserts the new default takes effect.
v1.0b2.102 — MRC Phase F follow-up #3: BEL character threw off border alignment (July 2026)
- FIX: found live on the Pi — one specific row's
│border still landed one column early even after the sidebar/status-bar/ticker border fixes (v1.0b2.99-101). Root cause: mention/DM alerts prepend a bare BEL (\x07, rings the terminal bell) ahead of the highlighted message text — zero-width on a real terminal, but_visible_lencounted it as one visible column since_ANSI_SEQ_REonly matched ESC-prefixed sequences, not a bare BEL. Every message that happened to mention the caller's own handle (or arrive as a DM) was one column short on padding, throwing its row's border out of alignment by exactly one column._ANSI_SEQ_REnow also matches BEL. - 3 new tests (711 passed total), including an end-to-end reproduction (a mention message's row lands at the same border column as every other row).
v1.0b2.101 — MRC Phase F follow-up #2: ticker border fix + STATS: inline noise silenced (July 2026)
- FIX: found live on the Pi as "border still has one spot" even after v1.0b2.100's status-bar fix — the scrolling ticker row (between the status bar and chat area) had no border/sidebar treatment at all, scrolling its text against the full terminal width with nothing lining up against the
│every other split-screen row draws. Fixed the same way as the status bar: measured/padded againstchat_widthand followed by a matching border + blank nick-column-width gap when the sidebar is enabled. - FIX: found live on the Pi — raw
STATS:175 15 39 2 170 34.0-shaped lines were popping up inline mid-chat. This was deliberate, documented, unchanged-on-purpose behavior from when the ticker was first built (STATS: predated the ticker and had nowhere else to go), but now that the ticker pool already captures it, the inline copy is just noise. Silenced the same wayBANNER:already is. - 3 new tests (708 passed total).
v1.0b2.100 — MRC Phase F follow-up: named /set tz zones + status-bar border fix (July 2026)
- FEATURE:
/set tznow accepts common named zone abbreviations (EST,EDT,CST,CDT,MST,MDT,PST,PDT,AKST,AKDT,HST,AST,ADT,UTC,GMT,BST,CET,CEST,EET,EEST,IST,JST,AWST,ACST,ACDT,AEST,AEDT,NZST,NZDT) in addition to raw±H[:MM]offsets — reported live on the Pi as the raw-offset-only form being confusing. Fixed-offset aliases, not DST-aware, matching the feature's existing plain-UTC-offset design (a caller currently observing daylight time picks the*DTname, not*ST). - FIX: found live on the Pi as "the border still messed up in one spot" after the v1.0b2.99 sidebar fix — the status bar (row 1) was drawn full terminal-width with no
│border of its own whenever the nick-list sidebar was enabled, so its right-aligned badges (mention count, latency, clock) landed past the column where every chat row's border sits, with nothing to match it. Now measured/padded againstchat_widthlike every other split-screen row and followed by the same border + a blank nick-column-width gap, so the status bar's right edge lines up with the sidebar below it. - 5 new tests (705 passed total).
v1.0b2.99 — MRC feature-parity rework, Phase F: quality-of-life additions (July 2026)
- FEATURE: sixth phase of the MRC feature-parity rework (see v1.0b2.94-98). Terminal MRC gains
/r <text>(reply to your last received DM), input-history recall on Ctrl+Up/Down (plain Up/Down stay bound to chat-scroll, the terminal's existing convention — Ctrl+arrow required actually preserving the CSI modifier byte in_read_escape_seq, which previously truncated every modified cursor sequence the same way legacy PgUp/PgDn does),/dlchatlog(download the session's scrollback as a text file via whatever ZMODEM-family protocol is available, same tempfile-then-send_file pattern already used by the ebook reader's download command), and a light/set palette <name>chrome color picker (default/green/amber/cyan/mono) covering the status-bar room tag, nick-list sidebar, and ticker line — cosmetic-only scope mirroring the web client's 5 CSS themes, not a full re-theme of every message color. Web MRC gets a matching client-side "Download chat log" button in the Chat Prefs panel. Also reviewed whether either client needed a dedicated welcome screen (a locked-in Phase F item) and found both already surface the bridge's realwelcomeevent on connect — terminal via its generic unknown-event fallback, web via an existing explicit handler — so nothing further was added there. - FIX: found live on the Pi — the MRC clock widget and message timestamps used the server's system clock, not the caller's own timezone, silently correct only for a caller who happens to share the server's TZ (server on UTC, sysop several hours off in this case). Added
/set tz <offset>(e.g.-5,+5:30,utc), persisted per-handle via the sameset_prefsmechanism as the other Phase E settings; terminal timestamps now compute from UTC + this offset instead of the server's local clock. Web was already correct (browser-local time), no change needed there. - FIX: found live on the Pi — the nick-list sidebar's
│border column could drift out of alignment when a buffered chat line had been word-wrapped against a wider chat width before the sidebar narrowed it (e.g. sidebar enabled mid-session, or a stale line surviving a resize). The redraw only ever padded short lines to the border column, never truncated long ones, so an over-width line pushed the border past its column and could auto-wrap the physical terminal row, corrupting the next row's redraw too. Now defensively truncated to the current chat width before the border is drawn. - 53 new tests (700 passed total, Python side; web JS syntax-validated).
v1.0b2.98 — MRC feature-parity rework, Phase E: /set command + remaining settings (July 2026)
- FEATURE: fifth phase of the MRC feature-parity rework (see v1.0b2.94-97). Terminal MRC gets a real
/set <field> <value>command (plus/set listand/set help) covering nick prefix/suffix/color, custom enter/leave/quit chat messages, and the scrolling ticker toggle — the terminal's/setfor prefix/suffix/color goes throughset_style, same wire message the web client's style panel already used, and now/setis the terminal's first way to change those at all (previously arrow-key cycling was the only style control terminal had). Web gets equivalent fields added to the "Chat Prefs" panel (ticker toggle, enter/leave/quit messages) for the same shared, server-persisted settings. Toggling the ticker via/set tickeror the web checkbox now actually re-lays-out the terminal screen live instead of only taking effect on next reconnect. - FIX: found while building the above — arrow-key outgoing-color cycling (
_cycle_color) sent only{type: set_style, typing_color: ...}to the bridge, but_handle_set_style(mrc/bridge/main.py) hard-defaultsprefix/suffixto empty string when the field is simply absent from the request (unlike every other style field, which correctly falls back to the existing session value). This silently wiped any prefix/suffix decoration a user had set via the web style panel every single time they cycled their outgoing color with the arrow keys — a real, previously-shipped bug, not something introduced by this phase. Fixed by always sending the full last-known style with just the changed field(s) overridden, via a new_style_payload()helper now shared by both/setand arrow-key cycling. - 20 new tests (Python side; web JS syntax-validated).
v1.0b2.97 — MRC feature-parity rework, Phase D: twit list + broadcast shield (July 2026)
- FEATURE: fourth phase of the MRC feature-parity rework (see v1.0b2.94-96). Both terminal and web MRC clients gain a twit/ignore list and a broadcast shield, persisted server-side per handle via a new
set_prefs/prefs_updatedround-trip and shared between both clients automatically. Filtering happens client-side by design (the bridge fans one event stream to potentially many local clients, so per-viewer muting can't be centrally enforced) — messages from a twitted sender are dropped and counted, not silently invisible. Terminal gets/twit add|del|list|clearand/shield on|off; web gets a new "Chat Prefs" panel alongside the existing style settings./broadcast(terminal) and the equivalent web slash command are now refused locally when the shield is on. 19 new tests (Python side — bridge protocol + terminal filtering logic; web JS syntax-validated, UI verified by hand).
v1.0b2.96 — MRC feature-parity rework, Phase C: terminal scrolling ticker (July 2026)
- FEATURE: third phase of the MRC feature-parity rework (see v1.0b2.94/95). Terminal MRC gets a scrolling ticker row between the status bar and chat area, rotating through static tips and — for the first time — real hub-pushed
BANNER:text, which was previously discarded entirely with no ticker to feed.STATS:text (already shown inline, unchanged) now also feeds the ticker rather than going unused after being displayed once. Short items dwell briefly before rotating; items too long for the terminal width scroll a few characters per tick instead. 12 new tests.
v1.0b2.95 — MRC feature-parity rework, Phase B: terminal nick-list sidebar + clock (July 2026)
- FEATURE: second phase of the MRC feature-parity rework (see v1.0b2.94). Terminal MRC (
anetbbs/features/mrc_chat.py) gains a live nick-list sidebar on wide-enough terminals (132+ columns) — the DECSTBM split-screen primitive only constrains vertical scrolling, so the sidebar rides along on the same row writes as the chat text rather than being a separate scroll region (no left/right sidebar rendering precedent existed anywhere in ANetBBS's terminal UI before this). Also adds a clock widget to the status bar, refreshed every 30s so it doesn't go stale during a quiet room. Both purely additive UI — no settings/persistence yet (that's Phase E). 10 new tests.
v1.0b2.94 — MRC feature-parity rework, Phase A: bridge protocol + prefs (July 2026)
- FEATURE: first phase of a multi-phase MRC (Multi Relay Chat) rework bringing ANetBBS's terminal and web clients toward full feature parity with the wider MRC ecosystem. This phase is bridge-only (
mrc/bridge/main.py,mrc/bridge/db.py) and lays the foundation the rest builds on: - New structured
userlistWebSocket event, sent alongside the existing raw-textUSERLIST:relay whenever the bridge already refreshes a room's roster (on join, periodically, or on certain server-text triggers) — a real, independently-verified wire format (matches the web client's existing parser and the terminal client's own USERLIST:/CHATTERS: handling), not a guess. - Periodic
STATSrequest added, feeding future ticker/banner work as opaque display text — deliberately not parsed into structured fields. An earlier draft of this phase assumed a structuredbbs_count/room_count/activity_levelformat based on the Mystic BBS multiplexer's own internal file-protocol; checking the actual reference C client that talks to a real MRC hub showed no evidence real hubs send anything but free text for STATS (same as MOTD/BANNERS), so the fabricated field parsing was dropped before shipping. - New
set_prefs/prefs_updatedrequest/response pair (mirrors the existingset_style/style_updatedpattern): persists twit/ignore list, broadcast shield toggle, ticker toggle, and custom enter/leave/quit message templates per MRC handle. - Per-user enter/leave message templates now actually apply (previously the bridge only ever had one global template for every user); explicit
/quit <message>is now threaded through end-to-end instead of being silently discarded by the bridge'sleave_roomhandler. - 19 new tests.
v1.0b2.93 — Hub identity seeded active by default (July 2026)
- FIX: the default
HubIdentityrow (see v1.0b2.92's multi-hub-identity foundation) was seeded withis_active=True, so simply turning onREGISTRY_MODE_ENABLEDmade ANotherNetwork look like a live, fully-configured hub — before the sysop had touched anything. Doesn't match the existing convention: the seeded ANotherNetworkEchomailNetworkrows (both BinkP and QWK) have always startedis_active=False, requiring the sysop to fill in real node address/packet-id/password before flipping them on. Fixed the seed to match (is_active=False), and changed the admin "Add Hub Identity" form's default to unchecked for the same reason — a freshly created identity shouldn't look live until deliberately activated.
v1.0b2.92 — Terminal MRC never actually read its configured bridge port (July 2026)
- FIX: a sysop reported MRC chat working fine in the web UI but never connecting over the terminal (SSH/telnet) client, with only a brief, unreadable error flash on screen. Root cause:
MRCChat.show_menu()(anetbbs/features/mrc_chat.py) readcurrent_app.configwith no Flask app context active — unlike_chat_flags()a few lines above it inchat.py, which correctly scopes its own DB read in one. Every config lookup therefore raisedRuntimeError: Working outside of application context, silently swallowed by a bareexcept: pass, so terminal MRC always fell back to the hardcodedDEFAULT_BRIDGE_URL(port 8080) — regardless of the real bridge port (WEB_PORT+1, 5001 by default). This wasn't specific to one install; it silently broke terminal MRC on every install, unnoticed until now. Fixed by wrapping the lookup inwith _app().app_context():, the same pattern already used elsewhere in the file. Also addedlogger.warning(...)to the connect-failure path, which previously only ever wrote to the failing user's own terminal screen and vanished on redraw — the exact reason the error was unreadable and left no trace to investigate afterward. 1 new regression test.
v1.0b2.91 — BinkP: unhandled send-side disconnect during EOB/GOT handshake (July 2026)
- FIX: a sysop reported a BinkP poll to one particular hub failing and repeating on every scheduled poll, while two other hubs on the same install worked fine. Root cause:
_receive_messages()(anetbbs/echomail/binkp.py) already treats the hub closing the connection during our receive calls as a clean, expected end of session (added in the v1.0b2.57-60 two-round-EOB work) — but the two acknowledgement sends right next to it, the secondM_EOBand the per-fileM_GOT, had no equivalent guard. If the hub closes its side of the socket in the narrow window before either of those sends goes out,sendall()raises an uncaughtOSError, which unwinds out ofpoll()and gets logged as a genuine poll failure even though the file transfer itself completed successfully. Wrapped both sends in the same log-and-break pattern already used for the receive side. - NOTE: polling that one hub on a 1-minute interval is a very plausible reason only it triggers this — if its mailer has any connection-rate throttling, frequent polling could be causing it to abort sessions mid-handshake, which is exactly the trigger condition here. Slower polling (30-60 min, standard FTN practice) should reduce how often this is hit regardless of this fix.
v1.0b2.90 — Configurable Last Callers row count + screen clear (July 2026)
- FEATURE: the terminal Last Callers screen (
anetbbs/features/lastcallers.py,show_last_callers()) always fetched and paginated a hardcoded 200 rows, which made for a long scroll on a busy install — the sysop reported the list had grown too long. AddedLASTCALLERS_DISPLAY_COUNT(5/10/15/20/25/30/50/100, default 20), configurable from/admin/lastcallers/alongside the existing InterBBS/hide-sysop settings, following the same.env-persistedcurrent_app.configpattern asLASTCALLERS_HIDE_SYSOP. Only the full terminal screen is affected — the admin audit list (/admin/lastcallers/) and the web one-liners page's fixed "Last 10" preview both keep their own independent limits, since neither was the thing the sysop was looking at. - FIX: the same terminal screen never cleared before drawing, unlike every other full-screen terminal view in the app (
\x1b[2J\x1b[H). Now clears first. - 3 new tests: display-count persists a valid choice, an out-of-range/tampered value snaps to the nearest offered choice rather than becoming an arbitrary unbounded limit, and the default (no value submitted) is 20.
v1.0b2.89 — Move pre-update backups off /tmp onto persistent disk (July 2026)
- FIX: found live, immediately after deploying v1.0b2.87's disk-space check to a real Pi install — the check reported the backup filesystem (
/tmp) had only 442MB free, well under the 500MB minimum, soupdate.shcorrectly refused to proceed. Butdf -h /on the same machine showed the real disk at 117GB total, only 4.9GB used, 108GB free — 5% utilization. The disk itself was nowhere near full. The actual cause: on this Pi,/tmpis mounted as a RAM-backed tmpfs, entirely separate from the real disk and sized off available memory rather than storage — a detail this codebase had already run into once before in an unrelated context (a prior memory note: "/tmpon the Pi is a small tmpfs (~453M, sized off the Pi3's 1GB RAM) and fills up fast"). The pre-update backup (.env, both SQLite databases, systemd unit files, nginx config) was landing on/tmp/anetbbs-backup-*, so it was competing for space against that same constrained RAM allocation — v1.0b2.87's disk-space check was doing exactly its job, correctly refusing to risk a backup that couldn't actually fit, but the real problem was the backup's location, not the amount of free space on the machine as a whole. - Moved the backup location in
update.shfrom/tmp/anetbbs-backup-$(date +%Y%m%d%H%M%S)to$INSTALL_DIR/data/backups/anetbbs-backup-$(date +%Y%m%d%H%M%S).INSTALL_DIR/data/is real, persistent disk — already excluded from the update's own file-sync step (--exclude='/data/'), so backups can't collide with anything the sync overwrites — and survives a reboot, unlike/tmpon distros that clear it at boot (a second, independent risk the old location carried that this move also happens to fix). The disk-space pre-flight check added in v1.0b2.87 now only needs to check$INSTALL_DIR— the backup and the rest of the update both live on that filesystem now, so the separate/tmpcheck (which was the thing that caught this exact problem, doing its job correctly) is no longer needed as a distinct check. - Updated
anetbbs/web/backups_admin.py(the/admin/backups/page that lists, deletes, and restores these snapshots) to match —_BACKUP_ROOTwas a hardcoded module-level'/tmp'constant; replaced with a_backup_root()function that resolvesINSTALL_DIRfrom the running Flask app's config the same way_restore_helper()in the same file already did, so it works correctly regardless of where a given install actually lives (/opt/anetbbs, a sysop's home directory, etc.) rather than assuming/tmpuniversally. - Carefully re-anchored
deploy/run_restore.sh— the privileged (sudo) helper that actually performs.env/DB restores and backup deletion on the admin UI's behalf — since its security path-allowlist was a hardcodedcase "$BACKUP" in /tmp/anetbbs-backup-*)pattern, specifically designed to stop a compromised or buggy caller from pointing the privileged helper at an arbitrary filesystem path. Reordered the script soINSTALL_DIRgets resolved from the root-written/etc/anetbbs.installsentinel before path validation runs (previously validation ran first), then anchored the allowlist to"$INSTALL_DIR"/data/backups/anetbbs-backup-*instead of the old literal —INSTALL_DIRitself comes from a root-owned sentinel file, never from caller input, so this doesn't weaken the check. Verified directly (not assumed) that both layers of the original defense still work exactly as before: the prefix-match layer rejects anything outside the new location, and the separate forbidden-character layer (*..*,*;*, etc.) still catches a path-traversal attempt that manages to satisfy the prefix pattern by embedding../after a valid-looking prefix. - 4 new tests confirming
_backup_root()tracks two differentINSTALL_DIRvalues exactly (rather than checking against a literal/tmp, which would have been confounded by test fixtures themselves living under/tmpon most systems), that the admin page's directory scan finds backups under the new location, and that path-traversal rejection still works.