Date: 2026-03-30
Auditor: Matt (subagent)
Scope: Tab layout, icon mapping, onboarding, settings, profile, rate limiting
Status: DOES NOT MATCH spec.
The spec calls for "Protocols" and "Clinical Tools" only. The current layout has 4 visible tabs:
- Home
- Clinical Tools
- Settings
- Profile
The _layout.tsx Tabs.Screen list:
Visible: home | tools | settings | profile
Hidden (href: null): search | calculator | coverage | history
GlassTabBar.tsx defines VISIBLE_TABS = ["home", "tools", "settings", "profile"] — all four render in the bottom bar.
The spec says "Protocols" and "Clinical Tools" only. Current reality: 4 tabs, no "Protocols" label (it's "Home"). Tanner needs to decide:
- Is "Home" the renamed "Protocols" tab? If so, rename its label to "Protocols".
- Should Settings and Profile be hidden from the bottom bar (moved to a header/gear icon)?
Action required: Clarify the intended tab structure with Tanner before making changes.
Status: 7 UNMAPPED icons found (will render as help-outline question mark).
The mapping in components/ui/icon-symbol.tsx contains 95 entries total. All but 7 of the actually-used icons are mapped. The following SF Symbol names are used in the codebase but have no entry in MAPPING and will fall back to help-outline:
| SF Symbol | Where Used | Suggested MaterialIcon |
|---|---|---|
bell |
settings.tsx (Notifications row) | notifications |
envelope.fill |
settings.tsx (Contact Us row) | email |
ellipsis.message.fill |
settings.tsx (Feedback row) | feedback |
checkmark.shield.fill |
settings.tsx (Privacy Policy row) | verified-user |
exclamationmark.bubble.fill |
unknown component | report-problem |
building.fill |
unknown component | business |
arrow.right.square |
unknown component | logout or open-in-new |
The mapping already has bell.fill but NOT bell (no fill variant). Settings uses the unfilled version.
Note: checkmark.shield.fill is listed in the mapping section comments but NOT in the MAPPING object itself — it's missing. exclamationmark.shield.fill IS in the mapping (different name).
Status: No "pink blob" placeholder images found — the flow is entirely text/list-based.
The app/onboarding.tsx is a 3-step functional flow with zero images or illustration components:
- Step 1 — State selection (scrollable FlatList of all 50 US states + search)
- Step 2 — Agency selection (tRPC
agencies.listByStatequery, filterable) - Step 3 — Demo search ("cardiac arrest" against selected agency, shows 5 results)
There are no Image components, no SVGs, no placeholder blobs in this file. The step indicator is a simple row of colored dots (red #EF4444 for active, dark #374151 for inactive — the dot turns red, not pink).
Possible source of "pink blob" reports:
- Could be on a different screen (login, splash, or a welcome screen not yet audited)
- Could be a cached/old build showing a placeholder
Imagecomponent from a previous version - Could be in
app/index.tsx,app/login.tsx, or a splash screen component
Recommendation: Audit app/index.tsx, app/login.tsx, and any welcome/splash components for <Image> placeholders with pink backgrounds.
Status: Mostly functional shell — navigation routes need implementation.
- Screen renders correctly with
ScreenContainer+ScrollView useColors()theming wired up- Version display (
expo-constantsversion fromapp.json) SettingsRowcomponent renders icon + label + description + chevron
- Feedback →
router.push("/feedback") - Terms of Service →
router.push("/terms") - Privacy Policy →
router.push("/privacy") - Disclaimer →
router.push("/disclaimer") - Contact Us →
router.push("/contact")
- Notifications row — has
onPressundefined, no navigation, no toggle, no permissions request. Renders but does nothing when tapped. bellicon is unmapped (renders ashelp-outlineon Android/web)ellipsis.message.fillicon is unmapped (Feedback row uses wrong icon on Android/web)
- No notification preference toggles (push, email, in-app)
- No dark/light theme toggle (that's on Profile instead)
- No agency change option (users can't re-onboard from Settings)
Status: Substantially built — most real functionality is present.
- Auth loading states with skeleton screens
- Unauthenticated state →
SignInScreencomponent - Profile error state with retry messaging
ProfileHeader— name, email, tier badge, subscription status badgeSubscriptionCard— shows for Pro users, links to Stripe portal (createPortalmutation)UsageCard— shows query count/limit for free tier usersRecentQueriesCard— shows last 5 queries viatrpc.query.historyOfflineCacheCard— shows cache size, item count (animated counter), clear cache buttonFavoritesCard— lists saved protocols, remove button functionalUpgradeCard— shown to free users, connects to Stripe checkout (createCheckoutmutation)RoleSelector(lazy-loaded) — functional peruseRolecontextThemeToggle(lazy-loaded) — functionalReferralCard(lazy-loaded) — presentFeedbackCard(lazy-loaded) — presentSupportMenu+LegalMenu— navigation links- Logout with confirmation modal
- Clear cache with confirmation modal
- Error modal for Stripe failures
- Admin panel shortcut (hardcoded to tanner@thefiredev.com and christiansafina@gmail.com)
- Haptics on destructive actions
useFocusEffectto trigger animated counters on tab switch
- Admin email list is hardcoded — should be role/claim-based, not email comparison
- Hero stats only shows Tier + Favorites count — no protocol views, search count, etc.
ReferralCard— unknown if referral backend is implemented (not audited)FeedbackCard— unknown if feedback submission backend is implemented (not audited)isTrustedRedirectUrlvalidation for Stripe redirects — implementation not verified here but the call exists
bolt.heart.fill— mapped ✓person.badge.shield.checkmark.fill— mapped ✓paintbrush.fill— mapped ✓lock.shield.fill— mapped ✓exclamationmark.triangle.fill— mapped ✓
Status: Fully implemented — 3-tier system with test bypass.
Three middleware functions in server/_core/middleware/rate-limit.ts:
1. createEnforceRateLimit (user-based daily limit)
- Checks
getUserUsage()from DB against tier's daily limit - Free: limited (DB-configured), Pro: unlimited (
-1) - Sets
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-Afterheaders - Throws
TOO_MANY_REQUESTSwith upgrade message on breach
2. createEnforcePublicRateLimit (IP-based, unauthenticated endpoints)
- Default: 10 requests per 15 minutes per IP
- In-memory
Mapstore, cleaned every 5 minutes - Test bypass confirmed: checks
ctx.req.headers["x-pg-test-key"]againstprocess.env.PG_TEST_SECRET - Throws
TOO_MANY_REQUESTSwith retry seconds on breach
3. createEnforceUserAwareRateLimit (tiered, high-volume search endpoints)
- Free: 50 req / 15 min
- Pro: 500 req / 15 min
- Enterprise: 2000 req / 15 min
- Falls back to IP-based for unauthenticated requests
- Same in-memory store pattern with 5-minute cleanup
The bypass header X-PG-Test-Key is checked in createEnforcePublicRateLimit only:
const testSecret = ctx.req.headers["x-pg-test-key"]?.toString();
if (testSecret && testSecret === process.env.PG_TEST_SECRET) {
return next();
}The bypass is NOT implemented in createEnforceUserAwareRateLimit or createEnforceRateLimit. If E2E tests hit authenticated endpoints, they will hit the rate limit.
- In-memory store only — rate limits are per-process and do not survive restarts. Multi-process/multi-instance deployments will not share state. If server is load-balanced, limits are effectively multiplied by instance count.
- No Redis or distributed backing store.
- Clarify tab layout — Spec says "Protocols" and "Clinical Tools" only. Current: 4 tabs. Decide if Settings/Profile get collapsed into a header icon or if spec has changed.
- Fix 7 unmapped icons —
bell,envelope.fill,ellipsis.message.fill,checkmark.shield.fill,exclamationmark.bubble.fill,building.fill,arrow.right.squareall fall back tohelp-outlineon Android/web. Quick fix: add entries to the MAPPING object. - Track down "pink blob" — Not in
onboarding.tsx. Checkapp/index.tsx,app/login.tsx, splash screen, and any welcome illustration components.
- Notifications in Settings — Row is present but taps do nothing. Either wire up
expo-notificationspermissions + preferences, or remove the row until it's ready. - Rate limit test bypass coverage — Add the
X-PG-Test-Keybypass tocreateEnforceUserAwareRateLimitandcreateEnforceRateLimitso E2E tests can hit authenticated endpoints without hitting daily limits. - Admin email hardcode — Replace
user.email === 'tanner@thefiredev.com'with a properrole === 'admin'claim from the auth context. Security risk if email changes.
- Distributed rate limiting — In-memory store is fine for single-instance. If server ever scales horizontally, add Redis backing for
publicRateLimitStoreanduserRateLimitStore. - Agency change from Settings — Users currently can't change their state/agency after onboarding without re-installing or a developer workaround.
- Rename "Home" tab to "Protocols" — If Home = Protocols, update the label in both
_layout.tsxandGlassTabBar.tsxTAB_CONFIG.