Skip to content

feat(web): Host the web console in the daemon - #119

Merged
4ndreello merged 20 commits into
mainfrom
feat/web-daemon
Sep 25, 2026
Merged

4ndreello merged 20 commits into
mainfrom
feat/web-daemon

Conversation

@4ndreello

Copy link
Copy Markdown
Owner

review, setup, usage --web and ui now ask the daemon for the web console through a new web.ensure IPC method instead of each starting its own HTTP server.

Before this, every command bound its own server on 3100 (or an ephemeral port), so running review while setup was open meant two servers, two tokens and a port clash. The daemon already owns sessions and outlives the terminal, so it is the natural owner of a single console.

How it works:

  • WebSupervisor (src/daemon/web-supervisor.ts) runs at most one child, node dist/web/child.js --web-child, which prints a JSON handshake (port, token, build). Concurrent callers share one start. A different build or entry replaces the child (SIGTERM, then SIGKILL after 3 s). The daemon stops it first on shutdown.
  • The HTTP code stays in a child process on purpose: daemon code imports nothing from src/web or src/cli, and a test walks the import graph to keep it that way.
  • launchWebPage (src/cli/web-launch.ts) sends the local build id and child entry. If the daemon is down or answers any error other than WEB_LISTEN_FAILED, the CLI serves in-process as before and says so. A busy explicit --port exits 1.

Behavior changes worth a look:

  • /api/review now requires an absolute ?repo=; review drafts are keyed by repo. The ui nav's Review link has no repo and shows a hint instead.
  • web setup no longer requires a TTY; the check applies only to --tui.
  • /usage reads interval from the URL and /setup honours refresh=1, since one shared server can no longer bake per-command options into the page.

The spec, tasks and verifier report live in .specs/features/web-daemon/. The verifier passed on round 3 with 50/50 acceptance criteria covered and 19/19 injected mutants killed.

4ndreello and others added 20 commits September 24, 2026 21:20
Co-Authored-By: Claude <noreply@anthropic.com>
The web server is about to outlive the command that starts it, so API
routes now need the session cookie on every method and pages need the
token or the cookie. Tests send the cookie through a shared helper.

Co-Authored-By: Claude <noreply@anthropic.com>
A throwing or rejecting route used to escape the dispatcher. Now it gets
a JSON 500, or an ended response when headers were already sent, and the
server keeps serving.

Co-Authored-By: Claude <noreply@anthropic.com>
The setup API routes discarded their promises with `void`, so a
rejection outside the route's own try/catch became an unhandled
rejection and left the request hanging. Returning the promise lets the
dispatcher answer 500 instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Extract listenWebServer, which binds, creates the session security and
closes, with no signal handlers and no browser. The daemon's web child
needs this. startWebServer delegates to it and passes an optional
fallbackToEphemeral through for the in-process CLI fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
A shared web server cannot use its own cwd as the review root, so
/api/review now takes an absolute repo parameter and rejects a missing
or relative one. The page forwards repo on every request, shows a hint
without one, and keys drafts by repo so two repos never mix comments.
The in-process review command opens /review?repo=<cwd>.

Co-Authored-By: Claude <noreply@anthropic.com>
`usage --web --interval` will open a shared server by URL, so the
/usage page reads interval from its query instead of from process-local
page options. The page keeps its existing normalization.

Co-Authored-By: Claude <noreply@anthropic.com>
`setup --refresh` will open a shared server by URL, so the page itself
sends one catalog refresh after its initial load when the query has
refresh=1, instead of the command refreshing before it serves.

Co-Authored-By: Claude <noreply@anthropic.com>
The daemon needs to know when its web child runs code older than the
CLI calling it. The build id is the newest .js mtime under the dist
root, so any rebuild changes it.

Co-Authored-By: Claude <noreply@anthropic.com>
The daemon will host the console in a supervised child instead of in
its own event loop. The child serves the full console route table,
prints one JSON handshake line with its port, token and build, and
exits on stdin EOF or SIGTERM without waiting for in-flight requests.

Co-Authored-By: Claude <noreply@anthropic.com>
WebSupervisor keeps at most one web child. It shares a start in
flight, reads the JSON handshake line with a timeout, maps failures to
WEB_LISTEN_FAILED, WEB_START_FAILED and WEB_BAD_ENTRY, and restarts
the child when the caller's build or entry differs. The child's stderr
goes to logs/web-child.log and the daemon never imports web code.

Co-Authored-By: Claude <noreply@anthropic.com>
The daemon answers web.ensure by starting or reusing its supervised web
child and returns the base URL, port and token. Supervisor failures
become IPC errors with the same code and details, and shutdown sends
SIGTERM to the child before the session drain.

Co-Authored-By: Claude <noreply@anthropic.com>
launchWebPage ensures the daemon, asks it for the web child with the
local build and entry, then opens or prints the page URL and returns.
A busy explicit port fails with exit 1. Any other daemon error, or a
daemon that cannot start, falls back to serving the full console from
this process.

Co-Authored-By: Claude <noreply@anthropic.com>
ui opens / and review opens /review?repo=<cwd> through launchWebPage,
so both return to the shell instead of holding a server. The commander
"3100" defaults are gone so an omitted --port reaches the daemon as no
port. The route table tests now serve createUiRoutes and
createReviewRoutes directly; the opener-failure case lives in the
launcher tests.

Co-Authored-By: Claude <noreply@anthropic.com>
setup's web branch opens /setup through launchWebPage and returns, and
it no longer needs a TTY because it prints or opens a URL. Only --tui
still requires a terminal. --refresh becomes refresh=1 on the page URL,
so the command-side route override is gone, and an omitted --port no
longer defaults to 3100.

Co-Authored-By: Claude <noreply@anthropic.com>
usage --web resolves its filters against the caller's cwd and passes
the non-empty ones, plus by and interval, as the page query to
launchWebPage. The shared server no longer needs a process-local page
configuration, and the command returns to the shell.

Co-Authored-By: Claude <noreply@anthropic.com>
Describe web.ensure, its params, result and error codes, the web child
handshake and restart on a build change, the token and cookie flow,
and the in-process fallback. Verified with npm run build, the pty gate
and an isolated smoke run of review, setup, usage --web and ui.

Co-Authored-By: Claude <noreply@anthropic.com>
The verifier showed seven spec values could change without a failing
test. Tests now run against the real 5000 ms start and 3000 ms stop
timeouts, the default child entry, the child's own build id, and the
daemon.log lines through the daemon. They also cover the fallback for
WEB_START_FAILED and WEB_BAD_ENTRY, the transitive import boundary,
and an invalid --port on setup and usage. The daemon and child gain
small seams (spawnWebChild, distRoot) for this.

Co-Authored-By: Claude <noreply@anthropic.com>
Validation round 2 left four mutants alive: the build and entry
launchWebPage sends by default, the child's default build root, and an
import-boundary walker that skipped side-effect imports.

Co-Authored-By: Claude <noreply@anthropic.com>
The verifier passed round 3: 50 of 50 acceptance criteria have
discriminating evidence and all 19 injected mutants break a test.

Co-Authored-By: Claude <noreply@anthropic.com>
@4ndreello
4ndreello marked this pull request as ready for review September 25, 2026 01:00
@sonarqubecloud

Copy link
Copy Markdown

@4ndreello
4ndreello merged commit f76dd5e into main Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant