ANetBBS Changelog
Versions are internal build numbers. Public releases are tagged
separately. Current release: v1.0b2.33 (July 2026). Full release: August 1 2026.
v1.0a2.74 — Fix: telnet/SSH "unable to open database file" on Pi / wizard installs (May 2026)
main.py (the anetbbs telnet/SSH entry point) was not pinning DATABASE_URL
before importing anetbbs.config. At import time, if DATABASE_URL was absent
from the environment — wizard installs wrote SQLALCHEMY_DATABASE_URI instead,
a key the config never reads — the fallback resolved DATA_DIR from
config.py's location in the venv site-packages, which doesn't exist, causing
"sqlite3.OperationalError: unable to open database file" on every DB operation
in the terminal session. The web server was unaffected because deploy/serve.py
always pins DATABASE_URL from its own __file__ location before any imports.
Fix: main.py now applies the same self-pinning logic at startup, before any
anetbbs imports. Also corrected wizard.py to write DATABASE_URL (not
SQLALCHEMY_DATABASE_URI) to .env so wizard-installed systems have the right
key going forward.
v1.0a2.73 — ANSI file drop-in, bulletin word-wrap, screen-slot editor link, sysop name fix (May 2026)
ANSI file drop-in system (data/text/): sysops can now drop .ans files
into the install to override screens without touching the admin UI. Resolution
order: file first, then DB, then built-in fallback.
- data/text/welcome.ans — pre-login / logon screen
- data/text/goodbye.ans — logoff screen
- data/text/newuser.ans — new-user registration screen
- data/text/menus/<name>.ans — menu header ANSI (e.g. main.ans, games.ans)
Display codes (@USER@, |BN, etc.) are applied to file-based screens too.
ANSI editor → screen slot link: the ANSI editor now has an "Apply to Screen
Slot" card alongside "Apply to Menu". One click pushes the art directly into
the welcome/goodbye/newuser DB slot — no copy-paste required.
Bulletin word-wrap: bulletin text in the terminal pager now word-wraps to
the user's negotiated terminal width (NAWS) instead of silently truncating every
line at 78 characters.
Sysop name as admin username default: the installer now defaults the admin
login username to the sysop display name entered earlier, so sysops who type
"Firehawke" as their sysop name get "Firehawke" as the default admin account
name instead of "admin". install.sh also now writes SYSOP_NAME to .env
and asks for a sysop display name separately.
Logged-out intro screen uses BBS_NAME: the fallback login menu shown when no
custom welcome ANSI is configured now reads BBS_NAME from config instead of
always printing "Welcome to AnetBBS".
v1.0a2.72 — Fix: login fails with CSRF error on HTTP-only installs (May 2026)
ProductionConfig hardcoded SESSION_COOKIE_SECURE = True. Browsers silently
discard Secure-flagged cookies on plain HTTP connections, so the CSRF token stored
in the session on the GET of /auth/login was never returned on the POST — Flask-WTF
saw no token and returned 400 Bad Request. Affected any install without nginx+TLS
(direct port 5000 access).
Fix: SESSION_COOKIE_SECURE is now an env-var flag (default false). install.sh
sets it to true only when SSL was enabled during setup. Upgrading installs that
lack the key in .env automatically get false via the env-var default.
v1.0a2.71 — Fix: Trade Wars 2 player database silently wiped on player deletion (May 2026)
json-client.js treated db.write(scope, key) with no value as setting the key to
undefined. JSON.stringify omits undefined-valued keys, so the players array was
deleted from tw2.json the first time any player was removed from the game. Two call sites
triggered this: DeletePlayer (which flushes the modified players array) and the kill
handler (which flushes a modified team record). Universe data (sectors, ports,
planets, etc.) was unaffected because those writes always supply an explicit value.
Fix: json-client.js now treats a no-value write(scope, key) as "mark dirty and save",
preserving the in-place mutation already made to the cached reference. Added null guards in
LoadPlayer, RankPlayers, and MatchPlayer for resilience, and a warning log when a db
file exists but fails to parse.
v1.0a2.70 — Fix: DATABASE_URL from EnvironmentFile not overridden correctly (May 2026)
serve.py guarded the DATABASE_URL override with if not os.environ.get('DATABASE_URL').
Systemd's EnvironmentFile loads .env before the process starts, so DATABASE_URL is
always set (even to a stale/relative value from a preserved .env), and the conditional
never triggered. Fix: always set DATABASE_URL from serve.py's own install dir path.
Operators who need Postgres or a non-default path can set ANETBBS_DB_URL instead.
Added diagnostic log lines (INSTALL_DIR, DATABASE_URL, DB file accessibility).
v1.0a2.69 — Fix: web server database path wrong after non-editable pip install (May 2026)
anetbbs-web crashed with sqlite3.OperationalError: unable to open database file.
config.py computes BASE_DIR = Path(__file__).parent.parent at import time. With an
editable install (pip install -e .), __file__ points to the source tree and BASE_DIR
is the install root. When pip falls back to a non-editable install, __file__ is in
site-packages and BASE_DIR is wrong. The admin setup heredoc always worked because it
explicitly sets sys.path and DATABASE_URL. deploy/serve.py did not.
Fix: deploy/serve.py now inserts INSTALL_DIR (from its own __file__) at the front
of sys.path and sets DATABASE_URL to the absolute path if not already set — mirroring
what the install.sh admin setup has always done.
v1.0a2.68 — Python 3.12 fix: patch threading.get_ident for greenlet safety (May 2026)
eventlet.monkey_patch() replaces threading.get_ident with a greenlet-based call to
greenlet.getcurrent(). During Python 3.12's import-time GC/logging cleanup, the hub
greenlet isn't fully initialised yet, so getcurrent() raises
RuntimeError: greenlet is being finalized. Python 3.12 changed the threading/GC startup
interaction, triggering a race that Python 3.10/3.11 didn't have.
Fix (in deploy/serve.py): after eventlet.monkey_patch(), wrap both
threading.get_ident and eventlet.green.thread.get_ident to catch RuntimeError and
fall back to _thread.get_ident() (real C-level thread ID). Safe because the crash only
occurs in finalizer paths, not during request handling. Also added try/except around
create_app() and socketio.run() to surface any other startup errors via the journal.
v1.0a2.67 — Web server: drop gunicorn+eventlet, use eventlet native WSGI (May 2026)
gunicorn --worker-class=eventlet is broken on Python 3.12: fork()+greenlet
causes RuntimeError: greenlet is being finalized at worker boot (exit code 3).
Flask-SocketIO's own docs say not to use gunicorn+eventlet; use eventlet's
built-in WSGI server instead (no fork = no crash).
- Added
deploy/serve.py— callssocketio.run()which uses eventlet.wsgi.server - Updated
deploy/anetbbs-web.service,install.sh,update.shto use
python deploy/serve.pyinstead of gunicorn - Logs now go to journalctl instead of a separate gunicorn-access.log
v1.0a2.66 — Install fix: Python 3.12 gunicorn/eventlet crash (May 2026)
anetbbs-web failed to start on Python 3.12 with gunicorn WORKER_BOOT_ERROR
(exit code 3). The eventlet worker crashed during init:
RuntimeError: greenlet is being finalized — eventlet/green/thread.py get_ident()
Python 3.12 changed threading/GC finalization in a way that breaks greenlet < 3.0.0.
eventlet>=0.33.0 did not constrain greenlet's version, so pip could resolve
greenlet 2.x, which crashes at startup. Fix: explicit greenlet>=3.0.0 +
eventlet>=0.35.2 in setup.py.
v1.0a2.65 — Install fix: PEP 440 version format (May 2026)
Fresh installs failed on Python 3.12+ with newer setuptools:
packaging.version.InvalidVersion: Invalid version: '1.0a2.64'.
The 1.0a2.NN format is not valid PEP 440. This was a long-standing bug hidden by
--quiet 2>/dev/null suppressing pip output until v1.0a2.62 made errors visible.
setup.pynow uses1.0a2.postNN(PEP 440 post-release notation).- Display version in admin panel / filenames stays
v1.0a2.NN(from__init__.py).
v1.0a2.64 — TIC file-echo ZIP binary + duplicate error spam (May 2026)
Fixed two BinkP/TIC processor bugs that caused tqwinfo.zip and similar
hatched file-echo ZIPs to never land in the inbound directory:
-
binkp.py: inbound ZIPs were always treated as mail-bundle ZIPs and
unpacked looking for FTS-0001 packets. If none found, the file was
discarded rather than written to inbound for the TIC scanner. Fix: fall
through to write as TIC binary when a ZIP contains no mail packets. -
tic.pyscan_inbound: duplicate-skip check compared the.ticfilename
againstTicFile.filename(which stores the binary name). They never
matched, so every echomail poll spawned a fresh error row for the same TIC.
Fix: peek at the TIC content to get the binary filename before the DB check.
v1.0a2.63 — File gallery 500 fix (May 2026)
Fixed 500 error on /files/ when any files had been uploaded. The template
used f.user / f.user_id but FileUpload's relationship and FK column are
named uploader / uploader_id. Gallery was completely broken for anyone
who had uploaded files.
v1.0a2.62 — Installer fix: pip failure on Python 3.13 / ARM64 (May 2026)
Fixed a cascade of misleading install errors on Python 3.13 / ARM64 (Pi 5,
Debian trixie). The installer was suppressing all pip install output with
--quiet 2>/dev/null, then blindly continuing into admin setup and service
starts against an empty venv — producing "ModuleNotFoundError: dotenv",
"gunicorn not found", "sqlalchemy not found" etc., all of which were
consequences of pip never running.
- Added
--prefer-binarytopip installto use pre-built wheels on ARM64. - Actual pip error output is now displayed when pip fails, so sysops can
diagnose the root cause. - Admin account setup is now skipped when pip failed (with a clear message).
- Service starts are now skipped when pip failed (with a clear message).
v1.0a2.61 — Newsletter bugfix (May 2026)
Fixed 500 error when sending a newsletter. The admin newsletter route was
passing content=body when constructing PrivateMessage objects, but the
model field is body. No PMs were delivered; the newsletter record was
rolled back cleanly. Corrected to body=body.
v1.0a2.60 — In-browser DOS games: DOOM + Duke Nukem 3D (May 2026)
New game type door_dos_browser — play classic DOS games directly in
the browser via EmulatorJS (dosbox_pure libretro core). No DOSBox
install on the server required; games run fully client-side.
Two shareware titles now ship pre-bundled:
- DOOM (Shareware) — id Software shareware, freely distributable.
SoundBlaster audio via dosbox_pure's SB16 emulation (IRQ 7, DMA 1). - Duke Nukem 3D (Shareware) — 3D Realms shareware, freely
distributable. Uses GUS/UltraSound emulation (--gusflag) because
the v1.3D binary only accepts FXDevice=9 (Build Engine audiolib
devices 3, 5, 13 are compiled out or silent in this release).
New files / changes:
anetbbs/web/games.py—door_dos_browserplay route,
/games/dos-data/<file>ZIP server,/games/dos-frame/<slug>with
COOP/COEP headers for SharedArrayBuffer.anetbbs/templates/games/play_jsdos.html— landing page.anetbbs/templates/games/play_jsdos_frame.html— EmulatorJS frame
with pointer-lock exit overlay and GAME OVER replay loop.anetbbs/features/games.py— terminal door menu now excludes
door_dos_browser(andbuiltin_web) so browser-only games don't
appear in the telnet/SSH game list.tools/prepare_dos_games.py— packages a DOS game directory into a
dosbox_pure ZIP bundle. New flags:--exclude(remove extra EXEs so
dosbox_pure auto-starts),--gus(GUS/UltraSound emulation for
Build Engine games),--dry-run.data/dos-games/doom.zipanddata/dos-games/duke3d.zip— the
pre-built game bundles (included in the release tarball).docs/14-door-games.md— newdoor_dos_browsersection with setup
guide, flags reference, GUS notes, and rebuild commands.
Deploy note for upgrades from v1.0.x:
The data/dos-games/ directory must be writable by the anetbbs
service user. SCP new ZIPs to /tmp/ on the server and move with
sudo:
sudo mv /tmp/*.zip /opt/anetbbs/data/dos-games/
sudo chown anetbbs:anetbbs /opt/anetbbs/data/dos-games/*.zip
Add the two Game rows manually if they're missing (see
docs/14-door-games.md → Pre-installed games).
Versions are internal build numbers. Public releases are tagged
separately. Previous internal series: v1.0a2.36 — alpha 2 (internal v287.35,
May 2026). v1.0a — alpha 1 (internal v196).