Full code quality review of all four application surfaces using automated agents:
code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, and code-simplifier.
Scope
| Project | Description | PR |
|---|
apps/photos | Photos app (Next.js) | #185 |
apps/links | Links app (Next.js) | #185 |
apps/web | PDF converter web app (Next.js) | #186 |
workers/api | Cloudflare Worker + Node API (Hono) | #186 |
Security Fixes
| Severity | Issue | Location | Fix |
|---|
| Critical | Missing authentication on webhook CRUD routes | workers/api/src/routes/webhooks.ts | Added requireAuth middleware to all webhook and admin webhook routes |
| Critical | XSS via unsanitized backgroundImageUrl in CSS url() | apps/links/src/components/editor/BackgroundPicker.tsx | URL validation + CSS.escape() |
| Critical | XSS via unescaped group title in iframe embed code | apps/links/src/app/dashboard/[groupId]/share/ShareClient.tsx | HTML-escape title before interpolation |
| Critical | Open redirect via postLoginRedirect in sessionStorage | apps/web/src/app/auth/callback/page.tsx | Validate redirect is relative path (startsWith('/') and not //) |
| High | URL injection in auth callback redirect | apps/web/src/app/auth/callback/page.tsx | encodeURIComponent() on error param |
| High | Timing-unsafe signature comparison | workers/api/src/utils/crypto.ts | Constant-time comparison loop |
| High | PostgREST filter injection in admin user search | workers/api/src/routes/admin.ts | Sanitize search input before .or() filter |
| High | Hardcoded Supabase anon key in version control | apps/photos/next.config.js | Removed; use env var only |
Bug Fixes
| Issue | Location | Fix |
|---|
displayName vs display_name field mismatch | apps/photos/src/app/dashboard/page.tsx | Read both fields with fallback |
album.name undefined (API returns title) | Photos dashboard + profile page | Fall back to album.title |
photoCount renders βundefined photosβ | Photos profile + dashboard | Default to 0 |
| Keyboard nav fires during text input | apps/photos/src/app/[slug]/photos/[photoId]/PhotoPageClient.tsx | Guard against INPUT/TEXTAREA focus |
Slideshow prefersReducedMotion stale value | apps/photos/src/components/photos/Slideshow.tsx | Use matchMedia change listener |
| PhotoUploadZone false success on failure | apps/photos/src/components/photos/PhotoUploadZone.tsx | Return success/failure from onSubmit |
Missing 'class' step in onboarding | apps/links/src/app/dashboard/onboarding/page.tsx | Added back to STEPS array |
| N+1 query fetching all view rows | apps/links/src/lib/db/analytics.ts | Per-ID count queries via Promise.all |
| ColorScheme preference lost on reload | apps/web/src/lib/color-scheme.tsx | Read localStorage on mount |
| Email branding uses wrong color | workers/api/src/services/email.ts, middleware/tenant.ts | #0284c7 β #054fb9 |
| Supabase placeholder client at runtime | All 3 web apps | Throw error instead of silent placeholder |
Error Handling Improvements
Silent failures fixed (empty catch blocks, missing user feedback):
| Pattern | Count | Apps |
|---|
Empty .catch(() => {}) replaced with error state/logging | 15+ | links, photos, worker API |
| Missing error feedback on user actions (delete, download, save) | 8 | photos, web |
console.error only (no user notification) | 5 | photos, web |
| Stripe webhook empty catch blocks | 5 | worker API |
Fire-and-forget without .catch() | 3 | worker API |
Specific fixes:
- Photos: Added error feedback to delete, create album, edit photo, like actions
- Links: All editor drag-and-drop reorder operations now show save errors; clipboard copy falls back to
window.prompt(); QR code generation shows error
- Web app: Download functions wrapped in
safeDownload with alert; delete/zip-download notify via toast; getPreferences() calls have .catch()
- Worker API: Stripe webhook catch blocks log errors; team quota alerts log on failure;
optionalAuth logs warnings; api-key-auth last_used_at has error handling; share emails use Promise.allSettled
Type Safety Improvements
| Issue | Location | Fix |
|---|
updateClassGroup template param was string | apps/links/src/lib/db/class-groups.ts | Changed to TemplateId |
ThemeMode duplicated LinkTheme | apps/links/src/components/public/ThemeToggle.tsx | Import from types.ts |
reorderClassGroups/Sections/Links discarded errors | apps/links/src/lib/db/class-groups.ts | Check results and throw on failure |
fetchApi returned undefined as T | apps/photos/src/lib/api.ts | Parse text first, handle empty responses |
| Analytics test mocked wrong code path | apps/links/src/__tests__/lib/db/analytics.test.ts | Rewrote to match per-ID count implementation |
API_BASE_URL bypassed validateUrl() | 3 photos page components | Import from api.ts |
Accessibility
| Issue | Location | Fix |
|---|
Settings tab missing role="tablist" | apps/links/src/app/dashboard/settings/page.tsx | Added to container div |
FileReader.onerror missing | apps/links/src/components/class-group/BulkImportDialog.tsx | Added handler |
Code Cleanup
- Removed 14
[form-debug] console.log statements from production code (4 files in apps/web)
- Removed
console.error calls from auth-context.tsx in photos, links, and web apps
- Removed unused
supabase imports in photos ProfilePageClient and PhotoPageClient
- Simplified redundant
supabase.auth.getSession() calls to use session from auth context
Test Verification
| Project | Typecheck | Tests |
|---|
apps/photos | β
0 errors | β
73/73 passed |
apps/links | β
0 errors | β
144/144 passed |
apps/web | β
0 errors | β
75/75 passed |
workers/api | β
0 errors | β
1729/1729 passed |
Known Remaining Items (documented, not addressed)
These were identified by the agents but deferred as lower priority or requiring larger refactors:
- Purple/violet/indigo colors in web app UI components (CLAUDE.md violation) β cosmetic, needs design pass
any types in TeamDashboardClient.tsx catch blocks and api.ts return types β needs proper error/response types
- Dashboard bypasses
photosApi client β uses raw fetch; should be migrated to centralized client
- Photo/Album types need normalization β dual
snake_case/camelCase fields cause ?? fallback patterns
InstructorProfile defined 3 times in links app β should be extracted to shared type
- DB mapper functions use unsafe
as casts β should use Supabase generated types or Zod validation
- Gateway routes have no authentication β rate limiting only at gateway Worker level
- Spend limits βfail openβ without user notification when DB is unreachable
- Test coverage gaps:
auth-context.tsx, api.ts (validateUrl, fetchApi), onboarding.ts, social-icons.ts need dedicated tests