fix: resolve 500s on notifications, cache errors in SW, and [object Object] on-ramp alert

- Notification routes: wrap GET / and GET /count in try-catch, return
  graceful fallbacks instead of 500s when DB table is missing/unavailable
- getUnreadCount: add null safety (row?.count ?? 0) and catch DB errors
- Service worker: add .catch(() => {}) to all cache.put() calls to
  suppress NetworkError on quota-exceeded or corrupted cache entries
- On-ramp error display: coerce err.error to string so alerts show the
  actual message instead of [object Object]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-10 12:31:14 -07:00
parent 97da6c729f
commit 49f55dffc8
4 changed files with 51 additions and 31 deletions

View File

@ -3873,7 +3873,9 @@ class FolkFlowsApp extends HTMLElement {
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Unknown error" }));
alert(`On-ramp failed: ${err.error || res.statusText}`);
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error) || res.statusText;
console.error("[UserOnRamp] Server error:", res.status, err);
alert(`On-ramp failed: ${msg}`);
return;
}
@ -3894,8 +3896,9 @@ class FolkFlowsApp extends HTMLElement {
// Open on-ramp widget
this.openWidgetModal(data.widgetUrl);
} catch (err) {
console.error("[UserOnRamp] Error:", err);
alert("Failed to start on-ramp. Check console for details.");
const msg = err instanceof Error ? err.message : String(err);
console.error("[UserOnRamp] Error:", msg, err);
alert(`On-ramp failed: ${msg}`);
}
}
@ -3926,7 +3929,9 @@ class FolkFlowsApp extends HTMLElement {
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Unknown error" }));
alert(`On-ramp failed: ${err.error || res.statusText}`);
const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error) || res.statusText;
console.error("[QuickFund] Server error:", res.status, err);
alert(`On-ramp failed: ${msg}`);
return;
}

View File

@ -34,32 +34,42 @@ async function requireAuth(req: Request) {
// ── GET / — Paginated notification list ──
notificationRouter.get("/", async (c) => {
const claims = await requireAuth(c.req.raw);
if (!claims) return c.json({ error: "Authentication required" }, 401);
try {
const claims = await requireAuth(c.req.raw);
if (!claims) return c.json({ error: "Authentication required" }, 401);
const url = new URL(c.req.url);
const unreadOnly = url.searchParams.get("unread") === "true";
const category = url.searchParams.get("category") || undefined;
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 100);
const offset = Number(url.searchParams.get("offset")) || 0;
const url = new URL(c.req.url);
const unreadOnly = url.searchParams.get("unread") === "true";
const category = url.searchParams.get("category") || undefined;
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 100);
const offset = Number(url.searchParams.get("offset")) || 0;
const notifications = await getUserNotifications(claims.sub, {
unreadOnly,
category,
limit,
offset,
});
const notifications = await getUserNotifications(claims.sub, {
unreadOnly,
category,
limit,
offset,
});
return c.json({ notifications });
return c.json({ notifications });
} catch (err) {
console.error("[notifications] GET / failed:", err instanceof Error ? err.message : err);
return c.json({ notifications: [] });
}
});
// ── GET /count — Lightweight unread count (polling fallback) ──
notificationRouter.get("/count", async (c) => {
const claims = await requireAuth(c.req.raw);
if (!claims) return c.json({ error: "Authentication required" }, 401);
try {
const claims = await requireAuth(c.req.raw);
if (!claims) return c.json({ error: "Authentication required" }, 401);
const count = await getUnreadCount(claims.sub);
return c.json({ unreadCount: count });
const count = await getUnreadCount(claims.sub);
return c.json({ unreadCount: count });
} catch (err) {
console.error("[notifications] GET /count failed:", err instanceof Error ? err.message : err);
return c.json({ unreadCount: 0 });
}
});
// ── PATCH /:id/read — Mark one notification as read ──

View File

@ -1156,11 +1156,16 @@ export async function getUserNotifications(
}
export async function getUnreadCount(userDid: string): Promise<number> {
const [row] = await sql`
SELECT COUNT(*)::int as count FROM notifications
WHERE user_did = ${userDid} AND NOT read AND NOT dismissed
`;
return row.count;
try {
const [row] = await sql`
SELECT COUNT(*)::int as count FROM notifications
WHERE user_did = ${userDid} AND NOT read AND NOT dismissed
`;
return row?.count ?? 0;
} catch (err) {
console.error("[notifications] getUnreadCount failed:", err instanceof Error ? err.message : err);
return 0;
}
}
export async function markNotificationRead(id: string, userDid: string): Promise<boolean> {

View File

@ -133,7 +133,7 @@ self.addEventListener("fetch", (event) => {
statusText: response.statusText,
headers,
});
cache.put(event.request, timedResponse);
cache.put(event.request, timedResponse).catch(() => {});
}
return response;
})
@ -171,7 +171,7 @@ self.addEventListener("fetch", (event) => {
return fetch(event.request).then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(STATIC_CACHE).then((cache) => cache.put(event.request, clone));
caches.open(STATIC_CACHE).then((cache) => cache.put(event.request, clone).catch(() => {}));
}
return response;
});
@ -190,7 +190,7 @@ self.addEventListener("fetch", (event) => {
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(HTML_CACHE).then((cache) => cache.put(event.request, clone));
caches.open(HTML_CACHE).then((cache) => cache.put(event.request, clone).catch(() => {}));
}
return response;
})
@ -210,7 +210,7 @@ self.addEventListener("fetch", (event) => {
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(STATIC_CACHE).then((cache) => cache.put(event.request, clone));
caches.open(STATIC_CACHE).then((cache) => cache.put(event.request, clone).catch(() => {}));
}
return response;
})