clementine-agent 1.2.2 → 1.3.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.
Files changed (41) hide show
  1. package/dist/agent/assistant.js +12 -0
  2. package/dist/cli/dashboard.js +3034 -734
  3. package/dist/cli/static/LICENSE-NOTICES.md +12 -0
  4. package/dist/cli/static/drawflow.min.css +1 -0
  5. package/dist/cli/static/drawflow.min.js +1 -0
  6. package/dist/config.d.ts +11 -0
  7. package/dist/config.js +16 -0
  8. package/dist/dashboard/builder/dry-run.d.ts +31 -0
  9. package/dist/dashboard/builder/dry-run.js +138 -0
  10. package/dist/dashboard/builder/events.d.ts +23 -0
  11. package/dist/dashboard/builder/events.js +28 -0
  12. package/dist/dashboard/builder/mcp-invoke.d.ts +25 -0
  13. package/dist/dashboard/builder/mcp-invoke.js +143 -0
  14. package/dist/dashboard/builder/runner.d.ts +68 -0
  15. package/dist/dashboard/builder/runner.js +418 -0
  16. package/dist/dashboard/builder/serializer.d.ts +79 -0
  17. package/dist/dashboard/builder/serializer.js +547 -0
  18. package/dist/dashboard/builder/snapshots.d.ts +32 -0
  19. package/dist/dashboard/builder/snapshots.js +138 -0
  20. package/dist/dashboard/builder/validation.d.ts +26 -0
  21. package/dist/dashboard/builder/validation.js +183 -0
  22. package/dist/gateway/router.js +31 -2
  23. package/dist/index.js +38 -0
  24. package/dist/memory/chunker.js +13 -2
  25. package/dist/memory/hot-cache.d.ts +38 -0
  26. package/dist/memory/hot-cache.js +73 -0
  27. package/dist/memory/integrity.d.ts +28 -0
  28. package/dist/memory/integrity.js +119 -0
  29. package/dist/memory/maintenance.d.ts +23 -2
  30. package/dist/memory/maintenance.js +140 -3
  31. package/dist/memory/store.d.ts +259 -2
  32. package/dist/memory/store.js +751 -21
  33. package/dist/memory/write-queue.d.ts +96 -0
  34. package/dist/memory/write-queue.js +165 -0
  35. package/dist/tools/builder-tools.d.ts +13 -0
  36. package/dist/tools/builder-tools.js +437 -0
  37. package/dist/tools/mcp-server.js +2 -0
  38. package/dist/tools/memory-tools.js +38 -1
  39. package/dist/types.d.ts +56 -2
  40. package/package.json +2 -2
  41. package/vault/00-System/skills/builder-canvas.md +126 -0
@@ -18,7 +18,7 @@ import cron from 'node-cron';
18
18
  import { TunnelManager } from './tunnel.js';
19
19
  import { AgentManager } from '../agent/agent-manager.js';
20
20
  import { discoverMcpServers, getClaudeIntegrations } from '../agent/mcp-bridge.js';
21
- import { AGENTS_DIR } from '../config.js';
21
+ import { AGENTS_DIR, SESSIONS_FILE } from '../config.js';
22
22
  import { parseTasks } from '../tools/shared.js';
23
23
  import { todayISO } from '../gateway/cron-scheduler.js';
24
24
  import { goalsRouter } from './routes/goals.js';
@@ -1265,6 +1265,13 @@ export async function cmdDashboard(opts) {
1265
1265
  const app = express();
1266
1266
  const jsonParser = express.json({ limit: '5mb' });
1267
1267
  const rawBodyParser = express.raw({ type: '*/*', limit: '5mb' });
1268
+ // Vendored frontend assets (Drawflow for the Builder canvas, etc.)
1269
+ // Public — no auth required.
1270
+ app.use('/static', express.static(path.join(__dirname, 'static'), {
1271
+ maxAge: '7d',
1272
+ etag: true,
1273
+ index: false,
1274
+ }));
1268
1275
  // Health check — always responds, no auth, no middleware dependency
1269
1276
  app.get('/health', (_req, res) => { res.json({ ok: true, ts: Date.now() }); });
1270
1277
  // ── Webhook ingestion (raw-body, HMAC-authed) ─────────────────────
@@ -1439,44 +1446,92 @@ export async function cmdDashboard(opts) {
1439
1446
  writeFileSync(tokenPath, dashboardToken, { mode: 0o600 });
1440
1447
  // ── Remote access + session management ─────────────────────────────
1441
1448
  const remoteConfig = loadRemoteConfig();
1442
- const sessions = new Map(); // sessionId → expiresAt
1449
+ const sessions = new Map();
1443
1450
  let tunnelManager = null;
1444
- const SESSION_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours
1451
+ const SESSION_TTL_DEFAULT = 24 * 60 * 60 * 1000; // 24 hours
1452
+ const SESSION_TTL_PERSISTENT = 30 * 24 * 60 * 60 * 1000; // 30 days
1445
1453
  const loginRateLimit = { count: 0, resetAt: Date.now() + 15 * 60 * 1000 };
1454
+ function loadSessions() {
1455
+ if (!existsSync(SESSIONS_FILE))
1456
+ return;
1457
+ try {
1458
+ const raw = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
1459
+ const now = Date.now();
1460
+ for (const s of raw) {
1461
+ if (s && s.id && s.expiresAt > now)
1462
+ sessions.set(s.id, s);
1463
+ }
1464
+ }
1465
+ catch { /* corrupt file — start fresh */ }
1466
+ }
1467
+ function persistSessions() {
1468
+ try {
1469
+ writeFileSync(SESSIONS_FILE, JSON.stringify(Array.from(sessions.values()), null, 2), { mode: 0o600 });
1470
+ }
1471
+ catch { /* best-effort; in-memory store still works */ }
1472
+ }
1473
+ loadSessions();
1446
1474
  function isRemoteRequest(req) {
1447
1475
  // cloudflared sets CF-Connecting-IP for tunneled traffic
1448
1476
  return Boolean(req.headers['cf-connecting-ip']);
1449
1477
  }
1450
- function hasValidSession(req) {
1478
+ function readSessionId(req) {
1451
1479
  const cookie = req.headers.cookie ?? '';
1452
1480
  const match = cookie.match(/__clem_session=([a-f0-9]+)/);
1453
- if (!match)
1481
+ return match ? match[1] : null;
1482
+ }
1483
+ function hasValidSession(req) {
1484
+ const sessionId = readSessionId(req);
1485
+ if (!sessionId)
1454
1486
  return false;
1455
- const sessionId = match[1];
1456
- const expiresAt = sessions.get(sessionId);
1457
- if (!expiresAt || Date.now() > expiresAt) {
1458
- sessions.delete(sessionId);
1487
+ const record = sessions.get(sessionId);
1488
+ if (!record || Date.now() > record.expiresAt) {
1489
+ if (record) {
1490
+ sessions.delete(sessionId);
1491
+ persistSessions();
1492
+ }
1459
1493
  return false;
1460
1494
  }
1495
+ record.lastUsedAt = Date.now();
1496
+ // Don't persist on every request — the cleanup interval will pick it up
1461
1497
  return true;
1462
1498
  }
1463
- function createSession(res) {
1499
+ function createSession(res, req, persistent = false) {
1464
1500
  const sessionId = randomBytes(32).toString('hex');
1465
- sessions.set(sessionId, Date.now() + SESSION_MAX_AGE);
1501
+ const ttl = persistent ? SESSION_TTL_PERSISTENT : SESSION_TTL_DEFAULT;
1502
+ const now = Date.now();
1503
+ const record = {
1504
+ id: sessionId,
1505
+ expiresAt: now + ttl,
1506
+ persistent,
1507
+ createdAt: now,
1508
+ lastUsedAt: now,
1509
+ userAgent: typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'].slice(0, 200) : undefined,
1510
+ };
1511
+ sessions.set(sessionId, record);
1512
+ persistSessions();
1466
1513
  res.cookie('__clem_session', sessionId, {
1467
1514
  httpOnly: true,
1468
1515
  sameSite: 'lax',
1469
- maxAge: SESSION_MAX_AGE,
1516
+ maxAge: ttl,
1470
1517
  path: '/',
1471
1518
  });
1519
+ return sessionId;
1472
1520
  }
1473
- // Clean expired sessions every 10 minutes
1521
+ function revokeSession(sessionId) {
1522
+ const existed = sessions.delete(sessionId);
1523
+ if (existed)
1524
+ persistSessions();
1525
+ return existed;
1526
+ }
1527
+ // Clean expired sessions every 10 minutes; also persist lastUsedAt updates
1474
1528
  setInterval(() => {
1475
1529
  const now = Date.now();
1476
- for (const [id, exp] of sessions) {
1477
- if (now > exp)
1530
+ for (const [id, rec] of sessions) {
1531
+ if (now > rec.expiresAt)
1478
1532
  sessions.delete(id);
1479
1533
  }
1534
+ persistSessions();
1480
1535
  }, 10 * 60 * 1000);
1481
1536
  // Quick ping — bypasses all middleware, tests /api path routing
1482
1537
  app.get('/api/ping', (_req, res) => { res.json({ pong: true }); });
@@ -1734,7 +1789,7 @@ export async function cmdDashboard(opts) {
1734
1789
  res.status(429).json({ error: 'Too many login attempts. Try again later.' });
1735
1790
  return;
1736
1791
  }
1737
- const { token } = req.body ?? {};
1792
+ const { token, remember } = req.body ?? {};
1738
1793
  if (!token || typeof token !== 'string') {
1739
1794
  res.status(400).json({ error: 'Token is required' });
1740
1795
  return;
@@ -1748,13 +1803,48 @@ export async function cmdDashboard(opts) {
1748
1803
  res.status(401).json({ error: 'Invalid access token' });
1749
1804
  return;
1750
1805
  }
1751
- createSession(res);
1806
+ createSession(res, req, Boolean(remember));
1752
1807
  res.json({ ok: true });
1753
1808
  });
1754
- app.get('/auth/logout', (_req, res) => {
1809
+ app.get('/auth/logout', (req, res) => {
1810
+ const sessionId = readSessionId(req);
1811
+ if (sessionId)
1812
+ revokeSession(sessionId);
1755
1813
  res.clearCookie('__clem_session', { path: '/' });
1756
1814
  res.redirect('/');
1757
1815
  });
1816
+ // List active sessions (cookie-authenticated; bearer-token gate doesn't apply at /auth/*)
1817
+ app.get('/auth/sessions', (req, res) => {
1818
+ if (!hasValidSession(req)) {
1819
+ res.status(401).json({ error: 'Unauthorized' });
1820
+ return;
1821
+ }
1822
+ const currentId = readSessionId(req);
1823
+ const list = Array.from(sessions.values())
1824
+ .sort((a, b) => b.lastUsedAt - a.lastUsedAt)
1825
+ .map(s => ({
1826
+ id: s.id,
1827
+ createdAt: s.createdAt,
1828
+ lastUsedAt: s.lastUsedAt,
1829
+ expiresAt: s.expiresAt,
1830
+ persistent: s.persistent,
1831
+ userAgent: s.userAgent ?? null,
1832
+ current: s.id === currentId,
1833
+ }));
1834
+ res.json({ sessions: list });
1835
+ });
1836
+ app.delete('/auth/sessions/:id', (req, res) => {
1837
+ if (!hasValidSession(req)) {
1838
+ res.status(401).json({ error: 'Unauthorized' });
1839
+ return;
1840
+ }
1841
+ const targetId = req.params.id;
1842
+ const existed = revokeSession(targetId);
1843
+ if (readSessionId(req) === targetId) {
1844
+ res.clearCookie('__clem_session', { path: '/' });
1845
+ }
1846
+ res.json({ ok: existed });
1847
+ });
1758
1848
  // ── Anthropic OAuth routes ──────────────────────────────────────
1759
1849
  // Check current auth status by spawning a lightweight SDK query
1760
1850
  // Anthropic auth status — check daemon's .env for API key presence instead of importing the SDK
@@ -2320,6 +2410,260 @@ export async function cmdDashboard(opts) {
2320
2410
  }
2321
2411
  // Let the lazy-gateway dispatcher publish deep_result events through SSE.
2322
2412
  dashboardSseBroadcast = broadcastEvent;
2413
+ // ── Builder event bridge ──────────────────────────────────────
2414
+ // Forward events from src/dashboard/builder/events.ts through SSE so the
2415
+ // Builder page can update its canvas live as the agent edits via MCP tools.
2416
+ (async () => {
2417
+ try {
2418
+ const { onAnyBuilderEvent } = await import('../dashboard/builder/events.js');
2419
+ onAnyBuilderEvent((e) => {
2420
+ broadcastEvent({ type: 'builder', data: e });
2421
+ });
2422
+ }
2423
+ catch {
2424
+ // Builder event bridge optional; ignore if module fails to load.
2425
+ }
2426
+ })();
2427
+ // ── Builder API routes ─────────────────────────────────────────
2428
+ app.get('/api/builder/workflows', async (_req, res) => {
2429
+ try {
2430
+ const { listAllForBuilder } = await import('../dashboard/builder/serializer.js');
2431
+ res.json({ workflows: listAllForBuilder() });
2432
+ }
2433
+ catch (err) {
2434
+ res.status(500).json({ error: 'Failed to list workflows', detail: String(err) });
2435
+ }
2436
+ });
2437
+ app.get('/api/builder/workflows/:id', async (req, res) => {
2438
+ try {
2439
+ const id = decodeURIComponent(req.params.id);
2440
+ const [{ readWorkflow, workflowToDrawflow }, { validateWorkflow }] = await Promise.all([
2441
+ import('../dashboard/builder/serializer.js'),
2442
+ import('../dashboard/builder/validation.js'),
2443
+ ]);
2444
+ const wf = readWorkflow(id);
2445
+ if (!wf) {
2446
+ res.status(404).json({ error: 'Not found' });
2447
+ return;
2448
+ }
2449
+ res.json({
2450
+ id,
2451
+ workflow: wf,
2452
+ drawflow: workflowToDrawflow(wf),
2453
+ validation: validateWorkflow(wf),
2454
+ });
2455
+ }
2456
+ catch (err) {
2457
+ res.status(500).json({ error: 'Failed to read workflow', detail: String(err) });
2458
+ }
2459
+ });
2460
+ app.put('/api/builder/workflows/:id', async (req, res) => {
2461
+ try {
2462
+ const id = decodeURIComponent(req.params.id);
2463
+ const body = req.body;
2464
+ if (!body || typeof body.workflow !== 'object') {
2465
+ res.status(400).json({ error: 'Missing workflow body' });
2466
+ return;
2467
+ }
2468
+ const [{ readWorkflow, saveWorkflow }, { validateWorkflow }, { emitBuilderEvent }] = await Promise.all([
2469
+ import('../dashboard/builder/serializer.js'),
2470
+ import('../dashboard/builder/validation.js'),
2471
+ import('../dashboard/builder/events.js'),
2472
+ ]);
2473
+ const existing = readWorkflow(id);
2474
+ if (!existing) {
2475
+ res.status(404).json({ error: 'Not found' });
2476
+ return;
2477
+ }
2478
+ const incoming = body.workflow;
2479
+ const next = { ...incoming, sourceFile: existing.sourceFile };
2480
+ const v = validateWorkflow(next);
2481
+ if (!v.ok && !body.force) {
2482
+ res.status(400).json({ error: 'validation', validation: v });
2483
+ return;
2484
+ }
2485
+ const result = saveWorkflow(id, next);
2486
+ if (!result.ok) {
2487
+ res.status(400).json({ error: result.error });
2488
+ return;
2489
+ }
2490
+ emitBuilderEvent({ type: 'workflow:patched', workflowId: id, payload: { workflow: next } });
2491
+ res.json({ ok: true, validation: v });
2492
+ }
2493
+ catch (err) {
2494
+ res.status(500).json({ error: 'Failed to save workflow', detail: String(err) });
2495
+ }
2496
+ });
2497
+ app.get('/api/builder/mcp-discovery', async (_req, res) => {
2498
+ try {
2499
+ const { discoverMcpServers, loadToolInventory } = await import('../agent/mcp-bridge.js');
2500
+ const servers = discoverMcpServers();
2501
+ const inv = loadToolInventory();
2502
+ const out = servers.map((s) => ({
2503
+ name: s.name,
2504
+ enabled: s.enabled,
2505
+ tools: (inv?.tools ?? [])
2506
+ .filter((t) => t.startsWith(`mcp__${s.name}__`))
2507
+ .map((t) => t.split('__')[2])
2508
+ .filter((x) => !!x),
2509
+ }));
2510
+ res.json({ servers: out });
2511
+ }
2512
+ catch (err) {
2513
+ res.status(500).json({ error: 'mcp-discovery failed', detail: String(err) });
2514
+ }
2515
+ });
2516
+ app.post('/api/builder/workflows/:id/save-from-drawflow', async (req, res) => {
2517
+ try {
2518
+ const id = decodeURIComponent(req.params.id);
2519
+ const body = req.body;
2520
+ if (!body || !body.drawflow) {
2521
+ res.status(400).json({ error: 'Missing drawflow body' });
2522
+ return;
2523
+ }
2524
+ const [{ readWorkflow, drawflowToWorkflow, saveWorkflow, workflowToDrawflow }, { validateWorkflow }, { emitBuilderEvent }] = await Promise.all([
2525
+ import('../dashboard/builder/serializer.js'),
2526
+ import('../dashboard/builder/validation.js'),
2527
+ import('../dashboard/builder/events.js'),
2528
+ ]);
2529
+ const existing = readWorkflow(id);
2530
+ if (!existing) {
2531
+ res.status(404).json({ error: 'Not found' });
2532
+ return;
2533
+ }
2534
+ const next = drawflowToWorkflow(body.drawflow, existing);
2535
+ const v = validateWorkflow(next);
2536
+ if (!v.ok && !body.force) {
2537
+ res.status(400).json({ error: 'validation', validation: v });
2538
+ return;
2539
+ }
2540
+ const result = saveWorkflow(id, next);
2541
+ if (!result.ok) {
2542
+ res.status(400).json({ error: result.error });
2543
+ return;
2544
+ }
2545
+ emitBuilderEvent({ type: 'workflow:patched', workflowId: id, payload: { workflow: next, saveToken: body.saveToken ?? null } });
2546
+ res.json({ ok: true, validation: v, drawflow: workflowToDrawflow(next) });
2547
+ }
2548
+ catch (err) {
2549
+ res.status(500).json({ error: 'Save failed', detail: String(err) });
2550
+ }
2551
+ });
2552
+ app.post('/api/builder/workflows/:id/validate', async (req, res) => {
2553
+ try {
2554
+ const id = decodeURIComponent(req.params.id);
2555
+ const [{ readWorkflow }, { validateWorkflow }] = await Promise.all([
2556
+ import('../dashboard/builder/serializer.js'),
2557
+ import('../dashboard/builder/validation.js'),
2558
+ ]);
2559
+ const wf = readWorkflow(id);
2560
+ if (!wf) {
2561
+ res.status(404).json({ error: 'Not found' });
2562
+ return;
2563
+ }
2564
+ res.json(validateWorkflow(wf));
2565
+ }
2566
+ catch (err) {
2567
+ res.status(500).json({ error: 'Validate failed', detail: String(err) });
2568
+ }
2569
+ });
2570
+ app.post('/api/builder/workflows/:id/test', async (req, res) => {
2571
+ try {
2572
+ const id = decodeURIComponent(req.params.id);
2573
+ const body = (req.body ?? {});
2574
+ const [{ readWorkflow }, { runWorkflowTest }] = await Promise.all([
2575
+ import('../dashboard/builder/serializer.js'),
2576
+ import('../dashboard/builder/runner.js'),
2577
+ ]);
2578
+ const wf = readWorkflow(id);
2579
+ if (!wf) {
2580
+ res.status(404).json({ error: 'Not found' });
2581
+ return;
2582
+ }
2583
+ const runId = (await import('node:crypto')).randomUUID();
2584
+ // Kick off async — respond immediately with the runId; events stream over SSE.
2585
+ res.json({ ok: true, runId });
2586
+ runWorkflowTest(wf, {
2587
+ workflowId: id,
2588
+ runId,
2589
+ mode: body.mode ?? 'mock',
2590
+ perStepTimeoutMs: body.perStepTimeoutMs,
2591
+ totalBudgetMs: body.totalBudgetMs,
2592
+ }).catch(() => { });
2593
+ }
2594
+ catch (err) {
2595
+ res.status(500).json({ error: 'Test failed to start', detail: String(err) });
2596
+ }
2597
+ });
2598
+ app.post('/api/builder/runs/:runId/cancel', async (req, res) => {
2599
+ try {
2600
+ const runId = decodeURIComponent(req.params.runId);
2601
+ const { cancelRun } = await import('../dashboard/builder/runner.js');
2602
+ const cancelled = cancelRun(runId);
2603
+ res.json({ ok: cancelled });
2604
+ }
2605
+ catch (err) {
2606
+ res.status(500).json({ error: 'Cancel failed', detail: String(err) });
2607
+ }
2608
+ });
2609
+ app.post('/api/builder/workflows/:id/dry-run', async (req, res) => {
2610
+ try {
2611
+ const id = decodeURIComponent(req.params.id);
2612
+ const [{ readWorkflow }, { dryRunWorkflow }] = await Promise.all([
2613
+ import('../dashboard/builder/serializer.js'),
2614
+ import('../dashboard/builder/dry-run.js'),
2615
+ ]);
2616
+ const wf = readWorkflow(id);
2617
+ if (!wf) {
2618
+ res.status(404).json({ error: 'Not found' });
2619
+ return;
2620
+ }
2621
+ res.json(dryRunWorkflow(wf));
2622
+ }
2623
+ catch (err) {
2624
+ res.status(500).json({ error: 'Dry-run failed', detail: String(err) });
2625
+ }
2626
+ });
2627
+ app.post('/api/builder/workflows', async (req, res) => {
2628
+ try {
2629
+ const body = req.body;
2630
+ if (!body || !body.name) {
2631
+ res.status(400).json({ error: 'name required' });
2632
+ return;
2633
+ }
2634
+ const [{ saveWorkflow, workflowId }, { emitBuilderEvent }] = await Promise.all([
2635
+ import('../dashboard/builder/serializer.js'),
2636
+ import('../dashboard/builder/events.js'),
2637
+ ]);
2638
+ const slug = body.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'workflow';
2639
+ const wf = {
2640
+ name: body.name,
2641
+ description: body.description ?? '',
2642
+ enabled: true,
2643
+ trigger: body.schedule ? { schedule: body.schedule, manual: false } : { manual: true },
2644
+ inputs: {},
2645
+ steps: [{
2646
+ id: 's1',
2647
+ prompt: body.initialPrompt ?? 'Describe what this workflow should do.',
2648
+ dependsOn: [],
2649
+ tier: 1,
2650
+ maxTurns: 15,
2651
+ }],
2652
+ sourceFile: '',
2653
+ };
2654
+ const id = workflowId(slug);
2655
+ const result = saveWorkflow(id, wf);
2656
+ if (!result.ok) {
2657
+ res.status(400).json({ error: result.error });
2658
+ return;
2659
+ }
2660
+ emitBuilderEvent({ type: 'workflow:created', workflowId: id, payload: { workflow: wf } });
2661
+ res.json({ ok: true, id });
2662
+ }
2663
+ catch (err) {
2664
+ res.status(500).json({ error: 'Create failed', detail: String(err) });
2665
+ }
2666
+ });
2323
2667
  // SSE events handler moved before auth middleware (see above)
2324
2668
  // ── POST routes (actions) ──────────────────────────────────────
2325
2669
  app.post('/api/cron/run/:job', (req, res) => {
@@ -4521,6 +4865,51 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
4521
4865
  // instead of having to wait for auto-extraction to drift in the right
4522
4866
  // direction. Soft-delete via deleted_at; FTS trigger keeps deleted
4523
4867
  // content out of search results.
4868
+ // Memory Health snapshot — single endpoint feeding the dashboard tab.
4869
+ // Read-only aggregate over the existing tables; no caching needed (cheap).
4870
+ app.get('/api/memory/health', async (_req, res) => {
4871
+ try {
4872
+ const gateway = await getGateway();
4873
+ const store = gateway.assistant?.memoryStore;
4874
+ if (!store?.getMemoryHealth) {
4875
+ res.status(503).json({ error: 'Memory store not available' });
4876
+ return;
4877
+ }
4878
+ const graphStore = gateway.assistant?.graphStore;
4879
+ const health = store.getMemoryHealth({ graphStore, topCitedLimit: 10 });
4880
+ res.json({ ok: true, health });
4881
+ }
4882
+ catch (err) {
4883
+ res.status(500).json({ error: String(err) });
4884
+ }
4885
+ });
4886
+ app.post('/api/memory/health/action', async (req, res) => {
4887
+ try {
4888
+ const action = (req.body?.action ?? '');
4889
+ const gateway = await getGateway();
4890
+ const store = gateway.assistant?.memoryStore;
4891
+ if (!store) {
4892
+ res.status(503).json({ error: 'Memory store not available' });
4893
+ return;
4894
+ }
4895
+ if (action === 'janitor') {
4896
+ const { runJanitor } = await import('../memory/maintenance.js');
4897
+ const result = runJanitor(store);
4898
+ res.json({ ok: true, action, result });
4899
+ return;
4900
+ }
4901
+ if (action === 'rebuild-fts' || action === 'fix-orphans') {
4902
+ const { runIntegrityProbes } = await import('../memory/integrity.js');
4903
+ const report = runIntegrityProbes(store);
4904
+ res.json({ ok: true, action, report });
4905
+ return;
4906
+ }
4907
+ res.status(400).json({ error: 'Unknown action: ' + action });
4908
+ }
4909
+ catch (err) {
4910
+ res.status(500).json({ error: String(err) });
4911
+ }
4912
+ });
4524
4913
  app.get('/api/memory/chunks/:id', async (req, res) => {
4525
4914
  try {
4526
4915
  const id = Number(req.params.id);
@@ -7730,6 +8119,7 @@ function getDashboardHTML(token) {
7730
8119
  if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(r){r.forEach(function(sw){sw.unregister()})});caches.keys().then(function(k){k.forEach(function(n){caches.delete(n)})})}
7731
8120
  </script>
7732
8121
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
8122
+ <link rel="stylesheet" href="/static/drawflow.min.css">
7733
8123
  <title>${name} Command Center</title>
7734
8124
  <style>
7735
8125
  :root {
@@ -7989,101 +8379,414 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
7989
8379
  letter-spacing: -0.02em;
7990
8380
  }
7991
8381
 
7992
- /* ── Cards ──────────────────────────────── */
7993
- .card {
7994
- background: var(--bg-card);
7995
- backdrop-filter: blur(8px);
7996
- border: 1px solid var(--border);
7997
- border-radius: var(--radius);
7998
- margin-bottom: 16px;
7999
- overflow: hidden;
8000
- box-shadow: 0 2px 8px rgba(0,0,0,0.06);
8001
- }
8002
- .card-header {
8003
- padding: 14px 18px;
8382
+ /* ── Standard page header (.page-head) — applied to most info pages ── */
8383
+ .page-head {
8384
+ display: flex;
8385
+ align-items: flex-start;
8386
+ gap: 14px;
8387
+ padding: 18px 22px 14px;
8004
8388
  border-bottom: 1px solid var(--border);
8389
+ margin-bottom: 18px;
8390
+ flex-wrap: wrap;
8391
+ }
8392
+ .page-head .icon {
8393
+ width: 36px;
8394
+ height: 36px;
8395
+ border-radius: 8px;
8396
+ background: linear-gradient(135deg, rgba(255,140,33,0.15), rgba(255,140,33,0.04));
8397
+ color: var(--clementine);
8005
8398
  display: flex;
8006
8399
  align-items: center;
8007
- justify-content: space-between;
8008
- font-size: 13px;
8400
+ justify-content: center;
8401
+ font-size: 20px;
8402
+ flex-shrink: 0;
8403
+ }
8404
+ .page-head .title-block {
8405
+ flex: 1;
8406
+ min-width: 220px;
8407
+ }
8408
+ .page-head .title-block h1 {
8409
+ font-size: 20px;
8009
8410
  font-weight: 600;
8411
+ margin: 0 0 2px;
8412
+ letter-spacing: -0.01em;
8010
8413
  color: var(--text-primary);
8011
8414
  }
8012
- .card-body {
8013
- padding: 18px;
8415
+ .page-head .title-block .desc {
8014
8416
  font-size: 13px;
8015
- line-height: 1.7;
8016
- }
8017
- .grid-2 {
8018
- display: grid;
8019
- grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
8020
- gap: 20px;
8021
- }
8022
-
8023
- /* ── Getting Started ────────────────────── */
8024
- .getting-started {
8025
- background: linear-gradient(135deg, var(--clementine-bg), rgba(77,158,255,0.06));
8026
- border: 1px solid var(--border);
8027
- border-radius: 16px;
8028
- padding: 24px;
8029
- margin-bottom: 24px;
8030
- position: relative;
8417
+ color: var(--text-muted);
8418
+ margin: 0;
8419
+ line-height: 1.4;
8031
8420
  }
8032
- .gs-header {
8421
+ .page-head .actions {
8033
8422
  display: flex;
8423
+ gap: 8px;
8034
8424
  align-items: center;
8035
- gap: 12px;
8036
- margin-bottom: 20px;
8425
+ flex-wrap: wrap;
8037
8426
  }
8038
- .gs-title {
8039
- font-size: 18px;
8040
- font-weight: 700;
8041
- color: var(--text-primary);
8427
+ .page-section {
8428
+ padding: 0 22px 22px;
8042
8429
  }
8043
- .gs-subtitle {
8044
- font-size: 13px;
8430
+ /* ── First-time empty state with CTA (use when there's truly no data yet) ── */
8431
+ .empty-cta {
8432
+ text-align: center;
8433
+ padding: 48px 24px;
8045
8434
  color: var(--text-muted);
8046
- flex: 1;
8047
8435
  }
8048
- .gs-dismiss {
8049
- position: absolute;
8050
- top: 12px;
8051
- right: 12px;
8052
- font-size: 18px;
8436
+ .empty-cta .icon {
8437
+ font-size: 36px;
8438
+ margin-bottom: 12px;
8439
+ opacity: 0.5;
8053
8440
  }
8054
- .gs-grid {
8055
- display: grid;
8056
- grid-template-columns: repeat(5, 1fr);
8057
- gap: 16px;
8441
+ .empty-cta .label {
8442
+ font-size: 14px;
8443
+ margin-bottom: 4px;
8444
+ color: var(--text-secondary);
8445
+ font-weight: 500;
8058
8446
  }
8059
- .gs-card {
8060
- background: var(--bg-card);
8061
- border: 1px solid var(--border);
8062
- border-radius: var(--radius);
8063
- padding: 20px 16px;
8064
- text-align: center;
8065
- position: relative;
8066
- display: flex;
8067
- flex-direction: column;
8068
- align-items: center;
8069
- gap: 8px;
8070
- transition: transform 0.15s, box-shadow 0.15s;
8447
+ .empty-cta .hint {
8448
+ font-size: 12px;
8449
+ color: var(--text-muted);
8450
+ margin-bottom: 18px;
8071
8451
  }
8072
- .gs-card:hover {
8073
- transform: translateY(-2px);
8074
- box-shadow: var(--shadow-sm);
8452
+ /* ── Skeleton shimmer for loading lists ── */
8453
+ .skel-block {
8454
+ padding: 12px 18px;
8075
8455
  }
8076
- .gs-card.gs-done {
8077
- opacity: 0.5;
8078
- border-color: var(--green);
8456
+ .skel-row {
8457
+ height: 14px;
8458
+ border-radius: 4px;
8459
+ background: linear-gradient(90deg, var(--bg-tertiary) 0%, var(--bg-hover) 50%, var(--bg-tertiary) 100%);
8460
+ background-size: 200% 100%;
8461
+ animation: skelShimmer 1.4s ease-in-out infinite;
8462
+ margin-bottom: 10px;
8079
8463
  }
8080
- .gs-card.gs-done::after {
8081
- content: '\\2713';
8082
- position: absolute;
8083
- top: 8px;
8084
- right: 10px;
8085
- color: var(--green);
8086
- font-size: 16px;
8464
+ .skel-row.short { width: 40%; }
8465
+ .skel-row.med { width: 65%; }
8466
+ @keyframes skelShimmer {
8467
+ 0% { background-position: 200% 0; }
8468
+ 100% { background-position: -200% 0; }
8469
+ }
8470
+ /* ── Row actions (icon-style buttons that appear inline on a list row) ── */
8471
+ .row-actions {
8472
+ display: inline-flex;
8473
+ gap: 6px;
8474
+ align-items: center;
8475
+ }
8476
+ .row-actions button {
8477
+ background: none;
8478
+ border: 1px solid transparent;
8479
+ color: var(--text-muted);
8480
+ font-size: 11px;
8481
+ padding: 3px 8px;
8482
+ border-radius: 4px;
8483
+ cursor: pointer;
8484
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
8485
+ }
8486
+ .row-actions button:hover {
8487
+ background: var(--bg-hover);
8488
+ border-color: var(--border);
8489
+ color: var(--text-primary);
8490
+ }
8491
+ .row-actions button.primary { color: var(--clementine); }
8492
+ .row-actions button.danger:hover { color: var(--red); border-color: rgba(239,68,68,0.3); }
8493
+ .clickable-row {
8494
+ cursor: pointer;
8495
+ transition: background 0.12s;
8496
+ }
8497
+ .clickable-row:hover { background: var(--bg-hover); }
8498
+
8499
+ /* ── Status pip (used for daemon status, channel status, agent status) ── */
8500
+ .status-pip {
8501
+ display: inline-block;
8502
+ width: 8px;
8503
+ height: 8px;
8504
+ border-radius: 50%;
8505
+ background: #888;
8506
+ margin-right: 6px;
8507
+ flex-shrink: 0;
8508
+ }
8509
+ .status-pip.green { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.5); }
8510
+ .status-pip.amber { background: #f59e0b; }
8511
+ .status-pip.red { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.4); }
8512
+ .status-pip.muted { background: var(--text-muted); }
8513
+
8514
+ /* Cmd+K palette */
8515
+ .cmdk-row:hover { background: var(--bg-hover) !important; }
8516
+
8517
+ /* ── Home layout — chat-first daily driver ─── */
8518
+ .home-layout {
8519
+ display: grid;
8520
+ grid-template-columns: 1fr 320px;
8521
+ gap: 18px;
8522
+ height: calc(100vh - var(--header-h));
8523
+ padding: 18px;
8524
+ box-sizing: border-box;
8525
+ }
8526
+ .home-main {
8527
+ display: flex;
8528
+ flex-direction: column;
8529
+ gap: 14px;
8530
+ min-height: 0;
8531
+ overflow-y: auto;
8532
+ }
8533
+ .home-chat {
8534
+ display: flex;
8535
+ flex-direction: column;
8536
+ flex: 1;
8537
+ min-height: 320px;
8538
+ background: var(--bg-card);
8539
+ border: 1px solid var(--border);
8540
+ border-radius: 12px;
8541
+ overflow: hidden;
8542
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
8543
+ }
8544
+ .home-chat-messages {
8545
+ flex: 1;
8546
+ overflow-y: auto;
8547
+ padding: 18px 20px;
8548
+ min-height: 280px;
8549
+ }
8550
+ .home-chat-input-row {
8551
+ display: flex;
8552
+ gap: 8px;
8553
+ padding: 12px 16px;
8554
+ border-top: 1px solid var(--border);
8555
+ background: var(--bg-secondary);
8556
+ align-items: center;
8557
+ }
8558
+ .home-chat-input-row input[type="text"] {
8559
+ flex: 1;
8560
+ padding: 10px 14px;
8561
+ border: 1px solid var(--border);
8562
+ border-radius: 8px;
8563
+ background: var(--bg-input);
8564
+ color: var(--text-primary);
8565
+ font-size: 14px;
8566
+ }
8567
+ .home-chat-input-row select {
8568
+ padding: 6px 10px;
8569
+ border: 1px solid var(--border);
8570
+ border-radius: 6px;
8571
+ background: var(--bg-secondary);
8572
+ color: var(--text-primary);
8573
+ font-size: 12px;
8574
+ }
8575
+ .home-chat-input-row button { padding: 9px 18px; border-radius: 8px; }
8576
+ .home-activity { margin: 0; }
8577
+
8578
+ /* Right rail */
8579
+ .home-rail {
8580
+ display: flex;
8581
+ flex-direction: column;
8582
+ gap: 12px;
8583
+ overflow-y: auto;
8584
+ position: relative;
8585
+ }
8586
+ .home-rail.collapsed {
8587
+ display: none;
8588
+ }
8589
+ .rail-collapse-btn {
8590
+ position: absolute;
8591
+ top: -4px;
8592
+ right: -4px;
8593
+ background: none;
8594
+ border: 1px solid var(--border);
8595
+ color: var(--text-muted);
8596
+ width: 22px;
8597
+ height: 22px;
8598
+ border-radius: 50%;
8599
+ cursor: pointer;
8600
+ font-size: 14px;
8601
+ display: none;
8602
+ align-items: center;
8603
+ justify-content: center;
8604
+ z-index: 5;
8605
+ }
8606
+ .rail-card {
8607
+ background: var(--bg-card);
8608
+ border: 1px solid var(--border);
8609
+ border-radius: 10px;
8610
+ overflow: hidden;
8611
+ box-shadow: 0 1px 4px rgba(0,0,0,0.04);
8612
+ }
8613
+ .rail-header {
8614
+ display: flex;
8615
+ align-items: center;
8616
+ justify-content: space-between;
8617
+ padding: 10px 14px;
8618
+ font-size: 12px;
8619
+ font-weight: 600;
8620
+ color: var(--text-secondary);
8621
+ border-bottom: 1px solid var(--border);
8622
+ background: var(--bg-secondary);
8623
+ }
8624
+ .rail-body { padding: 12px 14px; font-size: 12.5px; line-height: 1.5; }
8625
+ .rail-body .empty-state, .rail-body .skel-row { font-size: 11px; }
8626
+ .rail-badge {
8627
+ display: inline-flex;
8628
+ align-items: center;
8629
+ justify-content: center;
8630
+ min-width: 18px;
8631
+ height: 18px;
8632
+ padding: 0 6px;
8633
+ border-radius: 9px;
8634
+ background: var(--clementine);
8635
+ color: #fff;
8636
+ font-size: 10px;
8637
+ font-weight: 600;
8638
+ }
8639
+ .rail-row {
8640
+ display: flex;
8641
+ align-items: center;
8642
+ gap: 8px;
8643
+ padding: 6px 0;
8644
+ font-size: 12px;
8645
+ border-bottom: 1px dashed var(--border);
8646
+ }
8647
+ .rail-row:last-child { border-bottom: none; }
8648
+ .rail-row .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8649
+ .rail-row .meta { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
8650
+
8651
+ /* Floating "open rail" button when collapsed */
8652
+ .home-rail-toggle {
8653
+ position: fixed;
8654
+ top: 80px;
8655
+ right: 18px;
8656
+ z-index: 100;
8657
+ width: 36px;
8658
+ height: 36px;
8659
+ border-radius: 50%;
8660
+ background: var(--clementine);
8661
+ color: #fff;
8662
+ border: none;
8663
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
8664
+ cursor: pointer;
8665
+ font-size: 14px;
8666
+ display: none;
8667
+ }
8668
+ .home-rail.collapsed ~ .home-rail-toggle,
8669
+ .home-rail.collapsed + .home-rail-toggle { display: block; }
8670
+
8671
+ /* Narrow screens: rail becomes a slide-out drawer */
8672
+ @media (max-width: 1024px) {
8673
+ .home-layout { grid-template-columns: 1fr; }
8674
+ .home-rail {
8675
+ position: fixed;
8676
+ right: 0;
8677
+ top: var(--header-h);
8678
+ bottom: 0;
8679
+ width: 320px;
8680
+ max-width: 90vw;
8681
+ transform: translateX(100%);
8682
+ transition: transform 0.2s ease;
8683
+ background: var(--bg);
8684
+ border-left: 1px solid var(--border);
8685
+ box-shadow: -4px 0 20px rgba(0,0,0,0.15);
8686
+ padding: 14px;
8687
+ z-index: 50;
8688
+ }
8689
+ .home-rail.open { transform: translateX(0); }
8690
+ .rail-collapse-btn { display: flex; }
8691
+ .home-rail-toggle { display: block; }
8692
+ .home-rail.open ~ .home-rail-toggle { display: none; }
8693
+ }
8694
+
8695
+ /* ── Cards ──────────────────────────────── */
8696
+ .card {
8697
+ background: var(--bg-card);
8698
+ backdrop-filter: blur(8px);
8699
+ border: 1px solid var(--border);
8700
+ border-radius: var(--radius);
8701
+ margin-bottom: 16px;
8702
+ overflow: hidden;
8703
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
8704
+ }
8705
+ .card-header {
8706
+ padding: 14px 18px;
8707
+ border-bottom: 1px solid var(--border);
8708
+ display: flex;
8709
+ align-items: center;
8710
+ justify-content: space-between;
8711
+ font-size: 13px;
8712
+ font-weight: 600;
8713
+ color: var(--text-primary);
8714
+ }
8715
+ .card-body {
8716
+ padding: 18px;
8717
+ font-size: 13px;
8718
+ line-height: 1.7;
8719
+ }
8720
+ .grid-2 {
8721
+ display: grid;
8722
+ grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
8723
+ gap: 20px;
8724
+ }
8725
+
8726
+ /* ── Getting Started ────────────────────── */
8727
+ .getting-started {
8728
+ background: linear-gradient(135deg, var(--clementine-bg), rgba(77,158,255,0.06));
8729
+ border: 1px solid var(--border);
8730
+ border-radius: 16px;
8731
+ padding: 24px;
8732
+ margin-bottom: 24px;
8733
+ position: relative;
8734
+ }
8735
+ .gs-header {
8736
+ display: flex;
8737
+ align-items: center;
8738
+ gap: 12px;
8739
+ margin-bottom: 20px;
8740
+ }
8741
+ .gs-title {
8742
+ font-size: 18px;
8743
+ font-weight: 700;
8744
+ color: var(--text-primary);
8745
+ }
8746
+ .gs-subtitle {
8747
+ font-size: 13px;
8748
+ color: var(--text-muted);
8749
+ flex: 1;
8750
+ }
8751
+ .gs-dismiss {
8752
+ position: absolute;
8753
+ top: 12px;
8754
+ right: 12px;
8755
+ font-size: 18px;
8756
+ }
8757
+ .gs-grid {
8758
+ display: grid;
8759
+ grid-template-columns: repeat(5, 1fr);
8760
+ gap: 16px;
8761
+ }
8762
+ .gs-card {
8763
+ background: var(--bg-card);
8764
+ border: 1px solid var(--border);
8765
+ border-radius: var(--radius);
8766
+ padding: 20px 16px;
8767
+ text-align: center;
8768
+ position: relative;
8769
+ display: flex;
8770
+ flex-direction: column;
8771
+ align-items: center;
8772
+ gap: 8px;
8773
+ transition: transform 0.15s, box-shadow 0.15s;
8774
+ }
8775
+ .gs-card:hover {
8776
+ transform: translateY(-2px);
8777
+ box-shadow: var(--shadow-sm);
8778
+ }
8779
+ .gs-card.gs-done {
8780
+ opacity: 0.5;
8781
+ border-color: var(--green);
8782
+ }
8783
+ .gs-card.gs-done::after {
8784
+ content: '\\2713';
8785
+ position: absolute;
8786
+ top: 8px;
8787
+ right: 10px;
8788
+ color: var(--green);
8789
+ font-size: 16px;
8087
8790
  font-weight: 700;
8088
8791
  }
8089
8792
  .gs-step-num {
@@ -9908,11 +10611,92 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
9908
10611
  flex-shrink: 0;
9909
10612
  margin-top: 2px;
9910
10613
  }
9911
- #page-chat.active, #page-builder.active {
10614
+ /* Build page is full-height flex (canvas + chat). Home page handles its own layout. */
10615
+ #page-build.active {
9912
10616
  display: flex !important;
9913
10617
  flex-direction: column;
9914
10618
  height: calc(100vh - var(--header-h));
9915
10619
  }
10620
+ /* === Builder canvas (Drawflow node kinds) === */
10621
+ #builder-canvas .drawflow-node {
10622
+ background: #2a3040;
10623
+ border: 1px solid #3a4255;
10624
+ border-radius: 8px;
10625
+ color: #fff;
10626
+ padding: 10px 12px;
10627
+ min-width: 180px;
10628
+ max-width: 240px;
10629
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
10630
+ }
10631
+ #builder-canvas .drawflow-node.selected { border-color: var(--clementine); }
10632
+ #builder-canvas .drawflow-node.cl-node-prompt { background: #2a3550; border-color: #3a4570; }
10633
+ #builder-canvas .drawflow-node.cl-node-mcp { background: #2c4039; border-color: #3a5a4f; }
10634
+ #builder-canvas .drawflow-node.cl-node-channel { background: #44324a; border-color: #5b3f6a; }
10635
+ #builder-canvas .drawflow-node.cl-node-transform { background: #3d3a28; border-color: #564f33; }
10636
+ #builder-canvas .drawflow-node.cl-node-conditional { background: #4a352a; border-color: #6b4737; }
10637
+ #builder-canvas .drawflow-node.cl-node-loop { background: #2e4350; border-color: #3f5a6c; }
10638
+ #builder-canvas .drawflow-node .input, #builder-canvas .drawflow-node .output {
10639
+ background: #555c70;
10640
+ border: 2px solid #2a3040;
10641
+ }
10642
+ #builder-canvas .drawflow .connection .main-path {
10643
+ stroke: #5b6580;
10644
+ stroke-width: 2px;
10645
+ }
10646
+ #builder-canvas .drawflow .connection .main-path:hover { stroke: var(--clementine); }
10647
+ /* Per-step run status (Phase 2e) */
10648
+ #builder-canvas .drawflow-node.cl-step-running {
10649
+ box-shadow: 0 0 0 2px var(--clementine), 0 2px 8px rgba(0,0,0,0.3);
10650
+ animation: cl-step-pulse 1.6s ease-in-out infinite;
10651
+ }
10652
+ #builder-canvas .drawflow-node.cl-step-done {
10653
+ box-shadow: 0 0 0 2px #4caf50, 0 2px 8px rgba(0,0,0,0.3);
10654
+ }
10655
+ #builder-canvas .drawflow-node.cl-step-failed,
10656
+ #builder-canvas .drawflow-node.cl-step-timeout {
10657
+ box-shadow: 0 0 0 2px #e64a4a, 0 2px 8px rgba(0,0,0,0.3);
10658
+ }
10659
+ #builder-canvas .drawflow-node.cl-step-skipped,
10660
+ #builder-canvas .drawflow-node.cl-step-cancelled {
10661
+ opacity: 0.55;
10662
+ box-shadow: 0 0 0 1px #888, 0 2px 8px rgba(0,0,0,0.2);
10663
+ }
10664
+ @keyframes cl-step-pulse {
10665
+ 0%, 100% { box-shadow: 0 0 0 2px var(--clementine), 0 2px 8px rgba(0,0,0,0.3); }
10666
+ 50% { box-shadow: 0 0 0 4px rgba(255,140,33,0.5), 0 2px 8px rgba(0,0,0,0.3); }
10667
+ }
10668
+ /* Node palette popover */
10669
+ .builder-palette-item {
10670
+ padding: 7px 12px;
10671
+ border-radius: 5px;
10672
+ font-size: 12px;
10673
+ color: var(--text-primary);
10674
+ cursor: pointer;
10675
+ }
10676
+ .builder-palette-item:hover { background: var(--bg-hover); }
10677
+ /* Config panel rows */
10678
+ #builder-config-panel .cfg-row { margin-bottom: 10px; }
10679
+ #builder-config-panel .cfg-row label {
10680
+ display: block;
10681
+ font-size: 10px;
10682
+ text-transform: uppercase;
10683
+ letter-spacing: 0.04em;
10684
+ color: var(--text-muted);
10685
+ margin-bottom: 4px;
10686
+ }
10687
+ #builder-config-panel .cfg-row input,
10688
+ #builder-config-panel .cfg-row select,
10689
+ #builder-config-panel .cfg-row textarea {
10690
+ width: 100%;
10691
+ padding: 6px 8px;
10692
+ border: 1px solid var(--border);
10693
+ border-radius: 5px;
10694
+ background: var(--bg-input);
10695
+ color: var(--text-primary);
10696
+ font-size: 12px;
10697
+ font-family: inherit;
10698
+ }
10699
+ #builder-config-panel .cfg-row textarea { font-family: monospace; resize: vertical; }
9916
10700
  #builder-preview .preview-field {
9917
10701
  margin-bottom:12px;
9918
10702
  }
@@ -10077,10 +10861,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10077
10861
  .stat-value { font-size: 20px; }
10078
10862
  .stat-label { font-size: 10px; }
10079
10863
 
10080
- /* Chat: full height + mobile-friendly input */
10081
- #page-chat.active {
10082
- height: calc(100vh - var(--header-h));
10083
- }
10864
+ /* Mobile-friendly chat input (lives inside Home now). */
10084
10865
  #chat-input {
10085
10866
  font-size: 16px; /* prevents iOS zoom on focus */
10086
10867
  min-height: 40px;
@@ -10352,70 +11133,39 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10352
11133
  </div>
10353
11134
  </header>
10354
11135
 
10355
- <!-- Sidebar -->
11136
+ <!-- Sidebar — 5 destinations only. Sub-pages live as tabs within each. -->
10356
11137
  <nav class="sidebar">
10357
11138
  <div class="nav-section">
10358
- <div class="nav-section-title">Command Center</div>
10359
- <div class="nav-item active" data-page="home">
10360
- <span class="nav-icon">&#9679;</span> Home
11139
+ <div class="nav-item active" data-page="home" title="Chat, today, activity">
11140
+ <span class="nav-icon">&#127819;</span> Home
10361
11141
  </div>
10362
- <div class="nav-item" data-page="chat">
10363
- <span class="nav-icon">&#128172;</span> Chat
11142
+ <div class="nav-item" data-page="build" title="Workflows, crons, skills">
11143
+ <span class="nav-icon">&#128736;</span> Build
11144
+ <span class="nav-badge" id="nav-cron-count">0</span>
10364
11145
  </div>
10365
- </div>
10366
- <div class="nav-section">
10367
- <div class="nav-section-title">Build</div>
10368
- <div class="nav-item" data-page="builder">
10369
- <span class="nav-icon">&#128736;</span> Builder
11146
+ <div class="nav-item" data-page="team" title="Agents, activity, goals">
11147
+ <span class="nav-icon">&#128101;</span> Team
10370
11148
  </div>
10371
- <div class="nav-item" data-page="team">
10372
- <span class="nav-icon">&#128101;</span> The Office
11149
+ <div class="nav-item" data-page="brain" title="Memory, knowledge, ingestion, health">
11150
+ <span class="nav-icon">&#129504;</span> Brain
10373
11151
  </div>
10374
- <div class="nav-item" data-page="projects">
10375
- <span class="nav-icon">&#128194;</span> Projects
11152
+ <div class="nav-item" data-page="settings" title="Channels, integrations, system">
11153
+ <span class="nav-icon">&#9881;</span> Settings
10376
11154
  </div>
10377
11155
  </div>
10378
- <div class="nav-section">
10379
- <div class="nav-section-title">Team</div>
11156
+ <!-- Per-agent quick-jump (loaded from team-nav helper). -->
11157
+ <div class="nav-section" style="margin-top:18px">
11158
+ <div class="nav-section-title">Agents</div>
10380
11159
  <div id="team-nav"></div>
10381
11160
  <div class="team-hire-btn" onclick="showAgentCreateModal()">
10382
- <span style="font-size:14px">+</span> Hire Agent
11161
+ <span style="font-size:14px">+</span> Hire
10383
11162
  </div>
10384
11163
  </div>
11164
+ <div style="flex:1"></div>
10385
11165
  <div class="nav-section">
10386
- <div class="nav-section-title">Automate</div>
10387
- <div class="nav-item" data-page="automations">
10388
- <span class="nav-icon">&#9200;</span> Scheduled Tasks
10389
- <span class="nav-badge" id="nav-cron-count">0</span>
10390
- </div>
10391
- <div class="nav-item" onclick="openSkillStudio()">
10392
- <span class="nav-icon">&#128161;</span> Skill Studio
10393
- <span class="nav-badge" id="nav-skill-count" style="display:none">0</span>
10394
- </div>
10395
- <div class="nav-item" data-page="workflows">
10396
- <span class="nav-icon">&#128260;</span> Workflows
10397
- </div>
10398
- </div>
10399
- <div class="nav-section">
10400
- <div class="nav-section-title">Insights</div>
10401
- <div class="nav-item" data-page="team-status">
10402
- <span class="nav-icon">&#128202;</span> Team Status
10403
- </div>
10404
- <div class="nav-item" data-page="intelligence">
10405
- <span class="nav-icon">&#129504;</span> Brain
10406
- </div>
10407
- <div class="nav-item" data-page="claims">
10408
- <span class="nav-icon">&#128274;</span> Trust &amp; Claims
10409
- <span class="nav-badge" id="nav-trust-score" style="display:none">--</span>
10410
- </div>
10411
- <div class="nav-item" data-page="logs">
10412
- <span class="nav-icon">&#128220;</span> Logs
10413
- </div>
10414
- </div>
10415
- <div class="nav-section">
10416
- <div class="nav-section-title">System</div>
10417
- <div class="nav-item" data-page="settings">
10418
- <span class="nav-icon">&#9881;</span> Settings
11166
+ <div class="nav-item" onclick="openCommandK()" style="font-size:12px;color:var(--text-muted);justify-content:space-between" title="Quick search (Cmd+K)">
11167
+ <span><span class="nav-icon">&#128269;</span> Search</span>
11168
+ <kbd style="font-size:10px;padding:1px 5px;border:1px solid var(--border);border-radius:3px">&#8984;K</kbd>
10419
11169
  </div>
10420
11170
  </div>
10421
11171
  </nav>
@@ -10424,105 +11174,82 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10424
11174
  <!-- Content -->
10425
11175
  <div class="content">
10426
11176
 
10427
- <!-- ═══ Home Page ═══ -->
11177
+ <!-- ═══ Home chat-first daily driver ═══ -->
10428
11178
  <div class="page active" id="page-home">
10429
- <div class="agent-hero" id="agent-hero">
10430
- <div class="agent-hero-top">
10431
- <div class="agent-avatar" id="agent-avatar">${name.charAt(0).toUpperCase()}</div>
10432
- <div class="agent-info">
10433
- <div class="hero-wordmark" id="hero-wordmark"></div>
10434
- <div class="agent-activity" id="agent-activity">
10435
- <span class="agent-activity-dot"></span>
10436
- <span>Loading...</span>
11179
+ <div class="home-layout">
11180
+ <main class="home-main">
11181
+ <!-- Getting Started (shown for new users only) -->
11182
+ <div id="getting-started" class="getting-started" style="display:none">
11183
+ <div class="gs-header">
11184
+ <div class="gs-title">Get Started</div>
11185
+ <div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
11186
+ <button class="btn-ghost btn-sm gs-dismiss" onclick="dismissGettingStarted()" title="Dismiss">&times;</button>
11187
+ </div>
11188
+ <div class="gs-grid">
11189
+ <div class="gs-card" id="gs-step-auth">
11190
+ <div class="gs-step-num">1</div>
11191
+ <div class="gs-card-icon">&#128274;</div>
11192
+ <div class="gs-card-title">Login with Anthropic</div>
11193
+ <div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
11194
+ <button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
11195
+ </div>
11196
+ <div class="gs-card" id="gs-step-agent">
11197
+ <div class="gs-step-num">2</div>
11198
+ <div class="gs-card-icon">&#128101;</div>
11199
+ <div class="gs-card-title">Create an Agent</div>
11200
+ <div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
11201
+ <button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
11202
+ </div>
11203
+ <div class="gs-card" id="gs-step-channel">
11204
+ <div class="gs-step-num">3</div>
11205
+ <div class="gs-card-icon">&#128172;</div>
11206
+ <div class="gs-card-title">Connect a Channel</div>
11207
+ <div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
11208
+ <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'channels' })">Open Settings</button>
11209
+ </div>
11210
+ <div class="gs-card" id="gs-step-task">
11211
+ <div class="gs-step-num">4</div>
11212
+ <div class="gs-card-icon">&#9200;</div>
11213
+ <div class="gs-card-title">Schedule a Task</div>
11214
+ <div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
11215
+ <button class="btn btn-sm" onclick="navigateTo('build', { tab: 'crons' })">Add a Task</button>
11216
+ </div>
11217
+ <div class="gs-card" id="gs-step-project">
11218
+ <div class="gs-step-num">5</div>
11219
+ <div class="gs-card-icon">&#128194;</div>
11220
+ <div class="gs-card-title">Link a Project</div>
11221
+ <div class="gs-card-desc">Give agents context about your codebases and tools.</div>
11222
+ <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'projects' })">Browse Projects</button>
11223
+ </div>
10437
11224
  </div>
10438
- <div class="agent-meta" id="agent-meta"></div>
10439
- <div class="agent-channels" id="agent-channels"></div>
10440
- </div>
10441
- <div class="agent-controls" id="hero-controls"></div>
10442
- </div>
10443
- </div>
10444
-
10445
- <!-- Getting Started (shown for new users, hidden when setup is complete) -->
10446
- <div id="getting-started" class="getting-started" style="display:none">
10447
- <div class="gs-header">
10448
- <div class="gs-title">Get Started</div>
10449
- <div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
10450
- <button class="btn-ghost btn-sm gs-dismiss" onclick="dismissGettingStarted()" title="Dismiss">&times;</button>
10451
- </div>
10452
- <div class="gs-grid">
10453
- <div class="gs-card" id="gs-step-auth">
10454
- <div class="gs-step-num">1</div>
10455
- <div class="gs-card-icon">&#128274;</div>
10456
- <div class="gs-card-title">Login with Anthropic</div>
10457
- <div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
10458
- <button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
10459
- </div>
10460
- <div class="gs-card" id="gs-step-agent">
10461
- <div class="gs-step-num">2</div>
10462
- <div class="gs-card-icon">&#128101;</div>
10463
- <div class="gs-card-title">Create an Agent</div>
10464
- <div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
10465
- <button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
10466
- </div>
10467
- <div class="gs-card" id="gs-step-channel">
10468
- <div class="gs-step-num">3</div>
10469
- <div class="gs-card-icon">&#128172;</div>
10470
- <div class="gs-card-title">Connect a Channel</div>
10471
- <div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
10472
- <button class="btn btn-sm" onclick="navigateTo('settings')">Open Settings</button>
10473
- </div>
10474
- <div class="gs-card" id="gs-step-task">
10475
- <div class="gs-step-num">4</div>
10476
- <div class="gs-card-icon">&#9200;</div>
10477
- <div class="gs-card-title">Schedule a Task</div>
10478
- <div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
10479
- <button class="btn btn-sm" onclick="navigateTo('automations')">Add a Task</button>
10480
- </div>
10481
- <div class="gs-card" id="gs-step-project">
10482
- <div class="gs-step-num">5</div>
10483
- <div class="gs-card-icon">&#128194;</div>
10484
- <div class="gs-card-title">Link a Project</div>
10485
- <div class="gs-card-desc">Give agents context about your codebases and tools.</div>
10486
- <button class="btn btn-sm" onclick="navigateTo('projects')">Browse Projects</button>
10487
11225
  </div>
10488
- </div>
10489
- </div>
10490
-
10491
- <div class="summary-grid" id="summary-cards"></div>
10492
11226
 
10493
- <!-- Today's Plan + Team Pulse -->
10494
- <div style="display:grid;grid-template-columns:3fr 2fr;gap:16px;margin-bottom:16px">
10495
- <div class="card">
10496
- <div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
10497
- <span>Today's Plan</span>
10498
- <div style="display:flex;gap:8px;align-items:center">
10499
- <input type="date" id="plan-date-picker" style="padding:4px 8px;font-size:11px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary)">
10500
- <button class="btn btn-sm" onclick="loadPlanForDate(document.getElementById('plan-date-picker').value)" style="font-size:11px">Load</button>
11227
+ <!-- Chat panel primary surface -->
11228
+ <div class="home-chat">
11229
+ <div id="chat-messages" class="home-chat-messages">
11230
+ <div class="empty-state" style="margin-top:32px">
11231
+ <p style="margin-bottom:14px;color:var(--text-muted)">What can I help with?</p>
11232
+ <div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
11233
+ <button class="btn btn-sm quick-pill" onclick="quickChat(&quot;What&apos;s on my schedule?&quot;)">What's on my schedule?</button>
11234
+ <button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Check my email</button>
11235
+ <button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Run morning briefing</button>
11236
+ <button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">What did you do today?</button>
11237
+ </div>
11238
+ </div>
11239
+ </div>
11240
+ <div class="home-chat-input-row">
11241
+ <input type="text" id="chat-input" placeholder="Ask Clementine anything..." onkeydown="if(event.key==='Enter'&amp;&amp;!event.shiftKey){event.preventDefault();sendChat()}">
11242
+ <select id="chat-profile-select" onchange="switchProfile(this.value)" title="Active profile">
11243
+ <option value="">Default</option>
11244
+ </select>
11245
+ <button class="btn-primary" id="chat-send-btn" onclick="sendChat()">Send</button>
10501
11246
  </div>
10502
11247
  </div>
10503
- <div class="card-body" id="home-plan-content" style="max-height:320px;overflow-y:auto"><div class="empty-state">Loading plan...</div></div>
10504
- </div>
10505
- <div class="card">
10506
- <div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
10507
- <span>Team Pulse</span>
10508
- <span style="font-size:11px;color:var(--text-muted)" id="team-pulse-count"></span>
10509
- </div>
10510
- <div class="card-body" id="home-team-pulse" style="max-height:320px;overflow-y:auto"><div class="empty-state">Loading team...</div></div>
10511
- </div>
10512
- </div>
10513
11248
 
10514
- <!-- Home Tabs: Activity | Metrics | Agents | Sessions -->
10515
- <div class="tab-bar" id="home-tabs">
10516
- <button class="active" onclick="switchTab('home','activity')">Activity</button>
10517
- <button onclick="switchTab('home','metrics')">Metrics</button>
10518
- <button onclick="switchTab('home','agents')">Agents</button>
10519
- <button onclick="switchTab('home','sessions')">Sessions</button>
10520
- </div>
10521
- <div id="home-tab-content">
10522
- <div class="tab-pane active" id="tab-home-activity">
10523
- <div class="card">
11249
+ <!-- Activity feed -->
11250
+ <div class="card home-activity">
10524
11251
  <div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
10525
- <span>Live Activity</span>
11252
+ <span>Live activity</span>
10526
11253
  <div style="display:flex;gap:6px;align-items:center">
10527
11254
  <select id="activity-source-filter" onchange="refreshActivity()" style="font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary)">
10528
11255
  <option value="">All Sources</option>
@@ -10537,44 +11264,81 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10537
11264
  </select>
10538
11265
  </div>
10539
11266
  </div>
10540
- <div class="card-body" id="panel-activity"><div class="empty-state">Loading...</div></div>
11267
+ <div class="card-body" id="panel-activity"><div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div></div>
10541
11268
  </div>
10542
- </div>
10543
- <div class="tab-pane" id="tab-home-metrics">
10544
- <div id="metrics-content-home"><div class="empty-state">Loading metrics...</div></div>
10545
- </div>
10546
- <div class="tab-pane" id="tab-home-agents">
10547
- <div id="panel-agents-compare"><div class="empty-state">Loading agent stats...</div></div>
10548
- </div>
10549
- <div class="tab-pane" id="tab-home-sessions">
10550
- <div id="panel-sessions-home"><div class="empty-state">Loading sessions...</div></div>
10551
- </div>
10552
- </div>
11269
+ </main>
11270
+
11271
+ <!-- Right rail: Today / Upcoming / Active / Time-saved / Approvals -->
11272
+ <aside class="home-rail" id="home-rail">
11273
+ <button class="rail-collapse-btn" onclick="toggleHomeRail()" title="Hide rail">&times;</button>
10553
11274
 
10554
- <div class="card" id="claude-integrations-widget" style="display:none;margin-top:16px"></div>
10555
- <div class="card" id="mcp-status-widget" style="display:none;margin-top:16px"></div>
10556
- <!-- Hidden: Quick controls data target (kept for refreshStatus compat) -->
10557
- <div id="panel-controls" style="display:none"></div>
11275
+ <section class="rail-card">
11276
+ <div class="rail-header">
11277
+ <span><span class="status-pip green"></span>Daemon</span>
11278
+ <span id="rail-daemon-uptime" style="font-size:11px;color:var(--text-muted)">--</span>
11279
+ </div>
11280
+ <div class="rail-body" id="rail-daemon-body">
11281
+ <div class="agent-activity" id="agent-activity"><span class="agent-activity-dot"></span><span>Loading...</span></div>
11282
+ </div>
11283
+ </section>
11284
+
11285
+ <section class="rail-card">
11286
+ <div class="rail-header">
11287
+ <span>Today</span>
11288
+ <input type="date" id="plan-date-picker" style="padding:2px 6px;font-size:10px;background:var(--bg-input);border:1px solid var(--border);border-radius:3px;color:var(--text-primary)">
11289
+ </div>
11290
+ <div class="rail-body" id="home-plan-content"><div class="skel-row med"></div><div class="skel-row"></div></div>
11291
+ </section>
11292
+
11293
+ <section class="rail-card">
11294
+ <div class="rail-header"><span>Upcoming runs</span><span id="rail-upcoming-count" class="rail-badge">0</span></div>
11295
+ <div class="rail-body" id="rail-upcoming"><div class="skel-row short"></div></div>
11296
+ </section>
11297
+
11298
+ <section class="rail-card">
11299
+ <div class="rail-header"><span>Active runs</span><span id="rail-active-count" class="rail-badge" style="display:none">0</span></div>
11300
+ <div class="rail-body" id="rail-active"><div style="font-size:12px;color:var(--text-muted)">Nothing running.</div></div>
11301
+ </section>
11302
+
11303
+ <section class="rail-card">
11304
+ <div class="rail-header"><span>Time saved this week</span></div>
11305
+ <div class="rail-body" id="rail-time-saved"><div class="skel-row short"></div></div>
11306
+ </section>
11307
+
11308
+ <section class="rail-card">
11309
+ <div class="rail-header"><span>Approvals</span><span id="rail-approvals-count" class="rail-badge" style="display:none">0</span></div>
11310
+ <div class="rail-body" id="rail-approvals"><div style="font-size:12px;color:var(--text-muted)">Nothing pending.</div></div>
11311
+ </section>
11312
+ </aside>
11313
+ <button class="home-rail-toggle" onclick="toggleHomeRail()" title="Open status rail">&#9776;</button>
11314
+ </div>
10558
11315
  </div>
10559
11316
 
10560
11317
  <!-- ═══ Builder Page — Conversational Artifact Creation ═══ -->
10561
- <div class="page" id="page-builder">
10562
- <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border)">
10563
- <span class="page-title" id="builder-page-title" style="margin:0;font-size:16px">Builder</span>
10564
- <select id="builder-type" onchange="resetBuilder();updateBuilderMode()" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:13px">
10565
- <option value="skill">Skill</option>
10566
- <option value="cron">Cron Job</option>
10567
- <option value="agent">Agent</option>
10568
- <option value="workflow">Workflow</option>
11318
+ <div class="page" id="page-build">
11319
+ <div class="tab-bar" id="build-tabs" style="margin:0;padding:0 18px;background:var(--bg-secondary);border-bottom:1px solid var(--border)">
11320
+ <button class="active" data-build-tab="workflows" onclick="switchBuildTab('workflows')">&#128279; Workflows</button>
11321
+ <button data-build-tab="crons" onclick="switchBuildTab('crons')">&#9200; Crons <span class="tab-badge" id="build-tab-cron-count" style="display:none">0</span></button>
11322
+ <button data-build-tab="skills" onclick="switchBuildTab('skills')">&#128737; Skills <span class="tab-badge" id="build-tab-skill-count" style="display:none">0</span></button>
11323
+ <button data-build-tab="templates" onclick="switchBuildTab('templates')">&#128221; Templates</button>
11324
+ </div>
11325
+ <!-- Builder header strip — persists across tabs (except Templates) -->
11326
+ <div id="build-header-strip" style="display:flex;align-items:center;gap:12px;padding:10px 18px;border-bottom:1px solid var(--border)">
11327
+ <select id="builder-type" onchange="resetBuilder();updateBuilderMode()" style="display:none">
11328
+ <option value="skill">skill</option>
11329
+ <option value="cron">cron</option>
11330
+ <option value="agent">agent</option>
11331
+ <option value="workflow">workflow</option>
10569
11332
  </select>
10570
- <span id="builder-agent-label" style="padding:6px 12px;font-size:13px;color:var(--text-secondary);font-weight:500"></span>
11333
+ <span id="builder-agent-label" style="padding:0;font-size:13px;color:var(--text-secondary);font-weight:500"></span>
10571
11334
  <input type="hidden" id="builder-agent" value="">
10572
11335
  <span style="flex:1"></span>
10573
11336
  <button class="btn-sm" onclick="resetBuilder()" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px">New</button>
10574
11337
  <button class="btn-sm" id="builder-test-btn" onclick="testBuilderSkill()" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px;display:none">Test</button>
10575
11338
  <button class="btn-sm btn-primary" id="builder-save-btn" onclick="saveBuilderArtifact()" style="padding:4px 16px;font-size:12px;display:none">Save</button>
10576
11339
  </div>
10577
- <div style="display:flex;flex:1;min-height:0;overflow:hidden">
11340
+ <!-- Build tab content area -->
11341
+ <div id="build-tab-workflows" data-build-tabpane="workflows" style="display:flex;flex:1;min-height:0;overflow:hidden">
10578
11342
  <!-- Left: Chat -->
10579
11343
  <div style="flex:1;display:flex;flex-direction:column;border-right:1px solid var(--border)">
10580
11344
  <div id="builder-messages" style="flex:1;overflow-y:auto;padding:16px">
@@ -10607,15 +11371,44 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10607
11371
  <button class="btn-primary" onclick="sendBuilderChat()" style="padding:10px 18px;border-radius:8px">Send</button>
10608
11372
  </div>
10609
11373
  </div>
10610
- <!-- Right: Live Preview + Existing Skills -->
10611
- <div style="width:400px;display:flex;flex-direction:column;background:var(--bg-secondary)">
10612
- <div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:13px;color:var(--text-secondary)">
10613
- Live Preview
10614
- <span id="builder-preview-status" style="font-size:11px;color:var(--text-muted);margin-left:8px"></span>
11374
+ <!-- Right: Live Preview / Canvas + Existing Skills -->
11375
+ <div id="builder-right-pane" style="width:520px;display:flex;flex-direction:column;background:var(--bg-secondary)">
11376
+ <div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:13px;color:var(--text-secondary);display:flex;align-items:center;gap:10px;flex-wrap:wrap">
11377
+ <span id="builder-right-pane-title">Live Preview</span>
11378
+ <span id="builder-preview-status" style="font-size:11px;color:var(--text-muted)"></span>
11379
+ <span style="flex:1"></span>
11380
+ <select id="builder-canvas-picker" onchange="openBuilderWorkflow(this.value)" style="display:none;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:12px;max-width:240px">
11381
+ <option value="">— pick a workflow —</option>
11382
+ </select>
11383
+ <button id="builder-canvas-validate-btn" onclick="validateBuilderCanvas()" title="Static checks (cycles, missing fields, deps)" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">Validate</button>
11384
+ <button id="builder-canvas-dryrun-btn" onclick="dryRunBuilderCanvas()" title="Describe what each step would do (no execution)" style="display:none;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">Dry-run</button>
11385
+ <button id="builder-canvas-test-btn" onclick="testBuilderCanvas()" title="Test run (mock-safe by default)" style="display:none;background:var(--clementine);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Test</button>
11386
+ <button id="builder-canvas-cancel-btn" onclick="cancelBuilderTest()" title="Cancel test run" style="display:none;background:var(--red);border:none;color:#fff;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px">Cancel</button>
10615
11387
  </div>
10616
11388
  <div id="builder-preview" style="flex:1;overflow-y:auto;padding:16px">
10617
11389
  <div class="empty-state" style="font-size:13px;color:var(--text-muted)">The artifact will appear here as you build it</div>
10618
11390
  </div>
11391
+ <div id="builder-canvas-host" style="display:none;flex:1;flex-direction:column;min-height:0;position:relative">
11392
+ <div id="builder-canvas-banner" style="padding:8px 14px;background:var(--bg-tertiary);border-bottom:1px solid var(--border);font-size:11px;color:var(--text-muted);display:none"></div>
11393
+ <div id="builder-canvas" style="flex:1;background:var(--bg-tertiary);position:relative;overflow:hidden"></div>
11394
+ <!-- Floating add-node FAB + palette popover -->
11395
+ <button id="builder-palette-btn" onclick="toggleBuilderPalette()" title="Add a step" style="position:absolute;left:14px;bottom:48px;width:40px;height:40px;border-radius:50%;background:var(--clementine);color:#fff;border:none;font-size:20px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.25);z-index:10">+</button>
11396
+ <div id="builder-palette-pop" style="display:none;position:absolute;left:60px;bottom:48px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);z-index:11;min-width:160px">
11397
+ <div onclick="_builderAddNodeOfKind('prompt')" class="builder-palette-item" data-kind="prompt">prompt</div>
11398
+ <div onclick="_builderAddNodeOfKind('mcp')" class="builder-palette-item" data-kind="mcp">mcp tool</div>
11399
+ <div onclick="_builderAddNodeOfKind('channel')" class="builder-palette-item" data-kind="channel">channel</div>
11400
+ <div onclick="_builderAddNodeOfKind('transform')" class="builder-palette-item" data-kind="transform">transform</div>
11401
+ <div onclick="_builderAddNodeOfKind('conditional')" class="builder-palette-item" data-kind="conditional">conditional</div>
11402
+ <div onclick="_builderAddNodeOfKind('loop')" class="builder-palette-item" data-kind="loop">loop</div>
11403
+ </div>
11404
+ <!-- Slide-out config panel -->
11405
+ <div id="builder-config-panel" style="display:none;position:absolute;right:0;top:0;bottom:0;width:340px;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-4px 0 16px rgba(0,0,0,0.15);z-index:12;display:flex;flex-direction:column"></div>
11406
+ <div id="builder-canvas-footer" style="padding:6px 14px;border-top:1px solid var(--border);font-size:11px;color:var(--text-muted);display:flex;gap:14px;align-items:center">
11407
+ <span id="builder-canvas-status"></span>
11408
+ <span style="flex:1"></span>
11409
+ <span id="builder-canvas-id" style="font-family:monospace;opacity:0.6"></span>
11410
+ </div>
11411
+ </div>
10619
11412
  <!-- Existing skills drawer (visible in skill mode) -->
10620
11413
  <div id="builder-skills-drawer" style="display:none;border-top:2px solid var(--border);max-height:260px;overflow-y:auto">
10621
11414
  <div style="padding:10px 16px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--bg-secondary);z-index:1">
@@ -10626,192 +11419,132 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10626
11419
  </div>
10627
11420
  </div>
10628
11421
  </div>
10629
- </div>
10630
-
10631
- <!-- ═══ Agent Detail Page (full-screen management console) ═══ -->
10632
- <div class="page" id="page-agent-detail">
10633
- <div id="agent-detail-content"><div class="empty-state">Select an agent from the sidebar</div></div>
10634
- </div>
10635
11422
 
10636
- <!-- ═══ Scheduled Tasks Page (Cron + Timers + Self-Improve + Skills + Analytics) ═══ -->
10637
- <div class="page" id="page-automations">
10638
- <div class="page-title">Scheduled Tasks</div>
10639
- <div class="tab-bar" id="automations-tabs">
10640
- <button class="active" onclick="switchTab('automations','scheduled')">Scheduled Tasks</button>
10641
- <button onclick="switchTab('automations','broken')">Broken Jobs <span class="tab-badge" id="tab-broken-count" title="repeatedly failing" style="display:none;background:#ef4444;color:#fff">0</span></button>
10642
- <button onclick="switchTab('automations','timers')">Timers <span class="tab-badge" id="tab-timer-count" style="display:none">0</span></button>
10643
- <button onclick="switchTab('automations','self-improve')">Self-Improve <span class="tab-badge" id="tab-si-pending" style="display:none">0</span></button>
10644
- <button onclick="switchTab('automations','skills')">Skills <span class="tab-badge" id="tab-skill-count" style="display:none">0</span><span class="tab-badge" id="tab-pending-skill-count" title="pending approval" style="display:none;background:#f59e0b;color:#000">0</span></button>
10645
- <button onclick="switchTab('automations','analytics')">Execution Analytics</button>
10646
- </div>
10647
- <div id="automations-tab-content">
10648
- <div class="tab-pane active" id="tab-automations-scheduled">
10649
- <div id="panel-cron"><div class="empty-state">Loading...</div></div>
10650
- </div>
10651
- <div class="tab-pane" id="tab-automations-broken">
10652
- <div class="card">
10653
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
10654
- <span>Repeatedly Failing Jobs (last 48h)</span>
10655
- <span class="badge badge-gray" id="broken-count-badge" style="font-size:10px">0 jobs</span>
11423
+ <!-- Templates tab starter patterns -->
11424
+ <div id="build-tab-templates" data-build-tabpane="templates" style="display:none;padding:24px;overflow-y:auto">
11425
+ <div style="max-width:920px;margin:0 auto">
11426
+ <h2 style="font-size:18px;font-weight:600;margin:0 0 6px;color:var(--text-primary)">Start from a template</h2>
11427
+ <p style="font-size:13px;color:var(--text-muted);margin:0 0 18px">Pick a pre-built pattern to fork into a new editable workflow.</p>
11428
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px">
11429
+ <div class="card clickable-row" onclick="forkBuildTemplate('daily-news-digest')" style="padding:18px">
11430
+ <div style="font-size:24px;margin-bottom:8px">&#128240;</div>
11431
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">Daily news digest</div>
11432
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron 7am: pull RSS sources, summarize, send to Slack/email.</div>
11433
+ <div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 4 steps</div>
10656
11434
  </div>
10657
- <div class="card-body" id="panel-broken-jobs"><div class="empty-state">Loading...</div></div>
10658
- </div>
10659
- </div>
10660
- <div class="tab-pane" id="tab-automations-timers">
10661
- <div class="card">
10662
- <div class="card-body" id="panel-timers"><div class="empty-state">Loading...</div></div>
10663
- </div>
10664
- </div>
10665
- <div class="tab-pane" id="tab-automations-self-improve">
10666
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
10667
- <div style="font-size:13px;color:var(--text-secondary)">Self-improvement runs nightly at 1 AM. You can also trigger it manually.</div>
10668
- <button class="btn-sm btn-primary" onclick="siRunCycle()" id="si-run-btn">Run Now</button>
10669
- </div>
10670
- <div class="grid-2" id="si-status-cards"></div>
10671
- <div class="card" style="margin-top:16px">
10672
- <div class="card-header">Pending Proposals</div>
10673
- <div class="card-body" id="si-pending-list"><div class="empty-state">No pending proposals</div></div>
10674
- </div>
10675
- <div class="card" style="margin-top:16px">
10676
- <div class="card-header">Experiment History</div>
10677
- <div class="card-body" id="si-history-list"><div class="empty-state">No experiments yet</div></div>
10678
- </div>
10679
- </div>
10680
- <div class="tab-pane" id="tab-automations-skills">
10681
- <div class="card" style="margin-bottom:16px">
10682
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
10683
- <span>Teach a Skill</span>
10684
- <button class="btn-sm btn-primary" onclick="toggleTeachSkill()" id="teach-skill-toggle" style="font-size:12px">+ New Skill</button>
11435
+ <div class="card clickable-row" onclick="forkBuildTemplate('lead-picker')" style="padding:18px">
11436
+ <div style="font-size:24px;margin-bottom:8px">&#128202;</div>
11437
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">Lead picker → Salesforce</div>
11438
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.4">Manual workflow: search leads by ICP, review, push selected to SF.</div>
11439
+ <div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">manual · 3 steps</div>
10685
11440
  </div>
10686
- <div class="card-body" id="teach-skill-form" style="display:none;padding:16px">
10687
- <div style="display:grid;gap:12px">
10688
- <div>
10689
- <label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Title</label>
10690
- <input type="text" id="skill-title" placeholder="e.g., Deploy to production" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
10691
- </div>
10692
- <div>
10693
- <label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Description</label>
10694
- <input type="text" id="skill-description" placeholder="1-2 sentence summary of what this skill does" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
10695
- </div>
10696
- <div>
10697
- <label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Triggers <span style="font-weight:400;color:var(--text-muted)">(comma-separated keywords)</span></label>
10698
- <input type="text" id="skill-triggers" placeholder="deploy, production, release, ship" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">
10699
- </div>
10700
- <div>
10701
- <label style="font-size:12px;font-weight:600;color:var(--text-secondary)">Procedure <span style="font-weight:400;color:var(--text-muted)">(markdown steps)</span></label>
10702
- <textarea id="skill-steps" rows="6" placeholder="1. Run tests: npm test\n2. Build: npm run build\n3. Push: git push origin main\n4. Verify deploy succeeded" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px;font-family:monospace;resize:vertical"></textarea>
10703
- </div>
10704
- <div style="display:flex;gap:8px;justify-content:flex-end">
10705
- <button class="btn-sm" onclick="toggleTeachSkill()" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);padding:6px 16px;border-radius:6px;cursor:pointer">Cancel</button>
10706
- <button class="btn-sm btn-primary" onclick="saveSkill()" style="padding:6px 16px">Save Skill</button>
10707
- </div>
10708
- </div>
11441
+ <div class="card clickable-row" onclick="forkBuildTemplate('pr-review-queue')" style="padding:18px">
11442
+ <div style="font-size:24px;margin-bottom:8px">&#128221;</div>
11443
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">PR review queue</div>
11444
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron 9am M-F: list open PRs, summarize risk, message to Slack.</div>
11445
+ <div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 3 steps</div>
10709
11446
  </div>
10710
- </div>
10711
- <div class="card" id="pending-skills-card" style="display:none">
10712
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
10713
- <span>Pending Approval</span>
10714
- <span class="badge badge-orange" id="pending-skills-count-badge" style="font-size:10px">0 pending</span>
11447
+ <div class="card clickable-row" onclick="forkBuildTemplate('email-triage')" style="padding:18px">
11448
+ <div style="font-size:24px;margin-bottom:8px">&#128231;</div>
11449
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">Email triage</div>
11450
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron 8am: list unread emails, classify by intent, draft replies for review.</div>
11451
+ <div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 4 steps</div>
10715
11452
  </div>
10716
- <div class="card-body" id="panel-pending-skills"></div>
10717
- </div>
10718
- <div class="card">
10719
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
10720
- <span>Learned Skills</span>
10721
- <span class="badge badge-gray" id="skill-count-badge" style="font-size:10px">0 skills</span>
11453
+ <div class="card clickable-row" onclick="forkBuildTemplate('weekly-review')" style="padding:18px">
11454
+ <div style="font-size:24px;margin-bottom:8px">&#128197;</div>
11455
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">Weekly review</div>
11456
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.4">Cron Fri 6pm: review the week's daily notes, generate review note.</div>
11457
+ <div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">cron · 3 steps</div>
11458
+ </div>
11459
+ <div class="card clickable-row" onclick="forkBuildTemplate('blank-workflow')" style="padding:18px;border-style:dashed">
11460
+ <div style="font-size:24px;margin-bottom:8px">&#10133;</div>
11461
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">Blank workflow</div>
11462
+ <div style="font-size:12px;color:var(--text-muted);line-height:1.4">Start from scratch with a single prompt step.</div>
11463
+ <div style="margin-top:10px;font-size:11px;color:var(--clementine);font-weight:500">manual · 1 step</div>
10722
11464
  </div>
10723
- <div class="card-body" id="panel-skills"><div class="empty-state">No skills learned yet. Skills are auto-extracted from successful tasks or taught manually above.</div></div>
10724
11465
  </div>
10725
11466
  </div>
10726
- <div class="tab-pane" id="tab-automations-workflows">
10727
- <div id="panel-workflows"><div class="empty-state">Loading workflows...</div></div>
10728
- </div>
10729
- <div class="tab-pane" id="tab-automations-analytics">
10730
- <div id="advisor-analytics-content"><div class="empty-state">Loading analytics...</div></div>
10731
- </div>
10732
11467
  </div>
10733
11468
  </div>
10734
11469
 
10735
- <!-- ═══ Team Status Page ═══ -->
10736
- <div class="page" id="page-team-status">
10737
- <div class="page-title">Team Status</div>
10738
- <div id="team-status-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px;margin-top:16px"></div>
10739
- <script>
10740
- (function() {
10741
- function renderTeamStatus() {
10742
- fetch('/api/team/status').then(r => r.json()).then(data => {
10743
- const grid = document.getElementById('team-status-grid');
10744
- if (!grid) return;
10745
- if (!data.agents || data.agents.length === 0) {
10746
- grid.innerHTML = '<div class="empty-state">No agents found. Create an agent to get started.</div>';
10747
- return;
10748
- }
10749
- grid.innerHTML = data.agents.map(a => {
10750
- const avatarLetter = (a.name || a.slug || '?').charAt(0).toUpperCase();
10751
- const tasksHtml = a.tasks
10752
- ? '<div style="margin:4px 0"><span style="font-size:12px;color:var(--text-secondary)">Tasks: </span>' +
10753
- '<strong>' + (a.tasks.pending || 0) + ' pending</strong>' +
10754
- (a.tasks.overdue > 0 ? ' <span style="color:#ef4444;font-weight:600">' + a.tasks.overdue + ' overdue</span>' : '') +
10755
- '</div>'
10756
- : '';
10757
- const goalsHtml = a.activeGoals > 0
10758
- ? '<div style="margin:4px 0;font-size:12px"><span style="color:var(--text-secondary)">Goals: </span>' +
10759
- '<strong>' + a.activeGoals + ' active</strong>' +
10760
- (a.firstGoalTitle ? ' — ' + a.firstGoalTitle.slice(0, 60) : '') +
10761
- '</div>'
10762
- : '';
10763
- const noteHtml = a.lastNoteDate
10764
- ? '<div style="margin:4px 0;font-size:12px;color:var(--text-secondary)">Last log: ' + a.lastNoteDate +
10765
- (a.lastNoteSnippet ? ' ' + a.lastNoteSnippet.slice(0, 100) : '') + '</div>'
10766
- : '';
10767
- const wmHtml = a.workingMemorySnippet
10768
- ? '<div style="margin-top:8px;padding:8px;background:var(--bg-input);border-radius:6px;font-size:11px;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + a.workingMemorySnippet + '</div>'
10769
- : '';
10770
- return '<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:16px;cursor:pointer" onclick="showPage(\\'team\\');setTimeout(()=>selectAgent(\\''+a.slug+'\\'),100)">' +
10771
- '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">' +
10772
- '<div style="width:40px;height:40px;border-radius:50%;background:var(--clementine);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px">' + avatarLetter + '</div>' +
10773
- '<div><div style="font-weight:600">' + (a.name || a.slug) + '</div>' +
10774
- '<div style="font-size:11px;color:var(--text-secondary)">' + (a.description || a.slug) + '</div></div>' +
10775
- '</div>' +
10776
- tasksHtml + goalsHtml + noteHtml + wmHtml +
10777
- '</div>';
10778
- }).join('');
10779
- }).catch(() => {
10780
- const grid = document.getElementById('team-status-grid');
10781
- if (grid) grid.innerHTML = '<div class="empty-state">Failed to load team status.</div>';
10782
- });
10783
- }
10784
- // Render when page becomes visible
10785
- document.addEventListener('DOMContentLoaded', () => {
10786
- const obs = new MutationObserver(() => {
10787
- const page = document.getElementById('page-team-status');
10788
- if (page && page.classList.contains('active')) renderTeamStatus();
10789
- });
10790
- const content = document.querySelector('.content');
10791
- if (content) obs.observe(content, { subtree: true, attributes: true, attributeFilter: ['class'] });
11470
+ <!-- page-agent-detail merged into Team page; click an agent in Roster to drill down. -->
11471
+
11472
+
11473
+ <!-- DELETED in Session 5 — automations content moved to Build / Brain → Learning.
11474
+ (Block formerly housed: panel-cron, panel-broken-jobs, panel-timers,
11475
+ si-* status cards/proposals/history, teach-skill-form, panel-skills,
11476
+ pending-skills-card, panel-workflows, advisor-analytics-content.) -->
11477
+ <!-- (Session 5) page-automations parking removed. Self-Improve now lives in
11478
+ Brain Learning; Build (Workflows/Crons/Skills/Templates) is the home for
11479
+ everything else that was here. -->
11480
+
11481
+ <!-- page-team-status merged into Team Activity tab.
11482
+ Render is now triggered by switchTab('team','activity'). -->
11483
+ <script>
11484
+ (function() {
11485
+ window.renderTeamStatus = function renderTeamStatus() {
11486
+ fetch('/api/team/status').then(function(r) { return r.json(); }).then(function(data) {
11487
+ var grid = document.getElementById('team-status-grid');
11488
+ if (!grid) return;
11489
+ if (!data.agents || data.agents.length === 0) {
11490
+ grid.innerHTML = '<div class="empty-cta"><div class="label">No agents yet</div><div class="hint">Hire your first agent from the Roster tab.</div></div>';
11491
+ return;
11492
+ }
11493
+ grid.innerHTML = data.agents.map(function(a) {
11494
+ var avatarLetter = (a.name || a.slug || '?').charAt(0).toUpperCase();
11495
+ var tasksHtml = a.tasks
11496
+ ? '<div style="margin:4px 0"><span style="font-size:12px;color:var(--text-secondary)">Tasks: </span><strong>' + (a.tasks.pending || 0) + ' pending</strong>' +
11497
+ (a.tasks.overdue > 0 ? ' <span style="color:#ef4444;font-weight:600">' + a.tasks.overdue + ' overdue</span>' : '') + '</div>'
11498
+ : '';
11499
+ var goalsHtml = a.activeGoals > 0
11500
+ ? '<div style="margin:4px 0;font-size:12px"><span style="color:var(--text-secondary)">Goals: </span><strong>' + a.activeGoals + ' active</strong>' +
11501
+ (a.firstGoalTitle ? ' — ' + a.firstGoalTitle.slice(0, 60) : '') + '</div>'
11502
+ : '';
11503
+ var noteHtml = a.lastNoteDate
11504
+ ? '<div style="margin:4px 0;font-size:12px;color:var(--text-secondary)">Last log: ' + a.lastNoteDate +
11505
+ (a.lastNoteSnippet ? ' ' + a.lastNoteSnippet.slice(0, 100) : '') + '</div>'
11506
+ : '';
11507
+ var wmHtml = a.workingMemorySnippet
11508
+ ? '<div style="margin-top:8px;padding:8px;background:var(--bg-input);border-radius:6px;font-size:11px;color:var(--text-secondary)">' + a.workingMemorySnippet + '</div>'
11509
+ : '';
11510
+ return '<div class="clickable-row" style="background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:16px" onclick="navigateTo(\\x27team\\x27,{tab:\\x27roster\\x27,agentSlug:\\x27' + a.slug + '\\x27})">' +
11511
+ '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">' +
11512
+ '<div style="width:40px;height:40px;border-radius:50%;background:var(--clementine);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px">' + avatarLetter + '</div>' +
11513
+ '<div><div style="font-weight:600">' + (a.name || a.slug) + '</div>' +
11514
+ '<div style="font-size:11px;color:var(--text-secondary)">' + (a.description || a.slug) + '</div></div></div>' +
11515
+ tasksHtml + goalsHtml + noteHtml + wmHtml + '</div>';
11516
+ }).join('');
11517
+ }).catch(function() {
11518
+ var grid = document.getElementById('team-status-grid');
11519
+ if (grid) grid.innerHTML = '<div class="empty-state">Failed to load team status.</div>';
10792
11520
  });
10793
- })();
10794
- </script>
10795
- </div>
11521
+ };
11522
+ })();
11523
+ </script>
10796
11524
 
10797
11525
  <!-- ═══ Brain Page (unified: Search + Graph + Stats + Sources + Seed + Runs) ═══ -->
10798
- <div class="page" id="page-intelligence">
10799
- <div class="page-title">Brain</div>
10800
- <div style="color:var(--muted,#888);margin-bottom:16px;font-size:13px">
10801
- Query what you know, and feed new knowledge in. Everything on this page writes to or reads from the same memory + knowledge graph.
10802
- </div>
10803
- <div style="display:flex;gap:10px;margin-bottom:16px">
10804
- <input type="text" id="memory-search-input" placeholder="Search vault, notes, memory..." style="flex:1" onkeydown="if(event.key==='Enter')runMemorySearch()">
10805
- <button class="btn-primary" onclick="runMemorySearch()">Search</button>
11526
+ <div class="page" id="page-brain">
11527
+ <div class="page-head">
11528
+ <div class="icon">&#129504;</div>
11529
+ <div class="title-block">
11530
+ <h1>Brain</h1>
11531
+ <p class="desc">Query what you know, feed new knowledge in, and watch the system learn.</p>
11532
+ </div>
11533
+ <div class="actions" style="flex:1;max-width:480px;display:flex;gap:8px">
11534
+ <input type="text" id="memory-search-input" placeholder="Search vault, notes, memory..." style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px" onkeydown="if(event.key==='Enter')runMemorySearch()">
11535
+ <button class="btn-primary btn-sm" onclick="runMemorySearch()">Search</button>
11536
+ </div>
10806
11537
  </div>
10807
- <div class="tab-bar" id="intelligence-tabs">
10808
- <button class="active" onclick="switchTab('intelligence','search')">Search</button>
10809
- <button onclick="switchTab('intelligence','graph')">Knowledge Graph</button>
11538
+ <div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
11539
+ <button class="active" onclick="switchTab('intelligence','search')">Memory</button>
11540
+ <button onclick="switchTab('intelligence','graph')">Knowledge</button>
11541
+ <button onclick="switchTab('intelligence','sources')">Ingestion</button>
11542
+ <button onclick="switchTab('intelligence','health')">Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
10810
11543
  <button onclick="switchTab('intelligence','user-model')">User Model</button>
10811
- <button onclick="switchTab('intelligence','memory')">Memory Stats</button>
10812
- <button onclick="switchTab('intelligence','seed')">Seed Upload</button>
10813
- <button onclick="switchTab('intelligence','sources')">Sources</button>
10814
- <button onclick="switchTab('intelligence','runs')">Ingestion Runs</button>
11544
+ <button onclick="switchTab('intelligence','learning')">Learning <span class="tab-badge" id="brain-learning-badge" style="display:none;background:#f59e0b;color:#000">0</span></button>
11545
+ <button onclick="switchTab('intelligence','memory')">Stats</button>
11546
+ <button onclick="switchTab('intelligence','seed')">Seed</button>
11547
+ <button onclick="switchTab('intelligence','runs')">Runs</button>
10815
11548
  </div>
10816
11549
  <div id="intelligence-tab-content">
10817
11550
  <div class="tab-pane active" id="tab-intelligence-search">
@@ -10835,6 +11568,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10835
11568
  <div id="graph-detail-panel" style="margin-top:12px"></div>
10836
11569
  </div>
10837
11570
  <div class="tab-pane" id="tab-intelligence-memory">
11571
+ <div style="margin-bottom:12px;font-size:13px;color:var(--text-muted)">
11572
+ Stats and content browsing. For janitor, integrity, write queue, and staleness diagnostics see
11573
+ <a href="#" onclick="navigateTo('memory-health');return false" style="color:var(--accent)">Memory Health &rarr;</a>
11574
+ </div>
10838
11575
  <div class="grid-2" id="memory-stats"></div>
10839
11576
  <div class="card">
10840
11577
  <div class="card-header">MEMORY.md</div>
@@ -11009,6 +11746,68 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11009
11746
  <div class="tab-pane" id="tab-intelligence-runs">
11010
11747
  <div id="brain-runs-list"></div>
11011
11748
  </div>
11749
+ <div class="tab-pane" id="tab-intelligence-health">
11750
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
11751
+ <button class="btn-sm" onclick="memoryHealthAction('janitor')" title="Run the janitor cleanup pass now">Run cleanup</button>
11752
+ <button class="btn-sm" onclick="memoryHealthAction('rebuild-fts')" title="Rebuild the FTS5 index">Rebuild FTS</button>
11753
+ <button class="btn-sm" onclick="memoryHealthAction('fix-orphans')" title="Null out missing derived_from refs">Fix orphans</button>
11754
+ <button class="btn-sm" onclick="refreshMemoryHealth()">Refresh</button>
11755
+ </div>
11756
+ <div id="memory-health-content">
11757
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
11758
+ </div>
11759
+ <div class="card" style="margin-top:18px">
11760
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
11761
+ <span>Trust &amp; claim verification</span>
11762
+ <span id="trust-score-detail" style="font-size:11px;color:var(--text-muted)">Rolling 30-claim score</span>
11763
+ </div>
11764
+ <div class="card-body" style="padding:14px;display:flex;align-items:center;gap:14px">
11765
+ <span style="font-size:28px;font-weight:700" id="trust-score-big">--</span>
11766
+ <div style="flex:1;display:flex;gap:6px;flex-wrap:wrap">
11767
+ <button class="btn-sm" onclick="refreshClaims('all')" id="claims-filter-all">All</button>
11768
+ <button class="btn-sm" onclick="refreshClaims('pending')" id="claims-filter-pending">Pending</button>
11769
+ <button class="btn-sm" onclick="refreshClaims('verified')" id="claims-filter-verified">Verified</button>
11770
+ <button class="btn-sm" onclick="refreshClaims('failed')" id="claims-filter-failed">Failed</button>
11771
+ </div>
11772
+ </div>
11773
+ </div>
11774
+ <div class="card" style="margin-top:14px">
11775
+ <div class="card-header">Recent claims</div>
11776
+ <div class="card-body" id="panel-claims">
11777
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
11778
+ </div>
11779
+ </div>
11780
+ <div class="card" style="margin-top:14px">
11781
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
11782
+ <span>Team routing decisions</span>
11783
+ <span style="font-size:11px;color:var(--text-muted)">Owner-facing sessions only</span>
11784
+ </div>
11785
+ <div class="card-body" id="panel-routing-audit">
11786
+ <div class="skel-block"><div class="skel-row med"></div></div>
11787
+ </div>
11788
+ </div>
11789
+ </div>
11790
+ <div class="tab-pane" id="tab-intelligence-learning">
11791
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
11792
+ <div style="font-size:13px;color:var(--text-secondary)">Self-improvement runs nightly at 1 AM. You can also trigger it manually.</div>
11793
+ <button class="btn-sm btn-primary" onclick="siRunCycle()" id="si-run-btn">Run Now</button>
11794
+ </div>
11795
+ <div class="grid-2" id="si-status-cards">
11796
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
11797
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
11798
+ </div>
11799
+ <div class="card" style="margin-top:16px">
11800
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
11801
+ <span>Pending Proposals</span>
11802
+ <span class="tab-badge" id="tab-si-pending" style="display:none;background:#f59e0b;color:#000">0</span>
11803
+ </div>
11804
+ <div class="card-body" id="si-pending-list"><div class="empty-state">No pending proposals</div></div>
11805
+ </div>
11806
+ <div class="card" style="margin-top:16px">
11807
+ <div class="card-header">Experiment History</div>
11808
+ <div class="card-body" id="si-history-list"><div class="empty-state">No experiments yet</div></div>
11809
+ </div>
11810
+ </div>
11012
11811
  </div>
11013
11812
 
11014
11813
  <script>
@@ -11963,127 +12762,47 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11963
12762
  </script>
11964
12763
  </div>
11965
12764
 
11966
- <!-- Hidden: Sessions (shown in Home tabs in Phase 2) -->
11967
- <div class="page" id="page-sessions" style="display:none">
11968
- <div id="panel-sessions"><div class="empty-state">Loading...</div></div>
11969
- </div>
11970
-
11971
- <!-- ═══ Trust & Claims Page ═══ -->
11972
- <div class="page" id="page-claims">
11973
- <div class="page-title">Trust &amp; Claims</div>
11974
- <div class="card" style="margin-bottom:16px">
11975
- <div class="card-body" style="display:flex;align-items:center;gap:16px;padding:16px">
11976
- <div style="font-size:36px;font-weight:700" id="trust-score-big">--</div>
11977
- <div style="flex:1">
11978
- <div style="font-size:13px;font-weight:600">Clementine's trust score</div>
11979
- <div style="font-size:11px;color:var(--text-muted)" id="trust-score-detail">
11980
- Rolling over the last 30 verified or failed claims.
11981
- </div>
11982
- </div>
11983
- <div style="display:flex;gap:6px">
11984
- <button class="btn-sm" onclick="refreshClaims('all')" id="claims-filter-all" style="padding:4px 10px">All</button>
11985
- <button class="btn-sm" onclick="refreshClaims('pending')" id="claims-filter-pending" style="padding:4px 10px">Pending</button>
11986
- <button class="btn-sm" onclick="refreshClaims('verified')" id="claims-filter-verified" style="padding:4px 10px">Verified</button>
11987
- <button class="btn-sm" onclick="refreshClaims('failed')" id="claims-filter-failed" style="padding:4px 10px">Failed</button>
11988
- </div>
11989
- </div>
11990
- </div>
11991
- <div class="card">
11992
- <div class="card-header">Recent claims</div>
11993
- <div class="card-body" id="panel-claims"><div class="empty-state">Loading...</div></div>
11994
- </div>
11995
- <div class="card" style="margin-top:16px">
11996
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
11997
- <span>Team routing decisions</span>
11998
- <span style="font-size:11px;color:var(--text-muted)">Only owner-facing Clementine sessions are classified &mdash; agent-bot DMs bypass routing entirely.</span>
11999
- </div>
12000
- <div class="card-body" id="panel-routing-audit"><div class="empty-state">Loading...</div></div>
12001
- </div>
12002
- </div>
12003
-
12004
- <!-- ═══ Logs Page ═══ -->
12005
- <div class="page" id="page-logs">
12006
- <div class="page-title">Logs</div>
12007
- <div class="log-toolbar">
12008
- <input type="text" class="log-filter" id="log-filter" placeholder="Filter logs...">
12009
- <select id="log-level-filter" style="background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;font-size:12px;color:var(--text-primary);font-family:inherit;cursor:pointer" onchange="applyLogFilter()">
12010
- <option value="">All Levels</option>
12011
- <option value="error">Error+</option>
12012
- <option value="warn">Warn+</option>
12013
- <option value="info">Info+</option>
12014
- <option value="debug">Debug+</option>
12015
- </select>
12016
- <button onclick="refreshLogs()">Refresh</button>
12017
- <label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer">
12018
- <input type="checkbox" id="log-autoscroll" checked> Auto-scroll
12019
- </label>
12020
- </div>
12021
- <div class="log-viewer" id="panel-logs"><div class="empty-state">Loading...</div></div>
12022
- </div>
12023
-
12024
- <!-- ═══ Chat Page ═══ -->
12025
- <div class="page" id="page-chat">
12026
- <div id="chat-messages" style="flex:1;overflow-y:auto;padding:16px">
12027
- <div class="empty-state">
12028
- <p style="margin-bottom:14px;color:var(--text-muted)">Send a message to start a conversation.</p>
12029
- <div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
12030
- <button class="btn btn-sm quick-pill" onclick="quickChat(&quot;What&apos;s on my schedule?&quot;)">What's on my schedule?</button>
12031
- <button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Check my email</button>
12032
- <button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Run morning briefing</button>
12033
- <button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">What did you do today?</button>
12034
- </div>
12035
- </div>
12036
- </div>
12037
- <div style="border-top:1px solid var(--border);padding:8px 14px 0;display:flex;align-items:center;gap:8px" id="chat-profile-bar">
12038
- <span style="font-size:11px;color:var(--text-muted)">Profile:</span>
12039
- <select id="chat-profile-select" onchange="switchProfile(this.value)" style="font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary)">
12040
- <option value="">Default</option>
12041
- </select>
12042
- </div>
12043
- <div style="border-top:1px solid var(--border);padding:14px;display:flex;gap:10px">
12044
- <input type="text" id="chat-input" placeholder="Type a message..." style="flex:1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}">
12045
- <button class="btn-primary" id="chat-send-btn" onclick="sendChat()">Send</button>
12046
- </div>
12047
- </div>
12048
-
12049
- <!-- Hidden: Metrics (shown in Home tabs in Phase 2) -->
12050
- <div class="page" id="page-metrics" style="display:none">
12051
- <div id="metrics-content"><div class="empty-state">Loading metrics...</div></div>
12765
+ <!-- Sessions, Trust & Claims, Logs, Chat, Metrics, Daily Plan pages
12766
+ removed in Session 2 — content lives on Home or migrates to other
12767
+ destinations in Sessions 3-4. Page references for these IDs are
12768
+ resolved via ROUTE_REDIRECTS in navigateTo. -->
12769
+
12770
+ <!-- Hidden mounting points for daily-plan content used by the Home rail
12771
+ (refreshDailyPlan looks for these by ID). Keeping the IDs avoids
12772
+ touching every refreshDailyPlan callsite right now. -->
12773
+ <div style="display:none">
12774
+ <div id="plan-diff-content"></div>
12775
+ <div id="plan-history-list"></div>
12776
+ <input type="date" id="plan-date-picker-secondary" disabled>
12052
12777
  </div>
12053
12778
 
12054
- <!-- ═══ Daily Plan Page ═══ -->
12055
- <div class="page" id="page-daily-plan">
12056
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
12057
- <div class="page-title" style="margin-bottom:0">Daily Plan</div>
12058
- <div style="display:flex;gap:8px;align-items:center">
12059
- <input type="date" id="plan-date-picker" style="padding:6px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary)">
12060
- <button class="btn btn-sm" onclick="loadPlanForDate(document.getElementById('plan-date-picker').value)">Load</button>
12061
- </div>
12062
- </div>
12063
- <div id="daily-plan-content"><div class="empty-state">Loading plan...</div></div>
12064
- <div id="plan-diff-content" style="margin-top:16px"></div>
12065
- <details style="margin-top:16px">
12066
- <summary style="cursor:pointer;font-weight:600;color:var(--text-secondary);font-size:13px;padding:8px 0;user-select:none">Plan History</summary>
12067
- <div id="plan-history-list" style="margin-top:8px"><div class="empty-state">Loading...</div></div>
12068
- </details>
12069
- </div>
12779
+ <!-- (Session 5) page-memory-health parking stub removed. Brain → Health is the live home. -->
12070
12780
 
12071
- <!-- Hidden: Goals (moved to Agent Detail in Phase 3) -->
12072
- <div class="page" id="page-goals" style="display:none">
12073
- <div id="goals-progress-content"><div class="empty-state">Loading goals...</div></div>
12074
- </div>
12781
+ <!-- page-goals merged into Team Goals tab. -->
12075
12782
 
12076
12783
  <!-- ═══ Team Page — The Office ═══ -->
12077
12784
  <div class="page" id="page-team">
12078
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
12079
- <div class="page-title" style="margin-bottom:0">The Office</div>
12080
- <div style="display:flex;gap:8px;align-items:center">
12081
- <button class="btn" onclick="startHiringInterview()" style="background:var(--green);color:#000;font-weight:600">Hire a New Employee</button>
12082
- <button class="btn btn-sm" onclick="applyRoleTemplate('sdr')" style="color:var(--accent)" title="Pre-filled SDR role template">+ SDR</button>
12083
- <button class="btn btn-sm" onclick="applyRoleTemplate('researcher')" style="color:var(--text-muted)" title="Pre-filled researcher role template">+ Researcher</button>
12084
- <button class="btn btn-sm" onclick="showAgentCreateModal()" style="color:var(--text-muted)">Manual Setup</button>
12785
+ <div class="page-head">
12786
+ <div class="icon">&#128101;</div>
12787
+ <div class="title-block">
12788
+ <h1>The Office</h1>
12789
+ <p class="desc">Your team of agents — what they're doing, what they're contributing to.</p>
12790
+ </div>
12791
+ <div class="actions">
12792
+ <button class="btn-primary btn-sm" onclick="startHiringInterview()" style="background:var(--green);color:#000">Hire</button>
12793
+ <button class="btn-sm" onclick="applyRoleTemplate('sdr')" title="Pre-filled SDR role template">+ SDR</button>
12794
+ <button class="btn-sm" onclick="applyRoleTemplate('researcher')" title="Pre-filled researcher role template">+ Researcher</button>
12795
+ <button class="btn-sm" onclick="showAgentCreateModal()">Manual</button>
12085
12796
  </div>
12086
12797
  </div>
12798
+ <div class="tab-bar" id="team-tabs" style="margin:0 0 0 18px">
12799
+ <button class="active" onclick="switchTab('team','roster')">Roster</button>
12800
+ <button onclick="switchTab('team','activity')">Activity</button>
12801
+ <button onclick="switchTab('team','goals')">Goals</button>
12802
+ <button onclick="switchTab('team','comms')">Comms</button>
12803
+ </div>
12804
+ <div id="team-tab-content">
12805
+ <div class="tab-pane active" id="tab-team-roster">
12087
12806
  <div id="office-hero-section"></div>
12088
12807
  <div class="office-floor" id="team-agent-grid">
12089
12808
  <div class="empty-state">No agents configured</div>
@@ -12288,16 +13007,60 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
12288
13007
  </form>
12289
13008
  </div>
12290
13009
  </div>
13010
+ </div><!-- /tab-team-roster -->
13011
+
13012
+ <!-- Team → Activity tab (migrated from page-team-status) -->
13013
+ <div class="tab-pane" id="tab-team-activity" style="padding:18px">
13014
+ <div id="team-status-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px">
13015
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
13016
+ </div>
13017
+ </div>
13018
+
13019
+ <!-- Team → Goals tab (migrated from page-goals) -->
13020
+ <div class="tab-pane" id="tab-team-goals" style="padding:18px">
13021
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
13022
+ <p style="margin:0;font-size:13px;color:var(--text-muted)">Long-running objectives the team contributes to. Per-agent contribution + run success rate.</p>
13023
+ <button class="btn-primary btn-sm" onclick="openNewGoalForm()">+ New Goal</button>
13024
+ </div>
13025
+ <div id="new-goal-form" style="display:none;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:14px">
13026
+ <div style="font-weight:600;font-size:13px;margin-bottom:10px">New goal</div>
13027
+ <input type="text" id="new-goal-title" placeholder="Goal title (e.g., 'Pipeline: 12 qualified leads/week')" style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);margin-bottom:8px">
13028
+ <textarea id="new-goal-desc" placeholder="Why does this matter? (optional)" rows="2" style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-family:inherit;margin-bottom:10px"></textarea>
13029
+ <div style="display:flex;gap:8px;justify-content:flex-end">
13030
+ <button class="btn-sm" onclick="document.getElementById('new-goal-form').style.display='none'">Cancel</button>
13031
+ <button class="btn-sm btn-primary" onclick="submitNewGoal()">Create</button>
13032
+ </div>
13033
+ </div>
13034
+ <div id="goals-progress-content">
13035
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
13036
+ </div>
13037
+ </div>
13038
+
13039
+ <!-- Team → Comms tab — placeholder (Session 5 will fill). -->
13040
+ <div class="tab-pane" id="tab-team-comms" style="padding:18px">
13041
+ <div class="empty-cta">
13042
+ <div class="icon">&#128227;</div>
13043
+ <div class="label">Inter-agent communications</div>
13044
+ <div class="hint">Coming soon — message log + topology visualization across agents.</div>
13045
+ </div>
13046
+ </div>
13047
+ </div><!-- /team-tab-content -->
12291
13048
  </div>
12292
13049
 
13050
+ <!-- (Session 5) team-status / agent-detail / goals parking divs removed.
13051
+ Team's Roster / Activity / Goals tabs are the live homes. -->
13052
+
12293
13053
  <!-- ═══ Settings Page (merged: General + Remote + Integrations + Projects) ═══ -->
12294
13054
  <div class="page" id="page-settings">
12295
13055
  <div class="page-title">Settings</div>
12296
13056
  <div class="tab-bar" id="settings-tabs">
12297
- <button class="active" onclick="switchTab('settings','general')">General</button>
12298
- <button onclick="switchTab('settings','remote')">Remote Access</button>
13057
+ <button class="active" onclick="switchTab('settings','general')">Channels &amp; Env</button>
12299
13058
  <button onclick="switchTab('settings','integrations')">Integrations</button>
12300
- <button onclick="switchTab('settings','notifications')">Notifications</button>
13059
+ <button onclick="switchTab('settings','projects')">Projects</button>
13060
+ <button onclick="switchTab('settings','security')">Security</button>
13061
+ <button onclick="switchTab('settings','logs')">Logs</button>
13062
+ <button onclick="switchTab('settings','remote')">Remote Access</button>
13063
+ <button onclick="switchTab('settings','advanced')">Advanced</button>
12301
13064
  </div>
12302
13065
  <div id="settings-tab-content">
12303
13066
  <div class="tab-pane active" id="tab-settings-general">
@@ -12318,6 +13081,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
12318
13081
  </div>
12319
13082
  </div>
12320
13083
  </div>
13084
+ <div class="tab-pane" id="tab-settings-security">
13085
+ <div class="card">
13086
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
13087
+ <span>Active Sessions</span>
13088
+ <button class="btn-sm" style="font-size:11px" onclick="refreshAuthSessions()">Refresh</button>
13089
+ </div>
13090
+ <div class="card-body" style="padding:0" id="sessions-list">
13091
+ <div class="empty-state" style="padding:24px">Loading sessions...</div>
13092
+ </div>
13093
+ </div>
13094
+ <p style="color:var(--text-muted);font-size:12px;margin-top:12px">
13095
+ Sessions persist across daemon restarts. "Remember me" sessions last 30 days; standard sessions expire after 24 hours.
13096
+ Revoke any device you no longer trust.
13097
+ </p>
13098
+ </div>
12321
13099
  <div class="tab-pane" id="tab-settings-integrations">
12322
13100
  <div class="card" style="margin-bottom:20px">
12323
13101
  <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
@@ -12396,47 +13174,62 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
12396
13174
  </div>
12397
13175
  </div>
12398
13176
  </div>
12399
- <div class="tab-pane" id="tab-settings-notifications">
12400
- <div id="digest-settings-content"><div class="empty-state">Loading...</div></div>
12401
- </div>
12402
13177
  <div class="tab-pane" id="tab-settings-projects">
12403
- <p style="color:var(--text-muted);margin-bottom:16px">Link projects to give Clementine automatic access to their tools and MCP servers. When you mention a linked project's keywords in chat, Clementine switches into that project's context automatically.</p>
13178
+ <p style="color:var(--text-muted);margin-bottom:16px;font-size:13px">Link projects to give Clementine automatic access to their tools and MCP servers. When you mention a linked project's keywords in chat, Clementine switches into that project's context automatically.</p>
12404
13179
  <div class="card" style="margin-bottom:20px">
12405
13180
  <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
12406
13181
  <span>Workspace Directories</span>
12407
13182
  <button class="btn btn-sm btn-primary" onclick="promptAddWorkspaceDir()" style="font-size:11px">+ Add Path</button>
12408
13183
  </div>
12409
- <div class="card-body" id="workspace-dirs-list" style="font-size:13px">
12410
- <div class="empty-state">Loading...</div>
13184
+ <div class="card-body" id="workspace-dirs-list-projects" style="font-size:13px">
13185
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
12411
13186
  </div>
12412
13187
  </div>
12413
- <div id="panel-projects"><div class="empty-state">Loading...</div></div>
13188
+ <div id="panel-projects-page">
13189
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div></div>
13190
+ </div>
12414
13191
  </div>
12415
- </div>
12416
- </div>
12417
-
12418
- <!-- ═══ Projects Page (promoted from Settings) ═══ -->
12419
- <div class="page" id="page-projects">
12420
- <div class="page-title">Projects</div>
12421
- <p style="color:var(--text-muted);margin-bottom:16px">Link projects to give Clementine automatic access to their tools and MCP servers. When you mention a linked project's keywords in chat, Clementine switches into that project's context automatically.</p>
12422
- <div class="card" style="margin-bottom:20px">
12423
- <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
12424
- <span>Workspace Directories</span>
12425
- <button class="btn btn-sm btn-primary" onclick="promptAddWorkspaceDir()" style="font-size:11px">+ Add Path</button>
13192
+ <div class="tab-pane" id="tab-settings-logs">
13193
+ <div style="display:flex;gap:8px;align-items:center;margin-bottom:14px;flex-wrap:wrap">
13194
+ <input type="text" class="log-filter" id="log-filter" placeholder="Filter logs..." style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:12px;background:var(--bg-input);color:var(--text-primary);min-width:180px;flex:1">
13195
+ <select id="log-level-filter" style="background:var(--bg-input);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text-primary);font-family:inherit;cursor:pointer" onchange="applyLogFilter()">
13196
+ <option value="">All Levels</option>
13197
+ <option value="error">Error+</option>
13198
+ <option value="warn">Warn+</option>
13199
+ <option value="info">Info+</option>
13200
+ <option value="debug">Debug+</option>
13201
+ </select>
13202
+ <label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer">
13203
+ <input type="checkbox" id="log-autoscroll" checked> Auto-scroll
13204
+ </label>
13205
+ <button class="btn-sm" onclick="refreshLogs()">Refresh</button>
13206
+ </div>
13207
+ <div class="log-viewer" id="panel-logs">
13208
+ <div class="skel-block" style="padding:18px"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
13209
+ </div>
12426
13210
  </div>
12427
- <div class="card-body" id="workspace-dirs-list-projects" style="font-size:13px">
12428
- <div class="empty-state">Loading...</div>
13211
+ <div class="tab-pane" id="tab-settings-advanced">
13212
+ <div class="card" style="margin-bottom:16px">
13213
+ <div class="card-header">Diagnostics &amp; maintenance</div>
13214
+ <div class="card-body" style="padding:16px;display:flex;gap:8px;flex-wrap:wrap">
13215
+ <button class="btn-sm" onclick="restartDashboard()">Restart Dashboard</button>
13216
+ <button class="btn-sm" onclick="if(confirm('Restart the daemon? Active sessions drain first.')) apiPost('/api/restart')">Restart Daemon</button>
13217
+ <button class="btn-sm" onclick="apiFetch('/api/doctor').then(function(r){return r.text()}).then(function(t){alert(t)})">Run Doctor</button>
13218
+ <button class="btn-sm" onclick="apiFetch('/api/version').then(function(r){return r.json()}).then(function(d){alert('Version: '+(d.version||'?')+'\\nNode: '+(d.node||'?'))})">Build info</button>
13219
+ </div>
13220
+ </div>
13221
+ <div class="card">
13222
+ <div class="card-header">Notifications &amp; digest</div>
13223
+ <div class="card-body" style="padding:16px" id="digest-settings-content">
13224
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
13225
+ </div>
13226
+ </div>
12429
13227
  </div>
12430
13228
  </div>
12431
- <div id="panel-projects-page"><div class="empty-state">Loading...</div></div>
12432
13229
  </div>
12433
13230
 
12434
- <!-- ═══ Workflows Page (promoted from Automations) ═══ -->
12435
- <div class="page" id="page-workflows">
12436
- <div class="page-title">Workflows</div>
12437
- <p style="color:var(--text-muted);margin-bottom:16px">Multi-step pipelines that orchestrate agents, tools, and data. Define workflows as .md files in <code>vault/00-System/workflows/</code>.</p>
12438
- <div id="panel-workflows-page"><div class="empty-state">Loading workflows...</div></div>
12439
- </div>
13231
+ <!-- (Session 5) page-workflows / page-unleashed parking divs removed.
13232
+ Build Workflows + Home rail Active Runs are the live homes. -->
12440
13233
 
12441
13234
  </div><!-- /content -->
12442
13235
  </div><!-- /layout -->
@@ -13063,45 +13856,378 @@ let currentPage = 'home';
13063
13856
  var currentAgentSlug = null;
13064
13857
  var prevAgentSlugs = null;
13065
13858
 
13859
+ // ── Routing ────────────────────────────────────────────────────
13860
+ //
13861
+ // Five top-level destinations: home, build, team, brain, settings.
13862
+ // Sub-pages live as tabs within each destination.
13863
+ // Old routes redirect once for back-compat.
13864
+
13865
+ var DESTINATIONS = ['home', 'build', 'team', 'brain', 'settings'];
13866
+
13867
+ var ROUTE_REDIRECTS = {
13868
+ // old hash → new {page, tab}
13869
+ 'chat': { page: 'home', tab: 'chat' },
13870
+ 'sessions': { page: 'home', tab: 'activity' },
13871
+ 'daily-plan': { page: 'home', tab: 'today' },
13872
+ 'goals': { page: 'team', tab: 'goals' },
13873
+ 'workflows': { page: 'build', tab: 'workflows' },
13874
+ 'automations': { page: 'build', tab: 'crons' },
13875
+ 'unleashed': { page: 'build', tab: 'workflows' },
13876
+ 'builder': { page: 'build', tab: 'workflows' },
13877
+ 'skill-studio': { page: 'build', tab: 'skills' },
13878
+ 'team-status': { page: 'team', tab: 'activity' },
13879
+ 'agent-detail': { page: 'team', tab: 'roster' },
13880
+ 'intelligence': { page: 'brain', tab: 'memory' },
13881
+ 'memory-health': { page: 'brain', tab: 'health' },
13882
+ 'claims': { page: 'brain', tab: 'health' },
13883
+ 'metrics': { page: 'team', tab: 'activity' },
13884
+ 'logs': { page: 'settings', tab: 'logs' },
13885
+ 'projects': { page: 'settings', tab: 'projects' },
13886
+ };
13887
+
13066
13888
  function navigateTo(page, opts) {
13067
13889
  opts = opts || {};
13890
+
13891
+ // Redirect old route names to the new IA so existing callers + bookmarks work.
13892
+ if (ROUTE_REDIRECTS[page]) {
13893
+ var r = ROUTE_REDIRECTS[page];
13894
+ return navigateTo(r.page, Object.assign({ tab: r.tab }, opts));
13895
+ }
13896
+
13897
+ if (DESTINATIONS.indexOf(page) === -1) page = 'home';
13068
13898
  currentPage = page;
13069
- // Clear all active states
13070
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
13071
- document.querySelectorAll('.team-nav-item').forEach(n => n.classList.remove('active'));
13072
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
13073
- // Activate the right nav item
13899
+
13900
+ document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
13901
+ document.querySelectorAll('.team-nav-item').forEach(function(n) { n.classList.remove('active'); });
13902
+ document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
13903
+
13074
13904
  var navEl = document.querySelector('.nav-item[data-page="' + page + '"]');
13075
13905
  if (navEl) navEl.classList.add('active');
13076
13906
  if (opts.agentSlug != null) {
13077
13907
  var teamEl = document.querySelector('.team-nav-item[data-slug="' + opts.agentSlug + '"]');
13078
13908
  if (teamEl) teamEl.classList.add('active');
13079
13909
  }
13080
- // Show the page
13081
- var el = document.getElementById('page-' + page);
13082
- if (el) { el.style.display = ''; el.classList.add('active'); }
13083
- // Page-specific refresh
13084
- if (page === 'home') { refreshAll(); }
13085
- if (page === 'chat') { loadProfiles(); document.getElementById('chat-input').focus(); }
13086
- if (page === 'builder') {
13087
- var _builderPreselect = currentAgentSlug || '';
13088
- refreshBuilderAgents(_builderPreselect);
13089
- updateBuilderMode();
13090
- document.getElementById('builder-input').focus();
13091
- }
13092
- if (page === 'automations') { refreshCron(); refreshTimers(); refreshSelfImprove(); refreshSkills(); refreshBrokenJobs(); }
13093
- if (page === 'claims') { refreshClaims(); refreshRoutingAudit(); }
13094
- if (page === 'intelligence') { refreshMemory(); }
13095
- if (page === 'settings') { refreshSettings(); refreshRemoteAccess(); refreshSalesforce(); refreshClaudeIntegrations(); refreshMcpServers(); }
13096
- if (page === 'logs') refreshLogs();
13097
- if (page === 'team') { refreshTeam(); }
13098
- if (page === 'projects') { refreshProjects(); }
13099
- if (page === 'workflows') { refreshWorkflows(); }
13100
- if (page === 'agent-detail' && opts.agentSlug != null) {
13101
- currentAgentSlug = opts.agentSlug;
13102
- renderAgentDetail(opts.agentSlug);
13910
+
13911
+ var el = document.getElementById('page-' + page);
13912
+ if (el) { el.style.display = ''; el.classList.add('active'); }
13913
+
13914
+ // Per-destination init + tab routing
13915
+ switch (page) {
13916
+ case 'home':
13917
+ refreshAll();
13918
+ // tab is a soft hint on Home (one cohesive layout): focus the relevant area.
13919
+ var t = opts.tab || 'chat';
13920
+ setTimeout(function() {
13921
+ if (t === 'chat') {
13922
+ var ci = document.getElementById('chat-input');
13923
+ if (ci) ci.focus();
13924
+ } else if (t === 'today') {
13925
+ var rail = document.getElementById('home-rail');
13926
+ if (rail && window.matchMedia('(max-width: 1024px)').matches) rail.classList.add('open');
13927
+ var p = document.getElementById('home-plan-content');
13928
+ if (p) p.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
13929
+ } else if (t === 'activity') {
13930
+ var act = document.getElementById('panel-activity');
13931
+ if (act) act.scrollIntoView({ behavior: 'smooth', block: 'start' });
13932
+ }
13933
+ }, 80);
13934
+ break;
13935
+ case 'build':
13936
+ switchBuildTab(opts.tab || 'workflows');
13937
+ var bp = currentAgentSlug || '';
13938
+ refreshBuilderAgents(bp);
13939
+ break;
13940
+ case 'team':
13941
+ refreshTeam();
13942
+ switchDestTab('team', opts.tab || 'roster');
13943
+ if (opts.agentSlug) {
13944
+ currentAgentSlug = opts.agentSlug;
13945
+ if (typeof openAgentDrawer === 'function') openAgentDrawer(opts.agentSlug);
13946
+ }
13947
+ break;
13948
+ case 'brain':
13949
+ if (typeof refreshMemory === 'function') refreshMemory();
13950
+ var bt = opts.tab || 'memory';
13951
+ // Spec tab names → internal intelligence-tab ids
13952
+ var intelTab = bt === 'memory' ? 'search'
13953
+ : bt === 'knowledge' ? 'graph'
13954
+ : bt === 'ingestion' ? 'sources'
13955
+ : bt === 'health' ? 'health'
13956
+ : bt === 'user-model' ? 'user-model'
13957
+ : bt === 'learning' ? 'learning'
13958
+ : bt;
13959
+ try { switchTab('intelligence', intelTab); } catch (e) { /* */ }
13960
+ if (bt === 'health') {
13961
+ if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
13962
+ if (typeof refreshClaims === 'function') refreshClaims();
13963
+ if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
13964
+ }
13965
+ if (bt === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
13966
+ break;
13967
+ case 'settings':
13968
+ switchDestTab('settings', opts.tab || 'channels');
13969
+ break;
13970
+ }
13971
+
13972
+ closeSidebar();
13973
+ }
13974
+
13975
+ /** Switch the active tab inside a destination. Tabs use [data-tab] markup. */
13976
+ function switchDestTab(page, tab) {
13977
+ if (!tab) return;
13978
+ var pageEl = document.getElementById('page-' + page);
13979
+ if (!pageEl) return;
13980
+ pageEl.querySelectorAll('[data-tab]').forEach(function(el) {
13981
+ var match = el.getAttribute('data-tab') === tab;
13982
+ if (el.tagName === 'BUTTON' || el.classList.contains('tab-btn')) {
13983
+ el.classList.toggle('active', match);
13984
+ } else {
13985
+ el.style.display = match ? '' : 'none';
13986
+ }
13987
+ });
13988
+ // Per-tab init hook (called after DOM update)
13989
+ var initFn = window['_init_' + page + '_' + tab];
13990
+ if (typeof initFn === 'function') {
13991
+ try { initFn(); } catch (err) { console.warn('Tab init failed:', err); }
13992
+ }
13993
+ }
13994
+
13995
+ // (Session 5) openAutomationsTab compat shim removed — all callers route via navigateTo + ROUTE_REDIRECTS.
13996
+
13997
+ // ── Build (Workflows / Crons / Skills / Templates) tabs ─────────────
13998
+ function switchBuildTab(tab) {
13999
+ if (!tab) tab = 'workflows';
14000
+ // Update tab-bar active state
14001
+ document.querySelectorAll('#build-tabs button').forEach(function(b) {
14002
+ b.classList.toggle('active', b.getAttribute('data-build-tab') === tab);
14003
+ });
14004
+ // Show/hide tab panes
14005
+ var workPane = document.getElementById('build-tab-workflows');
14006
+ var tplPane = document.getElementById('build-tab-templates');
14007
+ var headerStrip = document.getElementById('build-header-strip');
14008
+ if (tab === 'templates') {
14009
+ if (workPane) workPane.style.display = 'none';
14010
+ if (tplPane) tplPane.style.display = '';
14011
+ if (headerStrip) headerStrip.style.display = 'none';
14012
+ } else {
14013
+ if (workPane) workPane.style.display = 'flex';
14014
+ if (tplPane) tplPane.style.display = 'none';
14015
+ if (headerStrip) headerStrip.style.display = 'flex';
14016
+ // Map build-tab → builder-type so the canvas + chat reflect the tab.
14017
+ var typeSel = document.getElementById('builder-type');
14018
+ if (typeSel) {
14019
+ var nextType = tab === 'crons' ? 'cron' : tab === 'skills' ? 'skill' : 'workflow';
14020
+ if (typeSel.value !== nextType) {
14021
+ typeSel.value = nextType;
14022
+ if (typeof resetBuilder === 'function') resetBuilder();
14023
+ if (typeof updateBuilderMode === 'function') updateBuilderMode();
14024
+ } else if (typeof updateBuilderMode === 'function') {
14025
+ updateBuilderMode();
14026
+ }
14027
+ }
14028
+ // Focus chat input
14029
+ setTimeout(function() {
14030
+ var bi = document.getElementById('builder-input');
14031
+ if (bi) bi.focus();
14032
+ }, 60);
14033
+ }
14034
+ }
14035
+
14036
+ // ── Build templates: fork a starter pattern into a new workflow ─────
14037
+ async function forkBuildTemplate(templateId) {
14038
+ var templates = {
14039
+ 'daily-news-digest': {
14040
+ name: 'Daily news digest',
14041
+ description: 'Morning news roundup pulled from configured RSS feeds.',
14042
+ schedule: '0 7 * * *',
14043
+ initialPrompt: 'Pull today headlines from configured RSS sources, summarize the top 5 stories, format as a brief digest, then send to my preferred channel.',
14044
+ },
14045
+ 'lead-picker': {
14046
+ name: 'Lead picker (manual)',
14047
+ description: 'Search leads matching ICP, pick from canvas, push selected to Salesforce.',
14048
+ schedule: undefined,
14049
+ initialPrompt: 'Use Salesforce search to find prospects matching the ICP I describe. Show results so I can pick which to push as leads.',
14050
+ },
14051
+ 'pr-review-queue': {
14052
+ name: 'PR review queue',
14053
+ description: 'Weekday morning summary of open PRs that need review.',
14054
+ schedule: '0 9 * * 1-5',
14055
+ initialPrompt: 'List open GitHub PRs that need my review, summarize each PRs risk level and key changes, send a digest to Slack.',
14056
+ },
14057
+ 'email-triage': {
14058
+ name: 'Email triage',
14059
+ description: 'Morning unread-email triage with classified suggested actions.',
14060
+ schedule: '0 8 * * *',
14061
+ initialPrompt: 'List unread emails, classify each by intent (reply / archive / snooze / delegate), draft replies for the ones needing one, surface for my approval.',
14062
+ },
14063
+ 'weekly-review': {
14064
+ name: 'Weekly review',
14065
+ description: 'Friday-evening review of the week and next-week priorities.',
14066
+ schedule: '0 18 * * 5',
14067
+ initialPrompt: 'Read this past 7 days of daily notes, summarize what got done, list whats still pending, suggest top 3 priorities for next week, append to today daily note as ## Weekly Review.',
14068
+ },
14069
+ 'blank-workflow': {
14070
+ name: 'New workflow',
14071
+ description: '',
14072
+ schedule: undefined,
14073
+ initialPrompt: 'Describe what this workflow should do.',
14074
+ },
14075
+ };
14076
+ var tpl = templates[templateId];
14077
+ if (!tpl) { toast('Unknown template', 'error'); return; }
14078
+ var name = prompt('Name for the new workflow:', tpl.name);
14079
+ if (!name) return;
14080
+ try {
14081
+ var r = await apiJson('POST', '/api/builder/workflows', {
14082
+ name: name,
14083
+ description: tpl.description,
14084
+ schedule: tpl.schedule,
14085
+ initialPrompt: tpl.initialPrompt,
14086
+ });
14087
+ if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
14088
+ if (r && r.id) {
14089
+ switchBuildTab(tpl.schedule ? 'crons' : 'workflows');
14090
+ refreshBuilderCanvasPicker(tpl.schedule ? 'cron' : 'workflow');
14091
+ setTimeout(function() { openBuilderWorkflow(r.id); }, 200);
14092
+ toast('Forked template: ' + name, 'success');
14093
+ }
14094
+ } catch (err) {
14095
+ toast('Fork failed: ' + err, 'error');
14096
+ }
14097
+ }
14098
+
14099
+ // Cmd+K palette — keyboard-only quick navigation.
14100
+ function openCommandK() {
14101
+ var existing = document.getElementById('cmdk-overlay');
14102
+ if (existing) { existing.remove(); return; }
14103
+ var overlay = document.createElement('div');
14104
+ overlay.id = 'cmdk-overlay';
14105
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:2000;display:flex;align-items:flex-start;justify-content:center;padding-top:120px';
14106
+ overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
14107
+ var box = document.createElement('div');
14108
+ box.style.cssText = 'width:520px;max-width:92vw;background:var(--bg-secondary);border:1px solid var(--border);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,0.35);overflow:hidden';
14109
+ box.innerHTML =
14110
+ '<input id="cmdk-input" type="text" placeholder="Jump to… (home, build/workflows, team/goals, brain/health, settings/logs)" style="width:100%;padding:14px 18px;border:none;background:transparent;color:var(--text-primary);font-size:14px;outline:none;border-bottom:1px solid var(--border)">' +
14111
+ '<div id="cmdk-results" style="max-height:320px;overflow-y:auto"></div>';
14112
+ overlay.appendChild(box);
14113
+ document.body.appendChild(overlay);
14114
+ var input = document.getElementById('cmdk-input');
14115
+ var results = document.getElementById('cmdk-results');
14116
+ var entries = [
14117
+ { kw: 'home chat', page: 'home', tab: 'chat', label: 'Home · Chat' },
14118
+ { kw: 'home today plan', page: 'home', tab: 'today', label: 'Home · Today' },
14119
+ { kw: 'home activity', page: 'home', tab: 'activity', label: 'Home · Activity' },
14120
+ { kw: 'build workflows', page: 'build', tab: 'workflows', label: 'Build · Workflows' },
14121
+ { kw: 'build crons', page: 'build', tab: 'crons', label: 'Build · Crons' },
14122
+ { kw: 'build skills', page: 'build', tab: 'skills', label: 'Build · Skills' },
14123
+ { kw: 'build templates', page: 'build', tab: 'templates', label: 'Build · Templates' },
14124
+ { kw: 'team roster', page: 'team', tab: 'roster', label: 'Team · Roster' },
14125
+ { kw: 'team activity', page: 'team', tab: 'activity', label: 'Team · Activity' },
14126
+ { kw: 'team comms', page: 'team', tab: 'comms', label: 'Team · Comms' },
14127
+ { kw: 'team goals', page: 'team', tab: 'goals', label: 'Team · Goals' },
14128
+ { kw: 'brain memory', page: 'brain', tab: 'memory', label: 'Brain · Memory' },
14129
+ { kw: 'brain knowledge', page: 'brain', tab: 'knowledge', label: 'Brain · Knowledge' },
14130
+ { kw: 'brain ingestion', page: 'brain', tab: 'ingestion', label: 'Brain · Ingestion' },
14131
+ { kw: 'brain health', page: 'brain', tab: 'health', label: 'Brain · Health' },
14132
+ { kw: 'brain user model', page: 'brain', tab: 'user-model', label: 'Brain · User Model' },
14133
+ { kw: 'settings channels', page: 'settings', tab: 'channels', label: 'Settings · Channels' },
14134
+ { kw: 'settings integrations mcp', page: 'settings', tab: 'integrations', label: 'Settings · Integrations' },
14135
+ { kw: 'settings projects', page: 'settings', tab: 'projects', label: 'Settings · Projects' },
14136
+ { kw: 'settings security', page: 'settings', tab: 'security', label: 'Settings · Security' },
14137
+ { kw: 'settings logs', page: 'settings', tab: 'logs', label: 'Settings · Logs' },
14138
+ { kw: 'settings advanced', page: 'settings', tab: 'advanced', label: 'Settings · Advanced' },
14139
+ ];
14140
+ function render() {
14141
+ var q = (input.value || '').toLowerCase().trim();
14142
+ var hits = q ? entries.filter(function(e) { return e.kw.indexOf(q) !== -1 || e.label.toLowerCase().indexOf(q) !== -1; }) : entries;
14143
+ results.innerHTML = hits.slice(0, 12).map(function(e, i) {
14144
+ return '<div class="cmdk-row" data-page="' + e.page + '" data-tab="' + e.tab + '" style="padding:10px 18px;font-size:13px;cursor:pointer;display:flex;justify-content:space-between;align-items:center' + (i === 0 ? ';background:var(--bg-hover)' : '') + '">' +
14145
+ '<span>' + e.label + '</span>' +
14146
+ '<span style="font-size:11px;color:var(--text-muted)">' + e.page + '/' + e.tab + '</span>' +
14147
+ '</div>';
14148
+ }).join('') || '<div style="padding:14px 18px;color:var(--text-muted);font-size:12px">No matches</div>';
14149
+ results.querySelectorAll('.cmdk-row').forEach(function(row) {
14150
+ row.onclick = function() {
14151
+ navigateTo(row.getAttribute('data-page'), { tab: row.getAttribute('data-tab') });
14152
+ overlay.remove();
14153
+ };
14154
+ });
14155
+ }
14156
+ input.oninput = render;
14157
+ input.onkeydown = function(e) {
14158
+ if (e.key === 'Escape') overlay.remove();
14159
+ if (e.key === 'Enter') {
14160
+ var first = results.querySelector('.cmdk-row');
14161
+ if (first) first.click();
14162
+ }
14163
+ };
14164
+ render();
14165
+ setTimeout(function() { input.focus(); }, 30);
14166
+ }
14167
+
14168
+ // Global keyboard shortcut for Cmd+K / Ctrl+K
14169
+ document.addEventListener('keydown', function(e) {
14170
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
14171
+ e.preventDefault();
14172
+ openCommandK();
14173
+ }
14174
+ });
14175
+
14176
+ async function refreshUnleashed() {
14177
+ var el = document.getElementById('panel-unleashed');
14178
+ if (!el) return;
14179
+ try {
14180
+ var r = await apiFetch('/api/unleashed');
14181
+ var d = await r.json();
14182
+ var tasks = d.tasks || [];
14183
+ var badge = document.getElementById('nav-unleashed-count');
14184
+ if (badge) {
14185
+ var running = tasks.filter(function(t) { return t.status === 'running'; }).length;
14186
+ if (running > 0) { badge.style.display = ''; badge.textContent = String(running); }
14187
+ else { badge.style.display = 'none'; }
14188
+ }
14189
+ if (tasks.length === 0) {
14190
+ el.innerHTML = '<div class="empty-state" style="padding:24px">No unleashed tasks. Schedule a cron job in <a href="#" onclick="navigateTo(\\'automations\\');return false">Scheduled Tasks</a> with mode = "unleashed" to start one.</div>';
14191
+ return;
14192
+ }
14193
+ var html = '<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr style="border-bottom:1px solid var(--border);background:var(--bg-tertiary)">';
14194
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Task</th>';
14195
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Status</th>';
14196
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Phase</th>';
14197
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Started</th>';
14198
+ html += '<th style="padding:8px 12px;text-align:right;color:var(--text-muted);font-weight:600">Action</th>';
14199
+ html += '</tr></thead><tbody>';
14200
+ tasks.forEach(function(t) {
14201
+ var statusColor = t.status === 'running' ? 'var(--green)' : t.status === 'completed' ? 'var(--blue)' : t.status === 'cancelled' ? 'var(--text-muted)' : 'var(--orange)';
14202
+ html += '<tr style="border-bottom:1px solid var(--border)">';
14203
+ html += '<td style="padding:10px 12px;font-weight:600">' + esc(t.name || '—') + '</td>';
14204
+ html += '<td style="padding:10px 12px"><span style="color:' + statusColor + ';font-size:11px;font-weight:600;text-transform:uppercase">' + esc(t.status || 'unknown') + '</span></td>';
14205
+ html += '<td style="padding:10px 12px;color:var(--text-muted)">' + esc(t.phase != null ? String(t.phase) : '—') + '</td>';
14206
+ html += '<td style="padding:10px 12px;color:var(--text-muted)">' + esc(t.startedAt || '—') + '</td>';
14207
+ html += '<td style="padding:10px 12px;text-align:right">';
14208
+ if (t.status === 'running') {
14209
+ html += '<button class="btn-sm" style="font-size:11px;color:#ef4444" onclick="cancelUnleashed(\\'' + esc(t.name) + '\\')">Cancel</button>';
14210
+ }
14211
+ html += '</td></tr>';
14212
+ });
14213
+ html += '</tbody></table>';
14214
+ el.innerHTML = html;
14215
+ } catch (ex) {
14216
+ el.innerHTML = '<div class="empty-state" style="padding:24px">Error: ' + esc(String(ex)) + '</div>';
14217
+ }
14218
+ }
14219
+
14220
+ async function cancelUnleashed(name) {
14221
+ if (!confirm('Cancel unleashed task "' + name + '"? It will stop at the next phase boundary.')) return;
14222
+ try {
14223
+ var r = await apiFetch('/api/unleashed/' + encodeURIComponent(name) + '/cancel', { method: 'POST' });
14224
+ var d = await r.json();
14225
+ if (d.ok) toast('Cancel requested', 'success');
14226
+ else toast(d.error || 'Cancel failed', 'error');
14227
+ refreshUnleashed();
14228
+ } catch (ex) {
14229
+ toast('Cancel failed: ' + ex, 'error');
13103
14230
  }
13104
- closeSidebar();
13105
14231
  }
13106
14232
 
13107
14233
  // Bind static nav items
@@ -13129,29 +14255,110 @@ function switchTab(group, tab) {
13129
14255
  if (pane) pane.classList.add('active');
13130
14256
  }
13131
14257
  // Tab-specific refresh
13132
- if (group === 'automations') {
13133
- if (tab === 'scheduled') refreshCron();
13134
- if (tab === 'broken') refreshBrokenJobs();
13135
- if (tab === 'timers') refreshTimers();
13136
- if (tab === 'self-improve') refreshSelfImprove();
13137
- if (tab === 'workflows') refreshWorkflows();
13138
- if (tab === 'analytics') refreshAdvisorAnalytics();
13139
- }
13140
14258
  if (group === 'intelligence') {
13141
14259
  if (tab === 'graph') refreshGraph();
13142
14260
  if (tab === 'memory') refreshMemory();
13143
- }
13144
- if (group === 'home') {
13145
- if (tab === 'metrics') refreshHomeMetrics();
13146
- if (tab === 'sessions') refreshHomeSessions();
13147
- if (tab === 'activity') refreshActivity();
13148
- if (tab === 'agents') refreshAgentComparison();
14261
+ if (tab === 'health') {
14262
+ if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
14263
+ if (typeof refreshClaims === 'function') refreshClaims();
14264
+ if (typeof refreshRoutingAudit === 'function') refreshRoutingAudit();
14265
+ }
14266
+ if (tab === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
13149
14267
  }
13150
14268
  if (group === 'settings') {
13151
14269
  if (tab === 'integrations') refreshSalesforce();
13152
- if (tab === 'projects') refreshProjects();
13153
14270
  if (tab === 'remote') refreshRemoteAccess();
13154
- if (tab === 'notifications') refreshDigestSettings();
14271
+ if (tab === 'security') refreshAuthSessions();
14272
+ if (tab === 'projects' && typeof refreshProjects === 'function') refreshProjects();
14273
+ if (tab === 'logs' && typeof refreshLogs === 'function') refreshLogs();
14274
+ if (tab === 'advanced' && typeof refreshDigestSettings === 'function') refreshDigestSettings();
14275
+ }
14276
+ if (group === 'team') {
14277
+ if (tab === 'activity' && typeof renderTeamStatus === 'function') renderTeamStatus();
14278
+ if (tab === 'goals' && typeof refreshGoalsProgress === 'function') refreshGoalsProgress();
14279
+ }
14280
+ }
14281
+
14282
+ async function refreshAuthSessions() {
14283
+ var el = document.getElementById('sessions-list');
14284
+ if (!el) return;
14285
+ try {
14286
+ var r = await fetch('/auth/sessions', { credentials: 'same-origin' });
14287
+ if (!r.ok) {
14288
+ if (r.status === 401) {
14289
+ el.innerHTML = '<div class="empty-state" style="padding:24px">Session-based view is only available for tunneled remote access. Localhost runs do not need login.</div>';
14290
+ return;
14291
+ }
14292
+ el.innerHTML = '<div class="empty-state" style="padding:24px">Failed to load sessions.</div>';
14293
+ return;
14294
+ }
14295
+ var d = await r.json();
14296
+ var rows = (d.sessions || []);
14297
+ if (rows.length === 0) {
14298
+ el.innerHTML = '<div class="empty-state" style="padding:24px">No active sessions.</div>';
14299
+ return;
14300
+ }
14301
+ function fmt(t) {
14302
+ if (!t) return '—';
14303
+ var diff = Date.now() - t;
14304
+ var s = Math.floor(diff / 1000);
14305
+ if (s < 60) return s + 's ago';
14306
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
14307
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
14308
+ return Math.floor(s / 86400) + 'd ago';
14309
+ }
14310
+ function fmtExpires(t) {
14311
+ var diff = t - Date.now();
14312
+ if (diff <= 0) return 'expired';
14313
+ var d = Math.floor(diff / 86400000);
14314
+ if (d > 1) return 'in ' + d + ' days';
14315
+ var h = Math.floor(diff / 3600000);
14316
+ if (h > 1) return 'in ' + h + ' hours';
14317
+ return 'in ' + Math.floor(diff / 60000) + ' min';
14318
+ }
14319
+ var html = '<table style="width:100%;border-collapse:collapse;font-size:13px">';
14320
+ html += '<thead><tr style="border-bottom:1px solid var(--border);background:var(--bg-tertiary)">';
14321
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Device</th>';
14322
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Created</th>';
14323
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Last used</th>';
14324
+ html += '<th style="padding:8px 12px;text-align:left;color:var(--text-muted);font-weight:600">Expires</th>';
14325
+ html += '<th style="padding:8px 12px;text-align:right;color:var(--text-muted);font-weight:600">Action</th>';
14326
+ html += '</tr></thead><tbody>';
14327
+ rows.forEach(function(s) {
14328
+ var ua = s.userAgent || 'Unknown device';
14329
+ var label = s.persistent ? '<span style="background:#f97316;color:#fff;padding:1px 6px;border-radius:3px;font-size:10px;margin-left:6px">Remember</span>' : '';
14330
+ var current = s.current ? '<span style="background:#10b981;color:#fff;padding:1px 6px;border-radius:3px;font-size:10px;margin-left:6px">This device</span>' : '';
14331
+ html += '<tr style="border-bottom:1px solid var(--border)">';
14332
+ html += '<td style="padding:10px 12px;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(ua) + label + current + '</td>';
14333
+ html += '<td style="padding:10px 12px;color:var(--text-muted)">' + fmt(s.createdAt) + '</td>';
14334
+ html += '<td style="padding:10px 12px;color:var(--text-muted)">' + fmt(s.lastUsedAt) + '</td>';
14335
+ html += '<td style="padding:10px 12px;color:var(--text-muted)">' + fmtExpires(s.expiresAt) + '</td>';
14336
+ html += '<td style="padding:10px 12px;text-align:right">';
14337
+ html += '<button class="btn-sm" style="font-size:11px;color:#ef4444" onclick="revokeSession(\\'' + esc(s.id) + '\\',' + (s.current ? 'true' : 'false') + ')">Revoke</button>';
14338
+ html += '</td></tr>';
14339
+ });
14340
+ html += '</tbody></table>';
14341
+ el.innerHTML = html;
14342
+ } catch (ex) {
14343
+ el.innerHTML = '<div class="empty-state" style="padding:24px">Error: ' + esc(String(ex)) + '</div>';
14344
+ }
14345
+ }
14346
+
14347
+ async function revokeSession(id, isCurrent) {
14348
+ var msg = isCurrent
14349
+ ? 'Revoke this session? You will be signed out immediately.'
14350
+ : 'Revoke this session?';
14351
+ if (!confirm(msg)) return;
14352
+ try {
14353
+ var r = await fetch('/auth/sessions/' + encodeURIComponent(id), { method: 'DELETE', credentials: 'same-origin' });
14354
+ if (!r.ok) { alert('Revoke failed'); return; }
14355
+ if (isCurrent) {
14356
+ window.location.href = '/';
14357
+ return;
14358
+ }
14359
+ refreshAuthSessions();
14360
+ } catch (ex) {
14361
+ alert('Revoke failed: ' + ex);
13155
14362
  }
13156
14363
  }
13157
14364
 
@@ -14135,7 +15342,7 @@ async function refreshSessions() {
14135
15342
  var s = d[key];
14136
15343
  var friendly = friendlySession(key);
14137
15344
  var safeKey = esc(key).replace(/'/g, '');
14138
- html += '<div class="session-card" style="cursor:pointer" onclick="viewSession(\\x27' + encodeURIComponent(key) + '\\x27)">'
15345
+ html += '<div class="session-card clickable-row" onclick="viewSession(\\x27' + encodeURIComponent(key) + '\\x27)">'
14139
15346
  + '<div class="session-card-header">'
14140
15347
  + '<span class="session-card-icon">' + friendly.icon + '</span>'
14141
15348
  + '<span class="session-card-name">' + esc(friendly.label) + '</span>'
@@ -14144,6 +15351,7 @@ async function refreshSessions() {
14144
15351
  + '<div class="session-card-meta">Last active: ' + timeAgo(s.timestamp) + '</div>'
14145
15352
  + '<div class="session-card-meta" style="font-family:monospace;font-size:10px">' + esc(key) + '</div>'
14146
15353
  + '<div class="session-card-actions">'
15354
+ + '<button class="btn-sm btn-primary" onclick="event.stopPropagation();resumeSession(\\x27' + encodeURIComponent(key) + '\\x27)" title="Open in chat">Resume</button>'
14147
15355
  + '<button class="btn-danger btn-sm" onclick="event.stopPropagation();if(confirm(\\x27Clear session ' + safeKey + '?\\x27))apiPost(\\x27/api/sessions/' + encodeURIComponent(key) + '/clear\\x27)">Clear</button>'
14148
15356
  + '</div></div>';
14149
15357
  }
@@ -14152,6 +15360,17 @@ async function refreshSessions() {
14152
15360
  } catch(e) { }
14153
15361
  }
14154
15362
 
15363
+ function resumeSession(encodedKey) {
15364
+ var key = decodeURIComponent(encodedKey);
15365
+ // The dashboard chat session is fixed at "dashboard:web" today, so we
15366
+ // navigate to chat and surface a hint. If/when chat gains multi-session
15367
+ // support, this is the place to bind the active session id.
15368
+ navigateTo('chat');
15369
+ var indicator = document.getElementById('chat-session-indicator');
15370
+ if (indicator) indicator.textContent = key;
15371
+ toast('Switched to ' + (friendlySession(key).label || key), 'info');
15372
+ }
15373
+
14155
15374
  async function viewSession(encodedKey) {
14156
15375
  var key = decodeURIComponent(encodedKey);
14157
15376
  var panel = document.getElementById('panel-sessions');
@@ -16835,128 +18054,810 @@ async function editChunk(id) {
16835
18054
  }
16836
18055
  }
16837
18056
 
16838
- async function saveEditChunk(id) {
16839
- var contentEl = document.getElementById('edit-content-' + id);
16840
- var sectionEl = document.getElementById('edit-section-' + id);
16841
- if (!contentEl) return;
16842
- try {
16843
- var r = await apiFetch('/api/memory/chunks/' + id, {
16844
- method: 'PUT',
16845
- headers: { 'Content-Type': 'application/json' },
16846
- body: JSON.stringify({
16847
- content: contentEl.value,
16848
- section: sectionEl ? sectionEl.value : undefined,
16849
- }),
16850
- });
16851
- var d = await r.json();
16852
- if (d.ok) {
16853
- toast('Chunk saved', 'success');
16854
- runMemorySearch(); // re-render with updated data
16855
- } else {
16856
- toast('Save failed: ' + (d.error || 'unknown'), 'error');
16857
- }
16858
- } catch (e) { toast('Save failed: ' + String(e), 'error'); }
18057
+ async function saveEditChunk(id) {
18058
+ var contentEl = document.getElementById('edit-content-' + id);
18059
+ var sectionEl = document.getElementById('edit-section-' + id);
18060
+ if (!contentEl) return;
18061
+ try {
18062
+ var r = await apiFetch('/api/memory/chunks/' + id, {
18063
+ method: 'PUT',
18064
+ headers: { 'Content-Type': 'application/json' },
18065
+ body: JSON.stringify({
18066
+ content: contentEl.value,
18067
+ section: sectionEl ? sectionEl.value : undefined,
18068
+ }),
18069
+ });
18070
+ var d = await r.json();
18071
+ if (d.ok) {
18072
+ toast('Chunk saved', 'success');
18073
+ runMemorySearch(); // re-render with updated data
18074
+ } else {
18075
+ toast('Save failed: ' + (d.error || 'unknown'), 'error');
18076
+ }
18077
+ } catch (e) { toast('Save failed: ' + String(e), 'error'); }
18078
+ }
18079
+
18080
+ function cancelEditChunk(id) {
18081
+ // Easiest: re-run the search to restore original rendering
18082
+ runMemorySearch();
18083
+ }
18084
+
18085
+ async function togglePinChunk(id, pinned) {
18086
+ try {
18087
+ var r = await apiFetch('/api/memory/chunks/' + id + '/pin', {
18088
+ method: 'POST',
18089
+ headers: { 'Content-Type': 'application/json' },
18090
+ body: JSON.stringify({ pinned: pinned }),
18091
+ });
18092
+ var d = await r.json();
18093
+ if (d.ok) {
18094
+ toast(pinned ? 'Pinned' : 'Unpinned', 'success');
18095
+ runMemorySearch();
18096
+ } else {
18097
+ toast('Pin failed: ' + (d.error || 'unknown'), 'error');
18098
+ }
18099
+ } catch (e) { toast('Pin failed: ' + String(e), 'error'); }
18100
+ }
18101
+
18102
+ async function deleteChunk(id) {
18103
+ if (!confirm('Delete this chunk? It will be excluded from search and retrieval. (Soft-delete — recoverable via the database.)')) return;
18104
+ try {
18105
+ var r = await apiFetch('/api/memory/chunks/' + id, { method: 'DELETE' });
18106
+ var d = await r.json();
18107
+ if (d.ok) {
18108
+ toast(d.removed ? 'Chunk deleted' : 'Chunk was already deleted', 'success');
18109
+ runMemorySearch();
18110
+ } else {
18111
+ toast('Delete failed: ' + (d.error || 'unknown'), 'error');
18112
+ }
18113
+ } catch (e) { toast('Delete failed: ' + String(e), 'error'); }
18114
+ }
18115
+
18116
+ // ── Profile Switching ─────────────────────
18117
+ async function loadProfiles() {
18118
+ try {
18119
+ var r = await apiFetch('/api/profiles');
18120
+ var d = await r.json();
18121
+ var sel = document.getElementById('chat-profile-select');
18122
+ sel.innerHTML = '<option value="">Default</option>';
18123
+ for (var p of (d.profiles || [])) {
18124
+ var opt = document.createElement('option');
18125
+ opt.value = p.slug;
18126
+ opt.textContent = p.name + (p.description ? ' — ' + p.description : '');
18127
+ if (p.slug === d.active) opt.selected = true;
18128
+ sel.appendChild(opt);
18129
+ }
18130
+ } catch(e) { /* profiles are optional */ }
18131
+ }
18132
+
18133
+ async function switchProfile(slug) {
18134
+ try {
18135
+ await apiJson('POST', '/api/profiles/switch', { slug: slug || null });
18136
+ // Clear chat display since session was reset
18137
+ var container = document.getElementById('chat-messages');
18138
+ container.innerHTML = '<div class="empty-state"><p style="margin-bottom:14px;color:var(--text-muted)">Profile switched' + (slug ? ' to <strong>' + esc(slug) + '</strong>' : '') + '. Session cleared.</p></div>';
18139
+ toast(slug ? 'Switched to ' + slug : 'Profile cleared', 'success');
18140
+ } catch(e) { toast('Failed to switch profile: ' + e, 'error'); }
18141
+ }
18142
+
18143
+ // ── Skill Studio — opens builder in skill-focused mode ──────────
18144
+
18145
+ function openSkillStudio() {
18146
+ // Pre-set type to skill before navigating
18147
+ var typeSelect = document.getElementById('builder-type');
18148
+ if (typeSelect) typeSelect.value = 'skill';
18149
+ navigateTo('builder');
18150
+ // Update the UI for skill mode
18151
+ updateBuilderMode();
18152
+ // Show skill-specific empty state
18153
+ var emptyState = document.getElementById('builder-empty-state');
18154
+ if (emptyState && !builderArtifact) {
18155
+ emptyState.innerHTML = '<p style="color:var(--text-muted);margin-bottom:8px;font-size:15px;font-weight:600">Skill Studio</p>'
18156
+ + '<p style="color:var(--text-muted);margin-bottom:16px;font-size:13px">Teach a new skill by describing it, attach reference files, link tools, test it, then save.</p>'
18157
+ + '<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">'
18158
+ + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for deploying to production — run tests, build, push, verify\\x27)">Deploy to prod</button>'
18159
+ + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for writing a weekly status report from git commits and calendar\\x27)">Weekly status report</button>'
18160
+ + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for onboarding a new team member — set up accounts, send welcome email\\x27)">Onboard team member</button>'
18161
+ + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for researching a company before a sales call\\x27)">Company research</button>'
18162
+ + '</div>';
18163
+ }
18164
+ }
18165
+
18166
+ function updateBuilderMode() {
18167
+ var type = (document.getElementById('builder-type') || {}).value || 'skill';
18168
+ var title = document.getElementById('builder-page-title');
18169
+ var drawer = document.getElementById('builder-skills-drawer');
18170
+ var preview = document.getElementById('builder-preview');
18171
+ var canvasHost = document.getElementById('builder-canvas-host');
18172
+ var picker = document.getElementById('builder-canvas-picker');
18173
+ var validateBtn = document.getElementById('builder-canvas-validate-btn');
18174
+ var dryrunBtn = document.getElementById('builder-canvas-dryrun-btn');
18175
+ var paneTitle = document.getElementById('builder-right-pane-title');
18176
+
18177
+ if (type === 'skill') {
18178
+ if (title) title.textContent = 'Skill Studio';
18179
+ if (drawer) { drawer.style.display = ''; refreshBuilderSkills(); }
18180
+ document.getElementById('builder-input').placeholder = 'Describe the skill you want to teach...';
18181
+ } else {
18182
+ if (title) title.textContent = 'Builder';
18183
+ if (drawer) drawer.style.display = 'none';
18184
+ document.getElementById('builder-input').placeholder = 'Describe what you want to build...';
18185
+ }
18186
+
18187
+ // Canvas mode for cron + workflow types
18188
+ var canvasMode = (type === 'cron' || type === 'workflow');
18189
+ var testBtn = document.getElementById('builder-canvas-test-btn');
18190
+ if (canvasMode) {
18191
+ if (preview) preview.style.display = 'none';
18192
+ if (canvasHost) canvasHost.style.display = 'flex';
18193
+ if (picker) picker.style.display = '';
18194
+ if (validateBtn) validateBtn.style.display = '';
18195
+ if (dryrunBtn) dryrunBtn.style.display = '';
18196
+ if (testBtn) testBtn.style.display = '';
18197
+ if (paneTitle) paneTitle.textContent = (type === 'cron' ? 'Crons' : 'Workflows');
18198
+ refreshBuilderCanvasPicker(type);
18199
+ } else {
18200
+ if (preview) preview.style.display = '';
18201
+ if (canvasHost) canvasHost.style.display = 'none';
18202
+ if (picker) picker.style.display = 'none';
18203
+ if (validateBtn) validateBtn.style.display = 'none';
18204
+ if (dryrunBtn) dryrunBtn.style.display = 'none';
18205
+ if (testBtn) testBtn.style.display = 'none';
18206
+ if (paneTitle) paneTitle.textContent = 'Live Preview';
18207
+ closeBuilderCanvas();
18208
+ }
18209
+ }
18210
+
18211
+ // ── Builder visual canvas (Phase 1: read-only, agent edits via MCP tools) ──
18212
+
18213
+ var _builderCanvasEditor = null;
18214
+ var _builderCanvasOpenId = null;
18215
+ var _builderCanvasLastWorkflow = null;
18216
+ var _builderDrawflowLoading = null;
18217
+
18218
+ function _ensureDrawflowLoaded() {
18219
+ if (window.Drawflow) return Promise.resolve();
18220
+ if (_builderDrawflowLoading) return _builderDrawflowLoading;
18221
+ _builderDrawflowLoading = new Promise(function(resolve, reject) {
18222
+ var s = document.createElement('script');
18223
+ s.src = '/static/drawflow.min.js';
18224
+ s.onload = function() { resolve(); };
18225
+ s.onerror = function() { reject(new Error('Failed to load Drawflow')); };
18226
+ document.head.appendChild(s);
18227
+ });
18228
+ return _builderDrawflowLoading;
18229
+ }
18230
+
18231
+ async function refreshBuilderCanvasPicker(type) {
18232
+ var picker = document.getElementById('builder-canvas-picker');
18233
+ if (!picker) return;
18234
+ try {
18235
+ var r = await apiFetch('/api/builder/workflows');
18236
+ var d = await r.json();
18237
+ var items = (d.workflows || []).filter(function(w) { return w.origin === type; });
18238
+ var opts = '<option value="">' + (items.length ? '— pick a ' + type + ' —' : '(none yet)') + '</option>';
18239
+ for (var i = 0; i < items.length; i++) {
18240
+ var w = items[i];
18241
+ var lbl = w.name + (w.schedule ? ' · ' + w.schedule : '') + (w.enabled ? '' : ' · off');
18242
+ opts += '<option value="' + esc(w.id) + '">' + esc(lbl) + '</option>';
18243
+ }
18244
+ picker.innerHTML = opts;
18245
+ if (_builderCanvasOpenId) picker.value = _builderCanvasOpenId;
18246
+ } catch (err) {
18247
+ picker.innerHTML = '<option value="">(failed to load)</option>';
18248
+ }
18249
+ }
18250
+
18251
+ async function openBuilderWorkflow(id) {
18252
+ if (!id) { closeBuilderCanvas(); return; }
18253
+ try {
18254
+ await _ensureDrawflowLoaded();
18255
+ var r = await apiFetch('/api/builder/workflows/' + encodeURIComponent(id));
18256
+ if (!r.ok) {
18257
+ var msg = await r.json().catch(function() { return {}; });
18258
+ toast('Failed to open: ' + (msg.error || r.status), 'error');
18259
+ return;
18260
+ }
18261
+ var d = await r.json();
18262
+ _builderCanvasOpenId = id;
18263
+ _builderCanvasLastWorkflow = d.workflow;
18264
+ _renderBuilderCanvas(d.drawflow);
18265
+
18266
+ var idEl = document.getElementById('builder-canvas-id');
18267
+ if (idEl) idEl.textContent = id;
18268
+
18269
+ var banner = document.getElementById('builder-canvas-banner');
18270
+ if (banner) {
18271
+ var issues = (d.validation && d.validation.issues) || [];
18272
+ var errors = issues.filter(function(i) { return i.severity === 'error'; });
18273
+ if (errors.length) {
18274
+ banner.style.display = '';
18275
+ banner.style.background = 'rgba(255,80,80,0.12)';
18276
+ banner.style.color = 'var(--red)';
18277
+ banner.textContent = errors.length + ' validation error' + (errors.length === 1 ? '' : 's') + ' — open Validate for details';
18278
+ } else {
18279
+ banner.style.display = 'none';
18280
+ }
18281
+ }
18282
+ } catch (err) {
18283
+ toast('Canvas error: ' + err, 'error');
18284
+ }
18285
+ }
18286
+
18287
+ function _renderBuilderCanvas(drawflowData) {
18288
+ var host = document.getElementById('builder-canvas');
18289
+ if (!host) return;
18290
+ // Tear down previous editor
18291
+ if (_builderCanvasEditor) {
18292
+ try { _builderCanvasEditor.clear(); } catch (e) { /* ignore */ }
18293
+ host.innerHTML = '';
18294
+ }
18295
+ var editor = new window.Drawflow(host);
18296
+ editor.reroute = true;
18297
+ editor.editor_mode = 'edit';
18298
+ editor.start();
18299
+ try {
18300
+ editor.import(drawflowData || { drawflow: { Home: { data: {} } } });
18301
+ _decorateBuilderNodes(host, _builderCanvasLastWorkflow);
18302
+ } catch (err) {
18303
+ host.innerHTML = '<div style="padding:24px;color:var(--red)">Failed to render canvas: ' + esc(String(err)) + '</div>';
18304
+ }
18305
+ _builderCanvasEditor = editor;
18306
+ _bindBuilderCanvasEvents(editor);
18307
+ }
18308
+
18309
+ var _builderSaveTimer = null;
18310
+ var _builderSavePending = false;
18311
+ var _builderRecentSaveTokens = new Set();
18312
+
18313
+ function _bindBuilderCanvasEvents(editor) {
18314
+ // Drawflow event names: nodeMoved, connectionCreated, connectionRemoved,
18315
+ // nodeDataChanged, nodeRemoved.
18316
+ ['nodeMoved', 'connectionCreated', 'connectionRemoved', 'nodeDataChanged', 'nodeRemoved'].forEach(function(evt) {
18317
+ try {
18318
+ editor.on(evt, function() {
18319
+ if (evt === 'nodeMoved') _scheduleBuilderSave(800);
18320
+ else _scheduleBuilderSave(300);
18321
+ });
18322
+ } catch (e) { /* drawflow always exposes .on, but be safe */ }
18323
+ });
18324
+ try {
18325
+ editor.on('nodeSelected', function(nodeId) { _openNodeConfigPanel(nodeId); });
18326
+ editor.on('nodeUnselected', function() { _closeNodeConfigPanel(); });
18327
+ } catch (e) { /* */ }
18328
+ }
18329
+
18330
+ function _scheduleBuilderSave(delay) {
18331
+ if (_builderSaveTimer) clearTimeout(_builderSaveTimer);
18332
+ _builderSaveTimer = setTimeout(function() { _flushBuilderSave(); }, delay || 500);
18333
+ }
18334
+
18335
+ async function _flushBuilderSave() {
18336
+ if (!_builderCanvasEditor || !_builderCanvasOpenId) return;
18337
+ if (_builderSavePending) {
18338
+ _scheduleBuilderSave(400);
18339
+ return;
18340
+ }
18341
+ _builderSavePending = true;
18342
+ _setBuilderSaveStatus('saving');
18343
+ var saveToken = 'sv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
18344
+ _builderRecentSaveTokens.add(saveToken);
18345
+ setTimeout(function() { _builderRecentSaveTokens.delete(saveToken); }, 5000);
18346
+ try {
18347
+ var data = _builderCanvasEditor.export();
18348
+ var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/save-from-drawflow', { drawflow: data, saveToken: saveToken });
18349
+ if (r.error) {
18350
+ if (r.validation && r.validation.issues) {
18351
+ _setBuilderSaveStatus('error', r.validation.issues.length + ' validation error' + (r.validation.issues.length === 1 ? '' : 's'));
18352
+ } else {
18353
+ _setBuilderSaveStatus('error', r.error);
18354
+ }
18355
+ } else {
18356
+ _setBuilderSaveStatus('saved');
18357
+ // Refresh banner from validation if warnings
18358
+ _updateBuilderBannerFromValidation(r.validation);
18359
+ }
18360
+ } catch (err) {
18361
+ _setBuilderSaveStatus('error', String(err));
18362
+ } finally {
18363
+ _builderSavePending = false;
18364
+ }
18365
+ }
18366
+
18367
+ function _setBuilderSaveStatus(state, detail) {
18368
+ var el = document.getElementById('builder-canvas-status');
18369
+ if (!el) return;
18370
+ if (state === 'saving') { el.textContent = 'Saving…'; el.style.color = 'var(--text-muted)'; }
18371
+ else if (state === 'saved') { el.textContent = 'Saved'; el.style.color = 'var(--green, #4caf50)'; setTimeout(function() { if (el.textContent === 'Saved') el.textContent = ''; }, 1500); }
18372
+ else if (state === 'error') { el.textContent = 'Save error: ' + (detail || ''); el.style.color = 'var(--red)'; }
18373
+ }
18374
+
18375
+ function _updateBuilderBannerFromValidation(v) {
18376
+ var banner = document.getElementById('builder-canvas-banner');
18377
+ if (!banner || !v) return;
18378
+ var issues = v.issues || [];
18379
+ var errors = issues.filter(function(i) { return i.severity === 'error'; });
18380
+ var warnings = issues.filter(function(i) { return i.severity === 'warning'; });
18381
+ if (errors.length) {
18382
+ banner.style.display = '';
18383
+ banner.style.background = 'rgba(255,80,80,0.12)';
18384
+ banner.style.color = 'var(--red)';
18385
+ banner.textContent = errors.length + ' validation error' + (errors.length === 1 ? '' : 's') + ' — open Validate for details';
18386
+ } else if (warnings.length) {
18387
+ banner.style.display = '';
18388
+ banner.style.background = 'rgba(240,180,0,0.12)';
18389
+ banner.style.color = '#a37100';
18390
+ banner.textContent = warnings.length + ' warning' + (warnings.length === 1 ? '' : 's') + ' — Validate for details';
18391
+ } else {
18392
+ banner.style.display = 'none';
18393
+ }
18394
+ }
18395
+
18396
+ function _decorateBuilderNodes(host, wf) {
18397
+ if (!wf) return;
18398
+ var byStepId = {};
18399
+ for (var i = 0; i < wf.steps.length; i++) byStepId[wf.steps[i].id] = wf.steps[i];
18400
+ // Drawflow renders blank node bodies by default — overlay our own content.
18401
+ var nodes = host.querySelectorAll('.drawflow-node');
18402
+ nodes.forEach(function(nodeEl) {
18403
+ var contentEl = nodeEl.querySelector('.drawflow_content_node');
18404
+ if (!contentEl || contentEl.dataset._decorated) return;
18405
+ contentEl.dataset._decorated = '1';
18406
+ var dataAttr = nodeEl.querySelector('input[df-stepId]');
18407
+ var stepId = dataAttr ? dataAttr.value : null;
18408
+ // Drawflow doesn't auto-bind without templates — derive from class instead
18409
+ var classNames = (nodeEl.className || '').split(/\s+/).filter(function(c) { return c.indexOf('cl-node-') === 0; });
18410
+ var kind = classNames.length ? classNames[0].replace('cl-node-', '') : 'prompt';
18411
+ var title = '';
18412
+ var body = '';
18413
+ var matchedStep = null;
18414
+ for (var j = 0; j < wf.steps.length; j++) {
18415
+ var s = wf.steps[j];
18416
+ if ((s.kind || 'prompt') === kind) { matchedStep = s; break; }
18417
+ }
18418
+ if (!matchedStep) {
18419
+ // Best-effort: use first step
18420
+ matchedStep = wf.steps[0];
18421
+ }
18422
+ title = (matchedStep ? matchedStep.id : '?');
18423
+ body = _summarizeStep(matchedStep, kind);
18424
+ contentEl.innerHTML =
18425
+ '<div style="font-weight:600;font-size:12px;margin-bottom:4px;color:#fff;text-transform:lowercase">' + esc(kind) + ' · ' + esc(title) + '</div>' +
18426
+ '<div style="font-size:11px;color:rgba(255,255,255,0.85);line-height:1.35;max-height:80px;overflow:hidden">' + esc(body) + '</div>';
18427
+ });
18428
+ }
18429
+
18430
+ function _summarizeStep(step, kind) {
18431
+ if (!step) return '';
18432
+ if (kind === 'mcp' && step.mcp) return step.mcp.server + '.' + step.mcp.tool;
18433
+ if (kind === 'channel' && step.channel) return step.channel.channel + ' → ' + step.channel.target;
18434
+ if (kind === 'transform' && step.transform) return step.transform.expression;
18435
+ if (kind === 'conditional' && step.conditional) return 'if ' + step.conditional.condition;
18436
+ if (kind === 'loop' && step.loop) return 'for each ' + step.loop.items;
18437
+ return (step.prompt || '').slice(0, 200);
18438
+ }
18439
+
18440
+ function closeBuilderCanvas() {
18441
+ _builderCanvasOpenId = null;
18442
+ _builderCanvasLastWorkflow = null;
18443
+ if (_builderCanvasEditor) {
18444
+ try { _builderCanvasEditor.clear(); } catch (e) { /* ignore */ }
18445
+ _builderCanvasEditor = null;
18446
+ }
18447
+ var host = document.getElementById('builder-canvas');
18448
+ if (host) host.innerHTML = '';
18449
+ var idEl = document.getElementById('builder-canvas-id');
18450
+ if (idEl) idEl.textContent = '';
18451
+ var banner = document.getElementById('builder-canvas-banner');
18452
+ if (banner) banner.style.display = 'none';
18453
+ }
18454
+
18455
+ async function validateBuilderCanvas() {
18456
+ if (!_builderCanvasOpenId) { toast('Open a workflow first', 'info'); return; }
18457
+ try {
18458
+ var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/validate', {});
18459
+ if (!r.issues || r.issues.length === 0) { toast('OK — no issues', 'success'); return; }
18460
+ var lines = r.issues.map(function(i) { return '[' + i.severity + '] ' + (i.stepId ? '(' + i.stepId + ') ' : '') + i.message; });
18461
+ alert('Validation:\\n\\n' + lines.join('\\n'));
18462
+ } catch (err) { toast('Validate failed: ' + err, 'error'); }
18463
+ }
18464
+
18465
+ async function dryRunBuilderCanvas() {
18466
+ if (!_builderCanvasOpenId) { toast('Open a workflow first', 'info'); return; }
18467
+ try {
18468
+ var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/dry-run', {});
18469
+ var lines = [];
18470
+ lines.push(r.ok ? 'DRY RUN — would execute:' : 'DRY RUN (validation issues found):');
18471
+ lines.push('');
18472
+ for (var i = 0; i < r.steps.length; i++) {
18473
+ var s = r.steps[i];
18474
+ lines.push('[wave ' + s.wave + '] ' + s.description);
18475
+ for (var k = 0; k < s.warnings.length; k++) lines.push(' ⚠ ' + s.warnings[k]);
18476
+ }
18477
+ if (r.estimatedTokens) lines.push('', '~' + r.estimatedTokens.total.toLocaleString() + ' tokens estimate (' + r.estimatedTokens.promptSteps + ' prompt step' + (r.estimatedTokens.promptSteps === 1 ? '' : 's') + ')');
18478
+ if (r.notes && r.notes.length) { lines.push(''); for (var n = 0; n < r.notes.length; n++) lines.push(r.notes[n]); }
18479
+ alert(lines.join('\\n'));
18480
+ } catch (err) { toast('Dry-run failed: ' + err, 'error'); }
18481
+ }
18482
+
18483
+ function _handleBuilderEvent(evt) {
18484
+ if (!evt || !evt.workflowId) return;
18485
+ // Run events are routed to the test-run handler.
18486
+ if (evt.type && evt.type.indexOf && evt.type.indexOf('run:') === 0) {
18487
+ _onRunEvent(evt);
18488
+ return;
18489
+ }
18490
+ // Suppress echoes of our own saves: server reflects the saveToken back.
18491
+ var token = evt.payload && evt.payload.saveToken;
18492
+ if (token && _builderRecentSaveTokens.has(token)) {
18493
+ _builderRecentSaveTokens.delete(token);
18494
+ return;
18495
+ }
18496
+ // Re-render if event is for the open workflow.
18497
+ if (evt.workflowId === _builderCanvasOpenId) {
18498
+ if (evt.type === 'workflow:deleted') { closeBuilderCanvas(); refreshBuilderCanvasPicker(document.getElementById('builder-type').value); return; }
18499
+ openBuilderWorkflow(_builderCanvasOpenId);
18500
+ }
18501
+ // Refresh picker when list changes
18502
+ if (evt.type === 'workflow:created' || evt.type === 'workflow:deleted' || evt.type === 'workflow:renamed') {
18503
+ refreshBuilderCanvasPicker(document.getElementById('builder-type').value);
18504
+ }
18505
+ }
18506
+
18507
+ // ── Node palette (Phase 2b) ─────────────────────────────────────
18508
+
18509
+ var _builderPaletteOpen = false;
18510
+
18511
+ function toggleBuilderPalette() {
18512
+ var pop = document.getElementById('builder-palette-pop');
18513
+ if (!pop) return;
18514
+ _builderPaletteOpen = !_builderPaletteOpen;
18515
+ pop.style.display = _builderPaletteOpen ? '' : 'none';
18516
+ }
18517
+
18518
+ function _builderAddNodeOfKind(kind) {
18519
+ if (!_builderCanvasEditor) return;
18520
+ toggleBuilderPalette();
18521
+ var canvas = document.getElementById('builder-canvas');
18522
+ if (!canvas) return;
18523
+ var rect = canvas.getBoundingClientRect();
18524
+ var posX = (rect.width / 2) - 100;
18525
+ var posY = (rect.height / 2) - 40;
18526
+ var stepId = _generateUniqueStepId(kind);
18527
+ var data = _defaultDataForKind(kind, stepId);
18528
+ var className = 'cl-node cl-node-' + kind;
18529
+ var nodeId = _builderCanvasEditor.addNode(_nodeNameForKind(kind), 1, 1, posX, posY, className, data, '');
18530
+ // Drawflow needs a tick before we can decorate
18531
+ setTimeout(function() {
18532
+ _decorateBuilderNodeById(nodeId, kind, data);
18533
+ _scheduleBuilderSave(200);
18534
+ }, 50);
18535
+ }
18536
+
18537
+ function _nodeNameForKind(kind) {
18538
+ switch (kind) {
18539
+ case 'mcp': return 'MCP Tool';
18540
+ case 'channel': return 'Channel';
18541
+ case 'transform': return 'Transform';
18542
+ case 'conditional': return 'Conditional';
18543
+ case 'loop': return 'Loop';
18544
+ default: return 'Prompt';
18545
+ }
18546
+ }
18547
+
18548
+ function _defaultDataForKind(kind, stepId) {
18549
+ var base = { stepId: stepId, prompt: '', tier: 1, maxTurns: 15, kind: kind };
18550
+ if (kind === 'prompt') return Object.assign({}, base, { prompt: 'Describe what this step should do.' });
18551
+ if (kind === 'mcp') return Object.assign({}, base, { mcp: { server: '', tool: '', inputs: {} } });
18552
+ if (kind === 'channel') return Object.assign({}, base, { channel: { channel: 'slack', target: '#me', content: '' } });
18553
+ if (kind === 'transform') return Object.assign({}, base, { transform: { expression: 'input' } });
18554
+ if (kind === 'conditional') return Object.assign({}, base, { conditional: { condition: 'true', trueNext: [], falseNext: [] } });
18555
+ if (kind === 'loop') return Object.assign({}, base, { loop: { items: 'input', bodyStepIds: [] } });
18556
+ return base;
18557
+ }
18558
+
18559
+ function _generateUniqueStepId(kind) {
18560
+ var used = new Set();
18561
+ if (_builderCanvasEditor) {
18562
+ var data = _builderCanvasEditor.export();
18563
+ var nodes = data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data ? data.drawflow.Home.data : {};
18564
+ for (var k in nodes) {
18565
+ var d = nodes[k].data || {};
18566
+ if (d.stepId) used.add(d.stepId);
18567
+ }
18568
+ }
18569
+ var prefix = kind.slice(0, 4);
18570
+ var i = 1;
18571
+ while (used.has(prefix + i)) i++;
18572
+ return prefix + i;
18573
+ }
18574
+
18575
+ function _decorateBuilderNodeById(nodeId, kind, data) {
18576
+ var nodeEl = document.querySelector('.drawflow-node[id="node-' + nodeId + '"]');
18577
+ if (!nodeEl) return;
18578
+ var content = nodeEl.querySelector('.drawflow_content_node');
18579
+ if (!content) return;
18580
+ content.dataset._decorated = '1';
18581
+ content.innerHTML =
18582
+ '<div style="font-weight:600;font-size:12px;margin-bottom:4px;color:#fff;text-transform:lowercase">' + esc(kind) + ' · ' + esc(data.stepId) + '</div>' +
18583
+ '<div style="font-size:11px;color:rgba(255,255,255,0.85);line-height:1.35;max-height:80px;overflow:hidden">' + esc(_summarizeStepData(data, kind)) + '</div>';
18584
+ }
18585
+
18586
+ function _summarizeStepData(d, kind) {
18587
+ if (!d) return '';
18588
+ if (kind === 'mcp' && d.mcp) return (d.mcp.server || '?') + '.' + (d.mcp.tool || '?');
18589
+ if (kind === 'channel' && d.channel) return d.channel.channel + ' → ' + d.channel.target;
18590
+ if (kind === 'transform' && d.transform) return d.transform.expression || '';
18591
+ if (kind === 'conditional' && d.conditional) return 'if ' + (d.conditional.condition || '');
18592
+ if (kind === 'loop' && d.loop) return 'for each ' + (d.loop.items || '');
18593
+ return (d.prompt || '').slice(0, 200);
18594
+ }
18595
+
18596
+ // ── Per-node config panel (Phase 2c) ───────────────────────────
18597
+
18598
+ var _builderConfigOpenNodeId = null;
18599
+ var _builderMcpToolsCache = null;
18600
+
18601
+ function _openNodeConfigPanel(nodeId) {
18602
+ if (!_builderCanvasEditor) return;
18603
+ _builderConfigOpenNodeId = nodeId;
18604
+ var node = _builderCanvasEditor.getNodeFromId(nodeId);
18605
+ if (!node) return;
18606
+ var data = node.data || {};
18607
+ var kind = data.kind || 'prompt';
18608
+ var panel = document.getElementById('builder-config-panel');
18609
+ if (!panel) return;
18610
+ panel.style.display = '';
18611
+ panel.innerHTML = _renderConfigPanel(nodeId, kind, data);
18612
+ _bindConfigPanelInputs(nodeId, kind);
18613
+ if (kind === 'mcp') _populateMcpDropdowns();
18614
+ }
18615
+
18616
+ function _closeNodeConfigPanel() {
18617
+ _builderConfigOpenNodeId = null;
18618
+ var panel = document.getElementById('builder-config-panel');
18619
+ if (panel) panel.style.display = 'none';
16859
18620
  }
16860
18621
 
16861
- function cancelEditChunk(id) {
16862
- // Easiest: re-run the search to restore original rendering
16863
- runMemorySearch();
18622
+ function _renderConfigPanel(nodeId, kind, d) {
18623
+ var common = (
18624
+ '<div class="cfg-row"><label>Step id</label><input type="text" data-cfg="stepId" value="' + esc(d.stepId || '') + '"></div>' +
18625
+ '<div class="cfg-row"><label>Tier</label><input type="number" min="1" max="5" data-cfg="tier" value="' + (d.tier || 1) + '"></div>' +
18626
+ '<div class="cfg-row"><label>Max turns</label><input type="number" min="1" data-cfg="maxTurns" value="' + (d.maxTurns || 15) + '"></div>' +
18627
+ '<div class="cfg-row"><label>Model</label><input type="text" data-cfg="model" placeholder="default" value="' + esc(d.model || '') + '"></div>'
18628
+ );
18629
+ var kindHtml = '';
18630
+ if (kind === 'prompt') {
18631
+ kindHtml = '<div class="cfg-row"><label>Prompt</label><textarea data-cfg="prompt" rows="6">' + esc(d.prompt || '') + '</textarea></div>';
18632
+ } else if (kind === 'mcp') {
18633
+ var m = d.mcp || {};
18634
+ kindHtml = (
18635
+ '<div class="cfg-row"><label>MCP server</label><select data-cfg="mcp.server" id="cfg-mcp-server"><option value="' + esc(m.server || '') + '">' + esc(m.server || '— pick —') + '</option></select></div>' +
18636
+ '<div class="cfg-row"><label>MCP tool</label><select data-cfg="mcp.tool" id="cfg-mcp-tool"><option value="' + esc(m.tool || '') + '">' + esc(m.tool || '— pick —') + '</option></select></div>' +
18637
+ '<div class="cfg-row"><label>Inputs (JSON)</label><textarea data-cfg="mcp.inputs" rows="4">' + esc(JSON.stringify(m.inputs || {}, null, 2)) + '</textarea></div>' +
18638
+ '<div class="cfg-row"><label>Description</label><textarea data-cfg="prompt" rows="3">' + esc(d.prompt || '') + '</textarea></div>'
18639
+ );
18640
+ } else if (kind === 'channel') {
18641
+ var c = d.channel || {};
18642
+ kindHtml = (
18643
+ '<div class="cfg-row"><label>Channel</label><select data-cfg="channel.channel">' +
18644
+ ['discord','slack','telegram','whatsapp','email','webhook'].map(function(k) { return '<option' + (c.channel === k ? ' selected' : '') + '>' + k + '</option>'; }).join('') +
18645
+ '</select></div>' +
18646
+ '<div class="cfg-row"><label>Target</label><input type="text" data-cfg="channel.target" placeholder="#channel, user id, email…" value="' + esc(c.target || '') + '"></div>' +
18647
+ '<div class="cfg-row"><label>Content</label><textarea data-cfg="channel.content" rows="6">' + esc(c.content || '') + '</textarea></div>'
18648
+ );
18649
+ } else if (kind === 'transform') {
18650
+ var t = d.transform || {};
18651
+ kindHtml = '<div class="cfg-row"><label>Expression</label><textarea data-cfg="transform.expression" rows="6" placeholder="JS expression that returns shaped data">' + esc(t.expression || '') + '</textarea></div>';
18652
+ } else if (kind === 'conditional') {
18653
+ var co = d.conditional || {};
18654
+ kindHtml = (
18655
+ '<div class="cfg-row"><label>Condition</label><textarea data-cfg="conditional.condition" rows="3" placeholder="JS expression — truthy/falsy">' + esc(co.condition || '') + '</textarea></div>' +
18656
+ '<div class="cfg-row"><label>True → step ids</label><input type="text" data-cfg="conditional.trueNext" placeholder="comma-separated" value="' + esc((co.trueNext || []).join(',')) + '"></div>' +
18657
+ '<div class="cfg-row"><label>False → step ids</label><input type="text" data-cfg="conditional.falseNext" placeholder="comma-separated" value="' + esc((co.falseNext || []).join(',')) + '"></div>'
18658
+ );
18659
+ } else if (kind === 'loop') {
18660
+ var l = d.loop || {};
18661
+ kindHtml = (
18662
+ '<div class="cfg-row"><label>Items</label><input type="text" data-cfg="loop.items" placeholder="JS expression returning iterable" value="' + esc(l.items || '') + '"></div>' +
18663
+ '<div class="cfg-row"><label>Body step ids</label><input type="text" data-cfg="loop.bodyStepIds" placeholder="comma-separated" value="' + esc((l.bodyStepIds || []).join(',')) + '"></div>'
18664
+ );
18665
+ }
18666
+ var lastRun = _builderRunStepResults && _builderRunStepResults[d.stepId || ''];
18667
+ var lastRunBtn = lastRun
18668
+ ? '<button onclick="showStepOutput(\\x27' + esc(d.stepId || '') + '\\x27)" style="margin-top:4px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px;width:100%">Show last run output (' + esc(lastRun.status || '') + ')</button>'
18669
+ : '';
18670
+ return (
18671
+ '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px">' +
18672
+ '<span>' + esc(kind) + ' step</span>' +
18673
+ '<span style="flex:1"></span>' +
18674
+ '<button onclick="_deleteSelectedNode()" title="Delete this node" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:16px">×</button>' +
18675
+ '</div>' +
18676
+ '<div style="padding:10px 14px;flex:1;overflow-y:auto" id="builder-config-fields">' +
18677
+ common +
18678
+ '<div style="border-top:1px dashed var(--border);margin:10px 0"></div>' +
18679
+ kindHtml +
18680
+ (lastRunBtn ? '<div style="border-top:1px dashed var(--border);margin:14px 0 8px"></div>' + lastRunBtn : '') +
18681
+ '</div>'
18682
+ );
16864
18683
  }
16865
18684
 
16866
- async function togglePinChunk(id, pinned) {
16867
- try {
16868
- var r = await apiFetch('/api/memory/chunks/' + id + '/pin', {
16869
- method: 'POST',
16870
- headers: { 'Content-Type': 'application/json' },
16871
- body: JSON.stringify({ pinned: pinned }),
16872
- });
16873
- var d = await r.json();
16874
- if (d.ok) {
16875
- toast(pinned ? 'Pinned' : 'Unpinned', 'success');
16876
- runMemorySearch();
16877
- } else {
16878
- toast('Pin failed: ' + (d.error || 'unknown'), 'error');
18685
+ function _bindConfigPanelInputs(nodeId) {
18686
+ var panel = document.getElementById('builder-config-panel');
18687
+ if (!panel) return;
18688
+ panel.querySelectorAll('[data-cfg]').forEach(function(el) {
18689
+ el.addEventListener('change', function() { _applyConfigField(nodeId, el); });
18690
+ if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
18691
+ el.addEventListener('blur', function() { _applyConfigField(nodeId, el); });
16879
18692
  }
16880
- } catch (e) { toast('Pin failed: ' + String(e), 'error'); }
18693
+ });
16881
18694
  }
16882
18695
 
16883
- async function deleteChunk(id) {
16884
- if (!confirm('Delete this chunk? It will be excluded from search and retrieval. (Soft-delete — recoverable via the database.)')) return;
16885
- try {
16886
- var r = await apiFetch('/api/memory/chunks/' + id, { method: 'DELETE' });
16887
- var d = await r.json();
16888
- if (d.ok) {
16889
- toast(d.removed ? 'Chunk deleted' : 'Chunk was already deleted', 'success');
16890
- runMemorySearch();
16891
- } else {
16892
- toast('Delete failed: ' + (d.error || 'unknown'), 'error');
16893
- }
16894
- } catch (e) { toast('Delete failed: ' + String(e), 'error'); }
18696
+ function _applyConfigField(nodeId, el) {
18697
+ if (!_builderCanvasEditor) return;
18698
+ var node = _builderCanvasEditor.getNodeFromId(nodeId);
18699
+ if (!node) return;
18700
+ var pathStr = el.getAttribute('data-cfg') || '';
18701
+ var pathParts = pathStr.split('.');
18702
+ var raw = el.value;
18703
+ var parsed;
18704
+ if (pathStr.endsWith('.inputs')) {
18705
+ try { parsed = raw ? JSON.parse(raw) : {}; } catch (e) { toast('Invalid JSON in inputs', 'error'); return; }
18706
+ } else if (pathStr === 'tier' || pathStr === 'maxTurns') {
18707
+ parsed = Number(raw) || 0;
18708
+ } else if (pathStr === 'conditional.trueNext' || pathStr === 'conditional.falseNext' || pathStr === 'loop.bodyStepIds') {
18709
+ parsed = raw ? raw.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
18710
+ } else {
18711
+ parsed = raw;
18712
+ }
18713
+ var newData = JSON.parse(JSON.stringify(node.data || {}));
18714
+ var cur = newData;
18715
+ for (var i = 0; i < pathParts.length - 1; i++) {
18716
+ if (!cur[pathParts[i]] || typeof cur[pathParts[i]] !== 'object') cur[pathParts[i]] = {};
18717
+ cur = cur[pathParts[i]];
18718
+ }
18719
+ cur[pathParts[pathParts.length - 1]] = parsed;
18720
+ _builderCanvasEditor.updateNodeDataFromId(nodeId, newData);
18721
+ // Re-decorate the node visually
18722
+ _decorateBuilderNodeById(nodeId, newData.kind || 'prompt', newData);
18723
+ _scheduleBuilderSave(300);
16895
18724
  }
16896
18725
 
16897
- // ── Profile Switching ─────────────────────
16898
- async function loadProfiles() {
18726
+ function _deleteSelectedNode() {
18727
+ if (!_builderCanvasEditor || !_builderConfigOpenNodeId) return;
18728
+ if (!confirm('Delete this node?')) return;
18729
+ _builderCanvasEditor.removeNodeId('node-' + _builderConfigOpenNodeId);
18730
+ _closeNodeConfigPanel();
18731
+ _scheduleBuilderSave(150);
18732
+ }
18733
+
18734
+ // ── Test runs (Phase 2d/2e) ─────────────────────────────────────
18735
+
18736
+ var _builderActiveRunId = null;
18737
+ var _builderRunStepResults = {};
18738
+
18739
+ async function testBuilderCanvas() {
18740
+ if (!_builderCanvasOpenId) { toast('Open a workflow first', 'info'); return; }
18741
+ if (_builderActiveRunId) { toast('A test is already running', 'info'); return; }
18742
+ // Always flush any pending save so the test sees the latest graph
18743
+ if (_builderSaveTimer) { clearTimeout(_builderSaveTimer); _builderSaveTimer = null; await _flushBuilderSave(); }
18744
+ _clearRunVisualState();
16899
18745
  try {
16900
- var r = await apiFetch('/api/profiles');
16901
- var d = await r.json();
16902
- var sel = document.getElementById('chat-profile-select');
16903
- sel.innerHTML = '<option value="">Default</option>';
16904
- for (var p of (d.profiles || [])) {
16905
- var opt = document.createElement('option');
16906
- opt.value = p.slug;
16907
- opt.textContent = p.name + (p.description ? ' ' + p.description : '');
16908
- if (p.slug === d.active) opt.selected = true;
16909
- sel.appendChild(opt);
16910
- }
16911
- } catch(e) { /* profiles are optional */ }
18746
+ var r = await apiJson('POST', '/api/builder/workflows/' + encodeURIComponent(_builderCanvasOpenId) + '/test', { mode: 'mock' });
18747
+ if (r.error) { toast(r.error, 'error'); return; }
18748
+ _builderActiveRunId = r.runId;
18749
+ _builderRunStepResults = {};
18750
+ var cancel = document.getElementById('builder-canvas-cancel-btn');
18751
+ if (cancel) cancel.style.display = '';
18752
+ _setBuilderSaveStatus('saved'); // override status with a more useful one below
18753
+ _setRunFooter('Running test… (' + r.runId.slice(0, 8) + ')');
18754
+ } catch (err) {
18755
+ toast('Test failed to start: ' + err, 'error');
18756
+ }
16912
18757
  }
16913
18758
 
16914
- async function switchProfile(slug) {
18759
+ async function cancelBuilderTest() {
18760
+ if (!_builderActiveRunId) return;
16915
18761
  try {
16916
- await apiJson('POST', '/api/profiles/switch', { slug: slug || null });
16917
- // Clear chat display since session was reset
16918
- var container = document.getElementById('chat-messages');
16919
- container.innerHTML = '<div class="empty-state"><p style="margin-bottom:14px;color:var(--text-muted)">Profile switched' + (slug ? ' to <strong>' + esc(slug) + '</strong>' : '') + '. Session cleared.</p></div>';
16920
- toast(slug ? 'Switched to ' + slug : 'Profile cleared', 'success');
16921
- } catch(e) { toast('Failed to switch profile: ' + e, 'error'); }
18762
+ await apiJson('POST', '/api/builder/runs/' + encodeURIComponent(_builderActiveRunId) + '/cancel', {});
18763
+ } catch (err) { /* SSE will still report the cancellation */ }
16922
18764
  }
16923
18765
 
16924
- // ── Skill Studio — opens builder in skill-focused mode ──────────
18766
+ function _clearRunVisualState() {
18767
+ document.querySelectorAll('#builder-canvas .drawflow-node').forEach(function(n) {
18768
+ n.classList.remove('cl-step-running', 'cl-step-done', 'cl-step-failed', 'cl-step-skipped', 'cl-step-cancelled', 'cl-step-timeout');
18769
+ });
18770
+ }
16925
18771
 
16926
- function openSkillStudio() {
16927
- // Pre-set type to skill before navigating
16928
- var typeSelect = document.getElementById('builder-type');
16929
- if (typeSelect) typeSelect.value = 'skill';
16930
- navigateTo('builder');
16931
- // Update the UI for skill mode
16932
- updateBuilderMode();
16933
- // Show skill-specific empty state
16934
- var emptyState = document.getElementById('builder-empty-state');
16935
- if (emptyState && !builderArtifact) {
16936
- emptyState.innerHTML = '<p style="color:var(--text-muted);margin-bottom:8px;font-size:15px;font-weight:600">Skill Studio</p>'
16937
- + '<p style="color:var(--text-muted);margin-bottom:16px;font-size:13px">Teach a new skill by describing it, attach reference files, link tools, test it, then save.</p>'
16938
- + '<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">'
16939
- + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for deploying to production — run tests, build, push, verify\\x27)">Deploy to prod</button>'
16940
- + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for writing a weekly status report from git commits and calendar\\x27)">Weekly status report</button>'
16941
- + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for onboarding a new team member — set up accounts, send welcome email\\x27)">Onboard team member</button>'
16942
- + '<button class="btn btn-sm quick-pill" onclick="builderQuick(\\x27Teach a skill for researching a company before a sales call\\x27)">Company research</button>'
16943
- + '</div>';
18772
+ function _setRunFooter(text) {
18773
+ var el = document.getElementById('builder-canvas-status');
18774
+ if (el) { el.textContent = text || ''; el.style.color = 'var(--text-muted)'; }
18775
+ }
18776
+
18777
+ function _findNodeElForStepId(stepId) {
18778
+ if (!_builderCanvasEditor) return null;
18779
+ var data = _builderCanvasEditor.export();
18780
+ var nodes = data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data ? data.drawflow.Home.data : {};
18781
+ for (var k in nodes) {
18782
+ var d = nodes[k].data || {};
18783
+ if (d.stepId === stepId) return document.querySelector('.drawflow-node[id="node-' + k + '"]');
16944
18784
  }
18785
+ return null;
16945
18786
  }
16946
18787
 
16947
- function updateBuilderMode() {
16948
- var type = (document.getElementById('builder-type') || {}).value || 'skill';
16949
- var title = document.getElementById('builder-page-title');
16950
- var drawer = document.getElementById('builder-skills-drawer');
18788
+ function _onRunEvent(evt) {
18789
+ if (!evt || !evt.runId || evt.workflowId !== _builderCanvasOpenId) return;
18790
+ if (evt.type === 'run:started') {
18791
+ _setRunFooter('Running test… (' + evt.runId.slice(0, 8) + ')');
18792
+ return;
18793
+ }
18794
+ if (evt.type === 'run:step-status') {
18795
+ var p = evt.payload || {};
18796
+ var nodeEl = _findNodeElForStepId(p.stepId);
18797
+ if (nodeEl) {
18798
+ nodeEl.classList.remove('cl-step-running', 'cl-step-done', 'cl-step-failed', 'cl-step-skipped', 'cl-step-cancelled', 'cl-step-timeout');
18799
+ nodeEl.classList.add('cl-step-' + p.status);
18800
+ }
18801
+ return;
18802
+ }
18803
+ if (evt.type === 'run:step-output') {
18804
+ var po = evt.payload || {};
18805
+ _builderRunStepResults[po.stepId] = po;
18806
+ return;
18807
+ }
18808
+ if (evt.type === 'run:completed' || evt.type === 'run:cancelled') {
18809
+ var ec = (evt.payload && evt.payload.status) || (evt.type === 'run:cancelled' ? 'cancelled' : 'ok');
18810
+ var dur = (evt.payload && evt.payload.durationMs) || 0;
18811
+ _builderActiveRunId = null;
18812
+ var cancel = document.getElementById('builder-canvas-cancel-btn');
18813
+ if (cancel) cancel.style.display = 'none';
18814
+ _setRunFooter('Test ' + ec + ' in ' + dur + 'ms — click a node for output');
18815
+ return;
18816
+ }
18817
+ }
16951
18818
 
16952
- if (type === 'skill') {
16953
- if (title) title.textContent = 'Skill Studio';
16954
- if (drawer) { drawer.style.display = ''; refreshBuilderSkills(); }
16955
- document.getElementById('builder-input').placeholder = 'Describe the skill you want to teach...';
16956
- } else {
16957
- if (title) title.textContent = 'Builder';
16958
- if (drawer) drawer.style.display = 'none';
16959
- document.getElementById('builder-input').placeholder = 'Describe what you want to build...';
18819
+ function showStepOutput(stepId) {
18820
+ var po = _builderRunStepResults[stepId];
18821
+ if (!po) { toast('No output for ' + stepId + ' yet', 'info'); return; }
18822
+ var lines = ['Step: ' + stepId, 'Status: ' + po.status, ''];
18823
+ if (po.error) lines.push('Error: ' + po.error, '');
18824
+ if (po.output != null) lines.push(typeof po.output === 'string' ? po.output : JSON.stringify(po.output, null, 2));
18825
+ alert(lines.join('\\n'));
18826
+ }
18827
+
18828
+ async function _populateMcpDropdowns() {
18829
+ try {
18830
+ if (!_builderMcpToolsCache) {
18831
+ // Fetch from the agent-side discovery — proxied through a simple endpoint
18832
+ // (we build a small client-side bridge by listing available servers via /api/builder/mcp-discovery)
18833
+ var r = await apiFetch('/api/builder/mcp-discovery');
18834
+ _builderMcpToolsCache = await r.json();
18835
+ }
18836
+ var serverSel = document.getElementById('cfg-mcp-server');
18837
+ var toolSel = document.getElementById('cfg-mcp-tool');
18838
+ if (!serverSel || !toolSel) return;
18839
+ var current = serverSel.value;
18840
+ var opts = '<option value="">— pick —</option>';
18841
+ var servers = (_builderMcpToolsCache && _builderMcpToolsCache.servers) || [];
18842
+ for (var i = 0; i < servers.length; i++) {
18843
+ var s = servers[i];
18844
+ opts += '<option value="' + esc(s.name) + '"' + (s.name === current ? ' selected' : '') + '>' + esc(s.name) + (s.enabled ? '' : ' (off)') + '</option>';
18845
+ }
18846
+ serverSel.innerHTML = opts;
18847
+ var rebuildTools = function() {
18848
+ var s = servers.filter(function(x) { return x.name === serverSel.value; })[0];
18849
+ var tools = (s && s.tools) || [];
18850
+ var curT = toolSel.value;
18851
+ var t = '<option value="">— pick —</option>';
18852
+ for (var j = 0; j < tools.length; j++) {
18853
+ t += '<option value="' + esc(tools[j]) + '"' + (tools[j] === curT ? ' selected' : '') + '>' + esc(tools[j]) + '</option>';
18854
+ }
18855
+ toolSel.innerHTML = t;
18856
+ };
18857
+ rebuildTools();
18858
+ serverSel.addEventListener('change', rebuildTools);
18859
+ } catch (err) {
18860
+ /* non-fatal — dropdowns just stay sparse */
16960
18861
  }
16961
18862
  }
16962
18863
 
@@ -17551,6 +19452,262 @@ function formatTokens(n) {
17551
19452
  return String(n);
17552
19453
  }
17553
19454
 
19455
+ function formatBytes(n) {
19456
+ if (n == null) return '—';
19457
+ if (n < 1024) return n + ' B';
19458
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
19459
+ if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB';
19460
+ return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
19461
+ }
19462
+
19463
+ async function memoryHealthAction(action) {
19464
+ var labels = { 'janitor': 'cleanup', 'rebuild-fts': 'FTS rebuild', 'fix-orphans': 'orphan fix' };
19465
+ if (!confirm('Run ' + (labels[action] || action) + ' now?')) return;
19466
+ try {
19467
+ var r = await apiJson('POST', '/api/memory/health/action', { action: action });
19468
+ if (r.error) { toast('Action failed: ' + r.error, 'error'); return; }
19469
+ var detail = '';
19470
+ if (r.result) detail = ' — ' + Object.entries(r.result).slice(0, 4).map(function(p) { return p[0] + ':' + p[1]; }).join(', ');
19471
+ if (r.report) detail = ' — orphans nulled: ' + (r.report.orphanRefsNulled || 0) + ', FTS rebuilds: ' + (r.report.ftsRebuilds || 0);
19472
+ toast(action + ' complete' + detail, 'success');
19473
+ refreshMemoryHealth();
19474
+ } catch (err) {
19475
+ toast('Action error: ' + err, 'error');
19476
+ }
19477
+ }
19478
+
19479
+ // ── Goals: inline create form ────────────────────────────────────
19480
+ function openNewGoalForm() {
19481
+ var el = document.getElementById('new-goal-form');
19482
+ if (!el) return;
19483
+ el.style.display = '';
19484
+ setTimeout(function() {
19485
+ var t = document.getElementById('new-goal-title');
19486
+ if (t) t.focus();
19487
+ }, 50);
19488
+ }
19489
+
19490
+ async function submitNewGoal() {
19491
+ var titleEl = document.getElementById('new-goal-title');
19492
+ var descEl = document.getElementById('new-goal-desc');
19493
+ var title = (titleEl?.value || '').trim();
19494
+ if (!title) { toast('Goal needs a title', 'error'); return; }
19495
+ try {
19496
+ var r = await apiJson('POST', '/api/goals', { title: title, description: (descEl?.value || '').trim() });
19497
+ if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
19498
+ if (titleEl) titleEl.value = '';
19499
+ if (descEl) descEl.value = '';
19500
+ document.getElementById('new-goal-form').style.display = 'none';
19501
+ toast('Goal created', 'success');
19502
+ if (typeof refreshGoals === 'function') refreshGoals();
19503
+ } catch (err) { toast('Create error: ' + err, 'error'); }
19504
+ }
19505
+
19506
+ // ── Workflows: open Builder for a brand-new workflow ─────────────
19507
+ function openBuilderForNewWorkflow() {
19508
+ navigateTo('builder');
19509
+ setTimeout(function() {
19510
+ var typeSel = document.getElementById('builder-type');
19511
+ if (typeSel) { typeSel.value = 'workflow'; updateBuilderMode(); }
19512
+ var name = prompt('Name your new workflow:');
19513
+ if (!name) return;
19514
+ apiJson('POST', '/api/builder/workflows', { name: name }).then(function(r) {
19515
+ if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
19516
+ if (r && r.id) {
19517
+ // Refresh picker, then open the new workflow
19518
+ refreshBuilderCanvasPicker('workflow');
19519
+ setTimeout(function() { openBuilderWorkflow(r.id); }, 200);
19520
+ }
19521
+ });
19522
+ }, 100);
19523
+ }
19524
+
19525
+ // ── Unleashed: open the start-task picker ────────────────────────
19526
+ function openStartUnleashedTask() {
19527
+ // Reuse the existing cron list — pick a cron job, run it in unleashed mode.
19528
+ // For v1 just route to Automations where the existing controls live.
19529
+ navigateTo('automations');
19530
+ toast('Pick a cron job and click "Run unleashed" — kicks off long-running mode.', 'info');
19531
+ }
19532
+
19533
+ async function refreshMemoryHealth() {
19534
+ var el = document.getElementById('memory-health-content');
19535
+ if (!el) return;
19536
+ try {
19537
+ var r = await apiFetch('/api/memory/health');
19538
+ var d = await r.json();
19539
+ if (!d.ok || !d.health) {
19540
+ el.innerHTML = '<div class="empty-state">' + esc(d.error || 'No data') + '</div>';
19541
+ return;
19542
+ }
19543
+ var h = d.health;
19544
+ var consolidatedPct = h.chunks.total > 0
19545
+ ? ((h.chunks.consolidated / h.chunks.total) * 100).toFixed(1)
19546
+ : '0.0';
19547
+ var zombiePct = h.chunks.total > 0
19548
+ ? ((h.chunks.zombieCount / h.chunks.total) * 100).toFixed(1)
19549
+ : '0.0';
19550
+
19551
+ var html = '';
19552
+
19553
+ // Hero tiles row.
19554
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:20px">';
19555
+ html += '<div class="metric-hero"><div class="metric-hero-value">' + (h.chunks.total || 0)
19556
+ + '</div><div class="metric-hero-label">Total Chunks</div>'
19557
+ + '<div class="metric-hero-sub">' + (h.chunks.pinned || 0) + ' pinned &middot; ' + (h.chunks.softDeleted || 0) + ' soft-deleted</div></div>';
19558
+ html += '<div class="metric-hero"><div class="metric-hero-value">' + consolidatedPct
19559
+ + '%</div><div class="metric-hero-label">Consolidated</div>'
19560
+ + '<div class="metric-hero-sub">' + (h.chunks.consolidated || 0) + ' of ' + (h.chunks.total || 0) + ' chunks</div></div>';
19561
+ html += '<div class="metric-hero"><div class="metric-hero-value">' + (h.chunks.zombieCount || 0)
19562
+ + '</div><div class="metric-hero-label">Zombies</div>'
19563
+ + '<div class="metric-hero-sub">' + zombiePct + '% of total &middot; eligible to expire</div></div>';
19564
+ html += '<div class="metric-hero"><div class="metric-hero-value">' + formatBytes(h.dbSizeBytes)
19565
+ + '</div><div class="metric-hero-label">DB File Size</div>'
19566
+ + '<div class="metric-hero-sub">last vacuum: ' + esc(h.lastVacuumAt || 'never') + '</div></div>';
19567
+ html += '</div>';
19568
+
19569
+ // Two-column layout: categories + table sizes on left, top cited on right.
19570
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">';
19571
+
19572
+ // Left column: categories.
19573
+ html += '<div class="card"><div class="card-header"><h3>Chunks by Category</h3></div><div class="card-body">';
19574
+ if (!h.chunksByCategory || h.chunksByCategory.length === 0) {
19575
+ html += '<div class="empty-state">No chunks yet.</div>';
19576
+ } else {
19577
+ html += '<table class="data-table"><thead><tr><th>Category</th><th style="text-align:right">Count</th></tr></thead><tbody>';
19578
+ for (var i = 0; i < h.chunksByCategory.length; i++) {
19579
+ var c = h.chunksByCategory[i];
19580
+ html += '<tr><td>' + esc(c.category || '—') + '</td><td style="text-align:right">' + c.count + '</td></tr>';
19581
+ }
19582
+ html += '</tbody></table>';
19583
+ }
19584
+ html += '</div></div>';
19585
+
19586
+ // Right column: top cited.
19587
+ html += '<div class="card"><div class="card-header"><h3>Top Cited (last 30d)</h3></div><div class="card-body">';
19588
+ if (!h.topCitedLast30d || h.topCitedLast30d.length === 0) {
19589
+ html += '<div class="empty-state">No outcomes recorded in the last 30 days.</div>';
19590
+ } else {
19591
+ html += '<table class="data-table"><thead><tr><th>Source</th><th>Section</th><th style="text-align:right">Refs</th></tr></thead><tbody>';
19592
+ for (var j = 0; j < h.topCitedLast30d.length; j++) {
19593
+ var t = h.topCitedLast30d[j];
19594
+ html += '<tr><td>' + esc(t.sourceFile || '—') + '</td><td>' + esc(t.section || '—')
19595
+ + '</td><td style="text-align:right">' + t.refCount + '</td></tr>';
19596
+ }
19597
+ html += '</tbody></table>';
19598
+ }
19599
+ html += '</div></div>';
19600
+
19601
+ html += '</div>';
19602
+
19603
+ // Staleness section — high-salience drift + user-model age.
19604
+ var staleSlots = h.staleUserModelSlots || [];
19605
+ var staleChunks = h.staleHighSalienceChunks || [];
19606
+ if (staleSlots.length > 0 || staleChunks.length > 0) {
19607
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">';
19608
+
19609
+ html += '<div class="card"><div class="card-header"><h3>Stale User-Model Slots</h3></div><div class="card-body">';
19610
+ if (staleSlots.length === 0) {
19611
+ html += '<div class="empty-state">All slots fresh.</div>';
19612
+ } else {
19613
+ html += '<table class="data-table"><thead><tr><th>Slot</th><th>Agent</th><th style="text-align:right">Age (days)</th></tr></thead><tbody>';
19614
+ for (var ss = 0; ss < staleSlots.length; ss++) {
19615
+ var s = staleSlots[ss];
19616
+ html += '<tr><td>' + esc(s.slot) + '</td><td>' + esc(s.agentSlug || 'global') + '</td><td style="text-align:right">' + s.ageDays + '</td></tr>';
19617
+ }
19618
+ html += '</tbody></table>';
19619
+ }
19620
+ html += '</div></div>';
19621
+
19622
+ html += '<div class="card"><div class="card-header"><h3>Stale High-Salience Chunks</h3><div style="font-size:11px;color:var(--text-muted)">High salience but EMA gone negative &mdash; ranked but not cited</div></div><div class="card-body">';
19623
+ if (staleChunks.length === 0) {
19624
+ html += '<div class="empty-state">No drift detected.</div>';
19625
+ } else {
19626
+ html += '<table class="data-table"><thead><tr><th>Source</th><th>Section</th><th style="text-align:right">Salience</th><th style="text-align:right">EMA</th></tr></thead><tbody>';
19627
+ for (var sc = 0; sc < staleChunks.length; sc++) {
19628
+ var sk = staleChunks[sc];
19629
+ html += '<tr><td>' + esc(sk.sourceFile || '') + '</td><td>' + esc(sk.section || '') + '</td><td style="text-align:right">' + sk.salience.toFixed(2) + '</td><td style="text-align:right">' + sk.lastOutcomeScore.toFixed(2) + '</td></tr>';
19630
+ }
19631
+ html += '</tbody></table>';
19632
+ }
19633
+ html += '</div></div>';
19634
+ html += '</div>';
19635
+ }
19636
+
19637
+ // Cache + write queue + integrity row.
19638
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin-top:16px">';
19639
+
19640
+ if (h.chunkCacheStats) {
19641
+ var cs = h.chunkCacheStats;
19642
+ var hitRatePct = (cs.hitRate * 100).toFixed(1);
19643
+ html += '<div class="card"><div class="card-header"><h3>Hot Chunk Cache</h3></div><div class="card-body">';
19644
+ html += '<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:13px">';
19645
+ html += '<div><strong>' + hitRatePct + '%</strong> hit rate</div>';
19646
+ html += '<div><strong>' + cs.hits + '</strong> hits</div>';
19647
+ html += '<div><strong>' + cs.misses + '</strong> misses</div>';
19648
+ html += '<div><strong>' + cs.size + ' / ' + cs.capacity + '</strong> entries</div>';
19649
+ html += '<div><strong>' + cs.evictions + '</strong> evictions</div>';
19650
+ html += '</div></div></div>';
19651
+ }
19652
+
19653
+ // Async write queue status.
19654
+ var wq = h.writeQueue;
19655
+ html += '<div class="card"><div class="card-header"><h3>Async Write Queue</h3></div><div class="card-body">';
19656
+ if (!wq) {
19657
+ html += '<div style="font-size:13px;color:var(--text-muted)">Sync mode (queue disabled)</div>';
19658
+ } else {
19659
+ html += '<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:13px">';
19660
+ html += '<div><strong>' + wq.size + '</strong> pending</div>';
19661
+ html += '<div><strong>' + wq.dropped + '</strong> dropped (back-pressure)</div>';
19662
+ html += '</div>';
19663
+ }
19664
+ html += '</div></div>';
19665
+
19666
+ // Integrity report.
19667
+ var ir = h.lastIntegrityReport;
19668
+ html += '<div class="card"><div class="card-header"><h3>Integrity</h3></div><div class="card-body">';
19669
+ if (!ir) {
19670
+ html += '<div style="font-size:13px;color:var(--text-muted)">No probes have run yet.</div>';
19671
+ } else {
19672
+ var ftsLabel = ir.ftsOk
19673
+ ? '<span style="color:var(--green,#3a3)">ok</span>'
19674
+ : (ir.ftsRebuilt ? '<span style="color:var(--orange,#f80)">rebuilt</span>' : '<span style="color:var(--red,#c33)">failing</span>');
19675
+ html += '<div style="display:flex;gap:18px;flex-wrap:wrap;font-size:13px">';
19676
+ html += '<div>FTS5: ' + ftsLabel + '</div>';
19677
+ html += '<div><strong>' + ir.orphanRefsNulled + '</strong> orphan refs nulled</div>';
19678
+ html += '<div><strong>' + ir.missingEmbeddings + '</strong> missing embeddings</div>';
19679
+ html += '<div style="color:var(--text-muted)">last: ' + esc(ir.ranAt || '') + '</div>';
19680
+ html += '</div>';
19681
+ }
19682
+ html += '</div></div>';
19683
+
19684
+ html += '</div>';
19685
+
19686
+ // Table row counts (full width).
19687
+ html += '<div class="card" style="margin-top:16px"><div class="card-header"><h3>Table Sizes</h3></div><div class="card-body">';
19688
+ var tableRows = Object.keys(h.tableRowCounts || {}).sort(function(a, b) {
19689
+ return (h.tableRowCounts[b] || 0) - (h.tableRowCounts[a] || 0);
19690
+ });
19691
+ if (tableRows.length === 0) {
19692
+ html += '<div class="empty-state">No table data.</div>';
19693
+ } else {
19694
+ html += '<table class="data-table"><thead><tr><th>Table</th><th style="text-align:right">Rows</th></tr></thead><tbody>';
19695
+ for (var k = 0; k < tableRows.length; k++) {
19696
+ var tn = tableRows[k];
19697
+ var rowCount = h.tableRowCounts[tn];
19698
+ var label = rowCount === -1 ? '<span style="color:var(--text-muted)">missing</span>' : String(rowCount);
19699
+ html += '<tr><td>' + esc(tn) + '</td><td style="text-align:right">' + label + '</td></tr>';
19700
+ }
19701
+ html += '</tbody></table>';
19702
+ }
19703
+ html += '</div></div>';
19704
+
19705
+ el.innerHTML = html;
19706
+ } catch (err) {
19707
+ el.innerHTML = '<div class="empty-state">Failed to load: ' + esc(String(err)) + '</div>';
19708
+ }
19709
+ }
19710
+
17554
19711
  async function refreshMetrics() {
17555
19712
  try {
17556
19713
  const [r, ur] = await Promise.all([apiFetch('/api/metrics'), apiFetch('/api/metrics/usage')]);
@@ -18088,6 +20245,131 @@ async function saveDiscordSetup() {
18088
20245
  } catch(e) { toast(String(e), 'error'); }
18089
20246
  }
18090
20247
 
20248
+ function toggleHomeRail() {
20249
+ var rail = document.getElementById('home-rail');
20250
+ if (!rail) return;
20251
+ // Mobile: open/close. Desktop: collapse/show.
20252
+ if (window.matchMedia('(max-width: 1024px)').matches) {
20253
+ rail.classList.toggle('open');
20254
+ } else {
20255
+ rail.classList.toggle('collapsed');
20256
+ }
20257
+ }
20258
+
20259
+ async function refreshHomeRail() {
20260
+ // Daemon status
20261
+ try {
20262
+ var rs = await apiFetch('/api/status');
20263
+ var ds = await rs.json();
20264
+ var pip = document.querySelector('#rail-daemon-body .agent-activity-dot');
20265
+ var label = document.querySelector('#rail-daemon-body .agent-activity span:last-child');
20266
+ if (label) label.textContent = ds.running ? 'Daemon running' : 'Daemon stopped';
20267
+ if (pip) pip.style.background = ds.running ? '#22c55e' : '#ef4444';
20268
+ var up = document.getElementById('rail-daemon-uptime');
20269
+ if (up && ds.uptimeMs) up.textContent = Math.round(ds.uptimeMs / 60000) + 'm';
20270
+ } catch { /* */ }
20271
+
20272
+ // Today's plan (compact)
20273
+ try {
20274
+ var rp = await apiFetch('/api/daily-plan');
20275
+ var dp = await rp.json();
20276
+ var planEl = document.getElementById('home-plan-content');
20277
+ if (planEl) {
20278
+ if (!dp || !dp.plan) {
20279
+ planEl.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No plan yet today.</div>';
20280
+ } else {
20281
+ var items = (dp.plan.items || []).slice(0, 4);
20282
+ if (items.length === 0) {
20283
+ planEl.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">No items in today\\x27s plan.</div>';
20284
+ } else {
20285
+ planEl.innerHTML = items.map(function(it) {
20286
+ return '<div class="rail-row"><span class="label">' + esc(it.title || it.text || '') + '</span><span class="meta">' + esc(it.time || '') + '</span></div>';
20287
+ }).join('');
20288
+ }
20289
+ }
20290
+ }
20291
+ } catch {
20292
+ var pe = document.getElementById('home-plan-content');
20293
+ if (pe) pe.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No plan available.</div>';
20294
+ }
20295
+
20296
+ // Upcoming cron fires (next 3)
20297
+ try {
20298
+ var rc = await apiFetch('/api/cron');
20299
+ var dc = await rc.json();
20300
+ var jobs = (dc.jobs || []).filter(function(j) { return j.enabled && j.nextRun; });
20301
+ jobs.sort(function(a, b) { return new Date(a.nextRun).getTime() - new Date(b.nextRun).getTime(); });
20302
+ var top = jobs.slice(0, 3);
20303
+ var ue = document.getElementById('rail-upcoming');
20304
+ var uc = document.getElementById('rail-upcoming-count');
20305
+ if (uc) uc.textContent = String(jobs.length);
20306
+ if (ue) {
20307
+ ue.innerHTML = top.length ? top.map(function(j) {
20308
+ return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})"><span class="label">' + esc(j.name) + '</span><span class="meta">' + esc(timeUntil(j.nextRun)) + '</span></div>';
20309
+ }).join('') : '<div style="font-size:11px;color:var(--text-muted)">Nothing scheduled soon.</div>';
20310
+ }
20311
+ } catch { /* */ }
20312
+
20313
+ // Active unleashed runs
20314
+ try {
20315
+ var ru = await apiFetch('/api/unleashed');
20316
+ var du = await ru.json();
20317
+ var active = (du.tasks || []).filter(function(t) { return t.status === 'running'; });
20318
+ var ae = document.getElementById('rail-active');
20319
+ var ac = document.getElementById('rail-active-count');
20320
+ if (ac) {
20321
+ if (active.length > 0) { ac.style.display = ''; ac.textContent = String(active.length); }
20322
+ else ac.style.display = 'none';
20323
+ }
20324
+ if (ae) {
20325
+ ae.innerHTML = active.length ? active.map(function(t) {
20326
+ return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27build\\x27,{tab:\\x27workflows\\x27})"><span class="label">' + esc(t.name) + '</span><span class="meta">' + esc(t.phase || '') + '</span></div>';
20327
+ }).join('') : '<div style="font-size:11px;color:var(--text-muted)">Nothing running.</div>';
20328
+ }
20329
+ } catch { /* */ }
20330
+
20331
+ // Time saved (rough: cron runs * 5min + activity exchanges * 2min, this week)
20332
+ try {
20333
+ var rm = await apiFetch('/api/metrics?period=week');
20334
+ var dm = await rm.json();
20335
+ var minutes = ((dm.cronRuns || 0) * 5) + ((dm.exchanges || 0) * 2);
20336
+ var ts = document.getElementById('rail-time-saved');
20337
+ if (ts) {
20338
+ if (minutes >= 60) ts.innerHTML = '<div style="font-size:18px;font-weight:600">' + (minutes / 60).toFixed(1) + 'h</div><div style="font-size:11px;color:var(--text-muted)">across ' + (dm.cronRuns || 0) + ' cron runs + ' + (dm.exchanges || 0) + ' chats</div>';
20339
+ else ts.innerHTML = '<div style="font-size:18px;font-weight:600">' + minutes + 'm</div><div style="font-size:11px;color:var(--text-muted)">across ' + (dm.cronRuns || 0) + ' cron runs</div>';
20340
+ }
20341
+ } catch { /* */ }
20342
+
20343
+ // Approvals (self-improve proposals + pending skills)
20344
+ try {
20345
+ var rsi = await apiFetch('/api/self-improve');
20346
+ var dsi = await rsi.json();
20347
+ var pending = (dsi.proposals || []).filter(function(p) { return p.status === 'pending'; });
20348
+ var ae2 = document.getElementById('rail-approvals');
20349
+ var ac2 = document.getElementById('rail-approvals-count');
20350
+ if (ac2) {
20351
+ if (pending.length > 0) { ac2.style.display = ''; ac2.textContent = String(pending.length); }
20352
+ else ac2.style.display = 'none';
20353
+ }
20354
+ if (ae2) {
20355
+ if (pending.length === 0) ae2.innerHTML = '<div style="font-size:11px;color:var(--text-muted)">Nothing pending.</div>';
20356
+ else ae2.innerHTML = pending.slice(0, 3).map(function(p) {
20357
+ return '<div class="rail-row clickable-row" onclick="navigateTo(\\x27brain\\x27,{tab:\\x27learning\\x27})"><span class="label">' + esc(p.area || 'proposal') + ': ' + esc((p.target || '').slice(0, 40)) + '</span><span class="meta">' + esc(((p.score || 0) * 100).toFixed(0)) + '%</span></div>';
20358
+ }).join('');
20359
+ }
20360
+ } catch { /* */ }
20361
+ }
20362
+
20363
+ function timeUntil(iso) {
20364
+ if (!iso) return '';
20365
+ var ms = new Date(iso).getTime() - Date.now();
20366
+ if (ms < 0) return 'past';
20367
+ var min = Math.round(ms / 60000);
20368
+ if (min < 60) return 'in ' + min + 'm';
20369
+ if (min < 24 * 60) return 'in ' + Math.round(min / 60) + 'h';
20370
+ return 'in ' + Math.round(min / (24 * 60)) + 'd';
20371
+ }
20372
+
18091
20373
  async function refreshAll() {
18092
20374
  // Use batch init for core data — avoids concurrent requests that freeze the event loop
18093
20375
  try {
@@ -18096,6 +20378,8 @@ async function refreshAll() {
18096
20378
  if (d.status) refreshStatus(d.status);
18097
20379
  if (d.activity) refreshActivity(false, d.activity);
18098
20380
  if (d.office) refreshTeamNav(d.office);
20381
+ // Home rail data — fire and forget, doesn't block init render.
20382
+ if (currentPage === 'home') refreshHomeRail();
18099
20383
  if (d.version) {
18100
20384
  if (d.version.needsRestart && !_restartBannerShown) {
18101
20385
  _restartBannerShown = true;
@@ -21465,6 +23749,9 @@ try {
21465
23749
  toast('Daemon restarted \u2014 refreshing data...', 'info');
21466
23750
  setTimeout(function() { refreshAll(); }, 1500);
21467
23751
  }
23752
+ if (evt.type === 'builder') {
23753
+ try { _handleBuilderEvent(evt.data); } catch(e) { /* non-fatal */ }
23754
+ }
21468
23755
  if (evt.type === 'deep_result') {
21469
23756
  try {
21470
23757
  var container = document.getElementById('chat-messages');
@@ -21573,6 +23860,14 @@ function getLoginPageHTML() {
21573
23860
  margin-top: 24px; font-size: 12px;
21574
23861
  color: #475569; text-align: center; line-height: 1.5;
21575
23862
  }
23863
+ .remember-row {
23864
+ display: flex; align-items: center; gap: 8px;
23865
+ margin: 16px 0; font-size: 13px; color: #94a3b8;
23866
+ cursor: pointer; user-select: none;
23867
+ }
23868
+ .remember-row input[type="checkbox"] {
23869
+ accent-color: #f97316; width: 14px; height: 14px; cursor: pointer;
23870
+ }
21576
23871
  </style>
21577
23872
  </head>
21578
23873
  <body>
@@ -21586,6 +23881,10 @@ function getLoginPageHTML() {
21586
23881
  <input type="password" class="form-input" id="token-input"
21587
23882
  placeholder="clem_XXXX-XXXX-XXXX" autocomplete="off" autofocus>
21588
23883
  </div>
23884
+ <label class="remember-row">
23885
+ <input type="checkbox" id="remember-input">
23886
+ <span>Remember me on this device for 30 days</span>
23887
+ </label>
21589
23888
  <button type="submit" class="btn-login" id="login-btn">Sign In</button>
21590
23889
  <div class="error-msg" id="error-msg"></div>
21591
23890
  </form>
@@ -21600,6 +23899,7 @@ function getLoginPageHTML() {
21600
23899
  var btn = document.getElementById('login-btn');
21601
23900
  var err = document.getElementById('error-msg');
21602
23901
  var token = document.getElementById('token-input').value.trim();
23902
+ var remember = document.getElementById('remember-input').checked;
21603
23903
  if (!token) return;
21604
23904
  btn.disabled = true;
21605
23905
  btn.textContent = 'Signing in...';
@@ -21608,7 +23908,7 @@ function getLoginPageHTML() {
21608
23908
  var r = await fetch('/auth/login', {
21609
23909
  method: 'POST',
21610
23910
  headers: { 'Content-Type': 'application/json' },
21611
- body: JSON.stringify({ token: token })
23911
+ body: JSON.stringify({ token: token, remember: remember })
21612
23912
  });
21613
23913
  var d = await r.json();
21614
23914
  if (d.ok) {