commit 049edc0238c1b81e92ea8190c717df5a8a56ed8d Author: Shawn Date: Mon Jun 1 23:44:10 2026 -0400 Initial scaffold: FastAPI server, responsive UI shell diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2ef65b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +venv/ +__pycache__/ +*.pyc +.env +*.db +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff2a5d9 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Hermes Command Center + +Mobile-friendly web dashboard for managing Hermes agents, configs, and network infrastructure. + +## Features + +- **Agent Chat Interface** - Talk to all Hermes profiles (kanban, coder, assistant, artist) +- **Config Editor** - Edit .env and YAML config files +- **Obsidian Vault Reader** - Browse markdown notes and wiki +- **Network Status** - Live host and service monitoring + +## Tech Stack + +- FastAPI backend +- Vanilla HTML/CSS/JS frontend +- Mobile-first responsive design +- Port: 8888 + +## Development + +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python3 main.py +``` + +Access at: http://localhost:8888 diff --git a/main.py b/main.py new file mode 100644 index 0000000..de9a611 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +"""Hermes Command Center - Main FastAPI application.""" + +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "server:app", + host="0.0.0.0", + port=8888, + reload=True, + reload_dirs=["."] + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8aef1c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +python-multipart>=0.0.6 +pyyaml>=6.0 diff --git a/server.py b/server.py new file mode 100644 index 0000000..35978e7 --- /dev/null +++ b/server.py @@ -0,0 +1,63 @@ +"""Hermes Command Center - FastAPI backend.""" + +import os +import base64 +import secrets +from pathlib import Path + +from fastapi import FastAPI, Request, Response +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse, JSONResponse + +# ── Auth ────────────────────────────────────────────────────────────── +DASHBOARD_USER = os.environ.get("DASHBOARD_USER", "shawn") +DASHBOARD_PASS = os.environ.get("DASHBOARD_PASS", "Brett85!@") + + +def _check_auth(auth_header: str | None) -> bool: + if not auth_header: + return False + try: + decoded = base64.b64decode(auth_header.removeprefix("Basic ")).decode() + user, _, passwd = decoded.partition(":") + return secrets.compare_digest(user, DASHBOARD_USER) and secrets.compare_digest(passwd, DASHBOARD_PASS) + except Exception: + return False + + +_unauthorized = Response( + content="

401 Unauthorized

", + status_code=401, + headers={"WWW-Authenticate": 'Basic realm="Command Center"'}, +) + +app = FastAPI(title="Hermes Command Center") + +STATIC_DIR = Path(__file__).parent / "static" + + +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + if request.url.path in ("/health", "/api/health"): + return await call_next(request) + if not _check_auth(request.headers.get("Authorization")): + return _unauthorized + return await call_next(request) + + +@app.get("/health") +async def health(): + return {"status": "ok", "service": "hermes-command-center"} + + +# ── Serve frontend ──────────────────────────────────────────────────── +if STATIC_DIR.exists(): + app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + + +@app.get("/", response_class=HTMLResponse) +async def index(): + index_path = STATIC_DIR / "index.html" + if index_path.exists(): + return HTMLResponse(content=index_path.read_text()) + return HTMLResponse(content="

Command Center - Loading...

") diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..78ed0cc --- /dev/null +++ b/static/app.js @@ -0,0 +1,17 @@ +// Command Center SPA Router +const sections = document.querySelectorAll('.panel'); +const links = document.querySelectorAll('.nav-links a'); + +function navigate(hash) { + const target = hash.replace('#', '') || 'agents'; + sections.forEach(s => s.hidden = s.id !== target); + links.forEach(l => l.classList.toggle('active', l.getAttribute('href') === '#' + target)); +} + +links.forEach(l => l.addEventListener('click', e => { + e.preventDefault(); + window.location.hash = l.getAttribute('href'); +})); + +window.addEventListener('hashchange', () => navigate(window.location.hash)); +navigate(window.location.hash || '#agents'); diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..f31ee38 --- /dev/null +++ b/static/index.html @@ -0,0 +1,41 @@ + + + + + + Hermes Command Center + + + +
+ +
+
+

Agent Chat

+

Coming soon...

+
+ + + +
+
+ + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..85e7264 --- /dev/null +++ b/static/style.css @@ -0,0 +1,33 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +:root { + --bg: #0d1117; + --surface: #161b22; + --border: #30363d; + --text: #e6edf3; + --text-muted: #8b949e; + --accent: #00d4ff; + --accent-hover: #33ddff; + --danger: #f85149; + --success: #3fb950; + --warning: #d29922; + --radius: 8px; +} +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; } +#app { display: flex; min-height: 100vh; } +.sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); padding: 16px 0; position: fixed; height: 100vh; overflow-y: auto; } +.logo { padding: 0 16px 16px; font-size: 18px; font-weight: 700; border-bottom: 1px solid var(--border); } +.nav-links { list-style: none; padding: 12px 0; } +.nav-links a { display: block; padding: 10px 16px; color: var(--text-muted); text-decoration: none; font-size: 14px; transition: all 0.15s; } +.nav-links a:hover, .nav-links a.active { color: var(--text); background: rgba(0,212,255,0.1); border-right: 2px solid var(--accent); } +.content { flex: 1; margin-left: 220px; padding: 24px; } +.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; } +.panel h2 { margin-bottom: 16px; font-size: 20px; } +/* Mobile */ +@media (max-width: 768px) { + .sidebar { width: 100%; height: auto; position: relative; display: flex; flex-wrap: wrap; padding: 8px 0; } + .logo { width: 100%; padding: 8px 16px; border-bottom: none; } + .nav-links { display: flex; width: 100%; padding: 0; overflow-x: auto; } + .nav-links a { padding: 8px 14px; white-space: nowrap; border-right: none; border-bottom: 2px solid transparent; } + .nav-links a.active { border-right: none; border-bottom-color: var(--accent); } + .content { margin-left: 0; padding: 12px; } +}