@burdenoff/microfe-mybodyshield 2026.731.1 → 2026.731.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.d.ts +28 -1
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.d.ts.map +1 -1
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.js +79 -40
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.js.map +1 -1
- package/dist/mybodyshield/lib/offlineSessionQueue.d.ts +76 -0
- package/dist/mybodyshield/lib/offlineSessionQueue.d.ts.map +1 -0
- package/dist/mybodyshield/lib/offlineSessionQueue.js +77 -0
- package/dist/mybodyshield/lib/offlineSessionQueue.js.map +1 -0
- package/dist/mybodyshield/lib/useOfflineSessionSync.d.ts +30 -0
- package/dist/mybodyshield/lib/useOfflineSessionSync.d.ts.map +1 -0
- package/dist/mybodyshield/lib/useOfflineSessionSync.js +80 -0
- package/dist/mybodyshield/lib/useOfflineSessionSync.js.map +1 -0
- package/dist/mybodyshield/pages/DashboardPage.d.ts.map +1 -1
- package/dist/mybodyshield/pages/DashboardPage.js +247 -170
- package/dist/mybodyshield/pages/DashboardPage.js.map +1 -1
- package/dist/mybodyshield/pages/nutrition/NutritionPage.d.ts.map +1 -1
- package/dist/mybodyshield/pages/nutrition/NutritionPage.js +540 -441
- package/dist/mybodyshield/pages/nutrition/NutritionPage.js.map +1 -1
- package/dist/mybodyshield/pages/train/SessionDetailPage.d.ts.map +1 -1
- package/dist/mybodyshield/pages/train/SessionDetailPage.js +242 -173
- package/dist/mybodyshield/pages/train/SessionDetailPage.js.map +1 -1
- package/dist/mybodyshield/types/index.d.ts +46 -0
- package/dist/mybodyshield/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMyBodyShieldApi.js","names":[],"sources":["../../../src/mybodyshield/hooks/useMyBodyShieldApi.ts"],"sourcesContent":["/**\n * MyBodyShield API hooks\n * @module mybodyshield/hooks\n *\n * React-Query hooks wired DIRECTLY to the real `wspace-mybodyshield-svc`\n * federated GraphQL contract (the `myb*` operations). Every query/mutation\n * below uses an inline gql string whose fields match the svc\n * `.hive-schema.graphql` exactly. No mock data.\n *\n * Transport: `graphqlFetch({ gateway: 'workspace', workspaceToken: true })` —\n * the host shell supplies the gateway base URL + a workspace-scoped token.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useMyBodyShield } from '../context';\nimport type {\n MybConnection,\n MybProgram,\n MybProgramStatus,\n MybWorkout,\n MybExercise,\n MybWorkoutSession,\n MybSessionStatus,\n MybSessionSet,\n MybGoal,\n MybGoalStatus,\n MybMealLog,\n MybMealType,\n MybNutritionPlan,\n MybDailyMacros,\n MybBodyMetric,\n MybBodyMetricType,\n MybRecoveryLog,\n MybRecoveryMetric,\n MybCoachingRelationship,\n MybCoachingStatus,\n MybCreateProgramInput,\n MybUpdateProgramInput,\n MybCreateWorkoutInput,\n MybCreateExerciseInput,\n MybStartSessionInput,\n MybLogSetInput,\n MybCompleteSessionInput,\n MybCreateGoalInput,\n MybUpdateGoalInput,\n MybCreateNutritionPlanInput,\n MybUpdateNutritionPlanInput,\n MybLogMealInput,\n MybRecordMetricInput,\n MybLogRecoveryInput,\n MybCreateCoachingInput,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Transport\n// ============================================================================\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in an in-memory cache keyed\n * by operationName+variables for ~15s (mutations are never cached). A read\n * issued right after a write (e.g. React-Query refetch triggered by our\n * `invalidateQueries`) would otherwise return the PRE-mutation response until the\n * TTL expires — making just-created/updated rows invisible until a full reload.\n * fe-libs' documented remedy is that write-performing callers clear those caches;\n * we do exactly that after every myb mutation so every follow-up read is fresh.\n */\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\nconst mybQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// React-Query key roots (one namespace per domain entity).\nconst KEY = {\n programs: ['mybodyshield', 'programs'] as const,\n program: (id: string) => ['mybodyshield', 'program', id] as const,\n programWorkouts: (id: string) => ['mybodyshield', 'program-workouts', id] as const,\n workoutExercises: (id: string) => ['mybodyshield', 'workout-exercises', id] as const,\n sessions: ['mybodyshield', 'sessions'] as const,\n session: (id: string) => ['mybodyshield', 'session', id] as const,\n exerciseBestSet: (name: string) => ['mybodyshield', 'exercise-best-set', name] as const,\n goals: ['mybodyshield', 'goals'] as const,\n goal: (id: string) => ['mybodyshield', 'goal', id] as const,\n meals: ['mybodyshield', 'meals'] as const,\n nutritionPlan: (id: string) => ['mybodyshield', 'nutrition-plan', id] as const,\n activePlan: ['mybodyshield', 'active-plan'] as const,\n dailyMacros: ['mybodyshield', 'daily-macros'] as const,\n bodyMetrics: ['mybodyshield', 'body-metrics'] as const,\n recovery: ['mybodyshield', 'recovery'] as const,\n coaching: ['mybodyshield', 'coaching'] as const,\n};\n\nconst DEFAULT_PAGE: PaginationInput = { limit: 50 };\n\n// ============================================================================\n// Reusable gql field selections (codegen-free strictness)\n// ============================================================================\n\nconst PROGRAM_FIELDS = `\n id workspaceId memberId coachId name description goalType level phase status\n weeks daysPerWeek startDate endDate tags createdAt updatedAt\n`;\n\nconst EXERCISE_FIELDS = `\n id workspaceId workoutId name instructions muscleGroup sets reps targetWeight\n restSeconds orderIndex demo { id } createdAt updatedAt\n`;\n\nconst WORKOUT_FIELDS = `\n id workspaceId programId name description type dayOfWeek weekNumber orderIndex\n durationMin createdAt updatedAt\n`;\n\nconst SET_FIELDS = `\n id workspaceId sessionId exerciseName setNumber reps weight distanceM durationSec\n isPersonalRecord createdAt updatedAt\n`;\n\nconst SESSION_FIELDS = `\n id workspaceId memberId programId workoutId status scheduledFor startedAt completedAt\n caloriesBurned perceivedExertion notes createdAt updatedAt\n`;\n\nconst GOAL_FIELDS = `\n id workspaceId memberId title description type status targetValue currentValue\n baselineValue unit targetDate achievedAt createdAt updatedAt\n`;\n\nconst MEAL_FIELDS = `\n id workspaceId memberId planId name mealType calories proteinG carbsG fatG servings\n isFavorite loggedAt createdAt updatedAt\n`;\n\nconst PLAN_FIELDS = `\n id workspaceId memberId coachId name goalType calorieTarget proteinTarget carbsTarget\n fatTarget startDate endDate isActive notes createdAt updatedAt\n`;\n\nconst METRIC_FIELDS = `\n id workspaceId memberId type value unit measuredAt notes photo { id } createdAt updatedAt\n`;\n\nconst RECOVERY_FIELDS = `\n id workspaceId memberId metric score sleepHours notes loggedAt createdAt updatedAt\n`;\n\nconst COACHING_FIELDS = `\n id workspaceId coachId memberId status specialty shareWorkouts shareNutrition\n shareMetrics startedAt endedAt notes createdAt updatedAt\n`;\n\nconst MACROS_FIELDS = `\n date calories proteinG carbsG fatG calorieTarget proteinTarget carbsTarget fatTarget\n`;\n\n/**\n * `MybWorkout.exercises` is a paginated `MybExerciseConnection` on the wire\n * (see `.hive-schema.graphql`); unwrap it back to the flat `MybExercise[]`\n * shape the rest of the app consumes.\n */\ntype MybWorkoutWithExerciseConnection = Omit<MybWorkout, 'exercises'> & {\n exercises: { items: MybExercise[] };\n};\n\nconst unwrapWorkoutItems = (items: MybWorkoutWithExerciseConnection[]): MybWorkout[] =>\n items.map(({ exercises, ...workout }) => ({ ...workout, exercises: exercises.items }));\n\n// ============================================================================\n// Programs (Training)\n// ============================================================================\n\nexport interface ProgramFilters {\n status?: MybProgramStatus;\n memberId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybPrograms = (filters: ProgramFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.programs, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListPrograms: MybConnection<MybProgram> }>(\n `query MybListPrograms($status: MybProgramStatus, $memberId: ID, $pagination: PaginationInput) {\n mybListPrograms(status: $status, memberId: $memberId, pagination: $pagination) {\n items { ${PROGRAM_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n memberId: filters.memberId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListPrograms;\n },\n });\n};\n\nexport const useMybProgram = (id: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.program(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await mybQuery<{\n mybProgram:\n | (Omit<MybProgram, 'workouts'> & {\n workouts: { items: MybWorkoutWithExerciseConnection[] };\n })\n | null;\n }>(\n `query MybProgram($id: ID!) {\n mybProgram(id: $id) {\n ${PROGRAM_FIELDS}\n workouts {\n items {\n ${WORKOUT_FIELDS}\n exercises { items { ${EXERCISE_FIELDS} } }\n }\n }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n if (!result.mybProgram) return null;\n const { workouts, ...program } = result.mybProgram;\n return { ...program, workouts: unwrapWorkoutItems(workouts.items) };\n },\n });\n};\n\nexport const useMybProgramWorkouts = (programId: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.programWorkouts(programId ?? ''),\n enabled: Boolean(programId),\n queryFn: async () => {\n const result = await mybQuery<{\n mybProgramWorkouts: { items: MybWorkoutWithExerciseConnection[] };\n }>(\n `query MybProgramWorkouts($programId: ID!) {\n mybProgramWorkouts(programId: $programId) {\n items {\n ${WORKOUT_FIELDS}\n exercises { items { ${EXERCISE_FIELDS} } }\n }\n }\n }`,\n { programId },\n apiGatewayUrl\n );\n return unwrapWorkoutItems(result.mybProgramWorkouts.items);\n },\n });\n};\n\n/**\n * Planned exercises for a single workout, in `orderIndex` order — the\n * guided-workout stepper (BOFF-4096) walks a live session's `workoutId`\n * through these to drive the \"next exercise\" flow.\n */\nexport const useMybWorkoutExercises = (workoutId: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.workoutExercises(workoutId ?? ''),\n enabled: Boolean(workoutId),\n queryFn: async () => {\n const result = await mybQuery<{ mybWorkoutExercises: { items: MybExercise[] } }>(\n `query MybWorkoutExercises($workoutId: ID!) {\n mybWorkoutExercises(workoutId: $workoutId, pagination: { limit: 100 }) {\n items { ${EXERCISE_FIELDS} }\n }\n }`,\n { workoutId },\n apiGatewayUrl\n );\n return result.mybWorkoutExercises.items.slice().sort((a, b) => a.orderIndex - b.orderIndex);\n },\n });\n};\n\nexport const useMybCreateProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateProgramInput) => {\n const result = await mybQuery<{ mybCreateProgram: MybProgram }>(\n `mutation MybCreateProgram($input: MybCreateProgramInput!) {\n mybCreateProgram(input: $input) { ${PROGRAM_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateProgram;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.programs }),\n });\n};\n\nexport const useMybUpdateProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybUpdateProgramInput }) => {\n const result = await mybQuery<{ mybUpdateProgram: MybProgram }>(\n `mutation MybUpdateProgram($id: ID!, $input: MybUpdateProgramInput!) {\n mybUpdateProgram(id: $id, input: $input) { ${PROGRAM_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybUpdateProgram;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.programs });\n qc.invalidateQueries({ queryKey: KEY.program(data.id) });\n },\n });\n};\n\nexport const useMybTransitionProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; status: MybProgramStatus }) => {\n const result = await mybQuery<{ mybTransitionProgram: MybProgram }>(\n `mutation MybTransitionProgram($id: ID!, $status: MybProgramStatus!) {\n mybTransitionProgram(id: $id, status: $status) { ${PROGRAM_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybTransitionProgram;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.programs });\n qc.invalidateQueries({ queryKey: KEY.program(data.id) });\n },\n });\n};\n\nexport const useMybArchiveProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybArchiveProgram: boolean }>(\n `mutation MybArchiveProgram($id: ID!) { mybArchiveProgram(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.mybArchiveProgram;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.programs }),\n });\n};\n\nexport const useMybAddWorkout = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateWorkoutInput) => {\n const result = await mybQuery<{ mybAddWorkout: MybWorkout }>(\n `mutation MybAddWorkout($input: MybCreateWorkoutInput!) {\n mybAddWorkout(input: $input) { ${WORKOUT_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybAddWorkout;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.program(data.programId) });\n qc.invalidateQueries({ queryKey: KEY.programWorkouts(data.programId) });\n },\n });\n};\n\nexport const useMybAddExercise = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateExerciseInput) => {\n const result = await mybQuery<{ mybAddExercise: { id: string; workoutId: string } }>(\n `mutation MybAddExercise($input: MybCreateExerciseInput!) {\n mybAddExercise(input: $input) { id workoutId }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybAddExercise;\n },\n onSuccess: () => {\n // Exercises live under a program's workouts; invalidate program + workout\n // caches so the newly-added exercise appears on the program detail tree.\n qc.invalidateQueries({ queryKey: ['mybodyshield', 'program'] });\n qc.invalidateQueries({ queryKey: ['mybodyshield', 'program-workouts'] });\n },\n });\n};\n\n// ============================================================================\n// Sessions / Sets (Training — live logging)\n// ============================================================================\n\nexport interface SessionFilters {\n status?: MybSessionStatus;\n programId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybSessions = (filters: SessionFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.sessions, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListSessions: MybConnection<MybWorkoutSession> }>(\n `query MybListSessions($status: MybSessionStatus, $programId: ID, $pagination: PaginationInput) {\n mybListSessions(status: $status, programId: $programId, pagination: $pagination) {\n items { ${SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n programId: filters.programId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListSessions;\n },\n });\n};\n\nexport const useMybSession = (id: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.session(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await mybQuery<{\n mybSession: (Omit<MybWorkoutSession, 'sets'> & { sets: { items: MybSessionSet[] } }) | null;\n }>(\n `query MybSession($id: ID!) {\n mybSession(id: $id) {\n ${SESSION_FIELDS}\n sets { items { ${SET_FIELDS} } }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n if (!result.mybSession) return null;\n const { sets, ...session } = result.mybSession;\n return { ...session, sets: sets.items };\n },\n });\n};\n\n/**\n * The calling member's own best-ever logged set for `exerciseName` (backend:\n * `mybExerciseBestSet`, self-scoped — no memberId argument). Drives live\n * auto-PR detection in the log-set dialog (BOFF-4096): `null` means the\n * member has never logged this exercise before, so there is no record to\n * beat yet.\n */\nexport const useMybExerciseBestSet = (exerciseName: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const trimmed = exerciseName.trim();\n return useQuery({\n queryKey: KEY.exerciseBestSet(trimmed),\n enabled: trimmed.length > 0,\n queryFn: async () => {\n const result = await mybQuery<{ mybExerciseBestSet: MybSessionSet | null }>(\n `query MybExerciseBestSet($exerciseName: String!) {\n mybExerciseBestSet(exerciseName: $exerciseName) { ${SET_FIELDS} }\n }`,\n { exerciseName: trimmed },\n apiGatewayUrl\n );\n return result.mybExerciseBestSet;\n },\n // Best-set history changes rarely mid-session; a short stale window\n // avoids refiring on every keystroke while the exercise name settles.\n staleTime: 15_000,\n });\n};\n\nexport const useMybStartSession = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybStartSessionInput) => {\n const result = await mybQuery<{ mybStartSession: MybWorkoutSession }>(\n `mutation MybStartSession($input: MybStartSessionInput!) {\n mybStartSession(input: $input) { ${SESSION_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybStartSession;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.sessions }),\n });\n};\n\nexport const useMybLogSet = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybLogSetInput) => {\n const result = await mybQuery<{ mybLogSet: MybSessionSet }>(\n `mutation MybLogSet($input: MybLogSetInput!) {\n mybLogSet(input: $input) { ${SET_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybLogSet;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.session(data.sessionId) });\n qc.invalidateQueries({ queryKey: KEY.sessions });\n // A just-logged set may itself now be the best-ever for this exercise —\n // refresh so the next auto-PR comparison (this session or a later one)\n // sees it (BOFF-4096).\n qc.invalidateQueries({ queryKey: KEY.exerciseBestSet(data.exerciseName) });\n },\n });\n};\n\nexport const useMybCompleteSession = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybCompleteSessionInput }) => {\n const result = await mybQuery<{ mybCompleteSession: MybWorkoutSession }>(\n `mutation MybCompleteSession($id: ID!, $input: MybCompleteSessionInput!) {\n mybCompleteSession(id: $id, input: $input) { ${SESSION_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybCompleteSession;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.session(data.id) });\n qc.invalidateQueries({ queryKey: KEY.sessions });\n },\n });\n};\n\n// ============================================================================\n// Goals (Progress)\n// ============================================================================\n\nexport interface GoalFilters {\n status?: MybGoalStatus;\n memberId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybGoals = (filters: GoalFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.goals, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListGoals: MybConnection<MybGoal> }>(\n `query MybListGoals($status: MybGoalStatus, $memberId: ID, $pagination: PaginationInput) {\n mybListGoals(status: $status, memberId: $memberId, pagination: $pagination) {\n items { ${GOAL_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n memberId: filters.memberId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListGoals;\n },\n });\n};\n\nexport const useMybCreateGoal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateGoalInput) => {\n const result = await mybQuery<{ mybCreateGoal: MybGoal }>(\n `mutation MybCreateGoal($input: MybCreateGoalInput!) {\n mybCreateGoal(input: $input) { ${GOAL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateGoal;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.goals }),\n });\n};\n\nexport const useMybUpdateGoal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybUpdateGoalInput }) => {\n const result = await mybQuery<{ mybUpdateGoal: MybGoal }>(\n `mutation MybUpdateGoal($id: ID!, $input: MybUpdateGoalInput!) {\n mybUpdateGoal(id: $id, input: $input) { ${GOAL_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybUpdateGoal;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.goals }),\n });\n};\n\nexport const useMybAchieveGoal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybAchieveGoal: MybGoal }>(\n `mutation MybAchieveGoal($id: ID!) {\n mybAchieveGoal(id: $id) { ${GOAL_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.mybAchieveGoal;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.goals }),\n });\n};\n\n// ============================================================================\n// Nutrition (plans, meals, macros)\n// ============================================================================\n\nexport const useMybActiveNutritionPlan = (memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.activePlan, memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybActiveNutritionPlan: MybNutritionPlan | null }>(\n `query MybActiveNutritionPlan($memberId: ID) {\n mybActiveNutritionPlan(memberId: $memberId) { ${PLAN_FIELDS} }\n }`,\n { memberId },\n apiGatewayUrl\n );\n return result.mybActiveNutritionPlan;\n },\n });\n};\n\nexport const useMybNutritionPlan = (id: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.nutritionPlan(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await mybQuery<{ mybNutritionPlan: MybNutritionPlan | null }>(\n `query MybNutritionPlan($id: ID!) {\n mybNutritionPlan(id: $id) {\n ${PLAN_FIELDS}\n meals { ${MEAL_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.mybNutritionPlan;\n },\n });\n};\n\nexport interface MealFilters {\n mealType?: MybMealType;\n planId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybMeals = (filters: MealFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.meals, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListMeals: MybConnection<MybMealLog> }>(\n `query MybListMeals($mealType: MybMealType, $planId: ID, $pagination: PaginationInput) {\n mybListMeals(mealType: $mealType, planId: $planId, pagination: $pagination) {\n items { ${MEAL_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n mealType: filters.mealType,\n planId: filters.planId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListMeals;\n },\n });\n};\n\nexport const useMybDailyMacros = (date?: string, memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.dailyMacros, date ?? 'today', memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybDailyMacros: MybDailyMacros }>(\n `query MybDailyMacros($date: DateTime, $memberId: ID) {\n mybDailyMacros(date: $date, memberId: $memberId) { ${MACROS_FIELDS} }\n }`,\n { date, memberId },\n apiGatewayUrl\n );\n return result.mybDailyMacros;\n },\n });\n};\n\nexport const useMybCreateNutritionPlan = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateNutritionPlanInput) => {\n const result = await mybQuery<{ mybCreateNutritionPlan: MybNutritionPlan }>(\n `mutation MybCreateNutritionPlan($input: MybCreateNutritionPlanInput!) {\n mybCreateNutritionPlan(input: $input) { ${PLAN_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateNutritionPlan;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.activePlan });\n },\n });\n};\n\nexport const useMybUpdateNutritionPlan = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybUpdateNutritionPlanInput }) => {\n const result = await mybQuery<{ mybUpdateNutritionPlan: MybNutritionPlan }>(\n `mutation MybUpdateNutritionPlan($id: ID!, $input: MybUpdateNutritionPlanInput!) {\n mybUpdateNutritionPlan(id: $id, input: $input) { ${PLAN_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybUpdateNutritionPlan;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.activePlan });\n qc.invalidateQueries({ queryKey: KEY.nutritionPlan(data.id) });\n },\n });\n};\n\nexport const useMybActivateNutritionPlan = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybActivateNutritionPlan: MybNutritionPlan }>(\n `mutation MybActivateNutritionPlan($id: ID!) {\n mybActivateNutritionPlan(id: $id) { ${PLAN_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.mybActivateNutritionPlan;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.activePlan });\n qc.invalidateQueries({ queryKey: KEY.nutritionPlan(data.id) });\n },\n });\n};\n\nexport const useMybLogMeal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybLogMealInput) => {\n const result = await mybQuery<{ mybLogMeal: MybMealLog }>(\n `mutation MybLogMeal($input: MybLogMealInput!) {\n mybLogMeal(input: $input) { ${MEAL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybLogMeal;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.meals });\n qc.invalidateQueries({ queryKey: KEY.dailyMacros });\n if (data.planId) qc.invalidateQueries({ queryKey: KEY.nutritionPlan(data.planId) });\n },\n });\n};\n\nexport const useMybDeleteMeal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybDeleteMeal: boolean }>(\n `mutation MybDeleteMeal($id: ID!) { mybDeleteMeal(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.mybDeleteMeal;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.meals });\n qc.invalidateQueries({ queryKey: KEY.dailyMacros });\n },\n });\n};\n\n// ============================================================================\n// Body metrics (Progress)\n// ============================================================================\n\nexport interface BodyMetricFilters {\n type?: MybBodyMetricType;\n memberId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybBodyMetrics = (filters: BodyMetricFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.bodyMetrics, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListBodyMetrics: MybConnection<MybBodyMetric> }>(\n `query MybListBodyMetrics($type: MybBodyMetricType, $memberId: ID, $pagination: PaginationInput) {\n mybListBodyMetrics(type: $type, memberId: $memberId, pagination: $pagination) {\n items { ${METRIC_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n type: filters.type,\n memberId: filters.memberId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListBodyMetrics;\n },\n });\n};\n\nexport const useMybRecordBodyMetric = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybRecordMetricInput) => {\n const result = await mybQuery<{ mybRecordBodyMetric: MybBodyMetric }>(\n `mutation MybRecordBodyMetric($input: MybRecordMetricInput!) {\n mybRecordBodyMetric(input: $input) { ${METRIC_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybRecordBodyMetric;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.bodyMetrics }),\n });\n};\n\n// ============================================================================\n// Recovery (Wellbeing)\n// ============================================================================\n\nexport const useMybRecoveryLogs = (metric?: MybRecoveryMetric, memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.recovery, metric ?? 'all', memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybListRecoveryLogs: MybConnection<MybRecoveryLog> }>(\n `query MybListRecoveryLogs($metric: MybRecoveryMetric, $memberId: ID) {\n mybListRecoveryLogs(metric: $metric, memberId: $memberId) {\n items { ${RECOVERY_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { metric, memberId },\n apiGatewayUrl\n );\n return result.mybListRecoveryLogs;\n },\n });\n};\n\nexport const useMybLogRecovery = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybLogRecoveryInput) => {\n const result = await mybQuery<{ mybLogRecovery: MybRecoveryLog }>(\n `mutation MybLogRecovery($input: MybLogRecoveryInput!) {\n mybLogRecovery(input: $input) { ${RECOVERY_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybLogRecovery;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.recovery }),\n });\n};\n\n// ============================================================================\n// Coaching (Wellbeing)\n// ============================================================================\n\nexport const useMybCoaching = (status?: MybCoachingStatus, memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.coaching, status ?? 'all', memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybListCoaching: MybConnection<MybCoachingRelationship> }>(\n `query MybListCoaching($status: MybCoachingStatus, $memberId: ID) {\n mybListCoaching(status: $status, memberId: $memberId) {\n items { ${COACHING_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { status, memberId },\n apiGatewayUrl\n );\n return result.mybListCoaching;\n },\n });\n};\n\nexport const useMybCreateCoaching = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateCoachingInput) => {\n const result = await mybQuery<{ mybCreateCoaching: MybCoachingRelationship }>(\n `mutation MybCreateCoaching($input: MybCreateCoachingInput!) {\n mybCreateCoaching(input: $input) { ${COACHING_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateCoaching;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.coaching }),\n });\n};\n\nexport const useMybTransitionCoaching = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; status: MybCoachingStatus }) => {\n const result = await mybQuery<{ mybTransitionCoaching: MybCoachingRelationship }>(\n `mutation MybTransitionCoaching($id: ID!, $status: MybCoachingStatus!) {\n mybTransitionCoaching(id: $id, status: $status) { ${COACHING_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybTransitionCoaching;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.coaching }),\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useMyBodyShieldApi.js","names":[],"sources":["../../../src/mybodyshield/hooks/useMyBodyShieldApi.ts"],"sourcesContent":["/**\n * MyBodyShield API hooks\n * @module mybodyshield/hooks\n *\n * React-Query hooks wired DIRECTLY to the real `wspace-mybodyshield-svc`\n * federated GraphQL contract (the `myb*` operations). Every query/mutation\n * below uses an inline gql string whose fields match the svc\n * `.hive-schema.graphql` exactly. No mock data.\n *\n * Transport: `graphqlFetch({ gateway: 'workspace', workspaceToken: true })` —\n * the host shell supplies the gateway base URL + a workspace-scoped token.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useMyBodyShield } from '../context';\nimport type {\n MybConnection,\n MybProgram,\n MybProgramStatus,\n MybWorkout,\n MybExercise,\n MybWorkoutSession,\n MybSessionStatus,\n MybSessionSet,\n MybGoal,\n MybGoalStatus,\n MybMealLog,\n MybMealType,\n MybNutritionPlan,\n MybDailyMacros,\n MybBodyMetric,\n MybBodyMetricType,\n MybRecoveryLog,\n MybRecoveryMetric,\n MybCoachingRelationship,\n MybCoachingStatus,\n MybCreateProgramInput,\n MybUpdateProgramInput,\n MybCreateWorkoutInput,\n MybCreateExerciseInput,\n MybStartSessionInput,\n MybLogSetInput,\n MybCompleteSessionInput,\n MybCreateGoalInput,\n MybUpdateGoalInput,\n MybCreateNutritionPlanInput,\n MybUpdateNutritionPlanInput,\n MybLogMealInput,\n MybRecordMetricInput,\n MybLogRecoveryInput,\n MybCreateCoachingInput,\n PaginationInput,\n MybDeviceConnection,\n MybDeviceProvider,\n MybWearableMetricType,\n MybTodayGuidance,\n MybFood,\n} from '../types';\n\n// ============================================================================\n// Transport\n// ============================================================================\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in an in-memory cache keyed\n * by operationName+variables for ~15s (mutations are never cached). A read\n * issued right after a write (e.g. React-Query refetch triggered by our\n * `invalidateQueries`) would otherwise return the PRE-mutation response until the\n * TTL expires — making just-created/updated rows invisible until a full reload.\n * fe-libs' documented remedy is that write-performing callers clear those caches;\n * we do exactly that after every myb mutation so every follow-up read is fresh.\n */\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\nconst mybQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// React-Query key roots (one namespace per domain entity).\nconst KEY = {\n programs: ['mybodyshield', 'programs'] as const,\n program: (id: string) => ['mybodyshield', 'program', id] as const,\n programWorkouts: (id: string) => ['mybodyshield', 'program-workouts', id] as const,\n workoutExercises: (id: string) => ['mybodyshield', 'workout-exercises', id] as const,\n sessions: ['mybodyshield', 'sessions'] as const,\n session: (id: string) => ['mybodyshield', 'session', id] as const,\n exerciseBestSet: (name: string) => ['mybodyshield', 'exercise-best-set', name] as const,\n goals: ['mybodyshield', 'goals'] as const,\n goal: (id: string) => ['mybodyshield', 'goal', id] as const,\n meals: ['mybodyshield', 'meals'] as const,\n nutritionPlan: (id: string) => ['mybodyshield', 'nutrition-plan', id] as const,\n activePlan: ['mybodyshield', 'active-plan'] as const,\n dailyMacros: ['mybodyshield', 'daily-macros'] as const,\n bodyMetrics: ['mybodyshield', 'body-metrics'] as const,\n recovery: ['mybodyshield', 'recovery'] as const,\n coaching: ['mybodyshield', 'coaching'] as const,\n deviceConnections: ['mybodyshield', 'device-connections'] as const,\n guidance: ['mybodyshield', 'guidance'] as const,\n foodSearch: (query: string) => ['mybodyshield', 'food-search', query] as const,\n foodByBarcode: (barcode: string) => ['mybodyshield', 'food-barcode', barcode] as const,\n};\n\nconst DEFAULT_PAGE: PaginationInput = { limit: 50 };\n\n// ============================================================================\n// Reusable gql field selections (codegen-free strictness)\n// ============================================================================\n\nconst PROGRAM_FIELDS = `\n id workspaceId memberId coachId name description goalType level phase status\n weeks daysPerWeek startDate endDate tags createdAt updatedAt\n`;\n\nconst EXERCISE_FIELDS = `\n id workspaceId workoutId name instructions muscleGroup sets reps targetWeight\n restSeconds orderIndex demo { id } createdAt updatedAt\n`;\n\nconst WORKOUT_FIELDS = `\n id workspaceId programId name description type dayOfWeek weekNumber orderIndex\n durationMin createdAt updatedAt\n`;\n\nconst SET_FIELDS = `\n id workspaceId sessionId exerciseName setNumber reps weight distanceM durationSec\n isPersonalRecord createdAt updatedAt\n`;\n\nconst SESSION_FIELDS = `\n id workspaceId memberId programId workoutId status scheduledFor startedAt completedAt\n caloriesBurned perceivedExertion notes createdAt updatedAt\n`;\n\nconst GOAL_FIELDS = `\n id workspaceId memberId title description type status targetValue currentValue\n baselineValue unit targetDate achievedAt createdAt updatedAt\n`;\n\nconst MEAL_FIELDS = `\n id workspaceId memberId planId name mealType calories proteinG carbsG fatG servings\n isFavorite loggedAt barcode createdAt updatedAt\n`;\n\nconst PLAN_FIELDS = `\n id workspaceId memberId coachId name goalType calorieTarget proteinTarget carbsTarget\n fatTarget startDate endDate isActive notes createdAt updatedAt\n`;\n\nconst METRIC_FIELDS = `\n id workspaceId memberId type value unit measuredAt notes photo { id } createdAt updatedAt\n`;\n\nconst RECOVERY_FIELDS = `\n id workspaceId memberId metric score sleepHours notes loggedAt createdAt updatedAt\n`;\n\nconst COACHING_FIELDS = `\n id workspaceId coachId memberId status specialty shareWorkouts shareNutrition\n shareMetrics startedAt endedAt notes createdAt updatedAt\n`;\n\nconst DEVICE_CONNECTION_FIELDS = `\n id workspaceId memberId provider status externalAccountId lastSyncedAt scopes createdAt updatedAt\n`;\n\nconst GUIDANCE_FIELDS = `\n memberId date readinessScore aiSummary cards { signal action reason }\n`;\n\nconst FOOD_FIELDS = `\n barcode name brand servingG calories proteinG carbsG fatG imageUrl\n`;\n\nconst MACROS_FIELDS = `\n date calories proteinG carbsG fatG calorieTarget proteinTarget carbsTarget fatTarget\n`;\n\n/**\n * `MybWorkout.exercises` is a paginated `MybExerciseConnection` on the wire\n * (see `.hive-schema.graphql`); unwrap it back to the flat `MybExercise[]`\n * shape the rest of the app consumes.\n */\ntype MybWorkoutWithExerciseConnection = Omit<MybWorkout, 'exercises'> & {\n exercises: { items: MybExercise[] };\n};\n\nconst unwrapWorkoutItems = (items: MybWorkoutWithExerciseConnection[]): MybWorkout[] =>\n items.map(({ exercises, ...workout }) => ({ ...workout, exercises: exercises.items }));\n\n// ============================================================================\n// Programs (Training)\n// ============================================================================\n\nexport interface ProgramFilters {\n status?: MybProgramStatus;\n memberId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybPrograms = (filters: ProgramFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.programs, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListPrograms: MybConnection<MybProgram> }>(\n `query MybListPrograms($status: MybProgramStatus, $memberId: ID, $pagination: PaginationInput) {\n mybListPrograms(status: $status, memberId: $memberId, pagination: $pagination) {\n items { ${PROGRAM_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n memberId: filters.memberId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListPrograms;\n },\n });\n};\n\nexport const useMybProgram = (id: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.program(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await mybQuery<{\n mybProgram:\n | (Omit<MybProgram, 'workouts'> & {\n workouts: { items: MybWorkoutWithExerciseConnection[] };\n })\n | null;\n }>(\n `query MybProgram($id: ID!) {\n mybProgram(id: $id) {\n ${PROGRAM_FIELDS}\n workouts {\n items {\n ${WORKOUT_FIELDS}\n exercises { items { ${EXERCISE_FIELDS} } }\n }\n }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n if (!result.mybProgram) return null;\n const { workouts, ...program } = result.mybProgram;\n return { ...program, workouts: unwrapWorkoutItems(workouts.items) };\n },\n });\n};\n\nexport const useMybProgramWorkouts = (programId: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.programWorkouts(programId ?? ''),\n enabled: Boolean(programId),\n queryFn: async () => {\n const result = await mybQuery<{\n mybProgramWorkouts: { items: MybWorkoutWithExerciseConnection[] };\n }>(\n `query MybProgramWorkouts($programId: ID!) {\n mybProgramWorkouts(programId: $programId) {\n items {\n ${WORKOUT_FIELDS}\n exercises { items { ${EXERCISE_FIELDS} } }\n }\n }\n }`,\n { programId },\n apiGatewayUrl\n );\n return unwrapWorkoutItems(result.mybProgramWorkouts.items);\n },\n });\n};\n\n/**\n * Planned exercises for a single workout, in `orderIndex` order — the\n * guided-workout stepper (BOFF-4096) walks a live session's `workoutId`\n * through these to drive the \"next exercise\" flow.\n */\nexport const useMybWorkoutExercises = (workoutId: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.workoutExercises(workoutId ?? ''),\n enabled: Boolean(workoutId),\n queryFn: async () => {\n const result = await mybQuery<{ mybWorkoutExercises: { items: MybExercise[] } }>(\n `query MybWorkoutExercises($workoutId: ID!) {\n mybWorkoutExercises(workoutId: $workoutId, pagination: { limit: 100 }) {\n items { ${EXERCISE_FIELDS} }\n }\n }`,\n { workoutId },\n apiGatewayUrl\n );\n return result.mybWorkoutExercises.items.slice().sort((a, b) => a.orderIndex - b.orderIndex);\n },\n });\n};\n\nexport const useMybCreateProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateProgramInput) => {\n const result = await mybQuery<{ mybCreateProgram: MybProgram }>(\n `mutation MybCreateProgram($input: MybCreateProgramInput!) {\n mybCreateProgram(input: $input) { ${PROGRAM_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateProgram;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.programs }),\n });\n};\n\nexport const useMybUpdateProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybUpdateProgramInput }) => {\n const result = await mybQuery<{ mybUpdateProgram: MybProgram }>(\n `mutation MybUpdateProgram($id: ID!, $input: MybUpdateProgramInput!) {\n mybUpdateProgram(id: $id, input: $input) { ${PROGRAM_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybUpdateProgram;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.programs });\n qc.invalidateQueries({ queryKey: KEY.program(data.id) });\n },\n });\n};\n\nexport const useMybTransitionProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; status: MybProgramStatus }) => {\n const result = await mybQuery<{ mybTransitionProgram: MybProgram }>(\n `mutation MybTransitionProgram($id: ID!, $status: MybProgramStatus!) {\n mybTransitionProgram(id: $id, status: $status) { ${PROGRAM_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybTransitionProgram;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.programs });\n qc.invalidateQueries({ queryKey: KEY.program(data.id) });\n },\n });\n};\n\nexport const useMybArchiveProgram = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybArchiveProgram: boolean }>(\n `mutation MybArchiveProgram($id: ID!) { mybArchiveProgram(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.mybArchiveProgram;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.programs }),\n });\n};\n\nexport const useMybAddWorkout = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateWorkoutInput) => {\n const result = await mybQuery<{ mybAddWorkout: MybWorkout }>(\n `mutation MybAddWorkout($input: MybCreateWorkoutInput!) {\n mybAddWorkout(input: $input) { ${WORKOUT_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybAddWorkout;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.program(data.programId) });\n qc.invalidateQueries({ queryKey: KEY.programWorkouts(data.programId) });\n },\n });\n};\n\nexport const useMybAddExercise = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateExerciseInput) => {\n const result = await mybQuery<{ mybAddExercise: { id: string; workoutId: string } }>(\n `mutation MybAddExercise($input: MybCreateExerciseInput!) {\n mybAddExercise(input: $input) { id workoutId }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybAddExercise;\n },\n onSuccess: () => {\n // Exercises live under a program's workouts; invalidate program + workout\n // caches so the newly-added exercise appears on the program detail tree.\n qc.invalidateQueries({ queryKey: ['mybodyshield', 'program'] });\n qc.invalidateQueries({ queryKey: ['mybodyshield', 'program-workouts'] });\n },\n });\n};\n\n// ============================================================================\n// Sessions / Sets (Training — live logging)\n// ============================================================================\n\nexport interface SessionFilters {\n status?: MybSessionStatus;\n programId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybSessions = (filters: SessionFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.sessions, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListSessions: MybConnection<MybWorkoutSession> }>(\n `query MybListSessions($status: MybSessionStatus, $programId: ID, $pagination: PaginationInput) {\n mybListSessions(status: $status, programId: $programId, pagination: $pagination) {\n items { ${SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n programId: filters.programId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListSessions;\n },\n });\n};\n\nexport const useMybSession = (id: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.session(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await mybQuery<{\n mybSession: (Omit<MybWorkoutSession, 'sets'> & { sets: { items: MybSessionSet[] } }) | null;\n }>(\n `query MybSession($id: ID!) {\n mybSession(id: $id) {\n ${SESSION_FIELDS}\n sets { items { ${SET_FIELDS} } }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n if (!result.mybSession) return null;\n const { sets, ...session } = result.mybSession;\n return { ...session, sets: sets.items };\n },\n });\n};\n\n/**\n * The calling member's own best-ever logged set for `exerciseName` (backend:\n * `mybExerciseBestSet`, self-scoped — no memberId argument). Drives live\n * auto-PR detection in the log-set dialog (BOFF-4096): `null` means the\n * member has never logged this exercise before, so there is no record to\n * beat yet.\n */\nexport const useMybExerciseBestSet = (exerciseName: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const trimmed = exerciseName.trim();\n return useQuery({\n queryKey: KEY.exerciseBestSet(trimmed),\n enabled: trimmed.length > 0,\n queryFn: async () => {\n const result = await mybQuery<{ mybExerciseBestSet: MybSessionSet | null }>(\n `query MybExerciseBestSet($exerciseName: String!) {\n mybExerciseBestSet(exerciseName: $exerciseName) { ${SET_FIELDS} }\n }`,\n { exerciseName: trimmed },\n apiGatewayUrl\n );\n return result.mybExerciseBestSet;\n },\n // Best-set history changes rarely mid-session; a short stale window\n // avoids refiring on every keystroke while the exercise name settles.\n staleTime: 15_000,\n });\n};\n\nexport const useMybStartSession = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybStartSessionInput) => {\n const result = await mybQuery<{ mybStartSession: MybWorkoutSession }>(\n `mutation MybStartSession($input: MybStartSessionInput!) {\n mybStartSession(input: $input) { ${SESSION_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybStartSession;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.sessions }),\n });\n};\n\nexport const useMybLogSet = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybLogSetInput) => {\n const result = await mybQuery<{ mybLogSet: MybSessionSet }>(\n `mutation MybLogSet($input: MybLogSetInput!) {\n mybLogSet(input: $input) { ${SET_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybLogSet;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.session(data.sessionId) });\n qc.invalidateQueries({ queryKey: KEY.sessions });\n // A just-logged set may itself now be the best-ever for this exercise —\n // refresh so the next auto-PR comparison (this session or a later one)\n // sees it (BOFF-4096).\n qc.invalidateQueries({ queryKey: KEY.exerciseBestSet(data.exerciseName) });\n },\n });\n};\n\nexport const useMybCompleteSession = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybCompleteSessionInput }) => {\n const result = await mybQuery<{ mybCompleteSession: MybWorkoutSession }>(\n `mutation MybCompleteSession($id: ID!, $input: MybCompleteSessionInput!) {\n mybCompleteSession(id: $id, input: $input) { ${SESSION_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybCompleteSession;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.session(data.id) });\n qc.invalidateQueries({ queryKey: KEY.sessions });\n },\n });\n};\n\n// ============================================================================\n// Goals (Progress)\n// ============================================================================\n\nexport interface GoalFilters {\n status?: MybGoalStatus;\n memberId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybGoals = (filters: GoalFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.goals, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListGoals: MybConnection<MybGoal> }>(\n `query MybListGoals($status: MybGoalStatus, $memberId: ID, $pagination: PaginationInput) {\n mybListGoals(status: $status, memberId: $memberId, pagination: $pagination) {\n items { ${GOAL_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n memberId: filters.memberId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListGoals;\n },\n });\n};\n\nexport const useMybCreateGoal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateGoalInput) => {\n const result = await mybQuery<{ mybCreateGoal: MybGoal }>(\n `mutation MybCreateGoal($input: MybCreateGoalInput!) {\n mybCreateGoal(input: $input) { ${GOAL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateGoal;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.goals }),\n });\n};\n\nexport const useMybUpdateGoal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybUpdateGoalInput }) => {\n const result = await mybQuery<{ mybUpdateGoal: MybGoal }>(\n `mutation MybUpdateGoal($id: ID!, $input: MybUpdateGoalInput!) {\n mybUpdateGoal(id: $id, input: $input) { ${GOAL_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybUpdateGoal;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.goals }),\n });\n};\n\nexport const useMybAchieveGoal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybAchieveGoal: MybGoal }>(\n `mutation MybAchieveGoal($id: ID!) {\n mybAchieveGoal(id: $id) { ${GOAL_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.mybAchieveGoal;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.goals }),\n });\n};\n\n// ============================================================================\n// Nutrition (plans, meals, macros)\n// ============================================================================\n\nexport const useMybActiveNutritionPlan = (memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.activePlan, memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybActiveNutritionPlan: MybNutritionPlan | null }>(\n `query MybActiveNutritionPlan($memberId: ID) {\n mybActiveNutritionPlan(memberId: $memberId) { ${PLAN_FIELDS} }\n }`,\n { memberId },\n apiGatewayUrl\n );\n return result.mybActiveNutritionPlan;\n },\n });\n};\n\nexport const useMybNutritionPlan = (id: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: KEY.nutritionPlan(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await mybQuery<{ mybNutritionPlan: MybNutritionPlan | null }>(\n `query MybNutritionPlan($id: ID!) {\n mybNutritionPlan(id: $id) {\n ${PLAN_FIELDS}\n meals { ${MEAL_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.mybNutritionPlan;\n },\n });\n};\n\nexport interface MealFilters {\n mealType?: MybMealType;\n planId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybMeals = (filters: MealFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.meals, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListMeals: MybConnection<MybMealLog> }>(\n `query MybListMeals($mealType: MybMealType, $planId: ID, $pagination: PaginationInput) {\n mybListMeals(mealType: $mealType, planId: $planId, pagination: $pagination) {\n items { ${MEAL_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n mealType: filters.mealType,\n planId: filters.planId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListMeals;\n },\n });\n};\n\nexport const useMybDailyMacros = (date?: string, memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.dailyMacros, date ?? 'today', memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybDailyMacros: MybDailyMacros }>(\n `query MybDailyMacros($date: DateTime, $memberId: ID) {\n mybDailyMacros(date: $date, memberId: $memberId) { ${MACROS_FIELDS} }\n }`,\n { date, memberId },\n apiGatewayUrl\n );\n return result.mybDailyMacros;\n },\n });\n};\n\nexport const useMybCreateNutritionPlan = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateNutritionPlanInput) => {\n const result = await mybQuery<{ mybCreateNutritionPlan: MybNutritionPlan }>(\n `mutation MybCreateNutritionPlan($input: MybCreateNutritionPlanInput!) {\n mybCreateNutritionPlan(input: $input) { ${PLAN_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateNutritionPlan;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.activePlan });\n },\n });\n};\n\nexport const useMybUpdateNutritionPlan = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: MybUpdateNutritionPlanInput }) => {\n const result = await mybQuery<{ mybUpdateNutritionPlan: MybNutritionPlan }>(\n `mutation MybUpdateNutritionPlan($id: ID!, $input: MybUpdateNutritionPlanInput!) {\n mybUpdateNutritionPlan(id: $id, input: $input) { ${PLAN_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybUpdateNutritionPlan;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.activePlan });\n qc.invalidateQueries({ queryKey: KEY.nutritionPlan(data.id) });\n },\n });\n};\n\nexport const useMybActivateNutritionPlan = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybActivateNutritionPlan: MybNutritionPlan }>(\n `mutation MybActivateNutritionPlan($id: ID!) {\n mybActivateNutritionPlan(id: $id) { ${PLAN_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.mybActivateNutritionPlan;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.activePlan });\n qc.invalidateQueries({ queryKey: KEY.nutritionPlan(data.id) });\n },\n });\n};\n\nexport const useMybLogMeal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybLogMealInput) => {\n const result = await mybQuery<{ mybLogMeal: MybMealLog }>(\n `mutation MybLogMeal($input: MybLogMealInput!) {\n mybLogMeal(input: $input) { ${MEAL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybLogMeal;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.meals });\n qc.invalidateQueries({ queryKey: KEY.dailyMacros });\n if (data.planId) qc.invalidateQueries({ queryKey: KEY.nutritionPlan(data.planId) });\n },\n });\n};\n\nexport const useMybDeleteMeal = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybDeleteMeal: boolean }>(\n `mutation MybDeleteMeal($id: ID!) { mybDeleteMeal(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.mybDeleteMeal;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.meals });\n qc.invalidateQueries({ queryKey: KEY.dailyMacros });\n },\n });\n};\n\n// ============================================================================\n// Body metrics (Progress)\n// ============================================================================\n\nexport interface BodyMetricFilters {\n type?: MybBodyMetricType;\n memberId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useMybBodyMetrics = (filters: BodyMetricFilters = {}) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.bodyMetrics, filters],\n queryFn: async () => {\n const result = await mybQuery<{ mybListBodyMetrics: MybConnection<MybBodyMetric> }>(\n `query MybListBodyMetrics($type: MybBodyMetricType, $memberId: ID, $pagination: PaginationInput) {\n mybListBodyMetrics(type: $type, memberId: $memberId, pagination: $pagination) {\n items { ${METRIC_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n type: filters.type,\n memberId: filters.memberId,\n pagination: filters.pagination ?? DEFAULT_PAGE,\n },\n apiGatewayUrl\n );\n return result.mybListBodyMetrics;\n },\n });\n};\n\nexport const useMybRecordBodyMetric = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybRecordMetricInput) => {\n const result = await mybQuery<{ mybRecordBodyMetric: MybBodyMetric }>(\n `mutation MybRecordBodyMetric($input: MybRecordMetricInput!) {\n mybRecordBodyMetric(input: $input) { ${METRIC_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybRecordBodyMetric;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.bodyMetrics }),\n });\n};\n\n// ============================================================================\n// Recovery (Wellbeing)\n// ============================================================================\n\nexport const useMybRecoveryLogs = (metric?: MybRecoveryMetric, memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.recovery, metric ?? 'all', memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybListRecoveryLogs: MybConnection<MybRecoveryLog> }>(\n `query MybListRecoveryLogs($metric: MybRecoveryMetric, $memberId: ID) {\n mybListRecoveryLogs(metric: $metric, memberId: $memberId) {\n items { ${RECOVERY_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { metric, memberId },\n apiGatewayUrl\n );\n return result.mybListRecoveryLogs;\n },\n });\n};\n\nexport const useMybLogRecovery = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybLogRecoveryInput) => {\n const result = await mybQuery<{ mybLogRecovery: MybRecoveryLog }>(\n `mutation MybLogRecovery($input: MybLogRecoveryInput!) {\n mybLogRecovery(input: $input) { ${RECOVERY_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybLogRecovery;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.recovery }),\n });\n};\n\n// ============================================================================\n// Coaching (Wellbeing)\n// ============================================================================\n\nexport const useMybCoaching = (status?: MybCoachingStatus, memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.coaching, status ?? 'all', memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybListCoaching: MybConnection<MybCoachingRelationship> }>(\n `query MybListCoaching($status: MybCoachingStatus, $memberId: ID) {\n mybListCoaching(status: $status, memberId: $memberId) {\n items { ${COACHING_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { status, memberId },\n apiGatewayUrl\n );\n return result.mybListCoaching;\n },\n });\n};\n\nexport const useMybCreateCoaching = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: MybCreateCoachingInput) => {\n const result = await mybQuery<{ mybCreateCoaching: MybCoachingRelationship }>(\n `mutation MybCreateCoaching($input: MybCreateCoachingInput!) {\n mybCreateCoaching(input: $input) { ${COACHING_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybCreateCoaching;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.coaching }),\n });\n};\n\nexport const useMybTransitionCoaching = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; status: MybCoachingStatus }) => {\n const result = await mybQuery<{ mybTransitionCoaching: MybCoachingRelationship }>(\n `mutation MybTransitionCoaching($id: ID!, $status: MybCoachingStatus!) {\n mybTransitionCoaching(id: $id, status: $status) { ${COACHING_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybTransitionCoaching;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.coaching }),\n });\n};\n\n// ============================================================================\n// Wearable device integrations (BOFF-4172)\n// ============================================================================\n\nexport const useMybDeviceConnections = (memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.deviceConnections, memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybListDeviceConnections: MybDeviceConnection[] }>(\n `query MybListDeviceConnections($memberId: ID) {\n mybListDeviceConnections(memberId: $memberId) { ${DEVICE_CONNECTION_FIELDS} }\n }`,\n { memberId },\n apiGatewayUrl\n );\n return result.mybListDeviceConnections;\n },\n });\n};\n\nexport const useMybConnectDevice = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: {\n provider: MybDeviceProvider;\n externalAccountId?: string;\n scopes?: string[];\n }) => {\n const result = await mybQuery<{ mybConnectDevice: MybDeviceConnection }>(\n `mutation MybConnectDevice($input: MybConnectDeviceInput!) {\n mybConnectDevice(input: $input) { ${DEVICE_CONNECTION_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.mybConnectDevice;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.deviceConnections }),\n });\n};\n\nexport const useMybDisconnectDevice = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await mybQuery<{ mybDisconnectDevice: boolean }>(\n `mutation MybDisconnectDevice($id: ID!) { mybDisconnectDevice(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.mybDisconnectDevice;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.deviceConnections }),\n });\n};\n\nexport interface MybSyncReadingInput {\n metricType: MybWearableMetricType;\n value: number;\n unit?: string;\n recordedAt: string;\n}\n\n/**\n * Sync a batch of readings a native health bridge (Apple HealthKit / Google\n * Health Connect) already pulled on-device. The bridge itself is a Capacitor\n * plugin concern that lives in the app shell (`mybodyshield-app`), not this\n * MFE; this hook is the transport primitive it calls once it has readings to\n * push (BOFF-4172).\n */\nexport const useMybSyncDeviceReadings = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { connectionId: string; readings: MybSyncReadingInput[] }) => {\n const result = await mybQuery<{ mybSyncDeviceReadings: unknown[] }>(\n `mutation MybSyncDeviceReadings($connectionId: ID!, $readings: [MybSyncReadingInput!]!) {\n mybSyncDeviceReadings(connectionId: $connectionId, readings: $readings) { id }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.mybSyncDeviceReadings;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.deviceConnections });\n qc.invalidateQueries({ queryKey: KEY.recovery });\n qc.invalidateQueries({ queryKey: KEY.bodyMetrics });\n },\n });\n};\n\n// ============================================================================\n// Adaptive guidance (BOFF-4174)\n// ============================================================================\n\nexport const useMybTodayGuidance = (memberId?: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: [...KEY.guidance, memberId ?? 'me'],\n queryFn: async () => {\n const result = await mybQuery<{ mybTodayGuidance: MybTodayGuidance }>(\n `query MybTodayGuidance($memberId: ID) {\n mybTodayGuidance(memberId: $memberId) { ${GUIDANCE_FIELDS} }\n }`,\n { memberId },\n apiGatewayUrl\n );\n return result.mybTodayGuidance;\n },\n // Guidance is derived from the day's logged data; a short stale window\n // avoids refetching on every focus while still catching a fresh log.\n staleTime: 60_000,\n });\n};\n\n// ============================================================================\n// Food database — Open Food Facts (BOFF-4173)\n// ============================================================================\n\nexport const useMybSearchFoods = (query: string, limit = 10) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const trimmed = query.trim();\n return useQuery({\n queryKey: KEY.foodSearch(trimmed),\n enabled: trimmed.length >= 2,\n queryFn: async () => {\n const result = await mybQuery<{ mybSearchFoods: MybFood[] }>(\n `query MybSearchFoods($query: String!, $limit: Int) {\n mybSearchFoods(query: $query, limit: $limit) { ${FOOD_FIELDS} }\n }`,\n { query: trimmed, limit },\n apiGatewayUrl\n );\n return result.mybSearchFoods;\n },\n staleTime: 30_000,\n });\n};\n\nexport const useMybFoodByBarcode = () => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useMutation({\n mutationFn: async (barcode: string) => {\n const result = await mybQuery<{ mybFoodByBarcode: MybFood | null }>(\n `query MybFoodByBarcode($barcode: String!) {\n mybFoodByBarcode(barcode: $barcode) { ${FOOD_FIELDS} }\n }`,\n { barcode },\n apiGatewayUrl\n );\n return result.mybFoodByBarcode;\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* offlineSessionQueue — an offline-first mutation queue for in-session
|
|
3
|
+
* workout logging (BOFF-4219).
|
|
4
|
+
* @module mybodyshield/lib
|
|
5
|
+
*
|
|
6
|
+
* Scope: a member starts a workout session while online (so a real
|
|
7
|
+
* `sessionId` already exists), then loses connectivity mid-session — a
|
|
8
|
+
* common real-world gap (gym basements, poor venue Wi-Fi) — and needs to
|
|
9
|
+
* keep logging sets and eventually complete the session without losing
|
|
10
|
+
* data or blocking on a network round-trip. Every "log set" / "complete
|
|
11
|
+
* session" action is persisted to IndexedDB the instant it's captured —
|
|
12
|
+
* online or offline — keyed by a client-generated `clientActionId` that is
|
|
13
|
+
* minted once and never regenerated for that action. The queue is drained
|
|
14
|
+
* automatically once the browser's `online` event fires (and periodically
|
|
15
|
+
* retried while online, in case the network is flaky rather than fully
|
|
16
|
+
* down) by `useOfflineSessionSync`.
|
|
17
|
+
*
|
|
18
|
+
* Modeled directly on the EventfullyManaged kiosk check-in offline queue
|
|
19
|
+
* (`microfe-eventfullymanaged/src/eventfullymanaged/lib/offlineCheckInQueue.ts`,
|
|
20
|
+
* BOFF-4330) — same IndexedDB-backed queue-and-replay shape, adapted for
|
|
21
|
+
* two action types instead of one.
|
|
22
|
+
*
|
|
23
|
+
* KNOWN LIMITATION (documented, not silently assumed away): unlike the
|
|
24
|
+
* EventfullyManaged check-in mutation, `mybLogSet`/`mybCompleteSession`
|
|
25
|
+
* carry no server-side idempotency key. If a sync attempt's response is
|
|
26
|
+
* lost after the server has actually applied it (app killed / tab closed
|
|
27
|
+
* mid-request), a retry could double-submit that one action. This is the
|
|
28
|
+
* same residual risk any offline-first client has without a backend
|
|
29
|
+
* idempotency contract; adding one is backend follow-up scope, tracked
|
|
30
|
+
* against BOFF-4219 rather than silently glossed over here. Starting a
|
|
31
|
+
* session fully offline (no `sessionId` yet) is explicitly OUT of this
|
|
32
|
+
* queue's scope for the same reason plus the larger local-only-session-view
|
|
33
|
+
* UX work it would require — see the BOFF-4219 Linear ticket.
|
|
34
|
+
*/
|
|
35
|
+
export type PendingSessionActionType = 'LOG_SET' | 'COMPLETE_SESSION';
|
|
36
|
+
export interface PendingLogSetInput {
|
|
37
|
+
exerciseName: string;
|
|
38
|
+
setNumber?: number | null;
|
|
39
|
+
reps?: number | null;
|
|
40
|
+
weight?: number | null;
|
|
41
|
+
distanceM?: number | null;
|
|
42
|
+
durationSec?: number | null;
|
|
43
|
+
isPersonalRecord?: boolean | null;
|
|
44
|
+
}
|
|
45
|
+
export interface PendingCompleteSessionInput {
|
|
46
|
+
caloriesBurned?: number | null;
|
|
47
|
+
perceivedExertion?: number | null;
|
|
48
|
+
notes?: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface PendingSessionAction {
|
|
51
|
+
/** Client-generated key — stable across retries, used as the IndexedDB key. */
|
|
52
|
+
clientActionId: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
type: PendingSessionActionType;
|
|
55
|
+
input: PendingLogSetInput | PendingCompleteSessionInput;
|
|
56
|
+
/** When the action actually happened on-device, independent of when it syncs. */
|
|
57
|
+
capturedAt: string;
|
|
58
|
+
queuedAt: string;
|
|
59
|
+
attempts: number;
|
|
60
|
+
lastError: string | null;
|
|
61
|
+
}
|
|
62
|
+
/** Persist a new pending action to the offline queue. Returns the stored record. */
|
|
63
|
+
export declare function enqueuePendingAction(action: Omit<PendingSessionAction, 'queuedAt' | 'attempts' | 'lastError'>): Promise<PendingSessionAction>;
|
|
64
|
+
/** List every pending action, oldest first (so sync replays in capture order — sets before a completion). */
|
|
65
|
+
export declare function listPendingActions(): Promise<PendingSessionAction[]>;
|
|
66
|
+
/** List pending actions for a single session, oldest first — powers the "N queued" badge on that session's page. */
|
|
67
|
+
export declare function listPendingActionsForSession(sessionId: string): Promise<PendingSessionAction[]>;
|
|
68
|
+
/** Remove an action once it has synced successfully. */
|
|
69
|
+
export declare function removePendingAction(clientActionId: string): Promise<void>;
|
|
70
|
+
/** Record a failed sync attempt so the UI can show why it's still pending. */
|
|
71
|
+
export declare function markPendingActionFailed(clientActionId: string, error: string): Promise<void>;
|
|
72
|
+
/** How many actions are currently queued — powers the global "pending sync" indicator. */
|
|
73
|
+
export declare function countPendingActions(): Promise<number>;
|
|
74
|
+
/** Mint a stable client-side id for a new pending action. */
|
|
75
|
+
export declare function newClientActionId(): string;
|
|
76
|
+
//# sourceMappingURL=offlineSessionQueue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offlineSessionQueue.d.ts","sourceRoot":"","sources":["../../../src/mybodyshield/lib/offlineSessionQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAEtE,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,wBAAwB,CAAC;IAC/B,KAAK,EAAE,kBAAkB,GAAG,2BAA2B,CAAC;IACxD,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAwCD,oFAAoF;AACpF,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC,GACxE,OAAO,CAAC,oBAAoB,CAAC,CAS/B;AAED,6GAA6G;AAC7G,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAG1E;AAED,oHAAoH;AACpH,wBAAsB,4BAA4B,CAChD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAGjC;AAED,wDAAwD;AACxD,wBAAsB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/E;AAED,8EAA8E;AAC9E,wBAAsB,uBAAuB,CAC3C,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED,0FAA0F;AAC1F,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3D;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/mybodyshield/lib/offlineSessionQueue.ts
|
|
2
|
+
var e = "myb-session-offline-queue", t = 1, n = "pending-actions";
|
|
3
|
+
function r() {
|
|
4
|
+
return new Promise((r, i) => {
|
|
5
|
+
if (typeof indexedDB > "u") {
|
|
6
|
+
i(/* @__PURE__ */ Error("IndexedDB is not available in this environment."));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
let a = indexedDB.open(e, t);
|
|
10
|
+
a.onupgradeneeded = () => {
|
|
11
|
+
let e = a.result;
|
|
12
|
+
e.objectStoreNames.contains(n) || e.createObjectStore(n, { keyPath: "clientActionId" });
|
|
13
|
+
}, a.onsuccess = () => r(a.result), a.onerror = () => i(a.error ?? /* @__PURE__ */ Error("Failed to open the offline session queue."));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async function i(e, t) {
|
|
17
|
+
let i = await r();
|
|
18
|
+
try {
|
|
19
|
+
return await new Promise((r, a) => {
|
|
20
|
+
let o = t(i.transaction(n, e).objectStore(n));
|
|
21
|
+
o.onsuccess = () => r(o.result), o.onerror = () => a(o.error ?? /* @__PURE__ */ Error("Offline session queue operation failed."));
|
|
22
|
+
});
|
|
23
|
+
} finally {
|
|
24
|
+
i.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function a(e) {
|
|
28
|
+
let t = {
|
|
29
|
+
...e,
|
|
30
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31
|
+
attempts: 0,
|
|
32
|
+
lastError: null
|
|
33
|
+
};
|
|
34
|
+
return await i("readwrite", (e) => e.put(t)), t;
|
|
35
|
+
}
|
|
36
|
+
async function o() {
|
|
37
|
+
return [...await i("readonly", (e) => e.getAll())].sort((e, t) => e.queuedAt.localeCompare(t.queuedAt));
|
|
38
|
+
}
|
|
39
|
+
async function s(e) {
|
|
40
|
+
return (await o()).filter((t) => t.sessionId === e);
|
|
41
|
+
}
|
|
42
|
+
async function c(e) {
|
|
43
|
+
await i("readwrite", (t) => t.delete(e));
|
|
44
|
+
}
|
|
45
|
+
async function l(e, t) {
|
|
46
|
+
let i = await r();
|
|
47
|
+
try {
|
|
48
|
+
await new Promise((r, a) => {
|
|
49
|
+
let o = i.transaction(n, "readwrite").objectStore(n), s = o.get(e);
|
|
50
|
+
s.onsuccess = () => {
|
|
51
|
+
let e = s.result;
|
|
52
|
+
if (!e) {
|
|
53
|
+
r();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let n = o.put({
|
|
57
|
+
...e,
|
|
58
|
+
attempts: e.attempts + 1,
|
|
59
|
+
lastError: t
|
|
60
|
+
});
|
|
61
|
+
n.onsuccess = () => r(), n.onerror = () => a(n.error ?? /* @__PURE__ */ Error("Failed to record sync failure."));
|
|
62
|
+
}, s.onerror = () => a(s.error ?? /* @__PURE__ */ Error("Failed to load pending action."));
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
i.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function u() {
|
|
69
|
+
return i("readonly", (e) => e.count());
|
|
70
|
+
}
|
|
71
|
+
function d() {
|
|
72
|
+
return typeof crypto < "u" && "randomUUID" in crypto ? crypto.randomUUID() : `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { u as countPendingActions, a as enqueuePendingAction, o as listPendingActions, s as listPendingActionsForSession, l as markPendingActionFailed, d as newClientActionId, c as removePendingAction };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=offlineSessionQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offlineSessionQueue.js","names":[],"sources":["../../../src/mybodyshield/lib/offlineSessionQueue.ts"],"sourcesContent":["/**\n * offlineSessionQueue — an offline-first mutation queue for in-session\n * workout logging (BOFF-4219).\n * @module mybodyshield/lib\n *\n * Scope: a member starts a workout session while online (so a real\n * `sessionId` already exists), then loses connectivity mid-session — a\n * common real-world gap (gym basements, poor venue Wi-Fi) — and needs to\n * keep logging sets and eventually complete the session without losing\n * data or blocking on a network round-trip. Every \"log set\" / \"complete\n * session\" action is persisted to IndexedDB the instant it's captured —\n * online or offline — keyed by a client-generated `clientActionId` that is\n * minted once and never regenerated for that action. The queue is drained\n * automatically once the browser's `online` event fires (and periodically\n * retried while online, in case the network is flaky rather than fully\n * down) by `useOfflineSessionSync`.\n *\n * Modeled directly on the EventfullyManaged kiosk check-in offline queue\n * (`microfe-eventfullymanaged/src/eventfullymanaged/lib/offlineCheckInQueue.ts`,\n * BOFF-4330) — same IndexedDB-backed queue-and-replay shape, adapted for\n * two action types instead of one.\n *\n * KNOWN LIMITATION (documented, not silently assumed away): unlike the\n * EventfullyManaged check-in mutation, `mybLogSet`/`mybCompleteSession`\n * carry no server-side idempotency key. If a sync attempt's response is\n * lost after the server has actually applied it (app killed / tab closed\n * mid-request), a retry could double-submit that one action. This is the\n * same residual risk any offline-first client has without a backend\n * idempotency contract; adding one is backend follow-up scope, tracked\n * against BOFF-4219 rather than silently glossed over here. Starting a\n * session fully offline (no `sessionId` yet) is explicitly OUT of this\n * queue's scope for the same reason plus the larger local-only-session-view\n * UX work it would require — see the BOFF-4219 Linear ticket.\n */\n\nconst DB_NAME = 'myb-session-offline-queue';\nconst DB_VERSION = 1;\nconst STORE_NAME = 'pending-actions';\n\nexport type PendingSessionActionType = 'LOG_SET' | 'COMPLETE_SESSION';\n\nexport interface PendingLogSetInput {\n exerciseName: string;\n setNumber?: number | null;\n reps?: number | null;\n weight?: number | null;\n distanceM?: number | null;\n durationSec?: number | null;\n isPersonalRecord?: boolean | null;\n}\n\nexport interface PendingCompleteSessionInput {\n caloriesBurned?: number | null;\n perceivedExertion?: number | null;\n notes?: string | null;\n}\n\nexport interface PendingSessionAction {\n /** Client-generated key — stable across retries, used as the IndexedDB key. */\n clientActionId: string;\n sessionId: string;\n type: PendingSessionActionType;\n input: PendingLogSetInput | PendingCompleteSessionInput;\n /** When the action actually happened on-device, independent of when it syncs. */\n capturedAt: string;\n queuedAt: string;\n attempts: number;\n lastError: string | null;\n}\n\nfunction openDb(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n if (typeof indexedDB === 'undefined') {\n reject(new Error('IndexedDB is not available in this environment.'));\n return;\n }\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME, { keyPath: 'clientActionId' });\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () =>\n reject(request.error ?? new Error('Failed to open the offline session queue.'));\n });\n}\n\nasync function withStore<T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>\n): Promise<T> {\n const db = await openDb();\n try {\n return await new Promise<T>((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, mode);\n const store = tx.objectStore(STORE_NAME);\n const request = fn(store);\n request.onsuccess = () => resolve(request.result);\n request.onerror = () =>\n reject(request.error ?? new Error('Offline session queue operation failed.'));\n });\n } finally {\n db.close();\n }\n}\n\n/** Persist a new pending action to the offline queue. Returns the stored record. */\nexport async function enqueuePendingAction(\n action: Omit<PendingSessionAction, 'queuedAt' | 'attempts' | 'lastError'>\n): Promise<PendingSessionAction> {\n const record: PendingSessionAction = {\n ...action,\n queuedAt: new Date().toISOString(),\n attempts: 0,\n lastError: null,\n };\n await withStore('readwrite', (store) => store.put(record));\n return record;\n}\n\n/** List every pending action, oldest first (so sync replays in capture order — sets before a completion). */\nexport async function listPendingActions(): Promise<PendingSessionAction[]> {\n const all = await withStore<PendingSessionAction[]>('readonly', (store) => store.getAll());\n return [...all].sort((a, b) => a.queuedAt.localeCompare(b.queuedAt));\n}\n\n/** List pending actions for a single session, oldest first — powers the \"N queued\" badge on that session's page. */\nexport async function listPendingActionsForSession(\n sessionId: string\n): Promise<PendingSessionAction[]> {\n const all = await listPendingActions();\n return all.filter((a) => a.sessionId === sessionId);\n}\n\n/** Remove an action once it has synced successfully. */\nexport async function removePendingAction(clientActionId: string): Promise<void> {\n await withStore('readwrite', (store) => store.delete(clientActionId));\n}\n\n/** Record a failed sync attempt so the UI can show why it's still pending. */\nexport async function markPendingActionFailed(\n clientActionId: string,\n error: string\n): Promise<void> {\n const db = await openDb();\n try {\n await new Promise<void>((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite');\n const store = tx.objectStore(STORE_NAME);\n const getReq = store.get(clientActionId);\n getReq.onsuccess = () => {\n const existing = getReq.result as PendingSessionAction | undefined;\n if (!existing) {\n resolve();\n return;\n }\n const putReq = store.put({\n ...existing,\n attempts: existing.attempts + 1,\n lastError: error,\n });\n putReq.onsuccess = () => resolve();\n putReq.onerror = () => reject(putReq.error ?? new Error('Failed to record sync failure.'));\n };\n getReq.onerror = () => reject(getReq.error ?? new Error('Failed to load pending action.'));\n });\n } finally {\n db.close();\n }\n}\n\n/** How many actions are currently queued — powers the global \"pending sync\" indicator. */\nexport async function countPendingActions(): Promise<number> {\n return withStore<number>('readonly', (store) => store.count());\n}\n\n/** Mint a stable client-side id for a new pending action. */\nexport function newClientActionId(): string {\n if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID();\n return `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"],"mappings":";AAmCA,IAAM,IAAU,6BACV,IAAa,GACb,IAAa;AAiCnB,SAAS,IAA+B;AACtC,QAAO,IAAI,SAAS,GAAS,MAAW;AACtC,MAAI,OAAO,YAAc,KAAa;AACpC,KAAO,gBAAI,MAAM,kDAAkD,CAAC;AACpE;;EAEF,IAAM,IAAU,UAAU,KAAK,GAAS,EAAW;AAQnD,EAPA,EAAQ,wBAAwB;GAC9B,IAAM,IAAK,EAAQ;AACnB,GAAK,EAAG,iBAAiB,SAAS,EAAW,IAC3C,EAAG,kBAAkB,GAAY,EAAE,SAAS,kBAAkB,CAAC;KAGnE,EAAQ,kBAAkB,EAAQ,EAAQ,OAAO,EACjD,EAAQ,gBACN,EAAO,EAAQ,SAAS,gBAAI,MAAM,4CAA4C,CAAC;GACjF;;AAGJ,eAAe,EACb,GACA,GACY;CACZ,IAAM,IAAK,MAAM,GAAQ;AACzB,KAAI;AACF,SAAO,MAAM,IAAI,SAAY,GAAS,MAAW;GAG/C,IAAM,IAAU,EAFL,EAAG,YAAY,GAAY,EAAK,CAC1B,YAAY,EAAW,CACf;AAEzB,GADA,EAAQ,kBAAkB,EAAQ,EAAQ,OAAO,EACjD,EAAQ,gBACN,EAAO,EAAQ,SAAS,gBAAI,MAAM,0CAA0C,CAAC;IAC/E;WACM;AACR,IAAG,OAAO;;;AAKd,eAAsB,EACpB,GAC+B;CAC/B,IAAM,IAA+B;EACnC,GAAG;EACH,2BAAU,IAAI,MAAM,EAAC,aAAa;EAClC,UAAU;EACV,WAAW;EACZ;AAED,QADA,MAAM,EAAU,cAAc,MAAU,EAAM,IAAI,EAAO,CAAC,EACnD;;AAIT,eAAsB,IAAsD;AAE1E,QAAO,CAAC,GADI,MAAM,EAAkC,aAAa,MAAU,EAAM,QAAQ,CAAC,CAC3E,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,SAAS,CAAC;;AAItE,eAAsB,EACpB,GACiC;AAEjC,SADY,MAAM,GAAoB,EAC3B,QAAQ,MAAM,EAAE,cAAc,EAAU;;AAIrD,eAAsB,EAAoB,GAAuC;AAC/E,OAAM,EAAU,cAAc,MAAU,EAAM,OAAO,EAAe,CAAC;;AAIvE,eAAsB,EACpB,GACA,GACe;CACf,IAAM,IAAK,MAAM,GAAQ;AACzB,KAAI;AACF,QAAM,IAAI,SAAe,GAAS,MAAW;GAE3C,IAAM,IADK,EAAG,YAAY,GAAY,YAAY,CACjC,YAAY,EAAW,EAClC,IAAS,EAAM,IAAI,EAAe;AAexC,GAdA,EAAO,kBAAkB;IACvB,IAAM,IAAW,EAAO;AACxB,QAAI,CAAC,GAAU;AACb,QAAS;AACT;;IAEF,IAAM,IAAS,EAAM,IAAI;KACvB,GAAG;KACH,UAAU,EAAS,WAAW;KAC9B,WAAW;KACZ,CAAC;AAEF,IADA,EAAO,kBAAkB,GAAS,EAClC,EAAO,gBAAgB,EAAO,EAAO,SAAS,gBAAI,MAAM,iCAAiC,CAAC;MAE5F,EAAO,gBAAgB,EAAO,EAAO,SAAS,gBAAI,MAAM,iCAAiC,CAAC;IAC1F;WACM;AACR,IAAG,OAAO;;;AAKd,eAAsB,IAAuC;AAC3D,QAAO,EAAkB,aAAa,MAAU,EAAM,OAAO,CAAC;;AAIhE,SAAgB,IAA4B;AAE1C,QADI,OAAO,SAAW,OAAe,gBAAgB,SAAe,OAAO,YAAY,GAChF,SAAS,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PendingCompleteSessionInput, PendingLogSetInput, PendingSessionAction } from './offlineSessionQueue';
|
|
2
|
+
|
|
3
|
+
export interface LogSetMutateResult {
|
|
4
|
+
id: string;
|
|
5
|
+
setNumber: number;
|
|
6
|
+
}
|
|
7
|
+
type LogSetMutate = (input: PendingLogSetInput & {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
}) => Promise<LogSetMutateResult>;
|
|
10
|
+
type CompleteSessionMutate = (vars: {
|
|
11
|
+
id: string;
|
|
12
|
+
input: PendingCompleteSessionInput;
|
|
13
|
+
}) => Promise<unknown>;
|
|
14
|
+
export interface OfflineSessionSyncState {
|
|
15
|
+
/** Live `navigator.onLine` state (assumes online in non-browser environments). */
|
|
16
|
+
isOnline: boolean;
|
|
17
|
+
/** How many actions are currently queued across all sessions. */
|
|
18
|
+
pendingCount: number;
|
|
19
|
+
/** Queue a set to be logged once connectivity returns. */
|
|
20
|
+
enqueueLogSet: (sessionId: string, input: PendingLogSetInput) => Promise<PendingSessionAction>;
|
|
21
|
+
/** Queue a session completion to be applied once connectivity returns. */
|
|
22
|
+
enqueueCompleteSession: (sessionId: string, input: PendingCompleteSessionInput) => Promise<PendingSessionAction>;
|
|
23
|
+
/** Manually kick a sync pass (e.g. a "Retry now" button). */
|
|
24
|
+
syncNow: () => void;
|
|
25
|
+
/** Refresh `pendingCount` from IndexedDB without triggering a sync. */
|
|
26
|
+
refreshPendingCount: () => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function useOfflineSessionSync(logSet: LogSetMutate, completeSession: CompleteSessionMutate, onActionSynced?: (action: PendingSessionAction) => void): OfflineSessionSyncState;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=useOfflineSessionSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOfflineSessionSync.d.ts","sourceRoot":"","sources":["../../../src/mybodyshield/lib/useOfflineSessionSync.ts"],"names":[],"mappings":"AA2BA,OAAO,EAOL,KAAK,2BAA2B,EAChC,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EAC1B,MAAM,uBAAuB,CAAC;AAI/B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,KAAK,YAAY,GAAG,CAClB,KAAK,EAAE,kBAAkB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,KAC9C,OAAO,CAAC,kBAAkB,CAAC,CAAC;AACjC,KAAK,qBAAqB,GAAG,CAAC,IAAI,EAAE;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,2BAA2B,CAAC;CACpC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEvB,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,QAAQ,EAAE,OAAO,CAAC;IAClB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC/F,0EAA0E;IAC1E,sBAAsB,EAAE,CACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,2BAA2B,KAC/B,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,6DAA6D;IAC7D,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,uEAAuE;IACvE,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,YAAY,EACpB,eAAe,EAAE,qBAAqB,EACtC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GACtD,uBAAuB,CAoHzB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { countPendingActions as e, enqueuePendingAction as t, listPendingActions as n, markPendingActionFailed as r, newClientActionId as i, removePendingAction as a } from "./offlineSessionQueue.js";
|
|
2
|
+
import { useCallback as o, useEffect as s, useRef as c, useState as l } from "react";
|
|
3
|
+
//#region src/mybodyshield/lib/useOfflineSessionSync.ts
|
|
4
|
+
var u = 2e4;
|
|
5
|
+
function d(d, f, p) {
|
|
6
|
+
let [m, h] = l(() => typeof navigator > "u" || navigator.onLine), [g, _] = l(0), v = c(!1), y = o(() => {
|
|
7
|
+
e().then(_);
|
|
8
|
+
}, []), b = o(async () => {
|
|
9
|
+
if (!v.current) {
|
|
10
|
+
v.current = !0;
|
|
11
|
+
try {
|
|
12
|
+
let e = await n();
|
|
13
|
+
for (let t of e) try {
|
|
14
|
+
t.type === "LOG_SET" ? await d({
|
|
15
|
+
sessionId: t.sessionId,
|
|
16
|
+
...t.input
|
|
17
|
+
}) : await f({
|
|
18
|
+
id: t.sessionId,
|
|
19
|
+
input: t.input
|
|
20
|
+
}), await a(t.clientActionId), p?.(t);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
let n = e instanceof Error ? e.message : "Sync failed";
|
|
23
|
+
await r(t.clientActionId, n);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
} finally {
|
|
27
|
+
v.current = !1, y();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}, [
|
|
31
|
+
d,
|
|
32
|
+
f,
|
|
33
|
+
p,
|
|
34
|
+
y
|
|
35
|
+
]), x = o(() => {
|
|
36
|
+
b();
|
|
37
|
+
}, [b]), S = o(async (e, n) => {
|
|
38
|
+
let r = await t({
|
|
39
|
+
clientActionId: i(),
|
|
40
|
+
sessionId: e,
|
|
41
|
+
type: "LOG_SET",
|
|
42
|
+
input: n,
|
|
43
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
44
|
+
});
|
|
45
|
+
return y(), r;
|
|
46
|
+
}, [y]), C = o(async (e, n) => {
|
|
47
|
+
let r = await t({
|
|
48
|
+
clientActionId: i(),
|
|
49
|
+
sessionId: e,
|
|
50
|
+
type: "COMPLETE_SESSION",
|
|
51
|
+
input: n,
|
|
52
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
53
|
+
});
|
|
54
|
+
return y(), r;
|
|
55
|
+
}, [y]);
|
|
56
|
+
return s(() => {
|
|
57
|
+
if (y(), b(), typeof window > "u") return;
|
|
58
|
+
let e = () => {
|
|
59
|
+
h(!0), b();
|
|
60
|
+
}, t = () => h(!1);
|
|
61
|
+
window.addEventListener("online", e), window.addEventListener("offline", t);
|
|
62
|
+
let n = setInterval(() => {
|
|
63
|
+
(typeof navigator > "u" || navigator.onLine) && b();
|
|
64
|
+
}, u);
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener("online", e), window.removeEventListener("offline", t), clearInterval(n);
|
|
67
|
+
};
|
|
68
|
+
}, []), {
|
|
69
|
+
isOnline: m,
|
|
70
|
+
pendingCount: g,
|
|
71
|
+
enqueueLogSet: S,
|
|
72
|
+
enqueueCompleteSession: C,
|
|
73
|
+
syncNow: x,
|
|
74
|
+
refreshPendingCount: y
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { d as useOfflineSessionSync };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=useOfflineSessionSync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOfflineSessionSync.js","names":[],"sources":["../../../src/mybodyshield/lib/useOfflineSessionSync.ts"],"sourcesContent":["/**\n * useOfflineSessionSync — drains the offline in-session mutation queue\n * (BOFF-4219).\n * @module mybodyshield/lib\n *\n * Wires the IndexedDB queue (`offlineSessionQueue.ts`) to the real\n * `mybLogSet` / `mybCompleteSession` mutations: replays every queued action\n * in capture order, on three triggers — mirrors\n * `useOfflineCheckInSync` (BOFF-4330) exactly:\n * 1. once on mount (covers \"the app was reloaded with a full queue\"),\n * 2. immediately when the browser fires `online`,\n * 3. periodically while online, in case the network is flaky rather than\n * fully down.\n *\n * An action whose sync attempt throws (network failure) stops the drain for\n * this pass so a dead connection doesn't get hammered; an action that gets\n * back a real GraphQL error (e.g. the session was deleted server-side) is\n * marked failed and skipped so one bad entry can't block the rest of a\n * healthy queue.\n *\n * Also exposes `enqueueLogSet` / `enqueueCompleteSession` — the write path a\n * page calls when it detects it's offline (or a mutation throws a\n * network-level error), so the two concerns (detecting \"should this be\n * queued instead\" and \"how the queue drains\") stay decoupled.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n countPendingActions,\n enqueuePendingAction,\n listPendingActions,\n markPendingActionFailed,\n newClientActionId,\n removePendingAction,\n type PendingCompleteSessionInput,\n type PendingLogSetInput,\n type PendingSessionAction,\n} from './offlineSessionQueue';\n\nconst RETRY_INTERVAL_MS = 20_000;\n\nexport interface LogSetMutateResult {\n id: string;\n setNumber: number;\n}\n\ntype LogSetMutate = (\n input: PendingLogSetInput & { sessionId: string }\n) => Promise<LogSetMutateResult>;\ntype CompleteSessionMutate = (vars: {\n id: string;\n input: PendingCompleteSessionInput;\n}) => Promise<unknown>;\n\nexport interface OfflineSessionSyncState {\n /** Live `navigator.onLine` state (assumes online in non-browser environments). */\n isOnline: boolean;\n /** How many actions are currently queued across all sessions. */\n pendingCount: number;\n /** Queue a set to be logged once connectivity returns. */\n enqueueLogSet: (sessionId: string, input: PendingLogSetInput) => Promise<PendingSessionAction>;\n /** Queue a session completion to be applied once connectivity returns. */\n enqueueCompleteSession: (\n sessionId: string,\n input: PendingCompleteSessionInput\n ) => Promise<PendingSessionAction>;\n /** Manually kick a sync pass (e.g. a \"Retry now\" button). */\n syncNow: () => void;\n /** Refresh `pendingCount` from IndexedDB without triggering a sync. */\n refreshPendingCount: () => void;\n}\n\nexport function useOfflineSessionSync(\n logSet: LogSetMutate,\n completeSession: CompleteSessionMutate,\n onActionSynced?: (action: PendingSessionAction) => void\n): OfflineSessionSyncState {\n const [isOnline, setIsOnline] = useState(\n () => typeof navigator === 'undefined' || navigator.onLine\n );\n const [pendingCount, setPendingCount] = useState(0);\n const syncingRef = useRef(false);\n\n const refreshPendingCount = useCallback(() => {\n void countPendingActions().then(setPendingCount);\n }, []);\n\n const drainQueue = useCallback(async () => {\n if (syncingRef.current) return;\n syncingRef.current = true;\n try {\n const pending = await listPendingActions();\n for (const action of pending) {\n try {\n if (action.type === 'LOG_SET') {\n await logSet({ sessionId: action.sessionId, ...(action.input as PendingLogSetInput) });\n } else {\n await completeSession({\n id: action.sessionId,\n input: action.input as PendingCompleteSessionInput,\n });\n }\n await removePendingAction(action.clientActionId);\n onActionSynced?.(action);\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Sync failed';\n await markPendingActionFailed(action.clientActionId, message);\n // Stop this pass on the first failure — whether it's a transient\n // network error (still offline) or a real backend rejection, the\n // remaining queue is retried in original order on the next\n // online/interval trigger rather than reordered around it.\n break;\n }\n }\n } finally {\n syncingRef.current = false;\n refreshPendingCount();\n }\n }, [logSet, completeSession, onActionSynced, refreshPendingCount]);\n\n const syncNow = useCallback(() => {\n void drainQueue();\n }, [drainQueue]);\n\n const enqueueLogSet = useCallback(\n async (sessionId: string, input: PendingLogSetInput) => {\n const action = await enqueuePendingAction({\n clientActionId: newClientActionId(),\n sessionId,\n type: 'LOG_SET',\n input,\n capturedAt: new Date().toISOString(),\n });\n refreshPendingCount();\n return action;\n },\n [refreshPendingCount]\n );\n\n const enqueueCompleteSession = useCallback(\n async (sessionId: string, input: PendingCompleteSessionInput) => {\n const action = await enqueuePendingAction({\n clientActionId: newClientActionId(),\n sessionId,\n type: 'COMPLETE_SESSION',\n input,\n capturedAt: new Date().toISOString(),\n });\n refreshPendingCount();\n return action;\n },\n [refreshPendingCount]\n );\n\n useEffect(() => {\n refreshPendingCount();\n // Covers the app being reloaded with actions still queued from a prior\n // offline stretch — don't wait for an `online` event that may never\n // fire again if the device was already online when it reloaded.\n void drainQueue();\n\n if (typeof window === 'undefined') return;\n const handleOnline = () => {\n setIsOnline(true);\n void drainQueue();\n };\n const handleOffline = () => setIsOnline(false);\n window.addEventListener('online', handleOnline);\n window.addEventListener('offline', handleOffline);\n\n const interval = setInterval(() => {\n if (typeof navigator === 'undefined' || navigator.onLine) {\n void drainQueue();\n }\n }, RETRY_INTERVAL_MS);\n\n return () => {\n window.removeEventListener('online', handleOnline);\n window.removeEventListener('offline', handleOffline);\n clearInterval(interval);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- drainQueue is stable enough (only logSet/completeSession identity churns across renders); re-subscribing on every mutate identity change would tear down/rebuild the interval constantly.\n }, []);\n\n return {\n isOnline,\n pendingCount,\n enqueueLogSet,\n enqueueCompleteSession,\n syncNow,\n refreshPendingCount,\n };\n}\n"],"mappings":";;;AAuCA,IAAM,IAAoB;AAiC1B,SAAgB,EACd,GACA,GACA,GACyB;CACzB,IAAM,CAAC,GAAU,KAAe,QACxB,OAAO,YAAc,OAAe,UAAU,OACrD,EACK,CAAC,GAAc,KAAmB,EAAS,EAAE,EAC7C,IAAa,EAAO,GAAM,EAE1B,IAAsB,QAAkB;AACvC,KAAqB,CAAC,KAAK,EAAgB;IAC/C,EAAE,CAAC,EAEA,IAAa,EAAY,YAAY;AACrC,SAAW,SACf;KAAW,UAAU;AACrB,OAAI;IACF,IAAM,IAAU,MAAM,GAAoB;AAC1C,SAAK,IAAM,KAAU,EACnB,KAAI;AAUF,KATI,EAAO,SAAS,YAClB,MAAM,EAAO;MAAE,WAAW,EAAO;MAAW,GAAI,EAAO;MAA8B,CAAC,GAEtF,MAAM,EAAgB;MACpB,IAAI,EAAO;MACX,OAAO,EAAO;MACf,CAAC,EAEJ,MAAM,EAAoB,EAAO,eAAe,EAChD,IAAiB,EAAO;aACjB,GAAK;KACZ,IAAM,IAAU,aAAe,QAAQ,EAAI,UAAU;AACrD,WAAM,EAAwB,EAAO,gBAAgB,EAAQ;AAK7D;;aAGI;AAER,IADA,EAAW,UAAU,IACrB,GAAqB;;;IAEtB;EAAC;EAAQ;EAAiB;EAAgB;EAAoB,CAAC,EAE5D,IAAU,QAAkB;AAC3B,KAAY;IAChB,CAAC,EAAW,CAAC,EAEV,IAAgB,EACpB,OAAO,GAAmB,MAA8B;EACtD,IAAM,IAAS,MAAM,EAAqB;GACxC,gBAAgB,GAAmB;GACnC;GACA,MAAM;GACN;GACA,6BAAY,IAAI,MAAM,EAAC,aAAa;GACrC,CAAC;AAEF,SADA,GAAqB,EACd;IAET,CAAC,EAAoB,CACtB,EAEK,IAAyB,EAC7B,OAAO,GAAmB,MAAuC;EAC/D,IAAM,IAAS,MAAM,EAAqB;GACxC,gBAAgB,GAAmB;GACnC;GACA,MAAM;GACN;GACA,6BAAY,IAAI,MAAM,EAAC,aAAa;GACrC,CAAC;AAEF,SADA,GAAqB,EACd;IAET,CAAC,EAAoB,CACtB;AAgCD,QA9BA,QAAgB;AAOd,MANA,GAAqB,EAIhB,GAAY,EAEb,OAAO,SAAW,IAAa;EACnC,IAAM,UAAqB;AAEpB,GADL,EAAY,GAAK,EACZ,GAAY;KAEb,UAAsB,EAAY,GAAM;AAE9C,EADA,OAAO,iBAAiB,UAAU,EAAa,EAC/C,OAAO,iBAAiB,WAAW,EAAc;EAEjD,IAAM,IAAW,kBAAkB;AACjC,IAAI,OAAO,YAAc,OAAe,UAAU,WAC3C,GAAY;KAElB,EAAkB;AAErB,eAAa;AAGX,GAFA,OAAO,oBAAoB,UAAU,EAAa,EAClD,OAAO,oBAAoB,WAAW,EAAc,EACpD,cAAc,EAAS;;IAGxB,EAAE,CAAC,EAEC;EACL;EACA;EACA;EACA;EACA;EACA;EACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/mybodyshield/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/mybodyshield/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmGH,wBAAgB,aAAa,gCAgmB5B"}
|