diff --git a/backend/drizzle/0011_stiff_randall_flagg.sql b/backend/drizzle/0011_stiff_randall_flagg.sql new file mode 100644 index 0000000..eb35192 --- /dev/null +++ b/backend/drizzle/0011_stiff_randall_flagg.sql @@ -0,0 +1,5 @@ +ALTER TABLE `medications` ADD `medication_form` text(20) DEFAULT 'tablet' NOT NULL;--> statement-breakpoint +ALTER TABLE `medications` ADD `pill_form` text(20);--> statement-breakpoint +ALTER TABLE `medications` ADD `lifecycle_category` text(30) DEFAULT 'refill_when_empty' NOT NULL;--> statement-breakpoint +ALTER TABLE `medications` ADD `medication_end_date` text;--> statement-breakpoint +ALTER TABLE `medications` ADD `auto_mark_obsolete_after_end_date` integer DEFAULT true NOT NULL; \ No newline at end of file diff --git a/backend/drizzle/meta/0011_snapshot.json b/backend/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..1accfda --- /dev/null +++ b/backend/drizzle/meta/0011_snapshot.json @@ -0,0 +1,1090 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b2f3aeb3-a855-428e-85e3-8bc34a2a3d69", + "prevId": "41342657-afb5-479d-bc93-2ba8d784c09b", + "tables": { + "dose_tracking": { + "name": "dose_tracking", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dose_id": { + "name": "dose_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taken_at": { + "name": "taken_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s','now'))" + }, + "marked_by": { + "name": "marked_by", + "type": "text(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "taken_source": { + "name": "taken_source", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "dismissed": { + "name": "dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "dose_tracking_user_id_users_id_fk": { + "name": "dose_tracking_user_id_users_id_fk", + "tableFrom": "dose_tracking", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "medications": { + "name": "medications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generic_name": { + "name": "generic_name", + "type": "text(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "taken_by_json": { + "name": "taken_by_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "package_type": { + "name": "package_type", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'blister'" + }, + "medication_form": { + "name": "medication_form", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tablet'" + }, + "pill_form": { + "name": "pill_form", + "type": "text(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lifecycle_category": { + "name": "lifecycle_category", + "type": "text(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'refill_when_empty'" + }, + "pack_count": { + "name": "pack_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "blisters_per_pack": { + "name": "blisters_per_pack", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "pills_per_blister": { + "name": "pills_per_blister", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "total_pills": { + "name": "total_pills", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loose_tablets": { + "name": "loose_tablets", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "stock_adjustment": { + "name": "stock_adjustment", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_stock_correction_at": { + "name": "last_stock_correction_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pill_weight_mg": { + "name": "pill_weight_mg", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dose_unit": { + "name": "dose_unit", + "type": "text(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mg'" + }, + "usage_json": { + "name": "usage_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "every_json": { + "name": "every_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "start_json": { + "name": "start_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "intakes_json": { + "name": "intakes_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expiry_date": { + "name": "expiry_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "intake_reminders_enabled": { + "name": "intake_reminders_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "medication_start_date": { + "name": "medication_start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "medication_end_date": { + "name": "medication_end_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_mark_obsolete_after_end_date": { + "name": "auto_mark_obsolete_after_end_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_obsolete": { + "name": "is_obsolete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "obsolete_at": { + "name": "obsolete_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prescription_enabled": { + "name": "prescription_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "prescription_authorized_refills": { + "name": "prescription_authorized_refills", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prescription_remaining_refills": { + "name": "prescription_remaining_refills", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prescription_low_refill_threshold": { + "name": "prescription_low_refill_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "prescription_expiry_date": { + "name": "prescription_expiry_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dismissed_until": { + "name": "dismissed_until", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "medications_user_id_users_id_fk": { + "name": "medications_user_id_users_id_fk", + "tableFrom": "medications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refill_history": { + "name": "refill_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "medication_id": { + "name": "medication_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packs_added": { + "name": "packs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "loose_pills_added": { + "name": "loose_pills_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used_prescription": { + "name": "used_prescription", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "refill_date": { + "name": "refill_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s','now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "refill_history_medication_id_medications_id_fk": { + "name": "refill_history_medication_id_medications_id_fk", + "tableFrom": "refill_history", + "tableTo": "medications", + "columnsFrom": [ + "medication_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "refill_history_user_id_users_id_fk": { + "name": "refill_history_user_id_users_id_fk", + "tableFrom": "refill_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked": { + "name": "revoked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "refresh_tokens_token_id_unique": { + "name": "refresh_tokens_token_id_unique", + "columns": [ + "token_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_tokens": { + "name": "share_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taken_by": { + "name": "taken_by", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_days": { + "name": "schedule_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "share_tokens_token_unique": { + "name": "share_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "share_tokens_user_id_users_id_fk": { + "name": "share_tokens_user_id_users_id_fk", + "tableFrom": "share_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_settings": { + "name": "user_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_enabled": { + "name": "email_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notification_email": { + "name": "notification_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_stock_reminders": { + "name": "email_stock_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "email_intake_reminders": { + "name": "email_intake_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "email_prescription_reminders": { + "name": "email_prescription_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shoutrrr_enabled": { + "name": "shoutrrr_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shoutrrr_url": { + "name": "shoutrrr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shoutrrr_stock_reminders": { + "name": "shoutrrr_stock_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shoutrrr_intake_reminders": { + "name": "shoutrrr_intake_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shoutrrr_prescription_reminders": { + "name": "shoutrrr_prescription_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "reminder_days_before": { + "name": "reminder_days_before", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 7 + }, + "repeat_daily_reminders": { + "name": "repeat_daily_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "skip_reminders_for_taken_doses": { + "name": "skip_reminders_for_taken_doses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "repeat_reminders_enabled": { + "name": "repeat_reminders_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "reminder_repeat_interval_minutes": { + "name": "reminder_repeat_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "max_nagging_reminders": { + "name": "max_nagging_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "low_stock_days": { + "name": "low_stock_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "normal_stock_days": { + "name": "normal_stock_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "high_stock_days": { + "name": "high_stock_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 180 + }, + "expiry_warning_days": { + "name": "expiry_warning_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "language": { + "name": "language", + "type": "text(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "stock_calculation_mode": { + "name": "stock_calculation_mode", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'automatic'" + }, + "share_stock_status": { + "name": "share_stock_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "upcoming_today_only": { + "name": "upcoming_today_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_schedule_today_only": { + "name": "share_schedule_today_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "swap_dashboard_main_sections": { + "name": "swap_dashboard_main_sections", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_auto_email_sent": { + "name": "last_auto_email_sent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_notification_type": { + "name": "last_notification_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_notification_channel": { + "name": "last_notification_channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reminder_med_name": { + "name": "last_reminder_med_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reminder_taken_by": { + "name": "last_reminder_taken_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_stock_reminder_sent": { + "name": "last_stock_reminder_sent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_stock_reminder_channel": { + "name": "last_stock_reminder_channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_stock_reminder_med_names": { + "name": "last_stock_reminder_med_names", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_prescription_reminder_sent": { + "name": "last_prescription_reminder_sent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_prescription_reminder_channel": { + "name": "last_prescription_reminder_channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_prescription_reminder_med_names": { + "name": "last_prescription_reminder_med_names", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_settings_user_id_unique": { + "name": "user_settings_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "oidc_subject": { + "name": "oidc_subject", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index b1f7176..1a67f83 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1771694832866, "tag": "0010_mean_spot", "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1772219947541, + "tag": "0011_stiff_randall_flagg", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/db-utils.ts b/backend/src/db/db-utils.ts index 67d230b..6816bcb 100644 --- a/backend/src/db/db-utils.ts +++ b/backend/src/db/db-utils.ts @@ -125,6 +125,14 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo `ALTER TABLE medications ADD COLUMN obsolete_at integer`, // Added for explicit medication lifecycle start date `ALTER TABLE medications ADD COLUMN medication_start_date text NOT NULL DEFAULT ''`, + // Added for form/lifecycle modeling (V1 medication forms) + `ALTER TABLE medications ADD COLUMN medication_form text NOT NULL DEFAULT 'tablet'`, + `ALTER TABLE medications ADD COLUMN pill_form text`, + `ALTER TABLE medications ADD COLUMN lifecycle_category text NOT NULL DEFAULT 'refill_when_empty'`, + `ALTER TABLE medications ADD COLUMN medication_end_date text`, + `ALTER TABLE medications ADD COLUMN auto_mark_obsolete_after_end_date integer NOT NULL DEFAULT 1`, + `ALTER TABLE medications ADD COLUMN package_amount_value integer NOT NULL DEFAULT 0`, + `ALTER TABLE medications ADD COLUMN package_amount_unit text NOT NULL DEFAULT 'ml'`, // Added for more detailed reminder info display `ALTER TABLE user_settings ADD COLUMN last_reminder_med_name text`, `ALTER TABLE user_settings ADD COLUMN last_reminder_taken_by text`, diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 333b358..e994527 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -29,6 +29,11 @@ export const medications = sqliteTable("medications", { genericName: text("generic_name", { length: 100 }), takenByJson: text("taken_by_json").notNull().default("[]"), // JSON array of person names packageType: text("package_type", { length: 20 }).notNull().default("blister"), // 'blister' or 'bottle' + medicationForm: text("medication_form", { length: 20 }).notNull().default("tablet"), // 'capsule' | 'tablet' | 'liquid' | 'topical' + pillForm: text("pill_form", { length: 20 }), // Only for blister/bottle with pill-based medications: 'tablet' | 'capsule' + lifecycleCategory: text("lifecycle_category", { length: 30 }).notNull().default("refill_when_empty"), // 'refill_when_empty' | 'treatment_period' + packageAmountValue: integer("package_amount_value").notNull().default(0), // Informational package quantity (ml/g) + packageAmountUnit: text("package_amount_unit", { length: 10 }).notNull().default("ml"), // 'ml' | 'g' packCount: integer("pack_count").notNull().default(1), blistersPerPack: integer("blisters_per_pack").notNull().default(1), pillsPerBlister: integer("pills_per_blister").notNull().default(1), @@ -48,6 +53,10 @@ export const medications = sqliteTable("medications", { notes: text("notes"), intakeRemindersEnabled: integer("intake_reminders_enabled", { mode: "boolean" }).notNull().default(false), medicationStartDate: text("medication_start_date").notNull().default(""), + medicationEndDate: text("medication_end_date"), + autoMarkObsoleteAfterEndDate: integer("auto_mark_obsolete_after_end_date", { mode: "boolean" }) + .notNull() + .default(true), isObsolete: integer("is_obsolete", { mode: "boolean" }).notNull().default(false), obsoleteAt: integer("obsolete_at", { mode: "timestamp" }), prescriptionEnabled: integer("prescription_enabled", { mode: "boolean" }).notNull().default(false), diff --git a/backend/src/routes/export.ts b/backend/src/routes/export.ts index d4903ef..dbf5a45 100644 --- a/backend/src/routes/export.ts +++ b/backend/src/routes/export.ts @@ -17,7 +17,7 @@ const IMAGES_DIR = resolve(getDataDir(), "images"); // ============================================================================= // Export Format Version (bump this when format changes) // ============================================================================= -const EXPORT_VERSION = "1.1"; +const EXPORT_VERSION = "1.3"; // ============================================================================= // Zod Schemas for Import Validation @@ -27,6 +27,7 @@ const scheduleSchema = z.object({ usage: z.number().nonnegative(), every: z.number().int().min(1), start: z.string(), // ISO datetime string + intakeUnit: z.enum(["ml", "tsp", "tbsp"]).nullable().optional(), remind: z.boolean().optional().default(false), takenBy: z.string().nullable().optional(), // Per-intake takenBy (new field) }); @@ -38,7 +39,9 @@ const inventorySchema = z.object({ totalPills: z.number().int().nullable().optional(), // For bottle type: total capacity looseTablets: z.number().int().min(0).default(0), stockAdjustment: z.number().int().default(0), // Manual stock correction - packageType: z.enum(["blister", "bottle"]).default("blister"), + packageType: z.enum(["blister", "bottle", "tube", "liquid_container"]).default("blister"), + packageAmountValue: z.number().int().min(0).default(0), + packageAmountUnit: z.enum(["ml", "g"]).default("ml"), }); const medicationExportSchema = z.object({ @@ -46,11 +49,16 @@ const medicationExportSchema = z.object({ name: z.string().min(1), genericName: z.string().nullable().optional(), takenBy: z.array(z.string()).default([]), + medicationForm: z.enum(["capsule", "tablet", "liquid", "topical"]).default("tablet"), + pillForm: z.enum(["capsule", "tablet"]).nullable().optional(), + lifecycleCategory: z.enum(["refill_when_empty", "treatment_period"]).default("refill_when_empty"), inventory: inventorySchema, pillWeightMg: z.number().int().nullable().optional(), doseUnit: z.enum(["mg", "g", "mcg", "ml", "IU", "units", "drops", "puffs"]).default("mg"), schedules: z.array(scheduleSchema).default([]), medicationStartDate: z.string().nullable().optional(), + medicationEndDate: z.string().nullable().optional(), + autoMarkObsoleteAfterEndDate: z.boolean().default(true), expiryDate: z.string().nullable().optional(), notes: z.string().nullable().optional(), intakeRemindersEnabled: z.boolean().default(false), @@ -155,9 +163,14 @@ async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise< } // Parse intakes from DB format to export format (with per-intake takenBy) -function parseIntakesForExport( - row: typeof medications.$inferSelect -): Array<{ usage: number; every: number; start: string; remind: boolean; takenBy: string | null }> { +function parseIntakesForExport(row: typeof medications.$inferSelect): Array<{ + usage: number; + every: number; + start: string; + intakeUnit: "ml" | "tsp" | "tbsp" | null; + remind: boolean; + takenBy: string | null; +}> { // Use the new parseIntakesJson which falls back to legacy format const intakes = parseIntakesJson( row.intakesJson, @@ -169,6 +182,7 @@ function parseIntakesForExport( usage: intake.usage, every: intake.every, start: intake.start, + intakeUnit: null, remind: intake.intakeRemindersEnabled, takenBy: intake.takenBy, // Per-intake takenBy })); @@ -295,6 +309,9 @@ export async function exportRoutes(app: FastifyInstance) { name: med.name, genericName: med.genericName, takenBy: parseTakenByJson(med.takenByJson), + medicationForm: med.medicationForm ?? "tablet", + pillForm: med.pillForm ?? null, + lifecycleCategory: med.lifecycleCategory ?? "refill_when_empty", inventory: { packCount: med.packCount ?? 1, blistersPerPack: med.blistersPerPack ?? 1, @@ -303,11 +320,15 @@ export async function exportRoutes(app: FastifyInstance) { looseTablets: med.looseTablets ?? 0, stockAdjustment: med.stockAdjustment ?? 0, packageType: med.packageType ?? "blister", + packageAmountValue: med.packageAmountValue ?? 0, + packageAmountUnit: (med.packageAmountUnit ?? "ml") as "ml" | "g", }, pillWeightMg: med.pillWeightMg, doseUnit: med.doseUnit ?? "mg", schedules: parseIntakesForExport(med), medicationStartDate: med.medicationStartDate || null, + medicationEndDate: med.medicationEndDate || null, + autoMarkObsoleteAfterEndDate: med.autoMarkObsoleteAfterEndDate ?? true, expiryDate: med.expiryDate, notes: med.notes, intakeRemindersEnabled: med.intakeRemindersEnabled ?? false, @@ -555,6 +576,7 @@ export async function exportRoutes(app: FastifyInstance) { usage: s.usage, every: s.every, start: s.start, + intakeUnit: s.intakeUnit ?? null, takenBy: s.takenBy || null, intakeRemindersEnabled: s.remind ?? false, })) @@ -570,7 +592,12 @@ export async function exportRoutes(app: FastifyInstance) { name: med.name, genericName: med.genericName || null, takenByJson, + medicationForm: med.medicationForm ?? "tablet", + pillForm: med.pillForm || null, + lifecycleCategory: med.lifecycleCategory ?? "refill_when_empty", packageType: med.inventory.packageType ?? "blister", + packageAmountValue: med.inventory.packageAmountValue ?? 0, + packageAmountUnit: med.inventory.packageAmountUnit ?? "ml", packCount: med.inventory.packCount, blistersPerPack: med.inventory.blistersPerPack, pillsPerBlister: med.inventory.pillsPerBlister, @@ -581,6 +608,8 @@ export async function exportRoutes(app: FastifyInstance) { pillWeightMg: med.pillWeightMg || null, doseUnit: med.doseUnit ?? "mg", medicationStartDate: med.medicationStartDate || "", + medicationEndDate: med.medicationEndDate || null, + autoMarkObsoleteAfterEndDate: med.autoMarkObsoleteAfterEndDate ?? true, intakesJson, usageJson, everyJson, diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts index ab6f73b..06a191f 100644 --- a/backend/src/routes/medications.ts +++ b/backend/src/routes/medications.ts @@ -14,7 +14,13 @@ import { streamToBuffer, writeOptimizedImageSet, } from "../utils/image-upload.js"; -import { type Intake, parseIntakesJson, parseLocalDateTime, parseTakenByJson } from "../utils/scheduler-utils.js"; +import { + type Intake, + normalizeIntakeUsageForStock, + parseIntakesJson, + parseLocalDateTime, + parseTakenByJson, +} from "../utils/scheduler-utils.js"; const IMAGES_DIR = resolve(getDataDir(), "images"); @@ -23,6 +29,7 @@ const intakeSchema = z.object({ usage: z.number().nonnegative(), every: z.number().int().min(1), start: z.string().datetime({ local: true }), + intakeUnit: z.enum(["ml", "tsp", "tbsp"]).nullable().optional(), takenBy: z.string().trim().max(100).nullable().optional(), // Person for this specific intake intakeRemindersEnabled: z.boolean().default(false), // Per-intake reminder setting }); @@ -34,26 +41,37 @@ const blisterSchema = z.object({ start: z.string().datetime({ local: true }), }); -const packageTypeSchema = z.enum(["blister", "bottle"]).default("blister"); +const packageTypeSchema = z.enum(["blister", "bottle", "tube", "liquid_container"]).default("blister"); +const medicationFormSchema = z.enum(["capsule", "tablet", "liquid", "topical"]).default("tablet"); +const pillFormSchema = z.enum(["capsule", "tablet"]); +const lifecycleCategorySchema = z.enum(["refill_when_empty", "treatment_period"]).default("refill_when_empty"); const doseUnitSchema = z.enum(["mg", "g", "mcg", "ml", "IU", "units", "drops", "puffs"]).default("mg"); const medicationStartDateSchema = z .union([z.string().regex(/^\d{4}-\d{2}-\d{2}$/), z.literal(""), z.null()]) .optional(); +const medicationEndDateSchema = z.union([z.string().regex(/^\d{4}-\d{2}-\d{2}$/), z.literal(""), z.null()]).optional(); const medicationSchema = z .object({ name: z.string().trim().max(100).default(""), genericName: z.string().trim().max(100).nullable().optional(), takenBy: z.array(z.string().trim().max(100)).default([]), // Medication-level takenBy (fallback) + medicationForm: medicationFormSchema, + pillForm: pillFormSchema.nullable().optional(), + lifecycleCategory: lifecycleCategorySchema, packageType: packageTypeSchema, packCount: z.number().int().min(0).default(1), blistersPerPack: z.number().int().min(1).default(1), pillsPerBlister: z.number().int().min(1).default(1), + packageAmountValue: z.number().int().min(0).default(0), + packageAmountUnit: z.enum(["ml", "g"]).default("ml"), totalPills: z.number().int().min(1).nullable().optional(), // For bottle type: total capacity looseTablets: z.number().int().min(0).default(0), pillWeightMg: z.number().nonnegative().nullable().optional(), doseUnit: doseUnitSchema, medicationStartDate: medicationStartDateSchema, + medicationEndDate: medicationEndDateSchema, + autoMarkObsoleteAfterEndDate: z.boolean().default(true), expiryDate: z.string().nullable().optional(), notes: z.string().max(2000).nullable().optional(), prescriptionEnabled: z.boolean().default(false), @@ -84,6 +102,77 @@ const medicationSchema = z path: ["medicationStartDate"], } ) + .refine( + (data) => { + const startDate = data.medicationStartDate ?? ""; + const endDate = data.medicationEndDate ?? ""; + if (!startDate || !endDate) return true; + return startDate <= endDate; + }, + { + message: "Medication end date must be on or after medication start date", + path: ["medicationEndDate"], + } + ) + .refine( + (data) => { + if (data.medicationForm === "capsule" || data.medicationForm === "tablet") { + return data.pillForm == null || data.pillForm === "capsule" || data.pillForm === "tablet"; + } + return true; + }, + { + message: "pillForm must be capsule or tablet for capsule/tablet medications", + path: ["pillForm"], + } + ) + .refine( + (data) => { + if (data.medicationForm === "topical") { + return data.packageType === "tube"; + } + return true; + }, + { + message: "Topical medications must use tube package type", + path: ["packageType"], + } + ) + .refine( + (data) => { + if (data.medicationForm === "liquid") { + return data.packageType === "liquid_container"; + } + return true; + }, + { + message: "Liquid medications must use liquid_container package type", + path: ["packageType"], + } + ) + .refine( + (data) => { + if (data.medicationForm === "capsule" || data.medicationForm === "tablet") { + return data.packageType !== "tube" && data.packageType !== "liquid_container"; + } + return true; + }, + { + message: "Capsule and tablet medications cannot use tube or liquid_container package type", + path: ["packageType"], + } + ) + .refine( + (data) => { + const schedules = data.intakes ?? data.blisters ?? []; + if (data.pillForm !== "capsule") return true; + return schedules.every((entry) => Number.isInteger(entry.usage)); + }, + { + message: "Fractional intake is not allowed for capsule", + path: ["intakes"], + } + ) .refine( (data) => { if (!data.prescriptionEnabled) return true; @@ -131,6 +220,26 @@ export async function medicationRoutes(app: FastifyInstance) { app.get<{ Querystring: { includeObsolete?: string } }>("/medications", async (request, reply) => { const userId = await getUserId(request, reply); const includeObsolete = request.query.includeObsolete === "true"; + const initialRows = await db + .select() + .from(medications) + .where(eq(medications.userId, userId)) + .orderBy(medications.id); + const todayDate = new Date().toISOString().slice(0, 10); + + for (const row of initialRows) { + if (row.isObsolete) continue; + if (!(row.autoMarkObsoleteAfterEndDate ?? true)) continue; + const endDate = row.medicationEndDate?.slice(0, 10); + if (!endDate) continue; + if (endDate > todayDate) continue; + + await db + .update(medications) + .set({ isObsolete: true, obsoleteAt: new Date(), updatedAt: new Date() }) + .where(and(eq(medications.id, row.id), eq(medications.userId, userId))); + } + const whereClause = includeObsolete ? eq(medications.userId, userId) : and(eq(medications.userId, userId), eq(medications.isObsolete, false)); @@ -148,10 +257,15 @@ export async function medicationRoutes(app: FastifyInstance) { name: row.name, genericName: row.genericName, takenBy: parseTakenByJson(row.takenByJson), + medicationForm: row.medicationForm ?? "tablet", + pillForm: row.pillForm ?? null, + lifecycleCategory: row.lifecycleCategory ?? "refill_when_empty", packageType: row.packageType ?? "blister", packCount: row.packCount ?? 1, blistersPerPack: row.blistersPerPack ?? 1, pillsPerBlister: row.pillsPerBlister ?? 1, + packageAmountValue: row.packageAmountValue ?? 0, + packageAmountUnit: (row.packageAmountUnit ?? "ml") as "ml" | "g", totalPills: row.totalPills ?? null, looseTablets: row.looseTablets ?? 0, stockAdjustment: row.stockAdjustment ?? 0, @@ -159,6 +273,8 @@ export async function medicationRoutes(app: FastifyInstance) { pillWeightMg: row.pillWeightMg, doseUnit: row.doseUnit ?? "mg", medicationStartDate: row.medicationStartDate || null, + medicationEndDate: row.medicationEndDate || null, + autoMarkObsoleteAfterEndDate: row.autoMarkObsoleteAfterEndDate ?? true, intakes, // New unified format with per-intake takenBy // Legacy blisters format (for backward compat with frontend during transition) blisters: intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start })), @@ -188,15 +304,22 @@ export async function medicationRoutes(app: FastifyInstance) { name, genericName, takenBy, + medicationForm, + pillForm, + lifecycleCategory, packageType, packCount, blistersPerPack, pillsPerBlister, + packageAmountValue, + packageAmountUnit, totalPills, looseTablets, pillWeightMg, doseUnit, medicationStartDate, + medicationEndDate, + autoMarkObsoleteAfterEndDate, expiryDate, notes, prescriptionEnabled, @@ -209,6 +332,9 @@ export async function medicationRoutes(app: FastifyInstance) { blisters: inputBlisters, } = parsed.data; + const normalizedPillForm = + medicationForm === "capsule" || medicationForm === "tablet" ? (pillForm ?? medicationForm) : null; + // Convert to unified intakes format let intakes: Intake[]; if (inputIntakes) { @@ -217,6 +343,7 @@ export async function medicationRoutes(app: FastifyInstance) { usage: i.usage, every: i.every, start: i.start, + intakeUnit: i.intakeUnit ?? null, takenBy: i.takenBy || null, intakeRemindersEnabled: i.intakeRemindersEnabled ?? false, })); @@ -226,6 +353,7 @@ export async function medicationRoutes(app: FastifyInstance) { usage: b.usage, every: b.every, start: b.start, + intakeUnit: null, takenBy: null, // No per-intake takenBy from legacy intakeRemindersEnabled: intakeRemindersEnabled ?? false, })); @@ -247,15 +375,22 @@ export async function medicationRoutes(app: FastifyInstance) { name, genericName: genericName || null, takenByJson, + medicationForm: medicationForm ?? "tablet", + pillForm: normalizedPillForm, + lifecycleCategory: lifecycleCategory ?? "refill_when_empty", packageType: packageType ?? "blister", packCount, blistersPerPack, pillsPerBlister, + packageAmountValue, + packageAmountUnit, totalPills: totalPills || null, looseTablets, pillWeightMg: pillWeightMg || null, doseUnit: doseUnit ?? "mg", medicationStartDate: medicationStartDate ?? "", + medicationEndDate: medicationEndDate || null, + autoMarkObsoleteAfterEndDate: autoMarkObsoleteAfterEndDate ?? true, expiryDate: expiryDate || null, notes: notes || null, prescriptionEnabled: prescriptionEnabled ?? false, @@ -276,10 +411,15 @@ export async function medicationRoutes(app: FastifyInstance) { name: inserted.name, genericName: inserted.genericName, takenBy: parseTakenByJson(inserted.takenByJson), + medicationForm: inserted.medicationForm ?? "tablet", + pillForm: inserted.pillForm ?? null, + lifecycleCategory: inserted.lifecycleCategory ?? "refill_when_empty", packageType: inserted.packageType ?? "blister", packCount: inserted.packCount, blistersPerPack: inserted.blistersPerPack, pillsPerBlister: inserted.pillsPerBlister, + packageAmountValue: inserted.packageAmountValue ?? 0, + packageAmountUnit: (inserted.packageAmountUnit ?? "ml") as "ml" | "g", totalPills: inserted.totalPills ?? null, looseTablets: inserted.looseTablets, stockAdjustment: inserted.stockAdjustment ?? 0, @@ -287,6 +427,8 @@ export async function medicationRoutes(app: FastifyInstance) { pillWeightMg: inserted.pillWeightMg, doseUnit: inserted.doseUnit ?? "mg", medicationStartDate: inserted.medicationStartDate || null, + medicationEndDate: inserted.medicationEndDate || null, + autoMarkObsoleteAfterEndDate: inserted.autoMarkObsoleteAfterEndDate ?? true, intakes, blisters: intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start })), imageUrl: inserted.imageUrl, @@ -323,15 +465,22 @@ export async function medicationRoutes(app: FastifyInstance) { name, genericName, takenBy, + medicationForm, + pillForm, + lifecycleCategory, packageType, packCount, blistersPerPack, pillsPerBlister, + packageAmountValue, + packageAmountUnit, totalPills, looseTablets, pillWeightMg, doseUnit, medicationStartDate, + medicationEndDate, + autoMarkObsoleteAfterEndDate, expiryDate, notes, prescriptionEnabled, @@ -344,6 +493,9 @@ export async function medicationRoutes(app: FastifyInstance) { blisters: inputBlisters, } = parsed.data; + const normalizedPillForm = + medicationForm === "capsule" || medicationForm === "tablet" ? (pillForm ?? medicationForm) : null; + // Convert to unified intakes format let intakes: Intake[]; if (inputIntakes) { @@ -352,6 +504,7 @@ export async function medicationRoutes(app: FastifyInstance) { usage: i.usage, every: i.every, start: i.start, + intakeUnit: i.intakeUnit ?? null, takenBy: i.takenBy || null, intakeRemindersEnabled: i.intakeRemindersEnabled ?? false, })); @@ -361,6 +514,7 @@ export async function medicationRoutes(app: FastifyInstance) { usage: b.usage, every: b.every, start: b.start, + intakeUnit: null, takenBy: null, // No per-intake takenBy from legacy intakeRemindersEnabled: intakeRemindersEnabled ?? false, })); @@ -392,15 +546,22 @@ export async function medicationRoutes(app: FastifyInstance) { name, genericName: genericName || null, takenByJson, + medicationForm: medicationForm ?? "tablet", + pillForm: normalizedPillForm, + lifecycleCategory: lifecycleCategory ?? "refill_when_empty", packageType: packageType ?? "blister", packCount, blistersPerPack, pillsPerBlister, totalPills: totalPills || null, + packageAmountValue, + packageAmountUnit, looseTablets, pillWeightMg: pillWeightMg || null, doseUnit: doseUnit ?? "mg", medicationStartDate: medicationStartDate ?? "", + medicationEndDate: medicationEndDate || null, + autoMarkObsoleteAfterEndDate: autoMarkObsoleteAfterEndDate ?? true, expiryDate: expiryDate || null, notes: notes || null, prescriptionEnabled: prescriptionEnabled ?? false, @@ -545,10 +706,15 @@ export async function medicationRoutes(app: FastifyInstance) { name: result[0].name, genericName: result[0].genericName, takenBy: parseTakenByJson(result[0].takenByJson), + medicationForm: result[0].medicationForm ?? "tablet", + pillForm: result[0].pillForm ?? null, + lifecycleCategory: result[0].lifecycleCategory ?? "refill_when_empty", packageType: result[0].packageType ?? "blister", packCount: result[0].packCount, blistersPerPack: result[0].blistersPerPack, pillsPerBlister: result[0].pillsPerBlister, + packageAmountValue: result[0].packageAmountValue ?? 0, + packageAmountUnit: (result[0].packageAmountUnit ?? "ml") as "ml" | "g", totalPills: result[0].totalPills ?? null, looseTablets: result[0].looseTablets, stockAdjustment: result[0].stockAdjustment ?? 0, @@ -556,6 +722,8 @@ export async function medicationRoutes(app: FastifyInstance) { pillWeightMg: result[0].pillWeightMg, doseUnit: result[0].doseUnit ?? "mg", medicationStartDate: result[0].medicationStartDate || null, + medicationEndDate: result[0].medicationEndDate || null, + autoMarkObsoleteAfterEndDate: result[0].autoMarkObsoleteAfterEndDate ?? true, intakes, blisters: intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start })), imageUrl: result[0].imageUrl, @@ -845,7 +1013,12 @@ export async function medicationRoutes(app: FastifyInstance) { { usageJson: row.usageJson, everyJson: row.everyJson, startJson: row.startJson }, row.intakeRemindersEnabled ?? false ); - const blisters = intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start })); + const medForm = row.medicationForm ?? "tablet"; + const blisters = intakes.map((i) => ({ + usage: normalizeIntakeUsageForStock(i, medForm, row.packageType), + every: i.every, + start: i.start, + })); const pillsPerBlister = row.pillsPerBlister ?? 1; const packCount = row.packCount ?? 1; const blistersPerPack = row.blistersPerPack ?? 1; @@ -854,8 +1027,9 @@ export async function medicationRoutes(app: FastifyInstance) { const packageType = row.packageType ?? "blister"; // For bottle type, looseTablets IS the current stock (no blister math) + const isTopical = medForm === "topical" || packageType === "tube"; const originalTotalPills = - packageType === "bottle" + packageType === "bottle" || packageType === "liquid_container" ? looseTablets + stockAdjustment : packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment; @@ -867,7 +1041,9 @@ export async function medicationRoutes(app: FastifyInstance) { let consumedUntilNow = 0; const msPerDay = 86400000; - if (stockCalculationMode === "automatic") { + if (isTopical) { + consumedUntilNow = 0; + } else if (stockCalculationMode === "automatic") { blisters.forEach((blister, blisterIdx) => { const blisterStart = parseLocalDateTime(blister.start).getTime(); if (Number.isNaN(blisterStart)) return; @@ -963,7 +1139,7 @@ export async function medicationRoutes(app: FastifyInstance) { }); } - const currentStock = Math.max(0, originalTotalPills - consumedUntilNow); + const currentStock = isTopical ? originalTotalPills : Math.max(0, originalTotalPills - consumedUntilNow); // Calculate usage for the planning period // Always use the user-selected start date for the usage calculation. @@ -973,7 +1149,7 @@ export async function medicationRoutes(app: FastifyInstance) { // The stock already reflects consumed doses, so no double-counting occurs. // When includeUntilStart is true, calculate from now to end (useful for trip planning) const effectivePlannerStart = includeUntilStart ? now : start; - const usageTotal = calculateUsageInRange(blisters, effectivePlannerStart, end); + const usageTotal = isTopical ? 0 : calculateUsageInRange(blisters, effectivePlannerStart, end); const blistersNeeded = pillsPerBlister > 0 ? Math.ceil(usageTotal / pillsPerBlister) : 0; @@ -983,7 +1159,7 @@ export async function medicationRoutes(app: FastifyInstance) { let fullBlisters: number; let loosePills: number; - if (packageType === "bottle") { + if (packageType === "bottle" || packageType === "tube" || packageType === "liquid_container") { // Bottle type: no blisters, everything is loose pills fullBlisters = 0; loosePills = availableAfterPeriod; diff --git a/backend/src/test/db-client.test.ts b/backend/src/test/db-client.test.ts index 30fe997..c458f5c 100644 --- a/backend/src/test/db-client.test.ts +++ b/backend/src/test/db-client.test.ts @@ -32,8 +32,8 @@ async function loadDbClientModule(options: ClientTestOptions = {}) { .mockReturnValue(dirWritable ? { success: true } : { success: false, error: "permission denied" }); const getDbPaths = vi.fn().mockReturnValue({ dataDir: "/tmp/medassist-data", - dbPath: "/tmp/medassist-data/medassist.db", - url: "file:/tmp/medassist-data/medassist.db", + dbPath: "/tmp/medassist-data/medassist-ng.db", + url: "file:/tmp/medassist-data/medassist-ng.db", }); const runDrizzleMigrations = vi.fn().mockResolvedValue({ success: true }); const runAlterMigrations = vi.fn().mockResolvedValue({ errors: [] }); @@ -102,7 +102,7 @@ describe("db/client bootstrap", () => { await mod.migrationsReady; expect(mocks.ensureDataDirectory).toHaveBeenCalledWith("/tmp/medassist-data"); - expect(mocks.createClient).toHaveBeenCalledWith({ url: "file:/tmp/medassist-data/medassist.db" }); + expect(mocks.createClient).toHaveBeenCalledWith({ url: "file:/tmp/medassist-data/medassist-ng.db" }); expect(mocks.runDrizzleMigrations).toHaveBeenCalledTimes(1); expect(mocks.runAlterMigrations).toHaveBeenCalledTimes(1); expect(mocks.repairTrailingHyphenDoseIds).toHaveBeenCalledTimes(1); diff --git a/backend/src/test/e2e-routes.test.ts b/backend/src/test/e2e-routes.test.ts index 230e410..0ce661f 100644 --- a/backend/src/test/e2e-routes.test.ts +++ b/backend/src/test/e2e-routes.test.ts @@ -82,7 +82,12 @@ async function createSchema(client: Client) { name text NOT NULL, generic_name text, taken_by_json text NOT NULL DEFAULT '[]', + medication_form text NOT NULL DEFAULT 'tablet', + pill_form text, + lifecycle_category text NOT NULL DEFAULT 'refill_when_empty', package_type text NOT NULL DEFAULT 'blister', + package_amount_value integer NOT NULL DEFAULT 0, + package_amount_unit text NOT NULL DEFAULT 'ml', pack_count integer NOT NULL DEFAULT 1, blisters_per_pack integer NOT NULL DEFAULT 1, pills_per_blister integer NOT NULL DEFAULT 1, @@ -101,6 +106,8 @@ async function createSchema(client: Client) { notes text, intake_reminders_enabled integer NOT NULL DEFAULT 0, medication_start_date text NOT NULL DEFAULT '', + medication_end_date text, + auto_mark_obsolete_after_end_date integer NOT NULL DEFAULT 1, is_obsolete integer NOT NULL DEFAULT 0, obsolete_at integer, prescription_enabled integer NOT NULL DEFAULT 0, @@ -2499,10 +2506,10 @@ describe("E2E Tests with Real Routes", () => { }); // --------------------------------------------------------------------------- - // Package Type (bottle vs blister) Tests + // Package Type (blister, bottle, liquid_container) Tests // --------------------------------------------------------------------------- - describe("Package type handling (bottle vs blister)", () => { + describe("Package type handling (blister, bottle, liquid_container)", () => { const bottleMedication = { name: "Vitamin D Drops", packageType: "bottle", @@ -2523,6 +2530,18 @@ describe("E2E Tests with Real Routes", () => { blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }], }; + const liquidContainerMedication = { + name: "Cough Syrup", + medicationForm: "liquid", + packageType: "liquid_container", + doseUnit: "ml", + packCount: 0, + blistersPerPack: 1, + pillsPerBlister: 1, + looseTablets: 180, + blisters: [{ usage: 5, every: 1, start: "2025-01-01T08:00:00.000Z" }], + }; + it("should create and return bottle type medication", async () => { const response = await app.inject({ method: "POST", @@ -2567,6 +2586,49 @@ describe("E2E Tests with Real Routes", () => { expect(data.medications[0].totalPills).toBe(120); }); + it("should create and return liquid_container type medication", async () => { + const response = await app.inject({ + method: "POST", + url: "/medications", + payload: liquidContainerMedication, + }); + + expect(response.statusCode).toBe(200); + const data = response.json(); + expect(data.packageType).toBe("liquid_container"); + expect(data.medicationForm).toBe("liquid"); + expect(data.doseUnit).toBe("ml"); + expect(data.looseTablets).toBe(180); + }); + + it("should return packageType and ml-based stock semantics in shared schedule for liquid_container", async () => { + await app.inject({ + method: "POST", + url: "/medications", + payload: { ...liquidContainerMedication, takenBy: ["Daniel"] }, + }); + + const shareResponse = await app.inject({ + method: "POST", + url: "/share", + payload: { takenBy: "Daniel", scheduleDays: 30 }, + }); + expect(shareResponse.statusCode).toBe(200); + const { token } = shareResponse.json(); + + const scheduleResponse = await app.inject({ + method: "GET", + url: `/share/${token}`, + }); + + expect(scheduleResponse.statusCode).toBe(200); + const data = scheduleResponse.json(); + expect(data.medications).toHaveLength(1); + expect(data.medications[0].packageType).toBe("liquid_container"); + // Liquid container follows container semantics (stock from looseTablets only). + expect(data.medications[0].totalPills).toBe(180); + }); + it("should calculate correct totalPills for shared blister medication", async () => { await app.inject({ method: "POST", @@ -2742,5 +2804,18 @@ describe("E2E Tests with Real Routes", () => { expect(medsResponse.json()).toHaveLength(1); expect(medsResponse.json()[0].packageType).toBe("blister"); }); + + it("should reject liquid medication form with non-liquid package type", async () => { + const response = await app.inject({ + method: "POST", + url: "/medications", + payload: { + ...liquidContainerMedication, + packageType: "bottle", + }, + }); + + expect(response.statusCode).toBe(400); + }); }); }); diff --git a/backend/src/test/integration.test.ts b/backend/src/test/integration.test.ts index 5cdde40..0c0cddf 100644 --- a/backend/src/test/integration.test.ts +++ b/backend/src/test/integration.test.ts @@ -76,7 +76,12 @@ async function createSchema(client: Client) { name text NOT NULL, generic_name text, taken_by_json text NOT NULL DEFAULT '[]', + medication_form text NOT NULL DEFAULT 'tablet', + pill_form text, + lifecycle_category text NOT NULL DEFAULT 'refill_when_empty', package_type text NOT NULL DEFAULT 'blister', + package_amount_value integer NOT NULL DEFAULT 0, + package_amount_unit text NOT NULL DEFAULT 'ml', pack_count integer NOT NULL DEFAULT 1, blisters_per_pack integer NOT NULL DEFAULT 1, pills_per_blister integer NOT NULL DEFAULT 1, @@ -95,6 +100,8 @@ async function createSchema(client: Client) { notes text, intake_reminders_enabled integer NOT NULL DEFAULT 0, medication_start_date text NOT NULL DEFAULT '', + medication_end_date text, + auto_mark_obsolete_after_end_date integer NOT NULL DEFAULT 1, is_obsolete integer NOT NULL DEFAULT 0, obsolete_at integer, prescription_enabled integer NOT NULL DEFAULT 0, diff --git a/backend/src/test/planner.test.ts b/backend/src/test/planner.test.ts index e2de0cc..f723db8 100644 --- a/backend/src/test/planner.test.ts +++ b/backend/src/test/planner.test.ts @@ -93,7 +93,12 @@ async function createSchema(client: Client) { name text NOT NULL, generic_name text, taken_by_json text NOT NULL DEFAULT '[]', + medication_form text NOT NULL DEFAULT 'tablet', + pill_form text, + lifecycle_category text NOT NULL DEFAULT 'refill_when_empty', package_type text NOT NULL DEFAULT 'blister', + package_amount_value integer NOT NULL DEFAULT 0, + package_amount_unit text NOT NULL DEFAULT 'ml', pack_count integer NOT NULL DEFAULT 1, blisters_per_pack integer NOT NULL DEFAULT 1, pills_per_blister integer NOT NULL DEFAULT 1, @@ -112,6 +117,8 @@ async function createSchema(client: Client) { notes text, intake_reminders_enabled integer NOT NULL DEFAULT 0, medication_start_date text NOT NULL DEFAULT '', + medication_end_date text, + auto_mark_obsolete_after_end_date integer NOT NULL DEFAULT 1, is_obsolete integer NOT NULL DEFAULT 0, obsolete_at integer, prescription_enabled integer NOT NULL DEFAULT 0, diff --git a/backend/src/utils/scheduler-utils.ts b/backend/src/utils/scheduler-utils.ts index 866e1c5..eee8db0 100644 --- a/backend/src/utils/scheduler-utils.ts +++ b/backend/src/utils/scheduler-utils.ts @@ -17,6 +17,22 @@ export type Intake = { intakeRemindersEnabled: boolean; }; +/** + * Normalize intake usage for stock math. + * + * Stock semantics currently treat numeric usage as-is for all supported + * medication forms/package types. The helper centralizes this behavior so route + * logic can depend on a single validated numeric value. + */ +export function normalizeIntakeUsageForStock( + intake: Pick, + _medicationForm?: string | null, + _packageType?: string | null +): number { + const usage = Number(intake.usage); + return Number.isFinite(usage) && usage > 0 ? usage : 0; +} + // ============================================================================= // Timezone utilities // ============================================================================= diff --git a/doku/memory_notes.md b/doku/memory_notes.md index 4464aa9..d8d5520 100644 --- a/doku/memory_notes.md +++ b/doku/memory_notes.md @@ -23,6 +23,607 @@ Use this block for each meaningful task: ## Entries +### 2026-02-28 (CI triage: Backend Tests failure on PR #356) + +- 🧩 Task: Reproduce and fix `Backend Tests` CI failure on `feat/package-amount-backend`. +- ✅ Decisions: + - Reproduced local backend failure (`15` failing tests) with `500` responses from `POST /medications/usage`. + - Identified root cause: `backend/src/routes/medications.ts` called `normalizeIntakeUsageForStock(...)` but helper export was missing from `backend/src/utils/scheduler-utils.ts`. + - Added minimal helper implementation that normalizes finite positive usage values and keeps stock usage semantics centralized. + - Re-ran targeted failing suites, then full backend suite; all passed. +- 📁 Files touched: + - `backend/src/utils/scheduler-utils.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - None for backend test check; release handoff can proceed without push from this agent. + +### 2026-02-28 (stacked branch validation: package amount + liquid units + topical no-depletion) + +- 🧩 Task: Validate stacked branch readiness and make tests compatible with new medication/package semantics. +- ✅ Decisions: + - Confirmed stack order on `feat/topical-no-depletion-planner`: `7ebd253` -> `3954ed2` -> `e689720` -> `f9deb1b`. + - Fixed backend in-memory test schemas (integration/planner/e2e-routes) to include new medication columns so route tests stop failing with fixture-level `500` errors. + - Updated focused E2E selectors/assertions for schedule usage labels and planner stock wording after liquid/topical/package updates. + - Stabilized medication lifecycle E2E edit flow by cleaning per-test medication state and using current edit-form labels. +- 📁 Files touched: + - `backend/src/test/integration.test.ts` + - `backend/src/test/planner.test.ts` + - `backend/src/test/e2e-routes.test.ts` + - `frontend/e2e/medication-edit.spec.ts` + - `frontend/e2e/medication-lifecycle.spec.ts` + - `frontend/e2e/planner-data.spec.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Frontend lint still reports 12 pre-existing `noNestedTernary` warnings in `MedicationsPage.tsx` and `ReportModal.tsx` (outside this compatibility-fix scope). + +### 2026-02-28 (package amount UX: numeric input only for tube and liquid) + +- 🧩 Task: Remove `+/-` steppers for non-tablet package amount fields. +- ✅ Decisions: + - Replaced `FormNumberStepper` with a plain numeric text input for: + - `tube` -> `Amount per tube` + - `liquid_container` -> `Package amount` + - Kept unit selectors read-only and fixed per domain rule: + - `tube` unit fixed to `g` + - `liquid_container` unit fixed to `ml` + - Added component regression tests in mobile modal suite to validate the new input style and fixed units. +- 📁 Files touched: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/test/components/MobileEditModal.test.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Existing unrelated `noNestedTernary` lint warnings remain in `MedicationsPage.tsx` (pre-existing scope). + +### 2026-02-28 (test update: tube grams + liquid ml regression coverage) + +- 🧩 Task: Review and update tests for the new package-unit behavior rules. +- ✅ Decisions: + - Extended existing `useMedicationForm` tests to assert `packageAmountUnit="ml"` for `liquid_container` defaults/locks. + - Added regression test that `tube` enforces/keeps `packageAmountUnit="g"` even if UI attempts to set `ml`. + - Added regression test that legacy `tube` records with `packageAmountUnit="ml"` are normalized to `g` during `startEdit`. + - Refactored touched code paths to satisfy `noNestedTernary` lint in changed files. +- 📁 Files touched: + - `frontend/src/test/hooks/useMedicationForm.test.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Optional backend route-level validation for `tube+ml` remains open if full server-side enforcement is desired. + +### 2026-02-28 (tube unit correction: no ml for tubes) + +- 🧩 Task: Enforce domain rule from user feedback: tubes use `g`, not `ml`. +- ✅ Decisions: + - Removed `ml` choice from tube amount input in desktop and mobile edit forms (unit is fixed to `g`). + - Added hard normalization so tube edit state and save payload always persist `packageAmountUnit="g"`. + - Added payload guard so `liquid_container` is normalized to `packageAmountUnit="ml"`. +- 📁 Files touched: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/hooks/useMedicationForm.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Optional backend validation hardening can be added later to reject historic `tube+ml` payloads server-side. + +### 2026-02-28 (tube package UX simplification: 1 tube x 150 g) + +- 🧩 Task: Make `tube` package fields intuitive and remove conflicting stock inputs. +- ✅ Decisions: + - Reworked stock UI for `packageType=tube` in desktop and mobile to show only `Tubes` + `Amount per tube` + computed total amount. + - Removed `total/current amount` steppers for tube to avoid contradictory input combinations. + - Save payload now normalizes tube values to a consistent amount model (`totalPills` and `looseTablets` derived from `packCount * packageAmountValue`). + - Added i18n keys for tube-specific labels (`form.tubes`, `form.packageAmountPerTube`) in EN/DE. +- 📁 Files touched: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If needed, align planner/report wording to explicitly display tube multiplication format (`NxY unit`) everywhere. + +### 2026-02-28 (start date optional placeholder consistency) + +- 🧩 Task: Make start-date hint consistent with other optional date fields. +- ✅ Decisions: + - Added `common.optional` placeholder to `Medication Start Date` in desktop and mobile edit forms. + - Kept validation behavior unchanged (start date remains optional). +- 📁 Files touched: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - None. + +### 2026-02-28 (full implementation: liquid units + topical stock behavior + package amount metadata) + +- 🧩 Task: Implement the approved suggestions end-to-end: liquid intake unit conversion, topical non-depleting stock logic, and package amount metadata. +- ✅ Decisions: + - Added persisted medication metadata fields `packageAmountValue` + `packageAmountUnit` (`ml|g`) in DB schema/migration and API/export/import mappings. + - Extended intake model with `intakeUnit` (`ml|tsp|tbsp`) and conversion logic (`tsp=5 ml`, `tbsp=15 ml`) for stock calculations. + - Enforced topical stock behavior as metadata-only depletion path in planner/reminder/frontend coverage calculations (`topical`/`tube` does not reduce stock in V1.1 behavior). + - Added desktop+mobile parity UI for liquid intake-unit selection and package-amount metadata inputs. + - Added EN/DE i18n keys for new fields and bumped export format to `1.3`. +- 📁 Files touched: + - `backend/src/routes/medications.ts` + - `backend/src/services/reminder-scheduler.ts` + - `backend/src/routes/export.ts` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/utils/schedule.ts` + - `frontend/src/components/SharedSchedule.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Testing execution remains delegated to `@testing-manager` per governance. + +### 2026-02-28 (topical non-measurable vs liquid measurable + tbsp conversion) + +- 🧩 Task: Clarify that topical content should not affect stock math while liquid should, and define tablespoon conversion. +- ✅ Decisions: + - Updated `doku/package_types.md` so `topical` package amount is metadata-only (no depletion effect) in V1/V1.1. + - Kept `liquid` as measurable stock with canonical `ml` deduction. + - Added liquid intake conversion rules: `1 tsp = 5 ml`, `1 tbsp = 15 ml`. + - Locked MedAssist conversion to medical metric convention (`tbsp=15 ml`), excluding regional culinary variants. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If approved for implementation: add intake-unit enum (`ml|tsp|tbsp`) and conversion logic in frontend/backend. + +### 2026-02-28 (packaging quantity unit decision for liquids/topicals) + +- 🧩 Task: Clarify how package amount should be measured for liquid and topical medications. +- ✅ Decisions: + - Added explicit recommendation in `doku/package_types.md` to introduce package quantity fields: `packageAmountValue` + `packageAmountUnit` (`ml|g`). + - Documented default mapping: `liquid_container -> ml`, `tube(topical) -> g`, with manual override to `ml` for topical liquids. + - Clarified separation between `doseUnit`, `packageAmountUnit`, and `strengthUnit`. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If approved, implement DB/API/frontend fields and migrations in a dedicated PR. + +### 2026-02-28 (liquid_container regression tests implemented) + +- 🧩 Task: Convert the checklist handoff into executable regression coverage for `liquid_container` and run targeted validation. +- ✅ Decisions: + - Added backend real-route regression tests in `e2e-routes.test.ts` for create/share semantics and invalid liquid/package combination rejection. + - Added frontend hook regression tests in `useMedicationForm.test.ts` for `liquid_container` form derivation (`medicationForm=liquid`, `doseUnit=ml`) and lock behavior. + - Fixed outdated in-memory test schema in `e2e-routes.test.ts` by adding missing medication columns (`medication_form`, `pill_form`, `lifecycle_category`, `medication_end_date`, `auto_mark_obsolete_after_end_date`) so current route inserts can be tested reliably. + - Executed only targeted test names to isolate new behavior from unrelated legacy failures in the larger file. +- 📁 Files touched: + - `backend/src/test/e2e-routes.test.ts` + - `frontend/src/test/hooks/useMedicationForm.test.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Optional next step: expand backend package/form matrix coverage beyond these focused regressions. + +### 2026-02-28 (testing-manager handoff checklist for liquid_container) + +- 🧩 Task: Create a concrete test handoff checklist after user confirmation. +- ✅ Decisions: + - Added a dedicated `@testing-manager` section to `doku/package_types.md` with backend validation cases, frontend desktop/mobile parity checks, data exchange checks, and E2E minimum scenarios. + - Kept checklist scoped to the `liquid_container` rollout and package/form invariants. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Execute the checklist via `@testing-manager` ownership path. + +### 2026-02-28 (liquid_container rollout) + +- 🧩 Task: Implement dedicated liquid package type end-to-end (`liquid_container`) after user decision. +- ✅ Decisions: + - Added `liquid_container` to backend and frontend package type contracts. + - Enforced compatibility: `liquid -> liquid_container`, `topical -> tube`, `capsule/tablet -> not tube/not liquid_container`. + - Updated desktop/mobile medication forms with explicit `liquid_container` option and fixed unit/label behavior (`ml`) in planner/schedule/dashboard/detail/report flows. + - Updated backend planner/reminder/export/refill/share logic to treat `liquid_container` as container stock semantics where applicable. + - Updated `doku/package_types.md` constraints to reflect new canonical mapping. +- 📁 Files touched: + - `backend/src/routes/medications.ts` + - `backend/src/routes/export.ts` + - `backend/src/routes/refills.ts` + - `backend/src/routes/share.ts` + - `backend/src/routes/planner.ts` + - `backend/src/services/reminder-scheduler.ts` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/pages/PlannerPage.tsx` + - `frontend/src/pages/SchedulePage.tsx` + - `frontend/src/pages/DashboardPage.tsx` + - `frontend/src/components/MedDetailModal.tsx` + - `frontend/src/components/ReportModal.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Run full regression via `@testing-manager` (ownership rule). + +### 2026-02-28 (logic correction: liquid cannot be in tube) + +- 🧩 Task: Fix package-type logic after user report that `liquid` in `tube` is invalid. +- ✅ Decisions: + - Backend validation changed to `topical -> tube` and `liquid != tube`. + - UI for `packageType=tube` now only allows `topical`; the `liquid` option was removed in desktop and mobile edit flows. + - Form defaults/derivations for `tube` now force `medicationForm=topical` with `doseUnit=units`. + - Updated source-of-truth plan wording in `doku/package_types.md` to match corrected logic. +- 📁 Files touched: + - `backend/src/routes/medications.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Re-check if a dedicated `liquid` package path should be modeled explicitly in a future master-plan phase. + +### 2026-02-28 (package-type hardening pass implemented) + +- 🧩 Task: Implement as much of the package-type remediation as possible before the master-plan rollout. +- ✅ Decisions: + - Hardened backend API validation in `medications.ts` by adding inverse compatibility rule (`capsule/tablet` cannot use `tube`). + - Updated frontend planner and schedule views to stop pill-only wording for `tube` medications and to render form-aware units. + - Updated backend planner/reminder notification wording to avoid pill assumptions for `tube` and use form-aware/generic unit terms. + - Extended backend translation common keys with unit terms required for the updated notification wording. +- 📁 Files touched: + - `backend/src/routes/medications.ts` + - `frontend/src/pages/PlannerPage.tsx` + - `frontend/src/pages/SchedulePage.tsx` + - `backend/src/routes/planner.ts` + - `backend/src/services/reminder-scheduler.ts` + - `backend/src/i18n/translations.ts` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Test ownership remains with `@testing-manager`; full four-form regression coverage still pending. + +### 2026-02-28 (package_types full-plan refresh to current implementation status) + +- 🧩 Task: Review the full `doku/package_types.md` and bring it to the latest code-aligned state. +- ✅ Decisions: + - Added a dated status snapshot (`2026-02-28`) with explicit split between implemented and still-open items. + - Removed scope ambiguity by changing V1 section into `already implemented` baseline plus `remaining work`. + - Added explicit note that persisted lifecycle values are currently limited to `refill_when_empty|treatment_period`, while `ongoing` is runtime-derived. + - Added progress interpretation to the 1:1 remediation section (verify-and-align for completed parts, prioritize known open gaps). +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Next implementation focus remains planner/schedule/reminder wording normalization and full four-form regression coverage. + +### 2026-02-28 (package_types plan: added 1:1 remediation execution order) + +- 🧩 Task: Write the full executable remediation order directly into `doku/package_types.md`. +- ✅ Decisions: + - Added a mandatory file-by-file sequence with explicit `file -> change -> acceptance` structure. + - Included all previously identified impacted backend, frontend, i18n, test, e2e, and documentation files. + - Added an execution gate: skipped files require explicit technical rationale, otherwise rollout is incomplete. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Execute the remediation in code in exactly this order and track skipped items with rationale. + +### 2026-02-28 (package_types plan made coherent + full impact inventory) + +- 🧩 Task: Adjust `doku/package_types.md` so the plan is coherent and explicitly lists all affected code/test/doc areas. +- ✅ Decisions: + - Fixed top-level contradiction by documenting current container reality as `blister|bottle|tube`. + - Added a mandatory explicit affected-file inventory across backend routes/services/schema, frontend runtime surfaces, i18n, backend tests, frontend tests, and e2e specs. + - Kept the no-partial-rollout enforcement and linked it to concrete file groups for execution control. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Implement remediation in code according to the newly enumerated file inventory (starting with planner/schedule wording and corresponding tests). + +### 2026-02-28 (package type plan hardening against partial implementation) + +- 🧩 Task: Strengthen `doku/package_types.md` so package/form changes cannot be considered done when only partial surfaces are updated. +- ✅ Decisions: + - Added a mandatory cross-layer implementation coverage section (backend validation, backend logic, desktop+mobile parity, read views, i18n, import/export/share, tests). + - Added explicit definition of done: all checklist areas must be updated or explicitly marked not impacted with rationale. + - Grounded follow-up review findings with concrete gap examples still visible in code (notably planner/schedule pill-only wording paths). +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Execute a dedicated cleanup pass for planner/schedule/backend planner notification wording and corresponding tests. + +### 2026-02-28 (release-manager doc cleanup: remove app-feature example) + +- 🧩 Task: Remove product-feature-specific text from `.github/agents/release-manager.agent.md` and keep it process-focused. +- ✅ Decisions: + - Replaced concrete app feature example under release notes guidance with a neutral, reusable template using placeholders. + - Kept release process rules intact; only example content was generalized. +- 📁 Files touched: + - `.github/agents/release-manager.agent.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Optionally align the Breaking Changes heading example with the no-emoji rule in a separate doc cleanup pass. + +### 2026-02-27 (dashboard overview tube unit fix) + +- 🧩 Task: Fix dashboard medication overview showing `pill` for `tube` medications. +- ✅ Decisions: + - Replaced pill-based stock label in dashboard overview with tube-aware amount labels. + - Added local dashboard helpers to render `tube` values as `ml` (liquid) or `applications` (topical). + - Updated timeline dose/total tags in dashboard day blocks to use tube-aware units and suppress pill-weight mg details for tube. +- 📁 Files touched: + - `frontend/src/pages/DashboardPage.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - None. + +### 2026-02-27 (date input placeholder casing fix) + +- 🧩 Task: Keep `optional` placeholder text lowercase in date inputs instead of inherited uppercase styling. +- ✅ Decisions: + - Root cause is inherited `text-transform: uppercase` from `.form-grid label`. + - Applied local override on `.date-input-display` (`text-transform: none`, `letter-spacing: normal`) to preserve calm, readable lowercase placeholder text. +- 📁 Files touched: + - `frontend/src/styles/schedule-mobile-edit.css` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - None. + +### 2026-02-27 (tube wording consistency in report exports) + +- 🧩 Task: Complete tube-specific wording consistency in medication report exports (text + print/PDF). +- ✅ Decisions: + - Added helper functions in `ReportModal` to centralize tube unit/label logic (`ml` vs `applications`). + - Replaced pill-centric wording with amount-based wording for `tube` in current stock, total capacity label, intake schedule entries, and refill history entries. + - Hid `Dose per pill` row for `tube` in both text and print report outputs. +- 📁 Files touched: + - `frontend/src/components/ReportModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Dashboard/Planner wording parity should be rechecked in a dedicated pass if product wants full app-wide amount terminology normalization for tube. + +### 2026-02-27 (holistic package-specific UI behavior for tube) + +- 🧩 Task: Make package tab behavior fully package-specific so `tube` does not show pill/mg-oriented fields. +- ✅ Decisions: + - For `tube`, relabeled stock fields from pill terminology to amount terminology. + - Hid the pill-specific strength field (`Dose per pill`) for `tube` in desktop and mobile package tabs. + - Adjusted total display text for `tube` to avoid `pill/pills` wording. + - Added tube-form default unit behavior: `liquid -> ml`, `topical -> units`. + - Added EN/DE i18n keys for amount-based labels. +- 📁 Files touched: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Optional backend extension: distinguish volume-based depletion for liquid vs application-based depletion for topical in planner/reminder calculations. + +### 2026-02-27 (align package_types doc with implemented tube/liquid/topical behavior) + +- 🧩 Task: Resolve contradiction between implementation and `doku/package_types.md` technical constraints. +- ✅ Decisions: + - Updated constraints to reflect actual support for `packageType=blister|bottle|tube`. + - Documented current UX split: + - `blister`/`bottle` use `pillForm`. + - `tube` uses `medicationForm` (`liquid`/`topical`). + - Removed stale claim that only `blister|bottle` are supported end-to-end. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Keep docs in lockstep with model/UI changes to avoid product-level confusion. + +### 2026-02-27 (restore liquid vs topical distinction for tube) + +- 🧩 Task: Reintroduce meaningful liquid/topical distinction after pillForm-first simplification removed explicit tube subform choice. +- ✅ Decisions: + - Keep `pillForm` as primary for non-tube packages. + - For `packageType=tube`, show `medicationForm` selector with only `liquid` and `topical` options. + - Tube intake behavior now respects selected tube subform: + - `liquid` -> fractional intake allowed, `usageMl` label. + - `topical` -> integer/application-style intake, `usageApplication` label. + - Default when switching to tube is now `liquid` unless an existing tube subform already exists. +- 📁 Files touched: + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If stock math for liquid should be volume-precise vs application-based, add explicit unit/policy handling in backend planner/reminder calculations. + +### 2026-02-27 (pillForm-first UX: removed medicationForm selector) + +- 🧩 Task: Remove semantically redundant `medicationForm` vs `pillForm` user choice and make `pillForm` the primary user-facing control. +- ✅ Decisions: + - Removed `medicationForm` selector from desktop and mobile forms. + - Kept `pillForm` as the user-facing form mechanic for non-tube package types. + - Kept `packageType` explicit (`blister`, `bottle`, `tube`) and derive `medicationForm` internally on save for backend compatibility. + - Intake behavior now keys off `packageType`/`pillForm` in UI logic (fraction rule + usage labels). +- 📁 Files touched: + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If liquid vs topical needs separate user-facing control later, add it only with concrete behavior differences. + +### 2026-02-27 (remove non-functional lifecycle selector from UI) + +- 🧩 Task: Ensure users only see options with concrete app impact. +- ✅ Decisions: + - Removed `lifecycleCategory` selector from desktop and mobile medication edit forms because both options currently have no distinct runtime behavior. + - Kept persistence/internal field compatibility untouched to avoid DB/API churn in the same scope. + - Documented that lifecycle selector remains hidden until it drives differentiated planner/reminder/stock behavior. +- 📁 Files touched: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If lifecycle should return as a visible control, implement real behavior differences first and then re-enable UI. + +### 2026-02-27 (package type pivot: liquid/topical use tube, not bottle) + +- 🧩 Task: Complete cross-layer pivot after user correction that liquid/topical must not use pill-bottle semantics. +- ✅ Decisions: + - Introduced/propagated dedicated `tube` package type in backend validation/export and frontend domain/UI types. + - Enforced medication-form mapping: liquid/topical -> `tube`; capsule/tablet keep blister/bottle options. + - Standardized stock logic so container semantics (`bottle` and `tube`) use direct loose/total capacity handling across planner/dashboard/detail/refill/report/scheduler. + - Added missing i18n keys for tube labels in form/report contexts (EN/DE). +- 📁 Files touched: + - `backend/src/routes/medications.ts` + - `backend/src/routes/export.ts` + - `backend/src/routes/refills.ts` + - `backend/src/routes/planner.ts` + - `backend/src/routes/share.ts` + - `backend/src/services/reminder-scheduler.ts` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/hooks/useRefill.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/pages/DashboardPage.tsx` + - `frontend/src/pages/PlannerPage.tsx` + - `frontend/src/components/MedDetailModal.tsx` + - `frontend/src/components/ReportModal.tsx` + - `frontend/src/utils/stock.ts` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Tests are owned by `@testing-manager`; no test execution was performed in this step. + +### 2026-02-27 (rest_api_med_overview plan improved) + +- 🧩 Task: Improve `doku/feat/rest_api_med_overview.md` based on review findings. +- ✅ Decisions: + - Added missing mandatory test workstream (backend, frontend, e2e). + - Corrected rate-limit implementation target to existing architecture (`share.ts` + plugin in `backend/src/index.ts`). + - Clarified response contract details: token format validation, `Cache-Control: no-store`, date format (`YYYY-MM-DD`), `shareStockStatus=false` nulling behavior. + - Clarified image strategy for v1: reuse existing image filename + `/api/images/...` flow (no new share image proxy endpoint in this phase). + - Updated effort estimate and explicitly recommended PR split due to scope size. +- 📁 Files touched: + - `doku/feat/rest_api_med_overview.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - If strict 500-line PR target must be enforced, execute as 3 linked PRs. + +### 2026-02-27 (review of shared overview API plan completeness) + +- 🧩 Task: Review `doku/feat/rest_api_med_overview.md` for quality and completeness. +- ✅ Decisions: + - Plan is directionally good, but not complete for implementation-readiness. + - Critical gaps identified: missing explicit test workstream, incorrect/unclear target file for rate-limit wiring (`backend/src/app.ts` does not exist), and unresolved image-delivery contract for share overview payload. + - Confirmed project uses `backend/src/index.ts` for Fastify plugin registration and already has `@fastify/rate-limit` registered globally. + - Confirmed share tokens are generated via `randomBytes(8).toString("hex")` (16 hex chars), so token-format checklist is consistent with current implementation. +- 📁 Files touched: + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Plan should be revised to include concrete backend/frontend/e2e tests and exact implementation locations before execution. + +### 2026-02-27 (auth loading/error screen follows light/dark theme) + +- 🧩 Task: Ensure the loading/connection screens shown before main app mount respect the selected theme. +- ✅ Decisions: + - Implemented theme resolution directly in `App.tsx` for pre-auth screens (`loading`, `authError`, `!authState`). + - Read `localStorage.theme` and support `light`, `dark`, and `system` (matchMedia fallback). + - Applied resolved theme via `data-theme` on the auth container so CSS variables immediately match the chosen theme. +- 📁 Files touched: + - `frontend/src/App.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - None. + +### 2026-02-27 (testing handoff + auth/registration env dependency clarification) + +- 🧩 Task: User requested broad Playwright improvements (auth setup fallback, planner logic assertions, lifecycle integration, retry robustness, performance timeline tests) and asked whether login/registration behavior depends on env flags. +- ✅ Decisions: + - Applied governance rule from `.github/skills/medassist-testing-handoff/SKILL.md`: test planning/writing/execution must be delegated to `@testing-manager`. + - Confirmed env dependency chain: + - Backend source of truth: `getAuthState()` in `backend/src/plugins/auth.ts`. + - `authEnabled` comes from `AUTH_ENABLED`. + - `registrationEnabled` is `REGISTRATION_ENABLED || !hasUsers`. + - `formLoginEnabled` is `needsSetup || (AUTH_ENABLED && FORM_LOGIN_ENABLED)`. + - OIDC visibility/flow depends on `OIDC_ENABLED` (+ OIDC config vars). + - Identified why current E2E auth setup can fail in SSO-only mode: `frontend/e2e/auth.setup.ts` assumes `#username/#password` are always present. +- 📁 Files touched: + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Hand off requested Playwright implementation scope to `@testing-manager` with concrete file-level guidance. + +### 2026-02-27 (intake reminder fallback removal) + +- 🧩 Task: Make intake reminders strictly per-intake and remove medication-level fallback override. +- ✅ Decisions: + - In `backend/src/services/intake-reminder-scheduler.ts`, removed effective reminder condition `intake.intakeRemindersEnabled || med.intakeRemindersEnabled`. + - Reminder eligibility is now strictly `intake.intakeRemindersEnabled`. + - Removed medication-level fallback argument when parsing intakes for reminder checks (`parseIntakesJson(..., false)`). + - Medication prefilter now checks whether any intake has `intakeRemindersEnabled=true`. +- 📁 Files touched: + - `backend/src/services/intake-reminder-scheduler.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Legacy medications that relied only on medication-level reminder flag will no longer trigger reminders until intake-level flags are enabled. + +### 2026-02-27 (remove legacy `medassist.db` leftovers) + +- 🧩 Task: Investigate why `backend/data/medassist.db` and `data/medassist.db` existed and remove old-path remnants. +- ✅ Decisions: + - Verified runtime DB path is `medassist-ng.db` via `getDbPaths()` in `backend/src/db/db-utils.ts`; no production code references `medassist.db`. + - Found remaining string references only in `backend/src/test/db-client.test.ts` mocks/assertions. + - Updated those test references to `medassist-ng.db` and removed legacy files `backend/data/medassist.db` and `data/medassist.db` from disk. +- 📁 Files touched: + - `backend/src/test/db-client.test.ts` + - `backend/data/medassist.db` (removed) + - `data/medassist.db` (removed) + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - None; global search shows no remaining `medassist.db` code references. + ### 2026-02-27 (split-and-ship all pending local changes) - 🧩 Task: Split one large local working tree into coherent PRs and merge all to `main` end-to-end. @@ -363,3 +964,51 @@ Use this block for each meaningful task: - `doku/report.md` - 🔜 Follow-up/open points: - `frontend check` remains blocked by unrelated TypeScript errors in other files (outside MedDetailModal test scope). + +### 2026-02-27 (package types plan decision lock) + +- 🧩 Task: Capture user-approved decisions for lifecycle derivation and V1 scope in package type planning. +- ✅ Decisions: + - `ongoing` is derived from `endDate == null` and should not be stored as an explicit lifecycle value. + - V1 form scope remains exactly 4 forms (`Capsule`, `Tablet`, `Liquid`, `Topical`) without subforms. + - `autoMarkObsoleteAfterEndDate` default is `true`. + - Updated wording to remove ambiguous `restore` label in lifecycle section. +- 📁 Files touched: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Reflect these locked decisions in implementation tickets before coding starts. + +### 2026-02-27 (implemented V1 medication form model in web app) + +- 🧩 Task: Implement the approved V1 medication-form concept end-to-end on website (desktop + mobile) with persistence. +- ✅ Decisions: + - Added persisted fields: `medicationForm`, `pillForm`, `lifecycleCategory`, `medicationEndDate`, `autoMarkObsoleteAfterEndDate`. + - Kept `ongoing` derived only: no explicit stored `ongoing` value introduced. + - Enforced validation rules: + - `pillForm` required for capsule/tablet medication forms. + - fractional intake forbidden for capsule. + - liquid/topical restricted to bottle container. + - Implemented automatic obsolete marking during medication fetch when end date has passed and auto-mark toggle is enabled. + - Preserved desktop/mobile parity by adding identical form controls to `MedicationsPage` and `MobileEditModal`. + - Updated export/import format to include new metadata (`EXPORT_VERSION` bumped to `1.2`). +- 📁 Files touched: + - `backend/src/db/schema.ts` + - `backend/src/db/db-utils.ts` + - `backend/src/routes/medications.ts` + - `backend/src/routes/export.ts` + - `backend/drizzle/0011_stiff_randall_flagg.sql` + - `backend/drizzle/meta/_journal.json` + - `backend/drizzle/meta/0011_snapshot.json` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `frontend/src/test/components/MobileEditModal.test.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- 🔜 Follow-up/open points: + - Full repo-wide frontend `npm run check` still reports unrelated pre-existing e2e formatting issues outside this scope. diff --git a/doku/report.md b/doku/report.md index 5aaceef..c966cba 100644 --- a/doku/report.md +++ b/doku/report.md @@ -28,6 +28,728 @@ For each task, add: ## Entries +### 2026-02-28 (PR #356 backend CI failure triage) + +- **🧩 Scope**: Reproduce and fix failing `Backend Tests` check on branch `feat/package-amount-backend`. +- **🛠️ What changed**: + - Reproduced failure locally with `CI=true npm run test:run` in `backend` (`15` failing tests, all returning `500` from planner usage endpoint paths). + - Root cause: missing utility export used at runtime by `POST /medications/usage`. + - Caller: `backend/src/routes/medications.ts` (`normalizeIntakeUsageForStock(...)`) + - Missing implementation/export in: `backend/src/utils/scheduler-utils.ts` + - Added minimal `normalizeIntakeUsageForStock(...)` helper to return a validated finite positive numeric usage value. +- **📁 Files touched**: + - `backend/src/utils/scheduler-utils.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔬 Validation run**: + - `cd backend && CI=true npx vitest run src/test/integration.test.ts src/test/stock-semantics-parity.test.ts src/test/e2e-routes.test.ts` -> **3 files passed, 143 tests passed** + - `cd backend && CI=true npm run test:run` -> **21 files passed, 572 tests passed** +- **🔜 Follow-ups**: + - None. + +### 2026-02-28 (Stacked Branch Validation + Compatibility Fixes) + +- **🧩 Scope**: Validate stacked commits for package amount, liquid intake units, and topical no-depletion behavior; fix test compatibility blockers. +- **🛠️ What changed**: + - Verified stack lineage on current branch: + - `7ebd253` (`feat/package-amount-backend`) + - `3954ed2` (`feat/tube-ui-simplification`) + - `e689720` (`feat/liquid-intake-units-conversion`) + - `f9deb1b` (`feat/topical-no-depletion-planner`) + - Fixed backend integration fixture drift by updating in-memory `medications` schemas to current column set (including medication form + package amount fields), which removed widespread `500` failures in backend tests. + - Updated focused Playwright specs to match current UI semantics: + - usage label selectors now support dynamic usage labels (not pills-only) + - lifecycle edit flow now uses robust row/action selectors and current `Commercial Name` label + - planner stock assertion now validates blister+loose-pill breakdown format + - Removed fixable E2E lint warnings in lifecycle spec (unused import/variable). +- **📁 Files touched**: + - `backend/src/test/integration.test.ts` + - `backend/src/test/planner.test.ts` + - `backend/src/test/e2e-routes.test.ts` + - `frontend/e2e/medication-edit.spec.ts` + - `frontend/e2e/medication-lifecycle.spec.ts` + - `frontend/e2e/planner-data.spec.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔬 Validation run**: + - `npm run lint` (root): backend clean; frontend reports 12 pre-existing `noNestedTernary` warnings in `MedicationsPage.tsx`/`ReportModal.tsx`; no new warning introduced by this change. + - `cd backend && CI=true npm run test:run`: **21 files passed, 572 tests passed**. + - `cd frontend && CI=true npm run test:run`: **42 files passed, 775 tests passed**. + - `cd frontend && PLAYWRIGHT_HTML_OPEN=never PLAYWRIGHT_WORKERS=1 npm run test:e2e -- --workers=1 e2e/medication-edit.spec.ts e2e/medication-lifecycle.spec.ts e2e/planner-data.spec.ts`: **25 passed, 0 failed**. +- **🔜 Follow-ups**: + - Optional dedicated cleanup PR for remaining frontend `noNestedTernary` warnings. + +### 2026-02-28 (UX update: no +/- for tube/liquid package amount) + +- **🧩 Scope**: Simplify package amount input for non-tablet package types. +- **🛠️ What changed**: + - Replaced `+/-` stepper controls with a single numeric input field for: + - `Tube -> Amount per tube` + - `Liquid container -> Package amount` + - Units remain fixed and non-editable: + - `Tube -> g` + - `Liquid container -> ml` + - Added mobile modal regression tests to verify plain input behavior and fixed unit selectors. +- **📁 Files touched**: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/test/components/MobileEditModal.test.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔬 Validation run**: + - `runTests`: `frontend/src/test/components/MobileEditModal.test.tsx` -> `59 passed, 0 failed` + - `get_errors`: no diagnostics errors on touched files +- **🔜 Follow-ups**: + - Optional: refactor pre-existing `noNestedTernary` warnings in `MedicationsPage.tsx` in a dedicated cleanup task. + +### 2026-02-28 (Tests updated for strict tube/liquid unit behavior) + +- **🧩 Scope**: Ensure automated tests cover the new unit rules (`tube -> g`, `liquid_container -> ml`). +- **🛠️ What changed**: + - Updated existing `useMedicationForm` tests to explicitly check `packageAmountUnit="ml"` for liquid-container defaults and lock behavior. + - Added new regression test: `tube` always enforces `packageAmountUnit="g"`, even if an `ml` change is attempted. + - Added new regression test: legacy `tube` records with `packageAmountUnit="ml"` are normalized to `g` during edit mapping. + - Refactored touched source code for lint compliance (`noNestedTernary`) in modified files. +- **📁 Files touched**: + - `frontend/src/test/hooks/useMedicationForm.test.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔬 Validation run**: + - `runTests`: `frontend/src/test/hooks/useMedicationForm.test.ts` -> `25 passed, 0 failed` + - `biome check` (focused): touched files -> clean +- **🔜 Follow-ups**: + - Optional backend guard to reject `tube+ml` payloads server-side. + +### 2026-02-28 (Domain fix: tube can no longer use `ml`) + +- **🧩 Scope**: Enforce correct measurement semantics for tube medications. +- **🛠️ What changed**: + - Tube forms no longer allow selecting `ml`. + - Tube amount unit is now fixed to `g` in desktop and mobile edit flows. + - Existing tube records are normalized to `g` when opened in edit mode. + - Save payload now enforces: + - `tube -> packageAmountUnit = g` + - `liquid_container -> packageAmountUnit = ml` +- **📁 Files touched**: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/hooks/useMedicationForm.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: add backend route-level guard to reject `tube+ml` for full server-side protection. + +### 2026-02-28 (Tube package made simple: `1 x 150 g`) + +- **🧩 Scope**: Remove confusing/duplicated stock inputs for tube medications. +- **🛠️ What changed**: + - Tube stock section was simplified in desktop and mobile forms. + - For `Package Type = Tube`, the form now shows: + - `Tubes` + - `Amount per tube` (`g`/`ml`) + - computed total amount (`Tubes * Amount per tube`) + - Removed tube-specific `Total Amount` / `Current Amount` stepper inputs that allowed conflicting values. + - Save logic now persists tube amounts consistently from the simple model (`packCount * packageAmountValue`). + - Added new localized labels in EN/DE for tube-specific fields. +- **📁 Files touched**: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: adjust read-only views to always surface the same multiplication format for tube medications. + +### 2026-02-28 (UI consistency: start date now shows optional) + +- **🧩 Scope**: Align date-input hint text for medication dates. +- **🛠️ What changed**: + - Added `optional` placeholder to `Medication Start Date` in desktop and mobile edit forms. + - This is a display-only consistency fix; start date validation behavior is unchanged (still optional). +- **📁 Files touched**: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - None. + +### 2026-02-28 (Implemented: liquid intake units, topical stock rule, package amount metadata) + +- **🧩 Scope**: Fully implement the approved behavior across backend/frontend for liquid measurements, topical stock handling, and package content metadata. +- **🛠️ What changed**: + - Added backend persistence + compatibility for package amount fields: + - `packageAmountValue` + - `packageAmountUnit` (`ml|g`) + - Extended intake model with `intakeUnit` (`ml|tsp|tbsp`) and applied stock conversion: + - `1 tsp = 5 ml` + - `1 tbsp = 15 ml` + - Updated stock/depletion logic so topical (`tube`) does not auto-deplete stock (metadata-only behavior), while liquid remains measurable and depleting. + - Updated export/import to carry new fields and bumped export format version to `1.3`. + - Added desktop + mobile UI controls for: + - intake unit selection on liquid-container schedules + - package amount metadata (`value + unit`) for tube/liquid-container + - Updated frontend coverage logic (including shared schedule view) to match backend conversion and topical no-depletion behavior. + - Added EN/DE translation keys for the new form labels. +- **📁 Files touched**: + - `backend/src/routes/medications.ts` + - `backend/src/services/reminder-scheduler.ts` + - `backend/src/routes/export.ts` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/utils/schedule.ts` + - `frontend/src/components/SharedSchedule.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Execute test plan via `@testing-manager` (ownership rule). + +### 2026-02-28 (Topical vs Liquid Stock Behavior Clarified) + +- **🧩 Scope**: Define practical stock behavior for topical vs liquid medications and add intake conversion for tablespoon dosing. +- **🛠️ What changed**: + - Updated `doku/package_types.md` with explicit behavior split: + - `topical`: package content is informational only (no stock depletion math in V1/V1.1) + - `liquid`: measurable stock, always deducted in canonical `ml` + - Added liquid intake conversion model: + - `ml`, `tsp`, `tbsp` intake units + - fixed conversion: `1 tsp = 5 ml`, `1 tbsp = 15 ml` + - Added implementation guidance to use metric medical conversion (`tbsp=15 ml`) and avoid regional kitchen variants. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Implement intake-unit enum + conversion pipeline in API/frontend if this concept is approved. + +### 2026-02-28 (Packaging Quantity Units Clarified) + +- **🧩 Scope**: Define how package amount should be measured for liquid and topical medications. +- **🛠️ What changed**: + - Added a new recommendation section in `doku/package_types.md` for explicit package quantity fields: + - `packageAmountValue` (number) + - `packageAmountUnit` (`ml|g`) + - Documented practical unit mapping: + - oral liquids (`liquid_container`) -> `ml` + - topical cream/ointment/gel (`tube`) -> `g` + - topical lotions/solutions -> `ml` + - Clarified that `packageAmountUnit` is separate from `doseUnit` and `strengthUnit`. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Implement model fields/migration/UI in a dedicated PR if this recommendation is approved. + +### 2026-02-28 (Liquid Container Regression Tests Executed) + +- **🧩 Scope**: Turn the `liquid_container` handoff checklist into real automated regression tests and validate the new behavior. +- **🛠️ What changed**: + - Added backend real-route tests in `backend/src/test/e2e-routes.test.ts` for: + - creating a `liquid_container` medication + - validating shared schedule stock semantics for `liquid_container` + - rejecting invalid `liquid` + non-`liquid_container` combinations + - Added frontend hook tests in `frontend/src/test/hooks/useMedicationForm.test.ts` for: + - automatic form derivation when switching to `packageType=liquid_container` + - enforcing lock behavior that keeps `medicationForm=liquid` and `doseUnit=ml` + - Fixed the in-memory backend test schema in `e2e-routes.test.ts` so current route inserts can run against the test DB without false `500` errors. + - Executed targeted test names for the new scenarios; all targeted tests passed. +- **📁 Files touched**: + - `backend/src/test/e2e-routes.test.ts` + - `frontend/src/test/hooks/useMedicationForm.test.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Expand the package/form matrix tests in backend route suites if broader hardening is desired. + +### 2026-02-28 (Testing Handoff Checklist Added) + +- **🧩 Scope**: Provide a concrete `@testing-manager` handoff for validating `liquid_container` rollout quality. +- **🛠️ What changed**: + - Added a dedicated testing section in `doku/package_types.md` with executable checks for: + - backend package/form validation matrix + - frontend desktop/mobile parity + - planner/schedule/dashboard/detail/report unit semantics (`ml` for liquid container) + - export/import/share/refill behavior + - minimum E2E scenarios and pass criteria + - Checklist is explicitly scoped to prevent regressions back to pill-only assumptions. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Execute this checklist through `@testing-manager` per repository governance. + +### 2026-02-28 (Dedicated Liquid Package Type Implemented) + +- **🧩 Scope**: Implement missing package type for liquid medications using `liquid_container` and propagate it across backend/frontend. +- **🛠️ What changed**: + - Added `liquid_container` to API/frontend package type unions and import/export validation. + - Enforced domain rules centrally: + - `liquid` must use `liquid_container` + - `topical` must use `tube` + - `capsule/tablet` cannot use `tube` or `liquid_container` + - Updated desktop + mobile medication edit flows to expose `liquid_container` and enforce correct form locking. + - Updated planner/schedule/dashboard/detail/report unit rendering so liquid container values use `ml` instead of pill wording. + - Updated refill/share/reminder/planner backend branches so container calculations include `liquid_container`. + - Added i18n labels: + - EN: `Liquid Container` + - DE: `Fluessigbehaeltnis` + - Updated `doku/package_types.md` to make `liquid_container` mapping explicit. +- **📁 Files touched**: + - `backend/src/routes/medications.ts` + - `backend/src/routes/export.ts` + - `backend/src/routes/refills.ts` + - `backend/src/routes/share.ts` + - `backend/src/routes/planner.ts` + - `backend/src/services/reminder-scheduler.ts` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/pages/PlannerPage.tsx` + - `frontend/src/pages/SchedulePage.tsx` + - `frontend/src/pages/DashboardPage.tsx` + - `frontend/src/components/MedDetailModal.tsx` + - `frontend/src/components/ReportModal.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Test execution/triage remains delegated to `@testing-manager`. + +### 2026-02-28 (Logic fix: Liquid is no longer allowed in Tube) + +- **🧩 Scope**: Correct package/form logic after identifying that `liquid` in `tube` is invalid. +- **🛠️ What changed**: + - Backend validation rules updated: + - `topical` must use `tube` + - `liquid` cannot use `tube` + - Desktop and mobile medication edit forms updated: + - when `Package Type = Tube`, only `Topical` is selectable + - `Liquid` option removed from tube-specific selector + - Form-state logic updated to keep tube selections deterministic: + - tube now forces `medicationForm=topical` + - tube defaults to `doseUnit=units` + - `doku/package_types.md` updated to reflect the corrected compatibility rules. +- **📁 Files touched**: + - `backend/src/routes/medications.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Decide in master-plan phase how `liquid` should be modeled operationally (explicit non-tube path with full UX and stock semantics). + +### 2026-02-28 (Package-type hardening implementation pass) + +- **🧩 Scope**: Implement high-impact package-type fixes immediately and reduce risk before the later master-plan rollout. +- **🛠️ What changed**: + - Hardened backend compatibility rules in `medications.ts`: + - `liquid/topical -> tube` (already present) + - added inverse guard `capsule/tablet != tube` + - Updated frontend planner/schedule wording for non-pill forms: + - usage and totals now render form-aware units for `tube` flows + - pill-weight helper is suppressed for tube flows + - Updated backend planner/reminder messaging: + - no pill-only assumption for `tube` package type + - unit labels are now package/form aware in plain-text and email table content + - Extended backend translation keys to support the updated unit terminology. + - Updated `doku/package_types.md` status snapshot to reflect this implementation progress. +- **📁 Files touched**: + - `backend/src/routes/medications.ts` + - `frontend/src/pages/PlannerPage.tsx` + - `frontend/src/pages/SchedulePage.tsx` + - `backend/src/routes/planner.ts` + - `backend/src/services/reminder-scheduler.ts` + - `backend/src/i18n/translations.ts` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Full regression suite expansion for all four forms remains open and should be handled by `@testing-manager`. + +### 2026-02-28 (Full package_types plan synchronized with current code state) + +- **🧩 Scope**: Review the complete package-types plan and update it to the latest implementation reality. +- **🛠️ What changed**: + - Added a new dated status snapshot (`2026-02-28`) with clear separation of: + - already implemented behavior + - still-open implementation gaps + - Updated scope section from generic "implement now" wording to: + - `V1 baseline (already implemented)` + - `V1 remaining work` + - Added an explicit lifecycle storage note that current persisted values are `refill_when_empty|treatment_period`, while `ongoing` is runtime-derived. + - Added progress interpretation for the 1:1 remediation sequence so already-delivered steps are handled as verify-and-align checks. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Prioritize planner/schedule/reminder wording cleanup and enforce full four-form regression coverage. + +### 2026-02-28 (1:1 remediation sequence added to package type plan) + +- **🧩 Scope**: Add the complete execution-ready implementation order directly into `doku/package_types.md`. +- **🛠️ What changed**: + - Added a new section with strict `file -> exact change -> acceptance criterion` sequencing. + - Expanded this sequence across all relevant implementation surfaces: + - backend schema/routes/services + - frontend runtime and parity-critical screens + - i18n + - backend tests, frontend tests, and e2e tests + - documentation tracking files + - Added an explicit execution gate so skipped files must be justified; otherwise the rollout is marked incomplete. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Apply the remediation sequence in code and validate each acceptance criterion per file group. + +### 2026-02-28 (Package type plan aligned and fully enumerated) + +- **🧩 Scope**: Make `doku/package_types.md` internally consistent and explicitly enumerate all impacted areas. +- **🛠️ What changed**: + - Corrected plan context to current container reality (`blister|bottle|tube`) to remove ambiguity. + - Added a full affected-file inventory grouped by: + - backend schema/routes/services + - frontend runtime surfaces + - i18n files + - backend tests + - frontend tests + - e2e specs + - documentation synchronization files + - This converts the plan into a practical implementation checklist so no affected area is accidentally skipped. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Execute code remediation based on this inventory and close gaps in planner/schedule/backend planner messaging first. + +### 2026-02-28 (Package type plan made implementation-safe) + +- **🧩 Scope**: Improve `doku/package_types.md` to prevent incomplete package/form rollouts. +- **🛠️ What changed**: + - Added a mandatory implementation coverage checklist spanning backend, frontend desktop/mobile parity, read views, i18n, import/export/share, and tests. + - Added a strict definition-of-done rule: package/form changes are incomplete unless all affected areas are updated (or explicitly marked not impacted with rationale). + - This turns the plan from descriptive guidance into an execution gate against partial implementations. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Run a targeted remediation pass for remaining pill-only wording in planner/schedule and backend planner notifications, plus matching tests. + +### 2026-02-28 (Release manager instructions cleaned up) + +- **🧩 Scope**: Remove app-feature text from release-manager agent instructions and keep the file process-oriented. +- **🛠️ What changed**: + - Replaced the concrete medication-feature release-notes example with a neutral template. + - New template keeps the expected section structure, commit-hash usage, and full-changelog format, but avoids product-specific content. +- **📁 Files touched**: + - `.github/agents/release-manager.agent.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: standardize the Breaking Changes example heading to remove emoji for full consistency with style rules. + +### 2026-02-27 (Dashboard tube stock wording correction) + +- **🧩 Scope**: Correct dashboard overview and timeline wording for `tube` medications. +- **🛠️ What changed**: + - Fixed medication overview stock cell so `tube` no longer renders as `pill/pills`. + - Tube values now render with amount units: + - `liquid` -> `ml` + - `topical` -> `applications` + - Updated timeline dose usage and total tags to use the same tube-aware units. + - Suppressed pill-weight (`mg`) helper text for tube dose rows. +- **📁 Files touched**: + - `frontend/src/pages/DashboardPage.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - None. + +### 2026-02-27 (Lowercase optional placeholder in date fields) + +- **🧩 Scope**: Make date-field placeholder text less aggressive by preventing automatic uppercase rendering. +- **🛠️ What changed**: + - Fixed inherited uppercase styling on custom date input display. + - Added explicit CSS override so placeholders like `optional` render lowercase as intended. + - Normalized letter spacing for that display text to keep the visual tone calmer. +- **📁 Files touched**: + - `frontend/src/styles/schedule-mobile-edit.css` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - None. + +### 2026-02-27 (Tube semantics in report exports) + +- **🧩 Scope**: Ensure generated medication reports do not use pill-centric wording when package type is `tube`. +- **🛠️ What changed**: + - Updated text export (`txt`/`md`) and print/PDF report generation to use amount-based wording for `tube`. + - Current stock in reports now uses tube units (`ml` or `applications`) instead of `pill/pills`. + - Total capacity row label for `tube` now uses unit-aware amount wording. + - Intake schedule rows for `tube` now render usage with amount units. + - Refill history rows for `tube` now render added quantities with amount units. + - `Dose per pill` row is now omitted for `tube` in report outputs. +- **📁 Files touched**: + - `frontend/src/components/ReportModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: run a final app-wide wording pass for dashboard/planner/schedule if complete tube terminology harmonization is desired beyond reports. + +### 2026-02-27 (Holistic package adaptation for Tube) + +- **🧩 Scope**: Ensure package UI reflects package semantics, especially for `tube` (liquid/topical), without pill-centric wording. +- **🛠️ What changed**: + - Package tab now uses amount terminology for `tube` instead of pill terminology. + - For `tube`, removed pill-specific dose field from package tab (`Dose per pill (mg)` is no longer shown). + - Total display for `tube` no longer appends `pill/pills` wording. + - Added i18n labels for amount-based stock fields (EN/DE). + - Added sensible unit defaults when choosing tube forms: + - `liquid` -> `ml` + - `topical` -> `units` +- **📁 Files touched**: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: implement distinct backend depletion math for `tube+liquid` versus `tube+topical` for full end-to-end semantic parity. + +### 2026-02-27 (Documentation correction: Liquid/Topical + Tube constraints) + +- **🧩 Scope**: Fix mismatch between implementation reality and `doku/package_types.md` constraints section. +- **🛠️ What changed**: + - Replaced outdated statement that backend/export only support `blister|bottle`. + - Documented actual supported package types: `blister|bottle|tube`. + - Documented current UI split clearly: + - `blister`/`bottle`: `pillForm` (`tablet`/`capsule`) + - `tube`: `medicationForm` (`liquid`/`topical`) + - Clarified that export/import now include tube and related metadata. +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Continue updating this document together with any future model/UI changes in the same PR. + +### 2026-02-27 (Tube form distinction restored: Liquid vs Topical) + +- **🧩 Scope**: Restore meaningful separation between liquid and cream/topical while keeping the previous pillForm simplification. +- **🛠️ What changed**: + - Reintroduced a dedicated `Medication Form` selector only when `Package Type = Tube`. + - Tube selector now offers exactly `Liquid` and `Topical` (no capsule/tablet overlap). + - Kept `Pill Form` as the only form selector for `blister`/`bottle`. + - Updated intake behavior to reflect tube form: + - `Liquid`: fractional intake enabled and ml-oriented usage label. + - `Topical`: application-oriented usage label and non-fractional behavior. + - Updated form-state logic so switching to tube defaults to `Liquid` unless an existing tube form is already set. +- **📁 Files touched**: + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: introduce explicit liquid volume stock policy in backend for stronger liquid-vs-topical operational differences. + +### 2026-02-27 (PillForm-first simplification) + +- **🧩 Scope**: Remove semantically confusing dual form selection and align UI with meaningful domain choices. +- **🛠️ What changed**: + - Removed `Medication Form` selector from desktop and mobile edit forms. + - Kept `Pill Form` as the primary form control for non-tube packages. + - Kept explicit package selection (`blister`, `bottle`, `tube`) and use it to control whether `Pill Form` is shown. + - Updated intake behavior logic in UI to use `packageType` + `pillForm` (fraction handling and usage label decision). + - Backend payload remains compatible by deriving `medicationForm` internally at save time. +- **📁 Files touched**: + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - If liquid/topical must become user-selectable again, add a dedicated control only together with visible behavior differences. + +### 2026-02-27 (Removed non-functional lifecycle option from UI) + +- **🧩 Scope**: Align medication form UX with product rule that users should only see controls that have concrete application effects. +- **🛠️ What changed**: + - Removed lifecycle dropdown (`Refill when empty` / `Treatment period`) from desktop medication form. + - Removed the same lifecycle dropdown from mobile edit modal to keep desktop/mobile parity. + - Updated package-type design doc to state lifecycle selector is intentionally hidden until lifecycle values produce distinct behavior in planner/reminder/stock logic. + - No DB/API migration changes in this step; this is a focused UX correction to remove non-functional user choice. +- **📁 Files touched**: + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Re-introduce lifecycle as visible user control only after implementing clear, user-visible behavior differences per option. + +### 2026-02-27 (Liquid/Topical packaging correction: dedicated tube type) + +- **🧩 Scope**: Apply user-requested domain correction so liquid/topical medications are not modeled as pill bottles and instead use a dedicated `tube` package type. +- **🛠️ What changed**: + - Completed backend enum/validation propagation for `tube` in medication CRUD and import/export contracts. + - Updated backend stock/planner/share/refill/scheduler logic to treat `tube` with container semantics (same stock math branch as bottle where appropriate). + - Updated frontend shared types and medication-form logic so liquid/topical default to `tube`. + - Updated desktop and mobile medication edit UIs to show tube option for liquid/topical and keep bottle option for capsule/tablet. + - Updated dashboard/planner/detail/refill/report displays and stock helpers to render/calculate tube correctly. + - Added missing translation keys for tube labels in EN/DE (`form.packageTypeTube`, `report.docTube`). + - Checked workspace diagnostics after edits: no compile/lint errors reported by VS Code diagnostics. +- **📁 Files touched**: + - `backend/src/routes/medications.ts` + - `backend/src/routes/export.ts` + - `backend/src/routes/refills.ts` + - `backend/src/routes/planner.ts` + - `backend/src/routes/share.ts` + - `backend/src/services/reminder-scheduler.ts` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/hooks/useRefill.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/pages/DashboardPage.tsx` + - `frontend/src/pages/PlannerPage.tsx` + - `frontend/src/components/MedDetailModal.tsx` + - `frontend/src/components/ReportModal.tsx` + - `frontend/src/utils/stock.ts` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Delegate targeted regression test execution (frontend/unit/e2e as needed) to `@testing-manager` per repository governance. + +### 2026-02-27 (Plan update: tokenized medication overview API) + +- **🧩 Scope**: Improve `doku/feat/rest_api_med_overview.md` to close completeness and execution gaps. +- **🛠️ What changed**: + - Added a dedicated test section with concrete required coverage for backend, frontend, and e2e. + - Fixed architecture ambiguity for rate limiting: + - use route-level limits in `backend/src/routes/share.ts` + - rely on already-registered plugin in `backend/src/index.ts` + - Tightened API contract details: + - token format validation (`^[a-f0-9]{16}$`) + - `Cache-Control: no-store` + - deterministic date format (`YYYY-MM-DD`) + - explicit `shareStockStatus=false` behavior (stock-derived fields set to `null`) + - Clarified image strategy for phase 1 (reuse existing `/api/images/...` behavior, no new share-image endpoint). + - Updated changed-files estimate and added recommendation to split implementation into 3 PRs due to size. +- **📁 Files touched**: + - `doku/feat/rest_api_med_overview.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Implement in split PRs (backend -> frontend -> e2e/docs) to stay reviewable. + +### 2026-02-27 (Review: shared overview API plan) + +- **🧩 Scope**: Quality/completeness review of `doku/feat/rest_api_med_overview.md`. +- **🛠️ What changed**: + - Reviewed plan against current backend/frontend architecture and share-token implementation. + - Found high-impact gaps to fix before implementation: + - Missing explicit test plan (backend route tests + frontend page tests + e2e flow). + - Ambiguous/non-existent target file for rate-limit setup (`backend/src/app.ts` in plan, but project uses `backend/src/index.ts`). + - Image URL contract in response example is not aligned with currently visible share routes and needs explicit endpoint/policy definition. + - Verified helpful strengths: + - Reuse of existing `share_tokens` is consistent. + - Token-format expectation (`hex`, 16 chars) matches current generator. +- **📁 Files touched**: + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Update plan with concrete test tasks, precise file targets, and final image-delivery strategy before starting implementation. + +### 2026-02-27 (Loading/Error screen theme parity) + +- **🧩 Scope**: Make pre-auth loading and connection-error screens follow the selected light/dark theme. +- **🛠️ What changed**: + - Added early theme resolution in `AppRouter` for screens rendered before `AppHeader`/`useTheme` setup. + - Supports `localStorage` values `light`, `dark`, and `system` (system resolved via `prefers-color-scheme`). + - Applied `data-theme` on auth container during `loading`, `authError`, and `!authState` states. +- **📁 Files touched**: + - `frontend/src/App.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - None. + +### 2026-02-27 (Playwright request triage + auth env dependency check) + +- **🧩 Scope**: Assess requested Playwright improvements and clarify whether login/registration behavior is controlled by `.env` flags. +- **🛠️ What changed**: + - Applied repository governance: test planning/writing/execution must be delegated to `@testing-manager`. + - Verified backend auth-state logic and confirmed env-driven behavior: + - `AUTH_ENABLED` controls global auth mode. + - `REGISTRATION_ENABLED` controls registration unless first-user bootstrap path (`!hasUsers`) is active. + - `FORM_LOGIN_ENABLED` controls username/password form availability (with first-user setup override). + - `OIDC_ENABLED` controls SSO route/button availability (with OIDC config requirements). + - Confirmed E2E auth setup failure mode in SSO-only environments is caused by unconditional username/password field usage in setup. +- **📁 Files touched**: + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - `@testing-manager` should implement the requested Playwright changes (auth fallback, planner calculation assertions, lifecycle integration flow, resilient retries/waits, timeline performance scenario). + +### 2026-02-27 (Intake reminder consistency fix) + +- **🧩 Scope**: Ensure reminders are sent only when explicitly enabled on the specific intake. +- **🛠️ What changed**: + - Removed medication-level reminder fallback from intake reminder scheduling. + - Previous behavior: `intake.intakeRemindersEnabled || med.intakeRemindersEnabled` could remind disabled intakes. + - New behavior: only `intake.intakeRemindersEnabled` qualifies an intake for reminder sending. + - Updated prefilter logic so medications are considered only when at least one intake has reminders enabled. + - Backend lint verified clean after change. +- **📁 Files touched**: + - `backend/src/services/intake-reminder-scheduler.ts` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Existing legacy medications using only medication-level reminder flags must enable reminders at intake level to continue receiving reminders. + +### 2026-02-27 (Legacy DB cleanup) + +- **🧩 Scope**: Remove obsolete `medassist.db` leftovers from old storage paths and clean stale code references. +- **🛠️ What changed**: + - Verified active runtime DB path is `medassist-ng.db` (`backend/src/db/db-utils.ts`). + - Searched repository for `medassist.db` and found only legacy test references in `backend/src/test/db-client.test.ts`. + - Updated test mock/expectation paths from `medassist.db` to `medassist-ng.db`. + - Deleted obsolete local files: + - `backend/data/medassist.db` + - `data/medassist.db` + - Confirmed no remaining code references to `medassist.db`. +- **📁 Files touched**: + - `backend/src/test/db-client.test.ts` + - `backend/data/medassist.db` (removed) + - `data/medassist.db` (removed) + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - None. + ### 2026-02-27 (All pending local changes split and merged) - **🧩 Scope**: Take the full pending local change set, split into meaningful PRs, and merge everything into `main`. @@ -476,3 +1198,72 @@ For each task, add: - `doku/report.md` - **🔜 Follow-ups**: - Resolve remaining unrelated `frontend` TypeScript errors before rerunning full `npm run check` and then the targeted MedDetailModal test command. + +### 2026-02-27 (Package types plan decisions finalized) + +- **🧩 Scope**: Integrate final product decisions into `doku/package_types.md` so the V1 concept is implementation-ready. +- **🛠️ What changed**: + - Locked lifecycle handling: + - `ongoing` is a derived state (`endDate == null`), not a stored explicit lifecycle value. + - Clarified precedence that `endDate` overrides `ongoing` behavior. + - Locked V1 form scope: + - Keep exactly 4 forms (`Capsule`, `Tablet`, `Liquid`, `Topical`). + - No subforms in V1; users map variants like cream/gel to these forms. + - Locked end-date behavior: + - `autoMarkObsoleteAfterEndDate` default set to `true`. + - Improved wording quality: + - renamed lifecycle section title from "restore" to neutral "categorization". +- **📁 Files touched**: + - `doku/package_types.md` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Use these locked rules as acceptance criteria when creating implementation tasks (backend validation, frontend parity, export/import compatibility). + +### 2026-02-27 (Website implementation: V1 medication forms + lifecycle fields) + +- **🧩 Scope**: Implement approved package-type plan decisions directly in the web product (backend persistence + desktop/mobile UI). +- **🛠️ What changed**: + - Backend data model extended with new medication metadata: + - `medicationForm` (`capsule|tablet|liquid|topical`) + - `pillForm` (`tablet|capsule`) + - `lifecycleCategory` (`refill_when_empty|treatment_period`) + - `medicationEndDate` + - `autoMarkObsoleteAfterEndDate` (default `true`) + - Backward-compatible DB rollout implemented: + - schema update in `backend/src/db/schema.ts` + - alter-migration compatibility statements in `backend/src/db/db-utils.ts` + - generated Drizzle migration: `backend/drizzle/0011_stiff_randall_flagg.sql` + - API validation and DTO mapping updated in `backend/src/routes/medications.ts`: + - `pillForm` required for capsule/tablet medication forms + - fractional intake rejected for capsule + - liquid/topical constrained to bottle container + - end-date/start-date consistency validation + - Auto-obsolete behavior implemented: + - medications are auto-marked obsolete when end date is reached and `autoMarkObsoleteAfterEndDate=true` + - Export/import now includes new metadata (`backend/src/routes/export.ts`, export format version `1.2`). + - Frontend desktop + mobile parity implemented: + - new form controls in `frontend/src/pages/MedicationsPage.tsx` + - same controls in `frontend/src/components/MobileEditModal.tsx` + - dynamic intake usage labels by form (`tablet/capsule/ml/application`) + - capsule intake now blocks fractional values in UI + - Frontend typing + defaults updated (`frontend/src/types/index.ts`, `frontend/src/hooks/useMedicationForm.ts`) and i18n keys added in both languages (`frontend/src/i18n/en.json`, `frontend/src/i18n/de.json`). +- **📁 Files touched**: + - `backend/src/db/schema.ts` + - `backend/src/db/db-utils.ts` + - `backend/src/routes/medications.ts` + - `backend/src/routes/export.ts` + - `backend/drizzle/0011_stiff_randall_flagg.sql` + - `backend/drizzle/meta/_journal.json` + - `backend/drizzle/meta/0011_snapshot.json` + - `frontend/src/types/index.ts` + - `frontend/src/hooks/useMedicationForm.ts` + - `frontend/src/pages/MedicationsPage.tsx` + - `frontend/src/components/MobileEditModal.tsx` + - `frontend/src/i18n/en.json` + - `frontend/src/i18n/de.json` + - `frontend/src/test/components/MobileEditModal.test.tsx` + - `doku/memory_notes.md` + - `doku/report.md` +- **🔜 Follow-ups**: + - Optional: run full repo-wide frontend check after existing unrelated E2E formatting diffs are cleaned up.