ANetBBS Changelog

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

v287.5 — profile form nested-<form> fix, deploy-script path bomb (May 2026)

Two more profile-edit bugs found while v287.4 was being deployed:

  • Profile edits never submitted at alltemplates/profile/edit.html
    had a nested <form action="/profile/avatar/remove"> inside the main
    profile form. HTML forbids nested forms; browsers auto-close the outer
    form at the inner <form> tag, so the Privacy / Signature / Tagline /
    Theme dropdown / Update Profile submit button ended up outside any
    form. Clicking Update did literally nothing — no POST left the browser,
    no field ever saved. Why nobody's users.theme_id was ever non-NULL.
    Fixed by lifting the avatar-remove form out of the main form and
    pointing the Remove Avatar button at it via the HTML5 form= attribute,
    so the visual layout is unchanged but the markup is now valid.
  • anetbbs-deploy-latest.sh step 4.5 path bomb — the script was
    copying deploy/*.service straight into /etc/systemd/system. The
    source units ship /opt/anetbbs as the install path, but real installs
    often live elsewhere (e.g. /home/stingray/anetbbs). After a deploy,
    systemd would try to exec /opt/anetbbs/venv/bin/gunicorn, fail with
    status=203/EXEC, and crash-loop the web service. Step 4.5 now rewrites
    /opt/anetbbs$INSTALL (the script's own install-path variable)
    before installing the unit. Existing live boxes that already got
    clobbered need a one-time sudo sed -i 's|/opt/anetbbs|/home/stingray/anetbbs|g' /etc/systemd/system/anetbbs*.service && sudo systemctl daemon-reload.

v287.4 — profile form + nginx upload limit (May 2026)

Two reported bugs in one cut:

  • Profile edits silently failed for anyone with a .local email.
    WTForms Email() delegates to email-validator 2.x, which rejects
    RFC 6761 special-use TLDs (.local, .test, .invalid, .example).
    The seeded admin user is admin@anetbbs.local, so every profile save
    — including the theme dropdown — bounced off email validation before
    reaching the DB. Theme picker looked broken, but the real problem
    was upstream. Replaced Email with a permissive regex validator
    (anetbbs/web/validators.py:PermissiveEmail) in auth.py,
    profile.py, and admin.py. Any RFC-shaped address now passes,
    including FidoNet-style aliases and internal-only TLDs.
  • Avatar uploads under 2 MB returned 413 Request Entity Too Large.
    The bundled nginx template (deploy/anetbbs-nginx.conf.template)
    inherited nginx's 1 MB default client_max_body_size. Added an
    explicit client_max_body_size 110m; directive (covers the 100 MB
    UPLOAD_MAX_SIZE plus headroom). On upgrade, regenerate the live
    nginx config or add the directive by hand inside the server {}
    block and nginx -t && systemctl reload nginx.

v1.0b.1 — branding consistency pass (May 2026)

Tiny but everywhere: replaced ANET BBS / Anet BBS / ANet BBS /
anet bbs / ANET-BBSANetBBS across 61 source files.
User-visible places include the "Powered by …" footer line, default
BBS_NAME fallbacks, systat banner, copyright lines, log messages,
admin tool descriptions, etc.

Functional code unchanged. Just one consistent spelling.

v1.0b — alpha 2 (May 2026)

Cut from internal build v287.1. Bundles every internal release from
v197 through v287.1; the per-build notes below capture the granular
changes. Headline additions:

  • LORD ships pre-installed and plays end-to-end under the Node +
    Synchronet compat shim (v283.x–v287 added the deep API stubs the
    game exercises).
  • Wiki at /wiki/ with revision history, diff, search,
    wanted/orphan reports, 41 seeded pages (v284).
  • RSS reader — web + terminal (v281).
  • Web doors at 80×25 with the real CGA palette, plus pre-join
    output buffering so welcome screens render immediately (v287, v287.1).
  • NodeSpy kick for stuck terminal sessions, cross-process via
    DB-flag/poll (v283.x).
  • Echomail terminal reader properly paged with [Q]=quit,
    CP437 body passthrough (v283.5–v283.7).

v287.1 — Web doors: buffer pre-join output (May 2026)

Some doors (ANetSIMS, A-Net Sixel TV, anything that draws a welcome
screen then waits for input before the user does anything) showed
nothing in the web terminal until the user pressed Enter. The
keystroke produced new output which then DID appear. Telnet / SSH /
rlogin worked fine because they don't have this race.

Root cause was a race in web/games.py:handle_start_game:

  1. launch_door_game() starts the PTY-reader thread synchronously.
    Within milliseconds the reader calls _emit_output(welcome_bytes).
  2. At that instant sid_box[0] is still None (set on the next
    line), so _emit_output returned early — bytes lost.
  3. Even after sid_box[0] was set, the next emit went to
    room=str(session_id) — but join_room() hadn't run yet, so the
    client wasn't in the room. Also dropped.
  4. First user keystroke → door writes more output → by then
    join_room() had run → bytes flow.

Fix: _emit_output now appends to a pre-join buffer (under a lock,
the reader is on its own thread) while a _buffering flag is true.
After launch_door_game returns, sid_box is set, the room is
joined, game_started is emitted, and _flush_pre_join_buffer()
drains the buffer as one chunk and flips the flag — subsequent
reads bypass the buffer and emit directly.

v287 — Web door terminal: pin to 80x25, real CGA palette (May 2026)

After LORD saved/loaded cleanly in v286.10 there were two remaining
visual issues:

  1. ANSI art bled into menus. Violet's portrait and her flirt
    menu appeared overlapped. Cause: play_terminal.html started the
    xterm at 80×24 but immediately ran fitAddon.fit() to fill the
    viewport — typically 150+ columns. LORD's gotoxy(50, 12) then
    landed at the wrong column and the menu overprinted the art.
  2. Gray where colors should be. xterm.js's default red/green/blue
    are web-ish midtones, not the CGA primaries doors are written for.

Fixes:

  • Pin xterm.js to 80×25 in play_terminal.html. Removed the fit
    addon and the window-resize handler. Wrapped the terminal in a
    centered, shrink-to-fit container so it sits cleanly in any
    browser width.
  • Real CGA palette — full 16 colors set on the xterm theme.
    Matches what door authors saw on a PC (DOS bright red is
    #ff5555, not whatever the browser thought).
  • screen_rows = 25 in synchronet_compat.py (was 24).
  • PTY initial winsize 80×25 in door_runner.py. Some kernels
    default new ptys to (0, 0) — without an explicit TIOCSWINSZ
    the door reads console.screen_rows as 0 and gotoxy breaks.
  • Font bumped to 16px Cascadia Mono / Consolas / Courier New for
    a sharper CP437 render.

v286.10 — LORD: File.write at-cursor + exit-hook silence (May 2026)

CriticalFile.prototype.write(str, len) in the compat shim
was this._content += strappend to EOF, ignore position.
But recordfile.js calls this.file.write(wr, len) for every
String / Date / Float field at a seeked record offset
(this.file.position = rec * RecordLength then a chain of writes).
Every string field on every record landed at the file's tail
instead of its proper slot. That's why the player-list rankings
showed character-name fragments mixed with timestamps, and
nonsense XP values: not corrupt bytes, just bytes in the wrong
columns.

Fixed write() to honor _pos and overwrite at the cursor
(padding with NULs if seeked past EOF), matching writeBin/writeStr.
writeln() now goes through the same path.

Also silenced the cosmetic [BBS] exit hook failed: player is not defined line that prints at every clean quit — LORD's
js.on_exit cleanup references variables that are already out
of scope by exit. The hook's purpose was already-redundant
cleanup; swallow the error. Set BBS_DEBUG_EXIT_HOOKS=1 if you
ever want to see them.

Save data after this update: old player.bin / state.bin
files were written with the broken layout. They'll READ wrong
in the new code (and may corrupt further on write). Recommended
to delete and start fresh:

sudo -u stingray rm -f \
  /home/stingray/anetbbs/anetbbs/games/sbbs_doors/lord/state.bin \
  /home/stingray/anetbbs/anetbbs/games/sbbs_doors/lord/player.bin \
  /home/stingray/anetbbs/anetbbs/games/sbbs_doors/lord/*.lock

v286.9 — LORD: check_gameover null-deref guard (May 2026)

Same pattern as v286.8's flirt-with-Violet patch:
check_gameover runs at the top of every door entry. If
state.won_by >= 0 (someone "won"), it does
wb = player_get(state.won_by) then immediately wb.name.
When the referenced player record is gone, the door crashes
with Cannot read property 'name' of null — and the next
entry crashes again because the bad flag is still in state.

Patched lord.js to null-check wb, reset state.won_by = -1,
put_state(), and let the door continue. The game effectively
re-opens for play instead of being stuck on a perpetual "game
over" screen.

v286.8 — LORD: flirt-with-Violet null-deref guard (May 2026)

In-game crash patch: flirt_with_violet() assumed
player_get(state.married_to_violet) always returns a record,
and dereferenced op.name directly. If state.married_to_violet
points at a stale record (record deleted between marriage and
the next flirt; or our fresh state file has a non-(-1) default),
player_get returns null and the door bombs with
Cannot read property 'name' of null.

Patched lord.js to:
- Check op === null after player_get
- Reset state.married_to_violet = -1 and put_state() (so the
flag stays cleared after the door exits)
- Show a generic Grizelda-kisses-you-painfully line instead of
the personalised you curse <name> text

This is an upstream LORD-JS bug we should ideally push back; for
now the patch is marked // ANetBBS patch: in the source for
easy review.

v286.7 — LORD: File.readBin / writeBin / Str + full mode parsing (May 2026)

Next missing piece: RecordFile.writeFieldthis.file.writeBin(val, N).
Synchronet's File has typed-binary I/O (readBin(N) / writeBin(value, N))
for fixed-width LE unsigned integers, plus readStr(N) / writeStr(s, N)
for fixed-width strings. recordfile.js (the library LORD uses to store
the per-record game state in lord.dat etc.) wires every field through
them.

Added in this round:

  • File.prototype.readBin(bytes) — read N-byte LE unsigned int
  • File.prototype.writeBin(value, bytes) — write same, overwriting at pos
  • File.prototype.readStr(n) / writeStr(str, n) — fixed-width strings
    with space-pad/truncate semantics

Also overhauled File.open(mode) to honor the full fopen language:
'r', 'w', 'a', 'r+', 'w+', 'a+' (with the 'b' suffix
ignored — same as glibc). Previously only the presence of 'w' was
detected, so opening rb+ was silently treated as read-only and writes
never flushed. Now tracks _can_write separately from the truncate /
append flags, and writeBin/writeStr/flush/close all gate on that
single signal.

v286.6 — LORD: File.lock / unlock / flush / truncate (May 2026)

After file_mutex, LORD's next stop was RecordFile.lock()
this.file.lock(rec*RecordLength, RecordLength). recordfile.js
uses Synchronet's File range-locking on every record open/close.
Single-node ANetBBS has no contention so:

  • File.prototype.lock(start, length) → returns true (always grant)
  • File.prototype.unlock(start, length) → returns true
  • File.prototype.flush() → persists in-memory _content to disk
  • File.prototype.truncate(n) → trims _content and the on-disk
    file (LORD uses this for the "reset save" path)

All added to synchronet_compat.py File prototype.

v286.5 — LORD: file_mutex stub (May 2026)

After v286.4 cleared the RIP-probe stall, the next missing
built-in tripped: ReferenceError: file_mutex is not defined at
LORD's first get_state() call. Synchronet's file_mutex is an
atomic single-writer lock primitive — creates a .lock file
carrying an owner identity, returns false if a peer holds it.

Single-node BBSes never contend, so a stub that always grants
the lock (and writes contents if provided, since LORD uses
the lock file as a write-once data drop for things like war
reports, mail messages, and fairy logs) is enough.

Stub added to synchronet_compat.py, exposed on globalThis.

v286.4 — LORD: skip the 10-second RIP probe (May 2026)

The DOS LORD had a /NORIP command-line flag to disable the RIP
terminal-detection probe. The JS port doesn't — it unconditionally
sends \x1b[!\x1b[6n and blocks read_str(10000, /RIPSCRIP/) for
up to ten seconds waiting for a RIPSCRIP response that no modern
terminal (xterm.js, SyncTERM, NetRunner, mTelnet) sends. That's
the 10+ second stall users felt before the welcome ANSI appeared.

Patched anetbbs/games/sbbs_doors/lord/lord.js to comment out
the probe + the if-block that loads RIP icons. rip stays false
(its var default), the welcome screen and main menu render
immediately. To re-enable: uncomment the block — it's marked with
// ANetBBS patch: in the source.

v286.3 — LORD: input plumbing actually works now (May 2026)

v286.2 cached Queue("name") so same-name calls returned the same
instance — but missed the case where two scripts pass DIFFERENT
suffixes:

  • dorkit.jsnew Queue("dorkit_input" + bbs.node_num)
    "dorkit_input1"
  • ansi_input.jsnew Queue("dorkit_input" + (argv[0] ?? ''))
    "dorkit_input" (argv is empty in our shim)

Different names = different cache entries = still two queues =
bytes still nowhere. Fixed in sbbs_stubs/dorkit/sbbs_input.js by
explicitly re-pointing ai.input_queue = dk.console.input_queue
after loading ansi_input.js, so processed keystrokes definitely
land where dorkit polls.

v286.2 — LORD: input + draw-speed fix (May 2026)

Two issues from a live launch of v286:

  1. Input did nothing. LORD's welcome screen drew but
    keystrokes were eaten. Cause: Synchronet's Queue("name")
    is a named IPC channel — two new Queue("dorkit_input"+N)
    calls in different scripts (dorkit.js + ansi_input.js) bind
    to the same wire. Our shim's Queue was a plain JS class, so
    the two became separate objects. ai.add(byte) wrote
    processed keystrokes into instance B; dorkit polled instance A.
    Forever empty. Fixed by name-caching: same name returns the
    same Queue instance.

  2. Welcome screen drew slowly. ~20 s on the live BBS. Cause:
    dk.console.print writes to BOTH local_io and remote_io.
    In sbbs mode local_io is unused — but our compat forces
    local_console.js to load unconditionally (it's required at
    the bottom of dorkit.js), leaving local_io defined. Every
    print byte went through a 24×80 Screen grid with per-cell
    setCell updates. Patched sbbs_console.js to
    delete dk.console.local_io so writes go straight to
    remote_io → stdout.

Test instructions if you're scripting smoke tests against the
shim: keep the test window >= 12 s for LORD's start path — it
has a hard-coded 10 s RIP-probe timeout before the welcome
display happens.