From 0de81620b02d647ce716a41dae828d1969dbea13 Mon Sep 17 00:00:00 2001 From: artheru Date: Wed, 10 Jun 2026 05:18:13 +0800 Subject: [PATCH] live activity feed (0.31.0): 'Live in the tavern' on the landing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/agents/activity: derived recent events (classes published, instances run, sessions started, showcases opened) — no new schema - landing feed with per-kind icons + relative time, auto-refreshes every 15s - social proof / 'platform is alive' on the home page --- index.html | 2 +- package.json | 2 +- server/amerc-api.mjs | 15 +++++++++++++++ src/Scene2D.jsx | 20 +++++++++++++++++++- src/styles.css | 10 ++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index e7e11e9..009c5a2 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ name="description" content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries." /> - + diff --git a/package.json b/package.json index 3fb2a44..61e10b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.30.0-landing", + "version": "0.31.0-activity", "private": true, "type": "module", "scripts": { diff --git a/server/amerc-api.mjs b/server/amerc-api.mjs index 468559e..ba41969 100644 --- a/server/amerc-api.mjs +++ b/server/amerc-api.mjs @@ -349,6 +349,21 @@ const server = http.createServer(async (req, res) => { return send(res, 200, { ok: true, classes, instances: insts.length, online, sessions, owners, tags }); } + // live activity feed (public, derived from existing tables) + if (path === '/agents/activity' && method === 'GET') { + const items = []; + for (const c of db.prepare("SELECT name, owner, created_at FROM agent_classes WHERE visibility='public' ORDER BY created_at DESC LIMIT 8").all()) + items.push({ kind: 'class', text: `${c.owner} published “${c.name}”`, at: c.created_at }); + for (const i of db.prepare("SELECT i.created_at, i.owner, c.name cn FROM agent_instances i JOIN agent_classes c ON c.id=i.class_id WHERE i.visibility='public' ORDER BY i.created_at DESC LIMIT 8").all()) + items.push({ kind: 'instance', text: `${i.owner} ran an instance of “${i.cn}”`, at: i.created_at }); + for (const s of db.prepare("SELECT s.started_at, c.name cn FROM agent_sessions s LEFT JOIN agent_classes c ON c.id=s.class_id ORDER BY s.started_at DESC LIMIT 8").all()) + items.push({ kind: 'session', text: `a session started on “${s.cn || 'an agent'}”`, at: s.started_at }); + for (const sc of db.prepare('SELECT owner, host, created_at FROM showcases ORDER BY created_at DESC LIMIT 5').all()) + items.push({ kind: 'showcase', text: `${sc.owner} opened a showcase ${sc.host}`, at: sc.created_at }); + items.sort((a, b) => b.at - a.at); + return send(res, 200, { ok: true, activity: items.slice(0, 12) }); + } + // browse classes (public + own private) if (path === '/agents/classes' && method === 'GET') { const tag = url.searchParams.get('tag'); const q = (url.searchParams.get('q') || '').toLowerCase(); const mine = url.searchParams.get('mine'); diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 9a48934..a70b063 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -6,7 +6,7 @@ import Mansion from './Mansion.jsx'; import TavernGame from './TavernGame.jsx'; import { api } from './api.js'; -const RELEASE = '0.30.0-landing'; +const RELEASE = '0.31.0-activity'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -102,6 +102,23 @@ const HF_USES = [ { icon: '🖥️', title: 'Web terminal', body: 'Open a relayed PTY to the agent’s terminal — watch and drive it right from the browser.' }, { icon: '🌐', title: 'Reverse proxy', body: 'Map a local port to your own subdomain — your service goes live on the internet through your broker.' }, ]; +function relTime(at) { const s = Math.floor((Date.now() - at) / 1000); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; } +const ACT_ICON = { class: '🗡️', instance: '⚙️', session: '💬', showcase: '🌐' }; +function ActivityFeed() { + const [items, setItems] = useState([]); + useEffect(() => { const f = () => api('/agents/activity').then((d) => setItems(d.activity || [])).catch(() => {}); f(); const t = setInterval(f, 15000); return () => clearInterval(t); }, []); + if (!items.length) return null; + return ( +
+

Live in the tavern

+ +
+ ); +} function HomeFeatures({ setRoute }) { return (
@@ -123,6 +140,7 @@ function HomeFeatures({ setRoute }) {
{u.icon}
{u.title}

{u.body}

))} +
diff --git a/src/styles.css b/src/styles.css index b11c2cd..2883040 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1136,3 +1136,13 @@ button { .hf-use strong { color: #fff; font-size: 14px; } .hf-use p { margin: 4px 0 0; color: #9a86b8; font-size: 12px; line-height: 1.5; } .hf-cta { display: flex; gap: 12px; justify-content: center; margin: 38px 0 0; } @media (max-width: 760px) { .hf-grid, .hf-uses { grid-template-columns: 1fr; } } + +/* live activity feed (0.31) */ +.hf-activity { max-width: 640px; margin: 0 auto; } +.act-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; } +.act-list li { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-radius: 10px; background: #0e0a18; border: 1px solid #1d1430; animation: act-in .4s ease; } +.act-list li:hover { border-color: #2e2440; } +@keyframes act-in { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: none; } } +.act-ico { font-size: 16px; flex: none; } +.act-text { font-size: 13px; color: #c9b8e6; flex: 1; } +.act-time { font-size: 11px; color: #6f5b8c; flex: none; }