Commit Graph

15 Commits

Author SHA1 Message Date
Paul Payne
05b28cb584 Rename Castle -> Wild PC across the repo
Repo-side rename only (Phases 1-3 of the migration plan); the live box
(~/.castle, systemd units, /data/castle, domains) is a separate cutover.

- Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api),
  dist names, entry point `wildpc = wildpc_cli.main:main`.
- Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*.
- Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml ->
  wildpc.yaml, /data/castle -> /data/wildpc.
- Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc.
- Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend.
- Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated;
  redundant nested uv.lock files dropped (workspace root lock is authoritative).

Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds.
Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
2026-07-18 22:55:08 -07:00
Paul Payne
1767a0a304 Adds custom domains. 2026-07-12 17:29:34 -07:00
Paul Payne
e438f45b54 feat: deployment-scoped requires + UI editors, gateway nav, services filter
Model:
- Drop `requires` from ProgramSpec; requires is now deployment-only.
  Collapse Requirement to {ref, bind} with kind defaulting to "deployment"
  (drop the unused `version`). System-package preconditions stay on the
  program as `system_dependencies`, synthesized into {kind: system} for the
  functional check. relations.py/deploy.py read only the deployment's requires.
- create.py seeds a stack's substrate dependency onto the deployment, not the
  program. Docs + tests updated.

UI:
- Add a `requires` editor (useRequires) on service, job, and static detail
  pages — edit service→service deps (ref + optional bind env var).
- Restore the gateway route table "Open" column: a kind-aware in-app link to
  each route's deployment.
- Reformat the reach control: name baked into each option
  (localhost:PORT (local) / <name>.<domain> (internal) / <name>.<pubdomain> (public)).
- Services page: sort statics in with systemd services by name, add a
  search + kind-filter bar mirroring the programs page.
2026-07-06 04:00:24 -07:00
Paul Payne
07c02aa151 fix(api): editable-spec detail + PATCH-merge saves + shared gateway parser
Deployment/program edits could silently drop spec fields:

- GET /deployments/{name} served the runtime view (no reach/program/root/expose)
  for deployed deployments, so the dashboard round-tripped a lossy manifest and
  stripped fields on save (broke astro). Now serves the editable castle.yaml spec
  when the deployment is in config.
- _save_deployment and save_program replaced the whole object with client input.
  Now shallow-merge over the existing spec (omit = preserve, null = clear), so a
  partial/lossy save can't drop untouched fields.
- save_yaml rebuilt GatewayConfig from `port` only, wiping tls/domain/tunnel/
  cert_hook on a whole-file save. Now uses a shared parse_gateway() (also used by
  load_config) so gateway fields can't drift between the two.

Dashboard forms (ServiceFields/StaticFields) send explicit null to clear, per the
merge contract; adds exposure host-label helpers. Coverage: detail-serves-spec,
save round-trip, partial-patch-preserves, null-clears, program partial-patch,
parse_gateway, and config save/load round-trip.
2026-07-05 23:23:59 -07:00
Paul Payne
3c566540aa feat: raw-TCP exposure with castle-managed TLS + reach model
Expose raw-TCP services (postgres) by name at <name>.<domain>:<port> and
cut the gateway's ACME wildcard cert onto them so they present a trusted
cert. Replaces the proxy/public booleans with a single `reach` enum
(off|internal|public); legacy input still parses via derived accessors.

Core:
- expose.tcp{port,tls} + TlsSpec(material: pair|combined|off, reload)
- tls.py: materialize cert files from Caddy's wildcard, reconcile on
  renewal; `castle tls` CLI; optional cert_obtained events-exec hook
  (gateway.cert_hook, gated so a plugin-less Caddy still parses)
- apply waits (bounded) for the wildcard to issue before materializing so
  a fresh-node TLS service starts with its cert in place, then scopes
  materialization to the deployments being applied
- reach: internal|public requires an expose block (no silent no-op);
  public raw-TCP guarded until tunnel support lands
- chain.pem (${tls_ca}) is the issuer chain (leaf stripped), a real CA
  bundle distinct from cert.pem
- one shared ${...} resolver (resolve_placeholders) for env and container
  run fields; run.env now expands like volumes/args; $$ escapes a literal
- validate the generated Caddyfile before reloading the gateway so an
  invalid config never degrades routing

Docs: docs/tcp-exposure.md. Tests cover reach/expose validation,
placeholder expansion + escape, issuer-chain material, TLS materialize.
2026-07-04 23:04:26 -07:00
Paul Payne
6929bd89d3 Detail pages: rename the 'Runs' field to 'Launch' (matches the launcher concept)
The field showing a deployment's launcher + target is now labelled 'Launch'
(consistent with launcher/manager vocabulary) on ServiceDetail and the Service/
Job/Create field editors.
2026-07-01 13:35:02 -07:00
Paul Payne
01505328ad Dashboard: programs list deployments; Tools page; statics on Services; editable launcher
- Programs page: each card lists the program's deployments (name · kind) instead
  of a single kind badge; kind-filter chips removed (nav is the kind lens now).
  protonmail honestly shows tool + job.
- New Tools nav page (Wrench) — programs with a tool deployment (/programs?kind=tool).
- Services page now shows statics too: /services returns service + static, and a
  static renders as a caddy-served card (KindBadge, served URL, no start/stop).
- Program detail: DeploymentsSection + deleteConfirm rebuilt over program.deployments;
  header drops the (now nonexistent) program kind.
- ServiceFields / JobFields: launcher is now an editable select (python|command|
  container|compose|node), persisted into run.launcher via the normal save.

clean pnpm build + tsc --noEmit.
2026-07-01 12:32:09 -07:00
Paul Payne
317232ca6a Manager-first deployment model: split runner, merge service/job, frontend→static
Replace the conflated `runner` axis with two orthogonal ones: `manager`
(systemd|caddy|path|none) — who supervises/realizes a deployment — and, for
systemd only, a nested `launcher` (python|command|container|compose|node) — how
the process starts. ServiceSpec and JobSpec collapse into one manager-
discriminated DeploymentSpec union (Systemd/Caddy/Path/Remote); the services/
and jobs/ config dirs collapse into one deployments/ dir. The human "kind"
(service|job|tool|static|reference) is fully derived (kind_for), never stored —
the frontend kind is renamed static. behavior is gone.

- core: DeploymentSpec union + LaunchSpec + kind_for; legacy-aware loader
  normalizes old runner shapes; CastleConfig.deployments with derived
  services/jobs/tools views; registry.Deployment carries manager/launcher/kind.
- cli: service/job/tool as filtered views + a deployment group; --behavior→--kind,
  create --runner→--launcher; lifecycle dispatches over config.deployments.
- castle-api: /deployments primary with /services,/jobs as views; summaries
  derive kind; PUT/DELETE /config/deployments/{name} (services/jobs aliased).
- app: KindBadge, frontend→static everywhere, pick-a-kind creation wizard,
  per-kind config editors.
- docs: single deployments/ layout, manager/launcher, static kind throughout.

Live migration verified byte-identical: regenerated Caddyfile and every unit
ExecStart line unchanged, so nothing restarted. Suites: core 124, cli 25,
castle-api 55; dashboard build + type-check clean.
2026-07-01 10:23:03 -07:00
Paul Payne
770574f575 Flatten proxy config to a bool (drop the caddy key)
CaddySpec had only `enable` left (path_prefix/host removed; extra_snippets was
unused), so `proxy: { caddy: {} }` was three levels of nesting for a checkbox —
and it invited drift (supabase silently reverted to proxy.caddy.host). Replace it
with a plain `ServiceSpec.proxy: bool`.

- Model: remove ProxySpec/CaddySpec; `proxy: bool = False`. Readers simplified to
  `bool(svc.proxy)` (generator, castle-api summaries, CLI info).
- CLI create / dashboard editor + create form: write `proxy: true` / a checkbox.
- Configs migrated to `proxy: true` (and supabase's stale proxy.caddy.host block
  removed).
- Docs (registry, dns-and-tls, design, stack guides) + tests/fixtures updated;
  also fixed lingering path-model staleness in the stack guides.
2026-06-30 21:23:14 -07:00
Paul Payne
15dc8ef6f7 Dashboard UI: subdomain checkbox, drop proxy path/host
Follow the backend to the subdomain-only model:
- Service editor + create form: replace the Proxy path / Proxy host text fields
  with a single "Expose" checkbox (writes proxy: { caddy: {} } or removes proxy).
- ServiceSummary/DeploymentSummary type: proxy_path/proxy_host → subdomain.
- ServiceCard / ServiceDetail: show the subdomain, linking to
  <subdomain>.<domain> via a new subdomainUrl() helper (domain derived from the
  dashboard's own host; null on a bare host / off mode).
2026-06-30 20:55:38 -07:00
Paul Payne
a3a862b63f feat: Add type-check script and enhance UI for better text handling 2026-06-27 12:46:15 -07:00
Paul Payne
ba60019a52 ui: field help text + editable program commands
- Add a 'hint' to the shared Field/TextField; annotate the config forms with
  short, accurate descriptions of each field:
  - program: source (working copy castle builds from), behavior (tool/daemon/
    frontend), stack (optional toolchain template), repo/ref (for clone),
    system deps (listed for reference, NOT auto-installed), commands.
  - service: port (listened-on, health-checked, proxied; map via ${port}),
    health path, proxy path vs proxy host, runs.
  - job: schedule (cron → systemd timer), runs.
- ProgramFields: the Commands section is now editable (build/test/lint/
  type-check/run), so a program with no stack can declare its own dev verbs
  by hand — a declared command overrides the stack default. Verified the
  commands/build shape round-trips through the config API.

App builds clean.
2026-06-15 08:50:03 -07:00
Paul Payne
7314b5cddb refactor(env): explicit defaults.env with placeholders; drop auto-injection
A service/job's env is now exactly its defaults.env — castle injects no hidden
convention vars. Values support ${port}/${data_dir}/${name} placeholders
(resolved at deploy, alongside ${secret:…}), so a program's own env var names
map to castle's computed values without hardcoding.

Why: the auto-injected <PREFIX>_PORT/<PREFIX>_DATA_DIR were a guess at the
program's env names — right for castle-scaffolded services, dead weight for
adopted ones (lakehouse carried two dead vars; notification-bridge/backup jobs
too). They also weren't visible in the config editor (computed at deploy), which
was the source of the 'four env vars but the UI shows none' mystery.

- core: resolve_env_vars gains a context (${port}/${data_dir}/${name});
  deploy builds env from defaults.env only — no <PREFIX>_* injection, no
  port_env. Removed the port_env field and the dead _env_prefix helper.
- cli: 'service/job create' gains repeatable --env KEY=VALUE (replaces
  --port-env); 'program create' scaffolds <PREFIX>_PORT/_DATA_DIR: ${…} for new
  daemons.
- app: removed the 'Port env' field; the Environment editor (defaults.env) is
  the single place, with a placeholder hint.
- live migration: central-context/castle-api/power-graph/protonmail mapped their
  real vars explicitly; lakehouse → just LAKEHOUSED_DAEMON_PORT: ${port}, data
  stays in ~/.lakehoused. Verified all services healthy on their ports, dead
  vars gone, zero failed units.
- docs: registry.md/design.md/stack guides + findings updated to the explicit
  model.

core 94 / cli 24 / api 52 green; ruff + app build clean.
2026-06-14 17:06:46 -07:00
Paul Payne
82f12c9d61 Remove buttons: confirm + block program removal with active deployments
The Stage 2 editor split dropped the old confirm() — Remove program/service/job
deleted immediately on click. Restored confirmation on all three (FormFooter
window.confirm with a per-type message).

Removing a program that still has services/jobs referencing it would orphan
their program: ref. Now blocked:
- API: DELETE /config/programs/{name} → 409 'Programs with active jobs or
  services cannot be removed. Delete these first: …' (authoritative).
- App: ProgramFields shows the blocked reason instead of a Remove button when
  the program has deployments.
- CLI: castle delete refuses a program with referencing deployments not named
  the same (which would survive the delete dangling).

Verified live: DELETE program lakehouse → 409. api 52 / cli 24 green; build clean.
2026-06-14 15:52:57 -07:00
Paul Payne
73698fafe7 Stage 2: App — split the one editor into typed program/service/job forms
The single service-shaped DeploymentFields was reused for all three sections,
so the program page bled service fields (port/proxy/health) and couldn't edit
program config, and the job page couldn't edit its schedule. Replaced it with:

- ProgramFields  — description, source, stack, behavior, version, repo/ref,
  system deps (+ read-only commands). Edits the program catalog entry.
- ServiceFields  — run target, port, port_env, health, proxy path + host,
  env/secrets. Prefills correctly now (Stage 1) and round-trips through PUT.
- JobFields      — schedule, run target, env/secrets.

ConfigPanel selects the form by section. Shared Field/TextField/useEnvSecrets/
FormFooter helpers in detail/fields.tsx. Retired DeploymentFields. run editing
only touches python/command specs (container/node/remote run blocks preserved).

Verified: GET service manifest → PUT back validates 200. App type-checks/builds.
2026-06-14 15:34:07 -07:00