#include "trainlog/web_prepared_items.h" #include #include #include #include #include "database_internal.h" static bool copy_column(sqlite3_stmt *statement, int column, char *output, size_t capacity) { const unsigned char *text; int bytes; if (sqlite3_column_type(statement, column) == SQLITE_NULL) { output[0] = '\0'; return true; } if (sqlite3_column_type(statement, column) != SQLITE_TEXT) { return false; } text = sqlite3_column_text(statement, column); bytes = sqlite3_column_bytes(statement, column); if (text == NULL || bytes < 0 || (size_t)bytes >= capacity) { return false; } (void)memcpy(output, text, (size_t)bytes); output[bytes] = '\0'; return true; } static bool read_count(sqlite3_stmt *statement, int column, size_t *output) { sqlite3_int64 value; if (sqlite3_column_type(statement, column) != SQLITE_INTEGER) { return false; } value = sqlite3_column_int64(statement, column); if (value < 0 || (uint64_t)value > (uint64_t)SIZE_MAX) { return false; } *output = (size_t)value; return true; } static bool format_now(char output[TRAINLOG_TIMESTAMP_MAX + 1U]) { time_t now = time(NULL); struct tm utc; return now != (time_t)-1 && gmtime_r(&now, &utc) != NULL && strftime(output, TRAINLOG_TIMESTAMP_MAX + 1U, "%Y-%m-%dT%H:%M:%SZ", &utc) > 0U; } static TrainlogStatus load_ai_proposals( TrainlogDatabase *database, TrainlogWebPreparedItems *output) { static const char SQL[] = "SELECT d.draft_id,COALESCE(d.title,''),COALESCE(d.planned_for,'')," "COUNT(e.id),CASE WHEN d.published_at IS NULL THEN 'pending_publication' ELSE 'published' " "END FROM ai_session_drafts d LEFT JOIN ai_session_draft_entries e ON " "e.draft_row_id=d.id GROUP BY d.id ORDER BY COALESCE(d.planned_for,'9999-12-31')," "d.created_at,d.draft_id LIMIT ?1;"; sqlite3_stmt *statement = NULL; int step; if (sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL) != SQLITE_OK || sqlite3_bind_int64( statement, 1, (sqlite3_int64)(TRAINLOG_WEB_PREPARED_ITEM_CAPACITY + 1U)) != SQLITE_OK) { goto fail; } while ((step = sqlite3_step(statement)) == SQLITE_ROW) { TrainlogWebPreparedItem *item; if (output->item_count == TRAINLOG_WEB_PREPARED_ITEM_CAPACITY) { output->partial = true; continue; } item = &output->items[output->item_count]; item->kind = TRAINLOG_WEB_PREPARED_AI_PROPOSAL; if (!copy_column(statement, 0, item->identity, sizeof(item->identity)) || !copy_column(statement, 1, item->title, sizeof(item->title)) || !copy_column(statement, 2, item->planned_for, sizeof(item->planned_for)) || !read_count(statement, 3, &item->occurrence_count) || !copy_column(statement, 4, item->state, sizeof(item->state))) { goto fail; } (void)snprintf(item->provenance, sizeof(item->provenance), "%s", "ai_import"); ++output->item_count; } if (step != SQLITE_DONE || sqlite3_finalize(statement) != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } return TRAINLOG_STATUS_OK; fail: if (statement != NULL) { (void)sqlite3_finalize(statement); } return TRAINLOG_STATUS_DATABASE_ERROR; } static TrainlogStatus load_execution_drafts( TrainlogDatabase *database, TrainlogWebPreparedItems *output) { static const char SQL[] = "SELECT d.session_id,d.state,d.session_type,COALESCE(substr(d.started_at,1,10),'')," "COALESCE(json_array_length(d.payload_json,'$.exercises'),0) FROM execution_drafts d " "WHERE NOT EXISTS(SELECT 1 FROM execution_draft_finalizations f WHERE " "f.session_id=d.session_id) AND NOT EXISTS(SELECT 1 FROM sync_causal_state c WHERE " "c.target_kind='execution_draft' AND c.target_id=d.session_id AND c.deleted=1) " "ORDER BY CASE d.state WHEN 'active' THEN 0 ELSE 1 END,d.session_id LIMIT ?1;"; sqlite3_stmt *statement = NULL; int step; size_t remaining = TRAINLOG_WEB_PREPARED_ITEM_CAPACITY - output->item_count; if (remaining == 0U) { return TRAINLOG_STATUS_OK; } if (sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL) != SQLITE_OK || sqlite3_bind_int64(statement, 1, (sqlite3_int64)(remaining + 1U)) != SQLITE_OK) { goto fail; } while ((step = sqlite3_step(statement)) == SQLITE_ROW) { TrainlogWebPreparedItem *item; if (output->item_count == TRAINLOG_WEB_PREPARED_ITEM_CAPACITY) { output->partial = true; continue; } item = &output->items[output->item_count]; item->kind = TRAINLOG_WEB_PREPARED_EXECUTION_DRAFT; if (!copy_column(statement, 0, item->identity, sizeof(item->identity)) || !copy_column(statement, 1, item->state, sizeof(item->state)) || !copy_column(statement, 2, item->title, sizeof(item->title)) || !copy_column(statement, 3, item->planned_for, sizeof(item->planned_for)) || !read_count(statement, 4, &item->occurrence_count)) { goto fail; } (void)snprintf(item->provenance, sizeof(item->provenance), "%s", "execution_store"); ++output->item_count; } if (step != SQLITE_DONE || sqlite3_finalize(statement) != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } return TRAINLOG_STATUS_OK; fail: if (statement != NULL) { (void)sqlite3_finalize(statement); } return TRAINLOG_STATUS_DATABASE_ERROR; } TrainlogStatus trainlog_web_prepared_items_load( TrainlogDatabase *database, TrainlogWebPreparedItems *output) { TrainlogStatus status; TrainlogStatus end_status; if (database == NULL || output == NULL) { return TRAINLOG_STATUS_INVALID_ARGUMENT; } (void)memset(output, 0, sizeof(*output)); if (!format_now(output->generated_at)) { return TRAINLOG_STATUS_SYSTEM_ERROR; } status = trainlog_database_read_snapshot_begin(database); if (status != TRAINLOG_STATUS_OK) { return status; } status = load_ai_proposals(database, output); if (status == TRAINLOG_STATUS_OK) { status = load_execution_drafts(database, output); } end_status = trainlog_database_read_snapshot_end(database, status == TRAINLOG_STATUS_OK); return status == TRAINLOG_STATUS_OK ? end_status : status; }