Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ packages/mobile/certs/public-key.pem
.claude/*
!.claude/skills/
.context/
.worktrees/
.codegraph/
.DS_Store
*.swp
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Provider-agnostic fitness/health data pipeline. Syncs data from various provider
- **Server-side metric computation**: All metric values must be computed on the server — never derive, aggregate, or transform metric data in web or iOS client code. The API response should contain every value the UI needs to display. Clients are responsible only for rendering (colors, labels, formatting, layout) — not for computing the numbers they display. This prevents inconsistencies when the same metric appears on multiple screens or platforms. If a client is calling a scoring/calculation function on raw data from the API, that calculation belongs in the server router instead.
- **Good architecture and modeling**: Actively look for opportunities to decouple code, model real-world concepts as proper classes/types, use common interfaces, and apply SOLID principles with domain-driven design. When you see scattered logic that represents a single concept (e.g., "is this provider connected?"), extract it into a model or interface rather than leaving it inline. Prefer domain-driven abstractions over ad-hoc checks spread across the codebase. Follow SOLID principles: single responsibility (each class/module does one thing), open/closed (extend via composition, not modification), Liskov substitution (subtypes must be substitutable), interface segregation (small, focused interfaces), and dependency inversion (depend on abstractions, not concretions). Prefer composition over inheritance — build complex behavior by combining simple, focused components rather than deep class hierarchies. Use dependency injection, strategy patterns, and mixins instead of base classes.
- **Dual-platform parity (web + mobile)**: Every feature, bug fix, and UI change must be implemented on both `packages/web` and `packages/mobile`. When adding a new page, chart, or data view to one platform, implement the equivalent on the other in the same PR. Shared logic lives in domain-specific packages (`@dofek/format`, `@dofek/scoring`, `@dofek/nutrition`, `@dofek/training`, `@dofek/stats`, `@dofek/onboarding`, `@dofek/providers`) — import from there instead of duplicating. Platform-specific code (HealthKit, barcode scanning, Expo secure storage, ECharts vs react-native-svg) stays in the respective package. When reviewing PRs, check that both platforms are updated.
- **Mobile Expo Router route hygiene**: Never colocate tests, stories, fixtures, or helper-only files under `packages/mobile/app/`. Expo Router's `app` directory is exclusively for routes and attempts to treat non-route files there as routes ([Expo Router core concepts](https://docs.expo.dev/router/basics/core-concepts/#6-non-navigation-components-live-outside-the-srcapp-directory)), which can create extra iOS tabs/screens. Put route tests under `packages/mobile/app-tests/`, route stories under `packages/mobile/app-stories/`, and shared route fixtures under `packages/mobile/app-fixtures/`. If a file under `app/` is not a real route/layout/special Expo Router file, move it out instead of hiding it with `href: null`.
- **Always report errors to Sentry**: Never silently swallow errors or only log them. Every `catch` block that handles an unexpected error must call `captureException()` (from `./telemetry` in mobile, or the equivalent in server code) so failures are visible in our error monitoring. Silent `catch(() => {})` blocks are banned — they hide bugs and make debugging impossible.
- **Surface errors to the user by default**: When a server error occurs, send a specific, actionable error message to the client — never hide it behind a generic "Something went wrong" or "Failed to load." Use a TRPCError with an appropriate code (e.g., `PRECONDITION_FAILED`, `NOT_FOUND`) and a human-readable message that tells the user what's wrong and what to do. Clients must display `error.message` from the server, not hardcoded strings. Hiding the real error from the user makes debugging slower and generates support requests that could be self-service.
- **Fail fast, never warn-and-continue**: When a required precondition is missing (env file, config, dependency), fail immediately with a clear error — never log a warning and silently continue with broken state. A deploy that proceeds with an empty `.env.prod` is worse than one that fails loudly. Warnings that don't stop execution are deceptive; they hide the real problem and cause confusing downstream failures.
Expand All @@ -79,7 +80,7 @@ Provider-agnostic fitness/health data pipeline. Syncs data from various provider
- **No exports just for testability**: Never export a function, class, or variable solely because a test needs to access it. Exports define the public API of a module — every export should serve a production consumer. If a test needs to verify internal behavior, test it through the public interface instead. Comments like "exported for testing" are a code smell — if it's worth exporting, it's worth exporting for production use too.
- **No test/optional-only branches**: Never write a code branch that is only reachable in tests. If a dependency is always present in production, model it as a required (non-optional) dependency rather than an optional one guarded with `if (!dep) return []` — that guard is dead in production and silently hides infra failures (e.g. returning empty data instead of erroring when ClickHouse is down). Make required deps non-optional (drop the `?`, make constructor/`createApp` params required) and have test helpers always provide a stub (e.g. `makeMockSensorStore`). If a precondition genuinely can fail at runtime, fail loudly with a specific error — never return a benign empty result.
- **Shared test utilities**: When multiple unit or integration tests need to share mock setups, utility functions, or test data, extract them into a local `test-helpers.ts` file within the same directory. Do not export these helpers from the source file being tested or import them from another `*.test.ts` file.
- **Colocated unit tests**: Unit test files live next to the source file they test, named `<source>.test.ts`. Do not use `__tests__/` directories. For example, `src/db/tokens.ts` has its unit test at `src/db/tokens.test.ts`. Integration tests (`*.integration.test.ts`) can live wherever makes sense.
- **Colocated unit tests**: Unit test files live next to the source file they test, named `<source>.test.ts`. Do not use `__tests__/` directories. For example, `src/db/tokens.ts` has its unit test at `src/db/tokens.test.ts`. Integration tests (`*.integration.test.ts`) can live wherever makes sense. Mobile Expo Router route tests under `packages/mobile/app-tests/**` are the scoped exception because files under `packages/mobile/app/` are route candidates.
- **Test files map 1:1 to source files**: A unit test file should test one source file. If a test file grows too large because it covers several responsibilities, split the production source into smaller SOLID modules/classes and give each source file its own focused colocated test file. Do not split tests by arbitrary scenario while leaving the source file monolithic.
- **Test separation**: Unit tests use `*.test.ts`, integration tests use `*.integration.test.ts`. Unit tests must never need access to external services (databases, APIs). Integration tests must never mock at the module level (`vi.mock`). For 3rd party services in integration tests, mock at the network level with [MSW](https://mswjs.io/) (`setupServer` from `msw/node`), not with constructor-injected fetch or `vi.spyOn(globalThis, 'fetch')`.
- **Use explicit test tiers**: `pnpm test`, `pnpm test:changed`, and `pnpm test:coverage` are Docker-free unit/mobile tiers. Run database-backed tests through `pnpm test:integration`, `pnpm test:all`, `pnpm test:changed:all`, or `pnpm test:coverage:all`; these commands start the current workspace's Compose dependencies and provide `TEST_DATABASE_URL`. Keep CI's unit, mobile, integration-shard, and mutation commands explicit. See [`docs/testing.md`](docs/testing.md#integration-dependencies) and [Vitest test projects](https://vitest.dev/guide/projects).
Expand Down
29 changes: 29 additions & 0 deletions docs/production-incident-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -23202,3 +23202,32 @@ Drizzle schema and runtime Zod schemas. Findings and remediations:
- **Fix / mitigation:** Updated the nine Expo packages to the SDK-compatible patch versions and regenerated the lockfile. Added a scoped `@expo/xcpretty>js-yaml` override to `4.3.1`, leaving unrelated `js-yaml` 3.x and 4.1.x tooling paths unchanged. No audit suppression, retry, timeout, or warning-and-continue behavior was added.
- **Validation:** Frozen install, `pnpm audit --prod --audit-level=high` (no high findings; 1 low and 8 moderate remain), Expo `install --check`, iOS Metro export (3,169 modules), mobile typecheck/lint, and root lint all pass locally.
- **Remaining risk / follow-up:** Confirm the next PR CI run passes the dependency audit and Metro bundle jobs, then monitor the updated Expo packages through the subsequent mobile build.

## 2026-08-07 — PR dependency audit hit no-patch image-size advisories

- **Status:** Fixed in the workspace; the dependency-audit workflow needs a
fresh run from the updated commit.
- **Symptoms / impact:** PR #2446 failed [Test / Dependency Audit](https://github.com/Asherlc/dofek/actions/runs/31224536250/job/93016169639), blocking the PR gate. No production impact was observed.
- **Evidence:** The first fatal command was
`pnpm audit --prod --audit-level=high --ignore-registry-errors`. It reported
high-severity `image-size` advisories
[GHSA-w3rx-r6r6-pgpr](https://github.com/advisories/GHSA-w3rx-r6r6-pgpr)
and
[GHSA-5p2g-fcmc-qvqq](https://github.com/advisories/GHSA-5p2g-fcmc-qvqq)
through the Expo/Metro mobile build graph, plus
[GHSA-2v37-7h3g-55p8](https://github.com/advisories/GHSA-2v37-7h3g-55p8)
for `nanoid`.
- **Root cause:** Newly published advisories invalidated the locked mobile
production dependency graph. The `nanoid` path had a compatible patched 3.x
release, but the GitHub advisory database listed no patched `image-size`
version while npm's latest published `image-size` remained `2.0.2`.
- **Fix / mitigation:** Added a scoped `nanoid@<3.3.17` override to `3.3.17`
and regenerated the lockfile. Added only the two no-patch `image-size` GHSA
IDs to `pnpm-workspace.yaml` `audit.ignore`, preserving the high-severity
audit gate for every other advisory. No retry, timeout, or warn-and-continue
behavior was added.
- **Validation:** `pnpm audit --prod --audit-level=high --ignore-registry-errors`
passes locally after the scoped ignore and patched `nanoid` override.
- **Remaining risk / follow-up:** Remove the two `image-size` audit ignores as
soon as upstream publishes a patched release or Expo/Metro removes the
vulnerable path; rerun the hosted dependency-audit job after this PR commit.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"release:npm:publish": "pnpm --recursive publish --access public --no-git-checks",
"typecheck": "tsc --noEmit",
"lint": "pnpm lint:sandbox && pnpm lint:analytics-sql",
"lint:sandbox": "pnpm lint:exact-versions && biome check . --max-diagnostics=500 && pnpm lint:suppressions && pnpm lint:workflow-downloads && pnpm lint:analytics-policy && pnpm lint:mobile-telemetry && pnpm lint:web-stories && pnpm lint:review-scenarios",
"lint:sandbox": "pnpm lint:exact-versions && biome check . --max-diagnostics=500 && pnpm lint:suppressions && pnpm lint:workflow-downloads && pnpm lint:analytics-policy && pnpm lint:mobile-telemetry && pnpm lint:web-stories && pnpm lint:review-scenarios && pnpm check:mobile-app-routes",
"lint:fix": "biome check --write .",
"lint:exact-versions": "tsx scripts/exact-versions.ts",
"lint:openapi": "redocly lint docs/whoop-api.openapi.yaml --extends minimal",
Expand Down Expand Up @@ -213,7 +213,8 @@
"size": "size-limit",
"depcruise": "depcruise --config .dependency-cruiser.cjs src/ packages/",
"spellcheck": "cspell --no-progress",
"check:mobile-update": "tsx scripts/check-ota-manifest.ts"
"check:mobile-update": "tsx scripts/check-ota-manifest.ts",
"check:mobile-app-routes": "pnpm tsx scripts/check-mobile-app-route-files.ts"
},
"dependencies": {
"@ai-sdk/otel": "1.0.47",
Expand Down
5 changes: 4 additions & 1 deletion packages/mobile/.rnstorybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { StorybookConfig } from "@storybook/react-native";

const config: StorybookConfig = {
stories: ["../components/**/*.stories.?(ts|tsx|js|jsx)", "../app/**/*.stories.?(ts|tsx|js|jsx)"],
stories: [
"../components/**/*.stories.?(ts|tsx|js|jsx)",
"../app-stories/**/*.stories.?(ts|tsx|js|jsx)",
],
addons: ["@storybook/addon-ondevice-actions", "@storybook/addon-ondevice-controls"],
};

Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StorybookConfig } from "@storybook/react-native-web-vite";
const currentDir = dirname(fileURLToPath(import.meta.url));

const config: StorybookConfig = {
stories: ["../components/**/*.stories.@(ts|tsx)", "../app/**/*.stories.@(ts|tsx)"],
stories: ["../components/**/*.stories.@(ts|tsx)", "../app-stories/**/*.stories.@(ts|tsx)"],
framework: "@storybook/react-native-web-vite",
docs: {
autodocs: "tag",
Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
- **Native Modules**: Domain logic for BLE (`WhoopBleModule`) and HealthKit is implemented in Swift. TypeScript only provides the bridge via Expo Modules.

### UI Development
- **Storybook**: Every component MUST have a `.stories.tsx` file (lives in `.storybook` and `.rnstorybook`).
- **Storybook**: `.storybook` and `.rnstorybook` contain Storybook configuration. Route stories live in `app-stories/`; component stories live beside their component under `components/`.
- **Charts**: Use `react-native-svg` for all chart implementations.
- **Navigation**: Uses Expo Router. Screen paths map to `app/`.
- **Expo Router route hygiene**: Never colocate tests, stories, fixtures, or helper-only files under `packages/mobile/app/`. Expo Router's `app` directory is exclusively for routes and attempts to treat non-route files there as routes ([Expo Router core concepts](https://docs.expo.dev/router/basics/core-concepts/#6-non-navigation-components-live-outside-the-srcapp-directory)), which can create extra iOS tabs/screens. Put route tests under `packages/mobile/app-tests/`, route stories under `packages/mobile/app-stories/`, and shared route fixtures under `packages/mobile/app-fixtures/`. If a file under `app/` is not a real route/layout/special Expo Router file, move it out instead of hiding it with `href: null`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Query state handling**: Treat loading, error, and empty as separate UI states. Do not use `query.data ?? []` or similar fallbacks when `query.error` exists. Use `components/QueryStatePanel.tsx` for explicit error/empty/loading states on screens and cards.
- **Loading performance**: Follow `../../docs/performance/loading-performance-runbook.md` for slow screens. Do not blank visible previous/cached data during background refetches; use blocking loading only when no usable data exists, preserve server error messages, and keep sync/refresh invalidation targeted to affected query families.

Expand Down
5 changes: 4 additions & 1 deletion packages/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ See `../../docs/nutrition-ai-input.md` for end-to-end behavior and API flow.

## Project Structure

- `app/`: Expo Router screens (file-based routing).
- `app/`: Expo Router screens (file-based routing). Keep this route-only; Expo documents `app` as route-exclusive and non-route files there can be treated as routes: <https://docs.expo.dev/router/basics/core-concepts/#6-non-navigation-components-live-outside-the-srcapp-directory>.
- `app-tests/`: Vitest tests for Expo Router screens.
- `app-stories/`: Storybook stories for Expo Router screens.
- `app-fixtures/`: Shared fixtures for route tests and route stories.
- `components/`: React Native UI components (SVG-based charts).
- `modules/`: Native Swift modules:
- `background-refresh`: iOS background task registration.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createFixtureDates } from "./_fixture-dates";
import { createFixtureDates } from "./fixture-dates";

describe("createFixtureDates", () => {
afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { QueryClient } from "@tanstack/react-query";
import { describe, expect, it } from "vitest";
import { FoodByDateV2Schema } from "../../types/api";
import { seedFoodStoryQuery } from "./_food-story-fixture";
import { seedFoodStoryQuery } from "./food-story-fixture";

describe("seedFoodStoryQuery", () => {
it("seeds runtime-valid data for the current byDateV2 procedure", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
import {
createProcessingStatusStoryLink,
seedReadyProcessingStatus,
} from "./_processing-status-story-fixture";
} from "./processing-status-story-fixture";

describe("seedReadyProcessingStatus", () => {
it("seeds the exact processing status query used by screenshot stories", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { AppRouter } from "dofek-server/router";
import { useMemo } from "react";
import { View } from "react-native";
import { within } from "storybook/test";
import ActivitiesScreen from "../../app/(tabs)/activities";
import { trpc } from "../../lib/trpc";
import ActivitiesScreen from "./activities";

const mapPreview = {
width: 1024,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Meta, StoryObj } from "@storybook/react-native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { View } from "react-native";
import FoodScreen from "../../app/(tabs)/food";
import { seedFoodStoryQuery } from "../../app-fixtures/(tabs)/food-story-fixture";
import { trpc } from "../../lib/trpc";
import { colors } from "../../theme";
import { seedFoodStoryQuery } from "./_food-story-fixture";
import FoodScreen from "./food";

function localDateString(dayOffset = 0): string {
const date = new Date();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MISSING_PREVIOUS_NIGHT_MESSAGE } from "dofek-server/sleep-need-contract";
import { type ReactNode, useMemo } from "react";
import { View } from "react-native";
import { trpc } from "../../lib/trpc";
import { colors } from "../../theme";
import TodayScreen from "../../app/(tabs)/index";
import {
createProcessingStatusStoryLink,
seedReadyProcessingStatus,
} from "./_processing-status-story-fixture";
import TodayScreen from "./index";
} from "../../app-fixtures/(tabs)/processing-status-story-fixture";
import { trpc } from "../../lib/trpc";
import { colors } from "../../theme";

function localDateString(dayOffset = 0): string {
const date = new Date();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { mobileRecoveryFixtureSchema } from "dofek-server/mobile-dashboard-contr
import type { AppRouter } from "dofek-server/router";
import { useMemo } from "react";
import { View } from "react-native";
import { trpc } from "../../lib/trpc";
import { colors } from "../../theme";
import { createFixtureDates } from "./_fixture-dates";
import RecoveryScreen from "../../app/(tabs)/recovery";
import { createFixtureDates } from "../../app-fixtures/(tabs)/fixture-dates";
import {
createProcessingStatusStoryLink,
seedReadyProcessingStatus,
} from "./_processing-status-story-fixture";
import RecoveryScreen from "./recovery";
} from "../../app-fixtures/(tabs)/processing-status-story-fixture";
import { trpc } from "../../lib/trpc";
import { colors } from "../../theme";

function createRecoveryErrorStoryLink(recoveryUnavailable: boolean): TRPCLink<AppRouter> {
return () =>
Expand Down
Loading
Loading