This commit is contained in:
2026-07-25 21:10:53 +02:00
parent e5bbc25c89
commit 6dd827b0b4
3 changed files with 509 additions and 30 deletions
+31 -28
View File
@@ -72,7 +72,7 @@
.save-pill.saved { color: var(--ok); }
.save-pill.error { color: var(--danger); }
.app { width: min(900px, 100%); min-width: 0; max-width: 100%; margin: 0 auto; padding: 12px 10px 104px; }
.app { width: min(1180px, 100%); min-width: 0; max-width: 100%; margin: 0 auto; padding: 12px 10px 104px; }
.card {
min-width: 0; max-width: 100%;
background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
@@ -117,7 +117,7 @@
.status-badge.in_progress { background: var(--accent-soft); color: var(--accent); }
.status-badge.stopped { background: color-mix(in srgb, var(--muted) 13%, var(--paper)); color: var(--muted); }
.status-badge.completed { background: color-mix(in srgb, var(--ok) 16%, var(--paper)); color: var(--ok); }
.item-status-select{width:auto;min-width:112px;padding:5px 7px;font-size:11px;font-weight:750}
.item-status-select{width:100%;min-width:0;max-width:132px;padding:5px 7px;font-size:11px;font-weight:750}
.exercise.skipped{opacity:.74}.exercise.partial{border-left-color:var(--accent)}
.skip-reason{margin-top:9px}
.notice { padding: 11px 12px; border-radius: var(--radius-sm); background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line)); font-size: 12px; margin-bottom: 10px; }
@@ -139,7 +139,7 @@
.rotation-label { padding: 9px 14px; color: var(--muted); background: var(--wash-2); font-size: 11px; font-weight: 800; letter-spacing: .04em; }
.exercise { padding: 13px 14px; border-top: 1px solid var(--line-soft); }
.exercise:first-child { border-top: 0; }
.exercise-title { display: grid; grid-template-columns: 28px minmax(0,1fr); gap: 8px; align-items: start; }
.exercise-title { display: grid; grid-template-columns: minmax(112px,132px) minmax(0,1fr); gap: 10px; align-items: start; }
.exercise.done { background: color-mix(in srgb, var(--ok) 7%, var(--paper)); }
.cue { color: var(--muted); font-size: 12px; line-height: 1.45; margin-top: 4px; }
.progression-box { margin-top: 9px; padding: 9px; background: var(--teal-soft); border: 1px solid color-mix(in srgb, var(--teal) 25%, var(--line)); border-radius: var(--radius-sm); font-size: 12px; }
@@ -284,6 +284,8 @@
.error-card { border: 1px solid color-mix(in srgb, var(--danger) 55%, var(--line)); background: color-mix(in srgb, var(--danger) 13%, var(--paper)); color: var(--danger); padding: 13px; border-radius: var(--radius); }
@media (max-width: 420px) {
.exercise-title { grid-template-columns: minmax(0,1fr); }
.item-status-select { max-width: none; }
.result-controls { grid-template-columns: minmax(0,1fr); }
.result-set-grid { grid-template-columns: repeat(3,minmax(0,1fr)); }
.result-editor { padding: 10px; }
@@ -360,6 +362,7 @@
(() => {
'use strict';
const BASE = document.body.dataset.base || '';
const RESULT_DATA_VERSION = 2;
const api = path => `${BASE}${path}`;
const state = {
plans: [], selectedId: null, plan: null, tracker: null, capabilities: {openai:false},
@@ -923,7 +926,7 @@
weightMode = 'optional';
}
return {
version: 1, mode, laterality, sides_mode: laterality === 'unilateral' ? sidesMode : 'same',
version: RESULT_DATA_VERSION, mode, laterality, sides_mode: laterality === 'unilateral' ? sidesMode : 'same',
weight_mode: weightMode,
default_sets: sets, locked_sets: lockedSets, fixed_interval: Boolean(base.fixed_interval),
work_seconds: Number(base.work_seconds) || null,
@@ -934,7 +937,7 @@
function emptyResultData(schema, sets = schema.default_sets) {
const count = schema.mode === 'none' ? 1 : Math.max(1, Math.min(20, Number(sets) || 1));
return {
version: 1, mode: schema.mode, laterality: schema.laterality,
version: RESULT_DATA_VERSION, mode: schema.mode, laterality: schema.laterality,
sides_mode: schema.laterality === 'unilateral' ? (schema.sides_mode || 'same') : 'same',
sets: count, weight_kg: null,
values: Array(count).fill(null), left_values: Array(count).fill(null), right_values: Array(count).fill(null)
@@ -946,7 +949,7 @@
const mode = ['reps','seconds','minutes','none'].includes(source.mode) ? source.mode : schema.mode;
const laterality = ['bilateral','unilateral'].includes(source.laterality) ? source.laterality : schema.laterality;
return {
version: 1, mode, laterality,
version: RESULT_DATA_VERSION, mode, laterality,
sides_mode: laterality === 'unilateral' && source.sides_mode === 'separate' ? 'separate' : 'same',
sets, weight_kg: numericOrNull(source.weight_kg),
values: ensureResultArray(source.values, sets),
@@ -956,11 +959,11 @@
}
function parseLegacyResultData(result, schema) {
const raw = String(result || '').trim();
if (!raw) return {data:null, incompatible:false};
if (!raw) return {data:null, incompatible:false, formatOverride:null};
const low = raw.toLowerCase().replace(/×/g, 'x');
const detectedMode = /(?:\d|\s)(?:min\.?|minuten)\b/i.test(low) ? 'minutes'
: /(?:\d|\s)(?:s|sek\.?|sekunden)\b/i.test(low) ? 'seconds' : 'reps';
if (schema.mode === 'none' || detectedMode !== schema.mode) return {data:null, incompatible:true};
const explicitMode = /(?:\d|\s)(?:min\.?|minuten)\b/i.test(low) ? 'minutes'
: /(?:\d|\s)(?:s|sek\.?|sekunden)\b/i.test(low) ? 'seconds' : null;
const detectedMode = explicitMode || (['reps','seconds','minutes'].includes(schema.mode) ? schema.mode : 'reps');
const weightMatch = low.match(/(\d+(?:[.,]\d+)?)\s*kg\b/i);
const weight = weightMatch ? numericOrNull(weightMatch[1]) : null;
const numberList = part => [...String(part || '').matchAll(/\d+(?:[.,]\d+)?/g)].map(match => numericOrNull(match[0])).filter(value => value !== null);
@@ -971,9 +974,12 @@
const right = numberList(cleanPart(separate[2]));
const sets = Math.max(left.length, right.length, 1);
const data = emptyResultData({...schema,laterality:'unilateral'}, sets);
data.sides_mode = 'separate'; data.weight_kg = weight;
data.mode = detectedMode; data.sides_mode = 'separate'; data.weight_kg = weight;
data.left_values = ensureResultArray(left, sets); data.right_values = ensureResultArray(right, sets);
return {data, incompatible:false};
const weightMode = weight !== null && schema.weight_mode === 'none' ? 'optional' : schema.weight_mode;
const formatOverride = data.mode !== schema.mode || data.laterality !== schema.laterality || data.sides_mode !== schema.sides_mode || weightMode !== schema.weight_mode
? {mode:data.mode,laterality:data.laterality,sides_mode:data.sides_mode,weight_mode:weightMode} : null;
return {data, incompatible:false, formatOverride};
}
let work = cleanPart(raw);
let values = [];
@@ -984,21 +990,17 @@
} else {
values = numberList(work);
}
if (!values.length) return {data:null, incompatible:false};
if (!values.length && weight === null) return {data:null, incompatible:false, formatOverride:null};
const perSide = /(?:je|pro)\s+seite|\/\s*seite/i.test(low);
if (!perSide && schema.laterality === 'unilateral' && schema.sides_mode === 'separate'
&& values.length === Math.max(1, Number(schema.default_sets) || 1) * 2) {
const sets = Math.max(1, Number(schema.default_sets) || 1);
const data = emptyResultData({...schema,laterality:'unilateral',sides_mode:'separate'}, sets);
data.sides_mode = 'separate'; data.weight_kg = weight;
data.left_values = ensureResultArray(values.filter((_, index) => index % 2 === 0), sets);
data.right_values = ensureResultArray(values.filter((_, index) => index % 2 === 1), sets);
return {data, incompatible:false};
}
const sets = Math.max(1, Math.min(20, values.length));
const data = emptyResultData({...schema,laterality: perSide ? 'unilateral' : schema.laterality}, sets);
const sets = Math.max(1, Math.min(20, values.length || Number(schema.default_sets) || 1));
const data = emptyResultData({...schema,mode:detectedMode,laterality: perSide ? 'unilateral' : schema.laterality,sides_mode:'same'}, sets);
data.mode = detectedMode;
data.sides_mode = 'same';
data.weight_kg = weight; data.values = ensureResultArray(values, sets);
return {data, incompatible:false};
const weightMode = weight !== null && schema.weight_mode === 'none' ? 'optional' : schema.weight_mode;
const formatOverride = data.mode !== schema.mode || data.laterality !== schema.laterality || data.sides_mode !== schema.sides_mode || weightMode !== schema.weight_mode
? {mode:data.mode,laterality:data.laterality,sides_mode:data.sides_mode,weight_mode:weightMode} : null;
return {data, incompatible:false, formatOverride};
}
function resultEditorModel(exercise, item, progression) {
const recommended = resolvedResultSchema(exercise, progression, item);
@@ -1016,9 +1018,10 @@
const legacy = parseLegacyResultData(item?.result || '', recommended);
if (legacy.data) {
const weightMode = numericOrNull(legacy.data.weight_kg) !== null && recommended.weight_mode === 'none' ? 'optional' : recommended.weight_mode;
return {schema:{...recommended,weight_mode:weightMode}, data:legacy.data, source:'legacy', incompatible:false};
const legacySchema = {...recommended,...(legacy.formatOverride || {}),weight_mode:legacy.formatOverride?.weight_mode || weightMode,schema_source:legacy.formatOverride ? 'session' : recommended.schema_source};
return {schema:legacySchema, data:legacy.data, source:'legacy', incompatible:false, formatOverride:legacy.formatOverride};
}
return {schema:recommended, data:emptyResultData(recommended), source:item?.result ? 'legacy-incompatible' : 'empty', incompatible:legacy.incompatible};
return {schema:recommended, data:emptyResultData(recommended), source:item?.result ? 'legacy-incompatible' : 'empty', incompatible:legacy.incompatible, formatOverride:null};
}
function formatResultData(data) {
@@ -1113,7 +1116,7 @@
const data = normalizeResultData(model.data, model.schema);
mutator(data, model.schema);
const normalized = normalizeResultData(data, {...model.schema,mode:data.mode,laterality:data.laterality,default_sets:data.sets});
if (storeFormat) item.result_format = {mode: normalized.mode, laterality: normalized.laterality, sides_mode: normalized.sides_mode, weight_mode: model.schema.weight_mode};
if (storeFormat || model.formatOverride) item.result_format = {mode: normalized.mode, laterality: normalized.laterality, sides_mode: normalized.sides_mode, weight_mode: model.schema.weight_mode};
if (normalized.mode !== 'none' && (resultHasValues(normalized) || numericOrNull(normalized.weight_kg) !== null)) {
item.result_data = normalized;
item.result = formatResultData(normalized);