release: v2.1.0 - #2615
Conversation
Replaces custom title header with LargeTitleHeader which includes native back button navigation, matching other screens in the app. Fixes #2548
…031/MD029) CodeRabbit findings on #2553: - MD031: add blank lines around fenced code blocks in both the runbook and the HNSW brainstorm - MD029: restart the post-deploy verification list at 1 (was 4-6, continuing from the pre-merge list which logically belongs to a separate ordered group) Cosmetic, no semantic changes.
Captures the Copilot follow-up surfaced on PR #2554 review while context is fresh. Five options laid out (A: opt-in total, B: drop it, C: approximate via reltuples, D: bounded count via LIMIT, E: accept cost) with investigation plan to pick one once the HNSW fix lands and the count query becomes the visible cost line. Not a blocker on PR #2554 — the count cost pre-dates the HNSW fix. The fix makes the SELECT path ~115× cheaper without making count worse. Captured here so the follow-up doesn't get lost.
fix: add back button to Gear Inventory screen
- Fix email login re-navigating to login screen on submit Router was using dismissTo() which dismissed the modal first then navigated, causing a race that reset inputs and left users on the login screen. Switched to router.replace() for direct navigation. - Remove cancel buttons from all auth and form modals Removed cancel button (headerLeft) from the create-account modal, the sign-in modal, the create trip modal, and the create pack modal. Also cleaned up now-unused useRouter, Pressable, Text, and testIds imports.
Root cause: the layout configured weather-alert-preferences with
headerShown: false, but LargeTitleHeader internally renders
<Stack.Screen options={{ headerShown: true }}> — dynamically
changing header visibility in a modal context.
React Navigation detects this conflict and remounts the entire
screen, resetting all useState to initial values. This is why every
toggle appeared to snap back after being tapped.
Fixes:
- Remove headerShown: false from the layout and use headerLargeTitle: true
instead, so the layout and LargeTitleHeader agree from the start and
no dynamic header visibility change occurs
- Add delaysContentTouches={false} and canCancelContentTouches={false}
to the ScrollView as a secondary defence against iOS UIScrollView
stealing and cancelling touches before UISwitch can complete its
gesture (also observed on iOS 26)
Closes #2540
fix(expo): add contentInsetAdjustmentBehavior to GearInventory ScrollView
… iOS This prop prevents UIScrollView from ever reclaiming the gesture for scrolling once a touch is delivered to a subview. The primary fix (headerLargeTitle: true in the layout) already resolves the snap-back by eliminating the screen remount. The secondary canCancelContentTouches defence is unnecessary and breaks scroll.
fix(expo): auth modal UX improvements
…s-toggle-snap-back fix(expo): fix weather alert preference toggles snapping back on iOS
…a top space
- Move share/edit/delete buttons from title row into getTripDetailOptions headerRight
- Add share handler to getTripDetailOptions using trip data from store
- Replace SafeAreaView edges default with edges={['bottom']} to avoid double
top inset stacking with the navigation header
- Remove now-redundant inline action buttons, AlertComponent ref, and
handleShareTrip/handleDeleteTrip handlers from TripDetailScreen
fix(expo): move trip detail action buttons to nav header, remove extra top space
GET /admin/analytics/db/snapshot returns the same metrics from
docs/runbooks/neon-cost-profiling.md so cost trends are visible from
the admin app without running Neon MCP / SQL editor by hand.
Response shape:
{
generatedAt: ISO8601,
statsResetAt: ISO8601 | null, // pg_stat_database.stats_reset
database: { name, sizeBytes },
tables: [
{ name, estimatedRows,
heapBytes, toastBytes, indexBytes, totalBytes,
seqScans, seqTuplesRead, idxScans, idxTuplesFetched,
inserts, updates, deletes, hotUpdates,
liveTuples, deadTuples,
lastAutovacuum, lastAutoanalyze }
],
indexes: [
{ table, name, bytes, isUnique,
scans, tuplesRead, tuplesFetched }
]
}
Tables ordered by total size DESC; indexes ordered by size DESC.
Implementation in packages/api/src/services/dbMetricsService.ts.
Queries are pure metadata — pg_class for sizing, pg_stat_user_tables +
pg_stat_user_indexes for activity. No row scans on user tables; no
pg_stat_statements dependency (works on Neon without enabling the
extension). Four parallel queries per call; total time stays well
under the 30s HTTP timeout even for large schemas.
Mounted under existing /admin/analytics/ namespace so the admin app's
analytics dashboard can poll it alongside the existing platform +
catalog routes. Auth inherited from admin parent (onBeforeHandle +
adminAuthGuard at packages/api/src/routes/admin/index.ts:245).
Test coverage in packages/api/test/admin.test.ts:
- Returns expected response shape with catalog_items present and
numeric typed fields
- Requires admin auth (raw api() request returns 401)
Refs: docs/runbooks/neon-cost-profiling.md (step 2-3 metrics)
…overage ratchet Five Copilot findings, all valid: 1. **pg_relation_size(reltoastrelid) throws on reltoastrelid=0** COALESCE evaluates AFTER the function call, so the error wins for small / non-TOASTed tables (most of the schema). Wrapped in CASE WHEN c.reltoastrelid = 0 THEN '0' ELSE pg_relation_size(...) END which short-circuits the call. 2. **Use createReadOnlyDb() instead of createDb()** Defense in depth — a future bug introducing a write would be rejected at the Postgres role level rather than silently mutating production. Service is intentionally read-only. 3. **Update constructor to use the read-only client** — follows from (2). 4. **Doc comment promised a statement-timeout that wasn't implemented** Rewrote the doc to reflect actual behavior: queries are pure metadata, total runtime stays well under Neon's 30s HTTP timeout, no explicit statement_timeout wrapper applied. 5. **500 response missing stable error code field** Added \`code: 'ANALYTICS_DB_SNAPSHOT_ERROR'\` matching the existing admin route convention (CATALOG_OVERVIEW_ERROR, ANALYTICS_GROWTH_ERROR etc.). Plus: coverage ratchet failed (98.31% → 92.68% on packages/api) because integration tests at packages/api/test/ aren't coverage-counted under the Cloudflare Workers test pool. Added packages/api/src/services/__tests__/dbMetricsService.test.ts with 5 unit tests covering: - Full snapshot shape with parsed numerics - Graceful fallback when database info query returns no rows - reltoastrelid=0 fixture (toast_bytes='0') parses to toastBytes: 0 - Order preservation from underlying SQL - Four parallel queries fire per snapshot Tests mock createReadOnlyDb at the module level and drive each of the four execute() calls via mockResolvedValueOnce in source order (the order Promise.all fires them).
✨ feat(api): admin DB sizing + activity snapshot endpoint
…ms (F4) (#2566) * 📝 docs: refined embedding-regen hypothesis post-investigation After confirming F4 at the code level in merged catalogService.ts and correlating against the pg_stat_user_tables delta (zero updates in 4 days post-Tier-1-merge), the bug shape is different than first framed: - The diff in upsertCatalogItems is dead code (excluded.col = input.col after UPSERT, so the comparison is structurally always-false). Never fires the 'second regen' branch. - The real waste is upstream in processValidItemsBatch which calls generateManyEmbeddings for EVERY batch row regardless of whether the catalog row already exists with byte-identical embedding. - 5.56M n_tup_upd ÷ 1.79M rows ≈ 3 lifetime ETL re-runs touching every row — matches 'we did a lot of data loading this month for catalog'. Real cost lines (in priority): OpenAI text-embedding-3 API spend, Cloudflare AI Gateway hit count (may already be caching — verify), Postgres n_tup_upd churn (likely HOT-suppressed since text-embedding-3 is deterministic on identical input), Worker compute + network. Fix shape: move the source-text diff upstream into processValidItemsBatch BEFORE generateManyEmbeddings. Bulk-fetch existing rows, compare getEmbeddingText output, generate only for the diff. UPSERT 'set' clause modified to skip embedding column when input embedding is undefined (COALESCE(excluded.embedding, catalog_items.embedding)). Also delete the dead diff in upsertCatalogItems. Open question before designing the fix: is Cloudflare AI Gateway already deduplicating identical embedding requests? If yes, OpenAI $ is already minimal; the fix recovers Worker + Postgres only. If no, the fix recovers full OpenAI $. Checking AI Gateway cache config next. * 🐛 fix(api): skip OpenAI embedding calls for unchanged catalog ETL items Implements the source-text diff upstream of generateManyEmbeddings in processValidItemsBatch, fixing the F4 embedding-regen anomaly captured in docs/brainstorms/2026-06-01-embedding-regen-anomaly-requirements.md. Before this fix: - processValidItemsBatch called generateManyEmbeddings for every batch row regardless of whether the catalog already had a byte-identical embedding from unchanged source text - The diff loop inside upsertCatalogItems was dead code (excluded.col = input.col after UPSERT, so the comparison was structurally always false) — but even if it had worked, it ran AFTER the OpenAI call, so it couldn't recover the cost - Every ETL re-run paid OpenAI \$ for N redundant embedding generations (5.56M lifetime n_tup_upd on catalog_items, ~3 lifetime ETL re-runs) After: - CatalogService.fetchExistingForRegen(skus) bulk-projects each existing row's embedding column + the fields getEmbeddingText reads. Explicit column projection per the U9 lint discipline. - processValidItemsBatch partitions incoming items into "needs fresh embedding" (new + source-text-changed) vs "reuse existing embedding" (existing + source-text-unchanged), and only calls generateManyEmbeddings on the fresh partition - text-embedding-3 is deterministic on identical input, so reused embeddings are byte-equivalent to what would have been regenerated - Dead diff loop in upsertCatalogItems removed; replaced with a comment pointing at this fix Observability: - logger.info('etl.embedding.diff') reports total / fresh / reused counts per batch, so the savings are visible in production logs - The admin DB snapshot endpoint (PR #2565) will start showing flat n_tup_upd growth on catalog_items between ETL runs once this ships Tests in packages/api/src/services/etl/__tests__/processValidItemsBatch.test.ts: - Calls generateManyEmbeddings only for the changed/new partition - Skips it entirely when every item is unchanged - Treats existing-with-null-embedding as needing fresh generation - All-new partition still works Complements the AI Gateway cache toggle (defense in depth): even when that's on, this fix prevents the redundant request from being made at all, saving Worker compute + network + cache pollution. Future optimization (deferred to its own plan): add an embedding_source_hash column on catalog_items so the diff doesn't need to re-fetch JSONB fields (reviews/qas/faqs/etc.) and re-run getEmbeddingText on existing rows. Native Web Crypto API in Workers makes the implementation trivial; the cost is a schema migration plus backfill. Refs: docs/brainstorms/2026-06-01-embedding-regen-anomaly-requirements.md * 🐛 fix(api): address PR #2566 review — short-response guard + COALESCE for reuse + as-unknown Three findings from PR #2566 review (Copilot + CodeRabbit), all valid: 1. **Copilot @ processValidItemsBatch:96** — `freshEmbeddings[freshIndex] ?? null` silently wrote NULL embedding if `generateManyEmbeddings` ever returned short. Lost vector coverage on affected items. Added a length guard: throw if mismatch. Routes execution to the existing fallback path that records the failure on `etl_jobs.total_embedding_failures` and upserts without embeddings. 2. **Copilot @ processValidItemsBatch:98** — reuse partition still passed `embedding: existingEmbedding` to UPSERT, which through `embedding = excluded.embedding` rewrote identical bytes. Counted as n_tup_upd and dirtied the HNSW page even with HOT-update suppression. Defeated half the point of the diff. Now: - Reuse partition passes `embedding: undefined` (→ NULL in INSERT) - upsertCatalogItems set clause for embedding column changed to `embedding = COALESCE(excluded.embedding, catalog_items.embedding)` - Preserves stored vector when input is NULL, takes new value when provided. text-embedding-3 is deterministic so byte equivalence holds. 3. **CodeRabbit @ test fixture** — `as any` on TEST_ENV. Swapped for `as unknown as Parameters<typeof processValidItemsBatch>[0]['env']`. Tests updated to assert the new contract + new test for the short-response throw → fallback path. 5/5 unit tests pass.
landing, guides, admin, and trails each had a `lint: next lint` script with no ESLint deps or config installed (it would error if run). The repo lints with Biome 2.0 (root `bun lint`), so these scripts are dead. Remove them, and document in each next.config that the existing `eslint.ignoreDuringBuilds` is intentional — Biome handles linting, so Next must never invoke ESLint during builds.
# Conflicts: # .github/workflows/web-e2e-tests.yml # apps/expo/features/trips/screens/TripDetailScreen.tsx # apps/expo/features/trips/utils/getTripDetailOptions.tsx # coverage-baselines.json # packages/api/src/index.ts # packages/api/src/routes/catalog/index.ts # packages/api/src/services/catalogService.ts # packages/api/src/services/etl/CatalogItemValidator.ts # packages/api/src/services/etl/processLogsBatch.ts # packages/api/src/services/etl/processValidItemsBatch.ts # packages/api/src/services/retention/__tests__/invalidLogRetention.test.ts # packages/api/src/services/retention/invalidLogRetention.ts # packages/api/src/utils/__tests__/logger.test.ts # packages/api/src/utils/logger.ts # packages/api/src/workflows/catalog-etl-workflow.ts # packages/api/src/workflows/shared/__tests__/chunk-csv-for-r2.test.ts # packages/api/src/workflows/shared/chunkCsvForR2.ts
Fixes the MCP OAuth flow for local development (MCP Inspector) and hardens JWT verification across all environments. ## JWT verification fixes (affect all environments) - token-verify.ts + mcp-token.ts: issuer was compared against bare PACKRAT_API_URL; Better Auth sets iss = baseURL + '/api/auth' — added the suffix so the claim matches - JWKS fetch was constructed from the issuer URL, producing the double-path /api/auth/api/auth/jwks; fixed to use the bare API base URL - Algorithm allowlist was ['ES256', 'RS256']; Better Auth signs with EdDSA (Ed25519) by default — added EdDSA to both verifiers - mcp-token.ts: MCP audience was hardcoded to the production URL; now reads env.PACKRAT_MCP_URL so non-production deployments verify correctly ## MCP Inspector local-dev support - MCP_PUBLIC_URL env var: canonicalResourceUrl and buildWwwAuthenticateHeader now use it in dev so the resource_metadata pointer and JWT aud resolve to the local worker, not the production domain - CORS on /.well-known/* extended to localhost:* origins so the browser can read metadata documents during Inspector-driven OAuth discovery - CORS applied to 401 responses on /mcp so Inspector sees the WWW-Authenticate header instead of a blocked cross-origin response - DO fetch wrapped in a 503-retry to handle brief Durable Object hibernation between the initialize WebSocket closing and notifications/initialized arriving - validAudiences includes http://localhost:8788/mcp in local dev and env.PACKRAT_MCP_URL + /mcp when set, so the resource param in OAuth authorize requests is accepted - PACKRAT_MCP_URL added to env-validation.ts schema (optional URL) - seed-inspector-oauth-client.ts: pre-registers MCP Inspector as a public OAuth client (DCR is disabled; Inspector needs a seeded row) ## OAuth consent UI - sign-in-page.tsx + signInRoute: Better Auth redirects unauthenticated users to /api/auth/sign-in mid-OAuth-flow; the route and rendered page were missing, causing a 404 that broke the authorize redirect loop - consent-page.tsx: updated to POST JSON to /api/auth/oauth2/consent; sanitizeOAuthConsentRequest in index.ts updated to parse JSON accordingly
…nges - token-verify.test.ts: split ISSUER into API_BASE + '/api/auth' to match Better Auth's actual iss claim (base URL + '/api/auth') - mcp-token.test.ts: update expected issuer and add EdDSA to algorithm allowlist to match resolveMcpBearerUser implementation - auth-cors.test.ts: add signInRoute to consent-route mock and make appBase.use() chainable to support .use(signInRoute).use(consentRoute)
…oauth fix(mcp): local-dev OAuth flow + JWT verification correctness
…een Workers Cloudflare Workers cannot make outbound HTTP requests to other Workers in the same account via their workers.dev URLs — the runtime returns error 1042 instead of the actual response. This caused verifyMcpToken to fail JWKS verification with `Expected 200 OK` on every deployed request, making the MCP connector unusable. Adding the global_fetch_strictly_public compatibility flag routes all fetch() calls through the public internet, bypassing the same-account restriction and allowing the MCP Worker to reach the API Worker's /api/auth/jwks endpoint. Local dev was unaffected because the tunnel URLs (trycloudflare.com) are on a different domain and don't trigger the restriction.
resolveMcpBearerUser fetches /api/auth/jwks from env.PACKRAT_API_URL to verify MCP JWTs. On workers.dev, this is a self-referential HTTP call that also triggers error 1042, causing JWT verification to silently return null and all authenticated API endpoints to 401. Same root cause as the MCP JWKS fix (error 1042 on same-account workers.dev fetch). global_fetch_strictly_public routes the JWKS fetch through the public internet, breaking the self-loop.
…y-public fix(mcp): add global_fetch_strictly_public to unblock JWKS fetch between Workers
…ctor metadata Two connector issues: 1. The WWW-Authenticate scope hint defaulted to `mcp:read`, so Claude.ai only requested read-only access. Changed the default in `buildWwwAuthenticateHeader` to `mcp:write` so the initial auth flow grants write access. (The glossary already documented `mcp:write` as "the default scope Claude.ai requests" — code now matches.) 2. `buildResourceMetadata` omitted `logo_uri`, so Claude.ai had no logo to display for the PackRat connector. Added `logo_uri` pointing at the 256px MCP logo already hosted on packratai.com.
…e-and-logo fix(mcp): request write scope by default and expose logo_uri in connector metadata
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
packrat-admin | a1b4362 | Commit Preview URL Branch Preview URL |
Jun 28 2026, 06:03 PM |
|
Important Review skippedToo many files! This PR contains 920 files, which is 770 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (17)
📒 Files selected for processing (920)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for packages/units (./packages/units)
File CoverageNo changed files found. |
Coverage Report for packages/analytics (./packages/analytics)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Coverage Report for apps/expo (./apps/expo)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Coverage Report for packages/utils (./packages/utils)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Coverage Report for packages/overpass (./packages/overpass)
File CoverageNo changed files found. |
Summary
Release notes
Test plan