personal memory agent
0
fork

Configure Feed

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

convey: signal month stats failures in date nav

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

+134 -30
+37
convey/static/app.css
··· 461 461 } 462 462 463 463 .date-nav-label { 464 + position: relative; 464 465 text-align: center; 465 466 color: inherit; 466 467 text-decoration: none; ··· 479 480 480 481 .date-nav-label:active { 481 482 background: rgba(0, 0, 0, 0.10); 483 + } 484 + 485 + .date-nav-label[data-stats-warning="true"] { 486 + padding-right: 18px; 487 + } 488 + 489 + .date-nav-label[data-stats-warning="true"]::after { 490 + content: ''; 491 + position: absolute; 492 + top: 4px; 493 + right: 4px; 494 + width: 8px; 495 + height: 8px; 496 + border-radius: 50%; 497 + background: var(--color-warning, #f59e0b); 498 + border: 2px solid var(--facet-bg-primary, #fff); 482 499 } 483 500 484 501 /* Keep date-nav stable during view transitions */ ··· 547 564 max-height: 280px; 548 565 opacity: 1; 549 566 pointer-events: auto; 567 + } 568 + 569 + .mp-error { 570 + margin: 8px 8px 0; 571 + padding: 8px 10px; 572 + border: 1px solid var(--color-warning, #f59e0b); 573 + border-radius: 8px; 574 + background: rgba(245, 158, 11, 0.08); 575 + color: #92400e; 576 + font-size: 12px; 577 + line-height: 1.35; 578 + } 579 + 580 + .mp-error-title { 581 + font-weight: 600; 582 + } 583 + 584 + .mp-error-detail { 585 + margin-top: 2px; 586 + color: #78350f; 550 587 } 551 588 552 589 .mp-weekdays {
+81 -16
convey/static/month-picker.js
··· 22 22 23 23 // External elements (set during init) 24 24 let labelEl = null; 25 + let labelTitle = null; 25 26 26 - // Cache: {YYYYMM: {data, facet}} 27 + // Cache: {YYYYMM: {data, error, facet}} 27 28 const cache = {}; 28 29 const providers = {}; 29 30 ··· 37 38 return now.getFullYear() + 38 39 String(now.getMonth() + 1).padStart(2, '0') + 39 40 String(now.getDate()).padStart(2, '0'); 41 + } 42 + 43 + function escapeHtml(value) { 44 + return String(value).replace(/[&<>"']/g, ch => ({ 45 + '&': '&amp;', 46 + '<': '&lt;', 47 + '>': '&gt;', 48 + '"': '&quot;', 49 + "'": '&#39;' 50 + }[ch])); 51 + } 52 + 53 + function getMonthErrorMessage(error) { 54 + return error?.serverMessage || "Couldn't load month stats."; 55 + } 56 + 57 + function logMonthStatsError(error) { 58 + if (window.logError) { 59 + window.logError(error, { context: 'month-stats' }); 60 + } 61 + } 62 + 63 + function setStatsWarning(monthState) { 64 + if (!labelEl) return; 65 + if (monthState?.error) { 66 + labelEl.dataset.statsWarning = 'true'; 67 + labelEl.setAttribute('title', getMonthErrorMessage(monthState.error)); 68 + return; 69 + } 70 + delete labelEl.dataset.statsWarning; 71 + if (labelTitle !== null) { 72 + labelEl.setAttribute('title', labelTitle); 73 + } else { 74 + labelEl.removeAttribute('title'); 75 + } 40 76 } 41 77 42 78 function isFutureDay(dateStr) { ··· 112 148 } 113 149 114 150 // Data fetching 115 - async function fetchMonthData(ym) { 151 + async function fetchMonthData(ym, facet) { 116 152 const provider = providers[app]; 117 - if (!provider) return null; 153 + if (!provider) return { data: null, error: null }; 118 154 119 155 try { 120 - const facet = window.selectedFacet || null; 121 - return await provider(ym, facet); 122 - } catch (e) { 123 - console.warn(`[MonthPicker] Provider error for ${ym}:`, e); 124 - return null; 156 + const result = await provider(ym, facet); 157 + if (result && typeof result === 'object' && 'data' in result && 'error' in result) { 158 + return { data: result.data, error: result.error || null }; 159 + } 160 + return { data: result, error: null }; 161 + } catch (err) { 162 + return { data: null, error: err }; 125 163 } 126 164 } 127 165 128 - async function getMonthData(ym) { 166 + async function getMonthData(ym, options = {}) { 129 167 const facet = window.selectedFacet || null; 130 168 const cacheKey = ym; 169 + const cached = cache[cacheKey]; 131 170 132 - if (cache[cacheKey]?.facet === facet) { 133 - return cache[cacheKey].data; 171 + if (!options.refresh && cached?.facet === facet && !cached.error) { 172 + return cached; 134 173 } 135 174 136 - const data = await fetchMonthData(ym); 137 - cache[cacheKey] = { data, facet }; 138 - return data; 175 + const result = await fetchMonthData(ym, facet); 176 + if (result.error) { 177 + logMonthStatsError(result.error); 178 + const staleData = cached?.facet === facet ? cached.data : null; 179 + cache[cacheKey] = { 180 + data: staleData || result.data || null, 181 + error: result.error, 182 + facet 183 + }; 184 + return cache[cacheKey]; 185 + } 186 + 187 + cache[cacheKey] = { data: result.data || {}, error: null, facet }; 188 + return cache[cacheKey]; 139 189 } 140 190 141 191 function preloadAdjacentMonths(ym) { ··· 149 199 function render() { 150 200 if (!container) return; 151 201 152 - const data = cache[currentMonth]?.data || {}; 202 + const monthState = cache[currentMonth] || { data: null, error: null }; 203 + const data = monthState.data || {}; 153 204 const daysInMonth = getDaysInMonth(currentMonth); 154 205 const startDay = getStartDayOfWeek(currentMonth); 206 + setStatsWarning(monthState); 155 207 156 208 // Calculate max for heat map scaling 157 209 let maxCount = 0; ··· 212 264 gridHtml += `<div role="row">${cells.slice(i, i + 7).join('')}</div>`; 213 265 } 214 266 267 + let errorHtml = ''; 268 + if (monthState.error && monthState.data === null) { 269 + const serverMessage = monthState.error.serverMessage; 270 + errorHtml = ` 271 + <div class="mp-error" role="status"> 272 + <div class="mp-error-title">Couldn't load month stats.</div> 273 + ${serverMessage ? `<div class="mp-error-detail">${escapeHtml(serverMessage)}</div>` : ''} 274 + </div> 275 + `; 276 + } 277 + 215 278 // Build full HTML — outer div owns the ARIA grid role so the 216 279 // columnheader row and the data rows share the same grid context. 217 280 const html = ` 281 + ${errorHtml} 218 282 <div role="grid" aria-label="${getFullMonthLabel(currentMonth)}"> 219 283 <div class="mp-weekdays" role="row"> 220 284 ${WEEKDAYS.map(d => `<span role="columnheader">${d}</span>`).join('')} ··· 259 323 260 324 async function showMonth(ym) { 261 325 currentMonth = ym; 262 - await getMonthData(ym); 326 + await getMonthData(ym, { refresh: true }); 263 327 render(); 264 328 preloadAdjacentMonths(ym); 265 329 } ··· 422 486 423 487 if (labelEl) { 424 488 dayLabel = labelEl.textContent; 489 + labelTitle = labelEl.getAttribute('title'); 425 490 labelEl.setAttribute('tabindex', '-1'); 426 491 } 427 492
+16 -14
convey/templates/date_nav.html
··· 41 41 // - Facet-aware: {day: {facet: count}} - filtered by selected facet 42 42 // - Simple: {day: count} - returned as-is 43 43 MonthPicker.registerDataProvider(app, async (month, facet) => { 44 - const resp = await fetch(`${baseUrl}api/stats/${month}`); 45 - if (!resp.ok) return {}; 46 - const raw = await resp.json(); 44 + try { 45 + const raw = await window.apiJson(`${baseUrl}api/stats/${month}`); 47 46 48 - // Auto-detect format: if first value is an object, it's facet-aware 49 - const values = Object.values(raw); 50 - if (values.length > 0 && typeof values[0] === 'object' && values[0] !== null) { 51 - // Facet-aware: {day: {facet: count}} 52 - const result = {}; 53 - for (const [day, facetCounts] of Object.entries(raw)) { 54 - result[day] = facet 55 - ? (facetCounts[facet] || 0) 56 - : Object.values(facetCounts).reduce((a, b) => a + b, 0); 47 + // Auto-detect format: if first value is an object, it's facet-aware 48 + const values = Object.values(raw); 49 + if (values.length > 0 && typeof values[0] === 'object' && values[0] !== null) { 50 + // Facet-aware: {day: {facet: count}} 51 + const result = {}; 52 + for (const [day, facetCounts] of Object.entries(raw)) { 53 + result[day] = facet 54 + ? (facetCounts[facet] || 0) 55 + : Object.values(facetCounts).reduce((a, b) => a + b, 0); 56 + } 57 + return { data: result, error: null }; 57 58 } 58 - return result; 59 + return { data: raw, error: null }; 60 + } catch (err) { 61 + return { data: null, error: err }; 59 62 } 60 - return raw; 61 63 }); 62 64 63 65 // Initialize month picker