personal memory agent
0
fork

Configure Feed

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

Merge branch 'hopper-i3doopxs-health-vitals-bar'

+91 -13
+91 -13
apps/health/workspace.html
··· 16 16 border-radius: 12px; 17 17 padding: 1.5em; 18 18 box-shadow: 0 4px 6px rgba(0,0,0,0.1); 19 + transition: background 0.5s ease; 20 + } 21 + 22 + .vitals-bar.warning { 23 + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); 24 + } 25 + 26 + .vitals-bar.error { 27 + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); 19 28 } 20 29 21 30 .vitals-header { ··· 41 50 display: flex; 42 51 align-items: center; 43 52 gap: 0.5em; 53 + } 54 + 55 + .connection-indicator { 56 + font-size: 0.8em; 57 + opacity: 0.8; 58 + margin-left: 0.5em; 59 + } 60 + 61 + .connection-indicator.stale { 62 + color: #fbbf24; 63 + opacity: 1; 64 + font-weight: 500; 65 + } 66 + 67 + .connection-indicator.disconnected { 68 + color: #fca5a5; 69 + opacity: 1; 70 + font-weight: 600; 44 71 } 45 72 46 73 .vitals-content { ··· 127 154 } 128 155 129 156 .health-badge { 130 - background: #dc2626; 157 + background: #6b7280; 131 158 color: white; 132 159 padding: 0.2em 0.6em; 133 160 border-radius: 12px; ··· 135 162 display: flex; 136 163 align-items: center; 137 164 gap: 0.3em; 165 + animation: none; 166 + } 167 + 168 + .health-badge.recording { 169 + background: #16a34a; 138 170 animation: pulse 2s infinite; 139 171 } 140 172 ··· 145 177 146 178 .health-badge.tmux { 147 179 background: #8b5cf6; 180 + animation: none; 148 181 } 149 182 150 183 @keyframes pulse { ··· 623 656 <span class="status-indicator active"></span> 624 657 <span>All Systems Go</span> 625 658 </div> 659 + <span class="connection-indicator" id="connectionIndicator"></span> 626 660 </div> 627 661 <div class="vitals-content"> 628 662 <div class="vitals-section"> ··· 654 688 <span>Waiting for status...</span> 655 689 </div> 656 690 </div> 657 - <div class="vitals-section" id="queuesSection" style="display: none;"> 691 + <div class="vitals-section" id="queuesSection"> 658 692 <div class="vitals-label">Queues</div> 659 693 <div class="vitals-value" id="queuesValue"></div> 660 694 </div> 661 - <div class="vitals-section" id="schedulesSection" style="display: none;"> 695 + <div class="vitals-section" id="schedulesSection"> 662 696 <div class="vitals-label">Schedules</div> 663 697 <div class="vitals-value" id="schedulesValue"></div> 664 698 </div> ··· 808 842 localHost: null, 809 843 deepLinkMode: false, 810 844 lastLogFilter: null, // Last rendered filter state for incremental append 845 + lastEventTs: null, // Timestamp of last event from WebSocket 811 846 }; 812 847 813 848 // DOM elements ··· 853 888 logServiceFilter: document.getElementById('logServiceFilter'), 854 889 logStreamFilter: document.getElementById('logStreamFilter'), 855 890 logFollowBtn: document.getElementById('logFollowBtn'), 856 - logClearBtn: document.getElementById('logClearBtn') 891 + logClearBtn: document.getElementById('logClearBtn'), 892 + connectionIndicator: document.getElementById('connectionIndicator'), 893 + vitalsBar: document.querySelector('.vitals-bar'), 857 894 }; 858 895 859 896 // Utility functions ··· 883 920 return div.innerHTML; 884 921 } 885 922 923 + function formatNextRun(epochMs) { 924 + if (!epochMs) return ''; 925 + const delta = epochMs - Date.now(); 926 + if (delta < 0) return 'overdue'; 927 + const mins = Math.floor(delta / 60000); 928 + if (mins < 60) return `in ${mins}m`; 929 + const hours = Math.floor(mins / 60); 930 + if (hours < 24) return `in ${hours}h`; 931 + const days = Math.floor(hours / 24); 932 + return `in ${days}d`; 933 + } 934 + 886 935 // Client-side elapsed time updater 887 936 let elapsedTimer = null; 888 937 function startElapsedTimer() { ··· 968 1017 } 969 1018 } 970 1019 971 - // Queues (show when non-empty) 1020 + // Queues (always visible) 972 1021 const queueEntries = Object.entries(state.queues).filter(([, count]) => count > 0); 973 1022 if (queueEntries.length > 0) { 974 - elements.queuesSection.style.display = ''; 975 1023 elements.queuesValue.innerHTML = '<div class="vitals-chips">' + 976 1024 queueEntries.map(([cmd, count]) => 977 1025 `<span class="vitals-chip">${escapeHtml(cmd)}: ${count}</span>` 978 1026 ).join('') + '</div>'; 979 1027 } else { 980 - elements.queuesSection.style.display = 'none'; 1028 + elements.queuesValue.textContent = '—'; 981 1029 } 982 1030 983 - // Schedules (show when any exist) 1031 + // Schedules (always visible, enriched) 984 1032 if (state.schedules.length > 0) { 985 - elements.schedulesSection.style.display = ''; 986 - const dueCount = state.schedules.filter(s => s.due).length; 987 - elements.schedulesValue.innerHTML = `<span>${dueCount} due / ${state.schedules.length} total</span>`; 1033 + const chips = state.schedules.map(s => { 1034 + const name = escapeHtml(s.name || 'unnamed'); 1035 + const next = formatNextRun(s.next_run); 1036 + const due = s.due ? ' ⏰' : ''; 1037 + return `<span class="vitals-chip" title="${escapeHtml(s.every || '')}">${name}${due}${next ? ' · ' + next : ''}</span>`; 1038 + }).join(''); 1039 + elements.schedulesValue.innerHTML = '<div class="vitals-chips">' + chips + '</div>'; 988 1040 } else { 989 - elements.schedulesSection.style.display = 'none'; 1041 + elements.schedulesValue.textContent = '—'; 990 1042 } 991 1043 } 992 1044 993 1045 function updateVitalsStatus(status) { 994 1046 const vitalsStatus = elements.vitalsStatus; 1047 + const bar = elements.vitalsBar; 1048 + bar.classList.remove('warning', 'error'); 995 1049 if (status === 'ok') { 996 1050 vitalsStatus.innerHTML = ` 997 1051 <span class="status-indicator active"></span> ··· 1002 1056 <span class="status-indicator restarting"></span> 1003 1057 <span>Warnings</span> 1004 1058 `; 1059 + bar.classList.add('warning'); 1005 1060 } else if (status === 'error') { 1006 1061 vitalsStatus.innerHTML = ` 1007 1062 <span class="status-indicator crashed"></span> 1008 1063 <span>Issues Detected</span> 1009 1064 `; 1065 + bar.classList.add('error'); 1010 1066 } 1011 1067 } 1012 1068 ··· 1023 1079 const label = elements.observeModeLabel; 1024 1080 1025 1081 if (mode === 'screencast') { 1026 - badge.className = 'health-badge'; 1082 + badge.className = 'health-badge recording'; 1027 1083 label.textContent = 'Recording'; 1028 1084 } else if (mode === 'tmux') { 1029 1085 badge.className = 'health-badge tmux'; ··· 1682 1738 1683 1739 // Main event handler 1684 1740 function handleEvent(msg) { 1741 + state.lastEventTs = Date.now(); 1685 1742 const tract = msg.tract; 1686 1743 if (tract === 'supervisor') handleSupervisorEvent(msg); 1687 1744 else if (tract === 'cortex') handleCortexEvent(msg); ··· 1782 1839 }); 1783 1840 if (changed) updateImporterGrid(); 1784 1841 }, 60000); 1842 + 1843 + // Connection health indicator — updated every 5s 1844 + setInterval(() => { 1845 + const el = elements.connectionIndicator; 1846 + if (!state.lastEventTs) { 1847 + el.textContent = ''; 1848 + el.className = 'connection-indicator'; 1849 + return; 1850 + } 1851 + const ago = Math.floor((Date.now() - state.lastEventTs) / 1000); 1852 + if (ago >= 60) { 1853 + el.textContent = `⚠ Disconnected (${formatElapsed(ago)})`; 1854 + el.className = 'connection-indicator disconnected'; 1855 + } else if (ago >= 30) { 1856 + el.textContent = `Stale (${formatElapsed(ago)})`; 1857 + el.className = 'connection-indicator stale'; 1858 + } else { 1859 + el.textContent = `Updated ${formatElapsed(ago)} ago`; 1860 + el.className = 'connection-indicator'; 1861 + } 1862 + }, 5000); 1785 1863 })(); 1786 1864 </script>