feat: castle UX fixes from the lakehouse adoption test
Driven by adopting lakehouse (an existing daemon that bundles its own SPA) — see docs/findings-lakehouse.md. #3 deploy reloads the gateway. castle deploy regenerated the Caddyfile but left the running Caddy on the old config, so new proxy routes were silently dead. Deploy now reloads the gateway when it's running. #1 castle expose <program>. Turns an adopted program into a service (run/port/health/proxy/systemd) in one command — the missing daemon-to-service step. Flags: --port --health --path --run --port-env --host --no-proxy. #2 port_env mapping. A service can declare expose.http.internal.port_env so castle sets the env var the program actually reads (e.g. lakehoused reads LAKEHOUSED_DAEMON_PORT, not castle's convention LAKEHOUSE_PORT). Castle now genuinely drives an adopted daemon's bind port. #4a auto-base for react-vite. The build passes VITE_BASE = the gateway serve prefix (/<name>/, or / for castle-app); vite.config reads it. A castle-built frontend now works at its subpath with no hand-tuned base. castle-app's vite.config updated as the reference. #4b host-based routing. proxy.caddy.host routes a whole hostname to the backend root via a host matcher inside the :9000 site, so a root-based SPA (base="/") serves unchanged — the fix for proxying an app castle can't rebuild. Caddyfile now emits 'auto_https off' (HTTP-only gateway on a non-standard port). Nit: activate skips the editable reinstall when the tool is already on PATH. Tests: core 94, cli 24, api 52; ruff + app build clean. Verified live: lakehouse runs under systemd, API at /lakehouse, full UI (SPA boots) via host routing.
This commit is contained in:
87
docs/findings-lakehouse.md
Normal file
87
docs/findings-lakehouse.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Findings — adopting `lakehouse` as a castle service
|
||||
|
||||
A completeness/UX test: take an existing daemon (`/data/repos/lakehouse`, a
|
||||
FastAPI `lakehoused` server on port 8420 that bundles its own SPA, normally
|
||||
started by a bespoke `lakehouse start`) and bring it fully under castle —
|
||||
systemd-managed, health-checked, proxied, on the dashboard.
|
||||
|
||||
## What worked cleanly
|
||||
|
||||
- `castle add` auto-detected `behavior: daemon` + `stack: python-fastapi` from
|
||||
the pyproject (fastapi/uvicorn in deps).
|
||||
- Adding the service via the config API (`PUT /config/services/lakehouse`)
|
||||
validated and persisted correctly.
|
||||
- `castle deploy lakehouse` produced a correct systemd unit, registry entry,
|
||||
and Caddy route, and auto-installed the editable package.
|
||||
- `castle service enable lakehouse` started it and enabled it on boot.
|
||||
- Health probe (direct `127.0.0.1:8420`) and dashboard data
|
||||
(program / service / deployment / `active`) were all correct.
|
||||
|
||||
## Gaps found
|
||||
|
||||
### #1 — No CLI/app path to create a service from an adopted daemon (High)
|
||||
|
||||
`castle add` writes a `daemon`-behavior **program**, but there is no command to
|
||||
turn it into a running **service** (port, health, proxy, systemd). We had to
|
||||
hand the service block to the config API. Worse: `castle install <daemon>` on a
|
||||
program with no service silently falls through to the *tool* install path
|
||||
(`uv tool install`) instead of running it — a dead end.
|
||||
|
||||
**Fix:** `castle expose <program> [--port --health --path]` to scaffold a
|
||||
service entry from an existing program. (App needs an equivalent "add service"
|
||||
flow — tracked separately.)
|
||||
|
||||
### #2 — castle can't actually configure an adopted program's port / data dir (Medium)
|
||||
|
||||
The generated unit injects castle's convention vars `LAKEHOUSE_PORT=8420` and
|
||||
`LAKEHOUSE_DATA_DIR=…`, but `lakehoused` reads **`LAKEHOUSED_DAEMON_PORT`** and
|
||||
its own storage paths — it ignores both. The port "worked" only because 8420 is
|
||||
the daemon's built-in default and we declared the same value. Castle's
|
||||
convention assumes a castle-aware program; an adopted one doesn't read
|
||||
`<PREFIX>_PORT`.
|
||||
|
||||
**Fix:** let a service declare which env var carries the port, e.g.
|
||||
`expose.http.port_env: LAKEHOUSED_DAEMON_PORT`, so castle sets *that* to the
|
||||
configured port and genuinely drives the bind. (`defaults.env` is the manual
|
||||
workaround.)
|
||||
|
||||
### #3 — `castle deploy` regenerates the Caddyfile but never reloads Caddy (High, bug)
|
||||
|
||||
Deploy wrote the new `/lakehouse` route to the Caddyfile on disk but didn't
|
||||
reload the running gateway, so every `/lakehouse/*` request fell through to the
|
||||
castle-app catch-all and returned the dashboard's `index.html` (a misleading
|
||||
`200`). `castle service enable` doesn't reload either, and the deploy output
|
||||
never mentions `castle gateway reload`.
|
||||
|
||||
**Fix:** `castle deploy` (and `service enable`) reload the gateway when proxy
|
||||
routes changed.
|
||||
|
||||
### #4 — Path-prefix proxying breaks root-based SPAs (Medium, architectural)
|
||||
|
||||
`lakehoused`'s bundled webapp is built with Vite `base: "/"` — its `index.html`
|
||||
references assets at absolute `/assets/…`. Proxied at `/lakehouse/`, the browser
|
||||
requests `/assets/…` (no prefix), which hits the castle-app catch-all → the UI
|
||||
can't boot. The API works through the gateway; the UI only works directly at
|
||||
`:8420/`.
|
||||
|
||||
Two distinct cases, because Vite bakes `base` in at **build** time (a runtime
|
||||
env in the unit is too late to rebase a pre-built bundle):
|
||||
|
||||
- **Case A — frontends castle builds** (the `react-vite` stack, served by
|
||||
`file_server`). Castle runs `pnpm build` *and* knows the serve prefix, so it
|
||||
can pass `--base=/<prefix>/` automatically. Today `power-graph-app` only works
|
||||
because its base was hand-set to match.
|
||||
**Fix:** derive the build `--base` from the serve path prefix.
|
||||
- **Case B — adopted daemons that self-serve a root SPA** (lakehouse). Castle
|
||||
never runs their build, so it can't pass `--base`. The clean answer is
|
||||
**host-based routing** (`lakehouse.civil.lan → :8420`) so `base: "/"` stays
|
||||
valid — also a generally useful castle capability.
|
||||
**Fix:** support proxy-by-hostname in the Caddyfile generator + manifest.
|
||||
|
||||
### Nits
|
||||
|
||||
- **Redundant editable reinstall:** `castle deploy` installs the package, then
|
||||
`castle install`/activate may `uv tool install` it again.
|
||||
- **Entry-point discovery:** the service's `run.program` had to be `lakehoused`,
|
||||
not `lakehouse` — the package ships two console scripts and castle gives no
|
||||
hint which is the server. Surfacing a package's `[project.scripts]` would help.
|
||||
Reference in New Issue
Block a user