fix(analysis): honor current program dates and ceded slots
This commit is contained in:
parent
e64b9ddcd3
commit
8995843e46
3 changed files with 143 additions and 19 deletions
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## Unreleased — 0.1.7
|
||||
|
||||
- Corrected the Dashboard/Analysis active-Program next-session projection to
|
||||
select title, identity, and current date from one pending Program session.
|
||||
Recorded rescheduling dates replace imported dates, ceded slots are excluded,
|
||||
and an overdue uncompleted session remains eligible. The read-only projection
|
||||
does not change Program history, preparation priority, or synchronization.
|
||||
|
||||
- Aligned the Web front/back BODY ZONES figures with Android's normalized
|
||||
Compose Canvas silhouettes. The Web now uses Android's head, neck and 11
|
||||
region paths per face, while retaining the existing Catppuccin intensity
|
||||
|
|
|
|||
|
|
@ -646,26 +646,26 @@ static bool add_measurements(TrainlogDatabase *database,
|
|||
static bool
|
||||
add_program(TrainlogDatabase *database, yyjson_mut_doc *document, yyjson_mut_val *root) {
|
||||
static const char SQL[] =
|
||||
/* WHY: imported dates are historical after Program rescheduling.
|
||||
* CONTRACT: select one pending, non-ceded session with its current date.
|
||||
* INVARIANT: a present planning row with NULL current_for stays undated. */
|
||||
"WITH eligible AS (SELECT ps2.program_id,ps2.program_session_id,ps2.title,ps2.position,"
|
||||
"CASE WHEN pp.program_session_id IS NULL THEN ps2.planned_for ELSE pp.current_for END "
|
||||
"AS current_for FROM program_sessions ps2 LEFT JOIN program_session_planning pp ON "
|
||||
"pp.program_session_id=ps2.program_session_id LEFT JOIN program_session_executions pe2 "
|
||||
"ON pe2.program_session_id=ps2.program_session_id WHERE COALESCE(pp.state,'active')"
|
||||
"!='ceded' AND COALESCE(pe2.state,'todo') NOT IN('completed','deleted')),"
|
||||
"ranked AS (SELECT eligible.*,ROW_NUMBER() OVER (PARTITION BY program_id ORDER BY "
|
||||
"CASE WHEN current_for IS NULL THEN 1 ELSE 0 END,current_for,position,"
|
||||
"program_session_id) AS rank FROM eligible) "
|
||||
"SELECT p.program_id,p.title,COUNT(ps.program_session_id),"
|
||||
"SUM(CASE WHEN pe.state='completed' THEN 1 ELSE 0 END),"
|
||||
"(SELECT ps2.title FROM program_sessions ps2 LEFT JOIN program_session_executions pe2 ON "
|
||||
"pe2.program_session_id=ps2.program_session_id WHERE ps2.program_id=p.program_id AND "
|
||||
"COALESCE(pe2.state,'todo') NOT IN('completed','deleted') ORDER BY "
|
||||
"CASE WHEN ps2.planned_for IS NULL THEN 1 ELSE 0 END,ps2.planned_for,ps2.position LIMIT 1),"
|
||||
"(SELECT ps2.program_session_id FROM program_sessions ps2 LEFT JOIN "
|
||||
"program_session_executions pe2 ON pe2.program_session_id=ps2.program_session_id WHERE "
|
||||
"ps2.program_id=p.program_id AND COALESCE(pe2.state,'todo') NOT IN('completed','deleted') "
|
||||
"ORDER BY CASE WHEN ps2.planned_for IS NULL THEN 1 ELSE 0 END,ps2.planned_for,ps2.position "
|
||||
"LIMIT 1),"
|
||||
"(SELECT ps2.planned_for FROM program_sessions ps2 LEFT JOIN program_session_executions "
|
||||
"pe2 "
|
||||
"ON pe2.program_session_id=ps2.program_session_id WHERE ps2.program_id=p.program_id AND "
|
||||
"COALESCE(pe2.state,'todo') NOT IN('completed','deleted') ORDER BY "
|
||||
"CASE WHEN ps2.planned_for IS NULL THEN 1 ELSE 0 END,ps2.planned_for,ps2.position LIMIT 1) "
|
||||
"next.title,next.program_session_id,next.current_for "
|
||||
"FROM programs p LEFT JOIN program_sessions ps ON ps.program_id=p.program_id LEFT JOIN "
|
||||
"program_session_executions pe ON pe.program_session_id=ps.program_session_id WHERE "
|
||||
"program_session_executions pe ON pe.program_session_id=ps.program_session_id LEFT JOIN "
|
||||
"ranked next ON next.program_id=p.program_id AND next.rank=1 WHERE "
|
||||
"p.state='active' AND p.deleted_at IS NULL GROUP BY p.program_id ORDER BY p.updated_at "
|
||||
"DESC "
|
||||
"DESC,p.program_id "
|
||||
"LIMIT 1";
|
||||
sqlite3_stmt *statement = NULL;
|
||||
yyjson_mut_val *program = yyjson_mut_obj(document);
|
||||
|
|
|
|||
|
|
@ -225,7 +225,8 @@ static bool test_factual_models(void) {
|
|||
{
|
||||
yyjson_val *program = yyjson_obj_get(root, "active_program");
|
||||
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(program, "next_session_title")), "Next") == 0);
|
||||
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(program, "next_session_id")), "ps_analysis") == 0);
|
||||
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(program, "next_session_id")), "ps_analysis") ==
|
||||
0);
|
||||
CHECK(strcmp(yyjson_get_str(yyjson_obj_get(program, "next_session_planned_for")),
|
||||
"2026-09-21") == 0);
|
||||
}
|
||||
|
|
@ -250,6 +251,123 @@ static bool test_factual_models(void) {
|
|||
return true;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
return test_empty_and_periods() && test_factual_models() ? 0 : 1;
|
||||
static bool check_next_session(TrainlogDatabase *database,
|
||||
const char *expected_id,
|
||||
const char *expected_title,
|
||||
const char *expected_date) {
|
||||
yyjson_doc *document = NULL;
|
||||
yyjson_val *program;
|
||||
yyjson_val *id;
|
||||
yyjson_val *title;
|
||||
yyjson_val *date;
|
||||
|
||||
CHECK(read_snapshot(database, TRAINLOG_WEB_ANALYSIS_30_DAYS, NULL, "weight", &document));
|
||||
program = yyjson_obj_get(yyjson_doc_get_root(document), "active_program");
|
||||
CHECK(!yyjson_is_null(program));
|
||||
id = yyjson_obj_get(program, "next_session_id");
|
||||
title = yyjson_obj_get(program, "next_session_title");
|
||||
date = yyjson_obj_get(program, "next_session_planned_for");
|
||||
CHECK(expected_id == NULL ? yyjson_is_null(id)
|
||||
: yyjson_is_str(id) && strcmp(yyjson_get_str(id), expected_id) == 0);
|
||||
CHECK(expected_title == NULL
|
||||
? yyjson_is_null(title)
|
||||
: yyjson_is_str(title) && strcmp(yyjson_get_str(title), expected_title) == 0);
|
||||
CHECK(expected_date == NULL
|
||||
? yyjson_is_null(date)
|
||||
: yyjson_is_str(date) && strcmp(yyjson_get_str(date), expected_date) == 0);
|
||||
yyjson_doc_free(document);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool test_current_program_plan(void) {
|
||||
TrainlogDatabase *database = NULL;
|
||||
yyjson_doc *document = NULL;
|
||||
|
||||
CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK);
|
||||
/* WHY: source dates remain historical; current planning and execution are
|
||||
* independent facts. These rows exercise their precedence without touching
|
||||
* any user database or invoking the rescheduling command. */
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"INSERT INTO programs VALUES('pg_current','Current',NULL,'active',NULL,"
|
||||
"NULL,'2026-09-01T00:00:00Z','2026-09-01T00:00:00Z','rev','test',1,"
|
||||
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',"
|
||||
"NULL);"
|
||||
"INSERT INTO program_sessions VALUES"
|
||||
"('ps_ceded','pg_current',0,'Optional','training','2026-09-29',NULL),"
|
||||
"('ps_s2c','pg_current',1,'S2 C','training','2026-09-30',NULL),"
|
||||
"('ps_later','pg_current',2,'Later','training','2026-10-03',NULL),"
|
||||
"('ps_tie','pg_current',3,'Tie','training','2026-10-02',NULL),"
|
||||
"('ps_undated','pg_current',4,'Undated','training',NULL,NULL);",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(check_next_session(database, "ps_ceded", "Optional", "2026-09-29"));
|
||||
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"INSERT INTO program_session_planning VALUES"
|
||||
"('ps_ceded',NULL,'ceded','op_a','2026-10-01T00:00:00Z'),"
|
||||
"('ps_s2c','2026-10-02','active','op_b','2026-10-01T00:00:00Z');",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(check_next_session(database, "ps_s2c", "S2 C", "2026-10-02"));
|
||||
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"INSERT INTO program_session_executions VALUES"
|
||||
"('ps_s2c','pg_current','se_fixture_1','in_progress',"
|
||||
"'2026-10-02T12:00:00Z');",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(check_next_session(database, "ps_s2c", "S2 C", "2026-10-02"));
|
||||
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"UPDATE program_session_executions SET state='completed' "
|
||||
"WHERE program_session_id='ps_s2c';",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(check_next_session(database, "ps_tie", "Tie", "2026-10-02"));
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"INSERT INTO program_session_executions VALUES"
|
||||
"('ps_tie','pg_current','se_fixture_2','completed','2026-10-02T12:00:00Z'),"
|
||||
"('ps_later','pg_current','se_fixture_3','deleted','2026-10-03T12:00:00Z');",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(check_next_session(database, "ps_undated", "Undated", NULL));
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"INSERT INTO program_session_executions VALUES"
|
||||
"('ps_undated','pg_current','se_fixture_4','completed',"
|
||||
"'2026-10-04T12:00:00Z');",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(check_next_session(database, NULL, NULL, NULL));
|
||||
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"UPDATE programs SET state='archived' WHERE program_id='pg_current';",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(read_snapshot(database, TRAINLOG_WEB_ANALYSIS_30_DAYS, NULL, "weight", &document));
|
||||
CHECK(yyjson_is_null(yyjson_obj_get(yyjson_doc_get_root(document), "active_program")));
|
||||
yyjson_doc_free(document);
|
||||
document = NULL;
|
||||
|
||||
CHECK(sqlite3_exec(database->connection,
|
||||
"UPDATE programs SET state='active',deleted_at='2026-10-05T00:00:00Z' "
|
||||
"WHERE program_id='pg_current';",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL) == SQLITE_OK);
|
||||
CHECK(read_snapshot(database, TRAINLOG_WEB_ANALYSIS_30_DAYS, NULL, "weight", &document));
|
||||
CHECK(yyjson_is_null(yyjson_obj_get(yyjson_doc_get_root(document), "active_program")));
|
||||
yyjson_doc_free(document);
|
||||
trainlog_database_close(database);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
return test_empty_and_periods() && test_factual_models() && test_current_program_plan() ? 0 : 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue