Skip to content

fix: reload node credentials and unify dashboard tables and settings - #359

Merged
theogravity merged 45 commits into
mainfrom
fix/node-reregister-and-filters
Oct 12, 2026
Merged

theogravity merged 45 commits into
mainfrom
fix/node-reregister-and-filters

Conversation

@theogravity

@theogravity theogravity commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Re-registering a node could leave its running daemon dialing with stale credentials. The daemon now reloads connection settings before dialing and on each heartbeat, preserving running subshells. The dashboard also gains consistent, responsive tables and page spacing.

Changes

  • Reload node credentials and server addresses as one validated bundle. Keep the running identity if the file is unreadable or invalid, avoid redials for formatting-only changes, and update address-change copy to describe live application.
  • Keep Nodes table-only, with search and status/access filters, a distinct zero-match state, and the existing inline status dot beside the linked name. The shared status-dot component is excluded.
  • Put tables in cards with normal-size text, compact rows, responsive columns, consistent disclosures, and destructive menu actions grouped last. Group presets by agent and subshells by the selected grouping; names inspect content and Edit stays in the menu.
  • Use plain text for routine field values and Yes/No for boolean columns. Keep management actions accessible on small screens.
  • Move provider settings into a Configure dialog with explanations and immediate, one-field saves. Disabling a provider preserves and disables dependent settings; last-provider protection and other authorization rules remain intact.
  • Use consistent 24px page gutters with fluid widths, and present browser backup/restore flows in dashboard cards. Codify these decisions in the design guide and agent instructions.

Validation

  • Full workspace build, type checks, lint, prose/design checks, and unit tests pass.
  • Browser layout checks cover 19 dashboard pages from 320px through 2560px, plus provider configuration and disabled-setting retention.
  • Isolated Playwright coverage: setup/login, preset create/save/clone, node enrollment/remote launch, and encrypted backup/restore flow.

Compatibility notes

A live server-address change can disconnect the old control plane before its command reply arrives, so that plane may report a timeout even when the change succeeded. The legacy restartRequired response field remains unchanged for compatibility; the daemon applies the new address without requiring a restart. Credential-triggered redials use the existing reconnect backoff.

theogravity and others added 30 commits October 10, 2026 17:31
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ilters

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…op reading as foreign

Review round 1 of PR #359 (quote-verified):
- MAJOR: the hot-reload gate validated only the pinned key before swapping, but
  loadConfig proves fields are STRINGS, not that they open a socket. A beat that
  swapped in a parseable-but-un-dialable serverUrl ("not a url", "", a junk
  nodeWsUrl) dropped the healthy connection, and the WebSocket constructor's
  throw has no catch between the dial and process exit (runConnection reject ->
  the loop -> runDaemon -> cli) — the service respawned into the same death,
  inverting the seam's stated worst-case-is-status-quo for exactly the writer
  that motivated it. The gate now validates the whole-bundle DIAL TARGET
  (parseable, ws:/wss:) beside the pin, deduped through the same warn; the
  refusal is not sticky (next legitimate write reconnects). Belt for the
  loop's missing catch was considered and left out on purpose: the gate closes
  the real entry, and a loop-catch changes the unwind paths the tests pin.
- MINOR m1: persistLink mirrors its own §5 heal onto the live config but not
  onto liveIdentity, so every legacy self-heal logged a false "config changed;
  reconnecting" line at the loop seam (and correctness rode on R7's close
  ordering). The mirror now advances the tracker with it.
- NIT n1 (same seam): a stale concurrent reader (beat in flight while the
  loop seam swapped) could have re-applied the superseded bundle for a beat;
  the reader now yields when the identity moved under its read.

Tests: red on pre-fix, green on fix — the new un-dialable-serverUrl case and
test (c)'s new false-line assertion; suite 913/913, biome + verify-types clean.

Co-Authored-By: Claude <noreply@anthropic.com>
)

* docs(style): agent means the AI agent, and status copy writes from the reader's seat

The operator ruled (2026-10-10) that 'agent' confuses users when it names the
node's background program: the daemon is the daemon (or node daemon), and a
space-tight column header reads Version. STYLE.md gains the Daemon/Agent
glossary rows, the ban on 'node agent' and an 'Agent' column for the daemon,
and a new Status and problem sentences section: name what is true of the
reader's machine, then the action that remains; no protocol verbs, no
possessive riddles. The worked pair is the held-node line, before and after.
.claude/rules/design-system.md carries the two-line pointer so UI agents see
the ruling without opening the guide.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): the node daemon is the daemon, everywhere a person reads it

The word 'agent' now means only the AI agent a pane drives (ruling
2026-10-10, doctrine committed last commit). This applies it:

- SPA: the Nodes table's Agent column becomes Version (the detail page
  already said Node version); the update card says the daemon restarts
  itself, and the held-node line becomes the operator-approved sentence
  'The server will not accept this machine's Subshell daemon version.
  You can still update it from here.' replacing the refuse-its-agent
  original.
- Node CLI, setup questions, probe warnings, update refusals, the
  4406 exit line, maintenance/service/reset/env-forced messages: daemon,
  with the reader-seat rewrite where the old text described what the
  protocol layer did. 'KICKS' prose becomes 'disconnects'.
- The daemon's own log file becomes logs/daemon.log. No migration: the
  plane's agent_log_read, the loopback dashboard, status --json, reset,
  and the desktop app's reveal-a-log ladder all fall back to agent.log
  until the daemon has written under the new name; tests pin the ladder
  on both the node and desktop side. Wire field names (agentLog,
  agentLogPath, agentVersion, kind 'agent') are identifiers and stay.
- Desktop: app descriptions, the downgrade refusal, and the no-log hint.
- docs: paths.mdx and security.md name the daemon log (and the quoted
  Nodes-dialog string is corrected to the label the dialog actually
  shows); node agent in-repo docs follow the file rename.

Kept per the ruling: 'launchd agent' (Apple's term for server
supervision) and every AI-meaning agent (Add an Agent, the harness card,
Send prompt to agent?). Full suites green: node 905, server 3953, rust
fmt/clippy/test, design tokens, prose.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(cli-e2e): the reset scenario seeds and names daemon.log

The bun suite stubs every service seam, so the renamed log file and the
reworded reset line land invisible to it; the compiled scenario is the thing
that drives the real binary. It seeded logs/agent.log and grepped 'left the
agent', both of which this branch renames.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(web): 'node you can't see' reveals nothing more on hover

The rail header (and the home tiles) carried the invisible node's full id
as tooltip text, a kept-over reveal from the retired node pill whose point
was telling machines apart. An operator, shown the uuid, asked the obvious
question: if you can't see it, why is it shown? An id with no page behind
it names nothing to act on; title now equals label, and both consumers'
equal-title branch renders no tooltip. The cold arm keeps its reveal (a
short id becoming the full one) because there the machine IS the reader's,
merely not loaded yet.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): the review round's findings — transfer lines, re-register refusal, OpenAPI prose

Four minor, four nit, all quote-verified: the transfer refusals named the
daemon 'the agent on node X' (the double meaning this PR exists to kill,
sharpest where a harness reads the sentence about itself); the re-register
refusal told the wizard's reader a row 'cannot re-register as an agent';
node-assistant.md still named agent.log as the first rung; the 4409 line
said 'another subshell' for a daemon; the rollback lines kept the verb
'refused' the new STYLE section bans in status prose; 'agent node' in
shipped OpenAPI descriptions reads as the AI sense. The readable-path
docblock now names the one accepted switchover cost (a held poll offset the
startup burst outgrows between polls skips bytes until a reload), and the
Rust ladder test now also pins legacy-before-manager ordering.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): the second round's findings — the rollback line, the MCP wrap, the schema twins

All quote-verified. The successful-rollback log line still said 'the
control plane refused' where its two failure siblings became 'would not
accept'; the MCP wrap around the transfer-too-old error said 'the agent on
the target node' three times around the (already fixed) interpolated
server sentence; the transfer tool's own z.string description was the
schema twin of the OpenAPI line round one renamed; three comments still
promised the hover reveal the tooltip ruling removed; the 4406 tail asked
for 'a newer subshell'; the restart fallback note kept 'refused'; the
cli-e2e step title kept the old word beside its renamed log file; and the
desktop package.json line this PR edited still shipped an em dash.

The rollback line now has a test pin (daemon.test.ts, 4406 revert path):
'would not accept 9.9.9' present, 'the control plane refused' absent. The
4406 fixture strings now quote the server's real phrasing ('this node is').

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(changeset): the two SPA entries name @internal/server

@internal/server-web is on changesets' ignore list; the SPA ships embedded
in the server binary, and every prior SPA-only changeset (#356's stale-
detection entry among them) names @internal/server. An ignored key here
would ride NO release.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): the third round's findings — the no-reason fallback and the dashboard verbs

The 4406 revert fallback recorded 'the control plane refused it' into
update-failed.json, which the loopback dashboard prints raw and which the
rewritten rollback line interpolated — 'no reason was given' now, with a
test that closes 4406 WITHOUT a reason and pins both the record and the
log line. The dashboard's three error fallbacks ('The update was refused'
et al) became plain outcomes, matching the update.ts note round two fixed.
The log-file test restores the preload's config home instead of deleting
the variable, so a test added below it cannot resolve the developer's real
~/.config/subshell.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): the fourth round's findings — transfer_nodes' own description

The MCP tool's shipped description told harnesses (and the humans reading
them) that transfers run 'between two AGENT nodes', and one HTTP refusal
in the same route family said it plainly - while the refusal it quotes
about itself now says 'the daemon'. Both re-worded to machine nodes; the
route's own docblock follows its text.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): the fifth round's findings — the broken pin, the docs twins, and three strays

The transfer-route test pinned toContain('agent nodes'), a shorter
fragment than the message the last commit re-worded: it failed the moment
that shipped (9/9 now, on 'machine nodes'). The public MCP tools page
duplicated the server description's two sentences verbatim, so it moved
with them. Three OpenAPI prose sites join the swept class ('an agent
node' -> 'a machine node', 'an enrolled agent' -> 'an enrolled machine
node', per the models.ts and reregister precedent this PR set). The
Subshell Server app's reset confirmation - the app no sweep had visited -
says 'a node agent on this machine keep running'; that is the node
daemon. And the dashboard's maxBytes schema line says agent log beside a
file this PR renamed.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(changeset): the Server app's reset dialog rides desktop-server

The reset confirmation that gained 'node daemon' renders inside Subshell
Server's window; no entry named @internal/desktop-server, so the app would
have carried a wording change with no version.

Co-Authored-By: Claude <noreply@anthropic.com>

* style(rust): the two test edits in the shapes CI's rustfmt wants

cargo fmt --check on the runner re-wrapped an assert_eq! into one line and
re-split an assert! into three; the local toolchain accepted both prior
shapes, so rust:check stayed green while CI's fmt gate saw the diff. Content
unchanged - wrapping only.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(copy): apply conversational style across product and docs

* test(e2e): follow fixed copy and selected setup addresses

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(web): simplify the nodes table and show owners

* fix(web): remove the nodes actions column label

* test(e2e): check the nodes table and status dots
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(ui): explain unrestricted directories and edit permissions

* docs(ui): use approved directory rules wording
* docs(website): align copy with the content guide

* docs(website): distinguish hosting security and sharing

* fix(website): balance the self-hosted section layout

* fix(website): simplify hosting details and correct GitHub mark

* docs(website): remove redundant installation link
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
github-actions Bot and others added 14 commits October 11, 2026 23:55
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Explain disabled controls across the app

* fix: show update progress immediately and highlight service restrictions

* fix: address disabled-control review findings

* fix: address second review pass on disabled controls

Spell e-mail in the mobile sign-in reason, and neutralize the devices-strip
note that accused a still-loading entry of view-only access. Narrow the
design guide's Help text rule to inline explanations so the tooltip popup
amendment stops contradicting it, and mirror the split in the loaded rules
summary. Document the tooltip-latch remount tradeoff in all three button
copies, and drop the blocker paragraph's now-unreferenced id.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@theogravity theogravity changed the title fix(node): hot-reload re-registered credentials; Nodes dot + filters fix: reload node credentials and unify dashboard tables and settings Oct 12, 2026
@theogravity
theogravity merged commit 2b7da8e into main Oct 12, 2026
21 checks passed
@theogravity
theogravity deleted the fix/node-reregister-and-filters branch October 12, 2026 07:34
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant