From 69163a37d1d79ba78774515267e791a076d8f16c Mon Sep 17 00:00:00 2001 From: chelsea Date: Mon, 16 Feb 2026 20:04:58 -0600 Subject: [PATCH] Add complete UI for adaptive medication settings with presence tracking and nagging configuration --- .../src/app/dashboard/settings/page.tsx | 264 ++++++++++++++++++ synculous-client/src/lib/api.ts | 52 ++++ 2 files changed, 316 insertions(+) diff --git a/synculous-client/src/app/dashboard/settings/page.tsx b/synculous-client/src/app/dashboard/settings/page.tsx index 8f5f41e..da98ff1 100644 --- a/synculous-client/src/app/dashboard/settings/page.tsx +++ b/synculous-client/src/app/dashboard/settings/page.tsx @@ -21,6 +21,23 @@ interface NotifSettings { ntfy_enabled: boolean; } +interface AdaptiveMedSettings { + adaptive_timing_enabled: boolean; + adaptive_mode: string; + presence_tracking_enabled: boolean; + nagging_enabled: boolean; + nag_interval_minutes: number; + max_nag_count: number; + quiet_hours_start: string | null; + quiet_hours_end: string | null; +} + +interface PresenceStatus { + is_online: boolean; + last_online_at: string | null; + typical_wake_time: string | null; +} + export default function SettingsPage() { const [prefs, setPrefs] = useState({ sound_enabled: false, @@ -34,8 +51,24 @@ export default function SettingsPage() { ntfy_topic: '', ntfy_enabled: false, }); + const [adaptiveMeds, setAdaptiveMeds] = useState({ + adaptive_timing_enabled: false, + adaptive_mode: 'shift_all', + presence_tracking_enabled: false, + nagging_enabled: true, + nag_interval_minutes: 15, + max_nag_count: 4, + quiet_hours_start: null, + quiet_hours_end: null, + }); + const [presence, setPresence] = useState({ + is_online: false, + last_online_at: null, + typical_wake_time: null, + }); const [isLoading, setIsLoading] = useState(true); const [saved, setSaved] = useState(false); + const [showHelp, setShowHelp] = useState(false); useEffect(() => { Promise.all([ @@ -46,6 +79,8 @@ export default function SettingsPage() { ntfy_topic: data.ntfy_topic, ntfy_enabled: data.ntfy_enabled, })), + api.adaptiveMeds.getSettings().then((data: AdaptiveMedSettings) => setAdaptiveMeds(data)), + api.adaptiveMeds.getPresence().then((data: PresenceStatus) => setPresence(data)), ]) .catch(() => {}) .finally(() => setIsLoading(false)); @@ -79,6 +114,18 @@ export default function SettingsPage() { } }; + const updateAdaptiveMeds = async (updates: Partial) => { + const prev = { ...adaptiveMeds }; + const updated = { ...adaptiveMeds, ...updates }; + setAdaptiveMeds(updated); + try { + await api.adaptiveMeds.updateSettings(updates); + flashSaved(); + } catch { + setAdaptiveMeds(prev); + } + }; + if (isLoading) { return (
@@ -241,6 +288,223 @@ export default function SettingsPage() {
+ {/* Adaptive Medication Settings */} +
+
+

Smart Medication Timing

+ +
+ + {showHelp && ( +
+

Adaptive Timing: Automatically adjusts your medication schedule based on when you wake up. If you wake up late, your morning meds get shifted too.

+

Discord Presence: Detects when you come online (wake up) and uses that to calculate adjustments. Requires Discord notifications to be enabled.

+

Nagging: Sends you reminders every 15 minutes (configurable) up to 4 times if you miss a dose.

+
+ )} + +
+ {/* Enable Adaptive Timing */} +
+
+
+

Enable adaptive timing

+

Adjust medication times based on your wake time

+
+ +
+ + {adaptiveMeds.adaptive_timing_enabled && ( +
+ {/* Adaptive Mode Selection */} +
+

Adjustment mode

+
+ + +
+
+
+ )} +
+ + {/* Presence Tracking */} +
+
+
+

Discord presence tracking

+

Detect when you wake up via Discord

+
+ +
+ + {!notif.discord_enabled && ( +

+ Enable Discord notifications above to use presence tracking +

+ )} + + {adaptiveMeds.presence_tracking_enabled && presence.typical_wake_time && ( +
+

+ Typical wake time: {presence.typical_wake_time} +

+

+ Status: {presence.is_online ? '🟢 Online' : '⚫ Offline'} +

+
+ )} +
+ + {/* Nagging Settings */} +
+
+
+

Enable nagging

+

Send reminders for missed doses

+
+ +
+ + {adaptiveMeds.nagging_enabled && ( + <> + {/* Nag Interval */} +
+ + updateAdaptiveMeds({ nag_interval_minutes: parseInt(e.target.value) || 15 })} + className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700" + /> +
+ + {/* Max Nag Count */} +
+ + updateAdaptiveMeds({ max_nag_count: parseInt(e.target.value) || 4 })} + className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700" + /> +
+ + )} +
+ + {/* Quiet Hours */} +
+

Quiet hours

+

Don't send notifications during these hours

+ +
+
+ + updateAdaptiveMeds({ quiet_hours_start: e.target.value || null })} + className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700" + /> +
+
+ + updateAdaptiveMeds({ quiet_hours_end: e.target.value || null })} + className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700" + /> +
+
+
+
+
+ {/* Celebration Style */}

Celebration Style

diff --git a/synculous-client/src/lib/api.ts b/synculous-client/src/lib/api.ts index a75cac3..df376e7 100644 --- a/synculous-client/src/lib/api.ts +++ b/synculous-client/src/lib/api.ts @@ -689,6 +689,58 @@ export const api = { }, }, + // Adaptive Medications + adaptiveMeds: { + getSettings: async () => { + return request<{ + adaptive_timing_enabled: boolean; + adaptive_mode: string; + presence_tracking_enabled: boolean; + nagging_enabled: boolean; + nag_interval_minutes: number; + max_nag_count: number; + quiet_hours_start: string | null; + quiet_hours_end: string | null; + }>('/api/adaptive-meds/settings', { method: 'GET' }); + }, + + updateSettings: async (data: { + adaptive_timing_enabled?: boolean; + adaptive_mode?: string; + presence_tracking_enabled?: boolean; + nagging_enabled?: boolean; + nag_interval_minutes?: number; + max_nag_count?: number; + quiet_hours_start?: string | null; + quiet_hours_end?: string | null; + }) => { + return request<{ success: boolean }>('/api/adaptive-meds/settings', { + method: 'PUT', + body: JSON.stringify(data), + }); + }, + + getPresence: async () => { + return request<{ + is_online: boolean; + last_online_at: string | null; + typical_wake_time: string | null; + }>('/api/adaptive-meds/presence', { method: 'GET' }); + }, + + getSchedule: async () => { + return request>('/api/adaptive-meds/schedule', { method: 'GET' }); + }, + }, + // Medications medications: { list: async () => {