diff --git a/cheddahbot/__main__.py b/cheddahbot/__main__.py
index 7d8099e..b324ed1 100644
--- a/cheddahbot/__main__.py
+++ b/cheddahbot/__main__.py
@@ -1,6 +1,5 @@
"""Entry point: python -m cheddahbot"""
-import json
import logging
from pathlib import Path
@@ -146,6 +145,13 @@ def main():
fastapi_app = FastAPI()
+ # Mount API endpoints
+ from .api import create_api_router
+
+ api_router = create_api_router(config, db, registry)
+ fastapi_app.include_router(api_router)
+ log.info("API router mounted at /api/")
+
# Mount the dashboard as static files (must come before Gradio's catch-all)
dashboard_dir = Path(__file__).resolve().parent.parent / "dashboard"
if dashboard_dir.is_dir():
@@ -161,76 +167,6 @@ def main():
)
log.info("Dashboard mounted at /dashboard/ (serving %s)", dashboard_dir)
- # Link building status API endpoint (for dashboard consumption)
- @fastapi_app.get("/api/linkbuilding/status")
- async def linkbuilding_status():
- """Return link building pipeline status for dashboard consumption."""
- result = {
- "pending_cora_runs": [],
- "in_progress": [],
- "completed": [],
- "failed": [],
- }
-
- # Query KV store for tracked link building states
- try:
- for key, value in db.kv_scan("linkbuilding:watched:"):
- try:
- state = json.loads(value)
- entry = {
- "filename": state.get("filename", key.split(":")[-1]),
- "status": state.get("status", "unknown"),
- "task_id": state.get("task_id", ""),
- }
- if state.get("status") == "completed":
- entry["completed_at"] = state.get("completed_at", "")
- result["completed"].append(entry)
- elif state.get("status") == "failed":
- entry["error"] = state.get("error", "")
- entry["failed_at"] = state.get("failed_at", "")
- result["failed"].append(entry)
- elif state.get("status") == "processing":
- entry["started_at"] = state.get("started_at", "")
- result["in_progress"].append(entry)
- except json.JSONDecodeError:
- pass
- except Exception as e:
- log.warning("Error reading linkbuilding KV state: %s", e)
-
- # Query ClickUp for pending tasks (to do + Link Building + Cora Backlinks)
- if config.clickup.enabled:
- try:
- from .clickup import ClickUpClient
-
- cu = ClickUpClient(
- api_token=config.clickup.api_token,
- workspace_id=config.clickup.workspace_id,
- task_type_field_name=config.clickup.task_type_field_name,
- )
- try:
- tasks = cu.get_tasks_from_space(config.clickup.space_id, statuses=["to do"])
- for task in tasks:
- if task.task_type != "Link Building":
- continue
- lb_method = task.custom_fields.get("LB Method", "")
- if lb_method and lb_method != "Cora Backlinks":
- continue
- result["pending_cora_runs"].append(
- {
- "keyword": task.custom_fields.get("Keyword", ""),
- "url": task.custom_fields.get("IMSURL", ""),
- "client": task.custom_fields.get("Client", ""),
- "task_id": task.id,
- "task_name": task.name,
- }
- )
- finally:
- cu.close()
- except Exception as e:
- log.warning("Error querying ClickUp for pending link building: %s", e)
-
- return result
-
# Mount Gradio at the root
gr.mount_gradio_app(fastapi_app, blocks, path="/", pwa=True, show_error=True)
diff --git a/cheddahbot/api.py b/cheddahbot/api.py
new file mode 100644
index 0000000..7b2c354
--- /dev/null
+++ b/cheddahbot/api.py
@@ -0,0 +1,297 @@
+"""Dashboard API endpoints.
+
+Provides JSON data for the standalone HTML dashboard.
+All ClickUp data is cached for 5 minutes to avoid hammering the API.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import shutil
+import threading
+import time
+from typing import TYPE_CHECKING
+
+from fastapi import APIRouter
+
+if TYPE_CHECKING:
+ from .agent_registry import AgentRegistry
+ from .config import Config
+ from .db import Database
+
+log = logging.getLogger(__name__)
+
+router = APIRouter(prefix="/api")
+
+# Module-level refs wired at startup by create_api_router()
+_config: Config | None = None
+_db: Database | None = None
+_registry: AgentRegistry | None = None
+
+# Simple in-memory cache for ClickUp data
+_cache: dict[str, dict] = {}
+_cache_lock = threading.Lock()
+CACHE_TTL = 300 # 5 minutes
+
+
+def create_api_router(
+ config: Config, db: Database, registry: AgentRegistry
+) -> APIRouter:
+ """Wire dependencies and return the router."""
+ global _config, _db, _registry
+ _config = config
+ _db = db
+ _registry = registry
+ return router
+
+
+def _get_cached(key: str) -> dict | None:
+ with _cache_lock:
+ entry = _cache.get(key)
+ if entry and time.time() - entry["ts"] < CACHE_TTL:
+ return entry["data"]
+ return None
+
+
+def _set_cached(key: str, data):
+ with _cache_lock:
+ _cache[key] = {"data": data, "ts": time.time()}
+
+
+def _get_clickup_client():
+ from .clickup import ClickUpClient
+
+ return ClickUpClient(
+ api_token=_config.clickup.api_token,
+ workspace_id=_config.clickup.workspace_id,
+ task_type_field_name=_config.clickup.task_type_field_name,
+ )
+
+
+# ── Tasks ──
+
+
+@router.get("/tasks")
+async def get_tasks():
+ """All ClickUp tasks grouped by work category."""
+ cached = _get_cached("tasks")
+ if cached is not None:
+ return cached
+
+ if not _config or not _config.clickup.enabled:
+ return {"tasks": [], "error": "ClickUp not configured"}
+
+ client = _get_clickup_client()
+ try:
+ raw_tasks = client.get_tasks_from_space(_config.clickup.space_id)
+ tasks = []
+ for t in raw_tasks:
+ tasks.append(
+ {
+ "id": t.id,
+ "name": t.name,
+ "status": t.status,
+ "task_type": t.task_type,
+ "url": t.url,
+ "due_date": t.due_date,
+ "list_name": t.list_name,
+ "custom_fields": t.custom_fields,
+ }
+ )
+ result = {"tasks": tasks, "count": len(tasks)}
+ _set_cached("tasks", result)
+ return result
+ except Exception as e:
+ log.error("Failed to fetch tasks for dashboard: %s", e)
+ return {"tasks": [], "error": str(e)}
+ finally:
+ client.close()
+
+
+@router.get("/tasks/by-company")
+async def get_tasks_by_company():
+ """Tasks grouped by Client custom field."""
+ data = await get_tasks()
+ by_company: dict[str, list] = {}
+ for task in data.get("tasks", []):
+ company = task["custom_fields"].get("Client") or "Unassigned"
+ by_company.setdefault(company, []).append(task)
+
+ # Sort companies by task count descending
+ sorted_companies = sorted(by_company.items(), key=lambda x: -len(x[1]))
+ return {
+ "companies": [
+ {"name": name, "tasks": tasks, "count": len(tasks)}
+ for name, tasks in sorted_companies
+ ]
+ }
+
+
+@router.get("/tasks/link-building")
+async def get_link_building_tasks():
+ """Link building tasks with KV state merged in."""
+ data = await get_tasks()
+ lb_tasks = [t for t in data.get("tasks", []) if t["task_type"] == "Link Building"]
+
+ # Merge KV state
+ if _db:
+ for task in lb_tasks:
+ kv_key = f"clickup:task:{task['id']}:state"
+ raw = _db.kv_get(kv_key)
+ if raw:
+ try:
+ task["kv_state"] = json.loads(raw)
+ except json.JSONDecodeError:
+ task["kv_state"] = None
+ else:
+ task["kv_state"] = None
+
+ # Group by company
+ by_company: dict[str, list] = {}
+ for task in lb_tasks:
+ company = task["custom_fields"].get("Client") or "Unassigned"
+ by_company.setdefault(company, []).append(task)
+
+ return {
+ "total": len(lb_tasks),
+ "companies": [
+ {"name": name, "tasks": tasks, "count": len(tasks)}
+ for name, tasks in sorted(by_company.items(), key=lambda x: -len(x[1]))
+ ],
+ "status_counts": _count_statuses(lb_tasks),
+ }
+
+
+@router.get("/tasks/press-releases")
+async def get_press_release_tasks():
+ """Press release tasks with KV state merged in."""
+ data = await get_tasks()
+ pr_tasks = [t for t in data.get("tasks", []) if t["task_type"] == "Press Release"]
+
+ if _db:
+ for task in pr_tasks:
+ kv_key = f"clickup:task:{task['id']}:state"
+ raw = _db.kv_get(kv_key)
+ if raw:
+ try:
+ task["kv_state"] = json.loads(raw)
+ except json.JSONDecodeError:
+ task["kv_state"] = None
+ else:
+ task["kv_state"] = None
+
+ by_company: dict[str, list] = {}
+ for task in pr_tasks:
+ company = task["custom_fields"].get("Client") or "Unassigned"
+ by_company.setdefault(company, []).append(task)
+
+ return {
+ "total": len(pr_tasks),
+ "companies": [
+ {"name": name, "tasks": tasks, "count": len(tasks)}
+ for name, tasks in sorted(by_company.items(), key=lambda x: -len(x[1]))
+ ],
+ "status_counts": _count_statuses(pr_tasks),
+ }
+
+
+def _count_statuses(tasks: list[dict]) -> dict[str, int]:
+ counts: dict[str, int] = {}
+ for t in tasks:
+ s = t.get("status", "unknown")
+ counts[s] = counts.get(s, 0) + 1
+ return counts
+
+
+# ── Agents ──
+
+
+@router.get("/agents")
+async def get_agents():
+ """Agent configurations."""
+ if not _config:
+ return {"agents": []}
+
+ agents = []
+ for ac in _config.agents:
+ agents.append(
+ {
+ "name": ac.name,
+ "display_name": ac.display_name,
+ "personality_file": ac.personality_file,
+ "model": ac.model or _config.chat_model,
+ "tools": ac.tools,
+ "skills": ac.skills,
+ "memory_scope": ac.memory_scope or "shared",
+ }
+ )
+ return {"agents": agents}
+
+
+# ── System ──
+
+
+@router.get("/system/health")
+async def get_system_health():
+ """System health: disk space, brains, integrations."""
+ health = {
+ "execution_brain": shutil.which("claude") is not None,
+ "clickup_enabled": _config.clickup.enabled if _config else False,
+ "chat_model": _config.chat_model if _config else "unknown",
+ "execution_model": _config.default_model if _config else "unknown",
+ "disks": [],
+ }
+
+ # Disk space (Windows drives)
+ for drive in ["C:", "D:", "E:", "Z:"]:
+ try:
+ usage = shutil.disk_usage(drive + "/")
+ health["disks"].append(
+ {
+ "drive": drive,
+ "total_gb": round(usage.total / (1024**3), 1),
+ "free_gb": round(usage.free / (1024**3), 1),
+ "percent_free": round(usage.free / usage.total * 100, 1),
+ }
+ )
+ except (FileNotFoundError, OSError):
+ pass
+
+ return health
+
+
+@router.get("/system/notifications")
+async def get_notifications():
+ """Recent notifications from the DB."""
+ if not _db:
+ return {"notifications": []}
+
+ notes = _db.get_notifications_after(0, limit=100)
+ return {"notifications": notes}
+
+
+@router.get("/system/kv-states")
+async def get_kv_states():
+ """All ClickUp task states from KV store."""
+ if not _db:
+ return {"states": []}
+
+ pairs = _db.kv_scan("clickup:task:")
+ states = []
+ for _key, val in pairs:
+ try:
+ state = json.loads(val)
+ states.append(state)
+ except json.JSONDecodeError:
+ pass
+
+ return {"states": states}
+
+
+@router.post("/cache/clear")
+async def clear_cache():
+ """Clear the ClickUp data cache."""
+ with _cache_lock:
+ _cache.clear()
+ return {"status": "cleared"}
diff --git a/cheddahbot/tools/linkbuilding.py b/cheddahbot/tools/linkbuilding.py
index a48dd42..e66a96e 100644
--- a/cheddahbot/tools/linkbuilding.py
+++ b/cheddahbot/tools/linkbuilding.py
@@ -413,7 +413,13 @@ def run_link_building(
ctx: dict | None = None,
) -> str:
"""Dispatch to the correct link building pipeline based on lb_method."""
- method = (lb_method or "Cora Backlinks").strip()
+ method = (lb_method or "").strip()
+
+ if not method:
+ return (
+ "Skipped: 'LB Method' field is empty. Each Link Building task must have "
+ "an LB Method set (e.g. 'Cora Backlinks') before processing can begin."
+ )
if method == "Cora Backlinks":
# For Cora Backlinks, xlsx_path is required
diff --git a/dashboard/index.html b/dashboard/index.html
new file mode 100644
index 0000000..6639ef7
--- /dev/null
+++ b/dashboard/index.html
@@ -0,0 +1,816 @@
+
+
+
+
+
+ CheddahBot Command Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
C
+
CheddahBot
+
Command Dashboard
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Total Tasks
+
Loading...
+
+
+
-
+
Link Building
+
Loading...
+
+
+
-
+
Press Releases
+
Loading...
+
+
+
-
+
Companies
+
Loading...
+
+
+
-
+
Agents
+
Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading system health...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/styles.css b/dashboard/styles.css
new file mode 100644
index 0000000..eebd0c4
--- /dev/null
+++ b/dashboard/styles.css
@@ -0,0 +1,1113 @@
+/* ============================================================
+ CheddahBot Command Dashboard
+ Churchill-esque: deep navy, rich slate, burnished gold
+ ============================================================ */
+
+/* --- Foundations --- */
+:root {
+ --navy: #0b1a2e;
+ --navy-mid: #122340;
+ --navy-light: #1a3050;
+ --slate: #2c3e50;
+ --slate-light:#3d5166;
+ --gold: #c9a84c;
+ --gold-light: #e0c872;
+ --gold-dim: #a68930;
+ --cream: #f0e9d8;
+ --cream-light:#faf6ed;
+ --white: #ffffff;
+ --red: #d9534f;
+ --green: #4caf50;
+ --amber: #f0ad4e;
+ --text: #e8e0d0;
+ --text-muted: #8a9bb5;
+ --border: rgba(201,168,76,0.18);
+ --shadow: 0 2px 16px rgba(0,0,0,0.35);
+ --shadow-lg: 0 8px 32px rgba(0,0,0,0.45);
+ --radius: 8px;
+ --radius-lg: 12px;
+ --transition: 0.25s ease;
+ --font: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ --font-serif: 'Playfair Display', 'Georgia', serif;
+}
+
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-size: 15px;
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: var(--font);
+ background: var(--navy);
+ color: var(--text);
+ line-height: 1.6;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* --- Layout Shell --- */
+.dashboard {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ grid-template-rows: auto 1fr;
+ min-height: 100vh;
+}
+
+/* --- Top Bar --- */
+.topbar {
+ grid-column: 1 / -1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 2rem;
+ height: 64px;
+ background: linear-gradient(135deg, var(--navy) 0%, var(--navy-mid) 100%);
+ border-bottom: 1px solid var(--border);
+}
+
+.topbar__brand {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.topbar__logo {
+ width: 36px;
+ height: 36px;
+ background: var(--gold);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ font-size: 1rem;
+ color: var(--navy);
+ flex-shrink: 0;
+}
+
+.topbar__title {
+ font-family: var(--font-serif);
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: var(--cream);
+ letter-spacing: 0.02em;
+}
+
+.topbar__subtitle {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-left: 0.5rem;
+}
+
+.topbar__right {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+
+.topbar__status {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.8rem;
+ color: var(--green);
+}
+
+.topbar__status .dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--green);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+}
+
+.topbar__time {
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+}
+
+/* --- Sidebar --- */
+.sidebar {
+ background: var(--navy-mid);
+ border-right: 1px solid var(--border);
+ padding: 1.5rem 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.sidebar__section {
+ padding: 0 1rem;
+ margin-bottom: 0.5rem;
+}
+
+.sidebar__label {
+ font-size: 0.65rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: var(--gold-dim);
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+ padding: 0 0.75rem;
+}
+
+.sidebar__link {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ padding: 0.6rem 0.75rem;
+ border-radius: var(--radius);
+ color: var(--text-muted);
+ text-decoration: none;
+ font-size: 0.85rem;
+ font-weight: 500;
+ transition: all var(--transition);
+ cursor: pointer;
+ border: none;
+ background: none;
+ width: 100%;
+ text-align: left;
+}
+
+.sidebar__link:hover {
+ background: rgba(201,168,76,0.08);
+ color: var(--cream);
+}
+
+.sidebar__link.active {
+ background: rgba(201,168,76,0.15);
+ color: var(--gold-light);
+}
+
+.sidebar__link .icon {
+ width: 18px;
+ text-align: center;
+ flex-shrink: 0;
+ font-size: 0.95rem;
+}
+
+.sidebar__link .badge {
+ margin-left: auto;
+ background: var(--gold);
+ color: var(--navy);
+ font-size: 0.65rem;
+ font-weight: 700;
+ padding: 0.1rem 0.45rem;
+ border-radius: 10px;
+ min-width: 20px;
+ text-align: center;
+}
+
+.sidebar__divider {
+ height: 1px;
+ background: var(--border);
+ margin: 0.75rem 1rem;
+}
+
+/* --- Main Content --- */
+.main {
+ padding: 1.75rem 2rem;
+ overflow-y: auto;
+ background: linear-gradient(180deg, var(--navy) 0%, #0d1f35 100%);
+}
+
+/* --- Tab Panels --- */
+.tab-panel { display: none; }
+.tab-panel.active { display: block; }
+
+/* --- Page Header (non-briefing tabs) --- */
+.page-header {
+ margin-bottom: 1.75rem;
+}
+
+.page-header__greeting {
+ font-family: var(--font-serif);
+ font-size: 1.75rem;
+ color: var(--cream);
+ font-weight: 700;
+ margin-bottom: 0.15rem;
+}
+
+.page-header__date {
+ font-size: 0.85rem;
+ color: var(--text-muted);
+}
+
+.page-header__quote {
+ margin-top: 1rem;
+ padding: 0.85rem 1.25rem;
+ background: rgba(201,168,76,0.06);
+ border-left: 3px solid var(--gold);
+ border-radius: 0 var(--radius) var(--radius) 0;
+ font-style: italic;
+ color: var(--gold-light);
+ font-size: 0.9rem;
+ line-height: 1.55;
+}
+
+.page-header__quote cite {
+ display: block;
+ margin-top: 0.35rem;
+ font-style: normal;
+ font-size: 0.75rem;
+ color: var(--gold-dim);
+}
+
+/* ============================================================
+ BRIEFING-SPECIFIC STYLES (data-dense layout)
+ ============================================================ */
+
+/* --- Compact Briefing Header --- */
+.briefing-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+ margin-bottom: 1.25rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.briefing-header__left {
+ flex-shrink: 0;
+}
+
+.briefing-header__greeting {
+ font-family: var(--font-serif);
+ font-size: 1.5rem;
+ color: var(--cream);
+ font-weight: 700;
+ line-height: 1.2;
+}
+
+.briefing-header__date {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ margin-top: 0.15rem;
+}
+
+.briefing-header__quote {
+ margin: 0;
+ padding: 0.6rem 1rem;
+ background: rgba(201,168,76,0.05);
+ border-left: 2px solid var(--gold-dim);
+ border-radius: 0 var(--radius) var(--radius) 0;
+ font-style: italic;
+ color: var(--gold-dim);
+ font-size: 0.78rem;
+ line-height: 1.45;
+ max-width: 420px;
+ flex-shrink: 1;
+}
+
+.briefing-header__quote cite {
+ font-style: normal;
+ font-size: 0.7rem;
+ opacity: 0.7;
+}
+
+/* --- User Reminders Section --- */
+.reminders-section {
+ margin-bottom: 1.25rem;
+}
+
+.reminders-section__header {
+ margin-bottom: 0.6rem;
+}
+
+.reminders-section__title {
+ font-family: var(--font-serif);
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--gold-light);
+}
+
+.reminders-list {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.reminder-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.65rem;
+ padding: 0.65rem 1rem;
+ border-left: 3px solid transparent;
+ font-size: 0.82rem;
+ line-height: 1.45;
+ color: var(--text);
+ background: var(--navy-mid);
+ border-bottom: 1px solid rgba(201,168,76,0.06);
+}
+
+.reminder-item:first-child {
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+}
+
+.reminder-item:last-child {
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
+ border-bottom: none;
+}
+
+.reminder-item:only-child {
+ border-radius: var(--radius-lg);
+}
+
+.reminder-item--urgent {
+ border-left-color: var(--red);
+ background: linear-gradient(90deg, rgba(217,83,79,0.08) 0%, var(--navy-mid) 30%);
+}
+
+.reminder-item--warning {
+ border-left-color: var(--amber);
+ background: linear-gradient(90deg, rgba(240,173,78,0.06) 0%, var(--navy-mid) 30%);
+}
+
+.reminder-item--info {
+ border-left-color: var(--gold-dim);
+}
+
+.reminder-item__icon {
+ flex-shrink: 0;
+ font-size: 0.55rem;
+ margin-top: 0.35rem;
+}
+
+.reminder-item--urgent .reminder-item__icon { color: var(--red); }
+.reminder-item--warning .reminder-item__icon { color: var(--amber); }
+.reminder-item--info .reminder-item__icon { color: var(--gold-dim); }
+
+.reminder-item__body strong {
+ color: var(--cream);
+}
+
+.reminder-item__body em {
+ color: var(--gold-light);
+ font-style: italic;
+}
+
+/* --- Action Items --- */
+.action-items {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 0.75rem;
+}
+
+.action-card {
+ display: flex;
+ gap: 0.75rem;
+ padding: 0.85rem 1rem;
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+}
+
+.action-card__indicator {
+ width: 3px;
+ align-self: stretch;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.action-card--blocked .action-card__indicator { background: var(--red); }
+.action-card--hold .action-card__indicator { background: var(--amber); }
+
+.action-card__label {
+ font-size: 0.6rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 0.15rem;
+}
+
+.action-card--blocked .action-card__label { color: var(--red); }
+.action-card--hold .action-card__label { color: var(--amber); }
+
+.action-card__title {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--cream);
+ margin-bottom: 0.15rem;
+}
+
+.action-card__detail {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ line-height: 1.4;
+}
+
+/* --- Compact Stats Row (briefing) --- */
+.stats-row--compact {
+ grid-template-columns: repeat(5, 1fr);
+ gap: 0.65rem;
+ margin-bottom: 1.25rem;
+}
+
+.stat-card--mini {
+ padding: 0.75rem 1rem;
+ text-align: center;
+}
+
+.stat-card--mini .stat-card__value {
+ font-size: 1.5rem;
+ margin-bottom: 0.1rem;
+}
+
+.stat-card--mini .stat-card__label {
+ margin-bottom: 0.15rem;
+}
+
+.stat-card--mini .stat-card__detail {
+ margin-top: 0.1rem;
+ font-size: 0.68rem;
+}
+
+/* --- Compact Task Table (Top 5) --- */
+.task-table-wrap--compact {
+ max-height: none;
+}
+
+.task-table--dense th {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.65rem;
+}
+
+.task-table--dense td {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8rem;
+}
+
+/* --- Health Inline (exception-based in briefing) --- */
+.health-inline {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.6rem 1rem;
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.8rem;
+}
+
+.health-inline__dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.health-inline__dot--ok { background: var(--green); }
+.health-inline__dot--warn { background: var(--amber); }
+.health-inline__dot--error { background: var(--red); }
+
+.health-inline__text {
+ font-weight: 600;
+ color: var(--cream);
+}
+
+.health-inline__detail {
+ color: var(--text-muted);
+ font-size: 0.75rem;
+ margin-left: auto;
+}
+
+.health-inline--warn {
+ border-color: rgba(240,173,78,0.3);
+ background: linear-gradient(90deg, rgba(240,173,78,0.08) 0%, var(--navy-mid) 40%);
+}
+
+.health-inline--error {
+ border-color: rgba(217,83,79,0.3);
+ background: linear-gradient(90deg, rgba(217,83,79,0.08) 0%, var(--navy-mid) 40%);
+}
+
+/* --- Section (tighter variant for briefing) --- */
+.section--tight {
+ margin-bottom: 1.25rem;
+}
+
+.section--tight .section__header {
+ margin-bottom: 0.65rem;
+}
+
+/* --- Section link inside badge --- */
+.section__link {
+ color: var(--gold-light);
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.section__link:hover {
+ text-decoration: underline;
+}
+
+/* ============================================================
+ SHARED COMPONENT STYLES (all tabs)
+ ============================================================ */
+
+/* --- Stat Cards Row --- */
+.stats-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+
+.stat-card {
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 1.25rem 1.5rem;
+ position: relative;
+ overflow: hidden;
+ transition: transform var(--transition), box-shadow var(--transition);
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow);
+}
+
+.stat-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+}
+
+.stat-card--gold::before { background: var(--gold); }
+.stat-card--green::before { background: var(--green); }
+.stat-card--amber::before { background: var(--amber); }
+.stat-card--red::before { background: var(--red); }
+.stat-card--blue::before { background: #5b9bd5; }
+
+.stat-card__label {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--text-muted);
+ margin-bottom: 0.5rem;
+}
+
+.stat-card__value {
+ font-family: var(--font-serif);
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--cream);
+ line-height: 1;
+}
+
+.stat-card__detail {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ margin-top: 0.35rem;
+}
+
+/* --- Section --- */
+.section {
+ margin-bottom: 2rem;
+}
+
+.section__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+}
+
+.section__title {
+ font-family: var(--font-serif);
+ font-size: 1.15rem;
+ font-weight: 700;
+ color: var(--cream);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section__title .icon {
+ color: var(--gold);
+}
+
+.section__badge {
+ background: rgba(201,168,76,0.15);
+ color: var(--gold);
+ font-size: 0.7rem;
+ font-weight: 700;
+ padding: 0.25rem 0.7rem;
+ border-radius: 20px;
+ border: 1px solid var(--border);
+}
+
+/* --- Task Table --- */
+.task-table-wrap {
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+}
+
+.task-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.task-table thead {
+ background: rgba(201,168,76,0.08);
+}
+
+.task-table th {
+ text-align: left;
+ padding: 0.75rem 1rem;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--gold);
+ font-weight: 700;
+ border-bottom: 1px solid var(--border);
+ white-space: nowrap;
+}
+
+.task-table td {
+ padding: 0.7rem 1rem;
+ font-size: 0.85rem;
+ border-bottom: 1px solid rgba(201,168,76,0.06);
+ color: var(--text);
+ vertical-align: middle;
+}
+
+.task-table tbody tr {
+ transition: background var(--transition);
+}
+
+.task-table tbody tr:hover {
+ background: rgba(201,168,76,0.04);
+}
+
+.task-table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+.task-table__num {
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+ width: 40px;
+}
+
+.task-table__title {
+ font-weight: 600;
+ color: var(--cream-light);
+}
+
+.task-table__company {
+ color: var(--gold-light);
+ font-weight: 500;
+}
+
+.task-table__keyword {
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+.task-table__status {
+ width: 100px;
+}
+
+.status-pill {
+ display: inline-block;
+ padding: 0.2rem 0.6rem;
+ border-radius: 12px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.status-pill--todo {
+ background: rgba(91,155,213,0.15);
+ color: #5b9bd5;
+ border: 1px solid rgba(91,155,213,0.25);
+}
+
+.status-pill--progress {
+ background: rgba(240,173,78,0.15);
+ color: var(--amber);
+ border: 1px solid rgba(240,173,78,0.25);
+}
+
+.status-pill--done {
+ background: rgba(76,175,80,0.15);
+ color: var(--green);
+ border: 1px solid rgba(76,175,80,0.25);
+}
+
+/* --- System Health Panel --- */
+.health-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 1rem;
+}
+
+.health-card {
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 1.25rem;
+}
+
+.health-card__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.75rem;
+}
+
+.health-card__title {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--cream);
+}
+
+.health-card__status {
+ font-size: 0.7rem;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.health-card__status--ok { color: var(--green); }
+.health-card__status--warn { color: var(--amber); }
+.health-card__status--error { color: var(--red); }
+
+.health-bar {
+ height: 6px;
+ background: rgba(255,255,255,0.06);
+ border-radius: 3px;
+ overflow: hidden;
+ margin-bottom: 0.5rem;
+}
+
+.health-bar__fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.6s ease;
+}
+
+.health-bar__fill--green { background: var(--green); }
+.health-bar__fill--amber { background: var(--amber); }
+.health-bar__fill--red { background: var(--red); }
+
+.health-card__meta {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ display: flex;
+ justify-content: space-between;
+}
+
+/* --- Log Panel --- */
+.log-list {
+ list-style: none;
+}
+
+.log-item {
+ display: flex;
+ gap: 1rem;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid rgba(201,168,76,0.06);
+ font-size: 0.82rem;
+ transition: background var(--transition);
+}
+
+.log-item:hover {
+ background: rgba(201,168,76,0.04);
+}
+
+.log-item__time {
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ min-width: 50px;
+}
+
+.log-item__type {
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 0.65rem;
+ padding: 0.15rem 0.45rem;
+ border-radius: 4px;
+ white-space: nowrap;
+ align-self: flex-start;
+ margin-top: 0.1rem;
+}
+
+.log-item__type--heartbeat {
+ background: rgba(76,175,80,0.15);
+ color: var(--green);
+}
+
+.log-item__type--execution {
+ background: rgba(91,155,213,0.15);
+ color: #5b9bd5;
+}
+
+.log-item__type--error {
+ background: rgba(217,83,79,0.15);
+ color: var(--red);
+}
+
+.log-item__type--user {
+ background: rgba(201,168,76,0.15);
+ color: var(--gold);
+}
+
+.log-item__msg {
+ color: var(--text);
+ line-height: 1.45;
+}
+
+/* --- Queue Panel --- */
+.queue-card {
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 1.25rem;
+ margin-bottom: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ transition: transform var(--transition), box-shadow var(--transition);
+}
+
+.queue-card:hover {
+ transform: translateX(4px);
+ box-shadow: var(--shadow);
+}
+
+.queue-card__priority {
+ width: 4px;
+ align-self: stretch;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.queue-card__priority--high { background: var(--red); }
+.queue-card__priority--med { background: var(--amber); }
+.queue-card__priority--low { background: var(--green); }
+
+.queue-card__body {
+ flex: 1;
+}
+
+.queue-card__title {
+ font-weight: 600;
+ color: var(--cream);
+ font-size: 0.9rem;
+ margin-bottom: 0.15rem;
+}
+
+.queue-card__meta {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+}
+
+.queue-card__status {
+ text-align: right;
+}
+
+/* --- PR Tab --- */
+.pr-card {
+ background: var(--navy-mid);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.pr-card__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 0.75rem;
+}
+
+.pr-card__company {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--gold);
+ font-weight: 700;
+}
+
+.pr-card__headline {
+ font-family: var(--font-serif);
+ font-size: 1.1rem;
+ color: var(--cream);
+ font-weight: 700;
+ line-height: 1.3;
+ margin-top: 0.25rem;
+}
+
+.pr-card__detail {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ margin-top: 0.5rem;
+}
+
+/* --- Responsive --- */
+@media (max-width: 900px) {
+ .dashboard {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ display: none;
+ position: fixed;
+ top: 64px;
+ left: 0;
+ bottom: 0;
+ width: 260px;
+ z-index: 100;
+ box-shadow: var(--shadow-lg);
+ }
+
+ .sidebar.open {
+ display: flex;
+ }
+
+ .mobile-toggle {
+ display: flex !important;
+ }
+
+ .main {
+ padding: 1.25rem 1rem;
+ }
+
+ .stats-row {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .stats-row--compact {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .task-table-wrap {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .task-table {
+ min-width: 700px;
+ }
+
+ .page-header__greeting {
+ font-size: 1.35rem;
+ }
+
+ .briefing-header {
+ flex-direction: column;
+ gap: 0.75rem;
+ }
+
+ .briefing-header__quote {
+ max-width: 100%;
+ }
+
+ .action-items {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 500px) {
+ .stats-row {
+ grid-template-columns: 1fr;
+ }
+
+ .stats-row--compact {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .topbar {
+ padding: 0 1rem;
+ height: 56px;
+ }
+
+ .topbar__subtitle {
+ display: none;
+ }
+
+ .health-inline {
+ flex-wrap: wrap;
+ }
+
+ .health-inline__detail {
+ margin-left: 0;
+ width: 100%;
+ padding-left: 1.4rem;
+ }
+}
+
+/* --- Utility --- */
+.mobile-toggle {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ background: transparent;
+ color: var(--cream);
+ cursor: pointer;
+ font-size: 1.1rem;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ border: 0;
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--slate-light);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--gold-dim);
+}
+
+/* Selection */
+::selection {
+ background: var(--gold);
+ color: var(--navy);
+}