Web UI: management control plane, config editor, diagnostics, service control, and live stats#15
Merged
Conversation
webui: scaffold management control plane, config model, and web UI adapter Introduce the foundation for an operator web UI gated behind a new `webui` build tag (joined into `all`): - config.Model: an in-memory, serialisable representation of the whole server.toml, with deep Clone for staged edits, ToTOML marshalling (go-toml/v2, now a direct dep), FromSource koanf loading, Defaults, and Save with numbered backups (server.toml.NNNN) + atomic write. - pkg/status: concurrency-safe per-unit status registry (enabled/running, binding, hostnames/zones/shares, dependency edges) for the dashboard. - pkg/metrics: streaming stats hub with fan-out sinks; expvar sink keeps counters visible. Per-second rate broadcasting lives in the control plane. - pkg/control: transport-agnostic management API (status, config stage/apply/save/export with a dirty flag, service restart, interface and serial-port enumeration, SSE-style Subscribe, diagnostics facade). This is the single implementation every UI front-end shares, so a future text/telnet UI reuses it without HTTP. - pkg/serialport: per-OS serial-port enumeration (Windows SERIALCOMM registry; Unix /dev/tty* globs) for the TashTalk dropdown. - service/webui (//go:build webui || all): thin HTTPS adapter over pkg/control — JSON API, SSE stats stream, embedded vanilla-JS SPA (dashboard + config editor + diagnostics), and TLS with an in-memory self-signed certificate fallback. - [WebUI] config section + -webui-* flags (enabled, bind, tls, cert/key). The dynamic router, supervisor, and main.go wiring land in follow-up commits; the web UI hook is not yet driven from main. Both the no-tags (router) and -tags all variants build; config and control plane have unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
router: support dynamic add/remove of ports and services at runtime Make the router membership (ports, services, socket dispatch map) mutable while running so the management plane can enable or disable a transport or service without restarting the process. - Guard Ports/Services/servicesBySAS with an RWMutex; the receive path (deliver) and lifecycle iteration (Start/Stop) take it for reading, mutators for writing, so dispatch never sees a half-updated map. - AddService/RemoveService start/stop the service and register/unregister its socket; socket registration is extracted from New into registerServiceSocket/unregisterServiceSocket. - AddPort/RemovePort start/stop the port and bind the LLAP link manager; bindLLAPManager is refactored into a per-port bindPortLLAP helper. - RemovePort performs real route reconciliation via RoutingTable.RemoveEntriesForPort, withdrawing every route reachable through the port and dropping its zone associations (mirrors the cleanup SetPortRange/Age already do), so disabling e.g. LToUDP drops its networks instead of leaving stale routes. Race-tested: concurrent deliver during AddService/RemoveService churn, plus add/remove bookkeeping and route withdrawal on port removal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
cmd: extract Supervisor; reduce main.go to flags + config + run main.go is now a thin entrypoint: parse flags / load TOML, project the resolved config into a config.Model, build the Supervisor, construct the management plane, wire the web UI, run, and tear down. All component construction and lifecycle moved into the Supervisor. - Supervisor owns ports, the AppleTalk router (and its DDP service set), and the standalone hooks (IPX/NetBEUI/NetBIOS/SMB/WebUI). It registers each as a named status unit and drives Start/Stop with the existing ordering (transports before the layers that consume them). - Per-service lifecycle from the UI: StartService/StopService/ RestartService act on a named hook and honour declared dependencies (stopping NetBIOS stops SMB; restarting brings dependents back around it). - Control-plane integration: Apply performs an atomic whole-stack rebuild from the edited model (finer-grained application can layer on later via the dynamic-router primitives); RestartService and ListInterfaces satisfy control.Supervisor. config.Save is installed as the planes
control: add per-service start/stop from the UI
Expose StartService/StopService alongside RestartService on the control
plane and the supervisor interface, add /api/services/{name}/{start,stop,
restart} endpoints, and render Start/Stop/Restart buttons on each
controllable (hook) card in the dashboard.
Stops are dependency-aware: stopping NetBIOS cascades to SMB. Verified
end-to-end against a running binary — SMB stop/start toggles its running
state and stopping NetBIOS brings SMB down with it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
docs: document the web UI; add [WebUI] example and CI tag coverage
- server.toml.example: commented [WebUI] stanza (enabled/bind/tls/cert/key).
- scripts/ci/test.{sh,ps1}: add the webui tag set to the test matrix.
- README: a Web UI section covering config keys, flags, live start/stop, and
the stage/apply/save/download flow.
- CLAUDE.md: package table entries for service/webui, pkg/control, pkg/status,
pkg/metrics, pkg/serialport, config; note that the Supervisor owns the
runtime and main.go is just flags + config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
webui: edit AFP volumes and SMB shares; source them from the config model The config editor gains table editors to add, update, and remove AFP volumes and SMB shares. They mutate the keyed maps in the config model (AFP.Volumes / SMB.Volumes); the supervisor now builds AFP and SMB from the editable model rather than re-reading the TOML file, so edits take effect on Apply (the AFP config is self-contained, including its volumes). - AFPFlagInputs.VolumeModels carries structured volumes from the model; applyAFPFlagsToConfig prefers them over the raw -afp-volume flag strings. smbSharesFromModel builds SMB shares from the model. The supervisor passes FromConfig:false for AFP and uses the model for SMB shares. - AFP/SMB status units now report their advertised name(s) and share list. - config.Model fields gain json tags mirroring their toml tags, so the API JSON keys match the lowercase TOML names the SPA editor binds to. This also fixes the scalar config editor, whose field paths previously did not match the PascalCase Go-default JSON. - Apply preserves the running Web UI server across the atomic rebuild: it is detached before the stack stops and re-attached (already running) to the rebuilt stack, so the in-flight Apply request and the operator connection survive. Documented the remaining TIME_WAIT rebind limitation of the atomic rebuild for fixed-port services (AFP/DSI, SMB). Verified end-to-end: adding an AFP volume via the API and applying rebuilds AFP with the new volume (CNID db opened at the new path, volumes=1), the Web UI stays up across Apply, and the volume appears in status and the downloaded TOML. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
webui: add packet-dump and pcap capture options to the config editor The config editor gains a Packet Dump & Capture panel: parse-packets, log-traffic, parse-output file, and per-transport pcap capture paths (localtalk/ethertalk/ipx/netbeui) plus snap length. These already live in the config model, so editing and Apply rebuilds the stack with the new settings. - Traffic logging (LogTraffic) is now wired by the Supervisor from config instead of once in main, so toggling it from the UI takes effect on Apply (disabling clears the netlog sink). - The Router status unit reports parse_packets / log_traffic state and a summary of which transports have an active capture. Verified end-to-end: toggling parse_packets on and adding an ethertalk capture path via the API + Apply flips the Router status and attaches the capture sink; the web UI stays up across Apply. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
Add a Logs tab to the management UI that replays recent log history on open then streams new lines live, with a client-side level filter. - pkg/logbuf: in-memory ring buffer that is both a slog.Handler and a fan-out broadcaster (mirrors the stats broadcaster); untagged so a future TUI can read logs too. - pkg/logging: Options.Extra appends extra slog.Handlers to the fanout, letting the root logger tee records into the ring buffer. - pkg/control: LogHistory()/SubscribeLogs() on the plane, Logs dep (defaults to logbuf.Default). - service/webui: GET /api/logs (history) and GET /api/logs/stream (SSE), reusing the stats-stream SSE pattern; Logs tab in the SPA. - cmd/classicstack: install logbuf.NewHandler on the root logger. Builds clean for no-tags / webui / all; go vet -tags all and full test suite pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itor Relocate the run-core from cmd/classicstack (package main) into internal/app so the interactive binary, the Windows service wrapper (cmd/classicstack-svc), and the Unix/macOS daemon (cmd/classicstackd) share one runtime. Make UI-driven stop/start of pcap-backed protocols safe and restartable: - rawlink: guard pcapLink with a closed flag + ErrClosed so calls after Close return an error instead of a use-after-free (0xC0000005) in pcap_compile. - ports (IPX/NetBEUI/EtherTalk/MacIP): open a fresh link per Start via a LinkFactory and recreate the read-loop channels each cycle, fixing the "close of closed channel" panic on the second stop/start. - ipx router: add UnregisterSocket; RIP/SAP/NetBIOS-over-IPX/SMB-direct now release their sockets on Stop, fixing "ipx: socket already registered". Model NetBEUI/IPX as detachable NetBIOS transports rather than hard dependencies: stopping one detaches just its binding while NetBIOS and SMB keep serving. Add AddTransport/RemoveTransport/Transports to the NetBIOS service and a transport-binding registry in the supervisor. Status summaries are running-aware; remove SMB's phantom :139 (no TCP listener exists) in favour of the real served transports. Web UI: spinner + disabled buttons during start/stop/restart. Config editor: group AFP volumes / SMB shares under their service panels; FS-type and interface dropdowns; friendly interface labels (pcap description, GUID stored) via /api/interfaces + /api/fs-types. Treat [Bridge] as a reusable virtual interface (InterfaceModel) that protocols inherit or override with a per- protocol [Section.Custom] interface; absent Custom keeps existing configs working unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the MacIP gateway's runtime state in the management UI: Diagnostics: new read-only "MacIP Leases" probe lists current static and DHCP-relayed leases (IP, AppleTalk net.node, source, last-seen) via /api/diag/macip-leases. Backed by (*ipPool).snapshot() -> Service.Leases() -> MacIPHook.Leases() -> routerDiagnostics.MacIPLeases, returning ErrDiagUnavailable when MacIP is not built/enabled. Dashboard: the MacIP status unit now reports gateway mode (nat/bridge), DHCP-relay, zone, and live counts (active leases, pinned ASP sessions) via Service.GatewayStats() -> MacIPHook.State() -> control.MacIPState. A 5s status-refresh ticker (runStatusRefresh) re-publishes the live counts while the stack runs; started in Start when MacIP is enabled, stopped/nilled in Stop. Types stay tag-clean: macip shapes live behind //go:build macip while the hook/control layer uses neutral control.LeaseInfo/MacIPState, so untagged diagnostics_impl.go/supervisor.go never import service/macip. Tests: TestIPPoolStatsAndSnapshot covers stats()/snapshot() over mixed static+dhcp leases and a pinned session; TestDiagnosticsFallback now asserts MacIPLeases returns ErrDiagUnavailable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Log download: a "Download" button on the Logs tab fetches /api/logs/download, which serves the retained ring-buffer history as a plain-text attachment (one entry per line: timestamp LEVEL message, filename classicstack-<ts>.log). Reuses Plane.LogHistory(); no new control-plane surface. Serial dropdown: the Windows enumerator now labels each port with its COM name first (e.g. "COM3" or "COM3 (\Device\Serial0)") instead of surfacing the raw driver device path as the option text, and sorts the list numerically (COM1, COM2, COM10) rather than in registry order. The stored value remains the bare COM name. Adds TestComNumber. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a raw-text editor for the Netatalk-style extension_map file on the Configuration tab. The file is edited verbatim (comments and ordering preserved) rather than parsed into a grid; the server validates it with the existing parseAFPExtensionMap before writing and reports the offending line on failure. Plumbing: - config.SaveBytes writes an arbitrary text file with the same numbered-backup + atomic-rename guarantees as config.Save. - Supervisor.ReadExtMap/WriteExtMap resolve the configured AFP.ExtensionMap path (relative to the config dir, as AFP wiring does), read it, and validate+save edits. validateExtMap is behind //go:build afp (with a disabled stub) so untagged code stays clean. - control.Plane.ExtMap/SaveExtMap delegate to the supervisor; webui serves them at GET/PUT /api/extmap. The saved map loads on the next config Apply (WriteExtMap does not restart AFP). A collapsible "Extension map" section lazy-loads the file on first expand, with Reload and Save actions. Tests: TestExtMapDelegates and TestExtMapWithoutSupervisor cover the plane delegation and the no-supervisor guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MacIP gateway panel gains the previously-unexposed fields: NAT gateway IP, lease file, nameserver, and BPF filter override, plus inline hints on the existing zone/subnet/gateway/DHCP-relay fields. Add an "IPX Gateway (MacIPX)" config panel (enabled toggle + editable "Object:Zone" zone-binding list) backed by a new "stringlist" field type and buildStringList() editor, with matching .stringlist CSS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The optional AppleTalk DDP subsystems (AFP, MacIP, the IPX gateway) used to ride the AppleTalk router's initial service set, so their only lifecycle was the whole-stack one and the dashboard showed no start/stop/restart controls (markServiceRunning just force-toggled them with the router). Wrap each subsystem's DDP services in a ddpServiceHook that drives the router's runtime AddService/RemoveService primitives, register it as a standalone hook, and promote its status unit to KindHook so the UI surfaces lifecycle controls. buildServices now collects these groups instead of appending them to the router's initial set; buildHooks wraps them once the router exists (they depend only on the router, which starts first). Start rolls back partially-added services on failure; Stop removes them in reverse. Disabled subsystems contribute no services, so they register no hook and remain uncontrollable, as before. markServiceRunning is removed. Adds ddp_service_hook_test.go covering start/stop, start-rollback, the empty-group nil hook, and KindService->KindHook promotion preserving detail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The metrics hub, SSE broadcaster, and expvar sink were all wired but nothing pushed samples, so the dashboard cards never showed live stats. Add the first producers: per-port rx/tx packet and byte counters for the AppleTalk ports (EtherTalk, LToUDP, TashTalk). Ports report traffic through a new optional port.TrafficMetered interface (SetTrafficObserver) so no port implementation depends on pkg/metrics — the data path only calls a plain observer func. The EtherTalk and LocalTalk ports emit Rx on inbound DDP delivery and Tx in Unicast/Broadcast/Multicast. The supervisor attaches a portMeter per port (carrying the status-unit name), accumulates counts, and publishes them once a second from the refresh ticker under "unit:<Name>:<metric>" so each sample attributes to exactly one card. The earlier wrapper approach was abandoned: ports self-register into the router's tables as their raw selves, so a wrapping port would have caught inbound but missed almost all forwarded tx. The observer hook sits inside the port where every send actually originates. SPA: replace the fuzzy substring rate-matching with exact "unit:<Name>:" prefix lookups, and render a per-card line — "↓ N pkt/s (bytes/s) · ↑ N pkt/s (bytes/s)" plus any gauge value (e.g. sessions) — hidden until live data arrives. Gauge frames are now consumed too. Remaining for full coverage (deferred): IPX/NetBEUI port metering (different frame type / observer point) and AFP/SMB active-connection gauges (needs session-table accessors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the per-port traffic metering added for the AppleTalk ports to the standalone IPX and NetBEUI ports so their dashboard cards also show live rx/tx packet and byte rates. The IPX/NetBEUI portImpls gain a SetTrafficObserver (an optional method, not added to their Port interfaces, so the existing test fakes need no stub) and emit Rx on inbound decode and Tx in Send. Their enabled hooks forward SetTrafficObserver to the underlying port; the supervisor installs a portMeter through that optional interface and publishes it on the same one-second ticker as the AppleTalk ports. Disabled-protocol hooks and ports that do not meter are skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PR CI golangci-lint step (run with --build-tags=all on Linux) flagged: - errcheck: unchecked os.Remove / tmp.Close / key.Close returns in config/save.go (atomicWrite) and pkg/serialport/serialport_windows.go. - errorlint: direct == / != comparisons against sentinel errors in pkg/control/control_test.go and serialport_windows.go; switched to errors.Is. - unlambda: redundant func() wrapper around over_tcp.NewTransport (which already returns netbios.Transport) in internal/app/netbios_enabled.go. - ineffassign: dead final n += copy(...) in service/smb/command_fs_search.go. No behaviour change; all builds and tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PR CI govulncheck step flagged stdlib CVEs (net/textproto, crypto/x509 at
go1.23.4) and golang.org/x/net@v0.33.0 advisories (GO-2026-5025..5039). Bump
the go directive/toolchain to 1.25.11 and x/net to v0.55.0 (pulling x/crypto,
x/sys, x/sync, x/text forward via go mod tidy). govulncheck now reports no
called vulnerabilities.
The x/net bump updates goquery's transitive x/net/html parser, which changed
how the macgarden client tests' hand-written fixtures parsed and broke the
pagination-count assertions. Replace those fixtures with real HTML captured
from macintoshgarden.org (testdata/category_antivirus_page{1,5}.html) and a
loadCapturedPage helper that reroutes the captured root-relative links to the
test server. The two affected tests now assert the live site's real structure
(10 items/page, last page ?page=5 with 8 items, 58 total).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dings Add scripts/ci/quality.sh — a single source for the CI Quality job's static gates (vet + golangci-lint + govulncheck + gosec) that also runs locally via `make quality`. The PR CI Quality job and the Makefile both call it, so local development runs the exact same vulnerability scan as CI. govulncheck and gosec self-install on demand, matching how CI bootstraps them; SKIP_LINT=1 lets CI skip the script's lint pass since it runs golangci-lint via its own action. The Makefile gains `quality`/`vet` targets and self-installing `vuln`/`gosec`. Clear the gosec gate (it never completed before because govulncheck failed first): exclude G115 (integer-overflow conversions on values already bounded by the wire formats — pure noise here), and add justified #nosec annotations to the 11 remaining intentional findings in the macgarden client (SHA-1 cache filenames, relaxed TLS for the abandonware mirror, world-readable public caches) and macip (DHCP xid via math/rand, operator-configured lease file). gosec now reports 0 issues over the scanned packages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add port/router supervisor hooks, routing-table snapshots and dynamic routing wiring, plus diagnostics and web UI surfaces (API, SSE stats, SPA assets) to inspect the router state. Excludes screenshot images. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit adds the GNU General Public License version 3 to the project, ensuring that the software can be freely used, modified, and distributed under the terms of the license.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a complete management plane and web UI for ClassicStack, plus the supporting runtime refactors (a
Supervisorthat owns the stack, a dynamic router, and daemon/Windows-service wrappers). This is the fullfeat-webuiline: 16 commits from the initial control-plane scaffold through live per-port statistics.What's included
Runtime / control plane
pkg/control— transport-agnostic management API (status, config stage/apply/save, per-service start/stop/restart, diagnostics) shared by every front-end.internal/app.Supervisor— owns ports, the router (+ DDP service set), and the standalone hooks; exposes dependency-aware lifecycle control.main.gois reduced to flags + config + handoff.router/— runtime add/remove of ports and services so the UI can toggle a transport or service without a full restart.pkg/status,pkg/metrics,pkg/logbuf,pkg/serialport— status registry, streaming-stats hub, log ring buffer, serial-port enumeration.Web UI (
service/webui,-tags webui)pkg/controlwith an embedded dependency-free vanilla-JS SPA (no build step).Daemon / service
cmd/classicstack-svc(Windows service) andcmd/classicstackd(Unix/macOS daemon, macOS LaunchAgent) sharing theinternal/apprun-core.Known follow-ups (not in this PR)
Testing
go build -tags all ./..., no-tags, and-tags webuiall green.go test -tags all ./...passes; new unit tests cover the DDP service hooks, per-port meters, transport bindings, extension-map round-trip, and control-plane delegates.go vet -tags all ./...andgofmtclean;node --checkon the SPA.🤖 Generated with Claude Code