anentrypoint-design 0.0.146 → 0.0.147

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.
@@ -1,33 +1,620 @@
1
- // Freddie page registry. Matches upstream shape:
2
- // FREDDIE_PAGES is an OBJECT mapping id page-renderer fn (not an array).
3
- // Per-page renderer functions are stubs by default consumers (gm-cc,
4
- // foph, hermes-fork, etc.) provide their own page bodies or import the
5
- // richer upstream renderers.
6
-
7
- import { renderPageStub, getRecentPaths, saveRecentPath, skillLabel, renderChatMessages } from './freddie/helpers.js';
8
-
9
- const make = (label) => (props) => renderPageStub({ id: label, ...props });
10
-
11
- export const home = make('home');
12
- export const chat = make('chat');
13
- export const voice = make('voice');
14
- export const sessions = make('sessions');
15
- export const projects = make('projects');
16
- export const agents = make('agents');
17
- export const analytics = make('analytics');
18
- export const models = make('models');
19
- export const cron = make('cron');
20
- export const skills = make('skills');
21
- export const config = make('config');
22
- export const env = make('env');
23
- export const tools = make('tools');
24
- export const batch = make('batch');
25
- export const gateway = make('gateway');
26
- export const chains = make('chains');
1
+ // Freddie page registry — REAL renderers (not stubs). Each page is a
2
+ // self-contained micro-app (see ./freddie/runtime.js) wired to freddie's
3
+ // gui-* plugin HTTP endpoints (/api/*). Consumers mount FREDDIE_PAGES[id]
4
+ // through their thin router; no per-page wiring needed downstream. This is
5
+ // the single maintenance point for freddie GUI per the dynamic-stack contract.
6
+
7
+ import * as webjsx from '../../vendor/webjsx/index.js';
8
+ import { makePage, api, loadingState, errorState, emptyState } from './freddie/runtime.js';
9
+ import { getRecentPaths, saveRecentPath, skillLabel, renderChatMessages } from './freddie/helpers.js';
10
+ import { Panel, Row, Table, Kpi, PageHeader, SearchInput, TextField, Select } from './content.js';
11
+ import { Chip, Btn } from './shell.js';
12
+ import { ChatMessage, ChatComposer } from './chat.js';
13
+
14
+ const h = webjsx.createElement;
15
+
16
+ // ---- shared bits -----------------------------------------------------------
17
+
18
+ const fmtTime = (t) => { try { return new Date(t).toLocaleString(); } catch { return String(t || ''); } };
19
+ const fmtAgo = (t) => {
20
+ if (!t) return '';
21
+ const s = Math.floor((Date.now() - new Date(t).getTime()) / 1000);
22
+ if (s < 60) return s + 's ago';
23
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
24
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
25
+ return Math.floor(s / 86400) + 'd ago';
26
+ };
27
+ const section = (title, ...children) => Panel({ title, children: children.flat().filter(Boolean) });
28
+ const noteAlert = (note) => note ? h('div', { class: 'ds-alert ds-alert-' + note.kind, role: 'alert' },
29
+ h('span', { class: 'ds-alert-icon' }, '!'),
30
+ h('div', { class: 'ds-alert-content' }, note.msg)) : null;
31
+
32
+ // ---- home ------------------------------------------------------------------
33
+
34
+ export const home = makePage((ctx) => {
35
+ async function load() {
36
+ try {
37
+ const [health, agents, sessions] = await Promise.all([
38
+ api('/api/health').catch(() => null),
39
+ api('/api/agents').catch(() => null),
40
+ api('/api/sessions').catch(() => []),
41
+ ]);
42
+ ctx.set({ loading: false, health, agents, sessions: Array.isArray(sessions) ? sessions : [], error: null });
43
+ } catch (e) { ctx.set({ loading: false, error: e }); }
44
+ }
45
+ load();
46
+ ctx.interval(load, 15000);
47
+ return () => {
48
+ const s = ctx.state;
49
+ if (s.loading) return loadingState('loading dashboard…');
50
+ if (s.error) return errorState(s.error, load);
51
+ const sessions = s.sessions || [];
52
+ const agents = s.agents || {};
53
+ const tools = ctx.host?.pi?.tools?.size ?? '—';
54
+ const skills = ctx.host?.pi?.skills?.size ?? '—';
55
+ return [
56
+ PageHeader({ eyebrow: 'freddie', title: 'dashboard', lede: 'agent harness · live overview' }),
57
+ Kpi({ items: [
58
+ [tools, 'tools'],
59
+ [skills, 'skills'],
60
+ [sessions.length, 'sessions'],
61
+ [agents.count ?? 0, 'active agents'],
62
+ ] }),
63
+ section('recent sessions',
64
+ sessions.length
65
+ ? Table({
66
+ headers: ['session', 'platform', 'updated'],
67
+ rows: sessions.slice(0, 8).map(x => [x.title || x.id, x.platform || '—', fmtAgo(x.updated_at)]),
68
+ })
69
+ : emptyState('no sessions yet')),
70
+ section('health',
71
+ s.health ? Table({ headers: ['check', 'status'], rows: Object.entries(s.health).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]) })
72
+ : emptyState('health endpoint unavailable')),
73
+ ];
74
+ };
75
+ });
76
+
77
+ // ---- chat ------------------------------------------------------------------
78
+
79
+ export const chat = makePage((ctx) => {
80
+ Object.assign(ctx.state, { loading: false, messages: [], draft: '', sending: false });
81
+ async function send(text) {
82
+ const t = (text || ctx.state.draft || '').trim();
83
+ if (!t || ctx.state.sending) return;
84
+ ctx.state.messages.push({ role: 'user', text: t, time: new Date().toLocaleTimeString() });
85
+ ctx.set({ draft: '', sending: true });
86
+ try {
87
+ const r = await api('/api/chat', { method: 'POST', body: { prompt: t } });
88
+ const reply = r.result || r.content || r.message || (r.messages && r.messages.at(-1)?.content) || JSON.stringify(r);
89
+ ctx.state.messages.push({ role: 'assistant', text: String(reply), time: new Date().toLocaleTimeString() });
90
+ } catch (e) {
91
+ ctx.state.messages.push({ role: 'assistant', text: '⚠ ' + String(e.message || e), time: new Date().toLocaleTimeString() });
92
+ }
93
+ ctx.set({ sending: false });
94
+ }
95
+ return () => {
96
+ const s = ctx.state;
97
+ return h('div', { class: 'fd-chat' },
98
+ PageHeader({ eyebrow: 'freddie', title: 'chat', lede: 'one-shot agent turns · POST /api/chat' }),
99
+ h('div', { class: 'chat-thread fd-chat-thread', role: 'log', 'aria-label': 'chat messages',
100
+ ref: (el) => { if (el) el.scrollTop = el.scrollHeight; } },
101
+ s.messages.length ? s.messages.map((m, i) => ChatMessage({ ...m, key: i }))
102
+ : emptyState('send a prompt to start', '✎'),
103
+ s.sending ? ChatMessage({ role: 'assistant', typing: true, key: '_typing' }) : null),
104
+ ChatComposer({
105
+ value: s.draft,
106
+ placeholder: s.sending ? 'waiting for reply…' : 'message…',
107
+ disabled: s.sending,
108
+ onInput: (v) => { s.draft = v; },
109
+ onSend: send,
110
+ }));
111
+ };
112
+ });
113
+
114
+ // ---- voice -----------------------------------------------------------------
115
+
116
+ export const voice = makePage((ctx) => {
117
+ Object.assign(ctx.state, { loading: false });
118
+ return () => [
119
+ PageHeader({ eyebrow: 'freddie', title: 'voice', lede: 'voice surfaces' }),
120
+ section('status', emptyState('no voice backend wired in this build. configure a transcription/tts plugin to enable.', '🎙')),
121
+ ];
122
+ });
123
+
124
+ // ---- sessions --------------------------------------------------------------
125
+
126
+ export const sessions = makePage((ctx) => {
127
+ Object.assign(ctx.state, { q: '', selected: null, messages: [], msgLoading: false });
128
+ async function load() {
129
+ try { ctx.set({ loading: false, list: await api('/api/sessions'), error: null }); }
130
+ catch (e) { ctx.set({ loading: false, error: e }); }
131
+ }
132
+ async function search(q) {
133
+ if (!q) return load();
134
+ try { ctx.set({ loading: false, list: await api('/api/search?q=' + encodeURIComponent(q)), error: null }); }
135
+ catch (e) { ctx.set({ loading: false, error: e }); }
136
+ }
137
+ async function open(id) {
138
+ ctx.set({ selected: id, msgLoading: true });
139
+ try { ctx.set({ messages: await api('/api/sessions/' + encodeURIComponent(id) + '/messages'), msgLoading: false }); }
140
+ catch (e) { ctx.set({ messages: [], msgLoading: false, error: e }); }
141
+ }
142
+ load();
143
+ return () => {
144
+ const s = ctx.state;
145
+ if (s.loading) return loadingState();
146
+ if (s.error && !s.list) return errorState(s.error, load);
147
+ const list = Array.isArray(s.list) ? s.list : [];
148
+ return [
149
+ PageHeader({ eyebrow: 'freddie', title: 'sessions', lede: list.length + ' sessions' }),
150
+ SearchInput({ value: s.q, placeholder: 'search messages…', onInput: (v) => { s.q = v; }, onSubmit: (v) => search(v) }),
151
+ section('sessions',
152
+ list.length
153
+ ? Table({ headers: ['session', 'platform', 'updated'], onRowClick: (i) => open(list[i].id),
154
+ rows: list.map(x => [x.title || x.id, x.platform || '—', fmtAgo(x.updated_at)]) })
155
+ : emptyState('no sessions match')),
156
+ s.selected ? section('messages · ' + s.selected,
157
+ s.msgLoading ? loadingState()
158
+ : (s.messages || []).length ? (s.messages).map((m, i) => ChatMessage({ role: m.role, text: m.content || m.text || '', time: m.ts ? fmtTime(m.ts) : '', key: i }))
159
+ : emptyState('no messages')) : null,
160
+ ];
161
+ };
162
+ });
163
+
164
+ // ---- projects --------------------------------------------------------------
165
+
166
+ export const projects = makePage((ctx) => {
167
+ Object.assign(ctx.state, { newName: '', newPath: '', busy: false, note: null });
168
+ async function load() {
169
+ try { ctx.set({ loading: false, data: await api('/api/projects'), error: null }); }
170
+ catch (e) { ctx.set({ loading: false, error: e }); }
171
+ }
172
+ async function create() {
173
+ const name = (ctx.state.newName || '').trim();
174
+ if (!name) { ctx.set({ note: { kind: 'warn', msg: 'name required' } }); return; }
175
+ ctx.set({ busy: true, note: null });
176
+ try { await api('/api/projects', { method: 'POST', body: { name, path: ctx.state.newPath || undefined } }); ctx.state.newName = ''; ctx.state.newPath = ''; await load(); }
177
+ catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
178
+ ctx.set({ busy: false });
179
+ }
180
+ async function activate(name) { ctx.set({ busy: true }); try { await api('/api/projects/active', { method: 'POST', body: { name } }); await load(); } catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); } ctx.set({ busy: false }); }
181
+ async function del(name) { ctx.set({ busy: true }); try { await api('/api/projects/' + encodeURIComponent(name), { method: 'DELETE' }); await load(); } catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); } ctx.set({ busy: false }); }
182
+ load();
183
+ return () => {
184
+ const s = ctx.state;
185
+ if (s.loading) return loadingState();
186
+ if (s.error && !s.data) return errorState(s.error, load);
187
+ const d = s.data || {}; const list = d.projects || [];
188
+ const activeName = (d.active && d.active.name) || d.active || 'default';
189
+ return [
190
+ PageHeader({ eyebrow: 'freddie', title: 'projects', lede: 'isolated workspaces · active: ' + activeName }),
191
+ noteAlert(s.note),
192
+ section('projects',
193
+ list.length ? list.map((p, i) => Row({
194
+ key: i, code: p.name === activeName ? '●' : '○', title: p.name, sub: p.path || '',
195
+ active: p.name === activeName,
196
+ trailing: h('span', { class: 'fd-row-actions' },
197
+ p.name !== activeName ? Btn({ children: 'activate', onClick: () => activate(p.name) }) : Chip({ tone: 'ok', children: 'active' }),
198
+ p.name !== 'default' ? Btn({ danger: true, children: 'delete', onClick: () => del(p.name) }) : null),
199
+ })) : emptyState('no projects')),
200
+ section('new project',
201
+ TextField({ label: 'name', value: s.newName, onInput: (v) => { s.newName = v; }, placeholder: 'my-project' }),
202
+ TextField({ label: 'path (optional)', value: s.newPath, onInput: (v) => { s.newPath = v; }, placeholder: 'C:/path/to/dir' }),
203
+ Btn({ primary: true, disabled: s.busy, children: s.busy ? 'working…' : 'create', onClick: create })),
204
+ ];
205
+ };
206
+ });
207
+
208
+ // ---- agents ----------------------------------------------------------------
209
+
210
+ export const agents = makePage((ctx) => {
211
+ async function load() { try { ctx.set({ loading: false, data: await api('/api/agents'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
212
+ load(); ctx.interval(load, 5000);
213
+ return () => {
214
+ const s = ctx.state;
215
+ if (s.loading) return loadingState();
216
+ if (s.error) return errorState(s.error, load);
217
+ const d = s.data || {};
218
+ return [
219
+ PageHeader({ eyebrow: 'freddie', title: 'agents', lede: 'live agent activity' }),
220
+ Kpi({ items: [[d.count ?? 0, 'active'], [d.turns ?? 0, 'total turns'], [d.last_activity ? fmtAgo(d.last_activity) : '—', 'last activity']] }),
221
+ section('detail', Table({ headers: ['field', 'value'], rows: Object.entries(d).map(([k, v]) => [k, String(v)]) })),
222
+ ];
223
+ };
224
+ });
225
+
226
+ // ---- analytics -------------------------------------------------------------
227
+
228
+ export const analytics = makePage((ctx) => {
229
+ async function load() {
230
+ try {
231
+ const [sampler, avail] = await Promise.all([
232
+ api('/api/models/sampler').catch(() => null),
233
+ api('/api/models/availability/summary').catch(() => null),
234
+ ]);
235
+ ctx.set({ loading: false, sampler, avail, error: null });
236
+ } catch (e) { ctx.set({ loading: false, error: e }); }
237
+ }
238
+ load(); ctx.interval(load, 15000);
239
+ return () => {
240
+ const s = ctx.state;
241
+ if (s.loading) return loadingState();
242
+ if (s.error) return errorState(s.error, load);
243
+ const samp = s.sampler?.status ? Object.values(s.sampler.status) : [];
244
+ const ok = samp.filter(x => x && x.available !== false).length;
245
+ const sum = s.avail?.summary || {};
246
+ return [
247
+ PageHeader({ eyebrow: 'freddie', title: 'analytics', lede: 'provider availability & sampler health' }),
248
+ Kpi({ items: [[ok + '/' + samp.length, 'providers up'], [sum.total_models ?? '—', 'models'], [sum.usable_in_any_mode ?? '—', 'usable']] }),
249
+ section('sampler', samp.length ? Table({ headers: ['provider', 'available', 'fails'], rows: Object.entries(s.sampler.status).map(([k, v]) => [k, v.available === false ? 'no' : 'yes', String(v.failCount ?? 0)]) }) : emptyState('no sampler data')),
250
+ ];
251
+ };
252
+ });
253
+
254
+ // ---- models ----------------------------------------------------------------
255
+
256
+ export const models = makePage((ctx) => {
257
+ Object.assign(ctx.state, { discovering: false });
258
+ async function load() {
259
+ try {
260
+ const [providers, cached, sampler] = await Promise.all([
261
+ api('/api/models/providers').catch(() => []),
262
+ api('/api/models/cached').catch(() => ({})),
263
+ api('/api/models/sampler').catch(() => ({})),
264
+ ]);
265
+ ctx.set({ loading: false, providers, cached, sampler, error: null });
266
+ } catch (e) { ctx.set({ loading: false, error: e }); }
267
+ }
268
+ async function discover() {
269
+ if (ctx.state.discovering) return;
270
+ ctx.set({ discovering: true });
271
+ try { await api('/api/models/discover', { method: 'POST', body: {} }); await load(); }
272
+ catch (e) { ctx.set({ error: e }); }
273
+ ctx.set({ discovering: false });
274
+ }
275
+ load();
276
+ return () => {
277
+ const s = ctx.state;
278
+ if (s.loading) return loadingState();
279
+ if (s.error && !s.providers) return errorState(s.error, load);
280
+ const providers = Array.isArray(s.providers) ? s.providers : [];
281
+ const cached = s.cached || {};
282
+ const status = s.sampler?.status || {};
283
+ return [
284
+ PageHeader({ eyebrow: 'freddie', title: 'models', lede: providers.length + ' providers', right: Btn({ primary: true, disabled: s.discovering, children: s.discovering ? 'discovering…' : 'discover', onClick: discover }) }),
285
+ section('providers', providers.length ? Table({
286
+ headers: ['provider', 'sampler', 'cached models'],
287
+ rows: providers.map(p => {
288
+ const name = typeof p === 'string' ? p : p.id || p.provider;
289
+ const st = status[name];
290
+ const cm = (cached[name] || []);
291
+ return [name, st ? (st.available === false ? Chip({ tone: 'miss', children: 'down' }) : Chip({ tone: 'ok', children: 'up' })) : '—', Array.isArray(cm) ? cm.length : '—'];
292
+ }),
293
+ }) : emptyState('no providers; set provider API keys')),
294
+ ];
295
+ };
296
+ });
297
+
298
+ // ---- cron ------------------------------------------------------------------
299
+
300
+ export const cron = makePage((ctx) => {
301
+ Object.assign(ctx.state, { expr: '', prompt: '', busy: false, note: null });
302
+ async function load() { try { ctx.set({ loading: false, list: await api('/api/cron'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
303
+ async function add() {
304
+ const expr = (ctx.state.expr || '').trim(); const prompt = (ctx.state.prompt || '').trim();
305
+ if (!expr || !prompt) { ctx.set({ note: { kind: 'warn', msg: 'cron expression and prompt required' } }); return; }
306
+ ctx.set({ busy: true, note: null });
307
+ try { await api('/api/cron', { method: 'POST', body: { cron: expr, prompt } }); ctx.state.expr = ''; ctx.state.prompt = ''; await load(); }
308
+ catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
309
+ ctx.set({ busy: false });
310
+ }
311
+ async function del(id) { ctx.set({ busy: true }); try { await api('/api/cron/' + id, { method: 'DELETE' }); await load(); } catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); } ctx.set({ busy: false }); }
312
+ load();
313
+ return () => {
314
+ const s = ctx.state;
315
+ if (s.loading) return loadingState();
316
+ if (s.error && !s.list) return errorState(s.error, load);
317
+ const list = Array.isArray(s.list) ? s.list : [];
318
+ return [
319
+ PageHeader({ eyebrow: 'freddie', title: 'cron', lede: list.length + ' scheduled jobs' }),
320
+ noteAlert(s.note),
321
+ section('jobs', list.length ? list.map((j, i) => Row({
322
+ key: i, code: j.enabled ? '▶' : '⏸', title: j.cron, sub: (j.prompt || '').slice(0, 80),
323
+ trailing: Btn({ danger: true, children: 'delete', onClick: () => del(j.id) }),
324
+ })) : emptyState('no cron jobs')),
325
+ section('new job',
326
+ TextField({ label: 'cron expression', value: s.expr, onInput: (v) => { s.expr = v; }, placeholder: '0 9 * * *' }),
327
+ TextField({ label: 'prompt', value: s.prompt, multiline: true, onInput: (v) => { s.prompt = v; }, placeholder: 'what to run…' }),
328
+ Btn({ primary: true, disabled: s.busy, children: s.busy ? 'working…' : 'add job', onClick: add })),
329
+ ];
330
+ };
331
+ });
332
+
333
+ // ---- skills ----------------------------------------------------------------
334
+
335
+ export const skills = makePage((ctx) => {
336
+ Object.assign(ctx.state, { open: null });
337
+ async function load() { try { ctx.set({ loading: false, list: await api('/api/skills'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
338
+ load();
339
+ return () => {
340
+ const s = ctx.state;
341
+ if (s.loading) return loadingState();
342
+ if (s.error) return errorState(s.error, load);
343
+ const list = Array.isArray(s.list) ? s.list : (s.list?.skills || []);
344
+ return [
345
+ PageHeader({ eyebrow: 'freddie', title: 'skills', lede: list.length + ' skills' }),
346
+ section('skills', list.length ? list.map((sk, i) => h('div', { key: i },
347
+ Row({ code: (sk.source || 'fs').slice(0, 3), title: sk.name, sub: (sk.description || '').slice(0, 90),
348
+ onClick: () => ctx.set({ open: s.open === i ? null : i }), active: s.open === i }),
349
+ s.open === i ? h('pre', { class: 'fd-pre fd-skill-body' }, sk.body || sk.content || '(no body)') : null,
350
+ )) : emptyState('no skills')),
351
+ ];
352
+ };
353
+ });
354
+
355
+ // ---- config ----------------------------------------------------------------
356
+
357
+ export const config = makePage((ctx) => {
358
+ Object.assign(ctx.state, { edited: {}, busy: false, note: null });
359
+ async function load() {
360
+ try {
361
+ const [cfg, skins] = await Promise.all([api('/api/config'), api('/api/skins').catch(() => null)]);
362
+ ctx.set({ loading: false, cfg, skins, error: null });
363
+ } catch (e) { ctx.set({ loading: false, error: e }); }
364
+ }
365
+ async function save() {
366
+ ctx.set({ busy: true, note: null });
367
+ try { await api('/api/config', { method: 'POST', body: ctx.state.edited }); ctx.state.edited = {}; await load(); ctx.set({ note: { kind: 'success', msg: 'saved' } }); }
368
+ catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
369
+ ctx.set({ busy: false });
370
+ }
371
+ async function setSkin(name) {
372
+ ctx.set({ busy: true, note: null });
373
+ try { await api('/api/config', { method: 'POST', body: { skin: name } }); await load(); ctx.set({ note: { kind: 'success', msg: 'skin → ' + name } }); }
374
+ catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
375
+ ctx.set({ busy: false });
376
+ }
377
+ load();
378
+ return () => {
379
+ const s = ctx.state;
380
+ if (s.loading) return loadingState();
381
+ if (s.error) return errorState(s.error, load);
382
+ const cfg = s.cfg || {};
383
+ const flat = Object.entries(cfg).filter(([, v]) => typeof v !== 'object' || v === null);
384
+ const skinList = Array.isArray(s.skins) ? s.skins : (s.skins?.skins || s.skins?.available || []);
385
+ const activeSkin = cfg.skin || s.skins?.active || '';
386
+ return [
387
+ PageHeader({ eyebrow: 'freddie', title: 'config', lede: 'runtime configuration' }),
388
+ noteAlert(s.note),
389
+ skinList.length ? section('skin',
390
+ Select({ label: 'active skin', value: activeSkin, options: skinList, onChange: (v) => setSkin(v) })
391
+ ) : null,
392
+ section('settings', flat.length ? flat.map(([k, v], i) =>
393
+ TextField({ key: i, label: k, value: String(ctx.state.edited[k] ?? v ?? ''), onInput: (val) => { ctx.state.edited[k] = val; } })
394
+ ) : emptyState('no scalar config keys')),
395
+ section('raw', h('pre', { class: 'fd-pre' }, JSON.stringify(cfg, null, 2))),
396
+ Btn({ primary: true, disabled: s.busy || !Object.keys(s.edited).length, children: s.busy ? 'saving…' : 'save changes', onClick: save }),
397
+ ];
398
+ };
399
+ });
400
+
401
+ // ---- env -------------------------------------------------------------------
402
+
403
+ export const env = makePage((ctx) => {
404
+ async function load() { try { ctx.set({ loading: false, data: await api('/api/env'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
405
+ load();
406
+ return () => {
407
+ const s = ctx.state;
408
+ if (s.loading) return loadingState();
409
+ if (s.error) return errorState(s.error, load);
410
+ const d = s.data || {};
411
+ const rows = Object.entries(d).map(([k, v]) => [k, v === true || v === 'set' ? Chip({ tone: 'ok', children: 'set' }) : (v ? String(v) : Chip({ tone: 'neutral', children: 'unset' }))]);
412
+ return [
413
+ PageHeader({ eyebrow: 'freddie', title: 'env', lede: 'environment / key presence' }),
414
+ section('variables', rows.length ? Table({ headers: ['key', 'status'], rows }) : emptyState('no env data')),
415
+ ];
416
+ };
417
+ });
418
+
419
+ // ---- tools -----------------------------------------------------------------
420
+
421
+ export const tools = makePage((ctx) => {
422
+ Object.assign(ctx.state, { open: null, q: '' });
423
+ async function load() { try { ctx.set({ loading: false, list: await api('/api/tools'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
424
+ load();
425
+ return () => {
426
+ const s = ctx.state;
427
+ if (s.loading) return loadingState();
428
+ if (s.error) return errorState(s.error, load);
429
+ let list = Array.isArray(s.list) ? s.list : (s.list?.tools || []);
430
+ if (s.q) list = list.filter(t => (t.name || '').includes(s.q));
431
+ const groups = {};
432
+ for (const t of list) { const g = t.toolset || 'core'; (groups[g] = groups[g] || []).push(t); }
433
+ return [
434
+ PageHeader({ eyebrow: 'freddie', title: 'tools', lede: list.length + ' tools' }),
435
+ SearchInput({ value: s.q, placeholder: 'filter tools…', onInput: (v) => ctx.set({ q: v }) }),
436
+ ...Object.entries(groups).map(([g, ts]) => section(g + ' · ' + ts.length, ts.map((t, i) => h('div', { key: i },
437
+ Row({ title: t.name, sub: (t.schema?.description || t.description || '').slice(0, 90), onClick: () => ctx.set({ open: ctx.state.open === t.name ? null : t.name }), active: ctx.state.open === t.name }),
438
+ ctx.state.open === t.name ? h('pre', { class: 'fd-pre' }, JSON.stringify(t.schema || t, null, 2)) : null,
439
+ )))),
440
+ list.length ? null : emptyState('no tools match'),
441
+ ];
442
+ };
443
+ });
444
+
445
+ // ---- batch -----------------------------------------------------------------
446
+
447
+ export const batch = makePage((ctx) => {
448
+ Object.assign(ctx.state, { loading: false, prompts: '', concurrency: 4, busy: false, result: null, note: null });
449
+ async function run() {
450
+ const prompts = (ctx.state.prompts || '').split('\n').map(x => x.trim()).filter(Boolean);
451
+ if (!prompts.length) { ctx.set({ note: { kind: 'warn', msg: 'enter at least one prompt (one per line)' } }); return; }
452
+ ctx.set({ busy: true, note: null, result: null });
453
+ try { const r = await api('/api/batch', { method: 'POST', body: { prompts, concurrency: Number(ctx.state.concurrency) || 4 } }); ctx.set({ result: r }); }
454
+ catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
455
+ ctx.set({ busy: false });
456
+ }
457
+ return () => {
458
+ const s = ctx.state;
459
+ return [
460
+ PageHeader({ eyebrow: 'freddie', title: 'batch', lede: 'parallel prompt runner' }),
461
+ noteAlert(s.note),
462
+ section('prompts',
463
+ TextField({ label: 'prompts (one per line)', value: s.prompts, multiline: true, rows: 6, onInput: (v) => { s.prompts = v; } }),
464
+ TextField({ label: 'concurrency', type: 'number', value: String(s.concurrency), onInput: (v) => { s.concurrency = v; } }),
465
+ Btn({ primary: true, disabled: s.busy, children: s.busy ? 'running…' : 'run batch', onClick: run })),
466
+ s.result ? section('result', h('pre', { class: 'fd-pre' }, JSON.stringify(s.result, null, 2))) : null,
467
+ ];
468
+ };
469
+ });
470
+
471
+ // ---- gateway ---------------------------------------------------------------
472
+
473
+ export const gateway = makePage((ctx) => {
474
+ async function load() { try { ctx.set({ loading: false, data: await api('/api/gateway'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
475
+ load(); ctx.interval(load, 10000);
476
+ return () => {
477
+ const s = ctx.state;
478
+ if (s.loading) return loadingState();
479
+ if (s.error) return errorState(s.error, load);
480
+ const d = s.data || {};
481
+ const platforms = d.platforms || d;
482
+ const rows = Object.entries(platforms).map(([k, v]) => [k, typeof v === 'object' ? (v.running || v.up ? Chip({ tone: 'ok', children: 'up' }) : Chip({ tone: 'miss', children: 'down' })) : String(v)]);
483
+ return [
484
+ PageHeader({ eyebrow: 'freddie', title: 'gateway', lede: 'messaging platform status' }),
485
+ section('platforms', rows.length ? Table({ headers: ['platform', 'status'], rows }) : emptyState('no platforms configured')),
486
+ ];
487
+ };
488
+ });
489
+
490
+ // ---- chains (acptoapi) -----------------------------------------------------
491
+
492
+ export const chains = makePage((ctx) => {
493
+ Object.assign(ctx.state, { name: '', links: '', busy: false, note: null });
494
+ async function load() {
495
+ try {
496
+ const [health, list, cfg] = await Promise.all([
497
+ api('/api/acptoapi/health').catch(() => null),
498
+ api('/api/acptoapi/chains').catch(() => null),
499
+ api('/api/acptoapi/config').catch(() => null),
500
+ ]);
501
+ ctx.set({ loading: false, health, list, cfg, error: null });
502
+ } catch (e) { ctx.set({ loading: false, error: e }); }
503
+ }
504
+ async function create() {
505
+ const name = (ctx.state.name || '').trim();
506
+ const links = (ctx.state.links || '').split(',').map(x => x.trim()).filter(Boolean);
507
+ if (!name || !links.length) { ctx.set({ note: { kind: 'warn', msg: 'name and comma-separated links required' } }); return; }
508
+ ctx.set({ busy: true, note: null });
509
+ try { await api('/api/acptoapi/chains', { method: 'POST', body: { name, links } }); ctx.state.name = ''; ctx.state.links = ''; await load(); }
510
+ catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
511
+ ctx.set({ busy: false });
512
+ }
513
+ async function del(name) { ctx.set({ busy: true }); try { await api('/api/acptoapi/chains/' + encodeURIComponent(name), { method: 'DELETE' }); await load(); } catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); } ctx.set({ busy: false }); }
514
+ load();
515
+ return () => {
516
+ const s = ctx.state;
517
+ if (s.loading) return loadingState();
518
+ if (s.error && !s.cfg && !s.health) return errorState(s.error, load);
519
+ const chainsList = s.list?.chains || s.list || [];
520
+ const up = s.health && (s.health.ok || s.health.status === 'ok' || s.health.healthy);
521
+ return [
522
+ PageHeader({ eyebrow: 'freddie', title: 'chains', lede: 'acptoapi fallback chains', right: up ? Chip({ tone: 'ok', children: 'acptoapi up' }) : Chip({ tone: 'miss', children: 'acptoapi down' }) }),
523
+ noteAlert(s.note),
524
+ section('chains', Array.isArray(chainsList) && chainsList.length ? chainsList.map((c, i) => Row({
525
+ key: i, title: c.name || c, sub: Array.isArray(c.links) ? c.links.join(' → ') : '',
526
+ trailing: Btn({ danger: true, children: 'delete', onClick: () => del(c.name || c) }),
527
+ })) : emptyState('no chains defined')),
528
+ section('new chain',
529
+ TextField({ label: 'name', value: s.name, onInput: (v) => { s.name = v; } }),
530
+ TextField({ label: 'links (comma-separated models)', value: s.links, onInput: (v) => { s.links = v; }, placeholder: 'mistral/large, openrouter/auto' }),
531
+ Btn({ primary: true, disabled: s.busy, children: s.busy ? 'working…' : 'create chain', onClick: create })),
532
+ s.cfg ? section('config', h('pre', { class: 'fd-pre' }, JSON.stringify(s.cfg, null, 2))) : null,
533
+ ];
534
+ };
535
+ });
536
+
537
+ // ---- machines --------------------------------------------------------------
538
+
539
+ export const machines = makePage((ctx) => {
540
+ async function load() { try { ctx.set({ loading: false, data: await api('/api/machines'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
541
+ load(); ctx.interval(load, 8000);
542
+ return () => {
543
+ const s = ctx.state;
544
+ if (s.loading) return loadingState();
545
+ if (s.error) return errorState(s.error, load);
546
+ const d = s.data || {};
547
+ const list = Array.isArray(d) ? d : (d.machines || Object.entries(d).map(([kind, v]) => ({ kind, ...(typeof v === 'object' ? v : { value: v }) })));
548
+ return [
549
+ PageHeader({ eyebrow: 'freddie', title: 'machines', lede: 'persisted xstate machine census' }),
550
+ section('machines', list.length ? Table({
551
+ headers: ['kind', 'key', 'state'],
552
+ rows: list.map(m => [m.kind || '—', m.key || m.machine_id || '—', m.state || m.value || JSON.stringify(m).slice(0, 60)]),
553
+ }) : emptyState('no live machines')),
554
+ ];
555
+ };
556
+ });
557
+
558
+ // ---- health ----------------------------------------------------------------
559
+
560
+ export const health = makePage((ctx) => {
561
+ async function load() {
562
+ try {
563
+ const [health, providers] = await Promise.all([
564
+ api('/api/health').catch(() => null),
565
+ api('/api/providers').catch(() => null),
566
+ ]);
567
+ ctx.set({ loading: false, health, providers, error: null });
568
+ } catch (e) { ctx.set({ loading: false, error: e }); }
569
+ }
570
+ load(); ctx.interval(load, 15000);
571
+ return () => {
572
+ const s = ctx.state;
573
+ if (s.loading) return loadingState();
574
+ if (s.error) return errorState(s.error, load);
575
+ const hd = s.health || {};
576
+ const provs = Array.isArray(s.providers) ? s.providers : (s.providers?.providers || []);
577
+ return [
578
+ PageHeader({ eyebrow: 'freddie', title: 'health', lede: 'system & provider health', right: hd.ok ? Chip({ tone: 'ok', children: 'healthy' }) : Chip({ tone: 'miss', children: 'degraded' }) }),
579
+ section('checks', Object.keys(hd).length ? Table({ headers: ['check', 'status'], rows: Object.entries(hd).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : (v === true ? Chip({ tone: 'ok', children: 'ok' }) : v === false ? Chip({ tone: 'miss', children: 'no' }) : String(v))]) }) : emptyState('no health data')),
580
+ provs.length ? section('providers', Table({ headers: ['provider', 'status'], rows: provs.map(p => { const n = typeof p === 'string' ? p : p.name || p.id; const ok = typeof p === 'object' ? (p.ok ?? p.available) : null; return [n, ok == null ? '—' : (ok ? Chip({ tone: 'ok', children: 'up' }) : Chip({ tone: 'miss', children: 'down' }))]; }) })) : null,
581
+ ];
582
+ };
583
+ });
584
+
585
+ // ---- debug -----------------------------------------------------------------
586
+
587
+ export const debug = makePage((ctx) => {
588
+ Object.assign(ctx.state, { sub: null, logs: null });
589
+ async function load() { try { ctx.set({ loading: false, data: await api('/api/debug'), error: null }); } catch (e) { ctx.set({ loading: false, error: e }); } }
590
+ async function loadLogs(name) {
591
+ ctx.set({ sub: name });
592
+ try { ctx.set({ logs: await api('/api/logs/' + encodeURIComponent(name)) }); }
593
+ catch (e) { ctx.set({ logs: { error: String(e.message || e) } }); }
594
+ }
595
+ load();
596
+ return () => {
597
+ const s = ctx.state;
598
+ if (s.loading) return loadingState();
599
+ if (s.error) return errorState(s.error, load);
600
+ const d = s.data || {};
601
+ const subsystems = d.subsystems || Object.keys(d);
602
+ return [
603
+ PageHeader({ eyebrow: 'freddie', title: 'debug', lede: 'subsystem snapshots & logs' }),
604
+ section('subsystems', subsystems.length ? subsystems.map((name, i) => Row({
605
+ key: i, title: name, onClick: () => loadLogs(name), active: s.sub === name,
606
+ })) : emptyState('no debug subsystems')),
607
+ s.sub ? section('logs · ' + s.sub, h('pre', { class: 'fd-pre' }, JSON.stringify(s.logs, null, 2))) : null,
608
+ ];
609
+ };
610
+ });
611
+
612
+ // ---- registry --------------------------------------------------------------
27
613
 
28
614
  export const FREDDIE_PAGES = {
29
615
  home, chat, voice, sessions, projects, agents, analytics,
30
- models, cron, skills, config, env, tools, batch, gateway, chains
616
+ models, cron, skills, config, env, tools, batch, gateway, chains,
617
+ machines, health, debug,
31
618
  };
32
619
 
33
620
  export { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages };