personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename live app to health; move event viewer to dev; add log tail, dream, sync

- Rename apps/live/ to apps/health/ with stethoscope icon and "Health" label
- Move callosum event viewer (JSON dump, pause/resume, 100-event cap) to dev app
- Replace events card with real-time service log tail grouped by service
- Add dream processing card (hidden when idle, shows progress)
- Add sync upload card (hidden when idle, shows queue/state)
- Add queues and schedules display to vitals bar
- Fix dream card visibility on mid-run page loads
- Update docs/APPS.md minimal app reference

+591 -131
+172 -1
apps/dev/workspace.html
··· 142 142 letter-spacing: 0.05em; 143 143 margin-top: 0.25rem; 144 144 } 145 + 146 + /* Events Card */ 147 + .events-card { 148 + background: white; 149 + border-radius: 12px; 150 + padding: 1.5em; 151 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); 152 + } 153 + 154 + .events-card.paused { 155 + border-left: 4px solid #f59e0b; 156 + } 157 + 158 + .events-header { 159 + display: flex; 160 + justify-content: space-between; 161 + align-items: center; 162 + margin-bottom: 1em; 163 + } 164 + 165 + .events-title { 166 + font-size: 1.1em; 167 + font-weight: 600; 168 + } 169 + 170 + .events-controls { 171 + display: flex; 172 + align-items: center; 173 + gap: 1em; 174 + } 175 + 176 + .pause-btn { 177 + background: #3b82f6; 178 + color: white; 179 + border: none; 180 + padding: 0.5em 1em; 181 + border-radius: 6px; 182 + cursor: pointer; 183 + font-size: 0.85em; 184 + font-weight: 500; 185 + display: flex; 186 + align-items: center; 187 + gap: 0.4em; 188 + transition: background 0.2s; 189 + } 190 + 191 + .pause-btn:hover { 192 + background: #2563eb; 193 + } 194 + 195 + .pause-btn.paused { 196 + background: #f59e0b; 197 + } 198 + 199 + .pause-btn.paused:hover { 200 + background: #d97706; 201 + } 202 + 203 + .missed-count { 204 + font-size: 0.85em; 205 + color: #f59e0b; 206 + font-weight: 500; 207 + } 208 + 209 + .event-log { 210 + height: 400px; 211 + overflow-y: auto; 212 + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; 213 + font-size: 12px; 214 + border: 1px solid #e5e7eb; 215 + border-radius: 6px; 216 + padding: 0.5em; 217 + background: #f9fafb; 218 + } 219 + 220 + .event-line { 221 + white-space: nowrap; 222 + overflow: hidden; 223 + text-overflow: ellipsis; 224 + padding: 3px 0; 225 + border-bottom: 1px solid #f3f4f6; 226 + color: #374151; 227 + } 228 + 229 + .event-line:last-child { 230 + border-bottom: none; 231 + } 232 + 233 + .event-line.error { 234 + color: #dc2626; 235 + background: #fef2f2; 236 + } 237 + 238 + .event-log:empty::before { 239 + content: 'Waiting for events...'; 240 + color: #9ca3af; 241 + font-style: italic; 242 + } 145 243 </style> 146 244 147 245 <div class="dev-container"> 148 - <h1>🛠️ Dev Tools - Notification Testing</h1> 246 + <h1>🛠️ Dev Tools</h1> 149 247 150 248 <!-- Quick Tests --> 151 249 <div class="dev-section"> ··· 239 337 <h2>Console Log</h2> 240 338 <p>Notification IDs and events</p> 241 339 <div class="dev-output" id="console"></div> 340 + </div> 341 + 342 + <!-- Callosum Event Viewer --> 343 + <div class="dev-section"> 344 + <h2>Callosum Event Viewer</h2> 345 + <p>Raw callosum event stream with pause/resume</p> 346 + <div class="events-card" id="eventsCard"> 347 + <div class="events-header"> 348 + <div class="events-title">EVENTS</div> 349 + <div class="events-controls"> 350 + <span class="missed-count" id="missedCount" style="display: none;"></span> 351 + <button class="pause-btn" id="pauseBtn"> 352 + <span>||</span> 353 + <span>Pause</span> 354 + </button> 355 + </div> 356 + </div> 357 + <div id="eventLog" class="event-log"></div> 358 + </div> 242 359 </div> 243 360 </div> 244 361 ··· 497 614 facetSelect.appendChild(option); 498 615 }); 499 616 } 617 + 618 + // Callosum Event Viewer 619 + (function() { 620 + const eventLog = document.getElementById('eventLog'); 621 + const eventsCard = document.getElementById('eventsCard'); 622 + const pauseBtn = document.getElementById('pauseBtn'); 623 + const missedCountEl = document.getElementById('missedCount'); 624 + let paused = false; 625 + let missed = 0; 626 + 627 + function togglePause() { 628 + paused = !paused; 629 + if (paused) { 630 + pauseBtn.classList.add('paused'); 631 + pauseBtn.innerHTML = '<span>|></span><span>Resume</span>'; 632 + eventsCard.classList.add('paused'); 633 + missed = 0; 634 + missedCountEl.style.display = 'none'; 635 + } else { 636 + pauseBtn.classList.remove('paused'); 637 + pauseBtn.innerHTML = '<span>||</span><span>Pause</span>'; 638 + eventsCard.classList.remove('paused'); 639 + missed = 0; 640 + missedCountEl.style.display = 'none'; 641 + } 642 + } 643 + 644 + function appendEvent(msg) { 645 + if (paused) { 646 + missed++; 647 + missedCountEl.textContent = missed + ' missed'; 648 + missedCountEl.style.display = 'inline'; 649 + return; 650 + } 651 + 652 + const line = document.createElement('div'); 653 + line.className = 'event-line'; 654 + if (msg.event === 'error' || (msg.event === 'exit' && msg.exit_code !== 0)) { 655 + line.className = 'event-line error'; 656 + } 657 + line.textContent = JSON.stringify(msg); 658 + eventLog.appendChild(line); 659 + eventLog.scrollTop = eventLog.scrollHeight; 660 + 661 + while (eventLog.children.length > 100) { 662 + eventLog.removeChild(eventLog.firstChild); 663 + } 664 + } 665 + 666 + pauseBtn.addEventListener('click', togglePause); 667 + if (window.appEvents) { 668 + window.appEvents.listen('*', appendEvent); 669 + } 670 + })(); 500 671 501 672 // Initial log and setup 502 673 populateFacetDropdown();
+5
apps/health/app.json
··· 1 + { 2 + "icon": "🩺", 3 + "label": "Health", 4 + "facets": {"disabled": true} 5 + }
-5
apps/live/app.json
··· 1 - { 2 - "icon": "⚡", 3 - "label": "Live Events", 4 - "facets": {"disabled": true} 5 - }
+413 -124
apps/live/workspace.html apps/health/workspace.html
··· 1 1 <style> 2 - /* Live Dashboard Layout */ 3 - .live-dashboard { 2 + /* Health Dashboard Layout */ 3 + .health-dashboard { 4 4 padding: 1em; 5 5 max-width: 1400px; 6 6 margin: 0 auto; ··· 126 126 gap: 0.5em; 127 127 } 128 128 129 - .live-badge { 129 + .health-badge { 130 130 background: #dc2626; 131 131 color: white; 132 132 padding: 0.2em 0.6em; ··· 138 138 animation: pulse 2s infinite; 139 139 } 140 140 141 - .live-badge.idle { 141 + .health-badge.idle { 142 142 background: #6b7280; 143 143 animation: none; 144 144 } 145 145 146 - .live-badge.tmux { 146 + .health-badge.tmux { 147 147 background: #8b5cf6; 148 148 } 149 149 ··· 365 365 transition: width 0.3s ease; 366 366 } 367 367 368 - /* Events Card */ 369 - .events-card { 368 + /* Service Logs */ 369 + .logs-card { 370 370 background: white; 371 371 border-radius: 12px; 372 372 padding: 1.5em; 373 373 box-shadow: 0 2px 8px rgba(0,0,0,0.1); 374 374 } 375 375 376 - .events-card.paused { 377 - border-left: 4px solid #f59e0b; 378 - } 379 - 380 - .events-header { 376 + .logs-header { 381 377 display: flex; 382 378 justify-content: space-between; 383 379 align-items: center; 384 380 margin-bottom: 1em; 385 381 } 386 382 387 - .events-title { 383 + .logs-title { 388 384 font-size: 1.1em; 389 385 font-weight: 600; 390 386 } 391 387 392 - .events-controls { 388 + .logs-controls { 393 389 display: flex; 394 390 align-items: center; 395 - gap: 1em; 391 + gap: 0.75em; 396 392 } 397 393 398 - .pause-btn { 394 + .logs-controls select { 395 + padding: 0.3em 0.5em; 396 + border: 1px solid #d1d5db; 397 + border-radius: 4px; 398 + font-size: 0.8em; 399 + background: white; 400 + } 401 + 402 + .logs-controls button { 403 + padding: 0.3em 0.6em; 404 + border: 1px solid #d1d5db; 405 + border-radius: 4px; 406 + background: white; 407 + cursor: pointer; 408 + font-size: 0.8em; 409 + } 410 + 411 + .logs-controls button.active { 399 412 background: #3b82f6; 400 413 color: white; 401 - border: none; 402 - padding: 0.5em 1em; 414 + border-color: #3b82f6; 415 + } 416 + 417 + .logs-viewport { 418 + height: 400px; 419 + overflow-y: auto; 420 + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; 421 + font-size: 12px; 422 + border: 1px solid #e5e7eb; 403 423 border-radius: 6px; 404 - cursor: pointer; 405 - font-size: 0.85em; 406 - font-weight: 500; 424 + padding: 0.5em; 425 + background: #1e1e1e; 426 + color: #d4d4d4; 427 + } 428 + 429 + .logs-viewport:empty::before { 430 + content: 'Waiting for log output...'; 431 + color: #6b7280; 432 + font-style: italic; 433 + } 434 + 435 + .logs-service-header { 436 + color: #6b7280; 437 + padding: 0.5em 0 0.2em; 438 + font-size: 11px; 439 + letter-spacing: 0.5px; 440 + } 441 + 442 + .logs-service-header:first-child { 443 + padding-top: 0; 444 + } 445 + 446 + .logs-line { 447 + white-space: pre-wrap; 448 + word-break: break-all; 449 + padding: 1px 0; 450 + line-height: 1.4; 451 + } 452 + 453 + .logs-line.stderr { 454 + color: #f87171; 455 + } 456 + 457 + .logs-line.log { 458 + color: #a78bfa; 459 + } 460 + 461 + /* Dream Card */ 462 + .dream-card { 463 + background: white; 464 + border-radius: 12px; 465 + padding: 1.5em; 466 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); 467 + border-left: 4px solid #f59e0b; 468 + } 469 + 470 + .dream-card.hidden { 471 + display: none; 472 + } 473 + 474 + .dream-info { 475 + display: flex; 476 + flex-wrap: wrap; 477 + gap: 1.5em; 478 + margin-bottom: 1em; 479 + font-size: 0.9em; 480 + } 481 + 482 + .dream-info-item { 407 483 display: flex; 408 - align-items: center; 409 - gap: 0.4em; 410 - transition: background 0.2s; 484 + flex-direction: column; 485 + gap: 0.2em; 411 486 } 412 487 413 - .pause-btn:hover { 414 - background: #2563eb; 488 + .dream-info-label { 489 + font-size: 0.8em; 490 + color: #6b7280; 491 + text-transform: uppercase; 492 + letter-spacing: 0.5px; 493 + } 494 + 495 + .dream-progress { 496 + margin-bottom: 0.5em; 497 + } 498 + 499 + .dream-progress-label { 500 + font-size: 0.85em; 501 + color: #374151; 502 + margin-bottom: 0.3em; 415 503 } 416 504 417 - .pause-btn.paused { 418 - background: #f59e0b; 505 + .dream-progress-bar { 506 + width: 100%; 507 + height: 6px; 508 + background: #e5e7eb; 509 + border-radius: 3px; 510 + overflow: hidden; 419 511 } 420 512 421 - .pause-btn.paused:hover { 422 - background: #d97706; 513 + .dream-progress-fill { 514 + height: 100%; 515 + background: #f59e0b; 516 + transition: width 0.3s ease; 423 517 } 424 518 425 - .missed-count { 519 + .dream-agents { 426 520 font-size: 0.85em; 427 - color: #f59e0b; 428 - font-weight: 500; 521 + color: #6b7280; 429 522 } 430 523 431 - .event-log { 432 - height: 400px; 433 - overflow-y: auto; 434 - font-family: 'Monaco', 'Menlo', 'Consolas', monospace; 435 - font-size: 12px; 436 - border: 1px solid #e5e7eb; 437 - border-radius: 6px; 438 - padding: 0.5em; 439 - background: #f9fafb; 524 + /* Sync Card */ 525 + .sync-card { 526 + background: white; 527 + border-radius: 12px; 528 + padding: 1.5em; 529 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); 530 + border-left: 4px solid #06b6d4; 440 531 } 441 532 442 - .event-line { 443 - white-space: nowrap; 444 - overflow: hidden; 445 - text-overflow: ellipsis; 446 - padding: 3px 0; 447 - border-bottom: 1px solid #f3f4f6; 448 - color: #374151; 533 + .sync-card.hidden { 534 + display: none; 449 535 } 450 536 451 - .event-line:last-child { 452 - border-bottom: none; 537 + .sync-info { 538 + display: flex; 539 + flex-wrap: wrap; 540 + gap: 1.5em; 541 + font-size: 0.9em; 453 542 } 454 543 455 - .event-line.error { 456 - color: #dc2626; 457 - background: #fef2f2; 544 + .sync-info-item { 545 + display: flex; 546 + flex-direction: column; 547 + gap: 0.2em; 458 548 } 459 549 460 - .event-log:empty::before { 461 - content: 'Waiting for events...'; 462 - color: #9ca3af; 463 - font-style: italic; 550 + .sync-info-label { 551 + font-size: 0.8em; 552 + color: #6b7280; 553 + text-transform: uppercase; 554 + letter-spacing: 0.5px; 464 555 } 465 556 466 557 /* Stale heartbeats list */ ··· 468 559 font-size: 0.85em; 469 560 opacity: 0.9; 470 561 } 562 + 563 + .vitals-chips { 564 + display: flex; 565 + gap: 0.3em; 566 + flex-wrap: wrap; 567 + } 568 + 569 + .vitals-chip { 570 + background: rgba(255,255,255,0.2); 571 + padding: 0.15em 0.5em; 572 + border-radius: 10px; 573 + font-size: 0.8em; 574 + } 471 575 </style> 472 576 473 - <div class="live-dashboard"> 577 + <div class="health-dashboard"> 474 578 <!-- Vitals Bar --> 475 579 <div class="vitals-bar"> 476 580 <div class="vitals-header"> ··· 512 616 <span>Waiting for status...</span> 513 617 </div> 514 618 </div> 619 + <div class="vitals-section" id="queuesSection" style="display: none;"> 620 + <div class="vitals-label">Queues</div> 621 + <div class="vitals-value" id="queuesValue"></div> 622 + </div> 623 + <div class="vitals-section" id="schedulesSection" style="display: none;"> 624 + <div class="vitals-label">Schedules</div> 625 + <div class="vitals-value" id="schedulesValue"></div> 626 + </div> 515 627 </div> 516 628 </div> 517 629 ··· 521 633 <div class="card-title"> 522 634 OBSERVATION STATUS 523 635 </div> 524 - <div class="live-badge idle" id="observeModeBadge"> 636 + <div class="health-badge idle" id="observeModeBadge"> 525 637 <span id="observeModeLabel">Waiting...</span> 526 638 </div> 527 639 </div> ··· 588 700 </div> 589 701 </div> 590 702 591 - <!-- Events Card --> 592 - <div class="events-card" id="eventsCard"> 593 - <div class="events-header"> 594 - <div class="events-title">EVENTS</div> 595 - <div class="events-controls"> 596 - <span class="missed-count" id="missedCount" style="display: none;"></span> 597 - <button class="pause-btn" id="pauseBtn"> 598 - <span>||</span> 599 - <span>Pause</span> 600 - </button> 703 + <!-- Dream Card (hidden when idle) --> 704 + <div class="dream-card hidden" id="dreamCard"> 705 + <div class="card-header"> 706 + <div class="card-title">DREAM PROCESSING</div> 707 + </div> 708 + <div class="dream-info" id="dreamInfo"></div> 709 + <div id="dreamProgress"></div> 710 + <div class="dream-agents" id="dreamAgents"></div> 711 + </div> 712 + 713 + <!-- Sync Card (hidden when idle) --> 714 + <div class="sync-card hidden" id="syncCard"> 715 + <div class="card-header"> 716 + <div class="card-title">SYNC</div> 717 + </div> 718 + <div class="sync-info" id="syncInfo"></div> 719 + </div> 720 + 721 + <!-- Service Logs --> 722 + <div class="logs-card"> 723 + <div class="logs-header"> 724 + <div class="logs-title">SERVICE LOGS</div> 725 + <div class="logs-controls"> 726 + <select id="logServiceFilter"> 727 + <option value="all">All Services</option> 728 + </select> 729 + <select id="logStreamFilter"> 730 + <option value="all">All Streams</option> 731 + <option value="stdout">stdout</option> 732 + <option value="stderr">stderr</option> 733 + <option value="log">log</option> 734 + </select> 735 + <button id="logFollowBtn" class="active">Follow</button> 736 + <button id="logClearBtn">Clear</button> 601 737 </div> 602 738 </div> 603 - <div id="eventLog" class="event-log"></div> 739 + <div class="logs-viewport" id="logsViewport"></div> 604 740 </div> 605 741 </div> 606 742 ··· 612 748 crashed: new Map(), // Crashed services (separate from running) 613 749 tasks: [], 614 750 health: null, 751 + queues: {}, // From supervisor.status 752 + schedules: [], // From supervisor.status 615 753 agents: new Map(), 616 754 agentCount: 0, // Quick count from cortex.status 617 755 imports: new Map(), 756 + dream: null, // Dream status snapshot (null when idle) 757 + dreamActive: false, // Whether dream is currently running 758 + sync: null, // Sync status snapshot (null when idle) 759 + serviceLogs: new Map(), // service name -> array of {ts, stream, line} 760 + logFollow: true, // Auto-scroll log viewport 618 761 remotes: new Map(), // Remote observer status by host 619 762 observe: { 620 763 mode: null, // Current observe mode ··· 624 767 activity: null, 625 768 describe: null, 626 769 transcribe: null 627 - }, 628 - eventsPaused: false, 629 - missedEvents: 0 770 + } 630 771 }; 631 772 632 773 // DOM elements ··· 656 797 cortexGrid: document.getElementById('cortexGrid'), 657 798 importerSection: document.getElementById('importerSection'), 658 799 importerGrid: document.getElementById('importerGrid'), 659 - eventLog: document.getElementById('eventLog'), 660 - eventsCard: document.getElementById('eventsCard'), 661 - pauseBtn: document.getElementById('pauseBtn'), 662 - missedCount: document.getElementById('missedCount') 800 + dreamCard: document.getElementById('dreamCard'), 801 + dreamInfo: document.getElementById('dreamInfo'), 802 + dreamProgress: document.getElementById('dreamProgress'), 803 + dreamAgents: document.getElementById('dreamAgents'), 804 + syncCard: document.getElementById('syncCard'), 805 + syncInfo: document.getElementById('syncInfo'), 806 + queuesSection: document.getElementById('queuesSection'), 807 + queuesValue: document.getElementById('queuesValue'), 808 + schedulesSection: document.getElementById('schedulesSection'), 809 + schedulesValue: document.getElementById('schedulesValue'), 810 + logsViewport: document.getElementById('logsViewport'), 811 + logServiceFilter: document.getElementById('logServiceFilter'), 812 + logStreamFilter: document.getElementById('logStreamFilter'), 813 + logFollowBtn: document.getElementById('logFollowBtn'), 814 + logClearBtn: document.getElementById('logClearBtn') 663 815 }; 664 816 665 817 // Utility functions ··· 761 913 updateVitalsStatus('ok'); 762 914 } 763 915 } 916 + 917 + // Queues (show when non-empty) 918 + const queueEntries = Object.entries(state.queues).filter(([, count]) => count > 0); 919 + if (queueEntries.length > 0) { 920 + elements.queuesSection.style.display = ''; 921 + elements.queuesValue.innerHTML = '<div class="vitals-chips">' + 922 + queueEntries.map(([cmd, count]) => 923 + `<span class="vitals-chip">${cmd}: ${count}</span>` 924 + ).join('') + '</div>'; 925 + } else { 926 + elements.queuesSection.style.display = 'none'; 927 + } 928 + 929 + // Schedules (show when any exist) 930 + if (state.schedules.length > 0) { 931 + elements.schedulesSection.style.display = ''; 932 + const dueCount = state.schedules.filter(s => s.due).length; 933 + elements.schedulesValue.innerHTML = `<span>${dueCount} due / ${state.schedules.length} total</span>`; 934 + } else { 935 + elements.schedulesSection.style.display = 'none'; 936 + } 764 937 } 765 938 766 939 function updateVitalsStatus(status) { ··· 790 963 const label = elements.observeModeLabel; 791 964 792 965 if (mode === 'screencast') { 793 - badge.className = 'live-badge'; 966 + badge.className = 'health-badge'; 794 967 label.textContent = 'Recording'; 795 968 } else if (mode === 'tmux') { 796 - badge.className = 'live-badge tmux'; 969 + badge.className = 'health-badge tmux'; 797 970 label.textContent = 'Tmux'; 798 971 } else if (mode === 'idle') { 799 - badge.className = 'live-badge idle'; 972 + badge.className = 'health-badge idle'; 800 973 label.textContent = 'Idle'; 801 974 } else { 802 - badge.className = 'live-badge idle'; 975 + badge.className = 'health-badge idle'; 803 976 label.textContent = 'Waiting...'; 804 977 } 805 978 } ··· 1003 1176 elements.importerGrid.innerHTML = html; 1004 1177 } 1005 1178 1006 - // Pause/resume event stream 1007 - function togglePause() { 1008 - state.eventsPaused = !state.eventsPaused; 1179 + function handleDreamEvent(msg) { 1180 + if (msg.event === 'started') { 1181 + state.dreamActive = true; 1182 + state.dream = { mode: msg.mode, day: msg.day }; 1183 + updateDreamCard(); 1184 + } else if (msg.event === 'status') { 1185 + state.dreamActive = true; 1186 + state.dream = { ...state.dream, ...msg }; 1187 + updateDreamCard(); 1188 + } else if (msg.event === 'completed') { 1189 + state.dreamActive = false; 1190 + state.dream = null; 1191 + updateDreamCard(); 1192 + } 1193 + } 1194 + 1195 + function updateDreamCard() { 1196 + if (!state.dreamActive || !state.dream) { 1197 + elements.dreamCard.classList.add('hidden'); 1198 + return; 1199 + } 1009 1200 1010 - if (state.eventsPaused) { 1011 - // Paused 1012 - elements.pauseBtn.classList.add('paused'); 1013 - elements.pauseBtn.innerHTML = '<span>|></span><span>Resume</span>'; 1014 - elements.eventsCard.classList.add('paused'); 1015 - state.missedEvents = 0; 1016 - elements.missedCount.style.display = 'none'; 1201 + elements.dreamCard.classList.remove('hidden'); 1202 + const d = state.dream; 1203 + 1204 + // Info fields 1205 + const infoParts = []; 1206 + if (d.mode) infoParts.push(`<div class="dream-info-item"><div class="dream-info-label">Mode</div><div>${d.mode}</div></div>`); 1207 + if (d.day) infoParts.push(`<div class="dream-info-item"><div class="dream-info-label">Day</div><div>${d.day}</div></div>`); 1208 + if (d.facet) infoParts.push(`<div class="dream-info-item"><div class="dream-info-label">Facet</div><div>${d.facet}</div></div>`); 1209 + if (d.segment) infoParts.push(`<div class="dream-info-item"><div class="dream-info-label">Segment</div><div>${d.segment}</div></div>`); 1210 + elements.dreamInfo.innerHTML = infoParts.join(''); 1211 + 1212 + // Progress bars 1213 + let progressHtml = ''; 1214 + if (d.agents_total > 0) { 1215 + const pct = Math.round((d.agents_completed || 0) / d.agents_total * 100); 1216 + progressHtml += ` 1217 + <div class="dream-progress"> 1218 + <div class="dream-progress-label">Agents: ${d.agents_completed || 0} / ${d.agents_total}</div> 1219 + <div class="dream-progress-bar"><div class="dream-progress-fill" style="width:${pct}%"></div></div> 1220 + </div>`; 1221 + } 1222 + if (d.segments_total > 0) { 1223 + const pct = Math.round((d.segments_completed || 0) / d.segments_total * 100); 1224 + progressHtml += ` 1225 + <div class="dream-progress"> 1226 + <div class="dream-progress-label">Segments: ${d.segments_completed || 0} / ${d.segments_total}</div> 1227 + <div class="dream-progress-bar"><div class="dream-progress-fill" style="width:${pct}%"></div></div> 1228 + </div>`; 1229 + } 1230 + elements.dreamProgress.innerHTML = progressHtml; 1231 + 1232 + // Current agents 1233 + if (d.current_agents && d.current_agents.length > 0) { 1234 + elements.dreamAgents.textContent = 'Running: ' + d.current_agents.join(', '); 1017 1235 } else { 1018 - // Resumed 1019 - elements.pauseBtn.classList.remove('paused'); 1020 - elements.pauseBtn.innerHTML = '<span>||</span><span>Pause</span>'; 1021 - elements.eventsCard.classList.remove('paused'); 1022 - state.missedEvents = 0; 1023 - elements.missedCount.style.display = 'none'; 1236 + elements.dreamAgents.textContent = ''; 1024 1237 } 1025 1238 } 1026 1239 1027 - // Append to event log 1028 - function appendEvent(msg) { 1029 - if (state.eventsPaused) { 1030 - state.missedEvents++; 1031 - elements.missedCount.textContent = `${state.missedEvents} missed`; 1032 - elements.missedCount.style.display = 'inline'; 1240 + function handleSyncEvent(msg) { 1241 + if (msg.event !== 'status') return; 1242 + 1243 + state.sync = msg; 1244 + updateSyncCard(); 1245 + } 1246 + 1247 + function updateSyncCard() { 1248 + const s = state.sync; 1249 + // Show when there's queued work or active upload 1250 + if (!s || (s.queue_size === 0 && !s.segment)) { 1251 + elements.syncCard.classList.add('hidden'); 1033 1252 return; 1034 1253 } 1035 1254 1036 - const line = document.createElement('div'); 1037 - line.className = 'event-line'; 1255 + elements.syncCard.classList.remove('hidden'); 1256 + 1257 + const infoParts = []; 1258 + if (s.host) infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">Host</div><div>${s.host}</div></div>`); 1259 + if (s.platform) infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">Platform</div><div>${s.platform}</div></div>`); 1260 + infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">Queue</div><div>${s.queue_size}</div></div>`); 1261 + if (s.state) infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">State</div><div>${s.state}${s.confirm_attempt ? ' (' + s.confirm_attempt + ')' : ''}</div></div>`); 1262 + if (s.segment) infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">Segment</div><div>${s.segment}</div></div>`); 1263 + elements.syncInfo.innerHTML = infoParts.join(''); 1264 + } 1265 + 1266 + const LOG_BUFFER_SIZE = 50; 1267 + 1268 + function handleLogsEvent(msg) { 1269 + if (msg.event !== 'line') return; 1270 + 1271 + const name = msg.name || 'unknown'; 1272 + const record = { ts: msg.ts || Date.now(), stream: msg.stream || 'stdout', line: msg.line || '' }; 1273 + 1274 + // Check if this is a new service before adding to buffer 1275 + const isNew = !state.serviceLogs.has(name); 1038 1276 1039 - // Highlight error events 1040 - if (msg.event === 'error' || (msg.event === 'exit' && msg.exit_code !== 0)) { 1041 - line.className = 'event-line error'; 1277 + // Buffer per service 1278 + if (isNew) { 1279 + state.serviceLogs.set(name, []); 1042 1280 } 1281 + const buf = state.serviceLogs.get(name); 1282 + buf.push(record); 1283 + if (buf.length > LOG_BUFFER_SIZE) buf.shift(); 1043 1284 1044 - line.textContent = JSON.stringify(msg); 1045 - elements.eventLog.appendChild(line); 1285 + // Update service filter dropdown if new service 1286 + if (isNew) { 1287 + const opt = document.createElement('option'); 1288 + opt.value = name; 1289 + opt.textContent = name; 1290 + elements.logServiceFilter.appendChild(opt); 1291 + } 1046 1292 1047 - // Auto-scroll to bottom 1048 - elements.eventLog.scrollTop = elements.eventLog.scrollHeight; 1293 + renderLogs(); 1294 + } 1295 + 1296 + function renderLogs() { 1297 + const serviceFilter = elements.logServiceFilter.value; 1298 + const streamFilter = elements.logStreamFilter.value; 1299 + const viewport = elements.logsViewport; 1300 + 1301 + // Check if user has scrolled away from bottom before updating 1302 + const wasAtBottom = state.logFollow && (viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 50); 1303 + 1304 + let html = ''; 1305 + // Get services to display, sorted alphabetically 1306 + const services = serviceFilter === 'all' 1307 + ? Array.from(state.serviceLogs.keys()).sort() 1308 + : (state.serviceLogs.has(serviceFilter) ? [serviceFilter] : []); 1309 + 1310 + for (const svc of services) { 1311 + const lines = state.serviceLogs.get(svc) || []; 1312 + const filtered = streamFilter === 'all' ? lines : lines.filter(l => l.stream === streamFilter); 1313 + if (filtered.length === 0) continue; 1049 1314 1050 - // Limit to last 100 events 1051 - while (elements.eventLog.children.length > 100) { 1052 - elements.eventLog.removeChild(elements.eventLog.firstChild); 1315 + html += `<div class="logs-service-header">── ${svc} ──</div>`; 1316 + for (const rec of filtered) { 1317 + const cls = rec.stream === 'stderr' ? 'logs-line stderr' : rec.stream === 'log' ? 'logs-line log' : 'logs-line'; 1318 + const escaped = rec.line.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1319 + html += `<div class="${cls}">${escaped}</div>`; 1320 + } 1321 + } 1322 + 1323 + viewport.innerHTML = html; 1324 + 1325 + // Auto-scroll if following 1326 + if (wasAtBottom || state.logFollow) { 1327 + viewport.scrollTop = viewport.scrollHeight; 1053 1328 } 1054 1329 } 1055 1330 ··· 1081 1356 state.health = { 1082 1357 stale_heartbeats: msg.stale_heartbeats || [] 1083 1358 }; 1359 + 1360 + // Update queues 1361 + state.queues = msg.queues || {}; 1362 + state.schedules = msg.schedules || []; 1084 1363 1085 1364 updateVitals(); 1086 1365 } ··· 1234 1513 updateImporterGrid(); 1235 1514 } 1236 1515 1237 - // Logs events are displayed in the event log via appendEvent 1238 - // Error highlighting is handled there based on event type 1239 - 1240 1516 // Main event handler 1241 1517 function handleEvent(msg) { 1242 - appendEvent(msg); 1243 - 1244 1518 const tract = msg.tract; 1245 1519 if (tract === 'supervisor') handleSupervisorEvent(msg); 1246 1520 else if (tract === 'cortex') handleCortexEvent(msg); 1247 1521 else if (tract === 'observe') handleObserveEvent(msg); 1248 1522 else if (tract === 'importer') handleImporterEvent(msg); 1249 - // logs tract events are displayed in event log via appendEvent 1523 + else if (tract === 'dream') handleDreamEvent(msg); 1524 + else if (tract === 'sync') handleSyncEvent(msg); 1525 + else if (tract === 'logs') handleLogsEvent(msg); 1250 1526 } 1251 1527 1252 - // Event listeners 1253 - elements.pauseBtn.addEventListener('click', togglePause); 1528 + // Log controls 1529 + elements.logServiceFilter.addEventListener('change', renderLogs); 1530 + elements.logStreamFilter.addEventListener('change', renderLogs); 1531 + elements.logFollowBtn.addEventListener('click', () => { 1532 + state.logFollow = !state.logFollow; 1533 + elements.logFollowBtn.classList.toggle('active', state.logFollow); 1534 + if (state.logFollow) { 1535 + elements.logsViewport.scrollTop = elements.logsViewport.scrollHeight; 1536 + } 1537 + }); 1538 + elements.logClearBtn.addEventListener('click', () => { 1539 + state.serviceLogs.clear(); 1540 + elements.logServiceFilter.innerHTML = '<option value="all">All Services</option>'; 1541 + renderLogs(); 1542 + }); 1254 1543 1255 1544 // Listen to all Callosum events 1256 1545 window.appEvents.listen('*', handleEvent);
+1 -1
docs/APPS.md
··· 815 815 816 816 Browse `apps/*/` directories for reference implementations. Apps range in complexity: 817 817 818 - - **Minimal** - Just `workspace.html` (e.g., `apps/home/`, `apps/live/`) 818 + - **Minimal** - Just `workspace.html` (e.g., `apps/home/`, `apps/health/`) 819 819 - **Styled** - Custom CSS, background services (e.g., `apps/dev/`) 820 820 - **Full-featured** - Routes, forms, AJAX, badges, tools (e.g., `apps/todos/`, `apps/chat/`) 821 821