personal memory agent
0
fork

Configure Feed

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

convey/link: migrate refreshers to wave 0 primitives (wave 2)

Keep last-known pairing status and device state visible on refresh failures.
Use apiJson for both refreshers and surface malformed status payloads.

+46 -9
+46 -9
apps/link/workspace.html
··· 2 2 <header class="link-header"> 3 3 <h1 class="link-h1">Reach your solstone from anywhere</h1> 4 4 <p class="link-trust">spl is blind by construction. Cloudflare and sol pbc cannot see anything inside the tunnel.</p> 5 - <div class="link-status-row"> 5 + <div class="link-status-row" id="link-status-panel"> 6 6 <span id="link-status-indicator" class="link-status-indicator" aria-live="polite"> 7 7 Service status: <span id="link-status-text">loading…</span> 8 8 </span> ··· 131 131 const devicesList = document.getElementById('link-devices-list'); 132 132 const emptyState = document.getElementById('link-empty-state'); 133 133 const lanNudge = document.getElementById('link-lan-nudge'); 134 + let devicesEverLoaded = false; 134 135 135 136 const pairBtn = document.getElementById('link-pair-btn'); 136 137 const pairModal = document.getElementById('link-pair-modal'); ··· 168 169 return `${Math.floor(diff / 86400)} days ago`; 169 170 } 170 171 172 + function clearRefreshError(containerId) { 173 + const container = document.getElementById(containerId); 174 + const siblingError = container?.nextElementSibling; 175 + if (siblingError && siblingError.classList.contains('surface-state-refresh-error')) { 176 + siblingError.remove(); 177 + } 178 + } 179 + 171 180 async function refreshStatus() { 172 181 try { 173 - const r = await fetch('/app/link/api/status', {headers: {'accept': 'application/json'}}); 174 - const data = await r.json(); 182 + const data = await window.apiJson('/app/link/api/status', { headers: { accept: 'application/json' } }); 183 + if (typeof data.enrolled !== 'boolean') { 184 + throw new window.ApiError({ 185 + cause: 'parse', 186 + status: 200, 187 + statusText: 'OK', 188 + serverMessage: 'Unexpected status shape', 189 + url: '/app/link/api/status', 190 + }); 191 + } 175 192 const state = data.enrolled ? 'online' : 'not-enrolled'; 193 + clearRefreshError('link-status-panel'); 176 194 setStatus(state); 177 195 lanNudge.hidden = !!data.lan_accessible; 178 - } catch (e) { 179 - setStatus('offline'); 196 + } catch (err) { 197 + window.logError(err, { context: 'link: refreshStatus failed' }); 198 + window.SurfaceState.replaceLoading('link-status-panel', window.SurfaceState.errorCard({ 199 + heading: "Can't reach pairing service — reload to try again.", 200 + desc: 'Reload to try again.', 201 + serverMessage: err.serverMessage, 202 + })); 180 203 } 181 204 } 182 205 ··· 191 214 192 215 async function refreshDevices() { 193 216 try { 194 - const r = await fetch('/app/link/api/devices', {headers: {'accept': 'application/json'}}); 195 - const data = await r.json(); 217 + const data = await window.apiJson('/app/link/api/devices', { headers: { accept: 'application/json' } }); 218 + clearRefreshError('link-devices-list'); 219 + devicesEverLoaded = true; 196 220 renderDevices(data.devices || []); 197 - } catch (e) { 198 - console.warn('devices fetch failed', e); 221 + } catch (err) { 222 + window.logError(err, { context: 'link: refreshDevices failed' }); 223 + if (!devicesEverLoaded) { 224 + devicesList.innerHTML = window.SurfaceState.errorCard({ 225 + heading: "Couldn't load paired devices", 226 + desc: 'Reload to try again.', 227 + serverMessage: err.serverMessage, 228 + }); 229 + return; 230 + } 231 + window.SurfaceState.replaceLoading('link-devices-list', window.SurfaceState.errorCard({ 232 + heading: "Couldn't refresh paired devices — showing last known state.", 233 + desc: 'Reload to try again.', 234 + serverMessage: err.serverMessage, 235 + })); 199 236 } 200 237 } 201 238