All notable changes to Sentinel Node Manager are documented in this file.
The format is based on Keep a Changelog, and this project follows the spirit of Semantic Versioning (though it has not yet shipped a tagged release).
This entry covers the work on feat/ui-overhaul since commit
4234893 feat: UI overhaul + CLI server + SSH batch deploy.
- New service
src/main/services/node-specs.ts— operator self-MsgSend (from == to, 1 udvpn) carrying memospecs:v1:{cpu,c,cr,r,rr}so any explorer can identify the report. Operator-reported, not consensus-validated; CQAP attestation supersedes it once that lands. - Triggered automatically after a successful local OR remote deploy via a
fire-and-forget IIFE in
deploy.ts(12 s settle window so the seed-funding tx propagates across the RPC pool before signing). - Gas budget set to 250 000 for the MsgSend (Sentinel ante handlers consume ~120.8 K; the previous 120 K hit code 11 out-of-gas).
- Startup replay path:
replayPendingSpecs()(called fromsrc/main/index.ts) re-broadcasts for any node whosespecsPublishPendingflag is still set from a prior session — handles app-killed-mid-broadcast and transient RPC outages cleanly. Sequential, gentle, fire-and-forget; UI never blocks on it. - Activity log surfaces a new
specs-reportedevent kind with the tx hash.
- New service
src/main/services/live-stats.ts— refcounted 1 Hz CPU/memory sampler that broadcastsLiveSystemStatsoverIPC.SYSTEM_LIVE_STATS. The refcount lets multiple subscribers share a single timer; the timer stops cleanly when the last subscriber unsubs. - New screen
src/renderer/src/screens/System.tsx(replaces ad-hoc system cards): live CPU per-core %, total memory %, OS / arch / kernel, Docker health summary, and a Reporting toggle for the on-chainspecs:v1cadence (default ON). - Live Specs default-on, persistent across navigation. The toggles
(
systemLive,systemReporting) live in the Zustand store and are bootstrapped at app start, so navigating between screens no longer kills the sampler. Previously eachSystem.tsxmount calledstartLiveStatsin auseEffectand the unmount cleanup tore it down; now the subscription is started once at bootstrap and the toggle just flips a boolean. src/main/services/system-report.ts— extracted single source of truth forLocalSystemReportso thesystem statusCLI command and the System page can never disagree.
- New screen
src/renderer/src/screens/Activity.tsx— grouped, filterable event log (Deployments, Node lifecycle, Wallet, Specs, Withdrawals). Renders allAppEventkinds with relative timestamps, kind-specific icons, and the related node moniker. - Existing
src/renderer/src/lib/events.tsextended withKIND_ICONandKIND_TONEmaps so the Activity feed and the toast layer share styling.
- New component
src/renderer/src/components/SeedPhraseModal.tsx— global dialog mounted atLayoutlevel so it survives tab switches. Pops ~3.3 s afterphase === 'done'(lets the progress ring complete cleanly first), shows the mnemonic with copy + verify-3-words confirmation, and is dismissed permanently perjobIdonce the user ticks "I've stored it". - Mnemonic visibility now resets across deploys: the previous deploy's mnemonic never bleeds into the next deploy's dialog.
- Renderer sandbox enabled (
webPreferences.sandbox: true) — a renderer compromise can no longer reach raw Node APIs through the preload's CJS context. Preload runs in a sandboxed context using onlycontextBridgeipcRenderer.
- Strict CSP injected as a response header from the main process
(
installContentSecurityPolicy). Prod policy isdefault-src 'self'withscript-src 'self'and no inline scripts; dev relaxes only to accommodate Vite HMR ('unsafe-inline','unsafe-eval', ws origin).object-src 'none',frame-src 'none',form-action 'none',base-uri 'self'. - Window-open guard —
setWindowOpenHandlernow allow-listshttps:andmailto:only and logs every blocked scheme.file:,javascript:,data:, and custom-protocol vectors are denied. - In-window navigation guard —
webContents.on('will-navigate')blocks any URL that isn't the dev server or the bundledfile://app origin. - Auto-start CLI server requires explicit ack —
SNM_AUTO_START_CLI=1alone is now ignored. The user must also exportSNM_AUTO_START_CLI_TOKEN=i-understand-this-opens-a-local-control-channel. Without the token the env var is no-op'd and a one-line refusal is logged. CLI server still defaults OFF; in-app Start button is unchanged. - TOFU host-key tracking for SSH — new
src/main/services/host-keys.tspersists the SHA-256 host fingerprint we saw on first connection and logswarn-level events on later mismatches. Non-blocking by design (rotating cloud hosts); creates a forensic trail inapp.log. - CLI scrubSecrets() — the CLI server's broadcast layer strips
--mnemonic,--password, and similar credential-bearing flags from events so a watching agent can't reconstruct secrets from the broadcast stream. The mnemonic remains reachable only via the gated in-app reveal flow. deploy.statusover the CLI redactsmnemonicForBackup— the seed never leaves the in-app reveal flow even when an agent is connected.
walletRefreshIntervalSecandnodeRefreshIntervalSecare now applied immediately on save viaonSettingsChanged— previously required app restart. Wallet balance poller and node manager poller both restart with the new cadence on the next tick.- Settings UI exposes both intervals with sensible bounds (10–600 s for wallet; 30–600 s for nodes).
- Post-deploy
MsgUpdateNodeDetailsbroadcast — pricing, gigabyte/hourly rates, and price mode are now pushed on-chain immediately after the node lands its first registration tx, instead of waiting for the next edit pass. - Local deploys capture a
NodeSpecsSnapshotviacaptureLocalSpecs()and persist it on the stored node so the System page and thespecs:v1memo see the same source of truth. - Per-deploy progress phase machine refactored to emit cleaner status
lines (
[ts] message) suitable for both the in-app log and the CLIdeploy.statusstream. - Failed deploys now drop the zombie node entry plus its logs / metrics / backup so the user isn't left with dead inventory.
- Cancellation honoured at every
awaitboundary insiderunLocalDeployandrunRemoteDeploy.
src/renderer/src/screens/DeploySshBatch.tsxrewritten (788 +/-) — CSV/JSON input, per-row credential override, parallel-with-cap scheduler, per-row live status and final summary. Mnemonics for batch rows are generated locally per host and surfaced via the same gated reveal flow as single deploys.<password>and<paste key here>placeholders in the template are inert; the screen blocks deploy until the user replaces them.
src/main/services/node-manager.ts(+~470 net) — per-node fast-poller with self-expiry. Nodes inloadingget probed every 4 s for up to 5 min so the UI flips "Pending registration → Active" promptly without hammering the chain. Online nodes keep the fast-poll for a 60 s grace window afterstartedAtso peer count, uptime, and reachability stay live during the post-online window.- New IPC pipeline
nodes:live-status— the fast-poller pushesNodeLiveStatusUpdateframes straight to the renderer store; UI no longer needs to re-query. reapStuckNodes()+ new IPCNODES_REAP_STUCK— surfaces the "garbage collect dead nodes" action to the renderer; useful when adocker rmfailed mid-shutdown last session and left orphaned containers.restartPollerCadence(intervalSec)— exported so the settings change hook can swap the global poller's interval without restart.
src/main/services/docker.ts(+209) — Windows pipe detection now prefersdockerDesktopLinuxEngineover the legacy\\.\pipe\docker_engine(matches upstream PR #3, expanded with stricter fallback ordering and an explicit "Docker Desktop is not running" diagnostic).- New IPC
DOCKER_OPEN_SETTINGS— opens Docker Desktop's settings window from the in-app Manage Docker screen so users can flip "Expose daemon on tcp://localhost:2375 without TLS" if they need it for remote-engine setups. dockerHealth()returns a structured result the System page renders inline (engine reachable, version, API version, daemon socket path).
src/main/services/wallet.ts(+/-173) — RPC-first balance refresh per global rules.refreshWalletBalance()usescreateRpcQueryClientWithFallback()and only falls through to LCD when every configured RPC has failed. Logs the elapsed-ms and chosen endpoint atdebuglevel for triage.signerFromMnemonic()extracted sonode-specs.tsanddeploy.tsshare a single signer-construction path (HD path44'/118'/0'/0/0, bech32 prefixsent).
src/main/services/sentinel-client.ts(+121) —signClient(mnemonic)helper that wrapsSigningStargateClient.connectWithSignerwith our RPC fallback list, default fee, and retry-on-transient errors. TheTRANSIENT_ERRregex now includes the fresh-account propagation lag ("account does not exist") so post-funding broadcasts retry cleanly.
src/main/services/cli-server.ts(+95) andcli-registry.ts(+/-88) — Windows named pipe / Unix socket server exposing typed commands (system.*,wallet.*,nodes.*,deploy.*,metrics.*). Only one client at a time; clean handshake error if a second tries to connect.- New
SeedPhraseModal-aware command flow: the CLI cannot fetch the mnemonic; agents must drive the user through the in-app reveal.
tests/e2e/cli-e2e.mjs— new real-money mainnet harness driving every CLI command, including a real self-send and a realMsgUpdateNodeDetailsbroadcast, verified on-chain via Sentinel RPCtx_search. Total spend per full run ≤ 0.0015 DVPN.tests/e2e/universal.mjs— a smaller universal-shell test for the CLI handshake and the no-money read-only commands.tools/poll-deploy.js— small helper that pollsdeploy.statusfor human-readable phase transitions during manual smoke runs.docs/e2e-cli-test.md— full protocol, on-chain verification details, troubleshooting table, safety knobs, and CI-mode flags.npm run test:e2eandnpm run test:universalscript entries.- README "End-to-end CLI test (real money)" section explains prereqs and run flags.
- Topbar (
src/renderer/src/components/Topbar.tsx) — back-button removed (the in-app history was confusing alongside the sidebar); wallet balance pill now flips to a refreshing spinner during the active fetch rather than going stale. - ProgressRing (
ProgressRing.tsx) — 0–5 % "blob" artifact fixed (sub-pixel arc rounding); reverse-flow flicker on phase regress (e.g. retrying after a soft failure) eliminated by clamping the underlyingrequestAnimationFrameinterpolator. - Deploy log — every "p2p" string rebranded.
- 1 s uptime ticker on Node Details so the uptime field doesn't sit on stale data between 60 s polls.
- Sidebar — System and Activity entries added; ordering tweaked so the three most-used screens (Overview, Nodes, Wallet) sit at the top.
- Layout (
Layout.tsx) — mounts the global<SeedPhraseModal />so it survives navigation. - PageHeader (
PageHeader.tsx) — title/subtitle now accept ReactNode for the trailing slot so screens can drop in custom buttons (Refresh, Export, etc.) without a layout shim. src/renderer/src/styles/index.css— small additions for the live CPU bar gradient and the seed-phrase reveal blur.src/renderer/src/lib/format.ts—relativeTime()andshortAddr()helpers used by Activity and System.
src/main/services/logger.ts(+72) —attachGlobalHandlers()now hooksprocess.on('unhandledRejection')andprocess.on('uncaughtException')and flushes them through the same log file.logDirexported so the diagnostics zip can include it.- Per-area child loggers (
log.child({ area: 'deploy' })) so grep'ingapp.logfor a single subsystem is straightforward.
ssh.forgetHostKey --host <host> [--port 22]— drop a TOFU-pinned SSH host key so the next connection re-pins it. Useful after a remote box is rebuilt and presents a new fingerprint. Already existed in the main-process pipe registry; now also exposed viawindow.api.ssh.forgetHostKey()and the renderer CLI registry so the two surfaces stay in lockstep.nodes.publishSpecs <nodeId>— publish the on-chain hardware specs report (specs:v1self-MsgSend memo) on demand. Idempotent: if the node'sspecsTxHashis already set, returns the cached hash without spending; otherwise broadcasts the same path the deploy hook uses. Added to BOTH registries and towindow.api.nodes.publishSpecs().- IPC handlers in
src/main/ipc.tsvalidate the new payloads (vUUID(nodeId)for publishSpecs, host/port shape check for forgetHostKey) and rate-limitnodes-publish-specsat 3 burst / 0.2 rps so a buggy caller can't loop self-MsgSends. docs/e2e-cli-test.mdupdated:nodes.publishSpecsadded to phase 5 (Inspect) — second run must return the cached hash without spending — andssh.forgetHostKeydocumented in the validation contract row. Cost section bumped to "at most three transactions" with the idempotency note.
src/shared/types.ts— new types:LiveSystemStats,NodeLiveStatusUpdate,NodeSpecsSnapshot,MnemonicExportResult,EventKindextended with'specs-reported'. New IPC channels:SYSTEM_LIVE_STATS_START,SYSTEM_LIVE_STATS_STOP,SYSTEM_LIVE_STATS,DOCKER_OPEN_SETTINGS,NODES_REAP_STUCK,NODES_LIVE_STATUS,NODES_EXPORT_MNEMONIC,SSH_FORGET_HOST_KEY,NODES_PUBLISH_SPECS.- All new IPC referenced via the
IPCenum; no raw-string channels introduced (per project rule).
tests/renderer/wallet-setup.test.tsx— updated for the new WalletSetup flow (mnemonic-first, address derived after, mocksignerFromMnemonic).
package.json—test:e2eandtest:universalscripts added..gitignore—vendor-clone/added (tester-only third-party clones).package-lock.json— minor lockfile churn from the new test deps.
*.tsbuildinfofiles were tracked in a previous commit; this changeset does not yet untrack them. Will land in a follow-up.- No persisted CHANGELOG entries before this one — historical changes
are recoverable from
git log.