281 lines
9.4 KiB
C
281 lines
9.4 KiB
C
#include "trainlog/web_dashboard.h"
|
|
#include "json_writer.h"
|
|
|
|
#include <string.h>
|
|
|
|
#define RAW(value) \
|
|
do { \
|
|
if (!trainlog_json_raw(&w, (value))) \
|
|
goto overflow; \
|
|
} while (0)
|
|
#define STR(value) \
|
|
do { \
|
|
if (!trainlog_json_string(&w, (value))) \
|
|
goto overflow; \
|
|
} while (0)
|
|
#define NUM(value) \
|
|
do { \
|
|
if (!trainlog_json_size(&w, (value))) \
|
|
goto overflow; \
|
|
} while (0)
|
|
|
|
static const char *tracking_name(TrainlogTrackingMode value) {
|
|
return value == TRAINLOG_TRACKING_DURATION ? "duration" : "reps";
|
|
}
|
|
static const char *load_name(TrainlogLoadMode value) {
|
|
if (value == TRAINLOG_LOAD_EXTERNAL) {
|
|
return "external";
|
|
}
|
|
if (value == TRAINLOG_LOAD_ASSISTANCE) {
|
|
return "assistance";
|
|
}
|
|
return "none";
|
|
}
|
|
static bool point_improved(const TrainlogDashboardSnapshot *p, size_t i) {
|
|
const TrainlogExercisePerformancePoint *a, *b;
|
|
if (i == 0U) {
|
|
return false;
|
|
}
|
|
a = &p->performance[i - 1U];
|
|
b = &p->performance[i];
|
|
if (!a->has_weight && !b->has_weight) {
|
|
return b->metric_value > a->metric_value;
|
|
}
|
|
if (a->has_weight != b->has_weight) {
|
|
return false;
|
|
}
|
|
if (p->performance_load_mode == TRAINLOG_LOAD_ASSISTANCE) {
|
|
return b->weight_kg < a->weight_kg ||
|
|
(b->weight_kg == a->weight_kg && b->metric_value > a->metric_value);
|
|
}
|
|
return b->weight_kg > a->weight_kg ||
|
|
(b->weight_kg == a->weight_kg && b->metric_value > a->metric_value);
|
|
}
|
|
|
|
static bool write_zone(TrainlogJsonWriter *w0, const TrainlogWebWorkedZone *z) {
|
|
TrainlogJsonWriter w = *w0;
|
|
RAW("{\"zone_id\":");
|
|
STR(z->zone_id);
|
|
RAW(",\"label\":");
|
|
STR(z->label);
|
|
RAW(",\"session_count\":");
|
|
NUM(z->session_count);
|
|
RAW(",\"occurrence_count\":");
|
|
NUM(z->occurrence_count);
|
|
RAW(",\"set_count\":");
|
|
NUM(z->set_count);
|
|
RAW("}");
|
|
*w0 = w;
|
|
return true;
|
|
overflow:
|
|
*w0 = w;
|
|
return false;
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_dashboard_serialize(const TrainlogWebDashboardSnapshot *s,
|
|
char *out,
|
|
size_t cap,
|
|
size_t *out_size) {
|
|
TrainlogJsonWriter w;
|
|
size_t i;
|
|
if (s == NULL || out == NULL || out_size == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
*out_size = 0U;
|
|
trainlog_json_init(&w, out, cap);
|
|
RAW("{\"api_version\":1,\"data\":{\"footer\":{\"user\":");
|
|
STR(s->user_display_name);
|
|
RAW(",\"last_session_date\":");
|
|
if (s->last_session.available) {
|
|
char date[11];
|
|
(void)memcpy(date, s->last_session.started_at, 10U);
|
|
date[10] = '\0';
|
|
STR(date);
|
|
} else {
|
|
RAW("null");
|
|
}
|
|
RAW(",\"last_zones\":[");
|
|
for (i = 0U; i < s->last_session.zone_count; ++i) {
|
|
if (i > 0U) {
|
|
RAW(",");
|
|
}
|
|
STR(s->last_session.zones[i].label);
|
|
}
|
|
RAW("]},");
|
|
RAW("\"next_session\":{\"available\":false,\"reason\":");
|
|
STR(s->next_session_reason);
|
|
RAW("},");
|
|
RAW("\"activity\":{\"available\":true,\"window_days\":90,\"days\":[");
|
|
for (i = 0U; i < s->activity_count; ++i) {
|
|
const TrainlogWebActivityDay *d = &s->activity[i];
|
|
if (i > 0U) {
|
|
RAW(",");
|
|
}
|
|
RAW("{\"date\":");
|
|
STR(d->date);
|
|
RAW(",\"active\":");
|
|
RAW(d->active ? "true" : "false");
|
|
RAW(",\"session_count\":");
|
|
NUM(d->session_count);
|
|
RAW(",\"set_count\":");
|
|
NUM(d->set_count);
|
|
RAW("}");
|
|
}
|
|
RAW("]},");
|
|
RAW("\"progression\":{");
|
|
if (!s->progression.has_performance) {
|
|
RAW("\"available\":false,\"reason\":\"no_comparable_performance\"");
|
|
} else {
|
|
RAW("\"available\":true,\"identity\":{\"exercise_id\":");
|
|
STR(s->progression.exercise.exercise_id);
|
|
RAW(",\"exercise_name\":");
|
|
STR(s->progression.exercise.name);
|
|
RAW(",\"equipment_id\":");
|
|
STR(s->progression.performance_equipment_id);
|
|
RAW(",\"equipment_label\":");
|
|
STR(s->progression.performance_equipment_label);
|
|
RAW(",\"tracking_mode\":");
|
|
STR(tracking_name(s->progression.performance_tracking_mode));
|
|
RAW(",\"load_mode\":");
|
|
STR(load_name(s->progression.performance_load_mode));
|
|
RAW(",\"dose\":");
|
|
if (!trainlog_json_i64(&w, (long long)s->progression.performance_dose)) {
|
|
goto overflow;
|
|
}
|
|
RAW("},\"points\":[");
|
|
for (i = 0U; i < s->progression.performance_count; ++i) {
|
|
const TrainlogExercisePerformancePoint *p = &s->progression.performance[i];
|
|
if (i > 0U) {
|
|
RAW(",");
|
|
}
|
|
RAW("{\"session_id\":");
|
|
STR(p->session_id);
|
|
RAW(",\"timestamp\":");
|
|
STR(p->started_at);
|
|
RAW(",\"metric_value\":");
|
|
if (!trainlog_json_i64(&w, (long long)p->metric_value)) {
|
|
goto overflow;
|
|
}
|
|
RAW(",\"weight_kg\":");
|
|
if (p->has_weight) {
|
|
if (!trainlog_json_double(&w, p->weight_kg)) {
|
|
goto overflow;
|
|
}
|
|
} else {
|
|
RAW("null");
|
|
}
|
|
RAW(",\"improved\":");
|
|
RAW(point_improved(&s->progression, i) ? "true" : "false");
|
|
RAW("}");
|
|
}
|
|
RAW("]");
|
|
}
|
|
RAW("},");
|
|
RAW("\"last_session\":{");
|
|
if (!s->last_session.available) {
|
|
RAW("\"available\":false,\"reason\":\"no_observable_session\"");
|
|
} else {
|
|
const TrainlogWebLastSession *l = &s->last_session;
|
|
RAW("\"available\":true,\"session_id\":");
|
|
STR(l->session_id);
|
|
RAW(",\"started_at\":");
|
|
STR(l->started_at);
|
|
RAW(",\"ended_at\":");
|
|
if (l->has_ended_at) {
|
|
STR(l->ended_at);
|
|
} else {
|
|
RAW("null");
|
|
}
|
|
RAW(",\"duration_seconds\":");
|
|
if (l->has_duration) {
|
|
if (!trainlog_json_i64(&w, (long long)l->duration_seconds)) {
|
|
goto overflow;
|
|
}
|
|
} else {
|
|
RAW("null");
|
|
}
|
|
RAW(",\"exercise_count\":");
|
|
NUM(l->exercise_count);
|
|
RAW(",\"set_count\":");
|
|
NUM(l->set_count);
|
|
RAW(",\"continuous_count\":");
|
|
NUM(l->continuous_count);
|
|
RAW(",\"max_count\":");
|
|
NUM(l->max_count);
|
|
RAW(",\"primary_zones\":[");
|
|
for (i = 0U; i < l->zone_count; ++i) {
|
|
if (i > 0U) {
|
|
RAW(",");
|
|
}
|
|
if (!write_zone(&w, &l->zones[i])) {
|
|
goto overflow;
|
|
}
|
|
}
|
|
RAW("]");
|
|
}
|
|
RAW("},");
|
|
RAW("\"max_records\":{\"available\":");
|
|
RAW(s->max_record_count > 0U ? "true" : "false");
|
|
if (s->max_record_count == 0U) {
|
|
RAW(",\"reason\":\"no_explicit_max_results\"");
|
|
}
|
|
RAW(",\"records\":[");
|
|
for (i = 0U; i < s->max_record_count; ++i) {
|
|
const TrainlogWebMaxRecord *r = &s->max_records[i];
|
|
if (i > 0U) {
|
|
RAW(",");
|
|
}
|
|
RAW("{\"session_id\":");
|
|
STR(r->session_id);
|
|
RAW(",\"entry_id\":");
|
|
STR(r->entry_id);
|
|
RAW(",\"exercise_id\":");
|
|
STR(r->exercise_id);
|
|
RAW(",\"exercise_name\":");
|
|
STR(r->exercise_name);
|
|
RAW(",\"equipment_id\":");
|
|
STR(r->equipment_id);
|
|
RAW(",\"weight_kg\":");
|
|
if (!trainlog_json_double(&w, r->weight_kg)) {
|
|
goto overflow;
|
|
}
|
|
RAW(",\"timestamp\":");
|
|
STR(r->timestamp);
|
|
RAW("}");
|
|
}
|
|
RAW("]},");
|
|
RAW("\"muscle_distribution\":{\"available\":");
|
|
RAW(s->muscle_zone_count > 0U ? "true" : "false");
|
|
if (s->muscle_zone_count == 0U) {
|
|
RAW(",\"reason\":\"no_worked_primary_zones\"");
|
|
}
|
|
RAW(",\"window_days\":30,\"primary_zones\":[");
|
|
for (i = 0U; i < s->muscle_zone_count; ++i) {
|
|
if (i > 0U) {
|
|
RAW(",");
|
|
}
|
|
if (!write_zone(&w, &s->muscle_zones[i])) {
|
|
goto overflow;
|
|
}
|
|
}
|
|
RAW("]},");
|
|
RAW("\"cardio\":{\"available\":false,\"reason\":");
|
|
STR(s->cardio_reason);
|
|
RAW("}},\"meta\":{\"partial\":");
|
|
RAW(s->partial ? "true" : "false");
|
|
RAW(",\"invalid_data\":");
|
|
RAW(s->invalid_data ? "true" : "false");
|
|
RAW(",\"generated_at\":");
|
|
STR(s->generated_at);
|
|
RAW("}}\n");
|
|
if (w.failed) {
|
|
goto overflow;
|
|
}
|
|
*out_size = w.size;
|
|
return TRAINLOG_STATUS_OK;
|
|
overflow:
|
|
if (cap > 0U) {
|
|
out[0] = '\0';
|
|
}
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|