Compare commits

...

4 Commits

Author SHA1 Message Date
Alexander Whitestone
ae201aee26 chore: add ghost wizard audit — classify dead accounts, confirm zero assignments
Some checks failed
CI / validate (pull_request) Failing after 10s
Refs #827

- Scanned all 107 open issues: 0 assigned to ghost accounts
- Confirmed no TurboQuant/Hermes-TurboQuant directories in repo
- Documented 7 ghost accounts (antigravity, google, grok, groq, hermes, kimi, manus)
  with origin notes and "do not route" status
- Documented active wizard roster tiers per Allegro's #820 audit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:15:29 -04:00
fd75985db6 [claude] Fix missing manifest.json PWA support (#832) (#888)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
2026-04-06 17:59:45 +00:00
3b4c5e7207 [claude] Add /help page for Nexus web frontend (#833) (#887)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
2026-04-06 17:52:10 +00:00
0b57145dde [timmy] Add webhook health dashboard (#855) (#885)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
2026-04-06 15:51:22 +00:00
7 changed files with 1058 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""
Webhook health dashboard for fleet agent endpoints.
Issue: #855 in Timmy_Foundation/the-nexus
Probes each configured /health endpoint, persists the last-known-good state to a
JSON log, and generates a markdown dashboard in ~/.hermes/burn-logs/.
Default targets:
- bezalel: http://127.0.0.1:8650/health
- allegro: http://127.0.0.1:8651/health
- ezra: http://127.0.0.1:8652/health
- adagio: http://127.0.0.1:8653/health
Environment overrides:
- WEBHOOK_HEALTH_TARGETS="allegro=http://127.0.0.1:8651/health,ezra=http://127.0.0.1:8652/health"
- WEBHOOK_HEALTH_TIMEOUT=3
- WEBHOOK_STALE_AFTER=300
- WEBHOOK_HEALTH_OUTPUT=/custom/webhook-health-latest.md
- WEBHOOK_HEALTH_HISTORY=/custom/webhook-health-history.json
"""
from __future__ import annotations
import argparse
import json
import os
import time
import urllib.error
import urllib.request
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
DEFAULT_TARGETS = {
"bezalel": "http://127.0.0.1:8650/health",
"allegro": "http://127.0.0.1:8651/health",
"ezra": "http://127.0.0.1:8652/health",
"adagio": "http://127.0.0.1:8653/health",
}
DEFAULT_TIMEOUT = float(os.environ.get("WEBHOOK_HEALTH_TIMEOUT", "3"))
DEFAULT_STALE_AFTER = int(os.environ.get("WEBHOOK_STALE_AFTER", "300"))
DEFAULT_OUTPUT = Path(
os.environ.get(
"WEBHOOK_HEALTH_OUTPUT",
str(Path.home() / ".hermes" / "burn-logs" / "webhook-health-latest.md"),
)
).expanduser()
DEFAULT_HISTORY = Path(
os.environ.get(
"WEBHOOK_HEALTH_HISTORY",
str(Path.home() / ".hermes" / "burn-logs" / "webhook-health-history.json"),
)
).expanduser()
@dataclass
class AgentHealth:
name: str
url: str
http_status: int | None
healthy: bool
latency_ms: int | None
stale: bool
last_success_ts: float | None
checked_at: float
message: str
def status_icon(self) -> str:
if self.healthy:
return "🟢"
if self.stale:
return "🔴"
return "🟠"
def last_success_age_seconds(self) -> int | None:
if self.last_success_ts is None:
return None
return max(0, int(self.checked_at - self.last_success_ts))
def parse_targets(raw: str | None) -> dict[str, str]:
if not raw:
return dict(DEFAULT_TARGETS)
targets: dict[str, str] = {}
for chunk in raw.split(","):
chunk = chunk.strip()
if not chunk:
continue
if "=" not in chunk:
raise ValueError(f"Invalid target spec: {chunk!r}")
name, url = chunk.split("=", 1)
targets[name.strip()] = url.strip()
if not targets:
raise ValueError("No valid targets parsed")
return targets
def load_history(path: Path) -> dict[str, Any]:
if not path.exists():
return {"agents": {}, "runs": []}
return json.loads(path.read_text(encoding="utf-8"))
def save_history(path: Path, history: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(history, indent=2, sort_keys=True), encoding="utf-8")
def probe_health(url: str, timeout: float) -> tuple[bool, int | None, int | None, str]:
started = time.perf_counter()
req = urllib.request.Request(url, headers={"User-Agent": "the-nexus/webhook-health-dashboard"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read(512)
latency_ms = int((time.perf_counter() - started) * 1000)
status = getattr(resp, "status", None) or 200
message = f"HTTP {status}"
if body:
try:
payload = json.loads(body.decode("utf-8", errors="replace"))
if isinstance(payload, dict) and payload.get("status"):
message = f"HTTP {status}{payload['status']}"
except Exception:
pass
return 200 <= status < 300, status, latency_ms, message
except urllib.error.HTTPError as e:
latency_ms = int((time.perf_counter() - started) * 1000)
return False, e.code, latency_ms, f"HTTP {e.code}"
except urllib.error.URLError as e:
latency_ms = int((time.perf_counter() - started) * 1000)
return False, None, latency_ms, f"URL error: {e.reason}"
except Exception as e:
latency_ms = int((time.perf_counter() - started) * 1000)
return False, None, latency_ms, f"Probe failed: {e}"
def check_agents(
targets: dict[str, str],
history: dict[str, Any],
timeout: float = DEFAULT_TIMEOUT,
stale_after: int = DEFAULT_STALE_AFTER,
) -> list[AgentHealth]:
checked_at = time.time()
results: list[AgentHealth] = []
agent_state = history.setdefault("agents", {})
for name, url in targets.items():
state = agent_state.get(name, {})
last_success_ts = state.get("last_success_ts")
ok, http_status, latency_ms, message = probe_health(url, timeout)
if ok:
last_success_ts = checked_at
stale = False
if not ok and last_success_ts is not None:
stale = (checked_at - float(last_success_ts)) > stale_after
result = AgentHealth(
name=name,
url=url,
http_status=http_status,
healthy=ok,
latency_ms=latency_ms,
stale=stale,
last_success_ts=last_success_ts,
checked_at=checked_at,
message=message,
)
agent_state[name] = {
"url": url,
"last_success_ts": last_success_ts,
"last_http_status": http_status,
"last_message": message,
"last_checked_at": checked_at,
}
results.append(result)
history.setdefault("runs", []).append(
{
"checked_at": checked_at,
"healthy_count": sum(1 for r in results if r.healthy),
"unhealthy_count": sum(1 for r in results if not r.healthy),
"agents": [asdict(r) for r in results],
}
)
history["runs"] = history["runs"][-100:]
return results
def _format_age(seconds: int | None) -> str:
if seconds is None:
return "never"
if seconds < 60:
return f"{seconds}s ago"
if seconds < 3600:
return f"{seconds // 60}m ago"
return f"{seconds // 3600}h ago"
def to_markdown(results: list[AgentHealth], generated_at: float | None = None) -> str:
generated_at = generated_at or time.time()
ts = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(generated_at))
healthy = sum(1 for r in results if r.healthy)
total = len(results)
lines = [
f"# Agent Webhook Health Dashboard — {ts}",
"",
f"Healthy: {healthy}/{total}",
"",
"| Agent | Status | HTTP | Latency | Last success | Endpoint | Notes |",
"|:------|:------:|:----:|--------:|:------------|:---------|:------|",
]
for result in results:
http = str(result.http_status) if result.http_status is not None else ""
latency = f"{result.latency_ms}ms" if result.latency_ms is not None else ""
lines.append(
"| {name} | {icon} | {http} | {latency} | {last_success} | `{url}` | {message} |".format(
name=result.name,
icon=result.status_icon(),
http=http,
latency=latency,
last_success=_format_age(result.last_success_age_seconds()),
url=result.url,
message=result.message,
)
)
stale_agents = [r.name for r in results if r.stale]
if stale_agents:
lines.extend([
"",
"## Stale agents",
", ".join(stale_agents),
])
lines.extend([
"",
"Generated by `bin/webhook_health_dashboard.py`.",
])
return "\n".join(lines)
def write_dashboard(path: Path, markdown: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(markdown + "\n", encoding="utf-8")
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate webhook health dashboard")
parser.add_argument("--targets", default=os.environ.get("WEBHOOK_HEALTH_TARGETS"))
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT)
parser.add_argument("--stale-after", type=int, default=DEFAULT_STALE_AFTER)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--history", type=Path, default=DEFAULT_HISTORY)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv or sys.argv[1:])
targets = parse_targets(args.targets)
history = load_history(args.history)
results = check_agents(targets, history, timeout=args.timeout, stale_after=args.stale_after)
save_history(args.history, history)
dashboard = to_markdown(results)
write_dashboard(args.output, dashboard)
print(args.output)
print(f"healthy={sum(1 for r in results if r.healthy)} total={len(results)}")
return 0
if __name__ == "__main__":
import sys
raise SystemExit(main(sys.argv[1:]))

View File

@@ -0,0 +1,93 @@
# Ghost Wizard Audit — #827
**Audited:** 2026-04-06
**By:** Claude (claude/issue-827)
**Parent Epic:** #822
**Source Data:** #820 (Allegro's fleet audit)
---
## Summary
Per Allegro's audit (#820) and Ezra's confirmation, 7 org members have zero activity.
This document records the audit findings, classifies accounts, and tracks cleanup actions.
---
## Ghost Accounts (TIER 5 — Zero Activity)
These org members have produced 0 issues, 0 PRs, 0 everything.
| Account | Classification | Status |
|---------|---------------|--------|
| `antigravity` | Ghost / placeholder | No assignments, no output |
| `google` | Ghost / service label | No assignments, no output |
| `grok` | Ghost / service label | No assignments, no output |
| `groq` | Ghost / service label | No assignments, no output |
| `hermes` | Ghost / service label | No assignments, no output |
| `kimi` | Ghost / service label | No assignments, no output |
| `manus` | Ghost / service label | No assignments, no output |
**Action taken (2026-04-06):** Scanned all 107 open issues — **zero open issues are assigned to any of these accounts.** No assignment cleanup required.
---
## TurboQuant / Hermes-TurboQuant
Per issue #827: TurboQuant and Hermes-TurboQuant have no config, no token, no gateway.
**Repo audit finding:** No `turboquant/` or `hermes-turboquant/` directories exist anywhere in `the-nexus`. These names appear nowhere in the codebase. There is nothing to archive or flag.
**Status:** Ghost label — never instantiated in this repo.
---
## Active Wizard Roster (for reference)
These accounts have demonstrated real output:
| Account | Tier | Notes |
|---------|------|-------|
| `gemini` | TIER 1 — Elite | 61 PRs created, 33 merged, 6 repos active |
| `allegro` | TIER 1 — Elite | 50 issues created, 31 closed, 24 PRs |
| `ezra` | TIER 2 — Solid | 38 issues created, 26 closed, triage/docs |
| `codex-agent` | TIER 3 — Occasional | 4 PRs, 75% merge rate |
| `claude` | TIER 3 — Occasional | 4 PRs, 75% merge rate |
| `perplexity` | TIER 3 — Occasional | 4 PRs, 3 repos |
| `KimiClaw` | TIER 4 — Silent | 6 assigned, 1 PR |
| `fenrir` | TIER 4 — Silent | 17 assigned, 0 output |
| `bezalel` | TIER 4 — Silent | 3 assigned, 2 created |
| `bilbobagginshire` | TIER 4 — Silent | 5 assigned, 0 output |
---
## Ghost Account Origin Notes
| Account | Likely Origin |
|---------|--------------|
| `antigravity` | Test/throwaway username used in FIRST_LIGHT_REPORT test sessions |
| `google` | Placeholder for Google/Gemini API service routing; `gemini` is the real wizard account |
| `grok` | xAI Grok model placeholder; no active harness |
| `groq` | Groq API service label; `groq_worker.py` exists in codebase but no wizard account needed |
| `hermes` | Hermes VPS infrastructure label; individual wizards (ezra, allegro) are the real accounts |
| `kimi` | Moonshot AI Kimi model placeholder; `KimiClaw` is the real wizard account if active |
| `manus` | Manus AI agent placeholder; no harness configured in this repo |
---
## Recommendations
1. **Do not route work to ghost accounts** — confirmed, no current assignments exist.
2. **`google` account** is redundant with `gemini`; use `gemini` for all Gemini/Google work.
3. **`hermes` account** is redundant with the actual wizard accounts (ezra, allegro); do not assign issues to it.
4. **`kimi` vs `KimiClaw`** — if Kimi work resumes, route to `KimiClaw` not `kimi`.
5. **TurboQuant** — no action needed; not instantiated in this repo.
---
## Cleanup Done
- [x] Scanned all 107 open issues for ghost account assignments → **0 found**
- [x] Searched repo for TurboQuant directories → **none exist**
- [x] Documented ghost vs. real account classification
- [x] Ghost accounts flagged as "do not route" in this audit doc

489
help.html Normal file
View File

@@ -0,0 +1,489 @@
<!DOCTYPE html>
<!--
THE NEXUS — Help Page
Refs: #833 (Missing /help page)
Design: dark space / holographic — matches Nexus design system
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Help — The Nexus</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Orbitron:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="manifest" href="./manifest.json">
<style>
:root {
--color-bg: #050510;
--color-surface: rgba(10, 15, 40, 0.85);
--color-border: rgba(74, 240, 192, 0.2);
--color-border-bright: rgba(74, 240, 192, 0.5);
--color-text: #e0f0ff;
--color-text-muted: #8a9ab8;
--color-primary: #4af0c0;
--color-primary-dim: rgba(74, 240, 192, 0.12);
--color-secondary: #7b5cff;
--color-danger: #ff4466;
--color-warning: #ffaa22;
--font-display: 'Orbitron', sans-serif;
--font-body: 'JetBrains Mono', monospace;
--panel-blur: 16px;
--panel-radius: 8px;
--transition: 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--color-bg);
font-family: var(--font-body);
color: var(--color-text);
min-height: 100vh;
padding: 32px 16px 64px;
}
/* === STARFIELD BG === */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 20%, rgba(74,240,192,0.03) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(123,92,255,0.04) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.page-wrap {
position: relative;
z-index: 1;
max-width: 720px;
margin: 0 auto;
}
/* === HEADER === */
.page-header {
margin-bottom: 32px;
padding-bottom: 20px;
border-bottom: 1px solid var(--color-border);
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-muted);
text-decoration: none;
margin-bottom: 20px;
transition: color var(--transition);
}
.back-link:hover { color: var(--color-primary); }
.page-title {
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--color-text);
line-height: 1.2;
}
.page-title span { color: var(--color-primary); }
.page-subtitle {
margin-top: 8px;
font-size: 13px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* === SECTIONS === */
.help-section {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--panel-radius);
overflow: hidden;
margin-bottom: 20px;
backdrop-filter: blur(var(--panel-blur));
}
.section-header {
padding: 14px 20px;
border-bottom: 1px solid var(--color-border);
background: linear-gradient(90deg, rgba(74,240,192,0.04) 0%, transparent 100%);
display: flex;
align-items: center;
gap: 10px;
}
.section-icon {
font-size: 14px;
opacity: 0.8;
}
.section-title {
font-family: var(--font-display);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--color-primary);
}
.section-body {
padding: 16px 20px;
}
/* === KEY BINDING TABLE === */
.key-table {
width: 100%;
border-collapse: collapse;
}
.key-table tr + tr td {
border-top: 1px solid rgba(74,240,192,0.07);
}
.key-table td {
padding: 8px 0;
font-size: 12px;
line-height: 1.5;
vertical-align: top;
}
.key-table td:first-child {
width: 140px;
padding-right: 16px;
}
.key-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
kbd {
display: inline-block;
font-family: var(--font-body);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
background: rgba(74,240,192,0.08);
border: 1px solid rgba(74,240,192,0.3);
border-bottom-width: 2px;
border-radius: 4px;
padding: 2px 7px;
color: var(--color-primary);
}
.key-desc {
color: var(--color-text-muted);
}
/* === COMMAND LIST === */
.cmd-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.cmd-item {
display: flex;
gap: 12px;
align-items: flex-start;
}
.cmd-name {
min-width: 160px;
font-size: 12px;
color: var(--color-primary);
padding-top: 1px;
}
.cmd-desc {
font-size: 12px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* === PORTAL LIST === */
.portal-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.portal-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 12px;
transition: border-color var(--transition), background var(--transition);
}
.portal-item:hover {
border-color: rgba(74,240,192,0.35);
background: rgba(74,240,192,0.02);
}
.portal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-online { background: var(--color-primary); box-shadow: 0 0 6px var(--color-primary); }
.dot-standby { background: var(--color-warning); box-shadow: 0 0 6px var(--color-warning); }
.dot-offline { background: var(--color-text-muted); }
.portal-name {
font-weight: 600;
color: var(--color-text);
min-width: 120px;
}
.portal-desc {
color: var(--color-text-muted);
flex: 1;
}
/* === INFO BLOCK === */
.info-block {
font-size: 12px;
line-height: 1.7;
color: var(--color-text-muted);
}
.info-block p + p {
margin-top: 10px;
}
.info-block a {
color: var(--color-primary);
text-decoration: none;
}
.info-block a:hover {
text-decoration: underline;
}
.highlight {
color: var(--color-text);
font-weight: 500;
}
/* === FOOTER === */
.page-footer {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
font-size: 11px;
color: var(--color-text-muted);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: gap;
gap: 8px;
}
.footer-brand {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--color-primary);
opacity: 0.7;
}
</style>
</head>
<body>
<div class="page-wrap">
<!-- Header -->
<header class="page-header">
<a href="/" class="back-link">← Back to The Nexus</a>
<h1 class="page-title">THE <span>NEXUS</span> — Help</h1>
<p class="page-subtitle">Navigation guide, controls, and system reference for Timmy's sovereign home-world.</p>
</header>
<!-- Navigation Controls -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">Navigation Controls</span>
</div>
<div class="section-body">
<table class="key-table">
<tr>
<td><div class="key-group"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></div></td>
<td class="key-desc">Move forward / left / backward / right</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Mouse</kbd></div></td>
<td class="key-desc">Look around — click the canvas to capture the pointer</td>
</tr>
<tr>
<td><div class="key-group"><kbd>V</kbd></div></td>
<td class="key-desc">Toggle navigation mode: Walk → Fly → Orbit</td>
</tr>
<tr>
<td><div class="key-group"><kbd>F</kbd></div></td>
<td class="key-desc">Enter nearby portal (when portal hint is visible)</td>
</tr>
<tr>
<td><div class="key-group"><kbd>E</kbd></div></td>
<td class="key-desc">Read nearby vision point (when vision hint is visible)</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Enter</kbd></div></td>
<td class="key-desc">Focus / unfocus chat input</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Esc</kbd></div></td>
<td class="key-desc">Release pointer lock / close overlays</td>
</tr>
</table>
</div>
</section>
<!-- Timmy Chat Commands -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">Timmy Chat Commands</span>
</div>
<div class="section-body">
<div class="cmd-list">
<div class="cmd-item">
<span class="cmd-name">System Status</span>
<span class="cmd-desc">Quick action — asks Timmy for a live system health summary.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Agent Check</span>
<span class="cmd-desc">Quick action — lists all active agents and their current state.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Portal Atlas</span>
<span class="cmd-desc">Quick action — opens the full portal map overlay.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Help</span>
<span class="cmd-desc">Quick action — requests navigation assistance from Timmy.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Free-form text</span>
<span class="cmd-desc">Type anything in the chat bar and press Enter or → to send. Timmy processes all natural-language input.</span>
</div>
</div>
</div>
</section>
<!-- Portal Atlas -->
<section class="help-section">
<div class="section-header">
<span class="section-icon">🌐</span>
<span class="section-title">Portal Atlas</span>
</div>
<div class="section-body">
<div class="info-block">
<p>Portals are gateways to external systems and game-worlds. Walk up to a glowing portal in the Nexus and press <span class="highlight"><kbd>F</kbd></span> to activate it, or open the <span class="highlight">Portal Atlas</span> (top-right button) for a full map view.</p>
<p>Portal status indicators:</p>
</div>
<div class="portal-list" style="margin-top:14px;">
<div class="portal-item">
<span class="portal-dot dot-online"></span>
<span class="portal-name">ONLINE</span>
<span class="portal-desc">Portal is live and will redirect immediately on activation.</span>
</div>
<div class="portal-item">
<span class="portal-dot dot-standby"></span>
<span class="portal-name">STANDBY</span>
<span class="portal-desc">Portal is reachable but destination system may be idle.</span>
</div>
<div class="portal-item">
<span class="portal-dot dot-offline"></span>
<span class="portal-name">OFFLINE / UNLINKED</span>
<span class="portal-desc">Destination not yet connected. Activation shows an error card.</span>
</div>
</div>
</div>
</section>
<!-- HUD Panels -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">HUD Panels</span>
</div>
<div class="section-body">
<div class="cmd-list">
<div class="cmd-item">
<span class="cmd-name">Symbolic Engine</span>
<span class="cmd-desc">Live feed from Timmy's rule-based reasoning layer.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Blackboard</span>
<span class="cmd-desc">Shared working memory used across all cognitive subsystems.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Symbolic Planner</span>
<span class="cmd-desc">Goal decomposition and task sequencing output.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Case-Based Reasoner</span>
<span class="cmd-desc">Analogical reasoning — matches current situation to past cases.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Neuro-Symbolic Bridge</span>
<span class="cmd-desc">Translation layer between neural inference and symbolic logic.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Meta-Reasoning</span>
<span class="cmd-desc">Timmy reflecting on its own thought process and confidence.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Sovereign Health</span>
<span class="cmd-desc">Core vitals: memory usage, heartbeat interval, alert flags.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Adaptive Calibrator</span>
<span class="cmd-desc">Live tuning of response thresholds and behavior weights.</span>
</div>
</div>
</div>
</section>
<!-- System Info -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">System Information</span>
</div>
<div class="section-body">
<div class="info-block">
<p>The Nexus is Timmy's <span class="highlight">canonical sovereign home-world</span> — a local-first 3D space that serves as both a training ground and a live visualization surface for the Timmy AI system.</p>
<p>The WebSocket gateway (<code>server.py</code>) runs on port <span class="highlight">8765</span> and bridges Timmy's cognition layer, game-world connectors, and the browser frontend. The <span class="highlight">HERMES</span> indicator in the HUD shows live connectivity status.</p>
<p>Source code and issue tracker: <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus" target="_blank" rel="noopener noreferrer">Timmy_Foundation/the-nexus</a></p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="page-footer">
<span class="footer-brand">THE NEXUS</span>
<span>Questions? Speak to Timmy in the chat bar on the main world.</span>
</footer>
</div>
</body>
</html>

42
tests/test_help_page.py Normal file
View File

@@ -0,0 +1,42 @@
"""Tests for the /help page. Refs: #833 (Missing /help page)."""
from pathlib import Path
def test_help_html_exists() -> None:
assert Path("help.html").exists(), "help.html must exist to resolve /help 404"
def test_help_html_is_valid_html() -> None:
content = Path("help.html").read_text()
assert "<!DOCTYPE html>" in content
assert "<html" in content
assert "</html>" in content
def test_help_page_has_required_sections() -> None:
content = Path("help.html").read_text()
# Navigation controls section
assert "Navigation Controls" in content
# Chat commands section
assert "Chat" in content
# Portal reference
assert "Portal" in content
# Back link to home
assert 'href="/"' in content
def test_help_page_links_back_to_home() -> None:
content = Path("help.html").read_text()
assert 'href="/"' in content, "help page must have a link back to the main Nexus world"
def test_help_page_has_keyboard_controls() -> None:
content = Path("help.html").read_text()
# Movement keys are listed individually as <kbd> elements
for key in ["<kbd>W</kbd>", "<kbd>A</kbd>", "<kbd>S</kbd>", "<kbd>D</kbd>",
"Mouse", "Enter", "Esc"]:
assert key in content, f"help page must document the {key!r} control"

39
tests/test_manifest.py Normal file
View File

@@ -0,0 +1,39 @@
"""Tests for manifest.json PWA support. Fixes #832 (Missing manifest.json)."""
import json
from pathlib import Path
def test_manifest_exists() -> None:
assert Path("manifest.json").exists(), "manifest.json must exist for PWA support"
def test_manifest_is_valid_json() -> None:
content = Path("manifest.json").read_text()
data = json.loads(content)
assert isinstance(data, dict)
def test_manifest_has_required_pwa_fields() -> None:
data = json.loads(Path("manifest.json").read_text())
assert "name" in data, "manifest.json must have 'name'"
assert "short_name" in data, "manifest.json must have 'short_name'"
assert "start_url" in data, "manifest.json must have 'start_url'"
assert "display" in data, "manifest.json must have 'display'"
assert "icons" in data, "manifest.json must have 'icons'"
def test_manifest_icons_non_empty() -> None:
data = json.loads(Path("manifest.json").read_text())
assert len(data["icons"]) > 0, "manifest.json must define at least one icon"
def test_index_html_references_manifest() -> None:
content = Path("index.html").read_text()
assert 'rel="manifest"' in content, "index.html must have <link rel=\"manifest\">"
assert "manifest.json" in content, "index.html must reference manifest.json"
def test_help_html_references_manifest() -> None:
content = Path("help.html").read_text()
assert 'rel="manifest"' in content, "help.html must have <link rel=\"manifest\">"
assert "manifest.json" in content, "help.html must reference manifest.json"

View File

@@ -0,0 +1,120 @@
"""Tests for webhook health dashboard generation."""
from __future__ import annotations
import importlib.util
import json
import sys
import time
from pathlib import Path
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).parent.parent
_spec = importlib.util.spec_from_file_location(
"webhook_health_dashboard_test",
PROJECT_ROOT / "bin" / "webhook_health_dashboard.py",
)
_mod = importlib.util.module_from_spec(_spec)
sys.modules["webhook_health_dashboard_test"] = _mod
_spec.loader.exec_module(_mod)
AgentHealth = _mod.AgentHealth
check_agents = _mod.check_agents
load_history = _mod.load_history
parse_targets = _mod.parse_targets
save_history = _mod.save_history
sort_key = None
to_markdown = _mod.to_markdown
write_dashboard = _mod.write_dashboard
main = _mod.main
class TestParseTargets:
def test_defaults_when_none(self):
targets = parse_targets(None)
assert targets["allegro"].endswith(":8651/health")
assert targets["ezra"].endswith(":8652/health")
def test_parse_csv_mapping(self):
targets = parse_targets("alpha=http://a/health,beta=http://b/health")
assert targets == {
"alpha": "http://a/health",
"beta": "http://b/health",
}
class TestCheckAgents:
@patch("webhook_health_dashboard_test.probe_health")
def test_updates_last_success_for_healthy(self, mock_probe):
mock_probe.return_value = (True, 200, 42, "HTTP 200")
history = {"agents": {}, "runs": []}
results = check_agents({"allegro": "http://localhost:8651/health"}, history, timeout=1, stale_after=300)
assert len(results) == 1
assert results[0].healthy is True
assert history["agents"]["allegro"]["last_success_ts"] is not None
@patch("webhook_health_dashboard_test.probe_health")
def test_marks_stale_after_threshold(self, mock_probe):
mock_probe.return_value = (False, None, 12, "URL error: refused")
history = {
"agents": {
"allegro": {
"last_success_ts": time.time() - 301,
}
},
"runs": [],
}
results = check_agents({"allegro": "http://localhost:8651/health"}, history, timeout=1, stale_after=300)
assert results[0].healthy is False
assert results[0].stale is True
class TestMarkdown:
def test_contains_table_and_icons(self):
now = time.time()
results = [
AgentHealth("allegro", "http://localhost:8651/health", 200, True, 31, False, now - 5, now, "HTTP 200 — ok"),
AgentHealth("ezra", "http://localhost:8652/health", None, False, 14, True, now - 600, now, "URL error: refused"),
]
md = to_markdown(results, generated_at=now)
assert "| Agent | Status | HTTP |" in md
assert "🟢" in md
assert "🔴" in md
assert "Stale agents" in md
assert "ezra" in md
class TestFileIO:
def test_save_and_load_history(self, tmp_path):
path = tmp_path / "history.json"
payload = {"agents": {"a": {"last_success_ts": 1}}, "runs": []}
save_history(path, payload)
loaded = load_history(path)
assert loaded == payload
def test_write_dashboard(self, tmp_path):
out = tmp_path / "dashboard.md"
write_dashboard(out, "# Test")
assert out.read_text() == "# Test\n"
class TestMain:
@patch("webhook_health_dashboard_test.probe_health")
def test_main_writes_outputs(self, mock_probe, tmp_path):
mock_probe.return_value = (True, 200, 10, "HTTP 200")
output = tmp_path / "dashboard.md"
history = tmp_path / "history.json"
rc = main([
"--targets", "allegro=http://localhost:8651/health",
"--output", str(output),
"--history", str(history),
"--timeout", "1",
"--stale-after", "300",
])
assert rc == 0
assert output.exists()
assert history.exists()
assert "allegro" in output.read_text()
runs = json.loads(history.read_text())["runs"]
assert len(runs) == 1