diff --git a/docs/polish-audit.md b/docs/polish-audit.md new file mode 100644 index 0000000..9b6abcb --- /dev/null +++ b/docs/polish-audit.md @@ -0,0 +1,80 @@ +# Pulse Polish Audit + +**Date:** 2026-03-16 +**Version:** 0.15.0-alpha + +## Current State + +Pulse is a mature product — 55+ routes, 156 API handlers, 50 migrations, 8 background workers, 3 third-party integrations. The codebase is clean with zero TODO/FIXME comments. Recent work has focused on UX refinements: empty state CTAs, animated number transitions, inline bar charts, and mobile responsiveness fixes. + +## Polish Opportunities + +### High Impact + +#### ~~1. Custom 404 Page~~ ✅ Already Done +- Branded 404 page exists at `app/not-found.tsx` with "Go back home" and "View FAQ" CTAs + +#### 2. Rate Limit (429) UX Feedback +- **Issue:** When rate limited, API requests fail silently with no user feedback +- **Effort:** Low +- **Action:** Catch 429 responses in the API client and show a toast with retry timing (`Retry-After` header) + +### Medium Impact + +#### 3. Export Progress Indicator +- **Issue:** PDF/Excel exports have no progress bar and can appear frozen on large datasets +- **Effort:** Low +- **Action:** Add a determinate or indeterminate progress bar inside `ExportModal.tsx` + +#### 4. Filter Application Feedback +- **Issue:** Applying filters has no loading/transition indicator +- **Effort:** Low +- **Action:** Show a subtle loading state on the dashboard when filters change and data is refetching + +#### 5. Inline Form Validation +- **Issue:** All validation errors go to toasts only; no inline field-level error messages +- **Effort:** Medium +- **Action:** Add inline error messages below form fields (funnel creation, goal creation, site settings) + +#### 6. Accessibility: Modal Focus Trapping +- **Issue:** Modals don't trap focus, breaking keyboard-only navigation +- **Effort:** Medium +- **Action:** Implement focus trapping in modal components (VerificationModal, ExportModal, settings modals) + +#### 7. Accessibility: ARIA Live Regions +- **Issue:** Real-time visitor count updates aren't announced to screen readers +- **Effort:** Low +- **Action:** Add `aria-live="polite"` to `RealtimeVisitors.tsx` and other auto-updating elements + +#### 8. Table Pagination Loading +- **Issue:** No loading indicator when paginating through table data +- **Effort:** Low +- **Action:** Show a loading spinner or skeleton overlay when fetching the next page of results + +### Low Impact + +#### 9. Remove Unused `axios` Dependency +- **Issue:** `apiRequest` is the actual HTTP client; `axios` is dead weight in the bundle +- **Effort:** Trivial +- **Action:** `npm uninstall axios` and verify no imports reference it + +#### 10. PWA Service Worker Caching +- **Issue:** `@ducanh2912/next-pwa` is configured but no offline caching strategy exists +- **Effort:** Medium +- **Action:** Define a caching strategy for static assets and API responses, or remove the PWA dependency + +#### 11. Image Lazy Loading +- **Issue:** Table/list images (favicons, flags) don't use `loading="lazy"` +- **Effort:** Low +- **Action:** Add `loading="lazy"` to images in referrer lists, country tables, and similar components + +#### 12. OpenAPI Specification +- **Issue:** Backend has no Swagger/OpenAPI doc; README serves as the only API documentation +- **Effort:** High +- **Action:** Generate an OpenAPI spec from Go handler annotations or write one manually + +## Additional Notes + +- **No i18n** — English only. Worth planning if expanding to international markets. +- **Test coverage** — 16 backend test files cover core logic well, but GSC/BunnyCDN sync and report delivery e2e are gaps. +- **Chart.tsx is 35KB** — A candidate for splitting into sub-components eventually. diff --git a/lib/api/client.ts b/lib/api/client.ts index 5014fc5..4d348e5 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -335,6 +335,15 @@ async function apiRequest( } const errorBody = await response.json().catch(() => ({})) + + // * Capture Retry-After header on 429 so callers can show precise timing + if (response.status === 429) { + const retryAfter = response.headers.get('Retry-After') + if (retryAfter) { + errorBody.retryAfter = parseInt(retryAfter, 10) + } + } + const message = authMessageFromStatus(response.status) throw new ApiError(message, response.status, errorBody) } diff --git a/lib/swr/dashboard.ts b/lib/swr/dashboard.ts index 9dc62e1..8b15a85 100644 --- a/lib/swr/dashboard.ts +++ b/lib/swr/dashboard.ts @@ -2,6 +2,7 @@ // * Implements stale-while-revalidate pattern for efficient data updates import useSWR from 'swr' +import { toast } from '@ciphera-net/ui' import { getDashboard, getDashboardOverview, @@ -105,7 +106,15 @@ const dashboardSWRConfig = { errorRetryInterval: 5000, // * Don't retry on 429 (rate limit) or 401/403 (auth) — retrying makes it worse onErrorRetry: (error: any, _key: string, _config: any, revalidate: any, { retryCount }: { retryCount: number }) => { - if (error?.status === 429 || error?.status === 401 || error?.status === 403) return + if (error?.status === 429) { + const retryAfter = error?.data?.retryAfter + const message = retryAfter + ? `Too many requests. Please try again in ${retryAfter} seconds.` + : 'Too many requests. Please wait a moment and try again.' + toast.error(message, { id: 'rate-limit' }) + return + } + if (error?.status === 401 || error?.status === 403) return if (retryCount >= 3) return setTimeout(() => revalidate({ retryCount }), 5000 * Math.pow(2, retryCount)) }, diff --git a/package-lock.json b/package-lock.json index 5c13c9c..5148ded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.15.0-alpha", "dependencies": { - "@ciphera-net/ui": "^0.2.6", + "@ciphera-net/ui": "^0.2.7", "@ducanh2912/next-pwa": "^10.2.9", "@phosphor-icons/react": "^2.1.10", "@simplewebauthn/browser": "^13.2.2", @@ -1667,9 +1667,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.2.6", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.6/85b0deed2ec86461502209b098f64e028b49e63e", - "integrity": "sha512-mNlK4FNwWAYiScMcyTP7Y5EEZjSUf8H63EdQUlcTEWoFo4km5ZPrlJcfPsbUsN65YB9OtT+iAiu/XRG4dI0/Gg==", + "version": "0.2.7", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.7/f5f170676cdd1bf53c091a0baa98c2d55a7c999f", + "integrity": "sha512-yvag9cYfX6c8aZ3bKI+i3l9ALJBXg7XL6soIjd65F7NyZN+1mEo1Fb+ARfWgjdNa5HjfexAnEOOVpjwMNPFCfg==", "dependencies": { "@phosphor-icons/react": "^2.1.10", "class-variance-authority": "^0.7.1", diff --git a/package.json b/package.json index ea1f781..cf8b313 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:watch": "vitest" }, "dependencies": { - "@ciphera-net/ui": "^0.2.6", + "@ciphera-net/ui": "^0.2.7", "@ducanh2912/next-pwa": "^10.2.9", "@phosphor-icons/react": "^2.1.10", "@simplewebauthn/browser": "^13.2.2",