Commit Graph

9 Commits

Author SHA1 Message Date
Paul Payne
fa65ae3d1c Dashboard: public routes link to their public URL; drop editable behavior field
- Gateway route table: a public route's address now links to its public URL
  (green, with the tunnel icon) instead of the internal one.
- Program detail: remove the editable `behavior` selector — behavior is a derived
  label now and drives no logic, so editing it was meaningless (the stored value
  is preserved on save via the manifest spread).
2026-07-01 08:35:37 -07:00
Paul Payne
e9b197756b Dashboard: editable gateway settings + public-exposure panel
Surfaces the DNS/Cloudflare/public state that previously required editing
castle.yaml + curl:

- API: GET /gateway now returns domain, public_domain, tunnel_id,
  tunnel_connected, and a public_url per public route; new PUT /gateway/config
  edits tls/domain/public_domain/tunnel_id in castle.yaml (deploy to apply).
  save_config now persists public_domain/tunnel_id (previously dropped).
- UI: a GatewaySettings row in the Gateway panel shows TLS / domain / public
  (tunnel up/down + public_domain) with inline edit; each public route gets a
  green "public" badge linking to its <name>.<public_domain> URL.
2026-07-01 08:29:36 -07:00
Paul Payne
f14ef3f40b Gateway panel: link route addresses (open in new tab)
Each route address is a subdomain label now, so render it as a link to its full
https://<subdomain>.<domain> URL (via subdomainUrl), opening in a new tab. Falls
back to plain text on a bare host (off mode) where no subdomain URL exists.
2026-06-30 21:08:23 -07:00
Paul Payne
a022a136fe Retire gateway.tls=internal; host routes use a bare subdomain label
acme mode fully replaced the internal-CA path (which required installing a private
CA on every device — the exact pain, esp. on Android, that acme avoids). Remove
`internal` entirely and simplify how services declare host routes.

- Generator (caddyfile.py): drop the `tls internal` branch — modes are now
  `off` | `acme`. In acme mode the published subdomain is the **first DNS label**
  of `proxy.caddy.host` (a bare `claw`, or a legacy `claw.civil.lan`, both →
  `claw.<domain>`), so services stay domain-agnostic and the declared value is
  authoritative again (no more silent service-name override). Shared
  `_host_matcher_block` reused by off-mode and the acme wildcard site.
- castle-api: delete `GET /gateway/ca.crt`, `_gateway_ca_pem`, `_ca_fingerprint`
  and the now-unused imports; drop `ca_fingerprint` from `GatewayInfo` (keep
  `tls`).
- Dashboard: remove the CA-cert download button + unused imports; drop
  `ca_fingerprint` from the `GatewayInfo` type.
- Tests: replace TestCaddyfileTlsInternal with an off-mode class (keeps the
  runner-agnostic host-route coverage); acme tests assert first-label derivation
  incl. label-wins-over-service-name; drop the castle-api CA-endpoint test.
- Docs: registry.md + dns-and-tls.md — two-mode tables (off|acme), remove the
  internal sections/CA-download, document the bare-label host convention; note a
  domain-less node stays on `off`.
2026-06-30 19:55:17 -07:00
Paul Payne
fc5802d823 Add CA download to dashboard: GET /gateway/ca.crt + Gateway-panel button
When the gateway serves internal-CA HTTPS (gateway.tls=internal), other devices
must trust Caddy's root CA to accept *.lan HTTPS. Surface that from the dashboard
instead of hand-copying root.crt around.

- API: GET /gateway/ca.crt returns the public root cert (PEM) as a download,
  sourced from Caddy's admin API (/pki/ca/local — matches the running gateway)
  with an on-disk root.crt fallback. 404 unless tls=internal. GatewayInfo gains
  `tls` and `ca_fingerprint` (SHA-256, for out-of-band verification). The CA
  private key is never exposed — only the public root.
- UI: a "CA cert" button in the Gateway panel, shown only when tls=internal,
  linking to the download with the fingerprint in its tooltip.
- Tests for the tls=off defaults and the 404-without-internal-tls path; docs note
  the button + endpoint.
2026-06-29 08:26:15 -07:00
Paul Payne
c40f84104d feat: unified gateway route table (static · proxy · remote)
The gateway does two things — reverse-proxy services and serve static
frontends — but the dashboard/API route table only ever showed the proxy
routes, so 'serving a frontend' and 'proxying a service' looked like unrelated
features and castle-app/power-graph-app were invisible in the route view.

Now there's one concept: a route maps an address (path '/foo' or host 'foo.lan')
to a target of one kind — static (a built dist served by file_server), proxy
(a local service port), or remote (a service on another node).

- core: caddyfile.py gains compute_routes() — the single source of truth for
  the route list; generate_caddyfile_from_registry renders it (output byte-for-
  byte identical, verified against the live Caddyfile). Adds a GatewayRoute
  dataclass.
- api: GatewayRoute model → {address, kind, target, name, node}; GET /gateway
  builds from compute_routes (incl. config for static frontends + mesh for
  remote), so the table matches what Caddy actually does.
- app: Gateway panel shows Address · Kind · Target for every route (static
  frontends + host routes now appear); program detail shows 'Reachable at
  /foo/ · served (static)' for static frontends.
- cli: 'castle gateway status' prints the full route table.
- docs: registry.md/design.md/CLAUDE.md describe routes as one concept with
  three target kinds.

core 94 / cli 24 / api 52 green; ruff + app build clean; Caddyfile unchanged.
2026-06-14 17:48:35 -07:00
Paul Payne
0515faadfd refactor(app): rename Component* UI widgets to accurate domain names
The dashboard widgets were named after the old 'component' concept. Renamed
each to what it actually renders:

- ComponentCard  → ServiceCard      (ServiceSummary, /services/, service actions)
- ComponentTable → ProgramTable     (ProgramSummary[], the program catalog)
- ComponentEditor→ DeploymentEditor (DeploymentDetail, the unified type)
- ComponentFields→ DeploymentFields (AnyDetail)
- AddComponent   → AddDeployment

Also purged the domain term 'component' from the rest of the app: the
useComponent hook → useDeployment, ConfigPanel + the three detail pages'
`component` prop/var → `deployment`, and the gateway/node table headers
(Component → Program / Deployment). Props renamed to match (component →
service / program / deployment). The React UI directory app/src/components/
keeps its conventional name.

App type-checks and builds clean.
2026-06-14 12:13:41 -07:00
Paul Payne
3f3b88f17b refactor: Align vocabulary — component → program / deployment
Canonical terms (see docs/registry.md glossary):
- program: any catalog entry (tool/daemon/frontend) — the software
- service / job: how a program is deployed (systemd .service / .timer)
- deployment: umbrella for the unified service+job+program view

Changes:
- core: ServiceSpec/JobSpec component: → program: (component accepted as
  back-compat validation_alias); DeployedComponent → Deployment.
- api: ComponentSummary/Detail → DeploymentSummary/Detail; GET /components
  → /deployments; GatewayRoute.component → program; GatewayInfo
  .component_count → deployment_count; ServiceActionResponse/action keys
  component → program.
- app: matching type renames, /components → /deployments hook, route
  /component/:name → /deployment/:name, GatewayRoute.program.
- docs: component-registry.md → registry.md (+ canonical glossary);
  CLAUDE.md endpoint list refreshed (drop removed /tools, add typed
  program/service/job + config routes).

Tests: core 92, cli 24, api 52 green; app build clean; ruff clean.
2026-06-14 11:05:22 -07:00
Paul Payne
3343e955fd feat: Implement multi-node support with MQTT and mDNS for service discovery and coordination 2026-02-23 02:30:12 -08:00