name: deploy-internal-dashboard description: Deploy internal SPAs (HTML/JS/CSS) with Python API backends behind Caddy as systemd services. Covers stdlib http.server/ThreadingHTTPServer, env-based basicauth, static file serving, JSON API routing, and lifecycle.
Deploy an internal SPA dashboard (static HTML/JS/CSS) with a Python stdlib API backend, running as a systemd service behind Caddy reverse proxy.
User → Browser → Caddy (HTTPS, public, reverse proxy)
→ 127.0.0.1:<PORT> (Python HTTP server, internal only)
→ static/ (SPA: index.html, app.js, style.css)
→ /api/* (JSON endpoints → calls lib/ CLI helpers)
→ /files/* (optional: read-only file browser)
Use http.server.ThreadingHTTPServer (Python 3.7+, stdlib) — single dependency, no framework.
Key design decisions:
- Bind 127.0.0.1 only: security by network boundary. Caddy is the public face.
- Env-based basicauth: credentials via env vars, not code. Format: DASH_USER, DASH_PASS_HASH.
- Hash generation: printf '%s' "$PASSWORD" | sha256sum | awk '{print $1}'. Store hex hash, never plaintext.
- Read-only by default: projection-only endpoints (not full file contents).
- ThreadingHTTPServer: prevents blocking on concurrent requests (e.g. browser favicon + page load).
Create a .service file at /etc/systemd/system/<name>.service:
[Unit]
Description=<app> dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/path/to/project
EnvironmentFile=/path/to/instance/<app>.env
ExecStart=/usr/bin/python3 /path/to/server.py
Restart=always
[Install]
WantedBy=multi-user.target
Activation:
systemctl daemon-reload
systemctl enable --now <name>.service
systemctl is-active <name>.service
Append to /root/.caddy/Caddyfile:
<subdomain>.<domain>.sslip.io {
reverse_proxy 127.0.0.1:<PORT>
}
Reload Caddy:
docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
BUILD.txt exists at TWO levels — both must be kept consistent:
- /opt/ai-os/BUILD.txt — instance-level, checked by setup scripts and installer
- /opt/ai-os/products/ceo/BUILD.txt — product-level, checked by instance_info.py
When enabling a dashboard feature, update BOTH files. The setup script silently skips if only the product-level BUILD.txt has dashboard: true but the instance-level still says dashboard: false.
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<PORT>/api/health401 (not 000 which means unreachable)curl -s -u admin:password http://127.0.0.1:<PORT>/api/healthWhen adding a /files/ browser or Custom Kanban OS viewer (e.g. at http://100.71.157.103:9120/):
- Set FILES_ROOT = "/": For a full-system view, serve from the filesystem root.
- Path Traversal Protection: Use os.path.realpath(os.path.join(FILES_ROOT, rel)) then verify the result still starts with FILES_ROOT before opening.
- Clickable Breadcrumbs on ALL pages: Not just Markdown views — directory listings need them too. Split the path into clickable segments everywhere.
- Multi-Board Selector: Parse the query parameter (e.g., ?board=nv-office) and list all directories under /root/.hermes/kanban/boards as buttons to switch between boards. Read the SQLite database (kanban.db) dynamically for the chosen board.
- Sorting Options: Implement a dropdown sorting selector (e.g. priority, created date, title, ascending/descending) that adds a sort query parameter and changes the SQL ORDER BY statement.
- Pagination / Collapse Pattern: To keep column sizes manageable, display only the first 5 tasks per column by default. Provide a "▼ Xem thêm (N)" button that uses client-side JavaScript to toggle visibility of a .hidden-tasks container containing the remaining items. Avoid complex modal overlays.
- Clickable Task Details: Make task titles clickable and point to a /task?id=<task_id>&board=<board> route. Display full fields (assignee, priority, created_at, status), optionally the project workspace path, description (body), and worker execution logs (result).
- Remove redundant navigation: Once breadcrumbs are clickable and show the full parent path, do NOT add a separate "Thư mục cha" (parent directory) link in the listing. It duplicates the last breadcrumb segment and clutters the UI.
- Define link colors explicitly on dark themes: Browser-default colors on #1a1b26 backgrounds are nearly invisible. Always specify: .breadcrumb a { color: #7aa2f7; font-weight: bold; } and li a { color: #ffd166; } with hover states. Separate icon from text via so the icon stays outside the tag.
- Address-in-Use fix: Set socketserver.ThreadingTCPServer.allow_reuse_address = True before starting the server.
/opt/ai-os/BUILD.txt (instance), product code reads its local BUILD.txt. Forgetting the instance-level BUILD.txt is the #1 cause of "tier not activating".ThreadingHTTPServer or ThreadingTCPServer.chmod 600 on .env files; systemd reads them as root, so 600 is sufficient.systemctl list-units --type=service before creating a new one.--config /etc/caddy/Caddyfile --adapter caddyfile, not bare --reload.kanban.db at ~/.hermes/kanban.db (0 tasks) can win over the real board at ~/.hermes/kanban/boards/<slug>/kanban.db (50+ tasks). Put the real board path FIRST in the candidate list since next(p for p in candidates if os.path.isfile(p)) returns the first match. Verify with: curl -u user:pass http://127.0.0.1:PORT/api/kanban | python3 -c "import sys,json; d=json.load(sys.stdin); print('total:', d['total'])".https://user:password@domain/; (c) always verify with local curl BEFORE resetting the password.aios-dashboard.service) the systemd process holds the port. Running fuser -k PORT/tcp; nohup python3 ... & starts a conflicting process that crashes with Address already in use because systemd respawns the original. Always use systemctl restart <service> instead. Verify with systemctl status <service>. Check service logs via journalctl -u <service> --no-pager -n 20.updated_at on an older tasks table). Ensure all database query endpoints query only existing columns or implement runtime fallbacks (e.g., fallback to created_at when updated_at is missing) to prevent HTTP 500 error overlays..kbcard-det block within the task card) utilizing event propagation control (event.stopPropagation()) on the child block to prevent accidental toggle triggers when users highlight text or click inside the detail region. Load details lazily on demand from API and cache locally inside the DOM (dataset.loaded = "true").body), (b) execution results/deliverables (result field in DB), (c) last failure logs if blocked (last_failure_error), and (d) full chronological comments timeline (task_comments table) to expose the worker run logs and human feedback history.ExecStart points to the correct venv/bin/python3 if the script relies on specific packages (e.g. markdown, flask). The system-wide Python might lack these. Check journalctl -u <service> for ModuleNotFoundError.Address already in use, it's often due to another process (or a lingering old one) holding the port. Use sudo fuser -k -9 <PORT>/tcp to forcefully release the port before restarting the service. Combine with socketserver.ThreadingTCPServer.allow_reuse_address = True in code to prevent future bind issues.tailscale-serve — alternative: expose via Tailscale instead of Caddy