chore: add 429 errors
This commit is contained in:
80
docs/polish-audit.md
Normal file
80
docs/polish-audit.md
Normal file
@@ -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.
|
||||||
@@ -335,6 +335,15 @@ async function apiRequest<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const errorBody = await response.json().catch(() => ({}))
|
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)
|
const message = authMessageFromStatus(response.status)
|
||||||
throw new ApiError(message, response.status, errorBody)
|
throw new ApiError(message, response.status, errorBody)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// * Implements stale-while-revalidate pattern for efficient data updates
|
// * Implements stale-while-revalidate pattern for efficient data updates
|
||||||
|
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
|
import { toast } from '@ciphera-net/ui'
|
||||||
import {
|
import {
|
||||||
getDashboard,
|
getDashboard,
|
||||||
getDashboardOverview,
|
getDashboardOverview,
|
||||||
@@ -105,7 +106,15 @@ const dashboardSWRConfig = {
|
|||||||
errorRetryInterval: 5000,
|
errorRetryInterval: 5000,
|
||||||
// * Don't retry on 429 (rate limit) or 401/403 (auth) — retrying makes it worse
|
// * 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 }) => {
|
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
|
if (retryCount >= 3) return
|
||||||
setTimeout(() => revalidate({ retryCount }), 5000 * Math.pow(2, retryCount))
|
setTimeout(() => revalidate({ retryCount }), 5000 * Math.pow(2, retryCount))
|
||||||
},
|
},
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.15.0-alpha",
|
"version": "0.15.0-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.2.6",
|
"@ciphera-net/ui": "^0.2.7",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
@@ -1667,9 +1667,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ciphera-net/ui": {
|
"node_modules/@ciphera-net/ui": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.6/85b0deed2ec86461502209b098f64e028b49e63e",
|
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.7/f5f170676cdd1bf53c091a0baa98c2d55a7c999f",
|
||||||
"integrity": "sha512-mNlK4FNwWAYiScMcyTP7Y5EEZjSUf8H63EdQUlcTEWoFo4km5ZPrlJcfPsbUsN65YB9OtT+iAiu/XRG4dI0/Gg==",
|
"integrity": "sha512-yvag9cYfX6c8aZ3bKI+i3l9ALJBXg7XL6soIjd65F7NyZN+1mEo1Fb+ARfWgjdNa5HjfexAnEOOVpjwMNPFCfg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.2.6",
|
"@ciphera-net/ui": "^0.2.7",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user