Obsługa błędów — dzwoneczek powiadomień + alerty autora
S-06: Error UX — notification bell + author alerts
Plan spisany wstecz 2026-07-04 dla pracy wdrożonej bezpośrednio z rozmowy, aby
/10x-impl-reviewmiał punkt odniesienia. Fazy 1–2 wdrożone (PR #91); Faza 3 świadomie odłożona.
Cel
Nieudane przetworzenia przepisów nie wiszą już w banerze nad listą, tylko żyją w dzwoneczku powiadomień w nagłówku (badge = liczba spraw do uwagi). Mama otwiera dzwoneczek i dla każdego wpisu ma „Ponów” i „Odrzuć”, a na górze „Wyczyść wszystkie”. Żadne udostępnione żądanie nie ginie cicho (NFR). Adresuje aktywny błąd na produkcji: baner nie znikał i przycisk „Ponów” był zepsuty (action="javascript:throw…").
Wymagania wstępne
- S-01: Complete (pipeline
recipe_shares→ Inngestrecipe/extract, status enumpending|completed|failed, magic-link auth, RLS per-user)
Fazy implementacji
Faza 1: Nagłówek + dzwoneczek powiadomień (surface)
src/app/(authenticated)/layout.tsx(nowy) — wspólny nagłówek nad trasami zalogowanymi; pobiera failed shares server-side i podaje do nagłówka.src/app/components/app-header.tsx(nowy) — przyklejony pasek: logo + dzwoneczek + wylogowanie (dotąd tylko na landingu).src/app/components/notification-bell.tsx(nowy,'use client') — badge z liczbą, rozwijany panel z listą nieudanych, per wpis „Ponów”/„Odrzuć”, „Wyczyść wszystkie”, pusty stan; zamykanie na outside-click + Escape;aria-expanded/aria-controls/role="menu".src/lib/failed-shares.ts(nowy) —getFailedShares(): zapytaniestatus='failed' AND recipe_id IS NULL, auto-resolve URL-i, które mają już przepis, dedup po URL;import 'server-only'jako bezpiecznik.src/app/(authenticated)/recipes/dismiss-action.ts(nowy) —dismissShare/dismissAllFailedShares: delete nieudanego wiersza (MVP, bez migracji enuma),revalidatePath.src/app/(authenticated)/recipes/retry-action.ts(zmiana) —retryShare(shareId: number)zamiastFormData; wołany bezpośrednio z dzwoneczka wuseTransition(omija placeholderjavascript:throw).src/app/(authenticated)/recipes/recipes-content.tsx+page.tsx(zmiana) — usunięcie inline banera i przekazywaniafailedShares(przeniesione do layoutu).src/lib/supabase/server.ts(zmiana) — wspólny guardrequireUser()(client + getUser + redirect) dla akcji retry/dismiss.
Faza 2: Odporność + naprawy z code-review
retry-action.ts— gdyinngest.sendpadnie, cofnij status dofailed(zerror_message) zamiast zostawić wpending(który znika z dzwoneczka i gubi żądanie — łamałoby NFR); sprawdź błąd resetującego update; zostań na trasie przezrevalidatePathzamiastredirect(dzwoneczek jest też na/recipes/[slug]).dismiss-action.ts— sprawdzaj i loguj błąd delete zamiast go połykać.notification-bell.tsx—aria-controlstylko gdy panel otwarty (bez wiszącego IDREF).
Faza 3: Email do autora przy trwałym fail (FR-012 cz. 3) — NIE ROZPOCZĘTA
- Wysyłka transactional email (Resend przez
lib/env.ts) wsrc/inngest/functions.ts, gdy Inngest wyczerpie retry i ekstrakcja trwale padnie: URL + komunikat błędu do autora. - Świadomie odłożona; wymaga decyzji o kanale (Otwarte Pytanie #4 w mapie). Faza pozostaje oczekująca.
Kryteria sukcesu
Automated Verification
-
pnpm typecheckprzechodzi -
pnpm lint— 0 błędów -
pnpm build— kompiluje
Manual Verification
- Nieudany przepis pojawia się w dzwoneczku (badge + wpis) zamiast w banerze
- „Odrzuć” usuwa wpis; „Wyczyść wszystkie” czyści listę
- „Ponów” ponawia i nie renderuje już
javascript:throw - Wpis znika automatycznie, gdy przepis dodał się inną drogą
Progress
Konwencja:
- [ ]oczekujące,- [x]wykonane,— <sha>przy realizacji.
Faza 1 — dzwoneczek (surface)
- Nagłówek
(authenticated)/layout.tsx+app-header.tsx— 9cf4e0a -
notification-bell.tsx(badge, panel, Ponów/Odrzuć/Wyczyść wszystkie, pusty stan) — 9cf4e0a -
getFailedShares()(auto-resolve + dedup) — 9cf4e0a, scoped w 4ce7d5c -
dismiss-action.ts(delete + revalidatePath) — 9cf4e0a -
retryShare(shareId)+ usunięcie inline banera — 9cf4e0a - wspólny
requireUser()— 4ce7d5c - a11y: Escape + aria-controls/role=menu — ef232b7
Faza 2 — odporność (code-review fixes)
- retry: revert do
failedprzy błędzieinngest.send+ sprawdzenie update error — 4cd59e8 - retry:
revalidatePathzamiast redirect (usunięty martwy retrying-toast) — 4cd59e8 - dismiss: sprawdzanie błędu delete — 4cd59e8
-
import 'server-only'wfailed-shares.ts— 4cd59e8 - aria-controls tylko gdy otwarty — 4cd59e8
Faza 3 — email autora (FR-012 cz.3)
- transactional email przy trwałym fail w
src/inngest/functions.ts
What We’re NOT Doing
- Migracja enuma
dismissed— MVP odrzuca przez delete wiersza; miękki soft-delete odłożony. - Placeholder „Zapisuję…” (FR-010) i push (FR-011) — nice-to-have, zaparkowane (V2).
- Pełna ekstrakcja audio / retry z transkrypcją — poza S-06.
- Toast „Ponawiam…” — usunięty; feedback = wpis znika z dzwoneczka (spójnie z „Odrzuć”).