personal memory agent
0
fork

Configure Feed

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

health: split observer terminology and add registered-observers strip

Renames the local-capture surface (card title, empty state, badge) so
the word "observer" no longer overloads between the in-process capture
streams and the registered-observer registry. Adds an adjacent
"Registered observers" card that consumes /app/observer/api/list on a
60s cadence (independent of the local-capture lanes), renders server
state/label per row, and shows a visible "clock skew" chip when the
server flags a future-timestamp drift beyond tolerance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+101 -5
+101 -5
apps/health/workspace.html
··· 430 430 font-size: 0.7em; 431 431 } 432 432 433 + .registered-observers-card { border-left: 4px solid #9ca3af; } 434 + .registered-observers-card.hidden { display: none; } 435 + .registered-observers-strip { display: flex; flex-direction: column; gap: 0.5em; } 436 + .registered-observer-row { display: flex; align-items: center; gap: 0.6em; padding: 0.65em 0.75em; background: #f9fafb; border-radius: 8px; } 437 + .registered-observer-name { font-weight: 600; color: #111827; } 438 + 439 + .registered-observer-label, 440 + .registered-observer-skew { 441 + font-size: 0.72em; 442 + border-radius: 999px; 443 + padding: 0.2em 0.55em; 444 + } 445 + 446 + .registered-observer-label.connected { background: #dcfce7; color: #15803d; } 447 + .registered-observer-label.stale { background: #fef3c7; color: #b45309; } 448 + .registered-observer-label.disconnected, 449 + .registered-observer-label.revoked { background: #e5e7eb; color: #4b5563; } 450 + 451 + .registered-observer-meta { color: #6b7280; font-size: 0.78em; margin-left: auto; } 452 + .registered-observer-skew { background: #fef3c7; color: #b45309; } 453 + .registered-observer-skew.hidden { display: none; } 454 + 433 455 /* Activity Grids Container */ 434 456 .activity-grids { 435 457 display: contents; ··· 1104 1126 <div class="dashboard-card observe-card"> 1105 1127 <div class="card-header"> 1106 1128 <div class="card-title"> 1107 - Observers 1129 + Local capture 1108 1130 </div> 1109 1131 <div class="health-badge idle" id="observeModeBadge"> 1110 1132 <span id="observeModeLabel">Waiting...</span> ··· 1112 1134 </div> 1113 1135 <div id="observeEmpty"> 1114 1136 <div class="surface-state surface-state--empty"> 1115 - <h2 class="surface-state-heading">No observers connected</h2> 1137 + <h2 class="surface-state-heading">No local capture active</h2> 1116 1138 <div class="surface-state-action"><a href="/app/observer/">Manage observers →</a></div> 1117 1139 </div> 1118 1140 </div> ··· 1154 1176 <div class="dashboard-card observers-card hidden" id="observersCard"> 1155 1177 <div class="card-header"> 1156 1178 <div class="card-title"> 1157 - Connected 1179 + Capture hosts 1158 1180 </div> 1159 1181 </div> 1160 1182 <div class="observers-grid" id="observersGrid"></div> 1183 + </div> 1184 + 1185 + <div class="dashboard-card registered-observers-card hidden" id="registeredObserversCard"> 1186 + <div class="card-header"> 1187 + <div class="card-title">Registered observers</div> 1188 + </div> 1189 + <div class="registered-observers-strip" id="registeredObserversStrip"></div> 1161 1190 </div> 1162 1191 1163 1192 <!-- Activity Grids --> ··· 1314 1343 transcribeDetail: document.getElementById('transcribeDetail'), 1315 1344 observersCard: document.getElementById('observersCard'), 1316 1345 observersGrid: document.getElementById('observersGrid'), 1346 + registeredObserversCard: document.getElementById('registeredObserversCard'), 1347 + registeredObserversStrip: document.getElementById('registeredObserversStrip'), 1317 1348 observeContent: document.getElementById('observeContent'), 1318 1349 observeEmpty: document.getElementById('observeEmpty'), 1319 1350 cortexSection: document.getElementById('cortexSection'), ··· 2013 2044 function updateObserveMode() { 2014 2045 if (state.observers.size === 0) { 2015 2046 elements.observeModeBadge.className = 'health-badge idle'; 2016 - elements.observeModeLabel.textContent = 'No observers'; 2047 + elements.observeModeLabel.textContent = 'No capture'; 2017 2048 return; 2018 2049 } 2019 2050 ··· 2266 2297 } 2267 2298 } 2268 2299 2300 + function registeredObserverMeta(observer) { 2301 + if (!observer.last_seen) return 'never seen'; 2302 + const deltaMs = Date.now() - observer.last_seen; 2303 + if (deltaMs < 0) return 'last seen from future'; 2304 + return `last seen ${formatElapsed(Math.floor(deltaMs / 1000))} ago`; 2305 + } 2306 + 2307 + function renderRegisteredObservers(observers) { 2308 + if (!observers || observers.length === 0) { 2309 + elements.registeredObserversCard.classList.add('hidden'); 2310 + elements.registeredObserversStrip.innerHTML = ''; 2311 + return; 2312 + } 2313 + 2314 + elements.registeredObserversCard.classList.remove('hidden'); 2315 + elements.registeredObserversStrip.innerHTML = ''; 2316 + for (const observer of observers) { 2317 + const stateClass = ['connected', 'stale', 'disconnected', 'revoked'].includes(observer.state) 2318 + ? observer.state 2319 + : 'disconnected'; 2320 + const row = document.createElement('div'); 2321 + row.className = 'registered-observer-row'; 2322 + 2323 + const nameEl = document.createElement('span'); 2324 + nameEl.className = 'registered-observer-name'; 2325 + nameEl.textContent = observer.name || observer.key_prefix || 'observer'; 2326 + row.appendChild(nameEl); 2327 + 2328 + const labelEl = document.createElement('span'); 2329 + labelEl.className = `registered-observer-label ${stateClass}`; 2330 + labelEl.textContent = observer.label || stateClass; 2331 + row.appendChild(labelEl); 2332 + 2333 + const metaEl = document.createElement('span'); 2334 + metaEl.className = 'registered-observer-meta'; 2335 + metaEl.textContent = registeredObserverMeta(observer); 2336 + row.appendChild(metaEl); 2337 + 2338 + const skewEl = document.createElement('span'); 2339 + skewEl.className = 'registered-observer-skew' + (observer.clock_skew ? '' : ' hidden'); 2340 + skewEl.textContent = 'clock skew'; 2341 + row.appendChild(skewEl); 2342 + 2343 + elements.registeredObserversStrip.appendChild(row); 2344 + } 2345 + } 2346 + 2347 + async function loadRegisteredObservers() { 2348 + try { 2349 + const response = await fetch('/app/observer/api/list'); 2350 + if (!response.ok) throw new Error(`HTTP ${response.status}`); 2351 + const payload = await response.json(); 2352 + renderRegisteredObservers(payload?.observers || []); 2353 + } catch (err) { 2354 + console.warn('Failed to load registered observers:', err); 2355 + } 2356 + } 2357 + 2269 2358 // Update cortex grid 2270 2359 function updateCortexGrid() { 2271 2360 const activeAgents = Array.from(state.agents.values()).filter(a => a.event !== 'finish' && a.event !== 'error'); ··· 2954 3043 2955 3044 // Hide dashboard cards and suppress live log rendering 2956 3045 const dashboard = document.querySelector('.health-dashboard'); 2957 - dashboard.querySelectorAll('.vitals-bar, .observe-card, .observers-card, .activity-grids, .think-card, .sync-card').forEach(el => el.style.display = 'none'); 3046 + dashboard.querySelectorAll('.vitals-bar, .observe-card, .observers-card, .registered-observers-card, .activity-grids, .think-card, .sync-card').forEach(el => el.style.display = 'none'); 2958 3047 state.deepLinkMode = true; 2959 3048 elements.logsSummaryBadge.style.display = 'none'; 2960 3049 ··· 3048 3137 // Sweep stale agents and imports every 60s 3049 3138 let staleSweepTimer = setInterval(sweepStale, 60000); 3050 3139 3140 + loadRegisteredObservers(); 3141 + let registeredObserversTimer = setInterval(loadRegisteredObservers, 60000); 3142 + 3051 3143 // Connection health indicator — updated every 5s 3052 3144 let connectionHealthTimer = setInterval(updateConnectionHealth, 5000); 3053 3145 ··· 3058 3150 staleSweepTimer = null; 3059 3151 clearInterval(connectionHealthTimer); 3060 3152 connectionHealthTimer = null; 3153 + clearInterval(registeredObserversTimer); 3154 + registeredObserversTimer = null; 3061 3155 if (elapsedTimer) { 3062 3156 clearInterval(elapsedTimer); 3063 3157 elapsedTimer = null; ··· 3070 3164 startElapsedTimer(); 3071 3165 } 3072 3166 staleSweepTimer = setInterval(sweepStale, 60000); 3167 + loadRegisteredObservers(); 3168 + registeredObserversTimer = setInterval(loadRegisteredObservers, 60000); 3073 3169 connectionHealthTimer = setInterval(updateConnectionHealth, 5000); 3074 3170 } 3075 3171 });