feat: add ComponentGrid and ComponentTable components for displaying components in grid and table formats

feat: implement HealthBadge and RoleBadge components for displaying health status and roles

feat: create LogViewer component for streaming logs of services

feat: develop SecretsEditor for managing secrets with CRUD operations

feat: introduce ToolCard component for displaying tool information

style: add global CSS variables and styles for consistent theming

feat: set up API client for handling requests to the backend

feat: implement hooks for fetching components, statuses, and tools

feat: create routes for dashboard, component details, and tools

feat: add service management endpoints for starting, stopping, and restarting services

feat: implement event bus for handling real-time updates via SSE

feat: create health check and logs endpoints for monitoring services

feat: add tests for health and tools endpoints to ensure functionality

chore: update project configuration files and dependencies for better development experience
This commit is contained in:
2026-02-21 01:23:37 -08:00
parent f99c2dad86
commit 930bc601b7
63 changed files with 154 additions and 157 deletions

View File

@@ -1,5 +1,5 @@
{ {
"name": "dashboard", "name": "app",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",

31
castle-api/CLAUDE.md Normal file
View File

@@ -0,0 +1,31 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working
with code in this repository.
## Overview
castle-api is a FastAPI service. Castle API.
## Commands
```bash
uv sync # Install dependencies
uv run castle-api # Run service (port 9020)
uv run pytest tests/ -v # Run tests
uv run ruff check . # Lint
uv run ruff format . # Format
```
## Architecture
- `src/castle_api/config.py` — Settings via pydantic-settings, env prefix `CASTLE_API_`
- `src/castle_api/main.py` — FastAPI app, lifespan, health endpoint
- `tests/` — pytest with TestClient fixtures
## Configuration
Environment variables with `CASTLE_API_` prefix:
- `CASTLE_API_DATA_DIR` — Data directory (default: ./data)
- `CASTLE_API_HOST` — Bind host (default: 0.0.0.0)
- `CASTLE_API_PORT` — Port (default: 9020)

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "dashboard-api" name = "castle-api"
version = "0.1.0" version = "0.1.0"
description = "Castle dashboard API" description = "Castle API"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"fastapi>=0.115.0", "fastapi>=0.115.0",
@@ -15,14 +15,14 @@ dependencies = [
castle-cli = { path = "../cli", editable = true } castle-cli = { path = "../cli", editable = true }
[project.scripts] [project.scripts]
dashboard-api = "dashboard_api.main:run" castle-api = "castle_api.main:run"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/dashboard_api"] packages = ["src/castle_api"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
@@ -33,4 +33,4 @@ dev = [
] ]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["dashboard_api"] known-first-party = ["castle_api"]

View File

@@ -0,0 +1,3 @@
"""Castle API."""
__version__ = "0.1.0"

View File

@@ -1,4 +1,4 @@
"""Configuration for dashboard-api.""" """Configuration for castle-api."""
from pathlib import Path from pathlib import Path
@@ -13,7 +13,7 @@ class Settings(BaseSettings):
port: int = 9020 port: int = 9020
model_config = { model_config = {
"env_prefix": "DASHBOARD_API_", "env_prefix": "CASTLE_API_",
"env_file": ".env", "env_file": ".env",
} }

View File

@@ -12,8 +12,8 @@ from pydantic import BaseModel
from castle_cli.config import load_config, save_config from castle_cli.config import load_config, save_config
from castle_cli.manifest import ComponentManifest from castle_cli.manifest import ComponentManifest
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.stream import broadcast from castle_api.stream import broadcast
router = APIRouter(prefix="/config", tags=["config"]) router = APIRouter(prefix="/config", tags=["config"])

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
from dashboard_api.bus import bus from castle_api.bus import bus
router = APIRouter(prefix="/events", tags=["events"]) router = APIRouter(prefix="/events", tags=["events"])

View File

@@ -9,7 +9,7 @@ import httpx
from castle_cli.config import CastleConfig from castle_cli.config import CastleConfig
from dashboard_api.models import HealthStatus from castle_api.models import HealthStatus
async def check_all_health(config: CastleConfig) -> list[HealthStatus]: async def check_all_health(config: CastleConfig) -> list[HealthStatus]:

View File

@@ -10,7 +10,7 @@ from starlette.responses import StreamingResponse
from castle_cli.config import load_config from castle_cli.config import load_config
from dashboard_api.config import settings from castle_api.config import settings
router = APIRouter(prefix="/logs", tags=["logs"]) router = APIRouter(prefix="/logs", tags=["logs"])

View File

@@ -11,16 +11,16 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse
from dashboard_api.bus import bus from castle_api.bus import bus
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.config_editor import router as config_router from castle_api.config_editor import router as config_router
from dashboard_api.events import router as events_router from castle_api.events import router as events_router
from dashboard_api.logs import router as logs_router from castle_api.logs import router as logs_router
from dashboard_api.routes import router as dashboard_router from castle_api.routes import router as dashboard_router
from dashboard_api.secrets import router as secrets_router from castle_api.secrets import router as secrets_router
from dashboard_api.services import router as services_router from castle_api.services import router as services_router
from dashboard_api.stream import close_all_subscribers, health_poll_loop, subscribe, unsubscribe from castle_api.stream import close_all_subscribers, health_poll_loop, subscribe, unsubscribe
from dashboard_api.tools import router as tools_router from castle_api.tools import router as tools_router
# Set by _watch_shutdown when uvicorn begins its shutdown sequence. # Set by _watch_shutdown when uvicorn begins its shutdown sequence.
_shutting_down = False _shutting_down = False
@@ -53,8 +53,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
app = FastAPI( app = FastAPI(
title="Castle API", title="castle-api",
description="Castle dashboard API, event bus, and service management", description="Castle API, event bus, and service management",
version="0.1.0", version="0.1.0",
lifespan=lifespan, lifespan=lifespan,
) )
@@ -109,7 +109,7 @@ async def sse_stream() -> StreamingResponse:
def run() -> None: def run() -> None:
"""Run the application with uvicorn.""" """Run the application with uvicorn."""
config = uvicorn.Config( config = uvicorn.Config(
"dashboard_api.main:app", "castle_api.main:app",
host=settings.host, host=settings.host,
port=settings.port, port=settings.port,
reload=False, reload=False,

View File

@@ -9,9 +9,9 @@ from fastapi import APIRouter, HTTPException, status
from castle_cli.config import load_config from castle_cli.config import load_config
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.health import check_all_health from castle_api.health import check_all_health
from dashboard_api.models import ( from castle_api.models import (
ComponentDetail, ComponentDetail,
ComponentSummary, ComponentSummary,
GatewayInfo, GatewayInfo,

View File

@@ -10,15 +10,15 @@ from starlette.responses import JSONResponse
from castle_cli.config import load_config from castle_cli.config import load_config
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.health import check_all_health from castle_api.health import check_all_health
from dashboard_api.models import HealthStatus from castle_api.models import HealthStatus
from dashboard_api.stream import broadcast from castle_api.stream import broadcast
router = APIRouter(prefix="/services", tags=["services"]) router = APIRouter(prefix="/services", tags=["services"])
UNIT_PREFIX = "castle-" UNIT_PREFIX = "castle-"
SELF_NAME = "dashboard-api" SELF_NAME = "castle-api"
async def _systemctl(action: str, unit: str) -> tuple[bool, str]: async def _systemctl(action: str, unit: str) -> tuple[bool, str]:

View File

@@ -9,8 +9,8 @@ import time
from castle_cli.config import load_config from castle_cli.config import load_config
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.health import check_all_health from castle_api.health import check_all_health
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -10,8 +10,8 @@ from fastapi import APIRouter, HTTPException, status
from castle_cli.config import load_config from castle_cli.config import load_config
from castle_cli.manifest import ComponentManifest from castle_cli.manifest import ComponentManifest
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.models import ToolCategory, ToolDetail, ToolSummary from castle_api.models import ToolCategory, ToolDetail, ToolSummary
router = APIRouter(tags=["tools"]) router = APIRouter(tags=["tools"])

View File

@@ -1,4 +1,4 @@
"""Test fixtures for dashboard-api.""" """Test fixtures for castle-api."""
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path from pathlib import Path
@@ -7,8 +7,8 @@ import pytest
import yaml import yaml
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from dashboard_api.config import settings from castle_api.config import settings
from dashboard_api.main import app from castle_api.main import app
@pytest.fixture @pytest.fixture

View File

@@ -1,4 +1,4 @@
"""Tests for dashboard-api health endpoint.""" """Tests for castle-api health endpoint."""
from fastapi.testclient import TestClient from fastapi.testclient import TestClient

View File

@@ -32,6 +32,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
] ]
[[package]]
name = "castle-api"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "castle-cli" },
{ name = "fastapi" },
{ name = "httpx" },
{ name = "pydantic-settings" },
{ name = "uvicorn" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pyyaml" },
]
[package.metadata]
requires-dist = [
{ name = "castle-cli", editable = "../cli" },
{ name = "fastapi", specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "pydantic-settings", specifier = ">=2.0.0" },
{ name = "uvicorn", specifier = ">=0.34.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "pytest", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
{ name = "pyyaml", specifier = ">=6.0.0" },
]
[[package]] [[package]]
name = "castle-cli" name = "castle-cli"
version = "0.1.0" version = "0.1.0"
@@ -86,43 +123,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "dashboard-api"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "castle-cli" },
{ name = "fastapi" },
{ name = "httpx" },
{ name = "pydantic-settings" },
{ name = "uvicorn" },
]
[package.dev-dependencies]
dev = [
{ name = "httpx" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pyyaml" },
]
[package.metadata]
requires-dist = [
{ name = "castle-cli", editable = "../cli" },
{ name = "fastapi", specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "pydantic-settings", specifier = ">=2.0.0" },
{ name = "uvicorn", specifier = ">=0.34.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "pytest", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
{ name = "pyyaml", specifier = ">=6.0.0" },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.129.0" version = "0.129.0"

View File

@@ -1,8 +1,7 @@
gateway: gateway:
port: 9000 port: 9000
components: components:
# ── Infrastructure ────────────────────────────────────── castle-gateway:
gateway:
description: Caddy reverse proxy gateway description: Caddy reverse proxy gateway
run: run:
runner: command runner: command
@@ -14,16 +13,15 @@ components:
- /home/payne/.castle/generated/Caddyfile - /home/payne/.castle/generated/Caddyfile
- --adapter - --adapter
- caddyfile - caddyfile
manage:
systemd:
exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile
--adapter caddyfile
expose: expose:
http: http:
internal: internal:
port: 9000 port: 9000
health_path: / health_path: /
manage:
systemd:
exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile --adapter caddyfile
# ── Services ──────────────────────────────────────────────
central-context: central-context:
description: Content storage API description: Content storage API
run: run:
@@ -44,7 +42,9 @@ components:
caddy: caddy:
path_prefix: /central-context path_prefix: /central-context
notification-bridge: notification-bridge:
description: Desktop notification forwarder description: Desktop notification forwarder. This taps into your notifications
system on the desktop and will forward all notifications the the central context
server.
run: run:
runner: python_uv_tool runner: python_uv_tool
cwd: notification-bridge cwd: notification-bridge
@@ -63,14 +63,14 @@ components:
proxy: proxy:
caddy: caddy:
path_prefix: /notifications path_prefix: /notifications
dashboard-api: castle-api:
description: Castle dashboard API description: Castle API
run: run:
runner: python_uv_tool runner: python_uv_tool
cwd: dashboard-api cwd: castle-api
env: env:
DASHBOARD_API_CASTLE_ROOT: /data/repos/castle CASTLE_API_CASTLE_ROOT: /data/repos/castle
tool: dashboard-api tool: castle-api
manage: manage:
systemd: {} systemd: {}
expose: expose:
@@ -137,10 +137,10 @@ components:
manage: manage:
systemd: {} systemd: {}
tool: {} tool: {}
dashboard: castle-app:
description: Castle web dashboard description: Castle web app
build: build:
working_dir: dashboard working_dir: app
commands: commands:
- - pnpm - - pnpm
- build - build

View File

@@ -8,13 +8,13 @@ import subprocess
from castle_cli.config import GENERATED_DIR, CastleConfig, ensure_dirs, load_config from castle_cli.config import GENERATED_DIR, CastleConfig, ensure_dirs, load_config
GATEWAY_COMPONENT = "gateway" GATEWAY_COMPONENT = "castle-gateway"
GATEWAY_UNIT = "castle-gateway.service" GATEWAY_UNIT = "castle-gateway.service"
def _find_dashboard_dist(config: CastleConfig) -> str | None: def _find_app_dist(config: CastleConfig) -> str | None:
"""Find the dashboard dist/ directory if it exists.""" """Find the app dist/ directory if it exists."""
dist = config.root / "dashboard" / "dist" dist = config.root / "app" / "dist"
if dist.exists() and (dist / "index.html").exists(): if dist.exists() and (dist / "index.html").exists():
return str(dist) return str(dist)
return None return None
@@ -42,17 +42,17 @@ def _generate_caddyfile(config: CastleConfig) -> str:
lines.append(" }") lines.append(" }")
lines.append("") lines.append("")
# Dashboard SPA at root (must come after more-specific handle_path rules) # App SPA at root (must come after more-specific handle_path rules)
dashboard_dist = _find_dashboard_dist(config) app_dist = _find_app_dist(config)
if dashboard_dist: if app_dist:
lines.append(" handle {") lines.append(" handle {")
lines.append(f" root * {dashboard_dist}") lines.append(f" root * {app_dist}")
lines.append(" try_files {path} /index.html") lines.append(" try_files {path} /index.html")
lines.append(" file_server") lines.append(" file_server")
lines.append(" }") lines.append(" }")
else: else:
# Fallback: serve from generated directory # Fallback: serve from generated directory
fallback = GENERATED_DIR / "dashboard" fallback = GENERATED_DIR / "app"
lines.append(" handle / {") lines.append(" handle / {")
lines.append(f" root * {fallback}") lines.append(f" root * {fallback}")
lines.append(" file_server") lines.append(" file_server")
@@ -70,11 +70,11 @@ def _write_generated_files(config: CastleConfig) -> None:
caddyfile_path.write_text(_generate_caddyfile(config)) caddyfile_path.write_text(_generate_caddyfile(config))
print(f" Generated {caddyfile_path}") print(f" Generated {caddyfile_path}")
dashboard_dist = _find_dashboard_dist(config) app_dist = _find_app_dist(config)
if dashboard_dist: if app_dist:
print(f" Dashboard: {dashboard_dist}") print(f" App: {app_dist}")
else: else:
print(" Dashboard: dist/ not found, using fallback") print(" App: dist/ not found, using fallback")
def run_gateway(args: argparse.Namespace) -> int: def run_gateway(args: argparse.Namespace) -> int:

View File

@@ -1,31 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working
with code in this repository.
## Overview
dashboard-api is a FastAPI service. Castle dashboard API.
## Commands
```bash
uv sync # Install dependencies
uv run dashboard-api # Run service (port 9020)
uv run pytest tests/ -v # Run tests
uv run ruff check . # Lint
uv run ruff format . # Format
```
## Architecture
- `src/dashboard_api/config.py` — Settings via pydantic-settings, env prefix `DASHBOARD_API_`
- `src/dashboard_api/main.py` — FastAPI app, lifespan, health endpoint
- `tests/` — pytest with TestClient fixtures
## Configuration
Environment variables with `DASHBOARD_API_` prefix:
- `DASHBOARD_API_DATA_DIR` — Data directory (default: ./data)
- `DASHBOARD_API_HOST` — Bind host (default: 0.0.0.0)
- `DASHBOARD_API_PORT` — Port (default: 9020)

View File

@@ -1,3 +0,0 @@
"""Castle dashboard API."""
__version__ = "0.1.0"

View File

@@ -1,23 +1,20 @@
# Web Frontends in Castle # Web Frontends in Castle
How to build, serve, and manage web frontends as castle components. Based on How to build, serve, and manage web frontends as castle components.
the stack used in [wild-cloud/web](https://github.com/civilsociety-dev/wild-cloud).
## Stack ## Stack
| Layer | Choice | - Build: Vite 6
|-------|--------| - Language: TypeScript 5.8 (strict)
| **Build** | Vite 6 | - Framework: React 19
| **Language** | TypeScript 5.8 (strict) | - Routing: React Router 7
| **Framework** | React 19 | - Styling: Tailwind CSS 4 (`@tailwindcss/vite` plugin)
| **Routing** | React Router 7 | - Components: shadcn/ui (new-york style) + Radix UI primitives
| **Styling** | Tailwind CSS 4 (`@tailwindcss/vite` plugin) | - Icons: Lucide React
| **Components** | shadcn/ui (new-york style) + Radix UI primitives | - Server state: TanStack React Query 5
| **Icons** | Lucide React | - Forms: React Hook Form + Zod validation
| **Server state** | TanStack React Query 5 | - Testing: Vitest + Testing Library
| **Forms** | React Hook Form + Zod validation | - Package manager: pnpm
| **Testing** | Vitest + Testing Library |
| **Package manager** | pnpm |
## Scaffolding a new frontend ## Scaffolding a new frontend