[{"data":1,"prerenderedAt":9},["ShallowReactive",2],{"document-en-manual":3},{"slug":4,"label":5,"description":6,"text":7,"plain":8},"manual","User manual","Installation, serial binding, protocols, licensing, and troubleshooting.","# BetterOne Console Server User Manual\n\n**Updated: October 13, 2026**\n\nFor customers deploying and using this product. [Chinese edition](..\u002Fcn\u002FUSER_MANUAL.md). Development, packaging, and licensing protocol details are maintained in the [internal documentation](..\u002F..\u002FREADME.md#内部开发维护).\n\nOpen System → Documentation & legal information for this manual and legal documents. The login page also links to the user agreement and privacy policy. Reading pages open in a new tab without requiring login or Internet access and offer Chinese \u002F English switching.\n\n## Installation and first login\n\nDeploy the release for your Windows, Linux, or Raspberry Pi host, start the executable, and verify serial drivers and access permissions. On Linux, grant the service account serial access. On Windows, check that the COM port appears in Device Manager.\n\nRelease targets are Windows x64 (amd64) and Linux amd64, arm64, and armv7 (32-bit ARM, GOARM=7). Choose a Raspberry Pi release by OS architecture, not board model alone. There is no Windows x86 installer. Only IPv4 is currently supported.\n\nWeb\u002FAPI defaults to `0.0.0.0:80`; browse to `http:\u002F\u002F\u003Cserver-IPv4>\u002F`. The actual address follows `http_host`, `http_port`, and startup logs. An occupied port or insufficient permission can prevent startup.\n\nA new database creates only an uninitialized `admin` account, with **no default password** and no `engineer` account. Open the Web page and set and confirm a password of at least eight characters that is neither letters-only nor digits-only. Initialization succeeds once; existing accounts and passwords are preserved. Before initialization, empty-password access, Web\u002FSSH login, and administrator operations are unavailable. Administrators create other accounts.\n\nAdministrators manage accounts, API keys, licensing, and complete configuration; engineers can perform daily serial\u002Fprotocol configuration.\n\nFor automated initialization, query `GET \u002Fapi\u002Fauth\u002Finitialization` for `required`, then submit `password` and `confirm_password` to `POST \u002Fapi\u002Fauth\u002Finitialize`. Matching compliant passwords return 204; an already initialized database returns 409 without resetting a password. Log in normally afterward. `--init-config` generates configuration and keys only; it does not set the administrator password.\n\n### Installation options\n\n- **Windows service:** run `betterone-cs_\u003Cversion>_Windows_x86_64_setup.exe` as administrator. WinSW registers and starts `BetterOneConsoleServer`. The executable is in Program Files; configuration, data, and logs are in ProgramData.\n- **Linux deb\u002Frpm:** install the package matching your architecture. It uses the `betterone-console-server` account and enables\u002Frestarts systemd. A normal user unable to bind port 80 can use port 8080.\n- **Portable ZIP\u002Ftar.gz:** extract, run `betterone-cs --init-config`, inspect settings, then run `betterone-cs`. Windows uses `.exe`; Linux commands in the current directory need `.\u002F`. Default mode is `portable`, with `config\u002F` and `data\u002F` beside the executable.\n\nInstaller upgrades restart the service and disconnect serial sessions. Installers do not move old data from other locations automatically. Stop and back up first. Uninstalling preserves configuration, database, license, keys, installation identity, and logs; it is not a factory reset.\n\n### Service management and logs\n\nWindows administrator PowerShell:\n\n```powershell\nGet-Service BetterOneConsoleServer\nRestart-Service BetterOneConsoleServer\nStop-Service BetterOneConsoleServer\nGet-Content \"$env:ProgramData\\BetterOne\\ConsoleServer\\logs\\betterone-console-service.err.log\" -Tail 50\n```\n\nLinux:\n\n```sh\nsudo systemctl status betterone-console-server\nsudo systemctl restart betterone-console-server\nsudo systemctl stop betterone-console-server\njournalctl -u betterone-console-server.service -n 50\n```\n\nStartup `configuration file:` and `data directory:` messages show actual paths. Check them and restart status when an edited setting appears ineffective.\n\n### What initialization does\n\nFour operations occur at different times and preserve existing content:\n\n| When | Action |\n| --- | --- |\n| Configuration initialization: `--init-config` or the start of normal startup | Create directories and test data-directory writability with a temporary file. Generate `settings.toml` (0600), defaults for Web address, SSH key, `license.txt`, license server, and `runtime.db`; generate `secret_key`, `api_key_pepper`, and `device_uuid`, also saving UUID to a machine-level file. `--init-config` exits here, without database creation or listeners. deb\u002Frpm installation uses this command. |\n| First normal startup | Open\u002Fcreate SQLite `runtime.db` in WAL mode, create tables and defaults for 48 logical consoles, and create passwordless pending `admin`. No engineer\u002Fdefault password is created. Incomplete default console configuration may be rewritten; existing complete data is preserved. |\n| First SSH listener startup | Generate missing Ed25519 `ssh_host_key` with permissions 0600; do not replace an existing key. |\n| First Web password setup | If the database has only passwordless `admin`, show setup, require two matching compliant passwords, and allow success only once. Later calls return 409. Return to login without automatically logging in. |\n\nDefault console configuration:\n\n| Setting | Default |\n| --- | --- |\n| Serial | 115200, 8 data bits, 1 stop bit, no parity, no flow control, no binding |\n| Connections | 1 |\n| Protocols | SSH and WebSocket enabled; TCP and Telnet disabled |\n| TCP | `127.0.0.1:5001–5048` |\n| SSH | `0.0.0.0:6001–6048` |\n| Telnet | `127.0.0.1:7001–7048` |\n| WebSocket | Shared Web port, path `\u002Fconsole\u002F\u003Cnumber>` |\n\nUse `--factory-reset` to remove the database and repeat database\u002Fpassword initialization. Initialization does not contact the licensing server automatically, create API keys or engineers, or bind hardware. Without a license, capacity is two ports.\n\n**Set the password promptly:** before setup, anyone able to reach the page can set it first. deb\u002Frpm automatically starts listening on port 80. On an untrusted network, first restrict `http_host` to `127.0.0.1` or use `--addr`, then open access after setup.\n\n### Factory reset\n\nFactory reset is available only in a local interactive terminal, not the Web interface. “Clear all serial configurations” resets serial configuration only.\n\n```sh\nbetterone-cs --factory-reset\n```\n\n**When the device goes to someone else**, add `--new-ssh-host-key` to replace the SSH host key as well:\n\n```sh\nbetterone-cs --factory-reset --new-ssh-host-key\n```\n\nThe old key is deleted and the service generates a new one when it starts, so a copy of the old key the previous user may still have (in a backup, for example) can no longer be used to impersonate the device. SSH clients that connected before warn that the host key changed until the old entry is removed from `known_hosts`. Leave it out when you are only starting over yourself.\n\n**Stop the service and back up first.** The command temporarily stages the database for rollback during the operation, then deletes that stage on success; it is not a lasting backup.\n\n1. Check whether a program responds at the configured Web address; refuse to proceed while the service is running.\n2. Display what will be deleted\u002Fretained and a backup warning.\n3. Require the exact uppercase word `RESET`. Any other input aborts without changes.\n4. Preflight permissions and file types, stage the database in the same directory, write the reset marker and rotate runtime secrets, then remove the staged files. On failure it attempts to restore the old database. An interrupted stage blocks the next startup rather than silently creating an empty database.\n\nIt deletes `runtime.db`, `runtime.db-wal`, and `runtime.db-shm`: users\u002Fpasswords, API keys, audits, and all 48 consoles' notes, bindings, serial parameters, connection limits, protocol switches, and listener addresses.\n\nOnce the database part has succeeded, it also deletes, where present:\n\n| File | Content | Afterwards |\n| --- | --- | --- |\n| `web-settings.toml` in the data directory | Web address, port, HTTPS switch and certificate settings changed on the web page | The values in `settings.toml` apply again; the command shows the resulting address and port |\n| `web-tls.pem` (data directory, and a copy older versions left in the configuration directory) | Uploaded HTTPS certificate **and private key** | Upload again when HTTPS is needed |\n| `web-tls-self-signed.pem` | Self-signed HTTPS certificate and key | A new one is generated when self-signed HTTPS is enabled again |\n| Windows: `ProgramData\\BetterOne\\ConsoleServer\\logs\\betterone-console-service*.log` | Service logs with user names, source addresses and actions | Written anew once the service starts |\n\nFiles that cannot be deleted (for example an old copy under root-owned `\u002Fetc`) are listed for manual removal; the database part is complete regardless.\n\nOn Linux the service logs to the system journal, which cannot be cleared for one service only. To clear it as well, run `sudo journalctl --rotate && sudo journalctl --vacuum-time=1s`; this removes the history of **every** service.\n\n| Retained | Explanation |\n| --- | --- |\n| `settings.toml` | Keep the addresses and other base settings written in it (changes made on the web page are cleared, see above). Factory reset rotates `secret_key` and `api_key_pepper` in `DataDir\u002Fruntime-identity.toml`, invalidating old sessions and keys. |\n| `license.txt` | Bound to this device; deletion would require activation again. |\n| SSH host key | Avoid host-key-change warnings; replaced only with `--new-ssh-host-key` (see above). |\n| `device_uuid` in configuration or writable runtime identity, plus machine file when available | Preserve device identity, especially without hardware identity. |\n\nRestart the service. It creates a database owned by its own account and records `factory-reset` with the OS user (under sudo, the person who ran sudo, e.g. `alice (sudo, as betterone-console-server)`) and local origin. Then promptly set a new administrator password.\n\nThe command requires an interactive terminal and refuses scripts, pipes, or service execution. It operates only on an existing database; use the service's `-user` \u002F `-system` \u002F `-portable`, `--config-dir`, and `--data-dir`. On Linux the command runs as the data-directory owner (the service account): for deb\u002Frpm installations, stop the service and run `sudo betterone-cs -system --factory-reset`; started as root, it switches to that account before it reads or writes anything, so every file it leaves belongs to the service. Without sudo, run `runuser -u betterone-console-server -- betterone-cs -system --factory-reset` as root. Other ordinary users are refused. If a `.runtime.db.factory-reset-stage-*` directory remains after an interruption, inspect the old database and `previous-runtime-identity.toml` inside it and restore both as needed before removing the stage; after copying or moving files as root, give them back with `sudo chown -R betterone-console-server: \u002Fvar\u002Flib\u002Fbetterone-console-server`, or the service cannot open them. Other directories are untouched.\n\n### Forgotten administrator password\n\nIf another administrator is available, log in with that account and reset the affected user in Users. Old sessions expire and the Web interface requires another password change at next login.\n\nOtherwise run locally with the same paths\u002Fmode as the service:\n\n```sh\nbetterone-cs --reset-admin-password\n```\n\nThe command shows the database path and prompts twice without echoing the password. Require eight or more characters, neither letters-only nor digits-only. It resets built-in `admin`, reenables it if disabled, invalidates its sessions, and sets the password-change flag. It exits without starting the service; the service may remain running.\n\nEach reset records `reset-admin-password`, the OS user (under sudo, the person who ran sudo), and local origin in audits and logs, never the password. Pipes, scripts, and noninteractive sessions are refused; no password command-line or file option is provided. Local access is required.\n\n| Installation | Command |\n| --- | --- |\n| Linux deb\u002Frpm | `sudo betterone-cs -system --reset-admin-password` (it switches to the service account by itself; without sudo, `runuser -u betterone-console-server -- betterone-cs -system --reset-admin-password` as root) |\n| Portable | `.\u002Fbetterone-cs -portable --reset-admin-password` in the program directory |\n| User mode | `betterone-cs -user --reset-admin-password` as the usual user |\n\nWrong paths produce “database not found” instead of creating another database. On Linux, the command must run as the data-directory owner (normally the service account); it checks before generating any runtime file. Windows service installations need an administrator terminal and the service's actual system-mode paths; the service runs as LocalService, with directory access for LocalService, SYSTEM, and Administrators.\n\nA channel without an interactive terminal cannot use this command. Deleting the database loses all console configuration, users, keys, and audits and is not a password-recovery method.\n\n### Directories and startup options\n\nThe configuration file is `settings.toml`. The [configuration template](..\u002F..\u002F..\u002Fsettings.example.toml) has Chinese comments describing settings\u002Fdefaults; it is not loaded automatically. Generate local configuration with `--init-config`, then copy only settings you need to change. **Do not overwrite existing keys or UUID.** Omitted fields use program defaults.\n\nThe template ships in archive roots, Windows installation directories, and `\u002Fusr\u002Fshare\u002Fdoc\u002Fbetterone-console-server\u002Fsettings.example.toml` on Linux. It does not replace runtime configuration.\n\n| Mode | Linux configuration \u002F data | Windows configuration \u002F data |\n| --- | --- | --- |\n| `-user` | `$XDG_CONFIG_HOME\u002Fbetterone-console-server` \u002F `$XDG_DATA_HOME\u002Fbetterone-console-server`, default `~\u002F.config\u002Fbetterone-console-server` \u002F `~\u002F.local\u002Fshare\u002Fbetterone-console-server` | LocalAppData Known Folder: `BetterOne\\ConsoleServer\\config` \u002F `BetterOne\\ConsoleServer\\data` |\n| `-system` | `\u002Fetc\u002Fbetterone-console-server` \u002F `\u002Fvar\u002Flib\u002Fbetterone-console-server` | ProgramData Known Folder: `BetterOne\\ConsoleServer\\config` \u002F `BetterOne\\ConsoleServer\\data` |\n| Default portable, `-portable` | `config\u002F` \u002F `data\u002F` beside executable | `config\\` \u002F `data\\` beside executable |\n\nChoose only one of `-user`, `-system`, or `-portable`; double-dash forms also work. Without a mode flag, `NBC_MODE` applies; otherwise portable is the default.\n\nLinux ignores relative XDG values; Windows resolves Known Folders through the OS API. Nonempty `--config`, `--config-dir`, `--data-dir`, `-user` \u002F `-system` \u002F `-portable` override `NBC_CONFIG_FILE`, `NBC_CONFIG_DIR`, `NBC_DATA_DIR`, `NBC_MODE`, then mode defaults. An explicit configuration file is independent of ConfigDir; otherwise use `\u003CConfigDir>\u002Fsettings.toml`. Relative CLI paths resolve once against startup cwd.\n\nRelative database, SSH key, and license paths resolve against DataDir, with defaults `runtime.db`, `ssh_host_key`, `license.txt`; absolute paths stay unchanged. Restart after startup-setting changes.\n\nFor a new deployment run `betterone-cs -user --init-config`, inspect configuration, then start normally. This creates initial configuration\u002Fmissing keys only, not database\u002Fusers\u002FSSH key\u002Flisteners. An existing base configuration is not rewritten; missing generated secrets or UUID are kept in `DataDir\u002Fruntime-identity.toml`. The base may be read-only, but DataDir must be writable for SQLite\u002FWAL and runtime state.\n\nOn Linux, do not run the server itself as root on the packaged data (`sudo betterone-cs -system`): the files it writes would belong to root and lock the service out, so the program refuses; use `systemctl`, or run it as the service account. Linux normal users usually cannot bind port 80: use 8080 or configured capability. Packaged systemd uses the dedicated account with `CAP_NET_BIND_SERVICE`; configuration directory `root:betterone-console-server`\u002F0750, file 0640, and data directory owned by the service\u002F0700. Windows WinSW uses LocalService with restricted ProgramData ACLs. Customers need no Go, Inno Setup, or WinSW development tools.\n\n### Configuration file\n\nOnly overridden fields need be written. Explicit values, including allowed empty strings, `0`, and `false`, take precedence. Omitting `license_server` uses its default; `license_server = \"\"` disables online licensing; an explicitly saved address remains in use. Restart after changes.\n\n| Setting | Default | Purpose |\n| --- | --- | --- |\n| `http_host`, `http_port` | `0.0.0.0`, `80` | Shared Web, REST, and WebSocket address |\n| `websocket_allowed_origins` | Empty | Additional allowed `host[:port]` origins |\n| `https_enabled`, `https_host`, `https_port` | `false`, `0.0.0.0`, `443` | Independent HTTPS switch and listener; HTTP redirects to HTTPS when enabled. Ports must differ. |\n| `tls_cert_file`, `tls_key_file` | Empty | HTTPS certificate\u002Fkey; omit to use a self-signed certificate. Relative to ConfigDir. |\n| `max_connections_per_ip` | `128` | Combined SSH\u002FTelnet\u002FRaw TCP connections per source across consoles; excess closes immediately, `0` means unlimited. NAT\u002Fproxy users share the source allowance. |\n| `secure_cookies` | `false` | Secure cookies behind TLS proxy; automatic with built-in HTTPS |\n| `ssh_host_key` | `ssh_host_key` | Relative to DataDir |\n| `license_file` | `license.txt` | Shared activation\u002Frenewal license, relative to DataDir |\n| `license_server` | `https:\u002F\u002Flicense.betterone.top:8443` | Online licensing; explicit empty disables |\n| `license_portal_url` | `https:\u002F\u002Flicense.betterone.top:8443\u002F#devices` | Browser device-management link, no admin API calls |\n| `license_offline_url` | `https:\u002F\u002Flicense.betterone.top:8443\u002F#offline` | Browser “Offline activation \u002F renewal” page; detects activation requests (`.request.txt`), renewal requests (`.renewal.txt`), and license files (`.license.txt`) by content |\n| `database_dsn` | `runtime.db` | SQLite settings, users, keys, and audits; relative to DataDir |\n| `secret_key` | Generated | Session encryption\u002Fauthentication key |\n| `api_key_pepper` | Generated | API key verification key |\n| `device_uuid` | Generated | UUID v4 also saved in machine-level file |\n\nThe three browser URL settings are independent of `license_server`. Current implementation uses the listed defaults when omitted; explicit empty values hide links. Restart after editing them.\n\n#### Session key: `secret_key`\n\nSessions use AES-256-GCM to encrypt\u002Fauthenticate user ID, session version, and expiry in cookies. Web and WebSocket login depend on this 64-character hexadecimal (32-byte) key. Replacing\u002Flosing it invalidates all sessions, not users\u002Fconfiguration. Disclosure allows forged cookies, including administrator impersonation.\n\n#### API key verification: `api_key_pepper`\n\nThe database stores HMAC-SHA256 verification data rather than plaintext API keys; this 64-character hexadecimal key is the HMAC key. Plaintext API keys are returned only once. Losing\u002Freplacing the pepper invalidates all existing keys irrecoverably; recreate keys and update clients. Pepper disclosure alone is not a login credential but weakens protection if the database is also disclosed.\n\n#### Storage and backups\n\nRestrict configuration access to the service user (program-created files use 0600); do not publish it in repositories, tickets, or chats. Diagnostics report only whether these keys are set. Back up keys with the corresponding database. Keep them during migration. Suspected disclosure: stop and remove the affected value from both `settings.toml` and `DataDir\u002Fruntime-identity.toml` if present; deleting only the base value may leave the runtime value in effect. Restart to generate a replacement, accepting session\u002Fkey invalidation. `--init-config` fills missing values without changing existing ones.\n\n### Upgrading an old deployment\n\nThe new default names are a breaking change: there is no automatic migration or fallback. If `settings.toml` or `runtime.db` is absent, the program ignores any old `nbc_settings.toml` or `nbc.db` in the same directory and creates fresh configuration or database files; it does not delete the old files. To retain old deployment data, stop the service and back up the configuration, database\u002FWAL\u002FSHM, SSH private key, `license.txt`, and old executable before migrating manually. Rename the old `nbc_settings.toml` to `settings.toml` in ConfigDir and the old `nbc.db` to `runtime.db` in DataDir. Make sure SQLite has shut down cleanly; if WAL\u002FSHM files remain, use a consistent SQLite backup instead of moving only the main database. Check custom `--config` and `database_dsn` paths separately.\n\nKeep `secret_key`, `api_key_pepper`, and the SSH host key. If the old TOML explicitly sets `database_dsn = \"nbc.db\"`, change it to `\"runtime.db\"`; explicit paths are otherwise still honored. Check other relative paths, including `data\u002Fnbc.db` and `data\u002Fssh_host_key`, and verify the license path. Keep backups until the startup paths and original data have been checked.\n\nLogs go to stdout\u002Fstderr: journald for systemd, terminal for portable, and WinSW captured\u002Frotated files for Windows service. `\u002Frun` is a systemd runtime directory; no PID file is currently written.\n\n## Serial binding and communication settings\n\nBind a console by device name, USB physical slot, or USB VID\u002FPID\u002Fserial identity. These are mutually exclusive. Duplicate name, slot, or USB device bindings are rejected; different methods targeting one physical device can still conflict when opening it.\n\nDefaults are `115200 8N1` without flow control. Allowed baud-rate input is 300–4,000,000; actual support depends on chipset, driver, and target. Match the device and test before production. Open handles may retain old settings after a save; new settings apply at next open. Check `active_serial_config` for active settings.\n\nEach console's shared client limit across protocols is **1–256**, default **1**; `0` and `-1` unlimited modes are no longer accepted. Invalid new values are rejected; no automatic migration of old unlimited configurations is provided. Clients share one stream and must coordinate commands.\n\n### Three binding methods\n\nSetting one method clears the others. A request providing multiple nonempty methods is rejected.\n\n| | Device name | USB slot | USB identity |\n| --- | --- | --- | --- |\n| Fields | `device` | `slot` | `vid`, `pid`, `serial_number` |\n| Example | `COM11`, `\u002Fdev\u002FttyUSB0` | `00.1400@2.3` | `0403`, `6015`, `D30J42Y6A` |\n| Identifies | Terminal name | Current serial port at a physical position | Particular adapter regardless of position |\n| Name changes | Binding may fail | Unaffected | Unaffected |\n| Replace adapter in same socket | Depends on name | No change needed | Rebind |\n| Move socket\u002Fcable | Depends on name | Rebind | Unaffected |\n| Applicable equipment | Any serial device | USB serial | USB serial with unique serial number |\n\n#### Device name (`device`)\n\nUse for motherboard ports, virtual com0com\u002FPTY ports, multiport devices where other methods are ambiguous, or stable single-adapter deployments. Names must remain stable and not be held by another program. Windows COM numbers can be assigned in Device Manager. Linux `\u002Fdev\u002FttyUSBn` enumeration may change: use udev or `\u002Fdev\u002Fserial\u002Fby-path\u002F` and `\u002Fdev\u002Fserial\u002Fby-id\u002F` names when needed. Multiple adapters with unfixed names risk binding the wrong equipment.\n\n#### USB physical slot (`slot`)\n\nUse for fixed rack cabling or adapters without unique serial numbers. A replacement at the same slot needs no change. Windows and Linux share slot syntax. Host hardware, motherboard, expansion cards, hub model\u002Forder, and sockets must remain stable. Firmware changes can also alter slots; recheck afterward. Multiport devices need an interface suffix for ports other than the first. Linux must expose readable sysfs; containers\u002FWSL may have no slots. This method does not apply to non-USB ports.\n\n#### USB identity (`vid`, `pid`, `serial_number`)\n\nUse to follow a particular adapter across sockets\u002Fhubs. All three fields are required, the serial number must be unique, and one identity must resolve to one port. Adapters without serial numbers cannot use it; duplicated serials or shared multiport identities are ambiguous. Replacement adapters require rebinding.\n\nUse `usb_vid`, `usb_pid`, and `usb_serial` exactly as enumerated. Windows FTDI values may include a channel letter (`D30J42Y6A`) while Linux uses `D30J42Y6`; FTDI identity configurations are not portable between these platforms. If `go.bug.st\u002Fserial` metadata enumeration fails, identity fields may be empty, temporarily blocking identity resolution while name\u002Fslot bindings remain usable.\n\n#### Choosing a method\n\n1. Non-USB or ambiguous multiport equipment: device name.\n2. Fixed cabling with potentially replaced adapters: slot, subject to topology stability.\n3. Stable adapters with changing positions and unique serials: identity.\n4. Inspect `GET \u002Fapi\u002Fsystem\u002Fserial-ports` for names, slots, and USB identifiers before choosing.\n\nMissing\u002Fambiguous bindings enter serial fault\u002Fretry handling and eventually close sessions on timeout. Status distinguishes unavailable device\u002FUSB position and multiple matches.\n\n### Slot syntax\n\n```text\n00.1C00.0000@1.4:2\n```\n\nBefore `@` is the PCI root bus (two hexadecimal digits) followed by PCI device\u002Ffunction segments (four hexadecimal digits). After `@` is the decimal USB port chain, followed by optional USB interface number `:2`. Omitted interface selects the first\u002Fminimum interface. Hex is case-insensitive and displayed uppercase; USB port numbers are 1–255.\n\nExamples: a hub on motherboard port 2, adapter on hub port 3: `00.1400@2.3`; third FT4232H interface: `00.1400@2.3:2`. Motherboard\u002FPCI serial ports may have no `@`, for example `00.1C00.0000`, using a suffix for multiple ports ordered by address. Device-tree platforms use `dt.\u003Cplatform-device>@...`, such as `dt.3f980000.usb@1.2`.\n\nA slot is a system-reported location, not a serial number or terminal name. Resolution finds the current COM\u002Ftty name there. Physical case labels do not necessarily correspond one-to-one: USB 3 sockets expose USB 2\u002F3 logical positions, internal devices consume numbers, and Raspberry Pi 3B\u002F3B+ sockets sit behind an onboard hub.\n\n### Slot stability\n\nSlots avoid changing USB bus numbers and Windows root-hub indices. PCI root bus numbering comes from firmware. With unchanged hardware it is normally stable, but BIOS changes, PCI\u002FUSB settings, speed fallback, or different hub internals can alter it and cause missing or wrong bindings.\n\nRecheck every slot after changes. Linux relies on sysfs; WSL\u002Fcontainers\u002Fpermissions may leave slots empty. usbipd-forwarded WSL ports do not reflect physical topology. Windows uses LocationPaths and hub queries independently of COM numbers. Virtual non-USB\u002Fnon-PCI ports have no slots. Use genuine cables\u002Fchipsets.\n\n### Multiport devices\n\nFT2232H\u002FFT4232H, CP2105, CH348, and similar devices share a location and need interface suffixes, such as `00.1400@2.3:0` and `:1`. Without a suffix, the lowest interface is selected. Firmware interface numbers normally remain stable across reconnects\u002Frestarts. Multiple ports under one interface (such as some MCS7840\u002FCH348 devices) require physical validation of ordering; stable device-name\u002Fudev bindings are an alternative. Cards containing an internal hub and separate single-port chips have distinct extended port chains.\n\n### Choosing cables and chipsets\n\nBuy genuine cables, adapters, and powered hubs from reputable suppliers, including genuine FTDI, Silicon Labs, Prolific, or WCH products. Use externally powered hubs for multiple adapters.\n\nCounterfeit\u002Funknown products may lack unique serial numbers, be rejected or rewritten by drivers, lose\u002Fcorrupt data, disconnect, use inaccurate baud rates, lack power\u002Fisolation\u002Fprotection, or mix incompatible 3.3 V, 5 V, and RS-232 levels. Users bear resulting identification, wrong-binding, data-loss, interruption, and equipment-damage risks. The Software cannot guarantee operation on them. Inspect USB identifiers in Physical Ports when receiving new adapters.\n\n### Expanding through hubs and replacing them\n\nEach extra hub adds a `.port` segment:\n\n| Connection | Slot | Windows LocationPaths ending |\n| --- | --- | --- |\n| Direct adapter | `00.1400@2` | `...#USB(2)` |\n| Hub port 3 | `00.1400@2.3` | `...#USB(2)#USB(3)` |\n| Cascaded hub | `00.1400@2.3.1` | `...#USB(2)#USB(3)#USB(1)` |\n\nHub hardware numbering is normally stable across reconnects and power cycles. Keep the same host socket, hub model, and cascade order. Do not infer slots from printed case numbers: a seven-port hub may contain two cascaded four-port chips. A case port 5 might be `00.1400@2.4.2`, not `@2.5`. Read actual slots. USB 3 hubs contain separate USB 2\u002F3 hubs; serial adapters usually use the USB 2 side, but changing host speed\u002Fcontroller may change paths. Prefer powered hubs, especially for more than four adapters; inadequate power causes repeated enumeration even if slots stay unchanged.\n\nExample: seven-port hub A at `00.1400@2` exposes ports 1–3 at `@2.1`, `@2.2`, `@2.3` and ports 4–7 at `@2.4.1` through `@2.4.4`. A different hub B may expose ports 1–4 at `@2.1.1` through `@2.1.4`, then ports 5–7 at `@2.2`, `@2.3`, `@2.4`.\n\nReplacing A with B and copying physical placements can leave console 1's `@2.1` missing while console 2's `@2.2` silently connects to the equipment now on case port 5. **Wrong equipment without an error is the most dangerous outcome.**\n\n1. Prefer identical replacement hubs at the same host socket.\n2. If the model changes, keep affected consoles inactive, insert adapters individually, read every new slot, and update bindings.\n3. Test each console against its intended equipment.\n4. Unique-serial adapters can use identity binding to avoid hub\u002Fsocket dependence.\n5. Label each hub socket with its slot and console number.\n\n### Serial binding API\n\n`PATCH \u002Fapi\u002Fconsoles\u002F{id}\u002Fserial` requires admin or engineer. It is a partial update: at least one field, omitted fields unchanged; `null` and unknown fields rejected.\n\n```json\n{\"device\":\"COM11\"}\n```\n\n```json\n{\"slot\":\"00.1400@2.3\"}\n```\n\n```json\n{\"vid\":\"0403\",\"pid\":\"6015\",\"serial_number\":\"D30J42Y6A\"}\n```\n\nVID\u002FPID are four hexadecimal digits, saved uppercase. Serial number cannot be blank. All identity fields must be sent together, even when changing one. A nonempty method clears others; multiple nonempty methods are rejected. Empty strings clear a method, including all three identity fields:\n\n```json\n{\"vid\":\"\",\"pid\":\"\",\"serial_number\":\"\"}\n```\n\nTo change parameters while retaining binding, omit binding fields:\n\n```json\n{\"baudrate\":115200}\n```\n\n| HTTP | Meaning |\n| --- | --- |\n| 401 | Not authenticated |\n| 403 | Insufficient role |\n| 404 | Console missing |\n| 409 | Device\u002Fslot already bound; response identifies the console |\n| 422 | Invalid\u002Fincomplete identity, multiple binding methods, malformed VID\u002FPID, or parameter outside range; `detail` explains |\n\nSerial discovery returns `port_name`, `slot`, `usb_vid`, `usb_pid`, `usb_serial`. Console `serial` objects return saved binding fields; unused methods are empty strings. Status exposes `configured_device`, `configured_slot`, `configured_vid`, `configured_pid`, `configured_serial_number`, and actual `resolved_device`. Existing handles retain their current settings; the next connect\u002Freconnect uses the new binding.\n\n### Identity binding details\n\nIdentity follows the adapter; slot follows the socket. Duplicate identities cannot bind multiple consoles, with case-insensitive serial comparison. The database adds `serial_vid`, `serial_pid`, and `serial_number` columns automatically to old databases without changing existing settings.\n\nIdentity cannot cover every device: Windows FTDI channel suffixes differ from Linux; failed metadata enumeration can leave all identity fields empty for that scan; missing\u002Fduplicate serials and multiport identities are unusable or ambiguous. Windows temporary instance IDs containing `&` are not USB serial numbers. Multiple matches are refused and candidates logged rather than selected arbitrarily; an ambiguity message may use the existing text `configured USB slot matches more than one port`.\n\n## Temporarily disabling a console\n\nAdmin\u002Fengineer can pause a licensed console:\n\n```http\nPATCH \u002Fapi\u002Fconsoles\u002F1\u002Fenabled\nContent-Type: application\u002Fjson\n\n{\"enabled\": false}\n```\n\nSet `true` to resume. The boolean is required; missing, null, or unknown fields produce 422 validation errors.\n\nPause disconnects bridged sessions and rejects new TCP\u002FTelnet\u002FSSH\u002FWebSocket forwarding; the owner closes the serial port after in-flight work ends. Settings and network listener ports remain. Pause is in memory only, cleared by service restart, not by configuration reload or license changes. Lists\u002Fdetails show `enabled=false`, `disabled_reason=\"manually_disabled\"`; status shows disabled\u002Fmanually_disabled. Configuration remains editable. Repeated requests are idempotent and audited. Unlicensed consoles return 409; this cannot bypass licensing.\n\n### Components remaining active\n\n| Component | Paused behavior |\n| --- | --- |\n| Physical serial port | Cancel\u002Fclose after pending work; no reopen |\n| Bridged clients | Disconnected |\n| TCP\u002FTelnet listeners | Retain bound ports; new bridge registration refused; Telnet may negotiate first |\n| SSH listener | Retain port; handshake\u002Fauthentication\u002Fsession creation may complete, but serial bridging is refused |\n| Shared WebSocket service | Continues for other consoles; rejects this console |\n| Console owner | Remains waiting without new opens |\n| Management APIs | Retain\u002Fquery\u002Fedit configuration |\n\nUnbridged SSH handshakes and sessions are not necessarily closed by pause; normal connection and timeout limits apply. Resuming does not open hardware until a client enters bridging. Disable individual TCP\u002FTelnet\u002FSSH protocols to release their ports. Disabling one console's WebSocket does not stop the shared service. Schema is available at `\u002Fapi\u002Fopenapi.json` and `\u002Fapi\u002Fdocs`.\n\n## Protocols and network protection\n\n| Protocol | Default | Authentication\u002Fencryption |\n| --- | --- | --- |\n| SSH | Enabled, `6000 + console number` | Enabled local user credentials; encrypted |\n| WebSocket | Enabled, shared Web port and `\u002Fconsole\u002F\u003Cnumber>` | Supported credentials; default `ws:\u002F\u002F` is plaintext |\n| Raw TCP | Disabled, `127.0.0.1`, `5000 + number` | No application authentication; plaintext |\n| Telnet | Disabled, `127.0.0.1`, `7000 + number` | No application authentication; plaintext |\n\nSSH\u002FWebSocket default to `0.0.0.0` (all local IPv4 addresses); clients use a real host address. Remote TCP\u002FTelnet requires an explicit listening address and firewall protection. Defaults apply to new\u002Freset configuration, not existing saved values. SSH example: `ssh -p 6001 admin@\u003Cserver-IPv4>`.\n\nWebSocket supports `X-API-Key`, HTTP Basic, or the `nbc_session` cookie, in that precedence order. Invalid earlier credentials do not fall back. Cookie access is restricted to admin\u002Fengineer and valid origins. Browser terminals use cookies directly: the former one-time ticket and `POST \u002Fapi\u002Fconsoles\u002F{id}\u002Fwebsocket-ticket` were removed. Web login does not authenticate TCP\u002FTelnet. Some configuration\u002Fstatus GET endpoints allow anonymous reads and need network controls.\n\nWebSocket behavior:\n\n- No independent WebSocket port; the former port 8001 and its `\u002Fhealth` are removed. Use `GET \u002Fapi\u002Fsystem\u002Fhealth`. The shared WebSocket listener's address follows Web settings; `PATCH \u002Fapi\u002Flisteners\u002F{id}` cannot change it (409). Legacy saved WebSocket host\u002Fport fields remain for compatibility and do not allocate or conflict with ports.\n- Browser `Origin` must match request `Host` (hostname\u002Fport) or a configured allowed origin. Nonbrowser clients without Origin still require credentials but skip browser origin checking.\n- Proxies should preserve Host and forward WebSocket upgrades. Configure additional `host[:port]` entries in `websocket_allowed_origins`, with `*` wildcard support; restart after changes.\n- Server ping interval is 30 seconds; no reply within 15 seconds closes the connection and frees capacity.\n\n```toml\nwebsocket_allowed_origins = [\"console.example.com\", \"console.example.com:8443\"]\n```\n\n| Protocol | Refusal feedback |\n| --- | --- |\n| SSH | Shell shows `maximum connections reached` or other text after authentication; handshake overload closes before text and is logged |\n| Telnet | `[bridge] maximum connections reached` or other text, then close |\n| WebSocket | Terminal text and close 1013 for capacity or 1008 for other refusals, with a reason |\n| Raw TCP | Close without refusal text to avoid corrupting device byte protocols; inspect logs |\n\nAuthentication precedes WebSocket console checks. Unauthenticated requests receive 401 without learning console existence. Authenticated browsers may complete upgrade then receive refusal text for nonexistent\u002Fdisabled\u002Funlicensed\u002Fpaused consoles because browser scripts cannot read handshake error bodies. Nonbrowser clients receive HTTP 404\u002F403\u002F409.\n\nResource protection:\n\n- SSH allows 16 concurrent unauthenticated handshakes separately from each console's authenticated 1–256 limit. Excess handshakes close immediately; authenticated users may log in but receive capacity text at shell startup and close after about 30 seconds. Handshake timeout is 10 seconds; up to eight sessions per connection, with 30 seconds to start each. PTY requests do not extend it. Started shells follow shared console limits.\n- Telnet negotiation writes time out after five seconds; empty\u002Finvalid or over-1024-byte subnegotiations close the connection.\n- HTTP\u002FWebSocket holds up to 4096 connections, queuing excess. Headers are limited to 64 KB and must arrive within five seconds. Each browser terminal uses one connection.\n- Up to 512 ordinary Web requests run concurrently (excluding terminal WebSockets); excess returns 503 with `Retry-After: 1` rather than queuing.\n- Login\u002Finitialization source limits allow a burst of 30, then one request per two seconds; excess returns 429. Proxy users share a source address. `X-Forwarded-For` is not trusted; account lockout supplies stronger password protection.\n- Combined SSH\u002FTelnet\u002FRaw TCP connections have a global 4096 cap and queue excess; per-console limits still apply.\n- No systemd `MemoryMax` is set; configure a drop-in if your deployment needs it.\n- Web bind failures include reasons\u002Fremedies. WebSocket authentication\u002Fpause\u002Flicense failures are checked before upgrading, subject to browser refusal feedback above.\n- Closing\u002Fcancelling a client cancels its blocked serial enqueue. A write queue still full after 30 seconds terminates that client. These limits do not replace firewalls and deployment-wide memory\u002Fconnection budgets.\n\nPrefer SSH and restrict listening\u002Ffirewall scope. For encrypted HTTP\u002FWS use built-in HTTPS or a TLS proxy. Serial bytes may contain equipment passwords; proxy TLS protects only connections passing through that proxy.\n\n## Enabling HTTPS\n\nAdministrators can select a PEM certificate and matching private key under Network → HTTPS and certificate, then enable HTTPS. To avoid sending a private key in cleartext, uploads are allowed only from an HTTPS page or `localhost` on the server itself. For headless devices managed over remote HTTP, leave the certificate files empty, click “Enable HTTPS” and confirm using a self-signed certificate, save, restart the service over SSH or with a service manager, then return over HTTPS to upload the production certificate. Alternatively, transfer certificate files securely over SSH and configure them on the server. If a reverse proxy already provides HTTPS, built-in HTTPS is usually unnecessary. Enabling or disabling HTTPS takes effect after a restart; saving does not interrupt existing connections. UI-managed certificates are stored as `web-tls.pem` in the data directory; restrict read access to this file. Replacing a valid managed certificate takes effect for new connections without a restart.\n\nIf you have no certificate, leave the files empty, click “Enable HTTPS” and confirm using a self-signed certificate. An invalid uploaded certificate\u002Fkey pair also selects a self-signed certificate, clearly indicated in the UI. It is stored as `web-tls-self-signed.pem` in the data directory and reused after restart. Browsers will warn that it is untrusted; use a trusted certificate for public access. If configured certificate files are unusable at startup, HTTPS starts with this self-signed fallback rather than reverting to HTTP. The original files are not overwritten.\n\nThe administrator-owned configuration is read-only to the service. Web UI changes are stored in `DataDir\u002Fweb-settings.toml` and override only the corresponding Web settings at startup; the original configuration remains unchanged. To restore its Web settings, stop the service, back up and move aside this override file, then restart. The license, local release markers, managed certificates, and any missing generated identity values are also persisted under `DataDir`.\n\nHTTP and HTTPS have independent listen ports. When HTTPS is enabled, HTTP redirects to HTTPS. Set the HTTPS options and, optionally, a PEM certificate and key, then restart:\n\n```toml\nhttp_port = 80\nhttps_enabled = true\nhttps_host = \"0.0.0.0\"\nhttps_port = 443\ntls_cert_file = \"server.crt\" # Relative to configuration directory, or absolute\ntls_key_file  = \"server.key\"\n```\n\nWith `https_enabled = false`, only HTTP is served. When enabled, HTTP and HTTPS must use different ports. If the certificate pair is missing or invalid, startup uses a persistent self-signed HTTPS fallback rather than reverting to HTTP. HTTPS uses minimum TLS 1.2 and HTTP\u002F1.1 for WebSockets. HTTP requests redirect to the HTTPS port. Browser terminals use `wss:\u002F\u002F`; cookies become Secure.\n\nHTTP and HTTPS start independently. If one listener cannot bind or exits while running, the error is logged and the other keeps serving. When HTTPS is unavailable, a running HTTP listener serves the application directly instead of redirecting to a dead HTTPS endpoint. If neither Web listener can start, server startup fails. If only HTTP is available, fix the HTTPS failure before sending sensitive data over an untrusted network.\n\nExisting configurations without `https_enabled` but with a certificate pair retain their original single-port HTTPS behaviour until the HTTPS settings are saved again in the web UI.\n\nReplacing certificate files does not need a restart: new connections use the new valid certificate, invalid replacements are logged and the old one remains. Existing connections are unaffected. Restrict private-key reads.\n\nHTTPS covers the Web port only. SSH is already encrypted; Telnet\u002FTCP remain plaintext. For TLS termination by Nginx\u002FCaddy, keep application HTTP and set `secure_cookies = true`. Diagnostics show TLS and certificate status.\n\n## Login protection\n\nWeb login, WebSocket Basic, and SSH password authentication share protections:\n\n- Five consecutive username failures lead to a 30-second lock starting at the sixth failed attempt; further failures double it up to five minutes. Correct passwords are refused while locked. Web returns 429; SSH\u002FWebSocket report authentication failure. Success or 15 minutes without failures clears state.\n- Unknown usernames count too; usernames are case-insensitive.\n- At most two expensive password checks run concurrently; queued checks waiting over three seconds are rejected with 429.\n- Password checks do not hold configuration locks. A failed last-login timestamp write logs an error without blocking successful login.\n- Lock state is in memory and clears on restart; API keys are unaffected. Wait, or use local password recovery and restart if necessary.\n\n## Listener status and manual retry\n\nIndividual console listener bind\u002Fkey failures or recoverable serve panics mark that listener `failed` without stopping others. The management HTTP service is essential; its startup\u002Fruntime failure remains fatal.\n\nAdmin\u002Fengineer `GET \u002Fapi\u002Flisteners` returns all 145 entries, including protocol-disabled and unlicensed items. Saved configuration does not establish that a listener is running. Existing ID\u002Fprotocol\u002Fconsole\u002Fhost\u002Fport fields remain, with:\n\n| Runtime field | Meaning |\n| --- | --- |\n| `state` | `running`, `failed`, `stopped`, `disabled`, `license_blocked`, or `unavailable` |\n| `last_error` | String, empty when no error; cleared on successful retry |\n| `retry_allowed` | Boolean |\n\nRunning does not guarantee hardware or authentication. Paused console listeners may remain running. WebSocket is one shared physical listener with `console_number=null`, not separately retried per console.\n\n```sh\n# Replace the key and choose listener_id from the list.\ncurl -sS -H 'X-API-Key: YOUR_API_KEY' http:\u002F\u002F127.0.0.1:80\u002Fapi\u002Flisteners\ncurl -i -X POST -H 'X-API-Key: YOUR_API_KEY' http:\u002F\u002F127.0.0.1:80\u002Fapi\u002Flisteners\u002F1\u002Fretry\n```\n\nRetry uses latest persisted settings and changes only the requested physical listener, without changing configuration. Running listeners return idempotently without restarts\u002Fdisconnects. Unknown IDs return 404; disabled\u002Fblocked\u002Fnonretryable listeners 409; unsupported runtime capability 503. An attempted bind that fails still returns **HTTP 200**:\n\n```json\n{\n  \"listener_id\": 1,\n  \"protocol\": \"tcp\",\n  \"console_number\": 1,\n  \"host\": \"127.0.0.1\",\n  \"port\": 5001,\n  \"runtime\": {\n    \"state\": \"failed\",\n    \"last_error\": \"listen tcp 127.0.0.1:5001: bind: address already in use\",\n    \"retry_allowed\": true\n  }\n}\n```\n\nHandle non-200 request errors first, then inspect `runtime.state`; only running with an empty error proves listener recovery. Resolve port\u002Faddress\u002Fkey problems before retrying. PATCH with the same host does not retry; use POST. Configuration reload\u002Flicense application can attempt enabled listeners again; there is no periodic listener retry. Restart flags do not represent listener failures. License `runtime_error` reports the most recent license-application failure, not current live listener status. Network provides status\u002Fretry UI. Retry audits metadata, not terminal data.\n\n## Configuration import, defaults, and backups\n\nAdmin `PUT \u002Fapi\u002Fconfiguration\u002F` imports all 48 console configurations. Check duplicate devices\u002Fslots, serial settings, and listener conflicts and back up first. Obtain the list through `GET \u002Fapi\u002Fconsoles`; consult `\u002Fapi\u002Fdocs` for shape.\n\nDatabase save is transactional, followed by runtime reload that may change limits, next-open settings, and listeners and interrupt clients. Runtime reload is not a single atomic switch; reload failures are not returned through the save response. Success proves saving, not full listener application. Check listener\u002Fstatus\u002Flogs, resolve failures, retry individually, and test real connections. Restart flags do not prove complete application.\n\n`POST \u002Fapi\u002Fconfiguration\u002Frestore-defaults` resets console defaults and attempts reload, without resetting users, API keys, or licensing. Web “Clear all serial configurations” is an admin-only System danger-zone action for 48 consoles' notes, bindings, parameters, limits, protocols, and listener addresses. It does not clear users, keys, audits, licenses, startup configuration\u002FUUID, or SSH key. Restart for complete application; the operation is irreversible, so export first. Use CLI factory reset to clear accounts\u002Faudits too.\n\nFor a complete backup, stop and copy SQLite, configuration, SSH key, and license, including existing WAL\u002FSHM or using a reliable consistent SQLite backup. A console export excludes accounts, API keys, audits, and startup keys. Restore the corresponding file set while stopped, restrict permissions, then start and verify. Wrong pepper invalidates API keys, wrong session key invalidates cookies, changed SSH key triggers client warnings. Databases\u002Fbackups contain sensitive authentication data and must not be public configuration attachments.\n\n## License application and installation\n\nFree licensing allows consoles 1–2. Commercial tiers sold directly are **8P, 16P, and 48P**; other tiers are not sold directly. Issued license capacity governs:\n\n| Tier | Maximum logical serial ports | Console range |\n| --- | ---: | --- |\n| 8P | 8 | 1–8 |\n| 16P | 16 | 1–16 |\n| 48P | 48 | 1–48 |\n\nCapacity is a total range, not added ports or arbitrary choices among 48. Startup creates all 48 bridges\u002Fowners; unlicensed consoles keep configuration but reject forwarding, do not open hardware, and do not start independent TCP\u002FTelnet\u002FSSH listeners. Shared WebSocket checks authorization per console. Even licensed hardware opens only with clients. Capacity is not listener count or client count.\n\n### Three ways to obtain a license\n\n| Method | Procedure |\n| --- | --- |\n| Online | Admin `POST \u002Fapi\u002Fsystem\u002Flicense\u002Factivate` with `{\"code\":\"ACT-23456-789AB-CDEFG-HRZQE\"}`; sends code, hashes, hostname\u002Fplatform\u002Fversion, verifies\u002Fsaves and applies the returned file. Repeat activation of the same device does not consume another seat. |\n| Offline | Generate a one-use request, upload from a connected computer, download\u002Fimport the signed file. The device clock does not block activation; `created_at` text participates in nonce validation and must not change. |\n| Existing file | Import original text via `POST \u002Fapi\u002Fsystem\u002Flicense\u002Fimport-file`, use compatible envelope `PUT \u002Fapi\u002Fsystem\u002Flicense`, or stop and place the file at `license_file`. |\n\nActivation confirmation explains binding and uploaded hashes\u002Fplatform, and that incorrect activation may be revoked on the website within 72 hours; other transfers need the seller. Do not store activation codes on the device; renewal does not need them.\n\n### File format and size\n\nLicense\u002Frequest files are UTF-8 JSON text without BOM. Current local export names are `cs-YYMMDDHHmmss.license.txt` and `cs-YYMMDDHHmmss.request.txt` using local generation time. Activation\u002Frenewal use the same file; internal storage defaults to `license.txt`.\n\nPreserve server-issued original text; do not reformat or reserialize. Changing payload\u002Fsignature invalidates verification. Limit is **8192 bytes**; oversized imports return 413\u002F422 depending on validation route, and oversized local files are invalid. Web selects `.txt`; legacy `.lic`\u002F`.token` selection is not supported by the current UI. Content\u002Ftype is checked after signature verification. For automation preserving original text, send the `file` string to the import-file endpoint; the legacy envelope PUT remains compatible.\n\n### License server\n\n`license_server` accepts `host:port` or full HTTP(S) URLs and defaults to `https:\u002F\u002Flicense.betterone.top:8443`. Explicit empty disables online activation\u002Fchecks; omitted settings use defaults and explicit existing addresses are not replaced. Restart to apply edits. Unreachable\u002Fmalformed server responses return 502; missing configuration returns 503.\n\n### Device identity\n\nLicense `device_id` uses SHA-256 hashes with product salt\u002Ftype, not raw values, except administrator diagnostics:\n\n| Field | Source | Required presence |\n| --- | --- | --- |\n| `hardware` | SMBIOS motherboard UUID (Windows), DMI `product_uuid` (Linux), CPU serial fallback on applicable ARM boards | May be absent |\n| `install` | Windows MachineGuid or Linux `\u002Fetc\u002Fmachine-id` | May be absent |\n| `generated` | UUID v4 saved in TOML `device_uuid` and independent machine file | Required |\n\nRequests also include informational `platform` and `device_name`; neither affects matching. MAC addresses are not used. Hardware UUID must be a valid 36-character UUID, not all zero\u002FF, repeated-byte, or known placeholder (for example `03000200-0400-0500-0006-000700080009`). CPU serial must be 8–32 hexadecimal characters and not repeated-character. Installation ID, after removing hyphens\u002Fbraces, must be 32 hex characters, not repeated-character or `uninitialized`. Generated UUID must be lowercase v4; invalid values refuse startup.\n\nLinux `product_uuid` often needs root. Packaged systemd copies it before startup to `\u002Frun\u002Fbetterone-console-server\u002Fproduct_uuid` for the service. Manually running as an unprivileged user may omit hardware identity.\n\nIf a license has hardware identity, only hardware matches: reinstall\u002Flost configuration does not matter; another board or unreadable hardware fails, without fallback. Otherwise installation and generated identities must both match. Without hardware and installation identity activation is impossible; generated-only identity is insufficient. Cloned installation templates with separately generated UUIDs therefore do not share a license.\n\nMachine UUID files:\n\n| Platform | Path |\n| --- | --- |\n| Linux | `\u002Fvar\u002Flib\u002Fbetterone-console-server\u002Fdevice_uuid` |\n| Windows | `%ProgramData%\\BetterOne\\ConsoleServer\\device_uuid` |\n\nStartup uses an existing UUID; when the machine-level file differs, it wins and the value is retained in writable `DataDir\u002Fruntime-identity.toml` instead of rewriting administrator-owned base configuration. If the machine file cannot be written, the base or runtime identity file retains the UUID. Invalid machine UUID refuses startup. `NBC_DEVICE_UUID_FILE` can override its path; `off` disables that copy for special deployments\u002Ftests. Uninstall preserves it. Removing all copies generates a new identity; OS reinstall may lose them, so devices without hardware identity need reactivation. Whole-system cloning copies all identities and cannot be distinguished locally; server limits help prevent repeat activation. Contact the seller for migration.\n\n### Installation and immediate application\n\nImport-file verifies type=license, signature, device and downgrade confirmation, atomically saves, and coordinates runtime immediately with `restart_required=false`. Upgrades start enabled protocols on newly licensed consoles; downgrades disconnect out-of-range sessions, stop serial ports, and close their independent listeners. Shared WebSocket stays with per-console checks. Manual pauses remain. Invalid files\u002Fsave failures preserve old licensing\u002Fruntime.\n\nOptional admin-only `runtime_error` means the file installed but some listeners failed to apply, not that installation failed or a restart is required. It reports the last license application, not live retry updates. Inspect `GET \u002Fapi\u002Flisteners`, resolve errors, and POST retry or reload\u002Freimport. No periodic retry is scheduled.\n\nVerification chooses the file's `kid`. Only the built-in key table is trusted; keys are **never fetched from the network**. A file with an unknown `kid` is rejected (reason `unsupported_key`) until you upgrade to a release that includes the new key.\n\n### Maintenance and upgrades\n\nThe license is perpetual. Coverage uses:\n\n```text\ncoverage_end = license.maintenance_until\nversion_allowed = version_build_date \u003C= coverage_end\n```\n\nThis is an offline comparison against build date, not the current clock. Covered versions remain usable after maintenance expiry. A later version runs as free 2P, while covered earlier versions remain fully licensed. Configuration for higher consoles is retained; those consoles cannot forward\u002Fopen\u002Flisten. Status becomes `update_not_covered`, retaining valid license metadata; importing a covering license restores capacity without restart.\n\nFor example, coverage through `2027-10-01` includes that day's build but not `2027-10-02`, even with a 48P license.\n\n### Migration, reinstall, and mistaken activation\n\nThe Software does not release\u002Funbind\u002Fdeactivate devices. Seat release is a website\u002Fseller process; existing offline files remain usable after release, but cannot obtain new coverage.\n\n| Situation | Action |\n| --- | --- |\n| Incorrect activation within 72 hours | Buyer verifies code plus order\u002Femail on Devices and revokes, subject to limits |\n| Later, limits exhausted, or missing order registration | Contact seller for administrative release |\n\nThen activate normally on the new device. License summary provides public `portal_url`; hide empty links. Seats-full and device-released messages guide buyers to the same page when configured.\n\n### Per-device maintenance\n\nEach device starts its own maintenance at first activation; renewal extends that device only. Use its signed file for display\u002Fchecks, never assume all devices under a code share an expiry.\n\n### License status for scripts and interfaces\n\n| `state` | Meaning |\n| --- | --- |\n| `free` | No installed license, normal two-port edition |\n| `licensed` | Valid license covering this version |\n| `update_not_covered` | Valid license but this build is outside maintenance; current capacity is free while older covered builds retain licensed capacity |\n| `invalid` | Installed file unusable; admin `reason` distinguishes device_mismatch, signature_invalid, unsupported_key, file_too_large, unreadable, license_invalid, device_unavailable |\n\n“Activation and renewal” uses one input to detect ACT \u002F RNW codes and select the operation. Activated devices can still enter a new ACT code to replace their license. An unconfigured server disables online operations; the corresponding offline request can still be downloaded. Request download and license import buttons appear below the shared input. Confirm before replacing capacity.\n\n`edition` is display-only; `max_ports` is currently usable capacity; `licensed_ports` is purchased capacity; `actions.activate\u002Frenew` reflects online availability. Deprecated `valid` is false only for invalid state. Detailed reason, licensed capacity, actions and metadata are administrator-only; anonymous summaries expose state\u002Fedition\u002Fcapacity\u002Fbuild date.\n\n### Checking renewal\n\n“Check renewal” sends stored file and identity to `\u002Fv1\u002Flicense\u002Frefresh`; no activation code or repeat-activation delay is needed. The new file preserves identity\u002Flicense ID\u002Fports and updates coverage\u002Fissuance. Replace the same license only when coverage is later. Released devices cannot obtain new files but can continue covered versions with the old one.\n\n#### Offline renewal\n\nWhen the device cannot go online, finish on a connected computer through the website's “Offline activation \u002F renewal” page. It takes a single file and recognises its type automatically; no renewal code is typed on the web page.\n\n1. Prepare the file and copy it to a connected computer:\n   - Bought a renewal code: enter the RNW code in “Activation and renewal” and click “Create renewal request” (name ends with `.renewal.txt`). After an online connection failure, you can also confirm the prompt to generate an offline request.\n   - The seller already renewed in the back office: expand the offline renewal section within “Activation and renewal” and export this device's license file (name ends with `.license.txt`). This retrieves the latest license from the server without redeeming a renewal code.\n2. Upload it on the “Offline activation \u002F renewal” page (`\u002F#offline`; the old `\u002F#renew` opens the same page).\n3. Download the new `.license.txt`, copy back, and import original text\u002Ffile.\n4. Repeat independently for each device.\n\nImport verifies the local device and never regresses the same license's date. Different licenses can replace it, with confirmation if purchased capacity falls. Cancellation leaves the old file unchanged. Compare purchased capacity, not free fallback capacity. Files are limited to 8192 bytes; choosing one does not install automatically.\n\nAdmin export is `GET \u002Fapi\u002Fsystem\u002Flicense\u002Fexport` without caching; import:\n\n```json\n{\"file\":\"\u003Coriginal file text>\",\"allow_downgrade\":false}\n```\n\nSet true only after explicit confirmation and resend the original. No new date means no celebration; successful renewal\u002Factivation retains their fireworks\u002Fconfetti feedback.\n\n### Redeeming renewal codes\n\nACT activation and RNW renewal prefixes precede 20 characters: 16 random and four check characters. Allowed alphabet is `23456789ABCDEFGHJKMNPQRSTUVWXYZ`; normalization ignores case, hyphens, spaces, and tabs. Omitted prefixes are identified by validating both checksums. Wrong-kind\u002Finvalid codes are rejected locally before requests.\n\nRedemption uses the stored license\u002Fidentity at `\u002Fv1\u002Frenew`, renews one device, and requires code capacity at least purchased capacity. The new signed file is automatically imported. Redemption cannot be undone or transferred.\n\nAdmin local request:\n\n```http\nPOST \u002Fapi\u002Fsystem\u002Flicense\u002Fredeem-renewal\nContent-Type: application\u002Fjson\n\n{\"renewal_code\":\"RNW-23456-789AB-CDEFG-H3G4C\"}\n```\n\nRetries are safe: `already_redeemed=true` means no duplicate years\u002Fuse deduction. Codes are not stored. Higher-capacity renewal codes require confirmation, then resend the original with `confirm_ports:true`; cancelling does not use the code. Equal capacity needs no confirmation; insufficient capacity is rejected without use. Higher capacity gives no extra ports or benefits and renews for the same duration as a matching code.\n\nMaintenance may extend no later than today plus five years. `renewal_exceeds_limit` means wait and try later; the code remains unused. Server natural-month\u002Fmonth-end rules determine dates; clients do not predict future expiry.\n\n### Online errors\n\n| HTTP | Meaning |\n| --- | --- |\n| 400\u002F403\u002F409\u002F422\u002F429 | Upstream refusal retained with structured code, including seats_full, code_voided, code_expired, device_released, invalid_license, rate_limited |\n| 422 | Invalid code; upstream invalid_code (404) mapped locally to 422; invalid signature\u002Fdevice |\n| 409 | Renewal requested without a license |\n| 502 | Unreachable server or malformed reply |\n| 503 | Missing `license_server` |\n\nThe server limits repeated failures: five failures per code\u002FIP start a three-minute lock, escalating under server rules. Unknown and otherwise invalid activation codes both use `invalid_code`; do not try to distinguish them.\n\n### Before upgrading\n\nCheck coverage and stop\u002Fback up executable, database, configuration, SSH key, and license. Renew\u002Fget the latest covering file before using a later version. Verify compatibility before reverting and restore old backups if needed. Users choosing old versions after maintenance ends bear resulting defect\u002Fsecurity\u002Fcompatibility risks, subject to nonexcludable legal liability and separately agreed support duties. Device identity changes need developer\u002Fseller verification; there is no automatic migration.\n\n## Serial status messages\n\nWith a configured binding, failed opens\u002Fdisconnections retain client sessions during reconnect. Input during failure is discarded, not buffered. Missing binding does not enter continuous retry. Sessions close if recovery exceeds 300 seconds.\n\n| Trigger | Exact client text |\n| --- | --- |\n| First failure\u002Fdisconnect | `\\r\\n[serial] device unavailable; input will be discarded while reconnecting\\r\\n` |\n| Continued failure | Same, at most once per 30 seconds |\n| Recovery | `\\r\\n[serial] device connected\\r\\n` |\n| 300-second timeout | `\\r\\n[serial] reconnect timeout; closing session\\r\\n`, then close |\n\nAn initial successful open does not print recovery text. These texts come from the server, not the equipment, and enter all client streams, including Raw TCP\u002FWebSocket binary protocols. Clients must recognize them.\n\nListener status diagnoses network binds\u002Fserving; console status diagnoses serial hardware\u002Fread\u002Fwrite\u002Freconnect. Running listeners do not guarantee hardware. Listener retries do not repair drivers\u002Fcables\u002Fbusy devices or restart already-running listeners. Automatic serial reconnect is separate from manual listener retry.\n\n### Permission and busy-port failures\n\n| Message | Meaning |\n| --- | --- |\n| `serial device access denied` | Service account lacks serial access, typically Linux |\n| `serial device is busy` | Another program owns the device |\n| `serial device is unavailable` | Missing\u002Funplugged device, wrong name, or missing driver |\n\n#### Linux\n\nDevices typically use root:dialout\u002F0660. Packages add the service to dialout or RPM uucp. Remaining issues include Raspberry Pi onboard login console (root:tty and held by getty), missing groups, manually run users without membership, or restrictive udev rules.\n\n```sh\nls -l \u002Fdev\u002FttyUSB0\nid betterone-console-server\njournalctl -u betterone-console-server -n 50\nsudo fuser \u002Fdev\u002FttyUSB0\n```\n\nAdd the service with `sudo usermod -aG dialout betterone-console-server`, then restart it. For an interactive user, add to the group then log out\u002Fin. Raspberry Pi onboard serial: raspi-config → Interface Options → Serial Port, disable login shell and enable hardware, then reboot. If dialout\u002Fuucp is absent, use the actual device group. Fix udev ownership\u002Fmode and reload\u002Ftrigger\u002Freplug. Stop competing tools\u002Fgetty after identifying them.\n\nAvoid chmod 666: it is temporary across reconnects and grants every user serial access.\n\n#### Windows\n\nCOM ports normally have no Linux-style group restriction. “Access denied” generally means already opened, displayed as busy. Close PuTTY, SecureCRT, vendor tools, or other program instances; one program can hold the COM port at a time. For unavailable ports inspect Device Manager → Ports, driver warnings, and configured COM number; install genuine vendor drivers\u002Freplug or use slot\u002Fidentity binding. Corporate group-policy blocks require administrator intervention.\n\n### Serial fault handling and hardware acceptance\n\nRecoverable driver panics become errors with logged stacks. Nil handles\u002Finvalid I\u002FO counts are rejected. Partial writes continue; reads containing valid bytes plus an error forward bytes first. Empty immediate reads use cancellable waits; retry waits respect the remaining window. Each open has a separate identity so old reads\u002Fqueued writes cannot enter new connections. Cancelled remainder is not replayed; bytes already delivered to hardware cannot be recalled.\n\nNative Windows\u002FLinux I\u002FO uses a default five-second write timeout and 250 ms bridge read timeout; ordinary read timeouts are not faults. `CancelIO()` stops new operations and requests cancellation, not buffer clearing or proof of physical release. Owners wait for active work before closing. Windows confirms overlapped completion before releasing events\u002Fbuffers, retains failed handle closes for retry, and marks Released only after all owned OS handles close. Linux uses nonblocking poll\u002Fcancellation, a 100 ms cancellation fallback, releases exclusivity, and closes the raw fd once without retrying a numeric fd after EINTR.\n\nFailed opens may still leave handles needing cleanup. Before a new open, old handles must be confirmed released; retained handles are isolated and cleanup retried by one owner timer, even without clients, while paused, or after bridge close. Control APIs returning, idle\u002Fdisabled state, context expiry, or cancellation cannot force a broken driver to release within an absolute deadline. Driver\u002Fdevice maintenance or service\u002Fsystem restart may be needed. Drain waits for an empty output queue, not guaranteed hardware shift-register completion; Break is cancellable. No protection is promised against OOM, fatal runtime, or OS failures.\n\n**Physical hardware acceptance remains incomplete.** Prior recorded checks: WSL Ubuntu Linux serial PTY suite passed three runs; actual opener\u002Fbridge integration passed ten runs with reads\u002Fwrites, idle checks, teardown\u002Frelease\u002Freopen, client departure\u002Fpause\u002Flicense revocation, initialization cleanup, and descriptor checks. Windows syscall mock checks passed. These did not enable race detection in that round and are not physical USB-driver or native Linux release acceptance. Reproduction is in [serial documentation](..\u002F..\u002F..\u002Finternal\u002Fserial\u002FREADME.md).\n\nBefore production, test real equipment with an independent healthy console and logs:\n\n- Unplug\u002Freplug during traffic; verify messages, retries, recovery, and no replay of discarded\u002Fqueued input.\n- Missing\u002Fbusy bindings; verify deadline closure\u002Fno extra opens and recovery after release.\n- Read\u002Fwrite faults; valid trailing read bytes forwarded and partial writes completed without replaying cancelled commands.\n- Slow nonreading clients; healthy clients\u002Fconsoles continue. Disconnect\u002Flog the slow client at a 64 KiB\u002F512-frame queue or ten-second write timeout rather than silently dropping bytes. SSH disconnection ends other sessions on that underlying connection.\n- Pause\u002Flicense\u002Fclose during continuous writes with injected cancellation\u002Fclose failures; do not equate API return to release. Never reopen before confirmed cleanup. Verify Windows retained handles\u002Fretry and Linux exclusivity\u002Ffd safety. Arrange maintenance\u002Frestart for permanently unresponsive drivers rather than assuming automatic recovery.\n\n## Console status API\n\nAuthenticated `GET \u002Fapi\u002Fconsoles\u002F{conf_id}\u002Fstatus`, IDs 1–48, uses session cookie or API key; unauthenticated returns 401.\n\n| Field | Meaning |\n| --- | --- |\n| `state`, `message` | idle\u002Fconnecting\u002Fconnected\u002Fretry\u002Ferror\u002Fdisabled plus explanatory text |\n| `serial_release_pending` | Always boolean; physical close\u002Fabort pending or failed cleanup retaining handles; no new open before release |\n| `serial_stalled_operation`, `serial_stalled_for_seconds` | Earliest operation incomplete for 30 seconds; empty\u002Fnull without warning. Indicates suspected unresponsiveness, not proof of permanent hang; clears on completion, not hidden by licensing. |\n| `connection_count`, `max_connections` | Combined sessions and saved 1–256 limit |\n| `configured_device`, `configured_slot` | Latest saved binding |\n| `resolved_device`, `active_serial_config` | Actual current device\u002Fconfiguration; empty\u002Fnull if unavailable |\n| `connected_for_seconds` | Whole seconds connected, otherwise null |\n| `last_error`, `last_error_ago_seconds` | Controlled error\u002Fage, overwritten by newer errors, cleared to empty\u002Fnull on success |\n| `retry_count` | Scheduled waits in this client-session cycle, retained after last departure and reset at next first client |\n| `retry_elapsed_seconds` | Retry-window age while retrying\u002Freconnecting, otherwise null |\n| `retry_in_seconds` | Rounded-up remaining wait only while retry waiting |\n\nDurations use server Go monotonic time, not client clocks or subsequent clock adjustments. Elapsed values round down, remaining waits up, minimum zero. Null means not applicable. Polling values are advisory. Former connected_at\u002Flast_error_at\u002Fretry_started_at\u002Fnext_retry_at timestamps are removed; audits retain absolute UTC.\n\nRelease-pending is independent of listener status and disabled\u002Fidle\u002Fcontrol responses. Last client departure closes hardware on demand, retaining a failed last error until later success. Detailed raw driver errors\u002Fpaths stay in logs. Saved settings may differ from active handles; USB slot active config may have empty device, so use resolved_device. Next connect uses saved settings.\n\nLicense-blocked state is disabled\u002Flicense_limit with zero connections\u002Fretries, no active handle information, and null durations; pending release remains independently reported. No per-protocol client counts, byte totals, or dropped-frame statistics are currently exposed.\n\n## User management\n\n### Functions and roles\n\nAdmin can list\u002Fcreate\u002Fedit\u002Fdelete users and reset passwords. Engineer cannot manage users and can change only their own password. Both authenticated roles can change their own password by supplying the current one. No password hashes are returned in lists.\n\n### Creating users\n\nUsernames must be nonempty and unique; roles are admin\u002Fengineer. Passwords are at least eight characters, not letters-only\u002Fdigits-only. New users default enabled and carry `must_change_password`. The Web interface enforces the initial change; API\u002FSSH do not universally block other operations on that flag. First initialized admin chose its password and is not flagged again.\n\n### Editing users\n\nAdmin may edit nonbuilt-in usernames, roles, and enabled state. Changes invalidate existing sessions. At least one enabled administrator must remain: the last one cannot be demoted, disabled, or deleted.\n\n### Built-in admin protection\n\nBuilt-in `admin` cannot be renamed\u002Fdeleted; its password can change\u002Freset. It can be disabled\u002Fdemoted only while another enabled administrator remains.\n\n### Password changes and resets\n\nSelf-change requires current password and invalidates all old sessions, including the current one. Admin reset needs neither target nor administrator current password, only two matching compliant new passwords; it invalidates target sessions and flags next Web login for change.\n\n### Deletion\n\nDelete other users permanently; built-in admin and last enabled administrator are protected. Deletion invalidates accounts\u002Fsessions and cannot be undone. Prefer disabling for a temporary block.\n\n### User APIs\n\nAll user-management APIs require admin. `\u002Fapi\u002Fdocs` assets are embedded and work without external Internet access.\n\n| Method | Purpose |\n| --- | --- |\n| `GET \u002Fapi\u002Fusers` | List |\n| `POST \u002Fapi\u002Fusers` | Create |\n| `PATCH \u002Fapi\u002Fusers\u002F{id}` | Edit name\u002Frole\u002Fenabled |\n| `DELETE \u002Fapi\u002Fusers\u002F{id}` | Delete |\n| `PUT \u002Fapi\u002Fusers\u002F{id}\u002Fpassword` | Reset without current password |\n\n### SSH public keys\n\nBesides the password, users can sign in to the SSH consoles with an SSH public key, which suits scripts and automation: no password in the script and nothing to guess. Both work at the same time; adding a key does not turn the password off.\n\n**Your own keys**: in the web interface, open the user menu (top right) → \"My SSH keys\", paste the whole line of a `.pub` file (such as `id_ed25519.pub`), optionally give it a name, and add it. To create a key on your computer: `ssh-keygen -t ed25519`.\n\n**Keys of other users**: administrators use \"SSH keys\" in that user's row on the Users page.\n\n**Connecting**: as with the password, for example `ssh -p 6001 alice@\u003Cserver IPv4>`; the client uses the private key by itself.\n\nRules:\n\n- ed25519, ECDSA (including hardware security keys, `sk-` types) and RSA of at least 2048 bits are accepted; DSA, certificates and lines with options such as `from=` or `command=` are not.\n- Up to 10 keys per user; the same key cannot be added twice for one user.\n- Paste only the public key (`.pub`). Private keys (starting with `-----BEGIN ... PRIVATE KEY-----`) are refused; never give them to anyone.\n- Keys do not sign in while the account is disabled, deleted or must change its password (a new user, or a password just reset). New users sign in to the web interface once and change the initial password first.\n- Deleting a key or disabling the account refuses new connections; SSH sessions already open stay open until disconnected on the Ports page.\n- The list shows each key's fingerprint (as `ssh-keygen -lf id_ed25519.pub` prints it) and when it was last used.\n- A sign-in with a key writes an `ssh-login` audit event whose target names the key's fingerprint; adding and deleting keys are audited too.\n- API keys belong to no user and cannot have SSH keys of their own.\n- A factory reset removes all users and with them their keys.\n\n| Method | Purpose |\n| --- | --- |\n| `GET \u002Fapi\u002Fauth\u002Fme\u002Fssh-keys` | The signed-in user's keys |\n| `POST \u002Fapi\u002Fauth\u002Fme\u002Fssh-keys` | Add a key for yourself: `{\"public_key\": \"...\", \"comment\": \"optional name\"}` |\n| `DELETE \u002Fapi\u002Fauth\u002Fme\u002Fssh-keys\u002F{key_id}` | Delete one of your keys |\n| `GET \u002Fapi\u002Fusers\u002F{id}\u002Fssh-keys` | Admin: list a user's keys |\n| `POST \u002Fapi\u002Fusers\u002F{id}\u002Fssh-keys` | Admin: add a key for a user |\n| `DELETE \u002Fapi\u002Fusers\u002F{id}\u002Fssh-keys\u002F{key_id}` | Admin: delete a user's key |\n\n## API key management\n\nAll management requires admin. Create with unique `client_name` and admin\u002Fengineer `role`. Save returned plaintext `api_key` immediately: it appears only once and cannot be retrieved later.\n\n### Editing, rotating, and deleting\n\nOnly role\u002Fenabled are editable; client_name\u002Fkey are immutable. PATCH only fields to change:\n\n```json\n{\"role\":\"engineer\",\"enabled\":false}\n```\n\nFor renaming, rotation, or lost plaintext, create a new uniquely named key, update clients, then delete the old one. Deletion is irreversible; disabling temporarily prevents new authentication requests.\n\n| Method | Purpose |\n| --- | --- |\n| `GET \u002Fapi\u002Fapi-keys` | List with masked hashes, no plaintext |\n| `POST \u002Fapi\u002Fapi-keys` | Create, plaintext once |\n| `PATCH \u002Fapi\u002Fapi-keys\u002F{id}` | Role\u002Fenabled |\n| `DELETE \u002Fapi\u002Fapi-keys\u002F{id}` | Permanently delete |\n\n## Changing the Web\u002FAPI address\n\nWeb\u002FREST\u002FWebSocket share `http_host`\u002F`http_port`, default `0.0.0.0:80`. Admin can GET `\u002Fapi\u002Fsystem\u002Fweb-address` or PUT the same path:\n\nThe response includes `settings_source` (`base` or `data`). Administrators can confirm and call `DELETE \u002Fapi\u002Fsystem\u002Fweb-settings` to clear the UI-saved HTTP\u002FHTTPS overrides; the base settings apply after restart and the address may change.\n\n```json\n{\"host\":\"0.0.0.0\",\"port\":8080}\n```\n\nEvery change is audited. Admin\u002Fengineer can query `\u002Fapi\u002Fsystem\u002Fipv4-addresses` for active interface names, IPv4\u002Fprefix, loopback and link_local flags. `0.0.0.0` means all addresses but is not a listed interface. Clients usually choose nonloopback, nonlink-local addresses.\n\n**Restart required:** saving changes only configuration. `running` is current, `saved` is next-start address, `restart_required=true`; system `service_restart_required` also reflects it. Saving the running address clears the flag.\n\nHost must be local IPv4 or all-addresses (422 otherwise). Port is 1–65535 excluding 5001–5048, 6001–6048, 7001–7048 reserved console ranges, even if disabled. A changed port is test-bound immediately; conflicts\u002Fpermission return 409. A host-only change does not test-bind because the current server owns the port.\n\nOnly the two Web address settings change in the data-directory override; the base configuration and its comments\u002Fsecrets remain untouched. A temporary-file rename replaces the override atomically. `--addr` overrides saved settings at every startup: `command_line_override=true`, restart flag false; remove it to use saved values. If a change makes the service unreachable, start once with `--addr 127.0.0.1:8000`, then correct the saved override. Validation cannot guarantee network\u002Ffirewall reachability.\n\n## System diagnostics for support\n\nAdmin: System → Support diagnostics → Generate report. Confirm the privacy warning before collection; cancellation collects nothing. Review, copy\u002Fdownload text, and send to support yourself. Admin `POST \u002Fapi\u002Fsystem\u002Fdiagnostics` returns full `text`; engineers\u002Fanonymous users are refused.\n\nReport body is always English, independent of UI language. `Report ID` and `X-Request-ID` identify its audit entry, enabling attribution to creator\u002Ftime\u002Fsource if disclosed.\n\n**Do not send it with your license file.** Raw identity plus a license could help someone reuse it on another host. Reports are **unredacted**, include identifying and network information\u002Flogs, and are for troubleshooting only. No automatic upload occurs; send only to contacted support, not publicly. Passwords, API keys, secret_key, pepper, and SSH private key are excluded.\n\n| Section | Information |\n| --- | --- |\n| Program\u002Fsystem | Version\u002Fbuild\u002Fplatform\u002FGo\u002FOS, CPUs, uptime, hostname |\n| Resources | Instantaneous physical memory\u002Favailable including reclaimable cache, swap, Go allocated\u002Fheap memory (not process resident memory), Linux 1\u002F5\u002F15-minute load; container values may describe host, not limits |\n| Files\u002Fconfiguration | Executable\u002Fcwd\u002Fuser, paths and file existence\u002Fsize\u002Ftime, writable data directory, Web address, allowed origins, license server; secret keys show set\u002Funset only |\n| License | Edition, ID, coverage, identity hashes, verification errors |\n| Raw identity | Motherboard UUID, CPU serial, installation\u002Fgenerated IDs and hashes; rejection\u002Fread-failure reasons and hardware_source_used. Only this report exposes raw values. |\n| Listeners | Running\u002Ffailed listeners and errors; disabled\u002Fblocked counts |\n| Consoles 1–48 | Licensing\u002Fenabled, bindings, serial settings, listeners, counts, state\u002Ferrors |\n| Discovered ports | Names, slots, VID\u002FPID\u002Fserial identifiers |\n| Recent logs | About 300 in-memory lines, cleared on restart |\n\n`data_dir_disk` reports free\u002Ftotal disk space. Failed collection of one section shows its reason without preventing other sections. Each successful\u002Frefused generation is audited as `create-system-diagnostics` with actor\u002Fsource, not report content. The page itself contains no report data until requested.\n\n## Connection logs\n\nTCP\u002FTelnet\u002FSSH\u002FWebSocket connections are application logs only, not database\u002Faudit entries. The process writes stdout\u002Fstderr, not its own rotating log files; use systemd\u002Fjournald, service management, or redirection for retention.\n\n```text\nconsole 3 TCP: client 192.0.2.10:51234 connected (1 of 4 connections)\nconsole 3 SSH: client 192.0.2.10:51240 (user admin) connected (2 of 4 connections)\nconsole 3 SSH: client 192.0.2.10:51240 (user admin) disconnected after 12m3s\nconsole 3 TCP: client 192.0.2.11:40022 refused: maximum connections reached\n```\n\nLogs show console\u002Fprotocol\u002FIP\u002Fport, authenticated SSH\u002FWebSocket username, current\u002Fconfigured counts, duration, and abnormal-close reason. TCP\u002FTelnet have no username. Refusals include maximum capacity, disabled console, shutdown, and SSH login failures; early WebSocket refusals are logged too.\n\nSame IP\u002Freason repeats are collapsed over 30 seconds with omitted counts. SSH failure consolidation is by IP, independent of usernames. Client-supplied strings are control-character stripped and length limited to prevent forged log lines. No terminal payload is recorded.\n\nThese logs can enter diagnostics' memory tail, exposing client IP\u002Fusernames to support. SSH authentication is additionally audited as `ssh-login`. Deployers control retention\u002Faccess to identifying logs.\n\n## Sensitive-operation audits\n\nSQLite `audit_events` records initial admin setup, Web login success\u002Ffailure\u002Flogout, password changes\u002Fresets, user\u002Fkey management, license import, configuration updates\u002Fdefault reset, console changes\u002Fdisconnections, and SSH password authentication. Current SSH callback does not supply source IP, so that field is empty for those entries.\n\nRecords contain UTC time, operation, actor\u002Frole, actual source IP, request ID, target and result; Web results success\u002Ffailure\u002Fdenied include HTTP status. Proxy addresses may be recorded; untrusted forwarding headers are not used. Passwords, cookies, plaintext keys, license text\u002Fsignatures, request\u002Fresponse bodies, terminal payloads, and before\u002Fafter field differences are excluded. Normal reads\u002Fpolls and former ticket issuance do not write audit entries.\n\nAdmin queries `GET \u002Fapi\u002Faudit-events?limit=50` newest ID first; page with `before=\u003Clast-ID>`, for example `?limit=50&before=123`. Limit 1–200, default 50; before default 0. Use session\u002Fadmin API key. Current Nuxt Logs supports queries; old deployed assets may not, while API queries remain available.\n\nSensitive operations synchronously append using SQLite WAL. Hourly cleanup when new events arrive retains at most 100,000 records from 90 days, with temporary excess between cleanups. Retention follows host clock. No individual edit\u002Fdelete API exists; OS database-file access can still modify records.\n\nAudit-write failure preserves the operation's result, logs event\u002Frequest ID, and adds Web `X-Audit-Status: failed`. Sensitive-operation responses include generated `X-Request-ID`. Business changes and audits are separate commits, so a crash can leave a completed operation without an audit. Failed logins can produce frequent writes; audit recording itself introduces no additional login throttling.\n",false,1792221811572]