ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

add dev server health check and offline detection to extension

Features:
- Check server health on popup init (dev mode only)
- Show 'server offline' state with setup instructions
- 'Check Again' button to retry connection
- Display target server URL for debugging
- 3-second timeout for health checks

Fixes port 8888 conflict workflow - extension now prompts user
to start dev server instead of hanging silently.

byarielm.fyi 90067b8f 917687b4

verified
Changed files
+112 -2
packages
extension
+32
packages/extension/src/lib/api-client.ts
··· 67 67 export function getExtensionVersion(): string { 68 68 return chrome.runtime.getManifest().version; 69 69 } 70 + 71 + /** 72 + * Check if ATlast server is running 73 + * Returns true if server is reachable, false otherwise 74 + */ 75 + export async function checkServerHealth(): Promise<boolean> { 76 + try { 77 + // Try to fetch the root URL with a short timeout 78 + const controller = new AbortController(); 79 + const timeoutId = setTimeout(() => controller.abort(), 3000); 80 + 81 + const response = await fetch(ATLAST_API_URL, { 82 + method: 'HEAD', 83 + signal: controller.signal 84 + }); 85 + 86 + clearTimeout(timeoutId); 87 + 88 + // Any response (even 404) means server is running 89 + return true; 90 + } catch (error) { 91 + console.error('[API Client] Server health check failed:', error); 92 + return false; 93 + } 94 + } 95 + 96 + /** 97 + * Get the API URL (for display purposes) 98 + */ 99 + export function getApiUrl(): string { 100 + return ATLAST_API_URL; 101 + }
+16
packages/extension/src/popup/popup.css
··· 4 4 box-sizing: border-box; 5 5 } 6 6 7 + code { 8 + background: rgba(0, 0, 0, 0.1); 9 + padding: 4px 8px; 10 + border-radius: 4px; 11 + font-family: 'Courier New', monospace; 12 + font-size: 11px; 13 + display: inline-block; 14 + margin: 8px 0; 15 + } 16 + 17 + @media (prefers-color-scheme: dark) { 18 + code { 19 + background: rgba(255, 255, 255, 0.1); 20 + } 21 + } 22 + 7 23 body { 8 24 width: 350px; 9 25 min-height: 400px;
+12
packages/extension/src/popup/popup.html
··· 64 64 <p id="error-message" class="error-message"></p> 65 65 <button id="btn-retry" class="btn-secondary">Try Again</button> 66 66 </div> 67 + 68 + <!-- Server offline state --> 69 + <div id="state-offline" class="state hidden"> 70 + <div class="icon">🔌</div> 71 + <p class="message">ATlast server not running</p> 72 + <p class="error-message"> 73 + Start the dev server:<br> 74 + <code>npx netlify-cli dev --filter @atlast/web</code> 75 + </p> 76 + <p class="hint" id="server-url"></p> 77 + <button id="btn-check-server" class="btn-primary">Check Again</button> 78 + </div> 67 79 </main> 68 80 69 81 <footer>
+52 -2
packages/extension/src/popup/popup.ts
··· 14 14 scraping: document.getElementById('state-scraping')!, 15 15 complete: document.getElementById('state-complete')!, 16 16 uploading: document.getElementById('state-uploading')!, 17 - error: document.getElementById('state-error')! 17 + error: document.getElementById('state-error')!, 18 + offline: document.getElementById('state-offline')! 18 19 }; 19 20 20 21 const elements = { ··· 23 24 finalCount: document.getElementById('final-count')!, 24 25 statusMessage: document.getElementById('status-message')!, 25 26 errorMessage: document.getElementById('error-message')!, 27 + serverUrl: document.getElementById('server-url')!, 26 28 progressFill: document.getElementById('progress-fill')! as HTMLElement, 27 29 btnStart: document.getElementById('btn-start')! as HTMLButtonElement, 28 30 btnUpload: document.getElementById('btn-upload')! as HTMLButtonElement, 29 - btnRetry: document.getElementById('btn-retry')! as HTMLButtonElement 31 + btnRetry: document.getElementById('btn-retry')! as HTMLButtonElement, 32 + btnCheckServer: document.getElementById('btn-check-server')! as HTMLButtonElement 30 33 }; 31 34 32 35 /** ··· 193 196 } 194 197 195 198 /** 199 + * Check server health and show offline state if needed 200 + */ 201 + async function checkServer(): Promise<boolean> { 202 + console.log('[Popup] 🏥 Checking server health...'); 203 + 204 + // Import health check function 205 + const { checkServerHealth, getApiUrl } = await import('../lib/api-client.js'); 206 + 207 + const isOnline = await checkServerHealth(); 208 + 209 + if (!isOnline) { 210 + console.log('[Popup] ❌ Server is offline'); 211 + showState('offline'); 212 + elements.serverUrl.textContent = `Trying to reach: ${getApiUrl()}`; 213 + return false; 214 + } 215 + 216 + console.log('[Popup] ✅ Server is online'); 217 + return true; 218 + } 219 + 220 + /** 196 221 * Initialize popup 197 222 */ 198 223 async function init(): Promise<void> { 199 224 console.log('[Popup] 🚀 Initializing popup...'); 225 + 226 + // Check server health first (only in dev mode) 227 + const { getApiUrl } = await import('../lib/api-client.js'); 228 + const isDev = getApiUrl().includes('127.0.0.1') || getApiUrl().includes('localhost'); 229 + 230 + if (isDev) { 231 + const serverOnline = await checkServer(); 232 + if (!serverOnline) { 233 + // Set up retry button 234 + elements.btnCheckServer.addEventListener('click', async () => { 235 + elements.btnCheckServer.disabled = true; 236 + elements.btnCheckServer.textContent = 'Checking...'; 237 + 238 + const online = await checkServer(); 239 + if (online) { 240 + // Server is back online, re-initialize 241 + init(); 242 + } else { 243 + elements.btnCheckServer.disabled = false; 244 + elements.btnCheckServer.textContent = 'Check Again'; 245 + } 246 + }); 247 + return; 248 + } 249 + } 200 250 201 251 // Get current state 202 252 console.log('[Popup] 📡 Requesting state from background...');