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 }) {
© 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.
+
+
Re-check
+
+
+ {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 ; }
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
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; } }