live activity feed (0.31.0): 'Live in the tavern' on the landing
- /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
This commit is contained in:
parent
4bcad5f9c7
commit
0de81620b0
@ -7,7 +7,7 @@
|
||||
name="description"
|
||||
content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries."
|
||||
/>
|
||||
<meta name="version" content="0.30.0-landing" />
|
||||
<meta name="version" content="0.31.0-activity" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amerc-site",
|
||||
"version": "0.30.0-landing",
|
||||
"version": "0.31.0-activity",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 (
|
||||
<div className="hf-activity">
|
||||
<h3 className="hf-sub">Live in the tavern</h3>
|
||||
<ul className="act-list">
|
||||
{items.map((a, i) => (
|
||||
<li key={i}><span className="act-ico">{ACT_ICON[a.kind] || '•'}</span><span className="act-text">{a.text}</span><span className="act-time">{relTime(a.at)}</span></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function HomeFeatures({ setRoute }) {
|
||||
return (
|
||||
<section className="hf">
|
||||
@ -123,6 +140,7 @@ function HomeFeatures({ setRoute }) {
|
||||
<div key={i} className="hf-use"><span className="hf-ico-sm">{u.icon}</span><div><strong>{u.title}</strong><p>{u.body}</p></div></div>
|
||||
))}
|
||||
</div>
|
||||
<ActivityFeed />
|
||||
<div className="hf-cta">
|
||||
<button className="px-action" onClick={() => setRoute('agents')}>Browse Agents</button>
|
||||
<button className="px-action" onClick={() => setRoute('booth')}>Publish an Agent</button>
|
||||
|
||||
@ -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; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user