personal memory agent
0
fork

Configure Feed

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

Merge branch 'hopper-g3xzhvfo-health-plain-language'

+106 -53
+106 -53
apps/health/workspace.html
··· 700 700 border-radius: 10px; 701 701 font-size: 0.8em; 702 702 } 703 + 704 + .trust-indicator { 705 + font-size: 0.75em; 706 + opacity: 0.6; 707 + color: white; 708 + padding: 0.5em 1.2em; 709 + text-align: right; 710 + } 711 + 712 + #cortexGrid .activity-card-id, 713 + #cortexGrid .activity-card-provider, 714 + #importerGrid .activity-card-id, 715 + #importerGrid .activity-card-provider { 716 + display: none; 717 + } 703 718 </style> 704 719 705 720 <div class="health-dashboard"> ··· 707 722 <div class="vitals-bar"> 708 723 <div class="vitals-header"> 709 724 <div class="vitals-title"> 710 - SYSTEM VITALS 725 + System vitals 711 726 </div> 712 727 <div class="vitals-status" id="vitalsStatus"> 713 728 <span class="status-indicator active"></span> ··· 754 769 <div class="vitals-value" id="schedulesValue"></div> 755 770 </div> 756 771 </div> 772 + <div class="trust-indicator" id="trustIndicator">All data stored locally on this machine</div> 757 773 </div> 758 774 759 775 <div class="status-summary"> ··· 764 780 <div class="observe-card"> 765 781 <div class="card-header"> 766 782 <div class="card-title"> 767 - OBSERVATION STATUS 783 + Observation status 768 784 </div> 769 785 <div class="health-badge idle" id="observeModeBadge"> 770 786 <span id="observeModeLabel">Waiting...</span> ··· 778 794 <div class="observe-group-label">Capture</div> 779 795 <div class="observe-group-channels"> 780 796 <div class="observe-section"> 781 - <div class="observe-section-title">Screencast</div> 797 + <div class="observe-section-title">Screen Recording</div> 782 798 <div class="observe-section-value" id="screencastStatus">Waiting...</div> 783 799 <div class="observe-section-detail" id="screencastDetail"></div> 784 800 </div> 785 801 <div class="observe-section"> 786 - <div class="observe-section-title">Tmux</div> 802 + <div class="observe-section-title">Terminal Sessions</div> 787 803 <div class="observe-section-value" id="tmuxStatus">Waiting...</div> 788 804 <div class="observe-section-detail" id="tmuxDetail"></div> 789 805 </div> 790 806 <div class="observe-section"> 791 - <div class="observe-section-title">Audio</div> 807 + <div class="observe-section-title">Audio Capture</div> 792 808 <div class="observe-section-value" id="audioStatus">Waiting...</div> 793 809 <div class="observe-section-detail" id="audioDetail"></div> 794 810 </div> 795 811 <div class="observe-section"> 796 - <div class="observe-section-title">Activity</div> 812 + <div class="observe-section-title">System Activity</div> 797 813 <div class="observe-section-value" id="activityStatus">Waiting...</div> 798 814 <div class="observe-section-detail" id="activityDetail"></div> 799 815 </div> ··· 803 819 <div class="observe-group-label">Processing</div> 804 820 <div class="observe-group-channels"> 805 821 <div class="observe-section"> 806 - <div class="observe-section-title">Describe</div> 822 + <div class="observe-section-title">Scene Analysis</div> 807 823 <div class="observe-section-value" id="describeStatus">Idle</div> 808 824 <div class="observe-section-detail" id="describeDetail"></div> 809 825 </div> 810 826 <div class="observe-section"> 811 - <div class="observe-section-title">Transcribe</div> 827 + <div class="observe-section-title">Audio Transcription</div> 812 828 <div class="observe-section-value" id="transcribeStatus">Idle</div> 813 829 <div class="observe-section-detail" id="transcribeDetail"></div> 814 830 </div> ··· 821 837 <div class="observers-card hidden" id="observersCard"> 822 838 <div class="card-header"> 823 839 <div class="card-title"> 824 - OBSERVERS 840 + Connected observers 825 841 </div> 826 842 </div> 827 843 <div class="observers-grid" id="observersGrid"></div> ··· 832 848 <!-- Cortex Agents --> 833 849 <div class="activity-section hidden" id="cortexSection"> 834 850 <div class="activity-section-header"> 835 - AGENTS 851 + Active agents 836 852 </div> 837 853 <div class="activity-grid" id="cortexGrid"></div> 838 854 </div> ··· 840 856 <!-- Importers --> 841 857 <div class="activity-section hidden" id="importerSection"> 842 858 <div class="activity-section-header"> 843 - IMPORTS 859 + Active imports 844 860 </div> 845 861 <div class="activity-grid" id="importerGrid"></div> 846 862 </div> ··· 848 864 849 865 <div class="activity-section hidden" id="errorSummary"> 850 866 <div class="activity-section-header" style="color: #dc2626;"> 851 - RECENT ERRORS 867 + Recent errors 852 868 </div> 853 869 <div id="errorSummaryContent"></div> 854 870 </div> 855 871 856 872 <div class="activity-section hidden" id="allQuietCard" style="text-align: center; color: #6b7280; padding: 2em;"> 857 - All quiet — no active agents, imports, or processing 873 + Everything is idle — nothing running right now 858 874 </div> 859 875 860 876 <!-- Dream Card (hidden when idle) --> 861 877 <div class="dream-card hidden" id="dreamCard"> 862 878 <div class="card-header"> 863 - <div class="card-title">DREAM PROCESSING</div> 879 + <div class="card-title">Overnight processing</div> 864 880 </div> 865 881 <div class="dream-info" id="dreamInfo"></div> 866 882 <div id="dreamProgress"></div> ··· 870 886 <!-- Sync Card (hidden when idle) --> 871 887 <div class="sync-card hidden" id="syncCard"> 872 888 <div class="card-header"> 873 - <div class="card-title">SYNC</div> 889 + <div class="card-title">Cloud sync</div> 874 890 </div> 875 891 <div class="sync-info" id="syncInfo"></div> 876 892 </div> ··· 878 894 <!-- Service Logs --> 879 895 <div class="logs-card logs-collapsed"> 880 896 <div class="logs-header"> 881 - <div class="logs-title">SERVICE LOGS</div> 897 + <div class="logs-title">Service logs</div> 882 898 <span class="logs-error-badge hidden" id="logErrorBadge"></span> 883 899 <span class="logs-collapse-indicator" id="logsCollapseIndicator">▶ Show</span> 884 900 <div class="logs-controls"> ··· 983 999 logErrorBadge: document.getElementById('logErrorBadge'), 984 1000 logsCollapseIndicator: document.getElementById('logsCollapseIndicator'), 985 1001 connectionIndicator: document.getElementById('connectionIndicator'), 1002 + trustIndicator: document.getElementById('trustIndicator'), 986 1003 vitalsBar: document.querySelector('.vitals-bar'), 987 1004 }; 988 1005 1006 + // Human-readable service names 1007 + const SERVICE_NAMES = { 1008 + supervisor: 'System Manager', 1009 + convey: 'Web Interface', 1010 + cortex: 'AI Engine', 1011 + observe: 'Screen & Audio', 1012 + dream: 'Overnight Processing', 1013 + sync: 'Cloud Sync', 1014 + importer: 'File Importer', 1015 + schedule: 'Task Scheduler', 1016 + }; 1017 + 1018 + function serviceName(internal) { 1019 + return SERVICE_NAMES[internal] || internal; 1020 + } 1021 + 989 1022 // Utility functions 990 1023 function formatElapsed(seconds) { 991 1024 if (seconds < 60) return `${seconds}s`; ··· 1037 1070 const captureDetails = []; 1038 1071 if (observers.some(observer => observer.screencast?.recording)) captureDetails.push('screen'); 1039 1072 if (observers.some(observer => observer.audio && ((observer.audio.threshold_hits || 0) > 0 || observer.audio.will_save))) captureDetails.push('audio'); 1040 - if (observers.some(observer => observer.tmux?.capturing)) captureDetails.push('tmux'); 1073 + if (observers.some(observer => observer.tmux?.capturing)) captureDetails.push('terminal'); 1041 1074 parts.push(captureDetails.length > 0 ? `Capturing (${captureDetails.join(', ')})` : 'Capturing'); 1042 1075 } 1043 1076 1044 1077 const processingDetails = []; 1045 - if (observers.some(observer => observer.describe?.running)) processingDetails.push('describe'); 1046 - if (observers.some(observer => observer.transcribe?.running)) processingDetails.push('transcribe'); 1078 + if (observers.some(observer => observer.describe?.running)) processingDetails.push('scene analysis'); 1079 + if (observers.some(observer => observer.transcribe?.running)) processingDetails.push('audio transcription'); 1047 1080 if (processingDetails.length > 0) { 1048 1081 parts.push(`Processing (${processingDetails.join(', ')})`); 1049 1082 } ··· 1055 1088 parts.push(`${activeImports} import${activeImports === 1 ? '' : 's'} active`); 1056 1089 } 1057 1090 if (state.crashed.size > 0) { 1058 - parts.push(`${state.crashed.size} service${state.crashed.size === 1 ? '' : 's'} crashed`); 1091 + parts.push(state.crashed.size === 1 ? '1 service needs attention' : `${state.crashed.size} services need attention`); 1059 1092 } 1060 1093 if (staleCount > 0) { 1061 - parts.push(`${staleCount} stale heartbeat${staleCount === 1 ? '' : 's'}`); 1094 + parts.push(`${staleCount} service${staleCount === 1 ? '' : 's'} not responding`); 1062 1095 } 1063 1096 if (state.recentErrors.length > 0) { 1064 1097 parts.push(`${state.recentErrors.length} recent error${state.recentErrors.length === 1 ? '' : 's'}`); ··· 1092 1125 1093 1126 elements.errorSummary.classList.remove('hidden'); 1094 1127 const recent = state.recentErrors.slice(-10).reverse(); 1095 - elements.errorSummaryContent.innerHTML = recent.map(e => { 1128 + const errorsHtml = recent.map(e => { 1096 1129 const icon = e.type === 'agent' ? '⚙' : '↓'; 1097 1130 const ago = formatElapsed(Math.floor((Date.now() - e.ts) / 1000)); 1098 1131 return `<div style="padding: 0.3em 0; font-size: 0.85em; color: #374151; border-bottom: 1px solid #f3f4f6;"> 1099 1132 ${icon} <strong>${escapeHtml(e.name)}</strong> — ${escapeHtml(truncate(e.error, 60))} <span style="color: #9ca3af; font-size: 0.85em;">${escapeHtml(ago)} ago</span> 1100 1133 </div>`; 1101 1134 }).join(''); 1135 + elements.errorSummaryContent.innerHTML = errorsHtml + '<div style="padding: 0.5em 0; font-size: 0.8em; color: #9ca3af; font-style: italic;">Usually resolves automatically. If persistent, restart solstone.</div>'; 1102 1136 } 1103 1137 1104 1138 // Client-side elapsed time updater ··· 1143 1177 }); 1144 1178 1145 1179 if (allServices.length > 0) { 1146 - const servicesHtml = allServices.map(({ name, info, crashed }) => { 1180 + const servicesHtml = allServices.map(({ name, crashed }) => { 1147 1181 const statusClass = crashed ? 'crashed' : 'active'; 1148 1182 const dotClass = crashed ? 'service-dot crashed' : 'service-dot'; 1149 - const restartInfo = crashed && info.restart_attempts ? ` (${escapeHtml(info.restart_attempts)}x)` : ''; 1183 + const restartInfo = crashed ? ' (restarting...)' : ''; 1150 1184 return ` 1151 - <div class="${dotClass}"> 1185 + <div class="${dotClass}" data-service="${escapeHtml(name)}"> 1152 1186 <span class="status-indicator ${statusClass}"></span> 1153 - <span>${escapeHtml(name)}${restartInfo}</span> 1187 + <span title="${escapeHtml(name)}">${escapeHtml(serviceName(name))}${restartInfo}</span> 1154 1188 </div> 1155 1189 `; 1156 1190 }).join(''); ··· 1171 1205 const hasCrashed = state.crashed.size > 0; 1172 1206 1173 1207 if (staleCount > 0) { 1174 - const staleNames = staleHeartbeats.join(', '); 1208 + const staleNames = staleHeartbeats.map(s => serviceName(s)).join(', '); 1175 1209 elements.healthValue.innerHTML = ` 1176 - <span style="color: #f59e0b;">! ${staleCount} stale</span> 1210 + <span style="color: #f59e0b;">${staleCount} service${staleCount === 1 ? '' : 's'} not responding</span> 1177 1211 <span class="stale-list">(${escapeHtml(staleNames)})</span> 1178 1212 `; 1179 1213 updateVitalsStatus('warning'); 1180 1214 } else if (hasCrashed) { 1181 - elements.healthValue.innerHTML = `<span style="color: #f87171;">! Services crashed</span>`; 1215 + elements.healthValue.innerHTML = `<span style="color: #f87171;">Services need attention</span>`; 1182 1216 updateVitalsStatus('error'); 1183 1217 } else { 1184 1218 elements.healthValue.innerHTML = `<span>OK</span>`; ··· 1225 1259 } else if (status === 'warning') { 1226 1260 vitalsStatus.innerHTML = ` 1227 1261 <span class="status-indicator restarting"></span> 1228 - <span>Warnings</span> 1262 + <span>Some services slow to respond</span> 1229 1263 `; 1230 1264 bar.classList.add('warning'); 1231 1265 } else if (status === 'error') { 1232 1266 vitalsStatus.innerHTML = ` 1233 1267 <span class="status-indicator crashed"></span> 1234 - <span>Issues Detected</span> 1268 + <span>Services need attention</span> 1235 1269 `; 1236 1270 bar.classList.add('error'); 1237 1271 } ··· 1254 1288 label.textContent = 'Recording'; 1255 1289 } else if (mode === 'tmux') { 1256 1290 badge.className = 'health-badge tmux'; 1257 - label.textContent = 'Tmux'; 1291 + label.textContent = 'Terminal Sessions'; 1258 1292 } else if (mode === 'idle') { 1259 1293 badge.className = 'health-badge idle'; 1260 1294 label.textContent = 'Idle'; ··· 1305 1339 const streams = primary.screencast.streams || []; 1306 1340 const elapsed = primary.screencast.window_elapsed_seconds || 0; 1307 1341 const streamCount = streams.length; 1308 - const label = streamCount === 1 ? 'monitor' : 'monitors'; 1309 - elements.screencastStatus.textContent = `${elapsed}s (${streamCount} ${label})`; 1342 + const displayLabel = streamCount === 1 ? 'display' : 'displays'; 1343 + const mins = Math.max(1, Math.round(elapsed / 60)); 1344 + elements.screencastStatus.textContent = `Recording (${streamCount} ${displayLabel}, ~${mins} min)`; 1310 1345 if (streamCount > 0) { 1311 1346 elements.screencastDetail.textContent = streams 1312 1347 .map(s => `${s.position || 'unknown'} ${s.connector || 'unknown'}`) ··· 1315 1350 elements.screencastDetail.textContent = ''; 1316 1351 } 1317 1352 } else { 1318 - elements.screencastStatus.textContent = 'Idle'; 1353 + elements.screencastStatus.textContent = 'Not recording'; 1319 1354 elements.screencastDetail.textContent = ''; 1320 1355 } 1321 1356 } else { 1322 - elements.screencastStatus.textContent = 'Idle'; 1357 + elements.screencastStatus.textContent = 'Not recording'; 1323 1358 elements.screencastDetail.textContent = ''; 1324 1359 } 1325 1360 ··· 1329 1364 const captures = tmux.tmux.captures || 0; 1330 1365 const sessions = tmux.tmux.sessions || []; 1331 1366 const elapsed = tmux.tmux.window_elapsed_seconds || 0; 1332 - elements.tmuxStatus.textContent = `${elapsed}s (${captures} captures)`; 1367 + const mins = Math.max(1, Math.round(elapsed / 60)); 1368 + elements.tmuxStatus.textContent = `Capturing (${captures} snapshots, ~${mins} min)`; 1333 1369 if (sessions.length > 0) { 1334 1370 elements.tmuxDetail.textContent = sessions.join(', '); 1335 1371 } else { 1336 1372 elements.tmuxDetail.textContent = ''; 1337 1373 } 1338 1374 } else { 1339 - elements.tmuxStatus.textContent = 'Idle'; 1375 + elements.tmuxStatus.textContent = 'Not capturing'; 1340 1376 elements.tmuxDetail.textContent = ''; 1341 1377 } 1342 1378 } else if (tmux) { 1343 - elements.tmuxStatus.textContent = 'Idle'; 1379 + elements.tmuxStatus.textContent = 'Not capturing'; 1344 1380 elements.tmuxDetail.textContent = ''; 1345 1381 } else { 1346 - elements.tmuxStatus.textContent = 'Waiting...'; 1382 + elements.tmuxStatus.textContent = 'Not capturing'; 1347 1383 elements.tmuxDetail.textContent = ''; 1348 1384 } 1349 1385 1350 1386 // Audio 1351 1387 if (primary.audio) { 1352 1388 const hits = primary.audio.threshold_hits || 0; 1353 - const willSave = primary.audio.will_save ? ' (saving)' : ''; 1354 - elements.audioStatus.textContent = `${hits} hits${willSave}`; 1389 + const willSave = primary.audio.will_save ? ' · saving' : ''; 1390 + if (hits > 0) { 1391 + elements.audioStatus.textContent = `Listening (${hits} sound${hits === 1 ? '' : 's'} detected)${willSave}`; 1392 + } else { 1393 + elements.audioStatus.textContent = 'Listening (quiet)'; 1394 + } 1355 1395 elements.audioDetail.textContent = ''; 1356 1396 } else { 1357 - elements.audioStatus.textContent = 'Idle'; 1397 + elements.audioStatus.textContent = 'Listening (quiet)'; 1358 1398 elements.audioDetail.textContent = ''; 1359 1399 } 1360 1400 ··· 1362 1402 if (primary.activity) { 1363 1403 const idleMs = primary.activity.idle_time_ms || 0; 1364 1404 if (primary.activity.power_save) { 1365 - elements.activityStatus.textContent = 'Power save'; 1405 + elements.activityStatus.textContent = 'Power saving'; 1366 1406 } else if (primary.activity.screen_locked) { 1367 - elements.activityStatus.textContent = 'Locked'; 1407 + elements.activityStatus.textContent = 'Screen locked'; 1368 1408 } else if (primary.activity.sink_muted) { 1369 - elements.activityStatus.textContent = 'Muted'; 1409 + elements.activityStatus.textContent = 'Audio muted'; 1370 1410 } else { 1371 1411 elements.activityStatus.textContent = `Idle: ${Math.floor(idleMs/1000)}s`; 1372 1412 } ··· 1500 1540 elements.cortexSection.classList.remove('hidden'); 1501 1541 1502 1542 const html = activeAgents.map(agent => { 1503 - const stateIcon = agent.event === 'thinking' ? '~' : 1504 - (agent.event === 'tool_start' || agent.event === 'tool_end') ? '#' : 'o'; 1543 + const stateLabel = agent.event === 'thinking' ? 'Thinking...' : 1544 + (agent.event === 'tool_start' || agent.event === 'tool_end') ? 'Working...' : 'Running...'; 1505 1545 const elapsed = agent.elapsed_seconds ? formatElapsed(agent.elapsed_seconds) : '0s'; 1506 1546 1507 1547 return ` 1508 1548 <div class="activity-card agent-active" data-agent-id="${agent.agent_id}"> 1509 1549 <div class="activity-card-id">...${getAgentId(agent.agent_id)}</div> 1510 1550 <div class="activity-card-name">${escapeHtml(agent.name || 'default')}</div> 1511 - <div class="activity-card-state">${stateIcon} ${escapeHtml(agent.event || 'unknown')}</div> 1551 + <div class="activity-card-state">${stateLabel}</div> 1512 1552 <div class="activity-card-elapsed">${elapsed}</div> 1513 1553 <div class="activity-card-provider">${escapeHtml(agent.provider || 'unknown')}</div> 1514 1554 </div> ··· 1540 1580 imp.stage === 'transcribing' ? 50 : 1541 1581 imp.stage === 'segmenting' ? 75 : 90; 1542 1582 const elapsed = imp.elapsed_ms ? formatDuration(imp.elapsed_ms) : '0s'; 1583 + const humanStage = imp.stage === 'initialization' ? 'Starting...' : 1584 + imp.stage === 'transcribing' ? 'Transcribing audio...' : 1585 + imp.stage === 'segmenting' ? 'Organizing segments...' : 'Processing...'; 1543 1586 1544 1587 let errorHtml = ''; 1545 1588 if (isError && imp.error) { ··· 1550 1593 <div class="activity-card ${cardClass}" data-import-id="${imp.import_id}"> 1551 1594 <div class="activity-card-id">...${getAgentId(imp.import_id)}</div> 1552 1595 <div class="activity-card-name">${escapeHtml(truncate(imp.input_file || 'import', 20))}</div> 1553 - <div class="activity-card-state">${isError ? '! ' : ''}${escapeHtml(imp.stage || 'processing')}</div> 1596 + <div class="activity-card-state" data-internal-stage="${escapeHtml(imp.stage || 'processing')}">${isError ? '! ' : ''}${escapeHtml(humanStage)}</div> 1554 1597 ${errorHtml} 1555 1598 <div class="activity-card-progress"> 1556 1599 <div class="activity-card-progress-bar" style="width: ${progress}%"></div> ··· 1644 1687 // Show when there's queued work or active upload 1645 1688 if (!s || (s.queue_size === 0 && !s.segment)) { 1646 1689 elements.syncCard.classList.add('hidden'); 1690 + if (elements.trustIndicator) { 1691 + elements.trustIndicator.textContent = 'All data stored locally on this machine'; 1692 + } 1647 1693 updateAllQuiet(); 1648 1694 updateStatusSummary(); 1649 1695 return; ··· 1658 1704 if (s.state) infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">State</div><div>${escapeHtml(s.state)}${s.confirm_attempt ? ' (' + escapeHtml(s.confirm_attempt) + ')' : ''}</div></div>`); 1659 1705 if (s.segment) infoParts.push(`<div class="sync-info-item"><div class="sync-info-label">Segment</div><div>${escapeHtml(s.segment)}</div></div>`); 1660 1706 elements.syncInfo.innerHTML = infoParts.join(''); 1707 + if (elements.trustIndicator) { 1708 + if (s && s.host) { 1709 + elements.trustIndicator.textContent = 'All data stored locally · Syncing to ' + s.host; 1710 + } else { 1711 + elements.trustIndicator.textContent = 'All data stored locally on this machine'; 1712 + } 1713 + } 1661 1714 1662 1715 updateAllQuiet(); 1663 1716 updateStatusSummary(); ··· 1704 1757 if (isNew) { 1705 1758 const opt = document.createElement('option'); 1706 1759 opt.value = name; 1707 - opt.textContent = name; 1760 + opt.textContent = serviceName(name); 1708 1761 elements.logServiceFilter.appendChild(opt); 1709 1762 } 1710 1763 ··· 1727 1780 const header = document.createElement('div'); 1728 1781 header.className = 'logs-service-header'; 1729 1782 header.setAttribute('data-svc', newService); 1730 - header.textContent = '── ' + newService + ' ──'; 1783 + header.textContent = '── ' + serviceName(newService) + ' ──'; 1731 1784 viewport.appendChild(header); 1732 1785 } 1733 1786 const line = document.createElement('div'); ··· 1755 1808 const filtered = streamFilter === 'all' ? lines : lines.filter(l => l.stream === streamFilter); 1756 1809 if (filtered.length === 0) continue; 1757 1810 1758 - html += `<div class="logs-service-header" data-svc="${escapeHtml(svc)}">── ${escapeHtml(svc)} ──</div>`; 1811 + html += `<div class="logs-service-header" data-svc="${escapeHtml(svc)}">── ${escapeHtml(serviceName(svc))} ──</div>`; 1759 1812 for (const rec of filtered) { 1760 1813 const cls = rec.stream === 'stderr' ? 'logs-line stderr' : rec.stream === 'log' ? 'logs-line log' : 'logs-line'; 1761 1814 const escaped = rec.line.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');