fixes yo
This commit is contained in:
@@ -80,6 +80,43 @@ def register(app):
|
|||||||
notifications.setNotificationSettings(user_uuid, {"web_push_enabled": True})
|
notifications.setNotificationSettings(user_uuid, {"web_push_enabled": True})
|
||||||
return flask.jsonify({"subscribed": True}), 201
|
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"])
|
@app.route("/api/notifications/subscribe", methods=["DELETE"])
|
||||||
def api_pushUnsubscribe():
|
def api_pushUnsubscribe():
|
||||||
"""Remove a push subscription. Body: {endpoint}"""
|
"""Remove a push subscription. Body: {endpoint}"""
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "8080:5000"
|
- "8010:5000"
|
||||||
env_file: config/.env
|
env_file: config/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -48,7 +48,7 @@ services:
|
|||||||
context: ./synculous-client
|
context: ./synculous-client
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3001:3000"
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_API_URL=http://app:5000
|
- NEXT_PUBLIC_API_URL=http://app:5000
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ export default function SessionRunnerPage() {
|
|||||||
setCurrentStep(sessionData.current_step);
|
setCurrentStep(sessionData.current_step);
|
||||||
setCurrentStepIndex(sessionData.session.current_step_index);
|
setCurrentStepIndex(sessionData.session.current_step_index);
|
||||||
setStatus(sessionData.session.status === 'paused' ? 'paused' : 'active');
|
setStatus(sessionData.session.status === 'paused' ? 'paused' : 'active');
|
||||||
|
if (sessionData.session.status !== 'paused') {
|
||||||
|
setIsTimerRunning(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark previous steps as completed
|
// Mark previous steps as completed
|
||||||
const completed = new Set<number>();
|
const completed = new Set<number>();
|
||||||
@@ -117,7 +120,7 @@ export default function SessionRunnerPage() {
|
|||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) clearInterval(timerRef.current);
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
};
|
};
|
||||||
}, [isTimerRunning, timerSeconds]);
|
}, [isTimerRunning]);
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import api from '@/lib/api';
|
|||||||
import { VolumeIcon, VolumeOffIcon, SparklesIcon } from '@/components/ui/Icons';
|
import { VolumeIcon, VolumeOffIcon, SparklesIcon } from '@/components/ui/Icons';
|
||||||
import { playStepComplete } from '@/lib/sounds';
|
import { playStepComplete } from '@/lib/sounds';
|
||||||
import { hapticTap } from '@/lib/haptics';
|
import { hapticTap } from '@/lib/haptics';
|
||||||
|
import PushNotificationToggle from '@/components/notifications/PushNotificationToggle';
|
||||||
|
|
||||||
interface Preferences {
|
interface Preferences {
|
||||||
sound_enabled: boolean;
|
sound_enabled: boolean;
|
||||||
@@ -13,6 +14,13 @@ interface Preferences {
|
|||||||
celebration_style: string;
|
celebration_style: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NotifSettings {
|
||||||
|
discord_webhook: string;
|
||||||
|
discord_enabled: boolean;
|
||||||
|
ntfy_topic: string;
|
||||||
|
ntfy_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [prefs, setPrefs] = useState<Preferences>({
|
const [prefs, setPrefs] = useState<Preferences>({
|
||||||
sound_enabled: false,
|
sound_enabled: false,
|
||||||
@@ -20,29 +28,57 @@ export default function SettingsPage() {
|
|||||||
show_launch_screen: true,
|
show_launch_screen: true,
|
||||||
celebration_style: 'standard',
|
celebration_style: 'standard',
|
||||||
});
|
});
|
||||||
|
const [notif, setNotif] = useState<NotifSettings>({
|
||||||
|
discord_webhook: '',
|
||||||
|
discord_enabled: false,
|
||||||
|
ntfy_topic: '',
|
||||||
|
ntfy_enabled: false,
|
||||||
|
});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.preferences.get()
|
Promise.all([
|
||||||
.then((data: Preferences) => setPrefs(data))
|
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(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const flashSaved = () => {
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
const updatePref = async (key: keyof Preferences, value: boolean | string) => {
|
const updatePref = async (key: keyof Preferences, value: boolean | string) => {
|
||||||
const updated = { ...prefs, [key]: value };
|
const updated = { ...prefs, [key]: value };
|
||||||
setPrefs(updated);
|
setPrefs(updated);
|
||||||
try {
|
try {
|
||||||
await api.preferences.update({ [key]: value });
|
await api.preferences.update({ [key]: value });
|
||||||
setSaved(true);
|
flashSaved();
|
||||||
setTimeout(() => setSaved(false), 1500);
|
|
||||||
} catch {
|
} catch {
|
||||||
// revert on failure
|
|
||||||
setPrefs(prefs);
|
setPrefs(prefs);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateNotif = async (updates: Partial<NotifSettings>) => {
|
||||||
|
const prev = { ...notif };
|
||||||
|
const updated = { ...notif, ...updates };
|
||||||
|
setNotif(updated);
|
||||||
|
try {
|
||||||
|
await api.notifications.updateSettings(updates);
|
||||||
|
flashSaved();
|
||||||
|
} catch {
|
||||||
|
setNotif(prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[50vh]">
|
<div className="flex items-center justify-center min-h-[50vh]">
|
||||||
@@ -136,6 +172,75 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Notifications */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 mb-3">Notifications</h2>
|
||||||
|
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
|
||||||
|
{/* Push Notifications */}
|
||||||
|
<PushNotificationToggle />
|
||||||
|
|
||||||
|
{/* ntfy */}
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">ntfy</p>
|
||||||
|
<p className="text-sm text-gray-500">Push notifications via ntfy.sh</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => updateNotif({ ntfy_enabled: !notif.ntfy_enabled })}
|
||||||
|
className={`w-12 h-7 rounded-full transition-colors ${
|
||||||
|
notif.ntfy_enabled ? 'bg-indigo-500' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
|
notif.ntfy_enabled ? 'translate-x-5' : ''
|
||||||
|
}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{notif.ntfy_enabled && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Your ntfy topic ID"
|
||||||
|
value={notif.ntfy_topic}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discord */}
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">Discord</p>
|
||||||
|
<p className="text-sm text-gray-500">Send notifications to a Discord channel</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => updateNotif({ discord_enabled: !notif.discord_enabled })}
|
||||||
|
className={`w-12 h-7 rounded-full transition-colors ${
|
||||||
|
notif.discord_enabled ? 'bg-indigo-500' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
|
notif.discord_enabled ? 'translate-x-5' : ''
|
||||||
|
}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{notif.discord_enabled && (
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
placeholder="Discord webhook URL"
|
||||||
|
value={notif.discord_webhook}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Celebration Style */}
|
{/* Celebration Style */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Celebration Style</h2>
|
<h2 className="text-lg font-semibold text-gray-900 mb-3">Celebration Style</h2>
|
||||||
|
|||||||
@@ -81,23 +81,21 @@ export default function PushNotificationToggle() {
|
|||||||
if (!supported) return null;
|
if (!supported) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between bg-white rounded-xl p-4 shadow-sm">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900 text-sm">Push Notifications</h3>
|
<p className="font-medium text-gray-900">Push notifications</p>
|
||||||
<p className="text-xs text-gray-500">Get reminders on this device</p>
|
<p className="text-sm text-gray-500">Get reminders on this device</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
className={`w-12 h-7 rounded-full transition-colors ${
|
||||||
enabled ? 'bg-indigo-600' : 'bg-gray-300'
|
enabled ? 'bg-indigo-500' : 'bg-gray-300'
|
||||||
} ${loading ? 'opacity-50' : ''}`}
|
} ${loading ? 'opacity-50' : ''}`}
|
||||||
>
|
>
|
||||||
<span
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
enabled ? 'translate-x-5' : ''
|
||||||
enabled ? 'translate-x-6' : 'translate-x-1'
|
}`} />
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -646,6 +646,28 @@ export const api = {
|
|||||||
body: JSON.stringify({ endpoint }),
|
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
|
// Medications
|
||||||
|
|||||||
Reference in New Issue
Block a user