personal memory agent
0
fork

Configure Feed

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

convey/speakers: migrate loadReview + loadUntilFound to wave 0 primitives (wave 2)

Split review first-paint failures from refresh failures.
Surface deep-link segment paging errors without wiping existing state.

+59 -11
+59 -11
apps/speakers/workspace.html
··· 110 110 padding: 24px 16px; 111 111 } 112 112 113 + .spk-segments-status, 114 + .spk-review-status { 115 + margin: 12px 0; 116 + } 117 + 118 + .spk-segments-status:empty, 119 + .spk-review-status:empty { 120 + display: none; 121 + } 122 + 113 123 .spk-detail > .surface-state { 114 124 flex: 1; 115 125 background: #f9fafb; ··· 1041 1051 <h2 class="spk-segments-header"> 1042 1052 Segments 1043 1053 </h2> 1054 + <div id="spkSegmentsStatus" class="spk-segments-status" aria-live="polite"></div> 1044 1055 <ul class="spk-segments-list" id="spkSegmentList" role="listbox"> 1045 1056 <li class="spk-empty">Loading...</li> 1046 1057 </ul> ··· 1117 1128 }); 1118 1129 } 1119 1130 1131 + function renderRefreshError(message, serverMessage) { 1132 + const detail = serverMessage ? `<p>${escapeHtml(serverMessage)}</p>` : ''; 1133 + return `<div class="surface-state-refresh-error"><strong>${escapeHtml(message)}</strong>${detail}</div>`; 1134 + } 1135 + 1136 + function setSegmentsStatus(message, serverMessage) { 1137 + if (!segmentsStatus) return; 1138 + segmentsStatus.innerHTML = message ? renderRefreshError(message, serverMessage) : ''; 1139 + } 1140 + 1141 + function setReviewStatus(message, serverMessage) { 1142 + const reviewStatus = document.getElementById('spkReviewStatus'); 1143 + if (!reviewStatus) return; 1144 + reviewStatus.innerHTML = message ? renderRefreshError(message, serverMessage) : ''; 1145 + } 1146 + 1120 1147 const METHOD_DISPLAY = { 1121 1148 acoustic: 'voice match', 1122 1149 structural_single_speaker: 'only speaker', ··· 1134 1161 const detailPanel = document.getElementById('spkDetail'); 1135 1162 const ownerBanner = document.getElementById('spkOwnerBanner'); 1136 1163 const discoveryBanner = document.getElementById('spkDiscoveryBanner'); 1164 + const segmentsStatus = document.getElementById('spkSegmentsStatus'); 1137 1165 1138 1166 let segments = []; 1139 1167 let segmentTotal = 0; ··· 1427 1455 fetch(`/app/speakers/api/segments/${day}?limit=${fetchLimit}&offset=0`) 1428 1456 .then(r => r.json()) 1429 1457 .then(data => { 1458 + setSegmentsStatus(); 1430 1459 segments = data.segments || []; 1431 1460 segmentTotal = data.total || 0; 1432 1461 renderSegmentList(); ··· 1449 1478 fetch(`/app/speakers/api/segments/${day}?limit=20&offset=${segments.length}`) 1450 1479 .then(r => r.json()) 1451 1480 .then(data => { 1481 + setSegmentsStatus(); 1452 1482 const newSegments = data.segments || []; 1453 1483 segments = segments.concat(newSegments); 1454 1484 segmentTotal = data.total || 0; ··· 1461 1491 } 1462 1492 1463 1493 function loadUntilFound(key) { 1464 - fetch(`/app/speakers/api/segments/${day}?limit=20&offset=${segments.length}`) 1465 - .then(r => r.json()) 1494 + window.apiJson(`/app/speakers/api/segments/${day}?limit=20&offset=${segments.length}`) 1466 1495 .then(data => { 1496 + setSegmentsStatus(); 1467 1497 const newSegments = data.segments || []; 1468 1498 if (newSegments.length === 0) return; 1469 1499 segments = segments.concat(newSegments); ··· 1473 1503 if (!selectSegmentByKey(key) && segments.length < segmentTotal) { 1474 1504 loadUntilFound(key); 1475 1505 } 1506 + }) 1507 + .catch((err) => { 1508 + setSegmentsStatus("Couldn't load more segments", err.serverMessage); 1509 + window.logError(err, { context: 'speakers: loadUntilFound failed' }); 1476 1510 }); 1477 1511 } 1478 1512 ··· 1629 1663 html += '<button class="spk-expand-btn" id="spkExpandBtn">Show sentences and details</button>'; 1630 1664 } 1631 1665 1666 + html += '<div id="spkReviewStatus" class="spk-review-status" aria-live="polite"></div>'; 1632 1667 html += '<div class="spk-sentences" id="spkSentences"><div class="spk-empty">Loading...</div></div>'; 1633 1668 detailPanel.innerHTML = html; 1634 1669 ··· 1726 1761 1727 1762 function loadReview(seg, source) { 1728 1763 const container = document.getElementById('spkSentences'); 1729 - if (container) { 1764 + const isFirstPaint = Boolean( 1765 + container 1766 + && container.querySelector('.spk-sentence') === null 1767 + && container.textContent.trim() === 'Loading...' 1768 + ); 1769 + if (container && isFirstPaint) { 1730 1770 container.innerHTML = '<div class="spk-empty">Loading...</div>'; 1731 1771 } 1732 1772 1733 - fetch(`/app/speakers/api/review/${day}/${seg.stream}/${seg.key}/${source}`) 1734 - .then(r => r.json()) 1773 + setReviewStatus(); 1774 + window.apiJson(`/app/speakers/api/review/${day}/${seg.stream}/${seg.key}/${source}`) 1735 1775 .then(data => { 1736 1776 if (data.error) { 1737 - if (container) { 1738 - container.innerHTML = `<div class="spk-empty">${escapeHtml(data.error)}</div>`; 1739 - } 1740 - return; 1777 + const err = new Error(data.error); 1778 + err.serverMessage = data.error; 1779 + throw err; 1741 1780 } 1742 1781 1743 1782 currentSentences = data.sentences || []; ··· 1747 1786 playingSentenceId = null; 1748 1787 renderReview(data); 1749 1788 }) 1750 - .catch(() => { 1789 + .catch((err) => { 1790 + window.logError(err, { context: 'speakers: loadReview failed' }); 1751 1791 if (container) { 1752 - container.innerHTML = '<div class="spk-empty">Failed to load review data</div>'; 1792 + if (isFirstPaint) { 1793 + container.innerHTML = window.SurfaceState.errorCard({ 1794 + heading: "Couldn't load review", 1795 + desc: 'Reload to try again.', 1796 + serverMessage: err.serverMessage, 1797 + }); 1798 + } else { 1799 + setReviewStatus("Couldn't reload review — showing last known state.", err.serverMessage); 1800 + } 1753 1801 } 1754 1802 }); 1755 1803 }