Skip to main content
Synced from the repo — do not edit here

Canonical source: docs/claude/app-redesign-wiring-recipes.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.

App Redesign — Real-Data Wiring Recipes (verified against code 2026-07-10)

The "wire real data" implementation spec for the redesign screens. Every recipe below is verified against actual code (file:line). Companion to app-redesign-data-contracts.md (backend contracts) and app-redesign-scores.md (Biological Age formula).

The one mechanism

  • context.service<T>() resolves any registered service from the ServiceCrate (basiscoreui/lib/src/services/service.dart:121). Pass a nullable type (context.service<ServiceClinic?>()) for feature-gated services → null instead of throw.
  • Registered at root via CrateProvider in basishybrid/lib/view/app_client.dart:1018. Service list authoritative in ClientAppContainer.create (app_client.dart:41-171).
  • Data comes back as RxDart streams → feed StreamBuilder. For the logged-in user use WidgetUserStream (basishybrid/lib/view/widgets/widget_user_stream.dart).
  • FetchableResource<T> (health resources): call .fetch(force: true) to kick, then either listen .stream (Stream<(void, T)> → take .$2) or read sync getters .dataOptional (T?) / .data (fetchable_resource.dart:489-490).

CLINIC RESOLUTION (gate for all clinic-scoped data)

  • ServiceClinic.clinic (basishybrid/lib/services/service_clinic.dart:167) is null after custom-token sign-in — nothing auto-resolves it (_updateAuthState does not read membership).
  • Fix: await context.service<ServiceClinic>().updateClinic('<clinicsv2-doc-id>') (service_clinic.dart:238) — sets + persists + emits, driving every ServiceClinicStorageCurrent.watch* to switchMap onto the real clinic. Same call the dev tool uses (route_developer.dart:188).
  • Clinic id = the clinicsv2 doc id. George has SEVERAL clinics; must pass a specific one. Multi-clinic picker normally at route_intro.dart:683.

Per-screen recipes

Identity (header, profile)

  • WidgetUserStream(equals: (a,b)=>..., builder: (ctx, snap){ final u = snap.data; })BasisUserV1 (basiscore/lib/src/models/user/user.dart:48): name (String?, no separate firstName — split it), email, profileImage, dateOfBirth (DateTime?, .ageInYears(now) :462), gender (BasisGender, may arrive as int → BasisGender.fromInt), height (double? cm). Weight is NOT here — it's a health metric (InsightType/BasisHealthType.weight, kg).
  • Clinic-side names/sex/membership: context.service<ServiceClinicStorageCurrent>().watchCurrentClinicUser()ClinicUserDetails (clinic.dart:1789): firstName, lastName, gender, dateOfBirth (String date-only), membershipType, status.

Wearables today (sleep, HRV, RHR) + biomarkers + labs — ALL via one pipeline

  • final res = context.service<ServiceDayMemory>().dayResource(today, debugName: 'redesign-health') (service_daymemory.dart:527; today must be normalized to day boundary — day.isDay required).
  • res.insightsFetchableResource<Map<InsightType, InsightToday>> (wearables/vitals)
  • res.labsFetchableResource<Map<String, InsightToday>> (keyed by lowercased analyteKey)
  • InsightToday (basiscore/lib/src/models/user/insight_type.dart:765): insightType, numericalValue (double? raw), value (String? formatted), inRange (EvaluatedBenchmark = inRange/optimal/outOfRange), time (DateTime? json key "day"), deviceInfo, progress. Lab fields: labAnalyteKey, labDisplayName, labMinRange, labMaxRange, labOptimalLow, labOptimalHigh, labUnit, labCategory.
  • Key InsightTypes: sleepDuration (numericalValue=asleep seconds), restingHeartRate ('rhr'), hrvSDNN/hrvRMSSD, steps, spo2, calories, glucoseAvg... (map in widget_trends_today.dart:_insightFromMetricCode).
  • Needs-attention = inRange == EvaluatedBenchmark.outOfRange. By-system = group labs by labCategory.
  • Reference consumer: WidgetProgress<K> / WidgetProgressState (widget_trends_today.dart:837+) — calls widget.fetch(day).fetch(force:true) in initState, renders .values of the map. Health screen: route_my_health.dart:50.
  • ⚠ NO sleep score exists (only duration + debt). ⚠ NO biological age exists anywhere — both net-new.

Active protocol + tasks (Plan)

  • No whole "Protocol" object client-side. Protocol = individual BasisEventV1 events tagged protocolId+habitId.
  • context.service<ServiceDayMemory>().scheduledEventsStream<List<BasisEventV1>> (service_daymemory.dart:319), or dayResource(day).
  • BasisEventV1 (basiscore/lib/src/models/event/event.dart:69): name, type (BasisEventType; type.isHabit = protocol task), completionStatus (BasisEventCompletionStatus, .isCompleted), recurrenceRule, protocolId/habitId/clinicId/clinicianId, coachNotes, workoutPreset/supplementPreset/medicationPreset/mealPreset, start/end.
  • Completion ratio: ServiceRecurringEvents.determineCompletionRatio (basiscore/lib/src/services/service_recurring_events.dart:357). Reader: widget_home.dart:1091WidgetEventList.filter.

Appointments / up-next (Today, Booking)

  • context.service<ServiceClinicStorageCurrent>().watchAppointmentsForCurrentUser({start,end})Stream<List<BasisEventV1>> (service_clinic_storage_current.dart:387, auto-scoped to clinic+uid). type == BasisEventType.scheduled.
  • Fields: name (title), start/end, status (BasisEventStatus? e.g. canceled), location (SummaryObjectPositionV1? .name/.text), attendees. Coach name from attendees: attendees.where((a)=>a.isOrganiser && !a.isCurrentUser).firstOrNull?.name.
  • Reader: route_appointments.dart:43; row list_item_appointment_compact.dart.

Care-team chat

  • context.service<ServiceChat>() (basiscoreui/lib/src/services/service_chat.dart:279). Conversation via ChatMessageSubscription.chatStream<BasisChatV2?>. Doc chosen by serviceFirebase.chatDoc(null, uid). Built route_chat.dart:2141, consumed :2190.
  • BasisChatV2 (service_chat_models.dart:1132): List<BasisChatMessageV2> messages. BasisChatMessageV2 (:951): content (String?), author (BasisChatAuthorV2: id,name,role (ChatRole: user/coach/expert/backend(AI)/system),image,title), time, media, readReceipts.

Membership / billing (Profile)

  • context.service<ServiceClinicStorageCurrent>().watchCurrentClinicUser()ClinicUserDetails.membershipType (holds membershipId) + .status (active/inactive).
  • Catalog: .watchClinicDetails()ClinicDetails.memberships (List<MembershipType> clinic.dart:3940); resolve name via memberships.firstWhereOrNull((m)=>m.membershipId==membershipType)?.name. Price: MembershipType.stripePrice['unit_amount'] (cents) / ['currency'] / ['recurring']['interval'].
  • Reader: widget_profile.dart:76-95. Subscribe: ServiceClinicStripe.subscribeToMembership (service_clinic_stripe.dart:296).

Cross-cutting cautions

  • Every clinic-scoped read funnels through ServiceClinicStorageCurrent (auto-scopes to ServiceClinic.clinic + uid) → needs clinic resolution above.
  • BasisEventV1 is the single source for BOTH plan tasks and appointments — discriminate by type (isHabit vs scheduled).
  • Firestore types not guaranteed at runtime — guard name, gender (int), stripePrice['unit_amount'], ranges with fallbacks + String(...) coercion.