personal memory agent
0
fork

Configure Feed

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

support: surface errors with recovery guidance and bound portal retry

- loadAnnouncements() now logs to console.warn and shows a subtle muted
message instead of silently swallowing failures
- loadTickets() error state includes a "try again" retry button
- openTicket() error state includes a "back to tickets" button to
restore the list view
- Reply and feedback submit errors show human-readable messages with
recovery hints instead of generic/raw error text
- PortalClient.register() strips and replaces handle suffix on 409
retry (preventing unbounded handle growth) and raises RuntimeError
after 3 failed attempts

+35 -9
+9 -4
apps/support/portal.py
··· 367 367 self._save_tos(tos_text) 368 368 return tos_text 369 369 370 - def register(self) -> dict[str, Any]: 370 + def register(self, _retry_count: int = 0) -> dict[str, Any]: 371 371 """Run the full welcome-mat registration flow. 372 372 373 373 1. Ensure keypair exists ··· 399 399 ) 400 400 401 401 if resp.status_code == 409: 402 - # Handle already taken — append random suffix 402 + if _retry_count >= 3: 403 + raise RuntimeError( 404 + "could not register — all handle variants were taken after 3 attempts" 405 + ) 406 + import re 403 407 import random 404 408 import string 405 409 410 + base = re.sub(r"-[a-z0-9]{4}$", "", self._handle) 406 411 suffix = "".join( 407 412 random.choices(string.ascii_lowercase + string.digits, k=4) 408 413 ) 409 - self._handle = f"{self.handle}-{suffix}" 410 - return self.register() 414 + self._handle = f"{base}-{suffix}" 415 + return self.register(_retry_count=_retry_count + 1) 411 416 412 417 self._raise_for_status(resp) 413 418 data = resp.json()
+26 -5
apps/support/workspace.html
··· 539 539 '<div class="support-empty-icon">⚠️</div>' + 540 540 '<div class="support-empty-heading">unable to load tickets</div>' + 541 541 '<div class="support-empty-hint">check your connection and try refreshing</div>' + 542 + '<button class="support-empty-action" id="retry-tickets-btn">try again</button>' + 542 543 '</div>'; 544 + const retryBtn = document.getElementById('retry-tickets-btn'); 545 + if (retryBtn) retryBtn.addEventListener('click', () => loadTickets()); 543 546 } 544 547 } 545 548 ··· 741 744 document.getElementById('reply-text').value = ''; 742 745 setTimeout(() => openTicket(id), 500); 743 746 } catch (e) { 744 - status.textContent = e.message || 'Failed to send.'; 747 + status.textContent = "couldn't send your reply — check your connection and try again"; 745 748 status.className = 'support-status-msg error'; 746 749 } 747 750 replyBtn.disabled = false; ··· 781 784 }); 782 785 } 783 786 } catch (e) { 784 - detail.innerHTML = '<p>Failed to load ticket.</p>'; 787 + detail.innerHTML = '<div class="support-empty">' + 788 + '<div class="support-empty-icon">⚠️</div>' + 789 + '<div class="support-empty-heading">couldn\'t load this ticket</div>' + 790 + '<div class="support-empty-hint">try going back and selecting it again</div>' + 791 + '<button class="support-empty-action" id="error-back-btn">back to tickets</button>' + 792 + '</div>'; 793 + const errorBackBtn = document.getElementById('error-back-btn'); 794 + if (errorBackBtn) errorBackBtn.addEventListener('click', () => { 795 + detail.classList.remove('active'); 796 + detail.innerHTML = ''; 797 + list.style.display = ''; 798 + }); 785 799 } 786 800 } 787 801 ··· 816 830 throw new Error('Failed'); 817 831 } 818 832 } catch (e) { 819 - status.textContent = 'Failed to submit feedback.'; 833 + status.textContent = "couldn't send your feedback — try again in a moment"; 820 834 status.className = 'support-status-msg error'; 821 835 } 822 836 document.getElementById('feedback-submit').disabled = false; ··· 833 847 834 848 // Load announcements 835 849 async function loadAnnouncements() { 850 + const banner = document.getElementById('support-announcements-banner'); 836 851 try { 837 852 const resp = await fetch('/app/support/api/announcements'); 838 853 if (!resp.ok) return; 839 854 const items = await resp.json(); 840 855 if (!items.length) return; 841 - const banner = document.getElementById('support-announcements-banner'); 842 856 banner.style.display = ''; 843 857 const icons = {'known-issue': '⚠️', 'maintenance': '🔧', 'info': '📢'}; 844 858 banner.innerHTML = items.map(a => 845 859 `<div><h4>${icons[a.type] || '📢'} ${esc(a.title || '')}</h4><p style="margin:0;font-size:0.85rem;">${esc((a.content || '').slice(0, 200))}</p></div>` 846 860 ).join(''); 847 - } catch (e) { /* ignore */ } 861 + } catch (e) { 862 + console.warn('Failed to load announcements:', e); 863 + banner.style.display = ''; 864 + banner.style.background = 'none'; 865 + banner.style.border = 'none'; 866 + banner.style.padding = '0.5rem 0'; 867 + banner.innerHTML = '<p style="margin:0;font-size:0.8rem;color:var(--muted,#595959);">couldn\'t load announcements</p>'; 868 + } 848 869 } 849 870 850 871 // Helpers