Skip to content

Telnet and the web terminal

wms-go gateway serve (the wms-gateway systemd unit) serves the interface to barcode scanners, old terminals and browsers:

  • Telnet on the ports in LISTEN_PORTS (default 2323 and 23). It negotiates the window size, so the interface fills your window.
  • Web terminal (ttyd behind a small proxy) on GATEWAY_PORT (default 7681), with tap-to-select for tablets.
██╗  ██╗ ██████╗      ██████╗  █████╗ ██████╗ ████████╗███████╗
██║ ██╔╝██╔════╝      ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
█████╔╝ ██║     █████╗██████╔╝███████║██████╔╝   ██║   ███████╗
██╔═██╗ ██║     ╚════╝██╔═══╝ ██╔══██║██╔══██╗   ██║   ╚════██║
██║  ██╗╚██████╗      ██║     ██║  ██║██║  ██║   ██║   ███████║
╚═╝  ╚═╝ ╚═════╝      ╚═╝     ╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   ╚══════╝
   LEGO · Part-DB · ModernWMS control suite
╭─ SYSTEM ───────────────╮ ╭─ COLLECTION ───────────╮ ╭─ ALERTS ───────────────╮
│ checking…              │ │ 0 sets · 0 pieces      │ │ ✓ nothing needs you    │
│                        │ │ all sets complete      │ │                        │
│                        │ │ missing   0            │ │                        │
│                        │ │ on order  0            │ │                        │
╰────────────────────────╯ ╰────────────────────────╯ ╰────────────────────────╯
Sat 24 Oct 2026  04:41:17  ·  telnet · 192.168.1.20
Note: telnet is not encrypted - use it only on a network you trust.
  [Q] Quit / Exit Suite (Press Q / ESC / tap Quit anytime to exit)
Username / User ID (or 'q' to Quit):  

Telnet is not encrypted

Passwords and 2FA codes cross the network readable by anyone on the path. The sign-on screen says so (quietly on a private network, as a warning from a public address). Use telnet only on a network you trust, and the web terminal over HTTPS elsewhere. Remote sessions require 2FA, unless an admin has exempted an account (limited groups, networks, channels and expiry — see Access control).

The sign-on screen

Both the telnet and the web terminal open on the control room: the KC-PARTS logo, live system status (Part-DB, ModernWMS, the sync service and the catalog's age — checked in the background, so a slow container never delays the prompt), the collection (sets, pieces, what is missing and on order), alerts, the admin's message of the day and a clock. The collection and alerts are visible before anyone signs in; Admin → Access Control → Security settings (or WMS_SIGNON_PUBLIC_STATS=0) hides them. wms motd "Stock check Saturday 10:00" sets the message.

After signing in you see when and where you last signed in, and any failed attempts on your name since. The Overview (hub 1) is the same control room with spend, the Set of the Day, a growth chart and recent activity; K shows the old ModernWMS / Part-DB KPI table.

What protects the gateway

  • At most 4 sessions per address; a session that uses up its 5 sign-in attempts is a strike, and 3 strikes in an hour block that address for 15 minutes (doubling up to an hour). This machine's own address is never throttled.
  • A session left at sign-on for 5 minutes is closed; a signed-in session locks after 15 idle minutes by default, and can be given a maximum length; both are set globally or per user in Admin → Access Control → Security settings.
  • The web terminal can't see client addresses, so it isn't throttled per address.

Multiple sessions at once

Every telnet connection gets its own OS process (a fresh wms tui under its own PTY) — there's no shared lock between sessions in the gateway itself, and this machine's own address is exempt from the 4-sessions-per-address limit above, so opening several sessions from here to test something is never refused. If sessions still seem to slow each other down, lego.db/Part-DB contention is the more likely cause, not the gateway — one real deadlock was found and fixed this way (a Part-DB write that looked up a location while its own earlier query was still open), but restricting each process to one database connection more broadly turned out to be too large a change to make safely in one pass (this codebase has dozens of query sites that would all need auditing for the same pattern first). Admin → Live Sessions shows who's actually connected, from where, and on what screen — the place to look if this comes up again.

Telnet was never actually the problem here — two telnet connections are already two fully independent processes. The web terminal was: see the next section for what "one persistent, shared session" means and /solo for a second, genuinely independent one.

Reconnecting after a dropped connection

Off by default. A handheld scanner's Wi-Fi radio drops and reassociates far more often than a desktop's connection does, and normally that means restarting at the sign-on screen — mid-pick, mid-check, every time. Setting WMS_TELNET_RECONNECT_SECONDS to a number of seconds turns on a short grace window: a dropped telnet connection's wms tui process is kept alive for that long, and a new connection from the same address within the window resumes it — same screen, same signed-in session, instead of a fresh login.

WMS_TELNET_RECONNECT_SECONDS=90   # tune to how long a real Wi-Fi blip on your fleet actually takes

Read this before turning it on. The grace window is keyed purely by the client's IP address — there's no handshake proving the reconnecting device is the same physical one. That's exactly right for a device with its own address (a WT41N0-class scanner with a per-device DHCP lease on its own SSID, which is the case this was built for), and exactly wrong behind shared NAT: any other device sharing that address within the window would be dropped straight into the previous user's already-authenticated session, no password asked. If your scanner fleet sits behind NAT rather than getting its own address each, leave this at the default (0, off).

A resumed session picks up wherever the screen was left — nothing is replayed, so any output the app produced while nobody was attached isn't shown, only what comes after. The reconnect also re-sends the window size, which prompts the TUI to redraw in full.

Two-factor authentication

wms-go users 2fa enable <user>       # shows a QR/secret and backup codes
wms-go users 2fa status <user>
wms-go users 2fa unlock <user>       # after a lockout

Codes are single-use (a code that just signed you in is refused if replayed; that's not counted as a guess). Five wrong codes lock the account for 5 minutes, doubling each time up to an hour. Signing back in from the same address within TWOFA_GRACE_MINUTES (default 30) skips the prompt on telnet; the web terminal doesn't get an address-based grace window (see below for why), but it doesn't need one either — a session it already verified stays signed in.

The web terminal is one persistent, shared session

Every telnet connection is its own process, with its own address, so a fresh connection can be trusted with the grace window above. A browser tab talking to ttyd has no such address to check — nothing about the connection proves "this is the same person who already passed 2FA" — so the web terminal deliberately never grants the grace window (an empty/unknown origin is refused, on purpose).

What it does instead: wms-gateway runs the web terminal's wms tui process inside a tmux session (wms-web) that outlives any one connection. Opening a new tab, the PWA resuming after being backgrounded, or a dropped mobile connection all attach to that same running process — wherever you left it, already signed in — instead of spawning a fresh one that has to ask again. Signing out (Q at the hub) still returns that process to the sign-on screen, so the next person to open the web terminal gets a real prompt.

The trade-off: it's one shared screen, not one per device. Every tab or device that opens the web terminal at the default address is looking at (and driving) the same session — the right model for one person using several devices, not for two different people at once. wms doctor reports whether tmux is installed; without it, the web terminal falls back to today's per-connection behaviour (a fresh sign-in and 2FA prompt every time).

tmux's own prefix key (Ctrl-B by default) is disabled inside this session — set-option -t wms-web prefix None is chained onto the same command that creates/attaches it, scoped to just that one session, never -g/global (so it never touches an admin's own unrelated tmux sessions on the same box). Without this, anyone with the web terminal open could press Ctrl-B then c and get a brand-new tmux window running a root shell — tmux handles its own key bindings before wms tui ever sees the keystroke, so none of the app's permission checks are in that path at all. /solo and telnet were never affected (neither ever wraps in tmux).

/solo: a second, independent web terminal

Open http://<host>:7681/solo (or /solo after whatever path your reverse proxy uses) for a completely separate web terminal — its own ttyd process, its own fresh wms tui per connection, sharing nothing with the default one at / or with any other /solo connection. This is the second identity the shared session above can't give you: two people (or one person testing as two accounts) on the web terminal simultaneously, neither one waiting on or seeing the other's screen. The cost is exactly what the default terminal avoids — no persistence: closing the tab or losing the connection ends that session for good, and reopening /solo is a fresh sign-on, 2FA included, every time.

/solo behind a reverse proxy/tunnel that can't rewrite paths

/solo works by forwarding whatever arrives at that exact path straight to a ttyd backend that was itself started expecting that path — reaching it through anything that preserves the path (a normal reverse proxy, SSH tunnel, this gateway's own default setup) just works. Some tunnels (Cloudflare Tunnel among them) forward the hostname but not a path rewrite, so a second hostname pointed at the gateway's main port lands on / (the shared session above), not /solo, however it's typed — there's no way to make that hostname's own root resolve to a path-scoped backend from the tunnel side alone.

Set WMS_WEB_SOLO_DIRECT_PORT to put a second, otherwise-identical /solo backend on its own port with no base path, so it answers correctly at its own root — point that second hostname's tunnel/proxy entry at localhost:<that port> instead of the gateway's main port. Off by default (no extra process, no extra port) until set. Independent of the proxied /solo above — neither affects the other.

Reaching the dev build from elsewhere: the Live/Test picker

By default, trying an in-progress dev build means either sitting at this machine or SSH-tunnelling to the test instance, which is deliberately 127.0.0.1-only. An admin who'd rather reach it from a browser or telnet client on another machine, without opening a second port, can turn on a picker on this already-exposed gateway instead:

# On the LIVE gateway's own env file only — never on the test instance itself:
WMS_TEST_BINARY_PATH=/usr/local/bin/wms-go-test
WMS_TEST_ENV_FILE=/root/.config/wms-go/test.env

Both unset (the default) means no change at all — no prompt, no extra path. Set both, and:

  • Telnet asks 1) Live 2) Test (dev build) right after connecting; anything but a clean "2" (a bare Enter, garbage, a dropped connection) means Live, same as today.
  • Web gets a third path, /test, next to / and /solo — same independent-per-connection shape as /solo, just pointed at the dev build and the test instance's own data instead.

Choosing Test really does run wms-go-test (whatever's actually on dev right now), reading WMS_TEST_ENV_FILE's paths — the same scratch LEGO collection and separate access/audit files the test instance itself uses.

Worth knowing before turning it on: this exposes the test (scratch) collection through the live gateway's existing public reach — behind the exact same real login and 2FA everything else there already requires, no weaker, but also no stronger. Anyone who can already sign in to the live instance can also reach the dev build this way. If that's not a trade-off you want, leave both variables unset and keep using the SSH tunnel documented in the test instance guide.