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.
259 lines
7.6 KiB
Python
259 lines
7.6 KiB
Python
"""Response models for the dashboard API."""
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class SystemdInfo(BaseModel):
|
|
"""Systemd unit information for a managed component."""
|
|
|
|
unit_name: str
|
|
unit_path: str
|
|
timer: bool = False
|
|
|
|
|
|
class DeploymentSummary(BaseModel):
|
|
"""Summary of a single component."""
|
|
|
|
id: str
|
|
category: str | None = None # "program", "service", or "job"
|
|
description: str | None = None
|
|
kind: str | None = None # derived: service|job|tool|static|reference
|
|
stack: str | None = None
|
|
manager: str | None = None # systemd|caddy|path|none
|
|
launcher: str | None = None # python|command|container|compose|node (systemd only)
|
|
port: int | None = None
|
|
health_path: str | None = None
|
|
subdomain: str | None = None # exposed at <subdomain>.<gateway.domain>, else None
|
|
managed: bool = False
|
|
systemd: SystemdInfo | None = None
|
|
version: str | None = None
|
|
source: str | None = None
|
|
repo: str | None = None
|
|
ref: str | None = None
|
|
commands: dict[str, list[list[str]]] | None = None
|
|
system_dependencies: list[str] = []
|
|
schedule: str | None = None
|
|
installed: bool | None = None
|
|
active: bool | None = None # uniform lifecycle state (on PATH / running / served)
|
|
enabled: bool = True # declared desired state; `apply` converges to it
|
|
node: str | None = None
|
|
|
|
|
|
class DeploymentDetail(DeploymentSummary):
|
|
"""Full detail for a single component, including raw manifest."""
|
|
|
|
manifest: dict
|
|
|
|
|
|
class ServiceSummary(BaseModel):
|
|
"""Summary of a service — a systemd daemon OR a caddy-served static site.
|
|
|
|
Both are "services" (exposed, URL-reachable things); `kind`/`manager`
|
|
distinguish them (service+systemd vs static+caddy).
|
|
"""
|
|
|
|
id: str
|
|
description: str | None = None
|
|
stack: str | None = None
|
|
kind: str | None = None # service | static
|
|
manager: str | None = None # systemd | caddy
|
|
launcher: str | None = None # python|command|container|compose|node (systemd only)
|
|
run_target: str | None = None # what it runs: program name, argv, image, …
|
|
port: int | None = None
|
|
health_path: str | None = None
|
|
subdomain: str | None = None # exposed at <subdomain>.<gateway.domain>, else None
|
|
managed: bool = False
|
|
systemd: SystemdInfo | None = None
|
|
program: str | None = None # the program this deployment references, if any
|
|
source: str | None = None
|
|
enabled: bool = True # declared desired state; `apply` converges to it
|
|
node: str | None = None
|
|
|
|
|
|
class ServiceDetail(ServiceSummary):
|
|
"""Full detail for a service, including raw manifest."""
|
|
|
|
manifest: dict
|
|
|
|
|
|
class JobSummary(BaseModel):
|
|
"""Summary of a job (scheduled task)."""
|
|
|
|
id: str
|
|
description: str | None = None
|
|
stack: str | None = None
|
|
launcher: str | None = None # python|command|container|compose|node
|
|
run_target: str | None = None # what it runs: program name, argv, …
|
|
schedule: str | None = None
|
|
managed: bool = False
|
|
systemd: SystemdInfo | None = None
|
|
program: str | None = None # the program this deployment references, if any
|
|
source: str | None = None
|
|
enabled: bool = True # declared desired state; `apply` converges to it
|
|
node: str | None = None
|
|
|
|
|
|
class JobDetail(JobSummary):
|
|
"""Full detail for a job, including raw manifest."""
|
|
|
|
manifest: dict
|
|
|
|
|
|
class DeploymentRef(BaseModel):
|
|
"""A reference to one of a program's deployments (name + its derived kind)."""
|
|
|
|
name: str
|
|
kind: str # service | job | tool | static | reference
|
|
|
|
|
|
class ProgramSummary(BaseModel):
|
|
"""Summary of a program (software catalog entry).
|
|
|
|
A program has NO kind of its own — it *has deployments*, each with a kind
|
|
(a program can be a tool AND a job). `deployments` is that list.
|
|
"""
|
|
|
|
id: str
|
|
description: str | None = None
|
|
stack: str | None = None
|
|
version: str | None = None
|
|
source: str | None = None
|
|
repo: str | None = None
|
|
ref: str | None = None
|
|
commands: dict[str, list[list[str]]] | None = None
|
|
system_dependencies: list[str] = []
|
|
installed: bool | None = None
|
|
active: bool | None = None # uniform lifecycle state (on PATH / running / served)
|
|
actions: list[str] = []
|
|
deployments: list[DeploymentRef] = [] # this program's deployments (name + kind)
|
|
node: str | None = None
|
|
|
|
|
|
class ProgramDetail(ProgramSummary):
|
|
"""Full detail for a program, including raw manifest."""
|
|
|
|
manifest: dict
|
|
|
|
|
|
class ToolStatusModel(BaseModel):
|
|
"""One host toolchain a stack needs, and whether it's present where used."""
|
|
|
|
command: str
|
|
purpose: str
|
|
phase: str # "run" | "build" | "both"
|
|
present: bool
|
|
install_hint: str
|
|
version: str | None = None
|
|
|
|
|
|
class StackStatusModel(BaseModel):
|
|
"""A stack's dependency health — its tools + who uses it. Powers the Stacks page."""
|
|
|
|
name: str
|
|
tools: list[ToolStatusModel]
|
|
programs: list[str]
|
|
deployments: list[str]
|
|
verbs: list[str]
|
|
has_enabled_deployment: bool
|
|
in_use: bool
|
|
ok: bool
|
|
|
|
|
|
class HealthStatus(BaseModel):
|
|
"""Health status of a single component."""
|
|
|
|
id: str
|
|
status: str # "up", "down", "unknown"
|
|
latency_ms: int | None = None
|
|
|
|
|
|
class StatusResponse(BaseModel):
|
|
"""Aggregated health status for all exposed components."""
|
|
|
|
statuses: list[HealthStatus]
|
|
|
|
|
|
class GatewayRoute(BaseModel):
|
|
"""One gateway route: a public address mapped to a target.
|
|
|
|
kind is `static` (Caddy serves a built dir), `proxy` (reverse-proxy a local
|
|
service), or `remote` (reverse-proxy another node). address is a path prefix
|
|
(`/foo`) or a host (`foo.lan`); target is the serve dir or `host:port`.
|
|
"""
|
|
|
|
address: str
|
|
kind: str
|
|
target: str
|
|
name: str | None = None
|
|
node: str
|
|
# Public exposure via the tunnel: the public URL, or None if this route is
|
|
# LAN-only. Set when the backing service has `public: true`.
|
|
public_url: str | None = None
|
|
|
|
|
|
class GatewayInfo(BaseModel):
|
|
"""Gateway configuration summary."""
|
|
|
|
port: int
|
|
hostname: str
|
|
deployment_count: int
|
|
service_count: int
|
|
managed_count: int
|
|
routes: list[GatewayRoute] = []
|
|
# TLS mode: None/"off" → HTTP-only; "acme" → Let's Encrypt wildcard (publicly
|
|
# trusted, no client CA setup) for host routes.
|
|
tls: str | None = None
|
|
# Routing/exposure config (editable from the dashboard).
|
|
domain: str | None = None # acme zone → <service>.<domain>
|
|
public_domain: str | None = None # tunnel zone → <service>.<public_domain>
|
|
tunnel_id: str | None = None
|
|
tunnel_connected: bool = False # cloudflared service active
|
|
|
|
|
|
class GatewayConfigRequest(BaseModel):
|
|
"""Editable gateway settings (saved to wildpc.yaml; deploy to apply)."""
|
|
|
|
tls: str | None = None
|
|
domain: str | None = None
|
|
public_domain: str | None = None
|
|
tunnel_id: str | None = None
|
|
|
|
|
|
class NodeSummary(BaseModel):
|
|
"""Summary of a discovered node in the mesh."""
|
|
|
|
hostname: str
|
|
gateway_port: int
|
|
gateway_domain: str | None = None # acme domain → dashboard at wildpc.<domain>
|
|
deployed_count: int
|
|
service_count: int
|
|
is_local: bool = False
|
|
online: bool = True
|
|
is_stale: bool = False
|
|
last_seen: float | None = None
|
|
|
|
|
|
class NodeDetail(NodeSummary):
|
|
"""Full detail for a node, including its deployed components."""
|
|
|
|
deployed: list[DeploymentSummary] = []
|
|
|
|
|
|
class MeshStatus(BaseModel):
|
|
"""Current state of the mesh coordination layer."""
|
|
|
|
enabled: bool = False
|
|
connected: bool = False
|
|
nats_url: str | None = None
|
|
mdns_enabled: bool = False
|
|
peer_count: int = 0
|
|
peers: list[str] = []
|
|
|
|
|
|
class ServiceActionResponse(BaseModel):
|
|
"""Response from a service management action."""
|
|
|
|
program: str
|
|
action: str
|
|
status: str
|