Sysop CLI
OxideBBS is CLI-first. The server process is controlled with local commands in oxidebbs-server, not a remote admin API.
Examples:
cargo run -p oxidebbs-server -- status
cargo run -p oxidebbs-server -- nodes list
cargo run -p oxidebbs-server -- doors test lord --user sysop --dry-run
cargo run -p oxidebbs-server -- db export --format jsonGlobal options:
-c, --config <PATH>--data <PATH>--json--no-color-v, --verbose(debug; repeat fortrace)
--data accepts either an explicit DecentDB file path or a directory path. When it points at a directory, OxideBBS uses oxidebbs.ddb inside that directory.
JSON outputs are stable objects for --json:
statusnodes listusers listmessages areas listdoors listdb stats
User security levels are documented in User Security Levels, including the current defaults, message-area gates, and the distinction between numeric level 255 and the separate is_sysop flag.
Local Sysop TUI
Launch the local Ratatui sysop console with:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml sysopsysop opens the full TUI by default. It first tries to attach to the live control socket at paths.runtime/oxidebbs-control.sock. If no live socket is reachable, it starts an embedded serve runtime in the same process, waits for that runtime's control socket, and stops the embedded runtime when the TUI exits.
The legacy --tui flag is accepted for compatibility, and --readonly starts the console with destructive actions disabled:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml sysop --readonlyBy default, pressing Q in the TUI opens a confirmation dialog before quitting. This helps prevent accidentally stopping the embedded serve runtime that sysop may have started. The behavior is controlled by:
[sysop]
confirm_quit = trueTo skip the routine confirmation for one launch when no callers are online, use:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml sysop --no-confirm-quitIf any nodes are active, the TUI always shows a stronger confirmation dialog with Nodes are active. Continue to shutdown? before it exits, even when sysop.confirm_quit = false or --no-confirm-quit is set.
Use --connect-only when sysop should never start an embedded server:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml sysop --connect-onlySelect a TUI color theme with --theme:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml sysop --theme telegardAvailable themes:
oxide-classicwildcattelegardvbbsmysticmidnighthigh-contrast
See Sysop TUI Themes for theme descriptions, command examples, and palette swatches.
The TUI uses the configured DecentDB path, paths.logs, paths.screens, and the Unix control socket. Live node actions such as disconnect, node message, broadcast, and reset-stale require either an existing serve process or the embedded runtime started by sysop.
While the TUI is active, OxideBBS keeps console logging off for that process so server log lines do not overwrite the terminal UI. Runtime logs still go to the configured file under paths.logs when [logging].file_enabled is true.
Doctor Screen
The sysop TUI includes a Doctor menu item for verbose local health checks. Open it from the left navigation rail, the command palette (F2, then Go to Doctor), or the Ctrl+O shortcut.
Doctor runs automatically when the screen opens. Press R or F5 to rerun the checks, and use Up/Down or PageUp/PageDown to scroll the report. The summary shows total checks, passed checks, warnings, failures, the run timestamp, and the database path.
Each check is rendered as [PASS], [WARN], or [FAIL] with a detailed result. Warnings and failures include a Fix: line with the next sysop action to take. The current TUI doctor checks:
- configured database path, database file presence, parent directory, and directory write permissions
- open DecentDB connection and current schema version
- required table readability and row counts for users, auth attempts, audit events, message areas, messages, sessions, doors, and door runs
- user repository readability, sysop-account presence, aliases, and security levels
- message-area availability, message visibility values, and loaded message references
- active and recent sessions, node-number validity, configured node range, and duplicate active node assignments
- door definitions, enabled doors, required door fields, door time-limit policy, sampled unfinished door runs, and sampled door-run references
- recent audit events and auth-attempt lockout state
Logging
The configured logging policy is:
[logging]
level = "info"
file_enabled = true
file_name = "oxidebbs-server.log"
format = "text"
[logging.rotation]
strategy = "daily"
max_size_mb = 50
max_files = 14Accepted levels are error, warn, info, debug, and trace. Accepted file formats are text and json.
When file logging is enabled, logs are appended under paths.logs. With the default paths this is:
logs/oxidebbs-server.logSet format = "json" for newline-delimited JSON log files. JSON lines include the formatter fields timestamp, level, and target, plus event fields such as message, node, remote, remote_ip, remote_port, session_id, menu, key, user_id, alias, event_type, door_key, door_name, area_key, message_id, reason, and related outcome fields when they apply.
Rotation strategies are:
daily: rotates when the running process crosses a UTC date boundary, keeping dated archives such asoxidebbs-server.2026-06-03.log.size: rotates before a write would exceedmax_size_mb, keeping numbered archives such asoxidebbs-server.log.1.never: appends indefinitely.
max_files is the number of rotated archives to retain.
serve --log-level <level> overrides [logging].level for that serve run:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml serve --log-level debugThe effective precedence is serve --log-level, then global -v, then [logging].level. One -v uses debug; two or more use trace.
At debug and trace, server logs include caller connections, session opens, menu key selections, audit events, login outcomes, door activity, message activity, and disconnect reasons. DecentDB audit events remain the durable activity record; file logs are the operational troubleshooting stream.
Read logs with:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml logs recent
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml logs tail --follow
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml logs search login_successThe log commands read regular files under paths.logs, including nested door runner logs under logs/doors/.
Setup flow
Start with:
cargo run -p oxidebbs-server -- setupImportant setup behavior:
- Writes default config and creates
runtime/,assets/,doors/, anddata/directories if needed. - Creates starter
data/oxidebbs.ddb. - Creates default sysop and default local message area.
- Requires
--sysop-passwordfor unattended setup.
After setup:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml check
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml config pathsConfig validation
Use:
oxidebbs-server checkfor a full file-level configuration sanity pass.oxidebbs-server config checkfor the same check through the config command group.
The check validates:
- config file existence/parsing
telnet.bind- node count
- configured terminal welcome/logoff assets
- configured screen paths/assets
- configured menu screen paths/assets with menu-specific error context
- door working directory + command + runner availability
- drop-file format (
DOOR.SYSorDORINFO1.DEF) - runtime directory writability
- Unix runtime directory mode (
0700expected for local control socket isolation)
During live serving, missing or unreadable caller assets are logged and printed to the serve console with the screen or asset name, terminal ANSI support, and negotiated width before fallback text is sent.
Missing optional directories are surfaced as warnings; parse failures, missing configured assets, and invalid bind/state values are errors. Binding telnet beyond loopback also surfaces as a warning because telnet sends credentials and caller traffic without encryption.
database.path accepts either an explicit DecentDB file path or a directory path. Directory paths, including paths written with a trailing slash, resolve to oxidebbs.ddb inside that directory.
Starting serve
cargo run -p oxidebbs-server -- serveor:
cargo run -p oxidebbs-server -- --config config/oxidebbs.toml serveserve starts telnet on the configured bind address and opens:
- DecentDB
- terminal assets
- user/session loops
- door execution bridge
- local control socket listener at
runtime/oxidebbs-control.sock(Unix only)
Before binding telnet, serve opens DecentDB, verifies the schema marker, reads the core user, auth, message, session, door, door-run, and audit tables, and writes required config_loaded and server_start audit events. Startup exits with a blocking error if any of those reads or required audit writes fail. serve --dry-run runs the same database startup health check after config validation succeeds.
When runtime/oxidebbs-control.sock already exists and is actively bound by a running process, startup exits with a clear error instead of entering offline mode.
Local control socket behavior (Unix)
The local control plane is:
- Local-only (
runtime/oxidebbs-control.sock) - Newline-delimited JSON request/response per connection
- Single command per connection
- Used by
status,nodes list,nodes show,nodes watch,nodes disconnect,nodes message,nodes broadcast, andnodes reset-stale - Protected on Unix by
0700runtime-directory permissions,0600socket permissions, and peer-UID checks against the server process UID
Run sysop commands as the same OS user as the running server process:
sudo -u oxidebbs oxidebbs-server nodes listRunning control commands as a different local user (including root) typically fails before request handling with a peer-UID mismatch error.
The runtime directory and any pre-existing runtime/node-NNN directories must be writable by the server UID. When changing the service user or restoring runtime files from backup, stop the server and remove stale runtime/node-* directories so OxideBBS can recreate them with mode 0700.
If the socket is unreachable:
status,nodes list, andnodes showfall back to persisted session rows.nodes disconnect,nodes message,nodes broadcast, andnodes reset-stalerecord sysop intent in audit events and return an explicit "live server not reachable" message.
Socket text fields are sanitized for control transport (newlines are normalized to spaces).
Node monitoring and control
cargo run -p oxidebbs-server -- nodes list
cargo run -p oxidebbs-server -- nodes show 1
cargo run -p oxidebbs-server -- nodes watch
cargo run -p oxidebbs-server -- nodes disconnect 1
cargo run -p oxidebbs-server -- nodes message 1 "System will restart in 1 minute."
cargo run -p oxidebbs-server -- nodes broadcast "Welcome to the night shift."
cargo run -p oxidebbs-server -- nodes reset-staleWhen live, node rows include states:
available,connecting,login,main_menu,reading_messages,posting_message,in_door,disconnecting,offline,stale
Each live row may include heartbeat age in seconds.
Live status --json also includes audit_write_failures, the in-memory count of best-effort audit writes that failed while the server was running.
Any future web admin interface must add CSRF and replay protection before it is enabled; the current admin/control surface remains local CLI plus Unix control socket.
Doors and caller launch
Door management:
oxidebbs-server doors listoxidebbs-server doors check(ordoors check <key>)oxidebbs-server doors test <key> --user sysop --dry-runoxidebbs-server doors dropfile <key> --user sysop --node 1 --format DORINFO1.DEF
Meaning:
--dry-rungenerates drop files and validates input without launching a child.- Live interactive DOS door testing requires a caller session. Start
serve, connect over telnet, and launch the door from the callerDoorsmenu. - The bundled test door is
oxide-check(OXIDECHK.EXE) for validating the DOSEMU2 serial runtime bridge. - Enabled configured doors are the only ones selectable by live caller menu.
- Live launch writes drop files in the node runtime directory, tracks
door_started/door_finished/door_timed_outevents, and returns the caller to the menu on completion or timeout.
Recommended smoke-test flow:
cargo run -p oxidebbs-server -- --config config/oxidebbs.example.toml doors check oxide-check
cargo run -p oxidebbs-server -- --config config/oxidebbs.example.toml doors dropfile oxide-check --user sysop --node 1 --format DORINFO1.DEF
cargo run -p oxidebbs-server -- --config config/oxidebbs.example.toml doors test oxide-check --user sysop --dry-runLive test expectation:
- DOSEMU2 receives run-local
OXDOSEMU2.CONFcontaining$_com1 = "pts <runtime>/node-001/OXCOM1.PTY". - DOSEMU2 receives container-safe runtime settings in the generated config.
- The door believes it is reading and writing
COM1; it is not reading from DOSEMU2 console stdio. - OxideBBS receives caller telnet bytes and forwards them to the PTY bridge. DOSEMU2 converts those bridge bytes into COM1 UART input for the door.
- Door output follows the reverse path: COM1 output goes through the DOSEMU2 PTY, OxideBBS reads it from the bridge, and writes to the caller's telnet connection.
- On a clean run,
OXNODE.TXTandOXIDECHK.RPTshould be written to the node runtime directory and include matching node metadata.
Byte path:
caller telnet client
<-> OxideBBS caller transport
<-> OxideBBS PTY byte bridge
<-> DOSEMU2 COM1 pts backend
<-> DOSEMU2-emulated COM1 UART
<-> DOS door programLive execution requires DOSEMU2 and the PTY bridge; it should return a clear missing-runner or bridge-start error when unavailable.
If caller sessions should exit a running door on system restart, use nodes disconnect <n> which closes an active bridge before normal disconnect cleanup.
Optional DOSEMU2 smoke script:
OXIDE_DOOR_INTERACTIVE=1 ./scripts/test-oxide-door-dosemu2.shDatabase operations
cargo run -p oxidebbs-server -- db backup backups/oxidebbs.ddb
cargo run -p oxidebbs-server -- db export --format json > backups/oxidebbs.json
cargo run -p oxidebbs-server -- db import --format json backups/oxidebbs.json
cargo run -p oxidebbs-server -- db compactdb backupcopies the active database file.db export --format jsonis read-only and safe.db import --format json <path>performs a full restore only:- requires a schema-4, schema-only target
- validates schema and all foreign-key references before writing
- preserves UUIDs and load ordering
- executes in one transaction and fails atomically
db compactcurrently returns a hard unsupported error because DecentDB has no safe compaction API contract in this release.[audit].retention_daysdefaults to365. Runtime audit inserts do not purge old rows automatically; the DecentDB repository exposes a retention purge helper for scheduled maintenance until a CLI wrapper is added.
Schema migration notes
- Schema version is currently
4. - Existing schema
2and3databases migrate automatically to4on first open. - Databases with missing, malformed, or future markers are rejected with explicit operator-facing errors.
status/nodesdo not attempt to operate on incompatible databases.
Local-only boundary
All operations here are local to the current machine:
- no remote TCP admin interface
- no remote secret/token auth model
- control socket path must be local filesystem access only
- local control socket uses Unix peer UID checks plus filesystem permissions
- any future web admin interface must include CSRF and replay protection before being enabled