Fix medication system and rename to Synculous.

- Add all 14 missing database tables (medications, med_logs, routines, etc.)
- Rewrite medication scheduling: support specific days, every N days, as-needed (PRN)
- Fix taken_times matching: match by created_at date, not scheduled_time string
- Fix adherence calculation: taken / expected doses, not taken / (taken + skipped)
- Add formatSchedule() helper for readable display
- Update client types and API layer
- Rename brilli-ins-client → synculous-client
- Make client PWA: add manifest, service worker, icons
- Bind dev server to 0.0.0.0 for network access
- Fix SVG icon bugs in Icons.tsx
- Add .dockerignore for client npm caching

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 03:23:38 -06:00
parent 3e1134575b
commit 97a166f5aa
47 changed files with 5231 additions and 61 deletions

View File

@@ -0,0 +1,697 @@
const API_URL = '';
function getToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('token');
}
function setToken(token: string): void {
localStorage.setItem('token', token);
}
function clearToken(): void {
localStorage.removeItem('token');
}
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = getToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
};
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Request failed' }));
throw new Error(error.error || 'Request failed');
}
return response.json();
}
export const api = {
// Auth
auth: {
login: async (username: string, password: string) => {
const result = await request<{ token: string }>('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
setToken(result.token);
return result;
},
register: async (username: string, password: string) => {
return request<{ success: boolean }>('/api/register', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
},
logout: () => {
clearToken();
},
getToken,
},
// User
user: {
get: async (userId: string) => {
return request<{ id: string; username: string; created_at: string }>(
`/api/user/${userId}`,
{ method: 'GET' }
);
},
getUUID: async (username: string) => {
return request<{ id: string }>(`/api/getUserUUID/${username}`, {
method: 'GET',
});
},
update: async (userId: string, data: Record<string, unknown>) => {
return request<{ success: boolean }>(`/api/user/${userId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
// Routines
routines: {
list: async () => {
return request<Array<{
id: string;
user_uuid: string;
name: string;
description?: string;
icon?: string;
created_at: string;
}>>('/api/routines', { method: 'GET' });
},
get: async (routineId: string) => {
return request<{
routine: {
id: string;
user_uuid: string;
name: string;
description?: string;
icon?: string;
};
steps: Array<{
id: string;
routine_id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
media_url?: string;
position: number;
}>;
}>(`/api/routines/${routineId}`, { method: 'GET' });
},
create: async (data: { name: string; description?: string; icon?: string }) => {
return request<{ id: string }>('/api/routines', {
method: 'POST',
body: JSON.stringify(data),
});
},
update: async (routineId: string, data: Record<string, unknown>) => {
return request<{ id: string }>(`/api/routines/${routineId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
delete: async (routineId: string) => {
return request<{ deleted: boolean }>(`/api/routines/${routineId}`, {
method: 'DELETE',
});
},
// Steps
getSteps: async (routineId: string) => {
return request<Array<{
id: string;
routine_id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
media_url?: string;
position: number;
}>>(`/api/routines/${routineId}/steps`, { method: 'GET' });
},
addStep: async (
routineId: string,
data: { name: string; duration_minutes?: number; position?: number }
) => {
return request<{ id: string }>(`/api/routines/${routineId}/steps`, {
method: 'POST',
body: JSON.stringify(data),
});
},
updateStep: async (
routineId: string,
stepId: string,
data: Record<string, unknown>
) => {
return request<{ id: string }>(`/api/routines/${routineId}/steps/${stepId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
deleteStep: async (routineId: string, stepId: string) => {
return request<{ deleted: boolean }>(
`/api/routines/${routineId}/steps/${stepId}`,
{ method: 'DELETE' }
);
},
reorderSteps: async (routineId: string, stepIds: string[]) => {
return request<Array<{ id: string }>>(
`/api/routines/${routineId}/steps/reorder`,
{
method: 'PUT',
body: JSON.stringify({ step_ids: stepIds }),
}
);
},
// Step extended
updateStepInstructions: async (
routineId: string,
stepId: string,
instructions: string
) => {
return request<{ id: string }>(
`/api/routines/${routineId}/steps/${stepId}/instructions`,
{
method: 'PUT',
body: JSON.stringify({ instructions }),
}
);
},
updateStepType: async (
routineId: string,
stepId: string,
stepType: string
) => {
return request<{ id: string }>(
`/api/routines/${routineId}/steps/${stepId}/type`,
{
method: 'PUT',
body: JSON.stringify({ step_type: stepType }),
}
);
},
updateStepMedia: async (
routineId: string,
stepId: string,
mediaUrl: string
) => {
return request<{ id: string }>(
`/api/routines/${routineId}/steps/${stepId}/media`,
{
method: 'PUT',
body: JSON.stringify({ media_url: mediaUrl }),
}
);
},
// Scheduling
getSchedule: async (routineId: string) => {
return request<{
id: string;
routine_id: string;
days: string[];
time: string;
remind: boolean;
}>(`/api/routines/${routineId}/schedule`, { method: 'GET' });
},
setSchedule: async (
routineId: string,
data: { days: string[]; time: string; remind?: boolean }
) => {
return request<{ id: string }>(`/api/routines/${routineId}/schedule`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
deleteSchedule: async (routineId: string) => {
return request<{ deleted: boolean }>(
`/api/routines/${routineId}/schedule`,
{ method: 'DELETE' }
);
},
// History
getHistory: async (routineId: string, days = 7) => {
return request<Array<{
id: string;
routine_id: string;
status: string;
created_at: string;
completed_at?: string;
}>>(`/api/routines/${routineId}/history?days=${days}`, {
method: 'GET',
});
},
// Stats
getStats: async (routineId: string, days = 30) => {
return request<{
routine_id: string;
routine_name: string;
period_days: number;
total_sessions: number;
completed: number;
aborted: number;
completion_rate_percent: number;
avg_duration_minutes: number;
total_time_minutes: number;
}>(`/api/routines/${routineId}/stats?days=${days}`, { method: 'GET' });
},
getStreak: async (routineId: string) => {
return request<{
routine_id: string;
routine_name: string;
current_streak: number;
longest_streak: number;
last_completed_date?: string;
}>(`/api/routines/${routineId}/streak`, { method: 'GET' });
},
// Tags
getTags: async (routineId: string) => {
return request<Array<{ id: string; name: string; color: string }>>(
`/api/routines/${routineId}/tags`,
{ method: 'GET' }
);
},
addTags: async (routineId: string, tagIds: string[]) => {
return request<Array<{ id: string; name: string; color: string }>>(
`/api/routines/${routineId}/tags`,
{
method: 'POST',
body: JSON.stringify({ tag_ids: tagIds }),
}
);
},
removeTag: async (routineId: string, tagId: string) => {
return request<{ removed: boolean }>(
`/api/routines/${routineId}/tags/${tagId}`,
{ method: 'DELETE' }
);
},
},
// Sessions
sessions: {
start: async (routineId: string) => {
return request<{
session: { id: string; status: string };
current_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
};
}>(`/api/routines/${routineId}/start`, { method: 'POST' });
},
getActive: async () => {
return request<{
session: {
id: string;
routine_id: string;
status: string;
current_step_index: number;
};
routine: { id: string; name: string; icon?: string };
current_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
} | null;
}>('/api/sessions/active', { method: 'GET' });
},
completeStep: async (sessionId: string, stepId: string) => {
return request<{
session: { status: string; current_step_index?: number };
next_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
} | null;
}>(`/api/sessions/${sessionId}/complete-step`, {
method: 'POST',
body: JSON.stringify({ step_id: stepId }),
});
},
skipStep: async (sessionId: string, stepId: string) => {
return request<{
session: { status: string; current_step_index?: number };
next_step: {
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
} | null;
}>(`/api/sessions/${sessionId}/skip-step`, {
method: 'POST',
body: JSON.stringify({ step_id: stepId }),
});
},
pause: async (sessionId: string) => {
return request<{ status: string }>(`/api/sessions/${sessionId}/pause`, {
method: 'POST',
});
},
resume: async (sessionId: string) => {
return request<{ status: string }>(`/api/sessions/${sessionId}/resume`, {
method: 'POST',
});
},
cancel: async (sessionId: string) => {
return request<{ status: string }>(`/api/sessions/${sessionId}/cancel`, {
method: 'POST',
});
},
abort: async (sessionId: string, reason?: string) => {
return request<{ status: string; reason: string }>(
`/api/sessions/${sessionId}/abort`,
{
method: 'POST',
body: JSON.stringify({ reason }),
}
);
},
addNote: async (
sessionId: string,
stepIndex: number | undefined,
note: string
) => {
return request<{ id: string }>(`/api/sessions/${sessionId}/note`, {
method: 'POST',
body: JSON.stringify({ step_index: stepIndex, note }),
});
},
setDuration: async (sessionId: string, durationMinutes: number) => {
return request<{ id: string }>(`/api/sessions/${sessionId}/duration`, {
method: 'PUT',
body: JSON.stringify({ actual_duration_minutes: durationMinutes }),
});
},
getDetails: async (sessionId: string) => {
return request<{
session: {
id: string;
routine_id: string;
status: string;
current_step_index: number;
};
routine: { id: string; name: string };
steps: Array<{ id: string; name: string; position: number }>;
notes: Array<{ id: string; step_index?: number; note: string }>;
}>(`/api/sessions/${sessionId}`, { method: 'GET' });
},
},
// Templates
templates: {
list: async () => {
return request<Array<{
id: string;
name: string;
description?: string;
icon?: string;
step_count: number;
}>>('/api/templates', { method: 'GET' });
},
get: async (templateId: string) => {
return request<{
template: {
id: string;
name: string;
description?: string;
icon?: string;
};
steps: Array<{
id: string;
name: string;
instructions?: string;
step_type: string;
duration_minutes?: number;
position: number;
}>;
}>(`/api/templates/${templateId}`, { method: 'GET' });
},
clone: async (templateId: string) => {
return request<{ id: string }>(`/api/templates/${templateId}/clone`, {
method: 'POST',
});
},
},
// Tags
tags: {
list: async () => {
return request<Array<{ id: string; name: string; color: string }>>(
'/api/tags',
{ method: 'GET' }
);
},
create: async (data: { name: string; color?: string }) => {
return request<{ id: string }>('/api/tags', {
method: 'POST',
body: JSON.stringify(data),
});
},
delete: async (tagId: string) => {
return request<{ deleted: boolean }>(`/api/tags/${tagId}`, {
method: 'DELETE',
});
},
},
// Stats
stats: {
getWeeklySummary: async () => {
return request<{
total_completed: number;
total_time_minutes: number;
routines_started: number;
routines: Array<{
routine_id: string;
name: string;
completed_this_week: number;
}>;
}>('/api/routines/weekly-summary', { method: 'GET' });
},
getStreaks: async () => {
return request<Array<{
routine_id: string;
routine_name: string;
current_streak: number;
longest_streak: number;
last_completed_date?: string;
}>>('/api/routines/streaks', { method: 'GET' });
},
},
// Medications
medications: {
list: async () => {
return request<Array<{
id: string;
name: string;
dosage: string;
unit: string;
frequency: string;
times: string[];
notes?: string;
active: boolean;
quantity_remaining?: number;
refill_date?: string;
}>>('/api/medications', { method: 'GET' });
},
get: async (medId: string) => {
return request<{
id: string;
name: string;
dosage: string;
unit: string;
frequency: string;
times: string[];
notes?: string;
active: boolean;
quantity_remaining?: number;
refill_date?: string;
}>(`/api/medications/${medId}`, { method: 'GET' });
},
create: async (data: {
name: string;
dosage: string;
unit: string;
frequency: string;
times?: string[];
days_of_week?: string[];
interval_days?: number;
start_date?: string;
notes?: string;
}) => {
return request<{ id: string }>('/api/medications', {
method: 'POST',
body: JSON.stringify(data),
});
},
update: async (medId: string, data: Record<string, unknown>) => {
return request<{ id: string }>(`/api/medications/${medId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
delete: async (medId: string) => {
return request<{ deleted: boolean }>(`/api/medications/${medId}`, {
method: 'DELETE',
});
},
take: async (medId: string, scheduledTime?: string, notes?: string) => {
return request<{ id: string }>(`/api/medications/${medId}/take`, {
method: 'POST',
body: JSON.stringify({ scheduled_time: scheduledTime, notes }),
});
},
skip: async (medId: string, scheduledTime?: string, reason?: string) => {
return request<{ id: string }>(`/api/medications/${medId}/skip`, {
method: 'POST',
body: JSON.stringify({ scheduled_time: scheduledTime, reason }),
});
},
snooze: async (medId: string, minutes = 15) => {
return request<{ snoozed_until_minutes: number }>(
`/api/medications/${medId}/snooze`,
{
method: 'POST',
body: JSON.stringify({ minutes }),
}
);
},
getLog: async (medId: string, days = 30) => {
return request<Array<{
id: string;
medication_id: string;
action: string;
scheduled_time?: string;
notes?: string;
created_at: string;
}>>(`/api/medications/${medId}/log?days=${days}`, { method: 'GET' });
},
getToday: async () => {
return request<Array<{
medication: {
id: string;
name: string;
dosage: string;
unit: string;
};
scheduled_times: string[];
taken_times: string[];
}>>('/api/medications/today', { method: 'GET' });
},
getAdherence: async (days = 30) => {
return request<Array<{
medication_id: string;
name: string;
taken: number;
skipped: number;
adherence_percent: number;
}>>(`/api/medications/adherence?days=${days}`, { method: 'GET' });
},
getRefillsDue: async (daysAhead = 7) => {
return request<Array<{
id: string;
name: string;
dosage: string;
quantity_remaining?: number;
refill_date?: string;
}>>(`/api/medications/refills-due?days_ahead=${daysAhead}`, {
method: 'GET',
});
},
setRefill: async (
medId: string,
data: {
quantity_remaining?: number;
refill_date?: string;
pharmacy_notes?: string;
}
) => {
return request<{ id: string }>(`/api/medications/${medId}/refill`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
},
};
export default api;