personal memory agent
0
fork

Configure Feed

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

feat: add quiet notifications badge for health app stderr errors

+107 -1
+8
apps/health/workspace.html
··· 1282 1282 buf.push(record); 1283 1283 if (buf.length > LOG_BUFFER_SIZE) buf.shift(); 1284 1284 1285 + if (record.stream === 'stderr' && /\bERROR\b/.test(record.line)) { 1286 + window.AppServices?.quietNotifs?.add({ 1287 + source: name, 1288 + message: record.line.replace(/^\S+\s+\[\S+\]\s+/, '').slice(0, 120), 1289 + ts: record.ts 1290 + }); 1291 + } 1292 + 1285 1293 // Update service filter dropdown if new service 1286 1294 if (isNew) { 1287 1295 const opt = document.createElement('option');
+19
convey/static/app.css
··· 942 942 pointer-events: none; 943 943 } 944 944 945 + .status-icon .quiet-notif-badge { 946 + position: absolute; 947 + bottom: 0; 948 + right: 0; 949 + min-width: 16px; 950 + height: 16px; 951 + border-radius: 8px; 952 + padding: 0 3px; 953 + font-size: 10px; 954 + font-weight: 700; 955 + background: #ef4444; 956 + color: white; 957 + display: flex; 958 + align-items: center; 959 + justify-content: center; 960 + pointer-events: none; 961 + user-select: none; 962 + } 963 + 945 964 /* Drag-and-drop states */ 946 965 .facet-pill:active { 947 966 cursor: grabbing;
+46
convey/static/app.js
··· 1357 1357 } 1358 1358 }, 1359 1359 1360 + quietNotifs: (() => { 1361 + let stored; 1362 + try { stored = JSON.parse(localStorage.getItem('solstone:quiet_notifs') || '[]'); } 1363 + catch(e) { stored = []; } 1364 + return { 1365 + _notifs: stored, 1366 + _unviewed: stored.length, 1367 + _nextId: stored.length ? Math.max(...stored.map(n => n.id || 0)) + 1 : 1, 1368 + 1369 + add({ source, message, ts }) { 1370 + const notif = { id: this._nextId++, source, message: message || '', ts: ts || Date.now() }; 1371 + this._notifs.push(notif); 1372 + if (this._notifs.length > 20) this._notifs.shift(); 1373 + this._unviewed++; 1374 + this._persist(); 1375 + this._updateBadge(); 1376 + }, 1377 + 1378 + markViewed() { 1379 + this._unviewed = 0; 1380 + this._updateBadge(); 1381 + }, 1382 + 1383 + getAll() { 1384 + return [...this._notifs].reverse(); 1385 + }, 1386 + 1387 + _persist() { 1388 + try { 1389 + localStorage.setItem('solstone:quiet_notifs', JSON.stringify(this._notifs)); 1390 + } catch(e) {} 1391 + }, 1392 + 1393 + _updateBadge() { 1394 + const badge = document.getElementById('quiet-notif-badge'); 1395 + if (!badge) return; 1396 + if (this._unviewed > 0) { 1397 + badge.textContent = String(this._unviewed); 1398 + badge.style.display = 'flex'; 1399 + } else { 1400 + badge.style.display = 'none'; 1401 + } 1402 + } 1403 + }; 1404 + })(), 1405 + 1360 1406 /** 1361 1407 * Request browser notification permission 1362 1408 * @returns {Promise<string>} Permission state
+1 -1
convey/templates/app.html
··· 52 52 <div class="facet-bar{% if not app_registry.apps[app].facets_enabled() %} facets-disabled{% endif %}"> 53 53 <div id="hamburger">☰</div> 54 54 <div class="facet-pills-container"></div> 55 - <div class="status-icon">🟢</div> 55 + <div class="status-icon">🟢<span id="quiet-notif-badge" class="quiet-notif-badge" style="display:none"></span></div> 56 56 </div> 57 57 58 58 <!-- Date Nav (centered below facet bar) -->
+33
convey/templates/status_pane.html
··· 14 14 </div> 15 15 </div> 16 16 17 + <div id="quiet-notifs-section" style="display:none; margin-top: 16px;"> 18 + <h3>Quiet Notifications</h3> 19 + <div id="quiet-notifs-list" style="display: flex; flex-direction: column; gap: 4px; font-size: 13px;"></div> 20 + </div> 21 + 17 22 <h3 style="margin-top: 16px;">Recent Activity <span id="notif-bell" style="cursor: pointer; font-size: 14px; margin-left: 6px; opacity: 0.5;" title="Enable browser notifications">🔔</span></h3> 18 23 <div id="notification-history" style="display: flex; flex-direction: column; gap: 4px; font-size: 13px;"> 19 24 <span style="color: #9ca3af;">No recent activity</span> ··· 35 40 36 41 if (statusPaneOpen) { 37 42 statusPane.classList.add('visible'); 43 + window.AppServices?.quietNotifs?.markViewed(); 44 + renderQuietNotifs(); 38 45 } else { 39 46 statusPane.classList.remove('visible'); 40 47 } ··· 87 94 // Update notification history 88 95 updateNotificationHistory(); 89 96 updateBellState(); 97 + } 98 + 99 + function renderQuietNotifs() { 100 + const section = document.getElementById('quiet-notifs-section'); 101 + const list = document.getElementById('quiet-notifs-list'); 102 + if (!section || !list) return; 103 + 104 + const notifs = window.AppServices?.quietNotifs?.getAll() || []; 105 + section.style.display = notifs.length > 0 ? '' : 'none'; 106 + 107 + if (notifs.length === 0) { 108 + list.innerHTML = '<span style="color: #9ca3af;">No quiet notifications</span>'; 109 + return; 110 + } 111 + 112 + const escape = window.AppServices._escapeHtml; 113 + list.innerHTML = notifs.map(n => { 114 + const timeAgo = window.AppServices.notifications._getRelativeTime(n.ts); 115 + const src = escape(n.source || ''); 116 + const msg = escape((n.message || '').slice(0, 120)); 117 + return `<div style="display: flex; align-items: center; gap: 8px; padding: 4px 0;"> 118 + <span style="color: #9ca3af; font-size: 11px; flex-shrink: 0;">${timeAgo}</span> 119 + <code style="flex-shrink: 0; font-size: 11px;">${src}</code> 120 + <span style="color: #fca5a5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${msg}</span> 121 + </div>`; 122 + }).join(''); 90 123 } 91 124 92 125 function updateNotificationHistory() {