@tpsdev-ai/flair 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -11
- package/SECURITY.md +52 -20
- package/config.yaml +32 -5
- package/dist/cli.js +2235 -260
- package/dist/deploy.js +96 -15
- package/dist/doctor-client.js +312 -0
- package/dist/fleet-presence.js +98 -0
- package/dist/fleet-verify.js +291 -0
- package/dist/install/clients.js +18 -0
- package/dist/mcp-client-assertion.js +364 -0
- package/dist/probe.js +97 -0
- package/dist/rem/restore.js +1 -1
- package/dist/rem/runner.js +82 -12
- package/dist/resources/Admin.js +1 -1
- package/dist/resources/AdminConnectors.js +1 -1
- package/dist/resources/AdminDashboard.js +1 -1
- package/dist/resources/AdminIdp.js +1 -1
- package/dist/resources/AdminInstance.js +1 -1
- package/dist/resources/AdminMemory.js +2 -2
- package/dist/resources/AdminPrincipals.js +1 -1
- package/dist/resources/Agent.js +14 -0
- package/dist/resources/AgentCard.js +1 -1
- package/dist/resources/AgentSeed.js +1 -1
- package/dist/resources/AttentionQuery.js +356 -0
- package/dist/resources/Credential.js +11 -1
- package/dist/resources/Federation.js +121 -2
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/MCPClientMetadata.js +88 -0
- package/dist/resources/Memory.js +127 -22
- package/dist/resources/MemoryBootstrap.js +441 -85
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/MemoryReflect.js +105 -49
- package/dist/resources/MemoryUsage.js +104 -0
- package/dist/resources/OAuth.js +69 -5
- package/dist/resources/OrgEvent.js +11 -0
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/Presence.js +268 -17
- package/dist/resources/RecordUsage.js +230 -0
- package/dist/resources/Relationship.js +108 -26
- package/dist/resources/SemanticSearch.js +88 -320
- package/dist/resources/SkillScan.js +15 -0
- package/dist/resources/Soul.js +14 -0
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +12 -1
- package/dist/resources/agent-auth.js +61 -3
- package/dist/resources/agentcard-fields.js +2 -2
- package/dist/resources/auth-middleware.js +42 -14
- package/dist/resources/bm25-filter.js +1 -1
- package/dist/resources/bm25.js +13 -7
- package/dist/resources/collision-lib.js +157 -0
- package/dist/resources/dedup.js +2 -2
- package/dist/resources/ed25519-auth.js +2 -2
- package/dist/resources/embeddings-boot.js +149 -0
- package/dist/resources/embeddings-provider.js +364 -106
- package/dist/resources/entity-vocab.js +139 -0
- package/dist/resources/federation-nonce-store.js +195 -0
- package/dist/resources/health.js +97 -6
- package/dist/resources/instance-identity.js +53 -0
- package/dist/resources/mcp-client-metadata-fields.js +112 -0
- package/dist/resources/mcp-handler.js +1 -1
- package/dist/resources/mcp-tools.js +88 -10
- package/dist/resources/memory-bootstrap-lib.js +1 -1
- package/dist/resources/memory-read-scope.js +58 -71
- package/dist/resources/memory-reflect-lib.js +289 -0
- package/dist/resources/memory-visibility.js +37 -0
- package/dist/resources/migration-boot.js +143 -0
- package/dist/resources/migrations/dir-safety.js +71 -0
- package/dist/resources/migrations/embedding-stamp.js +178 -0
- package/dist/resources/migrations/envelope.js +43 -0
- package/dist/resources/migrations/export.js +38 -0
- package/dist/resources/migrations/ledger.js +55 -0
- package/dist/resources/migrations/lock.js +154 -0
- package/dist/resources/migrations/progress.js +31 -0
- package/dist/resources/migrations/registry.js +36 -0
- package/dist/resources/migrations/risk-policy.js +23 -0
- package/dist/resources/migrations/runner.js +456 -0
- package/dist/resources/migrations/snapshot.js +127 -0
- package/dist/resources/migrations/source-fields.js +93 -0
- package/dist/resources/migrations/space.js +73 -0
- package/dist/resources/migrations/state.js +64 -0
- package/dist/resources/migrations/status.js +39 -0
- package/dist/resources/migrations/synthetic-test-migration.js +93 -0
- package/dist/resources/migrations/types.js +9 -0
- package/dist/resources/presence-internal.js +29 -0
- package/dist/resources/provenance.js +50 -0
- package/dist/resources/rate-limiter.js +8 -1
- package/dist/resources/scoring.js +124 -5
- package/dist/resources/semantic-retrieval-core.js +317 -0
- package/dist/version-check.js +167 -0
- package/dist/version-handshake.js +122 -0
- package/package.json +4 -5
- package/schemas/agent.graphql +7 -0
- package/schemas/event.graphql +5 -0
- package/schemas/federation.graphql +12 -0
- package/schemas/memory.graphql +82 -0
- package/schemas/schema.graphql +5 -43
- package/schemas/workspace.graphql +3 -0
- package/dist/resources/IngestEvents.js +0 -162
- package/dist/resources/IssueTokens.js +0 -19
- package/dist/resources/ObsAgentSnapshot.js +0 -13
- package/dist/resources/ObsEventFeed.js +0 -13
- package/dist/resources/ObsOffice.js +0 -19
- package/dist/resources/ObservationCenter.js +0 -23
- package/ui/observation-center.html +0 -385
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data:;">
|
|
7
|
-
<title>Flair · Observation Center</title>
|
|
8
|
-
<style>
|
|
9
|
-
:root{--bg:#f4efe6;--panel:rgba(255,251,243,.86);--ink:#1e241f;--muted:#697265;--line:rgba(30,36,31,.12);--accent:#0f766e;--accent-soft:rgba(15,118,110,.12);--ok:#166534;--warn:#b45309;--danger:#9f1239;--shadow:0 18px 48px rgba(69,58,35,.14)}
|
|
10
|
-
*{box-sizing:border-box}body{margin:0;min-height:100vh;font:15px/1.45 "IBM Plex Sans","Avenir Next",sans-serif;color:var(--ink);background:radial-gradient(circle at top left,rgba(15,118,110,.14),transparent 28rem),radial-gradient(circle at top right,rgba(180,83,9,.1),transparent 18rem),linear-gradient(180deg,#fcfaf5,var(--bg))}
|
|
11
|
-
.shell{max-width:1280px;margin:0 auto;padding:28px 18px 40px}
|
|
12
|
-
.hero,.panel{background:var(--panel);border:1px solid var(--line);border-radius:22px;backdrop-filter:blur(14px);box-shadow:var(--shadow)}
|
|
13
|
-
.hero{padding:24px;margin-bottom:18px}.hero h1,.panel h2{margin:0;font-family:"IBM Plex Serif","Iowan Old Style",serif}.hero h1{font-size:clamp(2rem,4vw,3.1rem)}.hero p,.subtle,.empty{color:var(--muted)}
|
|
14
|
-
.summary,.grid,.events,.toolbar,.layout,.tabnav,.memlist,.peers,.bridges{display:grid;gap:12px}
|
|
15
|
-
.summary{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin-top:18px}
|
|
16
|
-
.chip,.card,.event,.memcard,.peer,.bridge{background:rgba(255,255,255,.62);border:1px solid var(--line);border-radius:16px;padding:14px}
|
|
17
|
-
.chip strong{display:block;font-size:1.3rem}.chip span{font-size:.92rem;color:var(--muted)}
|
|
18
|
-
.toolbar{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));padding:16px;margin-bottom:14px}
|
|
19
|
-
.field{display:flex;flex-direction:column;gap:6px}.field label{font-size:.8rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
|
|
20
|
-
.field input,.field select,.field button,.field textarea{width:100%;padding:11px 12px;border-radius:12px;border:1px solid var(--line);background:#fffaf1;font:inherit;color:var(--ink)}
|
|
21
|
-
.field button{cursor:pointer;color:#fff;background:linear-gradient(135deg,var(--accent),#115e59);border:none;font-weight:600}
|
|
22
|
-
.tabnav{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));padding:8px;margin-bottom:18px;gap:6px}
|
|
23
|
-
.tabnav button{cursor:pointer;background:transparent;border:1px solid transparent;border-radius:14px;padding:10px 14px;font:inherit;color:var(--muted);font-weight:600}
|
|
24
|
-
.tabnav button.active{background:rgba(255,255,255,.7);border-color:var(--line);color:var(--ink);box-shadow:0 4px 12px rgba(69,58,35,.06)}
|
|
25
|
-
.tab{display:none}.tab.active{display:block}
|
|
26
|
-
.layout{grid-template-columns:1.05fr 1.4fr;align-items:start}
|
|
27
|
-
.panel{padding:18px}.head,.row{display:flex;justify-content:space-between;gap:10px}.head{align-items:baseline;margin-bottom:14px;flex-wrap:wrap}
|
|
28
|
-
.grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
|
|
29
|
-
.events,.memlist{max-height:72vh;overflow:auto;padding-right:4px}
|
|
30
|
-
.card h3,.event h3,.memcard h3,.peer h3,.bridge h3{margin:0;font-size:1rem}
|
|
31
|
-
.meta,.workspace{font-size:.9rem;color:var(--muted)}
|
|
32
|
-
.event p,.memcard p{margin:8px 0 0;white-space:pre-wrap;word-break:break-word}
|
|
33
|
-
.memcard pre{margin:8px 0 0;white-space:pre-wrap;word-break:break-word;font:13px/1.5 "IBM Plex Mono",monospace;background:rgba(15,118,110,.04);padding:10px;border-radius:10px;max-height:14em;overflow:auto}
|
|
34
|
-
.kind,.score{font:0.82rem "IBM Plex Mono",monospace;color:var(--accent)}
|
|
35
|
-
.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;font-size:.8rem;font-weight:600;background:var(--accent-soft);color:var(--accent)}
|
|
36
|
-
.ok{background:rgba(22,101,52,.12);color:var(--ok)}.warn{background:rgba(180,83,9,.14);color:var(--warn)}.danger{background:rgba(159,18,57,.12);color:var(--danger)}
|
|
37
|
-
.mem-toolbar{grid-template-columns:1.5fr 1fr 1fr 0.7fr 0.7fr}
|
|
38
|
-
.topology{display:grid;grid-template-columns:1fr 1.2fr;gap:14px}
|
|
39
|
-
@media (max-width:1080px){.toolbar,.mem-toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.layout,.topology{grid-template-columns:1fr}.events,.memlist{max-height:none}}
|
|
40
|
-
@media (max-width:640px){.shell{padding:18px 12px 32px}.toolbar,.mem-toolbar,.tabnav{grid-template-columns:1fr}}
|
|
41
|
-
</style>
|
|
42
|
-
</head>
|
|
43
|
-
<body>
|
|
44
|
-
<div class="shell">
|
|
45
|
-
<section class="hero">
|
|
46
|
-
<h1>Flair Observation Center</h1>
|
|
47
|
-
<p>Read-only dashboard for memories, agents, federation state, bridges, and org-wide events. Polling every 5 seconds on active tab.</p>
|
|
48
|
-
<div class="summary" id="summary"></div>
|
|
49
|
-
</section>
|
|
50
|
-
|
|
51
|
-
<section class="panel toolbar">
|
|
52
|
-
<div class="field"><label for="baseUrl">Flair URL</label><input id="baseUrl" placeholder="(same origin)"></div>
|
|
53
|
-
<div class="field"><label for="adminPass">Admin password</label><input id="adminPass" type="password" placeholder="Basic admin auth"></div>
|
|
54
|
-
<div class="field"><label> </label><button id="refresh">Refresh now</button></div>
|
|
55
|
-
</section>
|
|
56
|
-
|
|
57
|
-
<nav class="panel tabnav" role="tablist">
|
|
58
|
-
<button data-tab="agents" class="active" role="tab">Agents</button>
|
|
59
|
-
<button data-tab="memories" role="tab">Memories</button>
|
|
60
|
-
<button data-tab="federation" role="tab">Federation</button>
|
|
61
|
-
<button data-tab="bridges" role="tab">Bridges</button>
|
|
62
|
-
<button data-tab="activity" role="tab">Activity</button>
|
|
63
|
-
</nav>
|
|
64
|
-
|
|
65
|
-
<!-- ── Agents tab ──────────────────────────────────────────────────── -->
|
|
66
|
-
<section class="tab active" id="tab-agents">
|
|
67
|
-
<section class="panel toolbar">
|
|
68
|
-
<div class="field"><label for="agentFilter">Agent</label><select id="agentFilter"><option value="">All agents</option></select></div>
|
|
69
|
-
<div class="field"><label for="kindFilter">Event kind</label><select id="kindFilter"><option value="">All kinds</option></select></div>
|
|
70
|
-
<div class="field"><label for="timeFilter">Time range</label><select id="timeFilter"><option value="15m">Last 15m</option><option value="1h" selected>Last hour</option><option value="6h">Last 6h</option><option value="24h">Last 24h</option></select></div>
|
|
71
|
-
</section>
|
|
72
|
-
<div class="layout">
|
|
73
|
-
<section class="panel">
|
|
74
|
-
<div class="head"><div><h2>Agent Grid</h2><p class="subtle">Status derived from recent OrgEvents and workspace snapshots.</p></div><p class="subtle" id="agentStatus">Loading agents…</p></div>
|
|
75
|
-
<div class="grid" id="agentGrid"></div>
|
|
76
|
-
</section>
|
|
77
|
-
<section class="panel">
|
|
78
|
-
<div class="head"><div><h2>Agent-scoped events</h2><p class="subtle">Events visible to <code>anvil</code> via <code>/OrgEventCatchup/anvil</code>.</p></div><p class="subtle" id="feedStatus">Waiting for first poll…</p></div>
|
|
79
|
-
<div class="events" id="eventFeed"></div>
|
|
80
|
-
</section>
|
|
81
|
-
</div>
|
|
82
|
-
</section>
|
|
83
|
-
|
|
84
|
-
<!-- ── Memories tab ────────────────────────────────────────────────── -->
|
|
85
|
-
<section class="tab" id="tab-memories">
|
|
86
|
-
<section class="panel toolbar mem-toolbar">
|
|
87
|
-
<div class="field"><label for="memQuery">Search (semantic)</label><input id="memQuery" placeholder="freetext query…"></div>
|
|
88
|
-
<div class="field"><label for="memAgent">Agent</label><select id="memAgent"><option value="">All</option></select></div>
|
|
89
|
-
<div class="field"><label for="memDurability">Durability</label><select id="memDurability"><option value="">Any</option><option value="permanent">permanent</option><option value="persistent">persistent</option><option value="standard">standard</option><option value="ephemeral">ephemeral</option></select></div>
|
|
90
|
-
<div class="field"><label for="memLimit">Limit</label><input id="memLimit" type="number" value="20" min="1" max="100"></div>
|
|
91
|
-
<div class="field"><label> </label><button id="memSearch">Search</button></div>
|
|
92
|
-
</section>
|
|
93
|
-
<section class="panel">
|
|
94
|
-
<div class="head"><div><h2>Memories</h2><p class="subtle">Semantic search via <code>/SemanticSearch</code>. Empty query returns recent memories.</p></div><p class="subtle" id="memStatus">Enter a query and click Search.</p></div>
|
|
95
|
-
<div class="memlist" id="memList"></div>
|
|
96
|
-
</section>
|
|
97
|
-
</section>
|
|
98
|
-
|
|
99
|
-
<!-- ── Federation tab ──────────────────────────────────────────────── -->
|
|
100
|
-
<section class="tab" id="tab-federation">
|
|
101
|
-
<div class="topology">
|
|
102
|
-
<section class="panel">
|
|
103
|
-
<div class="head"><h2>This instance</h2></div>
|
|
104
|
-
<div id="fedInstance"><p class="subtle">Loading…</p></div>
|
|
105
|
-
</section>
|
|
106
|
-
<section class="panel">
|
|
107
|
-
<div class="head"><div><h2>Peers</h2><p class="subtle">Hub + spoke connections.</p></div><p class="subtle" id="fedPeerStatus"></p></div>
|
|
108
|
-
<div class="peers" id="fedPeers"></div>
|
|
109
|
-
</section>
|
|
110
|
-
</div>
|
|
111
|
-
</section>
|
|
112
|
-
|
|
113
|
-
<!-- ── Bridges tab ─────────────────────────────────────────────────── -->
|
|
114
|
-
<section class="tab" id="tab-bridges">
|
|
115
|
-
<section class="panel">
|
|
116
|
-
<div class="head"><div><h2>Memory Bridges</h2><p class="subtle">Pluggable import/export adapters.</p></div><p class="subtle" id="bridgeStatus">Loading…</p></div>
|
|
117
|
-
<div class="bridges" id="bridgeList"></div>
|
|
118
|
-
</section>
|
|
119
|
-
</section>
|
|
120
|
-
|
|
121
|
-
<!-- ── Activity tab ────────────────────────────────────────────────── -->
|
|
122
|
-
<section class="tab" id="tab-activity">
|
|
123
|
-
<section class="panel toolbar">
|
|
124
|
-
<div class="field"><label for="actAgent">Agent</label><select id="actAgent"><option value="">All</option></select></div>
|
|
125
|
-
<div class="field"><label for="actKind">Kind</label><select id="actKind"><option value="">All</option></select></div>
|
|
126
|
-
<div class="field"><label for="actTime">Time range</label><select id="actTime"><option value="15m">Last 15m</option><option value="1h" selected>Last hour</option><option value="6h">Last 6h</option><option value="24h">Last 24h</option></select></div>
|
|
127
|
-
</section>
|
|
128
|
-
<section class="panel">
|
|
129
|
-
<div class="head"><div><h2>Org-wide events</h2><p class="subtle">All OrgEvent rows visible to admin.</p></div><p class="subtle" id="actStatus">Loading…</p></div>
|
|
130
|
-
<div class="events" id="actFeed"></div>
|
|
131
|
-
</section>
|
|
132
|
-
</section>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<script>
|
|
136
|
-
// ── Constants + state ───────────────────────────────────────────────
|
|
137
|
-
const SS_PASS = "flair.observation.adminPass";
|
|
138
|
-
const SS_URL = "flair.observation.baseUrl";
|
|
139
|
-
const ESC_MAP = {"&":"&","<":"<",">":">",'"':""","'":"'"};
|
|
140
|
-
const state = {
|
|
141
|
-
tab: "agents",
|
|
142
|
-
timer: null,
|
|
143
|
-
anchorAgent: "anvil",
|
|
144
|
-
agents: [],
|
|
145
|
-
events: [],
|
|
146
|
-
workspace: {},
|
|
147
|
-
memories: [],
|
|
148
|
-
federationInstance: null,
|
|
149
|
-
federationPeers: [],
|
|
150
|
-
bridges: [],
|
|
151
|
-
orgEvents: [],
|
|
152
|
-
latestPoll: null,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const els = {};
|
|
156
|
-
[
|
|
157
|
-
"summary","baseUrl","adminPass","refresh",
|
|
158
|
-
"agentFilter","kindFilter","timeFilter","agentGrid","agentStatus","eventFeed","feedStatus",
|
|
159
|
-
"memQuery","memAgent","memDurability","memLimit","memSearch","memList","memStatus",
|
|
160
|
-
"fedInstance","fedPeers","fedPeerStatus",
|
|
161
|
-
"bridgeList","bridgeStatus",
|
|
162
|
-
"actAgent","actKind","actTime","actFeed","actStatus",
|
|
163
|
-
].forEach(k => { els[k] = document.querySelector("#" + k); });
|
|
164
|
-
const tabs = [...document.querySelectorAll(".tabnav button")];
|
|
165
|
-
|
|
166
|
-
// ── Auth + utilities ────────────────────────────────────────────────
|
|
167
|
-
const clean = v => String(v ?? "").trim();
|
|
168
|
-
const esc = v => String(v ?? "").replace(/[&<>"']/g, c => ESC_MAP[c]);
|
|
169
|
-
const parse = v => { if (!v) return null; if (typeof v === "object") return v; try { return JSON.parse(v); } catch { return null; } };
|
|
170
|
-
const list = v => Array.isArray(v) ? v : Array.isArray(v?.items) ? v.items : Array.isArray(v?.data) ? v.data : Array.isArray(v?.results) ? v.results : [];
|
|
171
|
-
const eventKind = e => clean(e?.kind || e?.type || e?.status || "unknown");
|
|
172
|
-
const eventText = e => clean(e?.summary || e?.detail || e?.message || "");
|
|
173
|
-
const eventAgents = e => [e?.authorId, ...(Array.isArray(e?.targetIds) ? e.targetIds : [])].map(clean).filter(Boolean);
|
|
174
|
-
const timeAgo = iso => { const d = Date.now() - new Date(iso).getTime(); if (!Number.isFinite(d)) return "unknown"; const m = Math.round(d/6e4); if (m<1) return "just now"; if (m<60) return `${m}m ago`; const h = Math.round(m/60); if (h<24) return `${h}h ago`; return `${Math.round(h/24)}d ago`; };
|
|
175
|
-
|
|
176
|
-
function authHeader() { const p = clean(els.adminPass.value); return p ? "Basic " + btoa("admin:" + p) : ""; }
|
|
177
|
-
function baseUrl() { return clean(els.baseUrl.value).replace(/\/$/, ""); }
|
|
178
|
-
function rangeMs(sel) { return ({"15m":9e5,"1h":36e5,"6h":216e5,"24h":864e5})[sel] || 36e5; }
|
|
179
|
-
|
|
180
|
-
sessionStorage.getItem(SS_PASS) && (els.adminPass.value = sessionStorage.getItem(SS_PASS));
|
|
181
|
-
sessionStorage.getItem(SS_URL) && (els.baseUrl.value = sessionStorage.getItem(SS_URL));
|
|
182
|
-
|
|
183
|
-
async function api(path, opts = {}) {
|
|
184
|
-
const headers = { accept: "application/json", ...(opts.headers || {}) };
|
|
185
|
-
const auth = authHeader();
|
|
186
|
-
if (auth) headers.authorization = auth;
|
|
187
|
-
if (opts.body && typeof opts.body !== "string") {
|
|
188
|
-
headers["content-type"] = "application/json";
|
|
189
|
-
opts.body = JSON.stringify(opts.body);
|
|
190
|
-
}
|
|
191
|
-
const res = await fetch(baseUrl() + path, { ...opts, headers });
|
|
192
|
-
if (res.status === 401) { sessionStorage.removeItem(SS_PASS); els.adminPass.value = ""; throw new Error("401 unauthorized — enter admin password"); }
|
|
193
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
194
|
-
const ct = res.headers.get("content-type") || "";
|
|
195
|
-
return ct.includes("application/json") ? res.json() : res.text();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ── Tab switching ───────────────────────────────────────────────────
|
|
199
|
-
function setTab(name) {
|
|
200
|
-
state.tab = name;
|
|
201
|
-
tabs.forEach(b => b.classList.toggle("active", b.dataset.tab === name));
|
|
202
|
-
document.querySelectorAll(".tab").forEach(s => s.classList.toggle("active", s.id === "tab-" + name));
|
|
203
|
-
refresh();
|
|
204
|
-
}
|
|
205
|
-
tabs.forEach(b => b.addEventListener("click", () => setTab(b.dataset.tab)));
|
|
206
|
-
|
|
207
|
-
// ── Agents tab ──────────────────────────────────────────────────────
|
|
208
|
-
async function loadAgentsTab() {
|
|
209
|
-
const since = new Date(Date.now() - rangeMs(els.timeFilter.value)).toISOString();
|
|
210
|
-
const [agents, events] = await Promise.all([
|
|
211
|
-
api("/Agent").then(list),
|
|
212
|
-
api(`/OrgEventCatchup/${encodeURIComponent(state.anchorAgent)}?since=${encodeURIComponent(since)}`).then(list),
|
|
213
|
-
]);
|
|
214
|
-
state.agents = agents;
|
|
215
|
-
state.events = events.sort((a,b) => clean(a.createdAt).localeCompare(clean(b.createdAt)));
|
|
216
|
-
const ids = agents.map(a => clean(a.id || a.agentId || a.name)).filter(Boolean);
|
|
217
|
-
const rows = await Promise.all(ids.map(async id => { try { return [id, await api("/WorkspaceLatest/" + encodeURIComponent(id))]; } catch { return [id, null]; } }));
|
|
218
|
-
state.workspace = Object.fromEntries(rows);
|
|
219
|
-
}
|
|
220
|
-
function deriveAgent(agent) {
|
|
221
|
-
const id = clean(agent.id || agent.agentId || agent.name);
|
|
222
|
-
const related = state.events.filter(e => eventAgents(e).includes(id));
|
|
223
|
-
const latest = related.at(-1);
|
|
224
|
-
const ws = state.workspace[id];
|
|
225
|
-
const snap = parse(ws?.snapshot);
|
|
226
|
-
const lastAt = clean(latest?.createdAt || ws?.timestamp || ws?.createdAt || agent.updatedAt || agent.createdAt);
|
|
227
|
-
const online = lastAt && (Date.now() - new Date(lastAt).getTime()) < 6e5;
|
|
228
|
-
return { id, name: clean(agent.name || id), role: clean(agent.role || agent.type || "agent"), status: online ? "online" : "offline", lastAt, currentTask: clean(parse(latest?.detail)?.taskId || latest?.refId || latest?.summary || "idle"), latestKind: eventKind(latest), workspace: clean(snap?.gitBranch || ws?.gitBranch || ws?.path || "unavailable") };
|
|
229
|
-
}
|
|
230
|
-
function renderAgentsTab() {
|
|
231
|
-
// populate filter dropdowns
|
|
232
|
-
const agentIds = state.agents.map(a => clean(a.id || a.agentId || a.name)).filter(Boolean).sort();
|
|
233
|
-
const kinds = [...new Set(state.events.map(eventKind).filter(Boolean))].sort();
|
|
234
|
-
els.agentFilter.innerHTML = `<option value="">All agents</option>` + agentIds.map(id => `<option value="${esc(id)}" ${id===els.agentFilter.value?"selected":""}>${esc(id)}</option>`).join("");
|
|
235
|
-
els.kindFilter.innerHTML = `<option value="">All kinds</option>` + kinds.map(k => `<option value="${esc(k)}" ${k===els.kindFilter.value?"selected":""}>${esc(k)}</option>`).join("");
|
|
236
|
-
// agent cards
|
|
237
|
-
const cards = state.agents.map(deriveAgent);
|
|
238
|
-
els.agentStatus.textContent = `${cards.length} agents via /Agent`;
|
|
239
|
-
els.agentGrid.innerHTML = cards.length ? cards.map(a => `<article class="card"><div class="row"><h3>${esc(a.name)}</h3><span class="badge ${a.status==="online"?"ok":"warn"}">${esc(a.status)}</span></div><div class="meta">${esc(a.role)}</div><div class="row"><span class="kind">${esc(a.latestKind)}</span><span class="meta">${a.lastAt?timeAgo(a.lastAt):"no signal"}</span></div><p><strong>Current task:</strong> ${esc(a.currentTask)}</p><div class="workspace">Workspace: ${esc(a.workspace)}</div></article>`).join("") : `<div class="empty">No agents found.</div>`;
|
|
240
|
-
// event feed (filtered)
|
|
241
|
-
const start = new Date(Date.now() - rangeMs(els.timeFilter.value));
|
|
242
|
-
const filteredEvents = state.events.filter(e => {
|
|
243
|
-
if (new Date(e.createdAt || 0) < start) return false;
|
|
244
|
-
if (els.agentFilter.value && !eventAgents(e).includes(els.agentFilter.value)) return false;
|
|
245
|
-
if (els.kindFilter.value && eventKind(e) !== els.kindFilter.value) return false;
|
|
246
|
-
return true;
|
|
247
|
-
}).slice().reverse();
|
|
248
|
-
els.feedStatus.textContent = `${filteredEvents.length} matching events`;
|
|
249
|
-
els.eventFeed.innerHTML = filteredEvents.length ? filteredEvents.map(e => `<article class="event"><div class="row"><h3>${esc(clean(e.summary||eventKind(e)))}</h3><span class="meta">${timeAgo(e.createdAt)}</span></div><div class="row"><span class="kind">${esc(eventKind(e))}</span><span class="badge">${esc(eventAgents(e).join(", "))||"org-wide"}</span></div><p>${esc(eventText(e))||"No detail provided."}</p></article>`).join("") : `<div class="empty">No events match.</div>`;
|
|
250
|
-
// summary chips
|
|
251
|
-
els.summary.innerHTML = [["Agents", cards.length], ["Online", cards.filter(c=>c.status==="online").length], ["Events", filteredEvents.length], ["Last Poll", state.latestPoll ? timeAgo(state.latestPoll) : "never"]].map(([l,v]) => `<div class="chip"><strong>${v}</strong><span>${l}</span></div>`).join("");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ── Memories tab ────────────────────────────────────────────────────
|
|
255
|
-
async function loadMemoriesTab() {
|
|
256
|
-
const q = clean(els.memQuery.value);
|
|
257
|
-
const limit = Math.max(1, Math.min(100, Number(els.memLimit.value) || 20));
|
|
258
|
-
els.memStatus.textContent = "Searching…";
|
|
259
|
-
const body = { q: q || "", limit };
|
|
260
|
-
if (els.memAgent.value) body.agentId = els.memAgent.value;
|
|
261
|
-
if (els.memDurability.value) body.durability = els.memDurability.value;
|
|
262
|
-
const res = await api("/SemanticSearch", { method: "POST", body });
|
|
263
|
-
state.memories = list(res);
|
|
264
|
-
}
|
|
265
|
-
function renderMemoriesTab() {
|
|
266
|
-
// populate agent dropdown from agents we already loaded
|
|
267
|
-
const agentIds = state.agents.map(a => clean(a.id || a.agentId || a.name)).filter(Boolean).sort();
|
|
268
|
-
const cur = els.memAgent.value;
|
|
269
|
-
els.memAgent.innerHTML = `<option value="">All</option>` + agentIds.map(id => `<option value="${esc(id)}" ${id===cur?"selected":""}>${esc(id)}</option>`).join("");
|
|
270
|
-
els.memStatus.textContent = `${state.memories.length} result${state.memories.length===1?"":"s"}`;
|
|
271
|
-
els.memList.innerHTML = state.memories.length ? state.memories.map(m => {
|
|
272
|
-
const tags = [m.durability && `<span class="badge">${esc(m.durability)}</span>`, m.archived && `<span class="badge warn">archived</span>`, m._safetyFlags && `<span class="badge danger">flagged</span>`].filter(Boolean).join(" ");
|
|
273
|
-
const score = m._score != null ? `<span class="score">score ${Number(m._score).toFixed(2)}</span>` : "";
|
|
274
|
-
return `<article class="memcard"><div class="row"><h3>${esc(m.id || "(no id)")}</h3>${score}</div><div class="row"><span class="meta">${esc(m.agentId||"")} · ${m.createdAt?timeAgo(m.createdAt):""}</span><span>${tags}</span></div><pre>${esc(m.content || "")}</pre></article>`;
|
|
275
|
-
}).join("") : `<div class="empty">No memories returned.</div>`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Federation tab ──────────────────────────────────────────────────
|
|
279
|
-
async function loadFederationTab() {
|
|
280
|
-
const [inst, peers] = await Promise.all([
|
|
281
|
-
api("/FederationInstance").catch(() => null),
|
|
282
|
-
api("/FederationPeers").catch(() => ({ peers: [] })),
|
|
283
|
-
]);
|
|
284
|
-
state.federationInstance = inst;
|
|
285
|
-
state.federationPeers = list(peers) || (peers && peers.peers) || [];
|
|
286
|
-
}
|
|
287
|
-
function renderFederationTab() {
|
|
288
|
-
const inst = state.federationInstance;
|
|
289
|
-
els.fedInstance.innerHTML = inst ? `<div class="card"><div class="row"><h3>${esc(inst.id || "(no id)")}</h3><span class="badge ${inst.status==="active"?"ok":"warn"}">${esc(inst.status||"unknown")}</span></div><div class="meta">role: ${esc(inst.role||"unknown")}</div><div class="meta" style="word-break:break-all">pubkey: <code>${esc(inst.publicKey||"")}</code></div></div>` : `<div class="empty">No federation instance configured.</div>`;
|
|
290
|
-
els.fedPeerStatus.textContent = `${state.federationPeers.length} peer(s)`;
|
|
291
|
-
els.fedPeers.innerHTML = state.federationPeers.length ? state.federationPeers.map(p => {
|
|
292
|
-
const status = p.status || "unknown";
|
|
293
|
-
const cls = status==="paired"||status==="connected" ? "ok" : status==="revoked" ? "danger" : "warn";
|
|
294
|
-
const sync = p.lastSyncAt ? `last sync ${timeAgo(p.lastSyncAt)}` : "never synced";
|
|
295
|
-
return `<article class="peer"><div class="row"><h3>${esc(p.id||"(no id)")}</h3><span class="badge ${cls}">${esc(status)}</span></div><div class="meta">role: ${esc(p.role||"")}${p.endpoint?` · <code>${esc(p.endpoint)}</code>`:""}</div><div class="meta">${esc(sync)}${p.relayOnly?" · relay-only":""}</div></article>`;
|
|
296
|
-
}).join("") : `<div class="empty">No peers configured. Use 'flair federation pair' on a spoke.</div>`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ── Bridges tab ─────────────────────────────────────────────────────
|
|
300
|
-
async function loadBridgesTab() {
|
|
301
|
-
// Try a few likely endpoint shapes; bridges API may have evolved.
|
|
302
|
-
let res = null;
|
|
303
|
-
for (const path of ["/Bridges", "/Bridge", "/bridges"]) {
|
|
304
|
-
try { res = await api(path); break; } catch {}
|
|
305
|
-
}
|
|
306
|
-
state.bridges = list(res);
|
|
307
|
-
}
|
|
308
|
-
function renderBridgesTab() {
|
|
309
|
-
els.bridgeStatus.textContent = `${state.bridges.length} bridge(s)`;
|
|
310
|
-
els.bridgeList.innerHTML = state.bridges.length ? state.bridges.map(b => {
|
|
311
|
-
const status = b.status || b.state || "unknown";
|
|
312
|
-
const cls = status==="active"||status==="allowed" ? "ok" : status==="denied"||status==="error" ? "danger" : "warn";
|
|
313
|
-
return `<article class="bridge"><div class="row"><h3>${esc(b.name||b.id||"(no name)")}</h3><span class="badge ${cls}">${esc(status)}</span></div><div class="meta">${esc(b.kind||b.type||"")}${b.version?` · v${esc(b.version)}`:""}</div><div class="meta">${esc(b.summary||b.description||"")}</div></article>`;
|
|
314
|
-
}).join("") : `<div class="empty">No bridges registered (or /Bridges endpoint unavailable on this instance).</div>`;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ── Activity tab ────────────────────────────────────────────────────
|
|
318
|
-
async function loadActivityTab() {
|
|
319
|
-
const since = new Date(Date.now() - rangeMs(els.actTime.value)).toISOString();
|
|
320
|
-
// Prefer /OrgEvent (admin-wide); fall back to /OrgEventCatchup/anvil.
|
|
321
|
-
let events;
|
|
322
|
-
try { events = list(await api(`/OrgEvent?since=${encodeURIComponent(since)}`)); }
|
|
323
|
-
catch { events = list(await api(`/OrgEventCatchup/anvil?since=${encodeURIComponent(since)}`)); }
|
|
324
|
-
state.orgEvents = events.sort((a,b) => clean(a.createdAt).localeCompare(clean(b.createdAt)));
|
|
325
|
-
}
|
|
326
|
-
function renderActivityTab() {
|
|
327
|
-
const agentIds = state.agents.map(a => clean(a.id || a.agentId || a.name)).filter(Boolean).sort();
|
|
328
|
-
const kinds = [...new Set(state.orgEvents.map(eventKind).filter(Boolean))].sort();
|
|
329
|
-
els.actAgent.innerHTML = `<option value="">All</option>` + agentIds.map(id => `<option value="${esc(id)}" ${id===els.actAgent.value?"selected":""}>${esc(id)}</option>`).join("");
|
|
330
|
-
els.actKind.innerHTML = `<option value="">All</option>` + kinds.map(k => `<option value="${esc(k)}" ${k===els.actKind.value?"selected":""}>${esc(k)}</option>`).join("");
|
|
331
|
-
const filtered = state.orgEvents.filter(e => {
|
|
332
|
-
if (els.actAgent.value && !eventAgents(e).includes(els.actAgent.value)) return false;
|
|
333
|
-
if (els.actKind.value && eventKind(e) !== els.actKind.value) return false;
|
|
334
|
-
return true;
|
|
335
|
-
}).slice().reverse();
|
|
336
|
-
els.actStatus.textContent = `${filtered.length} matching events`;
|
|
337
|
-
els.actFeed.innerHTML = filtered.length ? filtered.map(e => `<article class="event"><div class="row"><h3>${esc(clean(e.summary||eventKind(e)))}</h3><span class="meta">${timeAgo(e.createdAt)}</span></div><div class="row"><span class="kind">${esc(eventKind(e))}</span><span class="badge">${esc(eventAgents(e).join(", "))||"org-wide"}</span></div><p>${esc(eventText(e))||"No detail provided."}</p></article>`).join("") : `<div class="empty">No events match.</div>`;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ── Refresh dispatcher ─────────────────────────────────────────────
|
|
341
|
-
async function refresh() {
|
|
342
|
-
try {
|
|
343
|
-
// Always keep the agent roster fresh (used by multiple tabs for filters).
|
|
344
|
-
if (state.tab !== "memories" || !state.agents.length) {
|
|
345
|
-
try { state.agents = list(await api("/Agent")); } catch {}
|
|
346
|
-
}
|
|
347
|
-
if (state.tab === "agents") await loadAgentsTab();
|
|
348
|
-
if (state.tab === "memories") { /* memories load on Search click only */ }
|
|
349
|
-
if (state.tab === "federation") await loadFederationTab();
|
|
350
|
-
if (state.tab === "bridges") await loadBridgesTab();
|
|
351
|
-
if (state.tab === "activity") await loadActivityTab();
|
|
352
|
-
state.latestPoll = new Date().toISOString();
|
|
353
|
-
} catch (err) {
|
|
354
|
-
// Surface error in the active tab's status field.
|
|
355
|
-
const sel = { agents: els.feedStatus, memories: els.memStatus, federation: els.fedPeerStatus, bridges: els.bridgeStatus, activity: els.actStatus }[state.tab];
|
|
356
|
-
if (sel) sel.textContent = `Failed: ${err.message}`;
|
|
357
|
-
}
|
|
358
|
-
// Render whichever tab is visible.
|
|
359
|
-
if (state.tab === "agents") renderAgentsTab();
|
|
360
|
-
if (state.tab === "memories") renderMemoriesTab();
|
|
361
|
-
if (state.tab === "federation") renderFederationTab();
|
|
362
|
-
if (state.tab === "bridges") renderBridgesTab();
|
|
363
|
-
if (state.tab === "activity") renderActivityTab();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function schedule() { clearInterval(state.timer); state.timer = setInterval(refresh, 5e3); }
|
|
367
|
-
|
|
368
|
-
// ── Wire interactions ───────────────────────────────────────────────
|
|
369
|
-
els.refresh.addEventListener("click", refresh);
|
|
370
|
-
els.baseUrl.addEventListener("change", () => { sessionStorage.setItem(SS_URL, clean(els.baseUrl.value)); refresh(); schedule(); });
|
|
371
|
-
els.adminPass.addEventListener("change", () => { sessionStorage.setItem(SS_PASS, clean(els.adminPass.value)); refresh(); });
|
|
372
|
-
[els.agentFilter, els.kindFilter, els.timeFilter].forEach(el => el.addEventListener("change", () => { renderAgentsTab(); if (el === els.timeFilter) refresh(); }));
|
|
373
|
-
[els.actAgent, els.actKind, els.actTime].forEach(el => el.addEventListener("change", () => { renderActivityTab(); if (el === els.actTime) refresh(); }));
|
|
374
|
-
els.memSearch.addEventListener("click", async () => {
|
|
375
|
-
try { await loadMemoriesTab(); } catch (err) { els.memStatus.textContent = `Search failed: ${err.message}`; return; }
|
|
376
|
-
renderMemoriesTab();
|
|
377
|
-
});
|
|
378
|
-
els.memQuery.addEventListener("keydown", e => { if (e.key === "Enter") els.memSearch.click(); });
|
|
379
|
-
|
|
380
|
-
// Boot
|
|
381
|
-
refresh();
|
|
382
|
-
schedule();
|
|
383
|
-
</script>
|
|
384
|
-
</body>
|
|
385
|
-
</html>
|