mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Garmin Connect now offers a Personal API: a person creates a key for their own account and any client can buy short-lived tokens with it. HealthLog takes that key in a new Garmin card, checks it with Garmin before storing it sealed on the user row, and polls every hour. - Transport: key exchange with a per-process token cache keyed on the key, one re-exchange on a refused token and then reauth_required, the 429 Retry-After carried to the caller, the China host tried when the global one does not know the key, answers read with a byte cap, all egress through safeFetch to the two fixed hosts. - Sync: daily summaries, sleep summaries, HRV, weigh-ins and the activity search, chunked to each resource's documented range, read one after another and stopped at the first rate limit; today's menstrual cycle summary adds logged period days the person has not logged. - Backfill: a resumable walk through the shared admission lane that learns where the history starts from the daily summaries and the activities, waits out Garmin's rate limit and stamps its marker. - Types: GARMIN source; BODY_BATTERY and STRESS_LEVEL for Garmin's own day scores, kept apart from the computed STRESS_SCORE; TRAINING_READINESS and ACUTE_TRAINING_LOAD for figures a bridge pushes through the ingest token. All four chart on the recovery page and sit on the ladders, units, ranges, labels and report groups like the device-native scores. - Key expiry: learned from a token capped short of a day, else estimated from the validity picked at connect; the card warns a week ahead and asks for a new key once Garmin refuses the old one. - Ledger, health, freshness, watchdog, backup classes, wipe plan, encrypted-column registry, rotation script, OpenAPI and all ten locales carry the new integration. The old "no Garmin connector" note is gone.
490 lines
12 KiB
TypeScript
490 lines
12 KiB
TypeScript
import type { ModuleKey } from "@/lib/modules/registry";
|
|
|
|
/**
|
|
* Every Settings card the command palette lists on its own, with the anchor
|
|
* that lands on it (`/settings/<section>#<anchor>`).
|
|
*
|
|
* Most anchors are the `anchor` prop of the card's `SettingsCardHeader`; a
|
|
* few are older wrapper ids (the integration providers, `#channels`,
|
|
* `#sharing`, `#thresholds`, the reminder and Coach memory cards). The test
|
|
* `settings-cards.test.ts` reads the settings sources and fails when an
|
|
* anchor listed here exists nowhere, so a renamed card cannot quietly turn
|
|
* into a link to the top of its page.
|
|
*
|
|
* Left out on purpose: notices that come and go (the passkey nudge, a
|
|
* pending handover decision, score change notices, the AI operator notice),
|
|
* cards that only exist when the operator configured a channel (Telegram,
|
|
* ntfy, Web Push, email, document sources, the central Codex switch), and
|
|
* sections that are a single card, where the section entry is the card.
|
|
*/
|
|
export interface SettingsCardEntry {
|
|
/** The settings section slug, or a `layout/<module>` sub-page. */
|
|
section: string;
|
|
anchor: string;
|
|
/** i18n key of the card title, as the card itself renders it. */
|
|
titleKey: string;
|
|
/** Shown only while one of these modules is on. */
|
|
modules?: ReadonlyArray<ModuleKey>;
|
|
}
|
|
|
|
export const SETTINGS_CARDS: ReadonlyArray<SettingsCardEntry> = [
|
|
// ── Account ──
|
|
{ section: "account", anchor: "avatar", titleKey: "settings.avatar.title" },
|
|
{ section: "account", anchor: "profile", titleKey: "settings.profile" },
|
|
{
|
|
section: "account",
|
|
anchor: "display",
|
|
titleKey: "settings.displayCard.title",
|
|
},
|
|
// ── Security ──
|
|
{
|
|
section: "security",
|
|
anchor: "two-factor",
|
|
titleKey: "settings.security.totp.title",
|
|
},
|
|
{
|
|
section: "security",
|
|
anchor: "security-keys",
|
|
titleKey: "settings.security.keys.title",
|
|
},
|
|
{ section: "security", anchor: "passkeys", titleKey: "settings.passkeys" },
|
|
{
|
|
section: "security",
|
|
anchor: "password",
|
|
titleKey: "settings.passwordTitle",
|
|
},
|
|
// ── Access ──
|
|
{
|
|
section: "access",
|
|
anchor: "share-invite",
|
|
titleKey: "recordSharing.invite.title",
|
|
},
|
|
{
|
|
section: "access",
|
|
anchor: "managed-profiles",
|
|
titleKey: "recordSharing.managed.title",
|
|
},
|
|
{
|
|
section: "access",
|
|
anchor: "shared-by-you",
|
|
titleKey: "recordSharing.given.title",
|
|
},
|
|
{
|
|
section: "access",
|
|
anchor: "shared-with-you",
|
|
titleKey: "recordSharing.received.title",
|
|
},
|
|
{
|
|
section: "access",
|
|
anchor: "record-activity",
|
|
titleKey: "recordSharing.activity.title",
|
|
},
|
|
// ── Privacy ──
|
|
{
|
|
section: "privacy",
|
|
anchor: "encryption",
|
|
titleKey: "settings.privacy.encryption.title",
|
|
},
|
|
{
|
|
section: "privacy",
|
|
anchor: "stored-data",
|
|
titleKey: "settings.privacy.stored.title",
|
|
},
|
|
{
|
|
section: "privacy",
|
|
anchor: "retention",
|
|
titleKey: "settings.privacy.retention.title",
|
|
},
|
|
{
|
|
section: "privacy",
|
|
anchor: "delete-data",
|
|
titleKey: "settings.privacy.delete.title",
|
|
},
|
|
{
|
|
section: "privacy",
|
|
anchor: "privacy-posture",
|
|
titleKey: "settings.privacy.posture.title",
|
|
},
|
|
{
|
|
section: "security",
|
|
anchor: "sessions",
|
|
titleKey: "settings.security.sessionsTitle",
|
|
},
|
|
{
|
|
section: "security",
|
|
anchor: "trusted-devices",
|
|
titleKey: "settings.security.trustedDevices.title",
|
|
},
|
|
{
|
|
section: "security",
|
|
anchor: "security-activity",
|
|
titleKey: "settings.security.activityTitle",
|
|
},
|
|
// ── Integrations: connections (wrapper ids) ──
|
|
{
|
|
section: "integrations",
|
|
anchor: "withings",
|
|
titleKey: "settings.withings",
|
|
},
|
|
{ section: "integrations", anchor: "whoop", titleKey: "settings.whoop" },
|
|
{ section: "integrations", anchor: "fitbit", titleKey: "settings.fitbit" },
|
|
{
|
|
section: "integrations",
|
|
anchor: "google-health",
|
|
titleKey: "settings.googleHealth",
|
|
},
|
|
{
|
|
section: "integrations",
|
|
anchor: "apple-health",
|
|
titleKey: "settings.appleHealth.title",
|
|
},
|
|
{ section: "integrations", anchor: "polar", titleKey: "settings.polar" },
|
|
{ section: "integrations", anchor: "oura", titleKey: "settings.oura" },
|
|
{ section: "integrations", anchor: "strava", titleKey: "settings.strava" },
|
|
{
|
|
section: "integrations",
|
|
anchor: "nightscout",
|
|
titleKey: "settings.nightscout",
|
|
},
|
|
{
|
|
section: "integrations",
|
|
anchor: "garmin",
|
|
titleKey: "settings.garmin",
|
|
},
|
|
{
|
|
section: "integrations",
|
|
anchor: "nutrient-intake",
|
|
titleKey: "settings.sections.sources.nutrients.title",
|
|
modules: ["nutrients"],
|
|
},
|
|
// ── Integrations: channels ──
|
|
{
|
|
section: "integrations",
|
|
anchor: "channels",
|
|
titleKey: "settings.sections.integrations.channelsHeading",
|
|
},
|
|
{
|
|
section: "integrations",
|
|
anchor: "delivery-status",
|
|
titleKey: "settings.notificationStatus.title",
|
|
},
|
|
{ section: "integrations", anchor: "webhook", titleKey: "settings.webhook" },
|
|
// ── Health record ──
|
|
{
|
|
section: "gesundheitsakte",
|
|
anchor: "health-record",
|
|
titleKey: "settings.healthRecord.title",
|
|
},
|
|
{
|
|
section: "gesundheitsakte",
|
|
anchor: "share-link-create",
|
|
titleKey: "settings.sharing.createTitle",
|
|
},
|
|
{
|
|
section: "gesundheitsakte",
|
|
anchor: "share-links-active",
|
|
titleKey: "settings.sharing.activeTitle",
|
|
},
|
|
// ── Export and import ──
|
|
{
|
|
section: "export",
|
|
anchor: "measurements-csv",
|
|
titleKey: "settings.sections.export.cards.measurementsCsv.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "medications-csv",
|
|
titleKey: "settings.sections.export.cards.medicationsCsv.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "mood-csv",
|
|
titleKey: "settings.sections.export.cards.moodCsv.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "full-backup",
|
|
titleKey: "settings.sections.export.cards.fullBackup.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "import-apple-health",
|
|
titleKey: "settings.sections.export.import.appleHealth.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "import-health-connect",
|
|
titleKey: "settings.sections.export.import.healthConnect.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "import-json",
|
|
titleKey: "settings.sections.export.import.json.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "import-csv",
|
|
titleKey: "settings.sections.export.import.csv.title",
|
|
},
|
|
{
|
|
section: "export",
|
|
anchor: "import-dose-history",
|
|
titleKey: "settings.sections.export.import.doseHistory.title",
|
|
},
|
|
// ── Thresholds ──
|
|
{
|
|
section: "thresholds",
|
|
anchor: "glucose-reference",
|
|
titleKey: "settings.glucoseReference.title",
|
|
},
|
|
{
|
|
section: "thresholds",
|
|
anchor: "thresholds",
|
|
titleKey: "thresholds.cardTitle",
|
|
},
|
|
// ── Notifications ──
|
|
{
|
|
section: "notifications",
|
|
anchor: "mood-reminder",
|
|
titleKey: "notifications.moodReminder.title",
|
|
modules: ["mood"],
|
|
},
|
|
{
|
|
section: "notifications",
|
|
anchor: "low-stock",
|
|
titleKey: "notifications.lowStock.title",
|
|
modules: ["medications"],
|
|
},
|
|
{
|
|
section: "notifications",
|
|
anchor: "vorsorge-reminders",
|
|
titleKey: "notifications.checkupReminders.title",
|
|
},
|
|
// ── Anamnesis ──
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "ai-inclusion",
|
|
titleKey: "records.aiInclusion.cardTitle",
|
|
modules: ["coach", "insights"],
|
|
},
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "conditions",
|
|
titleKey: "records.conditions.cardTitle",
|
|
modules: ["coach"],
|
|
},
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "about-me",
|
|
titleKey: "settings.ai.aboutMe.title",
|
|
modules: ["coach", "insights"],
|
|
},
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "life-circumstances",
|
|
titleKey: "records.life.cardTitle",
|
|
},
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "allergies",
|
|
titleKey: "records.allergies.cardTitle",
|
|
},
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "family-history",
|
|
titleKey: "records.family.cardTitle",
|
|
},
|
|
{
|
|
section: "anamnesis",
|
|
anchor: "emergency",
|
|
titleKey: "records.emergency.cardTitle",
|
|
},
|
|
// ── Environment ──
|
|
{
|
|
section: "environment",
|
|
anchor: "home-location",
|
|
titleKey: "settings.sections.environment.home.title",
|
|
modules: ["environment"],
|
|
},
|
|
{
|
|
section: "environment",
|
|
anchor: "travel",
|
|
titleKey: "settings.sections.environment.travel.title",
|
|
modules: ["environment"],
|
|
},
|
|
{
|
|
section: "environment",
|
|
anchor: "weather-backfill",
|
|
titleKey: "settings.sections.environment.backfill.title",
|
|
modules: ["environment"],
|
|
},
|
|
{
|
|
section: "environment",
|
|
anchor: "air-quality",
|
|
titleKey: "settings.sections.environment.airQuality.title",
|
|
modules: ["environment"],
|
|
},
|
|
// ── AI ──
|
|
{
|
|
section: "ai",
|
|
anchor: "ai-provider",
|
|
titleKey: "settings.ai.activeProviderHeading",
|
|
},
|
|
{
|
|
section: "ai",
|
|
anchor: "provider-chain",
|
|
titleKey: "settings.ai.providerChain.title",
|
|
},
|
|
{
|
|
section: "ai",
|
|
anchor: "response-timeout",
|
|
titleKey: "settings.ai.responseTimeoutHeading",
|
|
},
|
|
{
|
|
section: "ai",
|
|
anchor: "auto-read",
|
|
titleKey: "settings.ai.autoRead.title",
|
|
},
|
|
{
|
|
section: "ai",
|
|
anchor: "ai-consent",
|
|
titleKey: "settings.ai.consent.title",
|
|
},
|
|
{ section: "ai", anchor: "ai-runtime", titleKey: "settings.ai.runtimeTitle" },
|
|
// ── Coach ──
|
|
{
|
|
section: "coach",
|
|
anchor: "coach-activate",
|
|
titleKey: "settings.coach.state.title",
|
|
},
|
|
{
|
|
section: "coach",
|
|
anchor: "coach-preferences",
|
|
titleKey: "insights.coach.settingsTitle",
|
|
},
|
|
{
|
|
section: "notifications",
|
|
anchor: "coach-nudge",
|
|
titleKey: "notifications.coachNudge.title",
|
|
},
|
|
{
|
|
section: "coach",
|
|
anchor: "coach-memory",
|
|
titleKey: "settings.ai.coachMemory.title",
|
|
},
|
|
{
|
|
section: "coach",
|
|
anchor: "coach-conversations",
|
|
titleKey: "settings.ai.coachConversations.title",
|
|
},
|
|
// ── API ──
|
|
{
|
|
section: "api",
|
|
anchor: "api-endpoints",
|
|
titleKey: "settings.apiEndpointsTitle",
|
|
},
|
|
{
|
|
section: "api",
|
|
anchor: "measurements-token",
|
|
titleKey: "settings.measurementsToken.title",
|
|
},
|
|
{
|
|
section: "api",
|
|
anchor: "workouts-token",
|
|
titleKey: "settings.workoutsToken.title",
|
|
},
|
|
{
|
|
section: "api",
|
|
anchor: "documents-token",
|
|
titleKey: "settings.documentsToken.title",
|
|
},
|
|
{ section: "api", anchor: "api-tokens", titleKey: "settings.apiTokens" },
|
|
// ── MCP ──
|
|
{
|
|
section: "mcp",
|
|
anchor: "mcp-enable",
|
|
titleKey: "settings.mcp.enableTitle",
|
|
},
|
|
{
|
|
section: "mcp",
|
|
anchor: "mcp-connections",
|
|
titleKey: "settings.mcp.connectionsTitle",
|
|
},
|
|
{
|
|
section: "mcp",
|
|
anchor: "mcp-tokens",
|
|
titleKey: "settings.mcp.tokensTitle",
|
|
},
|
|
// ── Advanced ──
|
|
{
|
|
section: "advanced",
|
|
anchor: "danger-zone",
|
|
titleKey: "settings.dangerZone",
|
|
},
|
|
{
|
|
section: "advanced",
|
|
anchor: "delete-account",
|
|
titleKey: "settings.deleteAccountCardTitle",
|
|
},
|
|
];
|
|
|
|
/**
|
|
* The Arrangement hub's sub-pages, each its own entry. A module's page shows
|
|
* only while the module is on.
|
|
*/
|
|
export const SETTINGS_LAYOUT_PAGES: ReadonlyArray<{
|
|
slug: string;
|
|
titleKey: string;
|
|
module?: ModuleKey;
|
|
}> = [
|
|
{ slug: "dashboard", titleKey: "settings.sections.layout.dashboard.title" },
|
|
{ slug: "insights", titleKey: "settings.sections.layout.insights.title" },
|
|
{
|
|
slug: "medications",
|
|
titleKey: "settings.sections.layout.medications.title",
|
|
module: "medications",
|
|
},
|
|
{
|
|
slug: "labs",
|
|
titleKey: "settings.sections.layout.labs.title",
|
|
module: "labs",
|
|
},
|
|
{
|
|
slug: "illness",
|
|
titleKey: "settings.sections.layout.illness.title",
|
|
module: "illness",
|
|
},
|
|
{
|
|
slug: "documents",
|
|
titleKey: "settings.sections.layout.documents.title",
|
|
module: "inboundDocuments",
|
|
},
|
|
{ slug: "vorsorge", titleKey: "settings.sections.layout.vorsorge.title" },
|
|
];
|
|
|
|
/**
|
|
* The module pages under Settings > Modules (`/settings/modules/<slug>`),
|
|
* each its own entry while its module is on.
|
|
*/
|
|
export const SETTINGS_MODULE_PAGES: ReadonlyArray<{
|
|
slug: string;
|
|
titleKey: string;
|
|
module: ModuleKey;
|
|
}> = [
|
|
{
|
|
slug: "medications",
|
|
titleKey: "settings.sections.modules.pages.medications.title",
|
|
module: "medications",
|
|
},
|
|
{
|
|
slug: "mood",
|
|
titleKey: "settings.sections.modules.pages.mood.title",
|
|
module: "mood",
|
|
},
|
|
{
|
|
slug: "labs",
|
|
titleKey: "settings.sections.modules.pages.labs.title",
|
|
module: "labs",
|
|
},
|
|
];
|