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
34 changes: 34 additions & 0 deletions docs/production-incident-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -20290,3 +20290,37 @@ Drizzle schema and runtime Zod schemas. Findings and remediations:
with the active SDK when React Native changes internal renderer boundaries,
and do not globally prefer the ESM `import` condition for mixed CommonJS/ESM
dependency graphs.

## 2026-07-29 — PR refresh cancelled exact-head workflows

- **Status:** Workflow-control cause corrected for PR #2300; final exact-head
validation is required before merge.
- **Symptoms:** After a one-time close/reopen refreshed PR #2300 from stale head
`7e58f89d5` to branch head `88edce888`, the newly created
[CI run 30481887258](https://github.com/Asherlc/dofek/actions/runs/30481887258),
CodeQL, Semgrep, and Mobile Preview OTA runs were cancelled before scheduling
jobs.
- **User impact:** No production impact. PR #2300 remained blocked from merging
despite its corrected head association.
- **Evidence:** The exact CI workflow attempt reported `jobs: []`,
`conclusion: cancelled`, and no failing command or fatal log line because no
job started. The previous
[CI run 30480125800](https://github.com/Asherlc/dofek/actions/runs/30480125800)
still occupied the same `ci-refs/pull/2300/merge` concurrency group with 79
successful jobs while its close-cancelled iOS build propagated through the
mobile and aggregate gates.
- **Root cause:** The close/reopen refresh overlapped the prior same-PR workflow
run. GitHub Actions concurrency arbitration kept the older run active and
cancelled the new exact-head workflows before their jobs could start.
- **Fix / mitigation:** Wait for the stale run to terminate, then rerun each
cancelled exact-head workflow exactly once after the concurrency group is
free. No actual test failure is rerun, and no retry, timeout, fallback, or
workflow change is added.
- **Validation:** Attempts 2 for CI, CodeQL, Semgrep, and Mobile Preview OTA
started with head SHA `88edce888dc48e046d5f056d0d492163a82cee98`
after the stale run terminated. Committing this required incident record
advances the branch again, so its resulting head still requires the normal
exact-head gates.
- **Remaining risk / follow-up:** Merge only after every exact-head attempt
completes successfully. Avoid close/reopen refreshes while a same-PR
concurrency group is still active.
88 changes: 88 additions & 0 deletions docs/superpowers/plans/2026-07-29-mobile-more-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Mobile More Navigation TDD Plan

> **Test-first workflow:** Write each failing test before its production change.

**Goal:** Give responsive web and native mobile users one clear More destination that exposes account settings, Breathwork, and Cycle tracking.

**Behavior:** Responsive web includes More in its primary navigation and `/more` presents accessible links to Account & settings, Breathwork, and Cycle tracking. Native mobile keeps its five primary tabs and presents a More action beside Alerts in every tab header; `/more` exposes the same destinations with native routing and accessibility semantics.

**Scope:** Issue [#2185](https://github.com/Asherlc/dofek/issues/2185) only. Web and native navigation, the new destination screens/routes, focused tests, and Storybook stories are included. Health-data behavior, tab hierarchy, a new navigation dependency, and a speculative shared navigation package are excluded.

**Docs:** [Web architecture](../../../packages/web/README.md), [mobile architecture](../../../packages/mobile/README.md), [TanStack Router links](https://tanstack.com/router/latest/docs/framework/react/guide/navigation/), [Expo Router navigation](https://docs.expo.dev/router/navigating-pages/), and [React Native accessibility roles](https://reactnative.dev/docs/accessibility#accessibilityrole).

---

## Current Evidence

- `packages/web/src/components/AppHeader.tsx` lists Overview through Reports but no Settings, Breathwork, Cycle, or More destination in the responsive menu.
- Web Settings is available only from the desktop-only user card; therefore a narrow viewport has no account/settings entry.
- Native Settings is exposed only from the Today header, Breathwork only from Recovery content, and Cycle tracking only from Settings.
- The native tab layout already contains five primary health domains. A global header action provides consistent discovery without displacing one of those domains or introducing a sixth tab.

## Chosen Design

- Add a real `/more` route on web and native mobile.
- Present exactly three destination entries: Account & settings, Breathwork, and Cycle tracking.
- Add More to the web navigation list so it appears in the mobile menu and desktop sidebar.
- Replace the Today-only native Settings header action with a More action in the shared tab header alongside Alerts.
- Keep each platform's static route metadata local; three labels do not justify a shared domain abstraction.

## Test Strategy

- Web navigation unit tests: More appears in the responsive navigation and points to `/more`.
- Web page unit tests: the More page exposes all three named destinations with link semantics and correct routes.
- Native tab-layout unit tests: the shared header exposes Alerts and More, and More navigates to `/more`.
- Native screen unit tests: all three destinations have link semantics and navigate to the expected Expo Router paths.
- Stories: add web and native More destination stories so the new UI state is reviewable.

## File Structure

- Modify `packages/web/src/components/AppHeader.tsx`, its colocated test, and story route fixture.
- Create `packages/web/src/pages/MorePage.tsx`, `MorePage.test.tsx`, and `MorePage.stories.tsx`.
- Create `packages/web/src/routes/more.tsx` and regenerate `packages/web/src/routeTree.gen.ts`.
- Modify `packages/mobile/app/(tabs)/_layout.tsx` and its colocated test.
- Create `packages/mobile/app/more.tsx`, `more.test.tsx`, and `more.stories.tsx`.
- Modify `packages/mobile/app/_layout.tsx` to register the More screen title.

## Tasks

### Task 1: Add Failing Web Navigation and Page Tests

- [ ] Add a failing AppHeader assertion for a More link to `/more`.
- [ ] Add a failing More page test for Account & settings, Breathwork, and Cycle tracking link targets and semantics.
- [ ] Run `rtk pnpm vitest run packages/web/src/components/AppHeader.test.tsx packages/web/src/pages/MorePage.test.tsx --project unit`.
- [ ] Confirm failures identify the missing navigation entry and page.

### Task 2: Implement the Minimal Web Destination

- [ ] Add More to the existing navigation metadata.
- [ ] Add the More page and `/more` route without adding a navigation dependency.
- [ ] Regenerate the TanStack route tree with `rtk pnpm --dir packages/web typecheck`.
- [ ] Add a Storybook story for the destination.
- [ ] Re-run the focused web tests and confirm they pass.

### Task 3: Add Failing Native Navigation and Screen Tests

- [ ] Extend the tab-layout test to require a global More action routing to `/more`.
- [ ] Add a failing screen test for all three accessible destination links and route pushes.
- [ ] Run `rtk pnpm test:mobile -- packages/mobile/app/(tabs)/_layout.test.tsx packages/mobile/app/more.test.tsx`.
- [ ] Confirm failures identify the missing header action and screen.

### Task 4: Implement the Minimal Native Destination

- [ ] Make the shared tab header render Alerts and More.
- [ ] Remove the Today-only Settings shortcut after the global More destination is available.
- [ ] Add and register the native More screen.
- [ ] Add a native Storybook story for the destination.
- [ ] Re-run the focused native tests and confirm they pass.

### Task 5: Final Verification and Delivery

- [ ] Run `rtk pnpm lint`.
- [ ] Run `rtk pnpm tsc --noEmit`.
- [ ] Run `rtk pnpm --dir packages/web typecheck`.
- [ ] Run `rtk pnpm --dir packages/mobile typecheck`.
- [ ] Run `rtk pnpm test`.
- [ ] Build both web and mobile Storybooks if the environment supports their browser/native bundling prerequisites.
- [ ] Review the diff for web/mobile parity, link semantics, the unchanged five-tab hierarchy, and absence of duplicate navigation tooling.
- [ ] Commit, push, open a PR with `Fixes #2185`, link it from the issue, monitor checks and reviews, address every actionable item, and merge only when all required gates permit.
8 changes: 8 additions & 0 deletions packages/mobile/app/(tabs)/_layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,12 @@ describe("tab layout selected state", () => {

expect(mockPush).toHaveBeenCalledWith("/alerts");
});

it("opens More from the global tab header", () => {
render(<TabsLayout />);

fireEvent.click(screen.getByRole("button", { name: "More" }));

expect(mockPush).toHaveBeenCalledWith("/more");
});
});
33 changes: 17 additions & 16 deletions packages/mobile/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,33 @@ export default function TabsLayout() {
const router = useRouter();
const alerts = useProcessingAlerts();
const activeAlertCount = alerts.data?.alerts.length ?? 0;
const alertsButton = () => (
<AlertsBell activeCount={activeAlertCount} onPress={() => router.push("/alerts")} />
const headerActions = () => (
<View style={styles.headerActions}>
<AlertsBell activeCount={activeAlertCount} onPress={() => router.push("/alerts")} />
<Pressable
onPress={() => router.push("/more")}
style={styles.headerButton}
accessibilityRole="button"
accessibilityLabel="More"
>
<Ionicons
name="ellipsis-horizontal-circle-outline"
size={22}
color={colors.textSecondary}
/>
</Pressable>
</View>
);

return (
<Tabs screenOptions={{ ...tabsScreenOptions, headerRight: alertsButton }}>
<Tabs screenOptions={{ ...tabsScreenOptions, headerRight: headerActions }}>
<Tabs.Screen
name="index"
options={{
title: "Today",
tabBarIcon: ({ color, size, focused }) => (
<Ionicons name={getTabIconName("index", focused)} size={size} color={color} />
),
headerRight: () => (
<View style={styles.headerActions}>
{alertsButton()}
<Pressable
onPress={() => router.push("/settings")}
style={styles.headerButton}
accessibilityRole="button"
accessibilityLabel="Settings"
>
<Ionicons name="settings-outline" size={22} color={colors.textSecondary} />
</Pressable>
</View>
),
}}
/>
<Tabs.Screen
Expand Down
6 changes: 6 additions & 0 deletions packages/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,12 @@ function AuthGate() {
title: "Health Reports",
}}
/>
<Stack.Screen
name="more"
options={{
title: "More",
}}
/>
<Stack.Screen
name="support"
options={{
Expand Down
25 changes: 25 additions & 0 deletions packages/mobile/app/more.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react-native";
import { View } from "react-native";
import { colors } from "../theme";
import MoreScreen from "./more";

const meta = {
title: "Pages/More",
component: MoreScreen,
parameters: {
layout: "fullscreen",
},
decorators: [
(Story) => (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<Story />
</View>
),
],
} satisfies Meta<typeof MoreScreen>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};
48 changes: 48 additions & 0 deletions packages/mobile/app/more.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/** @vitest-environment jsdom */
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

const { mockPush } = vi.hoisted(() => ({ mockPush: vi.fn() }));

vi.mock("@expo/vector-icons", () => ({
Ionicons: () => null,
}));

vi.mock("expo-router", () => ({
useRouter: () => ({ push: mockPush }),
}));

vi.mock("../theme", () => ({
colors: {
accent: "#16a34a",
background: "#ffffff",
border: "#d4d4d8",
surface: "#fafafa",
text: "#18181b",
textSecondary: "#52525b",
},
radius: { lg: 12 },
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
}));

describe("MoreScreen", () => {
beforeEach(() => {
mockPush.mockClear();
});

it.each([
[
"Account & settings. Manage your profile, preferences, data sources, and account.",
"/settings",
],
["Breathwork. Start a guided breathing session and review recent practice.", "/breathwork"],
["Cycle tracking. Review cycle phases and record period dates.", "/cycle"],
] as const)("opens %s from an accessible link", async (label, route) => {
const { default: MoreScreen } = await import("./more");
render(<MoreScreen />);

fireEvent.click(screen.getByRole("link", { name: label }));

expect(mockPush).toHaveBeenCalledWith(route);
});
});
Loading
Loading