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,
"version": "0.0.0",
"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]
name = "dashboard-api"
name = "castle-api"
version = "0.1.0"
description = "Castle dashboard API"
description = "Castle API"
requires-python = ">=3.13"
dependencies = [
"fastapi>=0.115.0",
@@ -15,14 +15,14 @@ dependencies = [
castle-cli = { path = "../cli", editable = true }
[project.scripts]
dashboard-api = "dashboard_api.main:run"
castle-api = "castle_api.main:run"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/dashboard_api"]
packages = ["src/castle_api"]
[dependency-groups]
dev = [
@@ -33,4 +33,4 @@ dev = [
]
[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
@@ -13,7 +13,7 @@ class Settings(BaseSettings):
port: int = 9020
model_config = {
"env_prefix": "DASHBOARD_API_",
"env_prefix": "CASTLE_API_",
"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.manifest import ComponentManifest
from dashboard_api.config import settings
from dashboard_api.stream import broadcast
from castle_api.config import settings
from castle_api.stream import broadcast
router = APIRouter(prefix="/config", tags=["config"])

View File

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

View File

@@ -9,7 +9,7 @@ import httpx
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]:

View File

@@ -10,7 +10,7 @@ from starlette.responses import StreamingResponse
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"])

View File

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

View File

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

View File

@@ -10,15 +10,15 @@ from starlette.responses import JSONResponse
from castle_cli.config import load_config
from dashboard_api.config import settings
from dashboard_api.health import check_all_health
from dashboard_api.models import HealthStatus
from dashboard_api.stream import broadcast
from castle_api.config import settings
from castle_api.health import check_all_health
from castle_api.models import HealthStatus
from castle_api.stream import broadcast
router = APIRouter(prefix="/services", tags=["services"])
UNIT_PREFIX = "castle-"
SELF_NAME = "dashboard-api"
SELF_NAME = "castle-api"
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 dashboard_api.config import settings
from dashboard_api.health import check_all_health
from castle_api.config import settings
from castle_api.health import check_all_health
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.manifest import ComponentManifest
from dashboard_api.config import settings
from dashboard_api.models import ToolCategory, ToolDetail, ToolSummary
from castle_api.config import settings
from castle_api.models import ToolCategory, ToolDetail, ToolSummary
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 pathlib import Path
@@ -7,8 +7,8 @@ import pytest
import yaml
from fastapi.testclient import TestClient
from dashboard_api.config import settings
from dashboard_api.main import app
from castle_api.config import settings
from castle_api.main import app
@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

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" },
]
[[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]]
name = "castle-cli"
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" },
]
[[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]]
name = "fastapi"
version = "0.129.0"

View File

@@ -1,8 +1,7 @@
gateway:
port: 9000
components:
# ── Infrastructure ──────────────────────────────────────
gateway:
castle-gateway:
description: Caddy reverse proxy gateway
run:
runner: command
@@ -14,16 +13,15 @@ components:
- /home/payne/.castle/generated/Caddyfile
- --adapter
- caddyfile
manage:
systemd:
exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile
--adapter caddyfile
expose:
http:
internal:
port: 9000
health_path: /
manage:
systemd:
exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile --adapter caddyfile
# ── Services ──────────────────────────────────────────────
central-context:
description: Content storage API
run:
@@ -44,7 +42,9 @@ components:
caddy:
path_prefix: /central-context
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:
runner: python_uv_tool
cwd: notification-bridge
@@ -63,14 +63,14 @@ components:
proxy:
caddy:
path_prefix: /notifications
dashboard-api:
description: Castle dashboard API
castle-api:
description: Castle API
run:
runner: python_uv_tool
cwd: dashboard-api
cwd: castle-api
env:
DASHBOARD_API_CASTLE_ROOT: /data/repos/castle
tool: dashboard-api
CASTLE_API_CASTLE_ROOT: /data/repos/castle
tool: castle-api
manage:
systemd: {}
expose:
@@ -137,10 +137,10 @@ components:
manage:
systemd: {}
tool: {}
dashboard:
description: Castle web dashboard
castle-app:
description: Castle web app
build:
working_dir: dashboard
working_dir: app
commands:
- - pnpm
- build

View File

@@ -8,13 +8,13 @@ import subprocess
from castle_cli.config import GENERATED_DIR, CastleConfig, ensure_dirs, load_config
GATEWAY_COMPONENT = "gateway"
GATEWAY_COMPONENT = "castle-gateway"
GATEWAY_UNIT = "castle-gateway.service"
def _find_dashboard_dist(config: CastleConfig) -> str | None:
"""Find the dashboard dist/ directory if it exists."""
dist = config.root / "dashboard" / "dist"
def _find_app_dist(config: CastleConfig) -> str | None:
"""Find the app dist/ directory if it exists."""
dist = config.root / "app" / "dist"
if dist.exists() and (dist / "index.html").exists():
return str(dist)
return None
@@ -42,17 +42,17 @@ def _generate_caddyfile(config: CastleConfig) -> str:
lines.append(" }")
lines.append("")
# Dashboard SPA at root (must come after more-specific handle_path rules)
dashboard_dist = _find_dashboard_dist(config)
if dashboard_dist:
# App SPA at root (must come after more-specific handle_path rules)
app_dist = _find_app_dist(config)
if app_dist:
lines.append(" handle {")
lines.append(f" root * {dashboard_dist}")
lines.append(f" root * {app_dist}")
lines.append(" try_files {path} /index.html")
lines.append(" file_server")
lines.append(" }")
else:
# Fallback: serve from generated directory
fallback = GENERATED_DIR / "dashboard"
fallback = GENERATED_DIR / "app"
lines.append(" handle / {")
lines.append(f" root * {fallback}")
lines.append(" file_server")
@@ -70,11 +70,11 @@ def _write_generated_files(config: CastleConfig) -> None:
caddyfile_path.write_text(_generate_caddyfile(config))
print(f" Generated {caddyfile_path}")
dashboard_dist = _find_dashboard_dist(config)
if dashboard_dist:
print(f" Dashboard: {dashboard_dist}")
app_dist = _find_app_dist(config)
if app_dist:
print(f" App: {app_dist}")
else:
print(" Dashboard: dist/ not found, using fallback")
print(" App: dist/ not found, using fallback")
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
How to build, serve, and manage web frontends as castle components. Based on
the stack used in [wild-cloud/web](https://github.com/civilsociety-dev/wild-cloud).
How to build, serve, and manage web frontends as castle components.
## Stack
| Layer | Choice |
|-------|--------|
| **Build** | Vite 6 |
| **Language** | TypeScript 5.8 (strict) |
| **Framework** | React 19 |
| **Routing** | React Router 7 |
| **Styling** | Tailwind CSS 4 (`@tailwindcss/vite` plugin) |
| **Components** | shadcn/ui (new-york style) + Radix UI primitives |
| **Icons** | Lucide React |
| **Server state** | TanStack React Query 5 |
| **Forms** | React Hook Form + Zod validation |
| **Testing** | Vitest + Testing Library |
| **Package manager** | pnpm |
- Build: Vite 6
- Language: TypeScript 5.8 (strict)
- Framework: React 19
- Routing: React Router 7
- Styling: Tailwind CSS 4 (`@tailwindcss/vite` plugin)
- Components: shadcn/ui (new-york style) + Radix UI primitives
- Icons: Lucide React
- Server state: TanStack React Query 5
- Forms: React Hook Form + Zod validation
- Testing: Vitest + Testing Library
- Package manager: pnpm
## Scaffolding a new frontend