From 84a37bb7edf896941dd3ee2fb68fb3957219cae7 Mon Sep 17 00:00:00 2001 From: artheru Date: Wed, 10 Jun 2026 07:02:41 +0800 Subject: [PATCH] live system status page (0.46.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new #/status route: probes Tavern/API/Docs/Git/Webagent and shows a per-service dashboard (pulsing beacon, colored pills, latency, state) - same-origin /api/health gives real status+latency; cross-origin services use a no-cors reachability probe with a 6s timeout - auto re-checks every 20s + manual Re-check; honest footer note on method - linked from the footer Platform column - audited PM (whiteboards/netdisk/portfolio) via a throwaway admin — all clean, no changes needed; account removed after - verified live: all 5 services operational --- index.html | 2 +- package.json | 2 +- src/Scene2D.jsx | 65 +++++++++++++++++++++++++++++++++++++++++++++++-- src/styles.css | 27 ++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 75a44aa..93e9abe 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + diff --git a/package.json b/package.json index 14614e8..bef7ca2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.45.0-motion", + "version": "0.46.0-status", "private": true, "type": "module", "scripts": { diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index a0d2089..b7035c1 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -7,7 +7,7 @@ import Account from './Account.jsx'; import TavernGame from './TavernGame.jsx'; import { api } from './api.js'; -const RELEASE = '0.45.0-motion'; +const RELEASE = '0.46.0-status'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -15,7 +15,7 @@ const NAV = [ { id: 'mansion', label: 'Mansion' }, { id: 'booth', label: 'My Booth' }, ]; -const VALID_ROUTES = ['home', 'agents', 'mansion', 'booth', 'account', 'admin', 'signin']; +const VALID_ROUTES = ['home', 'agents', 'mansion', 'booth', 'account', 'admin', 'signin', 'status']; function useHashRoute() { const normalize = () => { @@ -209,6 +209,7 @@ function Footer({ setRoute }) {
Platform
Git Webagent +
© 2026 amerc · agent mercenary tavern · v{RELEASE}
@@ -254,6 +255,65 @@ function Gatehouse({ auth, setRoute }) { ); } +const STATUS_TARGETS = [ + { key: 'web', name: 'Tavern (web)', url: 'https://amerc.ai/', mode: 'reach' }, + { key: 'api', name: 'amerc API', url: '/api/health', mode: 'json' }, + { key: 'docs', name: 'Docs', url: 'https://docs.amerc.ai/', mode: 'reach' }, + { key: 'git', name: 'Git', url: 'https://git.amerc.ai/', mode: 'reach' }, + { key: 'agent', name: 'Webagent relay', url: 'https://webagent.amerc.ai/health', mode: 'reach' }, +]; +async function probe(t) { + const ctrl = new AbortController(); const to = setTimeout(() => ctrl.abort(), 6000); const t0 = performance.now(); + try { + if (t.mode === 'json') { const r = await fetch(t.url, { signal: ctrl.signal, cache: 'no-store' }); return { ok: r.ok, ms: Math.round(performance.now() - t0) }; } + await fetch(t.url, { mode: 'no-cors', signal: ctrl.signal, cache: 'no-store' }); return { ok: true, ms: Math.round(performance.now() - t0) }; + } catch { return { ok: false, ms: Math.round(performance.now() - t0) }; } + finally { clearTimeout(to); } +} +function StatusPage() { + const [res, setRes] = useState({}); + const run = useCallback(() => { + STATUS_TARGETS.forEach(async (t) => { + setRes((r) => ({ ...r, [t.key]: { ...r[t.key], loading: true } })); + const o = await probe(t); + setRes((r) => ({ ...r, [t.key]: { ...o, loading: false } })); + }); + }, []); + useEffect(() => { run(); const id = setInterval(run, 20000); return () => clearInterval(id); }, [run]); + const known = STATUS_TARGETS.map((t) => res[t.key]).filter((x) => x && !x.loading); + const allUp = known.length === STATUS_TARGETS.length && known.every((x) => x.ok); + const anyDown = known.some((x) => !x.ok); + return ( +
+

System status

+
+ +
+ {!known.length ? 'Checking services…' : allUp ? 'All systems operational' : anyDown ? 'Some services are degraded' : 'Checking…'} +

Live health of the amerc edge and its services · re-checks every 20s.

+
+ +
+
+ {STATUS_TARGETS.map((t) => { + const o = res[t.key] || {}; + const state = o.loading || o.ok == null ? 'checking' : o.ok ? 'up' : 'down'; + return ( +
+ + {t.name} + {t.url.replace('https://', '').replace(/\/$/, '')} + {o.ms != null && !o.loading ? `${o.ms} ms` : '·'} + {state === 'up' ? 'operational' : state === 'down' ? 'unreachable' : 'checking…'} +
+ ); + })} +
+

amerc runs on a single edge node — nginx, a zero-dependency Node API, Gitea, and the webagent + showcase relays. Cross-origin checks measure reachability from your browser, so transient network blips can show as degraded.

+
+ ); +} + function Page({ children, setRoute }) { return
{children}
; } function Scene({ route, setRoute, auth }) { @@ -262,6 +322,7 @@ function Scene({ route, setRoute, auth }) { if (route === 'mansion') return ; if (route === 'booth') return ; if (route === 'account') return ; + if (route === 'status') return ; if (route === 'signin') return ; if (route === 'admin') return
BACKDOOR

Quartermaster

Tenant, key, session and risk controls.

diff --git a/src/styles.css b/src/styles.css index 167257c..c8d185e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1274,3 +1274,30 @@ button { .reveal:not(.in) { opacity: 0; transform: translateY(18px); } .reveal.in { opacity: 1; } @media (prefers-reduced-motion: reduce) { .reveal, .reveal:not(.in) { opacity: 1 !important; transform: none !important; transition: none !important; } } + +/* system status page (0.46) */ +.st { max-width: 760px; margin: 0 auto; } +.st-title { font-family: Georgia, "Times New Roman", serif; font-size: 24px; color: #fff; margin: 0 0 16px; } +.st-banner { display: flex; align-items: center; gap: 14px; padding: 16px 18px; border-radius: 14px; margin-bottom: 16px; + background: linear-gradient(180deg, #160f24, #120c1d); border: 1px solid #2a1f3e; } +.st-banner.up { border-color: #2f6e52; box-shadow: inset 0 0 0 1px rgba(54,240,176,0.08); } +.st-banner.down { border-color: #7a3030; box-shadow: inset 0 0 0 1px rgba(255,122,140,0.1); } +.st-banner-txt { flex: 1; } +.st-banner-txt strong { display: block; color: #fff; font-size: 16px; } +.st-banner-txt p { margin: 3px 0 0; color: #9d8fc0; font-size: 12.5px; } +.st-beacon { width: 12px; height: 12px; border-radius: 50%; background: #f2b85f; box-shadow: 0 0 0 0 rgba(242,184,95,0.5); animation: mnpulse 2s infinite; } +.st-banner.up .st-beacon { background: #36f0b0; box-shadow: 0 0 0 0 rgba(54,240,176,0.5); } +.st-banner.down .st-beacon { background: #ff7a8c; box-shadow: 0 0 0 0 rgba(255,122,140,0.5); animation: none; } +.st-list { display: flex; flex-direction: column; gap: 1px; background: #241a36; border: 1px solid #241a36; border-radius: 12px; overflow: hidden; } +.st-row { display: flex; align-items: center; gap: 12px; padding: 13px 16px; background: #130d20; } +.st-pill { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; } +.st-pill.up { background: #36f0b0; box-shadow: 0 0 8px rgba(54,240,176,0.6); } +.st-pill.down { background: #ff7a8c; box-shadow: 0 0 8px rgba(255,122,140,0.5); } +.st-pill.checking { background: #6a5a88; } +.st-name { color: #eaf2ff; font-size: 14px; min-width: 130px; } +.st-host { color: #7a6a98; font-size: 12px; font-family: ui-monospace, monospace; flex: 1; } +.st-ms { color: #9d8fc0; font-size: 12px; font-variant-numeric: tabular-nums; min-width: 56px; text-align: right; } +.st-state { font-size: 11px; font-weight: 600; letter-spacing: 0.3px; min-width: 92px; text-align: right; } +.st-state.up { color: #36f0b0; } .st-state.down { color: #ff7a8c; } .st-state.checking { color: #b6a7d4; } +.st-foot { color: #7a6a98; font-size: 12px; line-height: 1.6; margin: 14px 2px 0; } +@media (max-width: 560px) { .st-host { display: none; } .st-name { min-width: 0; flex: 1; } }