bug fixes
This commit is contained in:
40
api/main.py
40
api/main.py
@@ -155,8 +155,48 @@ def health_check():
|
||||
return flask.jsonify({"status": "ok"}), 200
|
||||
|
||||
|
||||
def _seed_templates_if_empty():
|
||||
"""Auto-seed routine templates if the table is empty."""
|
||||
try:
|
||||
count = postgres.count("routine_templates")
|
||||
if count == 0:
|
||||
import logging
|
||||
logging.getLogger(__name__).info("No templates found, seeding from seed_templates.sql...")
|
||||
seed_path = os.path.join(os.path.dirname(__file__), "..", "config", "seed_templates.sql")
|
||||
if os.path.exists(seed_path):
|
||||
with open(seed_path, "r") as f:
|
||||
sql = f.read()
|
||||
with postgres.get_cursor() as cur:
|
||||
cur.execute(sql)
|
||||
logging.getLogger(__name__).info("Templates seeded successfully.")
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Failed to seed templates: {e}")
|
||||
|
||||
|
||||
def _seed_rewards_if_empty():
|
||||
"""Auto-seed reward pool if the table is empty."""
|
||||
try:
|
||||
count = postgres.count("reward_pool")
|
||||
if count == 0:
|
||||
import logging
|
||||
logging.getLogger(__name__).info("No rewards found, seeding from seed_rewards.sql...")
|
||||
seed_path = os.path.join(os.path.dirname(__file__), "..", "config", "seed_rewards.sql")
|
||||
if os.path.exists(seed_path):
|
||||
with open(seed_path, "r") as f:
|
||||
sql = f.read()
|
||||
with postgres.get_cursor() as cur:
|
||||
cur.execute(sql)
|
||||
logging.getLogger(__name__).info("Rewards seeded successfully.")
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Failed to seed rewards: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for module in ROUTE_MODULES:
|
||||
if hasattr(module, "register"):
|
||||
module.register(app)
|
||||
_seed_templates_if_empty()
|
||||
_seed_rewards_if_empty()
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
|
||||
@@ -342,18 +342,26 @@ def register(app):
|
||||
|
||||
all_logs = postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med["id"], "action": "taken"},
|
||||
where={"medication_id": med["id"]},
|
||||
)
|
||||
today_taken = [
|
||||
log.get("scheduled_time", "")
|
||||
for log in all_logs
|
||||
if str(log.get("created_at", ""))[:10] == today_str
|
||||
if log.get("action") == "taken"
|
||||
and str(log.get("created_at", ""))[:10] == today_str
|
||||
]
|
||||
today_skipped = [
|
||||
log.get("scheduled_time", "")
|
||||
for log in all_logs
|
||||
if log.get("action") == "skipped"
|
||||
and str(log.get("created_at", ""))[:10] == today_str
|
||||
]
|
||||
|
||||
result.append({
|
||||
"medication": med,
|
||||
"scheduled_times": [] if is_prn else med.get("times", []),
|
||||
"taken_times": today_taken,
|
||||
"skipped_times": today_skipped,
|
||||
"is_prn": is_prn,
|
||||
})
|
||||
seen_med_ids.add(med["id"])
|
||||
@@ -378,18 +386,26 @@ def register(app):
|
||||
|
||||
all_logs = postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med["id"], "action": "taken"},
|
||||
where={"medication_id": med["id"]},
|
||||
)
|
||||
tomorrow_taken = [
|
||||
log.get("scheduled_time", "")
|
||||
for log in all_logs
|
||||
if str(log.get("created_at", ""))[:10] == tomorrow_str
|
||||
if log.get("action") == "taken"
|
||||
and str(log.get("created_at", ""))[:10] == tomorrow_str
|
||||
]
|
||||
tomorrow_skipped = [
|
||||
log.get("scheduled_time", "")
|
||||
for log in all_logs
|
||||
if log.get("action") == "skipped"
|
||||
and str(log.get("created_at", ""))[:10] == tomorrow_str
|
||||
]
|
||||
|
||||
result.append({
|
||||
"medication": med,
|
||||
"scheduled_times": early_times,
|
||||
"taken_times": tomorrow_taken,
|
||||
"skipped_times": tomorrow_skipped,
|
||||
"is_prn": False,
|
||||
"is_next_day": True,
|
||||
})
|
||||
@@ -415,18 +431,26 @@ def register(app):
|
||||
|
||||
all_logs = postgres.select(
|
||||
"med_logs",
|
||||
where={"medication_id": med["id"], "action": "taken"},
|
||||
where={"medication_id": med["id"]},
|
||||
)
|
||||
yesterday_taken = [
|
||||
log.get("scheduled_time", "")
|
||||
for log in all_logs
|
||||
if str(log.get("created_at", ""))[:10] == yesterday_str
|
||||
if log.get("action") == "taken"
|
||||
and str(log.get("created_at", ""))[:10] == yesterday_str
|
||||
]
|
||||
yesterday_skipped = [
|
||||
log.get("scheduled_time", "")
|
||||
for log in all_logs
|
||||
if log.get("action") == "skipped"
|
||||
and str(log.get("created_at", ""))[:10] == yesterday_str
|
||||
]
|
||||
|
||||
result.append({
|
||||
"medication": med,
|
||||
"scheduled_times": late_times,
|
||||
"taken_times": yesterday_taken,
|
||||
"skipped_times": yesterday_skipped,
|
||||
"is_prn": False,
|
||||
"is_previous_day": True,
|
||||
})
|
||||
|
||||
@@ -98,40 +98,56 @@ def _complete_session_with_celebration(session_id, user_uuid, session):
|
||||
else:
|
||||
duration_minutes = 0
|
||||
|
||||
# Update session as completed with duration
|
||||
# Update session as completed with duration — this MUST succeed
|
||||
postgres.update("routine_sessions", {
|
||||
"status": "completed",
|
||||
"completed_at": now.isoformat(),
|
||||
"actual_duration_minutes": int(duration_minutes),
|
||||
}, {"id": session_id})
|
||||
|
||||
# Update streak (returns streak with optional 'milestone' key)
|
||||
streak_result = routines_core._update_streak(user_uuid, session["routine_id"])
|
||||
# Gather celebration stats — failures here should not break completion
|
||||
streak_current = 1
|
||||
streak_longest = 1
|
||||
streak_milestone = None
|
||||
steps_completed = 0
|
||||
steps_skipped = 0
|
||||
total_completions = 1
|
||||
|
||||
# Get streak data
|
||||
streak = postgres.select_one("routine_streaks", {
|
||||
"user_uuid": user_uuid,
|
||||
"routine_id": session["routine_id"],
|
||||
})
|
||||
streak_milestone = streak_result.get("milestone") if streak_result else None
|
||||
try:
|
||||
streak_result = routines_core._update_streak(user_uuid, session["routine_id"])
|
||||
streak = postgres.select_one("routine_streaks", {
|
||||
"user_uuid": user_uuid,
|
||||
"routine_id": session["routine_id"],
|
||||
})
|
||||
if streak:
|
||||
streak_current = streak["current_streak"]
|
||||
streak_longest = streak["longest_streak"]
|
||||
streak_milestone = streak_result.get("milestone") if streak_result else None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Count step results for this session
|
||||
step_results = postgres.select("routine_step_results", {"session_id": session_id})
|
||||
steps_completed = sum(1 for r in step_results if r.get("result") == "completed")
|
||||
steps_skipped = sum(1 for r in step_results if r.get("result") == "skipped")
|
||||
try:
|
||||
step_results = postgres.select("routine_step_results", {"session_id": session_id})
|
||||
steps_completed = sum(1 for r in step_results if r.get("result") == "completed")
|
||||
steps_skipped = sum(1 for r in step_results if r.get("result") == "skipped")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Total completions for this routine
|
||||
all_completed = postgres.select("routine_sessions", {
|
||||
"routine_id": session["routine_id"],
|
||||
"user_uuid": user_uuid,
|
||||
"status": "completed",
|
||||
})
|
||||
try:
|
||||
all_completed = postgres.select("routine_sessions", {
|
||||
"routine_id": session["routine_id"],
|
||||
"user_uuid": user_uuid,
|
||||
"status": "completed",
|
||||
})
|
||||
total_completions = len(all_completed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = {
|
||||
"streak_current": streak["current_streak"] if streak else 1,
|
||||
"streak_longest": streak["longest_streak"] if streak else 1,
|
||||
"streak_current": streak_current,
|
||||
"streak_longest": streak_longest,
|
||||
"session_duration_minutes": duration_minutes,
|
||||
"total_completions": len(all_completed),
|
||||
"total_completions": total_completions,
|
||||
"steps_completed": steps_completed,
|
||||
"steps_skipped": steps_skipped,
|
||||
}
|
||||
@@ -339,6 +355,8 @@ def register(app):
|
||||
if not routine:
|
||||
return flask.jsonify({"error": "not found"}), 404
|
||||
active = postgres.select_one("routine_sessions", {"user_uuid": user_uuid, "status": "active"})
|
||||
if not active:
|
||||
active = postgres.select_one("routine_sessions", {"user_uuid": user_uuid, "status": "paused"})
|
||||
if active:
|
||||
return flask.jsonify({"error": "already have active session", "session_id": active["id"]}), 409
|
||||
steps = postgres.select(
|
||||
|
||||
@@ -58,7 +58,7 @@ def pause_session(session_id, user_uuid):
|
||||
return {"error": "not_active"}
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "paused", "paused_at": datetime.now().isoformat()},
|
||||
{"status": "paused", "paused_at": tz.user_now().isoformat()},
|
||||
{"id": session_id}
|
||||
)
|
||||
return result
|
||||
@@ -95,7 +95,7 @@ def abort_session(session_id, user_uuid, reason=None):
|
||||
{
|
||||
"status": "aborted",
|
||||
"abort_reason": reason or "Aborted by user",
|
||||
"completed_at": datetime.now().isoformat()
|
||||
"completed_at": tz.user_now().isoformat()
|
||||
},
|
||||
{"id": session_id}
|
||||
)
|
||||
@@ -111,7 +111,7 @@ def complete_session(session_id, user_uuid):
|
||||
if not session:
|
||||
return None
|
||||
|
||||
completed_at = datetime.now()
|
||||
completed_at = tz.user_now()
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "completed", "completed_at": completed_at.isoformat()},
|
||||
|
||||
@@ -147,7 +147,6 @@ export default function NewMedicationPage() {
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
|
||||
>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="twice_daily">Twice Daily</option>
|
||||
<option value="specific_days">Specific Days of Week</option>
|
||||
<option value="every_n_days">Every N Days</option>
|
||||
<option value="as_needed">As Needed (PRN)</option>
|
||||
@@ -205,7 +204,7 @@ export default function NewMedicationPage() {
|
||||
{/* Times picker — hidden for as_needed */}
|
||||
{frequency !== 'as_needed' && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Times</label>
|
||||
<button
|
||||
type="button"
|
||||
@@ -215,6 +214,9 @@ export default function NewMedicationPage() {
|
||||
+ Add Time
|
||||
</button>
|
||||
</div>
|
||||
{frequency === 'daily' && (
|
||||
<p className="text-xs text-gray-400 mb-2">Add multiple times for 2x, 3x, or more doses per day</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{times.map((time, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
|
||||
@@ -32,6 +32,7 @@ interface TodaysMedication {
|
||||
};
|
||||
scheduled_times: string[];
|
||||
taken_times: string[];
|
||||
skipped_times?: string[];
|
||||
is_prn?: boolean;
|
||||
is_next_day?: boolean;
|
||||
is_previous_day?: boolean;
|
||||
@@ -44,10 +45,11 @@ interface AdherenceEntry {
|
||||
is_prn?: boolean;
|
||||
}
|
||||
|
||||
type TimeStatus = 'overdue' | 'due_now' | 'upcoming' | 'taken';
|
||||
type TimeStatus = 'overdue' | 'due_now' | 'upcoming' | 'taken' | 'skipped';
|
||||
|
||||
function getTimeStatus(scheduledTime: string, takenTimes: string[], now: Date): TimeStatus {
|
||||
function getTimeStatus(scheduledTime: string, takenTimes: string[], skippedTimes: string[], now: Date): TimeStatus {
|
||||
if (takenTimes.includes(scheduledTime)) return 'taken';
|
||||
if (skippedTimes.includes(scheduledTime)) return 'skipped';
|
||||
|
||||
const [h, m] = scheduledTime.split(':').map(Number);
|
||||
const scheduled = new Date(now);
|
||||
@@ -69,7 +71,9 @@ const formatSchedule = (med: Medication): string => {
|
||||
return `Every ${med.interval_days} days`;
|
||||
}
|
||||
if (med.frequency === 'as_needed') return 'As needed';
|
||||
if (med.frequency === 'twice_daily') return 'Twice daily';
|
||||
const timesCount = med.times?.length || 0;
|
||||
if (med.frequency === 'twice_daily' || timesCount === 2) return 'Twice daily';
|
||||
if (timesCount >= 3) return `${timesCount}x daily`;
|
||||
return 'Daily';
|
||||
};
|
||||
|
||||
@@ -134,8 +138,8 @@ export default function MedicationsPage() {
|
||||
}
|
||||
|
||||
for (const time of item.scheduled_times) {
|
||||
const status = getTimeStatus(time, item.taken_times, now);
|
||||
if (status === 'upcoming') {
|
||||
const status = getTimeStatus(time, item.taken_times, item.skipped_times || [], now);
|
||||
if (status === 'upcoming' || status === 'skipped') {
|
||||
upcoming.push({ item, time, status });
|
||||
} else {
|
||||
due.push({ item, time, status });
|
||||
@@ -144,7 +148,7 @@ export default function MedicationsPage() {
|
||||
}
|
||||
|
||||
// Sort due: overdue first, then due_now, then by time
|
||||
const statusOrder: Record<TimeStatus, number> = { overdue: 0, due_now: 1, taken: 2, upcoming: 3 };
|
||||
const statusOrder: Record<TimeStatus, number> = { overdue: 0, due_now: 1, taken: 2, skipped: 3, upcoming: 4 };
|
||||
due.sort((a, b) => statusOrder[a.status] - statusOrder[b.status] || a.time.localeCompare(b.time));
|
||||
upcoming.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
@@ -197,6 +201,7 @@ export default function MedicationsPage() {
|
||||
if (status === 'overdue') return 'border-l-4 border-l-red-500';
|
||||
if (status === 'due_now') return 'border-l-4 border-l-amber-500';
|
||||
if (status === 'taken') return 'border-l-4 border-l-green-500';
|
||||
if (status === 'skipped') return 'border-l-4 border-l-gray-400';
|
||||
return '';
|
||||
};
|
||||
|
||||
@@ -245,6 +250,8 @@ export default function MedicationsPage() {
|
||||
<span className="text-green-600 font-medium flex items-center gap-1">
|
||||
<CheckIcon size={16} /> Taken
|
||||
</span>
|
||||
) : entry.status === 'skipped' ? (
|
||||
<span className="text-gray-400 font-medium">Skipped</span>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
|
||||
@@ -755,6 +755,7 @@ export const api = {
|
||||
};
|
||||
scheduled_times: string[];
|
||||
taken_times: string[];
|
||||
skipped_times?: string[];
|
||||
is_prn?: boolean;
|
||||
is_next_day?: boolean;
|
||||
is_previous_day?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user