Label every ingest source instead of leaking its enum value

Settings, Sources listed GOOGLE_HEALTH, OURA, POLAR and STRAVA by their
internal name: the catalogue was written for seven sources and every
provider added since fell through to the raw value. The measurement list
carried the same fall-through in its source filter and its table column,
and the medication intake history read "via APPLE_HEALTH" for a dose
synced from Apple Health.

The three catalogues move into one module keyed by the enum, so an
omission is a type error rather than a raw value on screen, and the sleep
discrepancy tooltip drops the private brand-name map it had grown to work
around the gap.
This commit is contained in:
Marc-André Bombeck committed 2026-09-02 12:08:31 +02:00
1 parent 72117104d5
commit 0548c0265e
15 files changed
+269 -78

No files matched your search

+17
View File
@@ -1,5 +1,22 @@
# Changelog
## [Unreleased]
### Fixed
- Settings, Sources: four of the sources in the priority ladder were listed
by their internal name. `GOOGLE_HEALTH`, `OURA`, `POLAR` and `STRAVA`
appeared exactly like that instead of Google Health, Oura, Polar and
Strava. The label catalogue was written when there were seven sources and
every provider added since fell through to the raw value. The same
fall-through sat in the measurement list, where the source filter offers
every source there is and the table paints one per row, and on the
medication intake history, where a dose synced from Apple Health read
"via APPLE_HEALTH". All three catalogues are complete in all seven
languages now, and a test reads the source enums out of the schema and
fails when a member has no label, so the next provider cannot ship
without its name.
## [1.38.2] — 2026-09-02
Anthropic keys work on current Claude models again, and the daily briefing
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "Berechnet",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "{count} Messwerte gelöscht",
"bulkDeleteSuccessOne": "Messwert gelöscht",
"bulkDeleteError": "Fehler beim Löschen der ausgewählten Messungen",
@@ -1630,6 +1637,7 @@
"sourceApi": "iOS-App",
"sourceReminder": "Erinnerung",
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "über {origin}",
"intakeHistoryEmptyTitle": "Noch keine Einnahmen erfasst",
"intakeHistoryEmptyDescription": "Trage eine Einnahme auf der Tagesübersicht ein, um den Verlauf aufzubauen.",
@@ -4889,7 +4897,14 @@
"IMPORT": "Import",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "Berechnet"
"COMPUTED": "Berechnet",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "Uhr",
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "Computed",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "{count} measurements deleted",
"bulkDeleteSuccessOne": "Measurement deleted",
"bulkDeleteError": "Error deleting the selected measurements",
@@ -1630,6 +1637,7 @@
"sourceApi": "iOS App",
"sourceReminder": "Reminder",
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "via {origin}",
"intakeHistoryEmptyTitle": "No intakes recorded yet",
"intakeHistoryEmptyDescription": "Log a dose on the daily intake page to start building the history.",
@@ -4889,7 +4897,14 @@
"IMPORT": "Import",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "Computed"
"COMPUTED": "Computed",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "Watch",
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "Calculado",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "{count} mediciones eliminadas",
"bulkDeleteSuccessOne": "Medición eliminada",
"bulkDeleteError": "Error al eliminar las mediciones seleccionadas",
@@ -1630,6 +1637,7 @@
"sourceApi": "App de iOS",
"sourceReminder": "Recordatorio",
"sourceImport": "Importación",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "vía {origin}",
"intakeHistoryEmptyTitle": "Aún no hay tomas",
"intakeHistoryEmptyDescription": "En cuanto registres una toma, aparecerá aquí con la hora y la fuente.",
@@ -4889,7 +4897,14 @@
"IMPORT": "Importar",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "Calculado"
"COMPUTED": "Calculado",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "Reloj",
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "Calculé",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "{count} mesures supprimées",
"bulkDeleteSuccessOne": "Mesure supprimée",
"bulkDeleteError": "Erreur lors de la suppression des mesures sélectionnées",
@@ -1630,6 +1637,7 @@
"sourceApi": "App iOS",
"sourceReminder": "Rappel",
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "via {origin}",
"intakeHistoryEmptyTitle": "Aucune prise pour l’instant",
"intakeHistoryEmptyDescription": "Dès que tu enregistreras une prise, elle apparaîtra ici avec l’heure et la source.",
@@ -4889,7 +4897,14 @@
"IMPORT": "Importer",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "Calculé"
"COMPUTED": "Calculé",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "Montre",
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "Calcolato",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "{count} misurazioni eliminate",
"bulkDeleteSuccessOne": "Misurazione eliminata",
"bulkDeleteError": "Errore durante l'eliminazione delle misurazioni selezionate",
@@ -1630,6 +1637,7 @@
"sourceApi": "App iOS",
"sourceReminder": "Promemoria",
"sourceImport": "Importazione",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "tramite {origin}",
"intakeHistoryEmptyTitle": "Ancora nessuna assunzione",
"intakeHistoryEmptyDescription": "Appena registrerai un’assunzione, comparirà qui con orario e sorgente.",
@@ -4889,7 +4897,14 @@
"IMPORT": "Importa",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "Calcolato"
"COMPUTED": "Calcolato",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "Orologio",
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "계산값",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "{count}개 측정을 삭제했어요",
"bulkDeleteSuccessOne": "측정을 삭제했어요",
"bulkDeleteError": "선택한 측정을 삭제하지 못했어요",
@@ -1630,6 +1637,7 @@
"sourceApi": "iOS 앱",
"sourceReminder": "알림",
"sourceImport": "불러오기",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "{origin} 경유",
"intakeHistoryEmptyTitle": "아직 복용 기록이 없어요",
"intakeHistoryEmptyDescription": "일일 복용 페이지에서 용량을 기록하면 이력이 쌓여요.",
@@ -4889,7 +4897,14 @@
"IMPORT": "불러오기",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "계산값"
"COMPUTED": "계산값",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "워치",
+16 -1
View File
@@ -731,6 +731,13 @@
"sourceComputed": "Obliczone",
"sourceWhoop": "WHOOP",
"sourceFitbit": "Fitbit",
"sourceNightscout": "Nightscout",
"sourcePolar": "Polar",
"sourceOura": "Oura",
"sourceTelegram": "Telegram",
"sourceMcp": "MCP",
"sourceGoogleHealth": "Google Health",
"sourceStrava": "Strava",
"bulkDeleteSuccess": "Usunięto pomiary: {count}",
"bulkDeleteSuccessOne": "Usunięto pomiar",
"bulkDeleteError": "Błąd podczas usuwania wybranych pomiarów",
@@ -1630,6 +1637,7 @@
"sourceApi": "Aplikacja iOS",
"sourceReminder": "Przypomnienie",
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "przez {origin}",
"intakeHistoryEmptyTitle": "Brak przyjęć",
"intakeHistoryEmptyDescription": "Gdy tylko zarejestrujesz przyjęcie, pojawi się tu wraz z godziną i źródłem.",
@@ -4889,7 +4897,14 @@
"IMPORT": "Importuj",
"WHOOP": "WHOOP",
"FITBIT": "Fitbit",
"COMPUTED": "Obliczone"
"COMPUTED": "Obliczone",
"NIGHTSCOUT": "Nightscout",
"POLAR": "Polar",
"OURA": "Oura",
"TELEGRAM": "Telegram",
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava"
},
"deviceLabels": {
"watch": "Zegarek",
@@ -110,12 +110,15 @@ const LEGIT_IDENTICAL = new Set<string>([
"admin.overview.snapshotOfflineGeoOn",
"admin.webPushVapidTitle",
"measurements.sourceAppleHealth",
"measurements.sourceGoogleHealth",
"medications.sourceAppleHealth",
"onboarding.source.more.appleHealth",
"settings.ai.providerChain.types.anthropic",
"settings.ai.providerChain.types.codex",
"settings.ai.providerSelect.anthropic",
"settings.sections.export.import.appleHealth.title",
"settings.sections.sources.sourceLabels.APPLE_HEALTH",
"settings.sections.sources.sourceLabels.GOOGLE_HEALTH",
"settings.appleHealth.title",
"mood.tag.fastFood",
// Technical / config labels conventionally kept verbatim.
@@ -10,6 +10,7 @@ import {
} from "@/components/ui/tooltip";
import { useTranslations } from "@/lib/i18n/context";
import { formatDurationMinutes } from "@/lib/i18n/duration";
import { MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS } from "@/lib/i18n/source-labels";
/**
* v1.28.x — the discreet "sources disagree" marker, extracted from
@@ -30,35 +31,6 @@ export interface SleepSourceDiscrepancyDto {
}[];
}
/**
* Localised label keys per `MeasurementSource` — the same map the
* source-priority settings surface uses (`sources-section.tsx`), so both
* surfaces humanise a source identically.
*/
const SOURCE_LABEL_KEYS: Record<string, string> = {
WITHINGS: "settings.sections.sources.sourceLabels.WITHINGS",
APPLE_HEALTH: "settings.sections.sources.sourceLabels.APPLE_HEALTH",
MANUAL: "settings.sections.sources.sourceLabels.MANUAL",
IMPORT: "settings.sections.sources.sourceLabels.IMPORT",
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
};
/**
* Brand names for sources without a localised settings label — brand
* spellings are locale-invariant, so no i18n key is needed for them.
*/
const SOURCE_BRAND_NAMES: Record<string, string> = {
OURA: "Oura",
POLAR: "Polar",
GOOGLE_HEALTH: "Google Health",
NIGHTSCOUT: "Nightscout",
STRAVA: "Strava",
TELEGRAM: "Telegram",
MCP: "MCP",
};
/**
* Localised device-type label keys (`Measurement.deviceType`), reused from
* the source-priority settings surface. Appended in parentheses so two
@@ -79,8 +51,13 @@ export function sourceDisplayName(
deviceType: string | null,
t: (key: string) => string,
): string {
const labelKey = SOURCE_LABEL_KEYS[source];
const base = labelKey ? t(labelKey) : (SOURCE_BRAND_NAMES[source] ?? source);
// The wire carries whatever the row's `source` column held, so an
// unknown value still has to render as itself rather than as a missing
// key. The catalogue covers every enum member the guard knows about.
const labelKey = MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS[
source as keyof typeof MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS
] as string | undefined;
const base = labelKey ? t(labelKey) : source;
const deviceKey = deviceType ? DEVICE_TYPE_LABEL_KEYS[deviceType] : undefined;
return deviceKey ? `${base} (${t(deviceKey)})` : base;
}
@@ -67,6 +67,7 @@ import { createPortal } from "react-dom";
import { toast } from "sonner";
import { formatDateOrRelative, formatDateTime } from "@/lib/format";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { MEASUREMENT_SOURCE_LIST_LABEL_KEYS } from "@/lib/i18n/source-labels";
import { CUMULATIVE_DAY_SUM_TYPES } from "@/lib/measurements/cumulative-day-sum";
import { rawDisplayFractionDigits } from "@/lib/measurements/display-transform";
import { getUnitForType } from "@/lib/validations/measurement";
@@ -217,23 +218,22 @@ function toDateTimeLocalValue(isoString: string): string {
}
/**
* Render the `MeasurementSource` enum (`MANUAL` / `WITHINGS` / `IMPORT`
* / `APPLE_HEALTH`) using the existing `measurements.source*`
* translation keys instead of leaking the SCREAMING_SNAKE enum into
* the table cell.
* Render the `MeasurementSource` enum using the `measurements.source*`
* translation keys instead of leaking the SCREAMING_SNAKE enum into the
* table cell or the filter dropdown. The catalogue is keyed by the enum,
* so a provider added without its label is a type error rather than a row
* that reads `GOOGLE_HEALTH`.
*/
function formatMeasurementSource(
source: string,
t: ReturnType<typeof useTranslations>["t"],
) {
if (source === "WITHINGS") return t("measurements.sourceWithings");
if (source === "IMPORT") return t("measurements.sourceImport");
if (source === "MANUAL") return t("measurements.sourceManual");
if (source === "APPLE_HEALTH") return t("measurements.sourceAppleHealth");
if (source === "COMPUTED") return t("measurements.sourceComputed");
if (source === "WHOOP") return t("measurements.sourceWhoop");
if (source === "FITBIT") return t("measurements.sourceFitbit");
return source;
const key = MEASUREMENT_SOURCE_LIST_LABEL_KEYS[
source as keyof typeof MEASUREMENT_SOURCE_LIST_LABEL_KEYS
] as string | undefined;
// A row written by a server the client does not know yet still renders
// as its own value rather than as a missing translation key.
return key ? t(key) : source;
}
/**
@@ -18,6 +18,10 @@ import {
import { useAuth } from "@/hooks/use-auth";
import { useIsMobile } from "@/hooks/use-is-mobile";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import {
INTAKE_SOURCE_LABEL_KEYS,
type IntakeSourceValue,
} from "@/lib/i18n/source-labels";
import { queryKeys } from "@/lib/query-keys";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -71,23 +75,22 @@ import { apiGet } from "@/lib/api/api-fetch";
* outline SkipForward + "Übersprungen" for skipped rows. The
* component never labels a row both at once.
* - Source badge surfaces where the intake was logged — WEB → "Web",
* API → "iOS App", REMINDER → "Reminder", IMPORT → "Import" — wrapped
* in a muted "via {origin}" caption next to the status. The labels map
* the coarse `IntakeSource` enum to the practical client per the
* existing `medications.source*` keys; there is no finer client field.
* API → "iOS App", REMINDER → "Reminder", IMPORT → "Import",
* APPLE_HEALTH → "Apple Health" — wrapped in a muted "via {origin}"
* caption next to the status. The labels map the coarse `IntakeSource`
* enum to the practical client per the existing `medications.source*`
* keys; there is no finer client field.
* - Empty state copy + CTA back to the daily intake page.
* - No edit-in-row, no delete buttons.
*/
type IntakeSource = "WEB" | "API" | "REMINDER" | "IMPORT";
export interface IntakeEvent {
id: string;
medicationId: string;
scheduledFor: string;
takenAt: string | null;
skipped: boolean;
source: IntakeSource;
source: IntakeSourceValue;
createdAt: string;
}
@@ -205,13 +208,14 @@ export function IntakeHistoryListV2({
enabled: isAuthenticated,
});
const sourceLabels: Record<IntakeSource, string> = useMemo(
() => ({
WEB: t("medications.sourceWeb"),
API: t("medications.sourceApi"),
REMINDER: t("medications.sourceReminder"),
IMPORT: t("medications.sourceImport"),
}),
const sourceLabels: Record<IntakeSourceValue, string> = useMemo(
() =>
Object.fromEntries(
Object.entries(INTAKE_SOURCE_LABEL_KEYS).map(([source, key]) => [
source,
t(key),
]),
) as Record<IntakeSourceValue, string>,
[t],
);
+2 -14
View File
@@ -48,6 +48,7 @@ import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS } from "@/lib/i18n/source-labels";
import { queryKeys, refetchInactiveDailyReads } from "@/lib/query-keys";
import {
DEFAULT_DEVICE_TYPE_PRIORITY,
@@ -96,19 +97,6 @@ const METRIC_LABEL_KEYS: Record<SourcePriorityMetricKey, string> = {
sleepDebt: "settings.sections.sources.metrics.sleepDebt",
};
const SOURCE_LABEL_KEYS: Record<string, string> = {
WITHINGS: "settings.sections.sources.sourceLabels.WITHINGS",
APPLE_HEALTH: "settings.sections.sources.sourceLabels.APPLE_HEALTH",
MANUAL: "settings.sections.sources.sourceLabels.MANUAL",
IMPORT: "settings.sections.sources.sourceLabels.IMPORT",
// v1.11.0 — WHOOP native source + the COMPUTED proxy (surfaces in the
// `recovery` ladder for native-vs-derived ordering).
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
// v1.12.0 — Fitbit/Pixel native source.
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
};
const DEVICE_TYPE_LABEL_KEYS: Record<DeviceType, string> = {
watch: "settings.sections.sources.deviceLabels.watch",
band: "settings.sections.sources.deviceLabels.band",
@@ -355,7 +343,7 @@ export function SourcesSection() {
{index + 1}.
</span>
<span className="flex-1 text-sm">
{t(SOURCE_LABEL_KEYS[source] ?? source)}
{t(MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS[source])}
</span>
{/* v1.4.27 R3d MB2 — stack up/down vertically on
narrow viewports so each button keeps the
@@ -260,6 +260,9 @@ describe("i18n locale file integrity", () => {
"medications.wizard.steps.step3.unit.g",
// v1.11.0 — "WHOOP" is a brand name, identical across every locale.
"settings.sections.sources.sourceLabels.WHOOP",
// "MCP" is the protocol's own acronym — the same four letters in every
// locale, exactly like the WHOOP entry above.
"settings.sections.sources.sourceLabels.MCP",
]);
it.each(ALL_LOCALES)("$locale locale has no empty values", ({ path }) => {
+94
View File
@@ -0,0 +1,94 @@
/**
* One place that says which i18n key labels which provenance enum member.
*
* Three surfaces render a source to the user and each one carried its own
* copy of the map: Settings → Sources, the sleep discrepancy tooltip, and
* the measurement list (filter dropdown + table badge). Every copy was
* written when the enum was shorter, and every copy fell through to the raw
* SCREAMING_SNAKE value for the providers added afterwards — the settings
* ladder listed `GOOGLE_HEALTH`, `OURA`, `POLAR` and `STRAVA` verbatim.
*
* The maps live here so a new provider is one edit, and they are keyed by
* the enum itself so the edit is not optional: leaving a member out is a
* type error, and `measurement-source-label-coverage.test.ts` proves the
* keys resolve in the message bundle.
*/
import { measurementSourceEnum } from "@/lib/validations/measurement";
export type MeasurementSourceValue =
(typeof measurementSourceEnum.options)[number];
/**
* Settings → Sources ("Source priority by metric") and the sleep
* source-discrepancy tooltip. Reads as a provenance name: "Manual entry",
* "Computed", and the provider's own brand spelling for everything else.
*/
export const MEASUREMENT_SOURCE_SETTINGS_LABEL_KEYS: Record<
MeasurementSourceValue,
string
> = {
WITHINGS: "settings.sections.sources.sourceLabels.WITHINGS",
APPLE_HEALTH: "settings.sections.sources.sourceLabels.APPLE_HEALTH",
MANUAL: "settings.sections.sources.sourceLabels.MANUAL",
IMPORT: "settings.sections.sources.sourceLabels.IMPORT",
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
NIGHTSCOUT: "settings.sections.sources.sourceLabels.NIGHTSCOUT",
POLAR: "settings.sections.sources.sourceLabels.POLAR",
OURA: "settings.sections.sources.sourceLabels.OURA",
TELEGRAM: "settings.sections.sources.sourceLabels.TELEGRAM",
MCP: "settings.sections.sources.sourceLabels.MCP",
GOOGLE_HEALTH: "settings.sections.sources.sourceLabels.GOOGLE_HEALTH",
STRAVA: "settings.sections.sources.sourceLabels.STRAVA",
};
/**
* The measurement list — the source facet in the filter rail and the badge
* in the table's source column. Its own catalogue because the badge column
* is narrow: "Manual" where the settings ladder says "Manual entry".
*/
export const MEASUREMENT_SOURCE_LIST_LABEL_KEYS: Record<
MeasurementSourceValue,
string
> = {
MANUAL: "measurements.sourceManual",
WITHINGS: "measurements.sourceWithings",
IMPORT: "measurements.sourceImport",
APPLE_HEALTH: "measurements.sourceAppleHealth",
COMPUTED: "measurements.sourceComputed",
WHOOP: "measurements.sourceWhoop",
FITBIT: "measurements.sourceFitbit",
NIGHTSCOUT: "measurements.sourceNightscout",
POLAR: "measurements.sourcePolar",
OURA: "measurements.sourceOura",
TELEGRAM: "measurements.sourceTelegram",
MCP: "measurements.sourceMcp",
GOOGLE_HEALTH: "measurements.sourceGoogleHealth",
STRAVA: "measurements.sourceStrava",
};
/**
* `IntakeSource` — where a medication intake was logged. Not a Zod enum
* anywhere (the write surfaces validate against narrower literals), so the
* member list is written out and pinned against `prisma/schema.prisma` by
* the coverage guard.
*/
export const INTAKE_SOURCE_VALUES = [
"WEB",
"API",
"REMINDER",
"IMPORT",
"APPLE_HEALTH",
] as const;
export type IntakeSourceValue = (typeof INTAKE_SOURCE_VALUES)[number];
/** The "via {origin}" caption on a medication intake row. */
export const INTAKE_SOURCE_LABEL_KEYS: Record<IntakeSourceValue, string> = {
WEB: "medications.sourceWeb",
API: "medications.sourceApi",
REMINDER: "medications.sourceReminder",
IMPORT: "medications.sourceImport",
APPLE_HEALTH: "medications.sourceAppleHealth",
};