Skip to content

Latest commit

 

History

History
205 lines (156 loc) · 10 KB

File metadata and controls

205 lines (156 loc) · 10 KB

Protocol Guide — App Audit

Date: 2026-03-30
Auditor: Matt (subagent)
Scope: Tab layout, icon mapping, onboarding, settings, profile, rate limiting


Tab Layout

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:

  1. Is "Home" the renamed "Protocols" tab? If so, rename its label to "Protocols".
  2. 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.


Icons

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).


Onboarding

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:

  1. Step 1 — State selection (scrollable FlatList of all 50 US states + search)
  2. Step 2 — Agency selection (tRPC agencies.listByState query, filterable)
  3. 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 Image component 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.


Settings

Status: Mostly functional shell — navigation routes need implementation.

Built / Working

  • Screen renders correctly with ScreenContainer + ScrollView
  • useColors() theming wired up
  • Version display (expo-constants version from app.json)
  • SettingsRow component renders icon + label + description + chevron

Functional rows (navigates somewhere)

  • Feedbackrouter.push("/feedback")
  • Terms of Servicerouter.push("/terms")
  • Privacy Policyrouter.push("/privacy")
  • Disclaimerrouter.push("/disclaimer")
  • Contact Usrouter.push("/contact")

Placeholder / Non-functional

  • Notifications row — has onPress undefined, no navigation, no toggle, no permissions request. Renders but does nothing when tapped.
  • bell icon is unmapped (renders as help-outline on Android/web)
  • ellipsis.message.fill icon is unmapped (Feedback row uses wrong icon on Android/web)

Missing

  • 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)

Profile

Status: Substantially built — most real functionality is present.

Built / Working

  • Auth loading states with skeleton screens
  • Unauthenticated state → SignInScreen component
  • Profile error state with retry messaging
  • ProfileHeader — name, email, tier badge, subscription status badge
  • SubscriptionCard — shows for Pro users, links to Stripe portal (createPortal mutation)
  • UsageCard — shows query count/limit for free tier users
  • RecentQueriesCard — shows last 5 queries via trpc.query.history
  • OfflineCacheCard — shows cache size, item count (animated counter), clear cache button
  • FavoritesCard — lists saved protocols, remove button functional
  • UpgradeCard — shown to free users, connects to Stripe checkout (createCheckout mutation)
  • RoleSelector (lazy-loaded) — functional per useRole context
  • ThemeToggle (lazy-loaded) — functional
  • ReferralCard (lazy-loaded) — present
  • FeedbackCard (lazy-loaded) — present
  • SupportMenu + 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
  • useFocusEffect to trigger animated counters on tab switch

Placeholder / Incomplete

  • 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)
  • isTrustedRedirectUrl validation for Stripe redirects — implementation not verified here but the call exists

Icons

  • bolt.heart.fill — mapped ✓
  • person.badge.shield.checkmark.fill — mapped ✓
  • paintbrush.fill — mapped ✓
  • lock.shield.fill — mapped ✓
  • exclamationmark.triangle.fill — mapped ✓

Rate Limiting

Status: Fully implemented — 3-tier system with test bypass.

Implementation

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-After headers
  • Throws TOO_MANY_REQUESTS with upgrade message on breach

2. createEnforcePublicRateLimit (IP-based, unauthenticated endpoints)

  • Default: 10 requests per 15 minutes per IP
  • In-memory Map store, cleaned every 5 minutes
  • Test bypass confirmed: checks ctx.req.headers["x-pg-test-key"] against process.env.PG_TEST_SECRET
  • Throws TOO_MANY_REQUESTS with 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

Test Bypass Verification

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.

Caveats

  • 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.

Recommendations

High Priority

  1. 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.
  2. Fix 7 unmapped iconsbell, envelope.fill, ellipsis.message.fill, checkmark.shield.fill, exclamationmark.bubble.fill, building.fill, arrow.right.square all fall back to help-outline on Android/web. Quick fix: add entries to the MAPPING object.
  3. Track down "pink blob" — Not in onboarding.tsx. Check app/index.tsx, app/login.tsx, splash screen, and any welcome illustration components.

Medium Priority

  1. Notifications in Settings — Row is present but taps do nothing. Either wire up expo-notifications permissions + preferences, or remove the row until it's ready.
  2. Rate limit test bypass coverage — Add the X-PG-Test-Key bypass to createEnforceUserAwareRateLimit and createEnforceRateLimit so E2E tests can hit authenticated endpoints without hitting daily limits.
  3. Admin email hardcode — Replace user.email === 'tanner@thefiredev.com' with a proper role === 'admin' claim from the auth context. Security risk if email changes.

Low Priority

  1. Distributed rate limiting — In-memory store is fine for single-instance. If server ever scales horizontally, add Redis backing for publicRateLimitStore and userRateLimitStore.
  2. Agency change from Settings — Users currently can't change their state/agency after onboarding without re-installing or a developer workaround.
  3. Rename "Home" tab to "Protocols" — If Home = Protocols, update the label in both _layout.tsx and GlassTabBar.tsx TAB_CONFIG.