From c29ec8e21005862b70794aba0180cbc4cf794b6c Mon Sep 17 00:00:00 2001 From: chelsea Date: Sun, 15 Feb 2026 01:51:34 -0600 Subject: [PATCH] fixes yo --- api/routes/notifications.py | 37 ++++++ docker-compose.yml | 4 +- .../app/dashboard/routines/[id]/run/page.tsx | 5 +- .../src/app/dashboard/settings/page.tsx | 115 +++++++++++++++++- .../notifications/PushNotificationToggle.tsx | 18 ++- synculous-client/src/lib/api.ts | 22 ++++ 6 files changed, 183 insertions(+), 18 deletions(-) diff --git a/api/routes/notifications.py b/api/routes/notifications.py index ca76494..ac88df3 100644 --- a/api/routes/notifications.py +++ b/api/routes/notifications.py @@ -80,6 +80,43 @@ def register(app): notifications.setNotificationSettings(user_uuid, {"web_push_enabled": True}) return flask.jsonify({"subscribed": True}), 201 + @app.route("/api/notifications/settings", methods=["GET"]) + def api_getNotificationSettings(): + """Return notification channel settings for the current user.""" + user_uuid = _auth(flask.request) + if not user_uuid: + return flask.jsonify({"error": "unauthorized"}), 401 + settings = notifications.getNotificationSettings(user_uuid) + if not settings: + return flask.jsonify({ + "discord_webhook": "", + "discord_enabled": False, + "ntfy_topic": "", + "ntfy_enabled": False, + "web_push_enabled": False, + }), 200 + return flask.jsonify({ + "discord_webhook": settings.get("discord_webhook") or "", + "discord_enabled": bool(settings.get("discord_enabled")), + "ntfy_topic": settings.get("ntfy_topic") or "", + "ntfy_enabled": bool(settings.get("ntfy_enabled")), + "web_push_enabled": bool(settings.get("web_push_enabled")), + }), 200 + + @app.route("/api/notifications/settings", methods=["PUT"]) + def api_updateNotificationSettings(): + """Update notification channel settings. Accepts partial updates.""" + user_uuid = _auth(flask.request) + if not user_uuid: + return flask.jsonify({"error": "unauthorized"}), 401 + data = flask.request.get_json() + if not data: + return flask.jsonify({"error": "missing body"}), 400 + result = notifications.setNotificationSettings(user_uuid, data) + if not result: + return flask.jsonify({"error": "no valid fields provided"}), 400 + return flask.jsonify({"updated": True}), 200 + @app.route("/api/notifications/subscribe", methods=["DELETE"]) def api_pushUnsubscribe(): """Remove a push subscription. Body: {endpoint}""" diff --git a/docker-compose.yml b/docker-compose.yml index 8019b55..39b575b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: app: build: . ports: - - "8080:5000" + - "8010:5000" env_file: config/.env depends_on: db: @@ -48,7 +48,7 @@ services: context: ./synculous-client dockerfile: Dockerfile ports: - - "3000:3000" + - "3001:3000" environment: - NEXT_PUBLIC_API_URL=http://app:5000 depends_on: diff --git a/synculous-client/src/app/dashboard/routines/[id]/run/page.tsx b/synculous-client/src/app/dashboard/routines/[id]/run/page.tsx index f909581..663b935 100644 --- a/synculous-client/src/app/dashboard/routines/[id]/run/page.tsx +++ b/synculous-client/src/app/dashboard/routines/[id]/run/page.tsx @@ -83,6 +83,9 @@ export default function SessionRunnerPage() { setCurrentStep(sessionData.current_step); setCurrentStepIndex(sessionData.session.current_step_index); setStatus(sessionData.session.status === 'paused' ? 'paused' : 'active'); + if (sessionData.session.status !== 'paused') { + setIsTimerRunning(true); + } // Mark previous steps as completed const completed = new Set(); @@ -117,7 +120,7 @@ export default function SessionRunnerPage() { return () => { if (timerRef.current) clearInterval(timerRef.current); }; - }, [isTimerRunning, timerSeconds]); + }, [isTimerRunning]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); diff --git a/synculous-client/src/app/dashboard/settings/page.tsx b/synculous-client/src/app/dashboard/settings/page.tsx index 81d4f93..d461408 100644 --- a/synculous-client/src/app/dashboard/settings/page.tsx +++ b/synculous-client/src/app/dashboard/settings/page.tsx @@ -5,6 +5,7 @@ import api from '@/lib/api'; import { VolumeIcon, VolumeOffIcon, SparklesIcon } from '@/components/ui/Icons'; import { playStepComplete } from '@/lib/sounds'; import { hapticTap } from '@/lib/haptics'; +import PushNotificationToggle from '@/components/notifications/PushNotificationToggle'; interface Preferences { sound_enabled: boolean; @@ -13,6 +14,13 @@ interface Preferences { celebration_style: string; } +interface NotifSettings { + discord_webhook: string; + discord_enabled: boolean; + ntfy_topic: string; + ntfy_enabled: boolean; +} + export default function SettingsPage() { const [prefs, setPrefs] = useState({ sound_enabled: false, @@ -20,29 +28,57 @@ export default function SettingsPage() { show_launch_screen: true, celebration_style: 'standard', }); + const [notif, setNotif] = useState({ + discord_webhook: '', + discord_enabled: false, + ntfy_topic: '', + ntfy_enabled: false, + }); const [isLoading, setIsLoading] = useState(true); const [saved, setSaved] = useState(false); useEffect(() => { - api.preferences.get() - .then((data: Preferences) => setPrefs(data)) + Promise.all([ + api.preferences.get().then((data: Preferences) => setPrefs(data)), + api.notifications.getSettings().then((data) => setNotif({ + discord_webhook: data.discord_webhook, + discord_enabled: data.discord_enabled, + ntfy_topic: data.ntfy_topic, + ntfy_enabled: data.ntfy_enabled, + })), + ]) .catch(() => {}) .finally(() => setIsLoading(false)); }, []); + const flashSaved = () => { + setSaved(true); + setTimeout(() => setSaved(false), 1500); + }; + const updatePref = async (key: keyof Preferences, value: boolean | string) => { const updated = { ...prefs, [key]: value }; setPrefs(updated); try { await api.preferences.update({ [key]: value }); - setSaved(true); - setTimeout(() => setSaved(false), 1500); + flashSaved(); } catch { - // revert on failure setPrefs(prefs); } }; + const updateNotif = async (updates: Partial) => { + const prev = { ...notif }; + const updated = { ...notif, ...updates }; + setNotif(updated); + try { + await api.notifications.updateSettings(updates); + flashSaved(); + } catch { + setNotif(prev); + } + }; + if (isLoading) { return (
@@ -136,6 +172,75 @@ export default function SettingsPage() {
+ {/* Notifications */} +
+

Notifications

+
+ {/* Push Notifications */} + + + {/* ntfy */} +
+
+
+

ntfy

+

Push notifications via ntfy.sh

+
+ +
+ {notif.ntfy_enabled && ( + setNotif({ ...notif, ntfy_topic: e.target.value })} + onBlur={() => updateNotif({ ntfy_topic: notif.ntfy_topic })} + className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400" + /> + )} +
+ + {/* Discord */} +
+
+
+

Discord

+

Send notifications to a Discord channel

+
+ +
+ {notif.discord_enabled && ( + setNotif({ ...notif, discord_webhook: e.target.value })} + onBlur={() => updateNotif({ discord_webhook: notif.discord_webhook })} + className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400" + /> + )} +
+
+
+ {/* Celebration Style */}

Celebration Style

diff --git a/synculous-client/src/components/notifications/PushNotificationToggle.tsx b/synculous-client/src/components/notifications/PushNotificationToggle.tsx index 798d480..8aae9a4 100644 --- a/synculous-client/src/components/notifications/PushNotificationToggle.tsx +++ b/synculous-client/src/components/notifications/PushNotificationToggle.tsx @@ -81,23 +81,21 @@ export default function PushNotificationToggle() { if (!supported) return null; return ( -
+
-

Push Notifications

-

Get reminders on this device

+

Push notifications

+

Get reminders on this device

); diff --git a/synculous-client/src/lib/api.ts b/synculous-client/src/lib/api.ts index 9502944..2fec9b5 100644 --- a/synculous-client/src/lib/api.ts +++ b/synculous-client/src/lib/api.ts @@ -646,6 +646,28 @@ export const api = { body: JSON.stringify({ endpoint }), }); }, + + getSettings: async () => { + return request<{ + discord_webhook: string; + discord_enabled: boolean; + ntfy_topic: string; + ntfy_enabled: boolean; + web_push_enabled: boolean; + }>('/api/notifications/settings', { method: 'GET' }); + }, + + updateSettings: async (data: { + discord_webhook?: string; + discord_enabled?: boolean; + ntfy_topic?: string; + ntfy_enabled?: boolean; + }) => { + return request<{ updated: boolean }>('/api/notifications/settings', { + method: 'PUT', + body: JSON.stringify(data), + }); + }, }, // Medications