From e4dba9d264e46d072f914f937dfadf1931d1d4f9 Mon Sep 17 00:00:00 2001 From: artheru Date: Thu, 11 Jun 2026 09:35:34 +0800 Subject: [PATCH] quartermaster: fold PM + company tools into one command post (2.4.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new Quartermaster.jsx: the admin panel is now amerc's internal station β€” a command-post dashboard (live member/agent/online/session counts + quick links to git, docs, pm workspace, webagent, tavern) plus tabs for Tenants/keys/company (AdminConsole), Whiteboards, and Netdisk - Scene2D #/admin renders Quartermaster (AdminConsole no longer imported directly); pm.amerc.ai stays as a standalone too - verified locally: first signup=admin, all four tabs render Co-Authored-By: Claude Fable 5 --- index.html | 2 +- package.json | 2 +- src/Quartermaster.jsx | 114 ++++++++++++++++++++++++++++++++++++++++++ src/Scene2D.jsx | 11 ++-- src/styles.css | 25 +++++++++ 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 src/Quartermaster.jsx diff --git a/index.html b/index.html index bfb79db..a47b0d8 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ @media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } } - + diff --git a/package.json b/package.json index e3e15cb..7ccad16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "2.3.0-villa", + "version": "2.4.0-quartermaster", "private": true, "type": "module", "scripts": { diff --git a/src/Quartermaster.jsx b/src/Quartermaster.jsx new file mode 100644 index 0000000..fd0141f --- /dev/null +++ b/src/Quartermaster.jsx @@ -0,0 +1,114 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { AdminConsole } from './Auth.jsx'; +import Netdisk from './Netdisk.jsx'; +import Whiteboard from './Whiteboard.jsx'; +import { api } from './api.js'; + +// The Quartermaster β€” amerc's internal management station. One panel for +// members, agents, companies & products, whiteboards, netdisk, and quick +// links to every company tool (git, docs, pm, webagent). +const LINKS = [ + { ico: 'πŸ—‘οΈ', label: 'Git', sub: 'source Β· Gitea', href: 'https://git.amerc.ai/' }, + { ico: 'πŸ“„', label: 'Docs', sub: 'guides + API reference', href: 'https://docs.amerc.ai/' }, + { ico: '🧠', label: 'PM workspace', sub: 'standalone pm.amerc.ai', href: 'https://pm.amerc.ai/' }, + { ico: 'πŸ€–', label: 'Webagent', sub: 'relay health', href: 'https://webagent.amerc.ai/health' }, + { ico: '🏰', label: 'Tavern', sub: 'the public site', href: 'https://amerc.ai/' }, +]; + +function Boards() { + const [boards, setBoards] = useState([]); + const [open, setOpen] = useState(null); + const [err, setErr] = useState(''); + const load = useCallback(async () => { try { const d = await api('/boards'); setBoards(d.boards || []); } catch (e) { setErr(e.message); } }, []); + useEffect(() => { load(); }, [load]); + const create = async () => { + const name = window.prompt('Whiteboard name', 'New mindmap'); if (!name) return; + try { const r = await api('/boards', { method: 'POST', body: { name, data: JSON.stringify({ nodes: [], edges: [] }) } }); await load(); setOpen(r.id); } catch (e) { setErr(e.message); } + }; + const del = async (id) => { if (!window.confirm('Delete board?')) return; try { await api(`/boards/${id}`, { method: 'DELETE' }); load(); } catch (e) { setErr(e.message); } }; + if (open) return { setOpen(null); load(); }} />; + return ( +
+

Whiteboards

+ {err &&

{err}

} +
+ {boards.map((b) => ( +
setOpen(b.id)}> +
🧠
+ {b.name} + {(JSON.parse(b.links || '[]') || []).length} linked files + updated {new Date(b.updated_at).toLocaleString()} Β· {b.updated_by} + +
+ ))} + {!boards.length &&

No whiteboards yet. Create a mindmap and link netdisk files to its nodes.

} +
+
+ ); +} + +function CommandPost() { + const [stats, setStats] = useState(null); + const [members, setMembers] = useState(null); + useEffect(() => { + api('/agents/stats').then(setStats).catch(() => {}); + api('/admin/users').then((d) => setMembers((d.users || []).length)).catch(() => {}); + }, []); + const S = [ + ['members', members], ['agent classes', stats?.classes], ['online now', stats?.online], ['live sessions', stats?.sessions], + ]; + return ( +
+
+ {S.map(([label, v]) =>
{v ?? 'Β·'}{label}
)} +
+

Company tools

+
+ {LINKS.map((l) => ( + + +
{l.label}{l.sub}
+ +
+ ))} +
+
+ ); +} + +const QTABS = [ + { key: 'post', label: '⚜ Command post' }, + { key: 'ops', label: 'πŸ›‘ Tenants Β· keys Β· company' }, + { key: 'boards', label: '🧠 Whiteboards' }, + { key: 'disk', label: 'πŸ—„ Netdisk' }, +]; + +export default function Quartermaster({ auth }) { + const [tab, setTab] = useState('post'); + if (!auth.ready) return

Loading…

; + if (!auth.user) return ( +
+

Backdoor sealed

Quartermaster credentials required.

+ +
+ ); + if (auth.user.role !== 'admin') return

Not authorised

{auth.user.handle} is a member, not Quartermaster.

; + return ( +
+
+ ⚜ QUARTERMASTER +

amerc command post

+

The company's internal station β€” members, agents, companies & products, whiteboards, netdisk, and every tool in one place.

+
+
+ {QTABS.map((t) => )} +
+
+ {tab === 'post' && } + {tab === 'ops' && } + {tab === 'boards' && } + {tab === 'disk' &&

Netdisk

} +
+
+ ); +} diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 91eef0f..41ab2d5 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useAuth, AuthPanel, AdminConsole } from './Auth.jsx'; +import { useAuth, AuthPanel } from './Auth.jsx'; +import Quartermaster from './Quartermaster.jsx'; import { AgentBrowse, LegionDashboard } from './AgentPlatform.jsx'; import Mansion from './Mansion.jsx'; import Account from './Account.jsx'; @@ -11,7 +12,7 @@ import { api } from './api.js'; const Menu = ({ size = 20 }) => (); const X = ({ size = 20 }) => (); -const RELEASE = '2.3.0-villa'; +const RELEASE = '2.4.0-quartermaster'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -476,6 +477,7 @@ function StatusPage() { } const CHANGELOG = [ + { v: '2.4', date: 'Jun 2026', title: 'The Quartermaster command post', notes: ['The admin Quartermaster is now amerc’s internal management station β€” members, agents, companies & products, whiteboards and netdisk all in one panel', 'A command-post dashboard with live counts and quick links to git, docs, the PM workspace and the webagent relay'] }, { v: '2.3', date: 'Jun 2026', title: 'My Mansion gets its villa', notes: ['My Mansion is now a moonlit estate β€” your villa with glowing windows, and the services (Showcase, File-mapper, Net-disk) are chambers within it'] }, { v: '2.2', date: 'Jun 2026', title: 'My Legion becomes a war camp', notes: ['Your classes are banners and each instance is a soldier β€” alive and bobbing when online, standing by when pending, fallen when down', 'Tap a soldier to command it: chat with your own running agent, watch its live terminal, or stand it down', 'Statuses refresh on their own so the camp stays current'] }, { v: '2.1', date: 'Jun 2026', title: 'The roster comes through the door', notes: ['Browse Agents is now a doorway β€” the mercenaries descend and hang on wooden sign-bubbles showing what each one does', 'Open one and its detail opens in an ornate war-banner dialog; instances are shown as a squad β€” living soldiers when online, fallen when down'] }, @@ -557,10 +559,7 @@ function Scene({ route, setRoute, auth }) { if (route === 'status') return ; if (route === 'changelog') return ; if (route === 'signin') return ; - if (route === 'admin') return
-
BACKDOOR

Quartermaster

Tenant, key, session and risk controls.

- -
; + if (route === 'admin') return ; return ; } diff --git a/src/styles.css b/src/styles.css index ec282fc..affee03 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1831,3 +1831,28 @@ button { .mn-villa-body p { margin: 0 0 6px; color: #9b8fb8; font-size: 13.5px; line-height: 1.5; max-width: 540px; } .mn-wing-title { font-family: Georgia, serif; color: #cbb8e8; font-size: 16px; margin: 4px 2px 12px; letter-spacing: 0.5px; } @media (max-width: 680px) { .mn-villa { flex-direction: column; align-items: center; } .mn-villa-scene { width: 100%; max-width: 320px; } .mn-villa-body { align-items: center; text-align: center; } .mn-villa-tag { align-self: center; } } + +/* ===== Quartermaster = the command post (2.4) ===== */ +.qm { max-width: 940px; margin: 0 auto; } +.qm-head { margin-bottom: 14px; } +.qm-tag { font-size: 10px; letter-spacing: 1.6px; color: #ffd75e; background: rgba(255,215,94,0.08); border: 1px solid #5a4a1c; padding: 3px 10px; border-radius: 999px; } +.qm-head h2 { margin: 8px 0 4px; font-family: Georgia, serif; font-size: 24px; color: #f3e9d2; } +.qm-head p { margin: 0; color: #9b8fb8; font-size: 13px; max-width: 640px; } +.qm-tabs { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 16px; border-bottom: 1px solid #2a2440; padding-bottom: 12px; } +.qm-tabs button { background: #11101e; border: 1px solid #2a2440; color: #b6a7d4; border-radius: 9px; padding: 7px 14px; font-size: 12.5px; cursor: pointer; font-family: inherit; } +.qm-tabs button.on { background: #2a2360; border-color: #6a5a9a; color: #fff; } +.qm-tabs button:hover:not(.on) { border-color: #6a5a9a; } +.qm-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 22px; } +.qm-stat { background: linear-gradient(180deg, #15102a, #0d0a18); border: 1px solid #2a2440; border-radius: 12px; padding: 16px; text-align: center; } +.qm-stat b { display: block; font-family: Georgia, serif; font-size: 30px; color: #7fe9ff; } +.qm-stat span { color: #8a7bb0; font-size: 11.5px; } +.qm-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 11px; margin-top: 8px; } +.qm-link { display: flex; align-items: center; gap: 12px; background: #11101e; border: 1px solid #2a2440; border-radius: 11px; padding: 12px 14px; text-decoration: none; transition: border-color 0.14s, transform 0.14s; } +.qm-link:hover { border-color: #46c8e0; transform: translateY(-2px); } +.qm-link-ico { font-size: 24px; } +.qm-link-t { flex: 1; display: flex; flex-direction: column; gap: 1px; } +.qm-link-t strong { color: #eee6ff; font-size: 13.5px; } +.qm-link-t span { color: #8a7bb0; font-size: 11px; } +.qm-link-go { color: #46c8e0; font-size: 15px; } +.qm-body .px-admin { position: static; transform: none; width: 100%; margin: 0; } +.qm-body .pm-disk { padding: 0; }