From ecb79af44eaf8cbb1e86c0cf1f3778d89ead61b4 Mon Sep 17 00:00:00 2001
From: chelsea
Date: Thu, 19 Feb 2026 19:04:52 -0600
Subject: [PATCH] Fix bugs, add auto-refresh, quick-complete tasks, and
every-N-day routines
- Fix bot auth: merge duplicate on_ready handlers so session restore runs (#13)
- Fix push notifications: pass Uint8Array directly as applicationServerKey (#6)
- Show specific conflict reason on schedule save instead of generic error (#17)
- Add inline checkmark button to complete tasks on routines timeline (#18)
- Add visibility-change + 60s polling auto-refresh to routines, meds, tasks (#15)
- Add every-N-day routine scheduling: schema, API, scheduler, and UI (#16)
Co-Authored-By: Claude Opus 4.6
---
api/routes/routines.py | 54 +++---
bot/bot.py | 9 +-
config/schema.sql | 10 +-
scheduler/daemon.py | 27 ++-
.../src/app/dashboard/medications/page.tsx | 44 +++--
.../src/app/dashboard/routines/[id]/page.tsx | 159 +++++++++++++-----
.../src/app/dashboard/routines/page.tsx | 58 ++++++-
.../src/app/dashboard/tasks/page.tsx | 13 ++
.../notifications/PushNotificationToggle.tsx | 2 +-
synculous-client/src/lib/api.ts | 8 +-
10 files changed, 288 insertions(+), 96 deletions(-)
diff --git a/api/routes/routines.py b/api/routes/routines.py
index e1ca42e..bbddb8a 100644
--- a/api/routes/routines.py
+++ b/api/routes/routines.py
@@ -661,17 +661,20 @@ def register(app):
continue
steps = postgres.select("routine_steps", where={"routine_id": r["id"]})
total_duration = sum(s.get("duration_minutes") or 0 for s in steps)
- result.append(
- {
- "routine_id": r["id"],
- "routine_name": r.get("name", ""),
- "routine_icon": r.get("icon", ""),
- "days": sched.get("days", []),
- "time": sched.get("time"),
- "remind": sched.get("remind", True),
- "total_duration_minutes": total_duration,
- }
- )
+ entry = {
+ "routine_id": r["id"],
+ "routine_name": r.get("name", ""),
+ "routine_icon": r.get("icon", ""),
+ "days": sched.get("days", []),
+ "time": sched.get("time"),
+ "remind": sched.get("remind", True),
+ "total_duration_minutes": total_duration,
+ "frequency": sched.get("frequency", "weekly"),
+ }
+ if sched.get("frequency") == "every_n_days":
+ entry["interval_days"] = sched.get("interval_days")
+ entry["start_date"] = str(sched.get("start_date")) if sched.get("start_date") else None
+ result.append(entry)
return flask.jsonify(result), 200
def _get_routine_duration_minutes(routine_id):
@@ -745,7 +748,10 @@ def register(app):
@app.route("/api/routines//schedule", methods=["PUT"])
def api_setRoutineSchedule(routine_id):
- """Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}"""
+ """Set when this routine should run.
+ Body: {days, time, remind, frequency?, interval_days?, start_date?}
+ frequency: 'weekly' (default, uses days) or 'every_n_days' (uses interval_days + start_date)
+ """
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
@@ -758,15 +764,18 @@ def register(app):
if not data:
return flask.jsonify({"error": "missing body"}), 400
- # Check for schedule conflicts
- new_days = data.get("days", [])
- new_time = data.get("time")
- has_conflict, conflict_msg = _check_schedule_conflicts(
- user_uuid, new_days, new_time, exclude_routine_id=routine_id,
- new_routine_id=routine_id,
- )
- if has_conflict:
- return flask.jsonify({"error": conflict_msg}), 409
+ frequency = data.get("frequency", "weekly")
+
+ # Check for schedule conflicts (only for weekly โ interval conflicts checked at reminder time)
+ if frequency == "weekly":
+ new_days = data.get("days", [])
+ new_time = data.get("time")
+ has_conflict, conflict_msg = _check_schedule_conflicts(
+ user_uuid, new_days, new_time, exclude_routine_id=routine_id,
+ new_routine_id=routine_id,
+ )
+ if has_conflict:
+ return flask.jsonify({"error": conflict_msg}), 409
existing = postgres.select_one("routine_schedules", {"routine_id": routine_id})
schedule_data = {
@@ -774,6 +783,9 @@ def register(app):
"days": json.dumps(data.get("days", [])),
"time": data.get("time"),
"remind": data.get("remind", True),
+ "frequency": frequency,
+ "interval_days": data.get("interval_days"),
+ "start_date": data.get("start_date"),
}
if existing:
result = postgres.update(
diff --git a/bot/bot.py b/bot/bot.py
index 7a858e2..0f231d5 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -663,14 +663,6 @@ def _restore_sessions_from_cache():
print(f"Restored {restored} user session(s) from cache")
-@client.event
-async def on_ready():
- print(f"Bot logged in as {client.user}")
- loadCache()
- _restore_sessions_from_cache()
- backgroundLoop.start()
-
-
@client.event
async def on_message(message):
if message.author == client.user:
@@ -866,6 +858,7 @@ async def on_ready():
print(f"Bot logged in as {client.user}", flush=True)
print(f"Connected to {len(client.guilds)} guilds", flush=True)
loadCache()
+ _restore_sessions_from_cache()
backgroundLoop.start()
presenceTrackingLoop.start()
print(f"[DEBUG] Presence tracking loop started", flush=True)
diff --git a/config/schema.sql b/config/schema.sql
index 8d632db..21831c7 100644
--- a/config/schema.sql
+++ b/config/schema.sql
@@ -74,7 +74,10 @@ CREATE TABLE IF NOT EXISTS routine_schedules (
routine_id UUID REFERENCES routines(id) ON DELETE CASCADE,
days JSON DEFAULT '[]',
time VARCHAR(5),
- remind BOOLEAN DEFAULT FALSE
+ remind BOOLEAN DEFAULT FALSE,
+ frequency VARCHAR(20) DEFAULT 'weekly',
+ interval_days INTEGER,
+ start_date DATE
);
CREATE TABLE IF NOT EXISTS routine_session_notes (
@@ -314,3 +317,8 @@ CREATE TABLE IF NOT EXISTS tasks (
);
CREATE INDEX IF NOT EXISTS idx_tasks_user_scheduled ON tasks(user_uuid, scheduled_datetime);
CREATE INDEX IF NOT EXISTS idx_tasks_pending ON tasks(status) WHERE status = 'pending';
+
+-- Add every-N-day scheduling to routine_schedules (run once on existing DBs)
+ALTER TABLE routine_schedules ADD COLUMN IF NOT EXISTS frequency VARCHAR(20) DEFAULT 'weekly';
+ALTER TABLE routine_schedules ADD COLUMN IF NOT EXISTS interval_days INTEGER;
+ALTER TABLE routine_schedules ADD COLUMN IF NOT EXISTS start_date DATE;
diff --git a/scheduler/daemon.py b/scheduler/daemon.py
index b87119b..7d5a57d 100644
--- a/scheduler/daemon.py
+++ b/scheduler/daemon.py
@@ -108,6 +108,8 @@ def check_medication_reminders():
def check_routine_reminders():
"""Check for scheduled routines due now and send notifications."""
try:
+ from datetime import date as date_type
+
schedules = postgres.select("routine_schedules", where={"remind": True})
for schedule in schedules:
@@ -117,13 +119,30 @@ def check_routine_reminders():
now = _user_now_for(routine["user_uuid"])
current_time = now.strftime("%H:%M")
- current_day = now.strftime("%a").lower()
+ today = now.date()
if current_time != schedule.get("time"):
continue
- days = schedule.get("days", [])
- if current_day not in days:
- continue
+
+ frequency = schedule.get("frequency", "weekly")
+ if frequency == "every_n_days":
+ start = schedule.get("start_date")
+ interval = schedule.get("interval_days")
+ if start and interval:
+ start_d = (
+ start
+ if isinstance(start, date_type)
+ else datetime.strptime(str(start), "%Y-%m-%d").date()
+ )
+ if (today - start_d).days < 0 or (today - start_d).days % interval != 0:
+ continue
+ else:
+ continue
+ else:
+ current_day = now.strftime("%a").lower()
+ days = schedule.get("days", [])
+ if current_day not in days:
+ continue
user_settings = notifications.getNotificationSettings(routine["user_uuid"])
if user_settings:
diff --git a/synculous-client/src/app/dashboard/medications/page.tsx b/synculous-client/src/app/dashboard/medications/page.tsx
index 8f76a8a..a390d5b 100644
--- a/synculous-client/src/app/dashboard/medications/page.tsx
+++ b/synculous-client/src/app/dashboard/medications/page.tsx
@@ -91,24 +91,36 @@ export default function MedicationsPage() {
const [isLoading, setIsLoading] = useState(true);
const [tick, setTick] = useState(0);
+ const fetchData = async () => {
+ try {
+ const [medsData, todayData, adherenceData] = await Promise.all([
+ api.medications.list(),
+ api.medications.getToday().catch(() => []),
+ api.medications.getAdherence(30).catch(() => []),
+ ]);
+ setMedications(medsData);
+ setTodayMeds(todayData);
+ setAdherence(adherenceData);
+ } catch (err) {
+ console.error('Failed to fetch medications:', err);
+ }
+ };
+
useEffect(() => {
- const fetchData = async () => {
- try {
- const [medsData, todayData, adherenceData] = await Promise.all([
- api.medications.list(),
- api.medications.getToday().catch(() => []),
- api.medications.getAdherence(30).catch(() => []),
- ]);
- setMedications(medsData);
- setTodayMeds(todayData);
- setAdherence(adherenceData);
- } catch (err) {
- console.error('Failed to fetch medications:', err);
- } finally {
- setIsLoading(false);
- }
+ fetchData().finally(() => setIsLoading(false));
+ }, []);
+
+ // Re-fetch when tab becomes visible or every 60s
+ useEffect(() => {
+ const onVisible = () => {
+ if (document.visibilityState === 'visible') fetchData();
+ };
+ document.addEventListener('visibilitychange', onVisible);
+ const poll = setInterval(fetchData, 60_000);
+ return () => {
+ document.removeEventListener('visibilitychange', onVisible);
+ clearInterval(poll);
};
- fetchData();
}, []);
// Auto-refresh grouping every 60s
diff --git a/synculous-client/src/app/dashboard/routines/[id]/page.tsx b/synculous-client/src/app/dashboard/routines/[id]/page.tsx
index a95fd6b..ea224d0 100644
--- a/synculous-client/src/app/dashboard/routines/[id]/page.tsx
+++ b/synculous-client/src/app/dashboard/routines/[id]/page.tsx
@@ -29,6 +29,9 @@ interface Schedule {
days: string[];
time: string;
remind: boolean;
+ frequency?: string;
+ interval_days?: number;
+ start_date?: string;
}
const ICONS = ['โจ', '๐
', '๐', 'โ๏ธ', '๐ช', '๐ง', '๐', '๐ณ', '๐', '๐ผ', '๐ฏ', 'โญ', '๐ฅ', '๐ค', '๐ง ', 'โ', '๐', '๐ง', '๐', '๐ต', '๐', '๐ด', '๐๏ธ', '๐ถ', '๐', '๐ก๏ธ', '๐', '๐'];
@@ -77,6 +80,9 @@ export default function RoutineDetailPage() {
const [editDays, setEditDays] = useState(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
const [editTime, setEditTime] = useState('08:00');
const [editRemind, setEditRemind] = useState(true);
+ const [editFrequency, setEditFrequency] = useState<'weekly' | 'every_n_days'>('weekly');
+ const [editIntervalDays, setEditIntervalDays] = useState(2);
+ const [editStartDate, setEditStartDate] = useState(() => new Date().toISOString().split('T')[0]);
const [showScheduleEditor, setShowScheduleEditor] = useState(false);
useEffect(() => {
@@ -99,6 +105,9 @@ export default function RoutineDetailPage() {
setEditDays(scheduleData.days || []);
setEditTime(scheduleData.time || '08:00');
setEditRemind(scheduleData.remind ?? true);
+ setEditFrequency((scheduleData.frequency as 'weekly' | 'every_n_days') || 'weekly');
+ setEditIntervalDays(scheduleData.interval_days || 2);
+ setEditStartDate(scheduleData.start_date || new Date().toISOString().split('T')[0]);
} else {
setEditDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
if (isNewRoutine) {
@@ -162,13 +171,27 @@ export default function RoutineDetailPage() {
const handleSaveSchedule = async () => {
try {
- if (editDays.length > 0) {
- await api.routines.setSchedule(routineId, {
+ const hasSchedule = editFrequency === 'every_n_days' || editDays.length > 0;
+ if (hasSchedule) {
+ const schedulePayload = {
days: editDays,
time: editTime || '08:00',
remind: editRemind,
+ frequency: editFrequency,
+ ...(editFrequency === 'every_n_days' && {
+ interval_days: editIntervalDays,
+ start_date: editStartDate,
+ }),
+ };
+ await api.routines.setSchedule(routineId, schedulePayload);
+ setSchedule({
+ days: editDays,
+ time: editTime || '08:00',
+ remind: editRemind,
+ frequency: editFrequency,
+ interval_days: editFrequency === 'every_n_days' ? editIntervalDays : undefined,
+ start_date: editFrequency === 'every_n_days' ? editStartDate : undefined,
});
- setSchedule({ days: editDays, time: editTime || '08:00', remind: editRemind });
} else if (schedule) {
await api.routines.deleteSchedule(routineId);
setSchedule(null);
@@ -176,7 +199,7 @@ export default function RoutineDetailPage() {
setShowScheduleEditor(false);
} catch (err) {
console.error('Failed to save schedule:', err);
- alert('Failed to save schedule. Please try again.');
+ alert((err as Error).message || 'Failed to save schedule. Please try again.');
}
};
@@ -462,56 +485,108 @@ export default function RoutineDetailPage() {
{showScheduleEditor ? (
<>
- {/* Quick select */}
+ {/* Frequency selector */}
setEditDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
- className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
- editDays.length === 7 ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
+ onClick={() => setEditFrequency('weekly')}
+ className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
+ editFrequency === 'weekly' ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
- Every day
+ Weekly
setEditDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
- className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
- editDays.length === 5 && !editDays.includes('sat') && !editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
+ onClick={() => setEditFrequency('every_n_days')}
+ className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
+ editFrequency === 'every_n_days' ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
- Weekdays
-
- setEditDays(['sat', 'sun'])}
- className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
- editDays.length === 2 && editDays.includes('sat') && editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
- }`}
- >
- Weekends
+ Every N Days
-
-
-
Days
-
- {DAY_OPTIONS.map((day) => (
+
+ {editFrequency === 'every_n_days' ? (
+
+
+
Repeat every
+
+ setEditIntervalDays(Math.max(2, Number(e.target.value)))}
+ className="w-20 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
+ />
+ days
+
+
+
+ Starting from
+ setEditStartDate(e.target.value)}
+ className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
+ />
+
+
+ ) : (
+ <>
+ {/* Quick select */}
+
toggleDay(day.value)}
- className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
- editDays.includes(day.value)
- ? 'bg-indigo-600 text-white border-indigo-600'
- : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
+ onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
+ className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
+ editDays.length === 7 ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
- {day.label}
+ Every day
- ))}
-
-
+
setEditDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
+ className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
+ editDays.length === 5 && !editDays.includes('sat') && !editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
+ }`}
+ >
+ Weekdays
+
+
setEditDays(['sat', 'sun'])}
+ className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
+ editDays.length === 2 && editDays.includes('sat') && editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
+ }`}
+ >
+ Weekends
+
+
+
+
+
Days
+
+ {DAY_OPTIONS.map((day) => (
+ toggleDay(day.value)}
+ className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
+ editDays.includes(day.value)
+ ? 'bg-indigo-600 text-white border-indigo-600'
+ : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
+ }`}
+ >
+ {day.label}
+
+ ))}
+
+
+ >
+ )}
Time
>
- ) : schedule && schedule.days.length > 0 ? (
+ ) : schedule && (schedule.days.length > 0 || schedule.frequency === 'every_n_days') ? (
<>
- {formatDays(schedule.days)} at {schedule.time}
+ {schedule.frequency === 'every_n_days'
+ ? `Every ${schedule.interval_days} days at ${schedule.time}`
+ : `${formatDays(schedule.days)} at ${schedule.time}`}
{schedule.remind && (
Reminders on
diff --git a/synculous-client/src/app/dashboard/routines/page.tsx b/synculous-client/src/app/dashboard/routines/page.tsx
index ebedab5..4a92e0b 100644
--- a/synculous-client/src/app/dashboard/routines/page.tsx
+++ b/synculous-client/src/app/dashboard/routines/page.tsx
@@ -21,6 +21,9 @@ interface ScheduleEntry {
time: string;
remind: boolean;
total_duration_minutes: number;
+ frequency?: string;
+ interval_days?: number;
+ start_date?: string;
}
interface TodaysMedication {
@@ -230,7 +233,15 @@ export default function RoutinesPage() {
const dayKey = getDayKey(selectedDate);
const scheduledForDay = allSchedules
- .filter((s) => s.days.includes(dayKey))
+ .filter((s) => {
+ if (s.frequency === 'every_n_days') {
+ if (!s.interval_days || !s.start_date) return false;
+ const start = new Date(s.start_date + 'T00:00:00');
+ const diffDays = Math.round((selectedDate.getTime() - start.getTime()) / 86400000);
+ return diffDays >= 0 && diffDays % s.interval_days === 0;
+ }
+ return s.days.includes(dayKey);
+ })
.sort((a, b) => timeToMinutes(a.time) - timeToMinutes(b.time));
const tasksForDay = allTasks.filter((t) => {
@@ -433,7 +444,7 @@ export default function RoutinesPage() {
setUndoAction(null);
};
- useEffect(() => {
+ const fetchAllData = () =>
Promise.all([
api.routines.list(),
api.routines.listAllSchedules(),
@@ -446,8 +457,10 @@ export default function RoutinesPage() {
setTodayMeds(meds);
setAllTasks(tasks);
})
- .catch(() => {})
- .finally(() => setIsLoading(false));
+ .catch(() => {});
+
+ useEffect(() => {
+ fetchAllData().finally(() => setIsLoading(false));
}, []);
useEffect(() => {
@@ -459,6 +472,19 @@ export default function RoutinesPage() {
return () => clearInterval(timer);
}, []);
+ // Re-fetch when tab becomes visible or every 60s
+ useEffect(() => {
+ const onVisible = () => {
+ if (document.visibilityState === 'visible') fetchAllData();
+ };
+ document.addEventListener('visibilitychange', onVisible);
+ const poll = setInterval(fetchAllData, 60_000);
+ return () => {
+ document.removeEventListener('visibilitychange', onVisible);
+ clearInterval(poll);
+ };
+ }, []);
+
useEffect(() => {
if (!isLoading && isToday && timelineRef.current) {
const scrollTarget = nowTopPx - window.innerHeight / 3;
@@ -471,6 +497,18 @@ export default function RoutinesPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, isToday]);
+ const handleCompleteTask = async (taskId: string) => {
+ try {
+ await api.tasks.update(taskId, { status: 'completed' });
+ setAllTasks((prev) =>
+ prev.map((t) => (t.id === taskId ? { ...t, status: 'completed' } : t))
+ );
+ } catch (err) {
+ console.error('Failed to complete task:', err);
+ setError(err instanceof Error ? err.message : 'Failed to complete task');
+ }
+ };
+
const handleStartRoutine = async (routineId: string) => {
try {
await api.sessions.start(routineId);
@@ -838,10 +876,20 @@ export default function RoutinesPage() {
{task.description && ` ยท ${task.description}`}
- {isPast && (
+ {isPast ? (
+ ) : (
+ {
+ e.stopPropagation();
+ handleCompleteTask(task.id);
+ }}
+ className="bg-green-600 text-white p-1 rounded-lg flex-shrink-0"
+ >
+
+
)}
diff --git a/synculous-client/src/app/dashboard/tasks/page.tsx b/synculous-client/src/app/dashboard/tasks/page.tsx
index fec2ade..b1150f0 100644
--- a/synculous-client/src/app/dashboard/tasks/page.tsx
+++ b/synculous-client/src/app/dashboard/tasks/page.tsx
@@ -53,6 +53,19 @@ export default function TasksPage() {
loadTasks(showCompleted ? 'all' : 'pending');
}, [showCompleted]);
+ // Re-fetch when tab becomes visible or every 60s
+ useEffect(() => {
+ const onVisible = () => {
+ if (document.visibilityState === 'visible') loadTasks(showCompleted ? 'all' : 'pending');
+ };
+ document.addEventListener('visibilitychange', onVisible);
+ const poll = setInterval(() => loadTasks(showCompleted ? 'all' : 'pending'), 60_000);
+ return () => {
+ document.removeEventListener('visibilitychange', onVisible);
+ clearInterval(poll);
+ };
+ }, [showCompleted]);
+
const handleMarkDone = async (task: Task) => {
try {
await api.tasks.update(task.id, { status: 'completed' });
diff --git a/synculous-client/src/components/notifications/PushNotificationToggle.tsx b/synculous-client/src/components/notifications/PushNotificationToggle.tsx
index a87d286..471ce9d 100644
--- a/synculous-client/src/components/notifications/PushNotificationToggle.tsx
+++ b/synculous-client/src/components/notifications/PushNotificationToggle.tsx
@@ -65,7 +65,7 @@ export default function PushNotificationToggle() {
const { public_key } = await api.notifications.getVapidPublicKey();
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
- applicationServerKey: urlBase64ToUint8Array(public_key).buffer as ArrayBuffer,
+ applicationServerKey: urlBase64ToUint8Array(public_key) as BufferSource,
});
const subJson = sub.toJSON();
diff --git a/synculous-client/src/lib/api.ts b/synculous-client/src/lib/api.ts
index 529953e..c46edff 100644
--- a/synculous-client/src/lib/api.ts
+++ b/synculous-client/src/lib/api.ts
@@ -323,12 +323,15 @@ export const api = {
days: string[];
time: string;
remind: boolean;
+ frequency?: string;
+ interval_days?: number;
+ start_date?: string;
}>(`/api/routines/${routineId}/schedule`, { method: 'GET' });
},
setSchedule: async (
routineId: string,
- data: { days: string[]; time: string; remind?: boolean }
+ data: { days: string[]; time: string; remind?: boolean; frequency?: string; interval_days?: number; start_date?: string }
) => {
return request<{ id: string }>(`/api/routines/${routineId}/schedule`, {
method: 'PUT',
@@ -352,6 +355,9 @@ export const api = {
time: string;
remind: boolean;
total_duration_minutes: number;
+ frequency?: string;
+ interval_days?: number;
+ start_date?: string;
}>>('/api/routines/schedules', { method: 'GET' });
},