Add EPA speeds, entry editing, and tap-to-type numeric inputs
- EPA Agility & Speed: add Max Speed and Max Weighted Speed (mph), decimal record-tracked metrics, per session. - Logged sessions are editable: tapping a category or a logged entry opens it pre-filled; saving updates that entry (PUT /api/entries/:id) instead of creating a duplicate. Record auto-update runs on edit too. - Every value field is now a tap-to-type number input (decimal keypad for speeds) while keeping the +/- stepper buttons; native spinners removed. New decimal metric kind; seed + idempotent migration add the EPA speed metrics. - Bump StartOS package to 0.1.5:0; service worker cache to v5.
This commit is contained in:
@@ -83,3 +83,31 @@ if (getSetting('migr_records_scores') !== 'done') {
|
||||
tx();
|
||||
setSetting('migr_records_scores', 'done');
|
||||
}
|
||||
|
||||
// One-off data migration: add Max Speed + Max Weighted Speed (record-tracked
|
||||
// decimal metrics) to the EPA Agility & Speed category.
|
||||
if (getSetting('migr_epa_speed') !== 'done') {
|
||||
const tx = db.transaction(() => {
|
||||
const epa = db.prepare("SELECT id FROM categories WHERE name = 'EPA Agility & Speed'").get();
|
||||
if (epa) {
|
||||
const addSpeed = (name) => {
|
||||
const exists = db.prepare(
|
||||
'SELECT 1 FROM category_metrics WHERE category_id = ? AND name = ?'
|
||||
).get(epa.id, name);
|
||||
if (!exists) {
|
||||
const maxOrder = db.prepare(
|
||||
'SELECT COALESCE(MAX(sort_order), -1) AS m FROM category_metrics WHERE category_id = ?'
|
||||
).get(epa.id).m;
|
||||
db.prepare(
|
||||
'INSERT INTO category_metrics (category_id, name, unit, kind, step, higher_is_better, track_record, sort_order) ' +
|
||||
"VALUES (?, ?, 'mph', 'decimal', 1, 1, 1, ?)"
|
||||
).run(epa.id, name, maxOrder + 1);
|
||||
}
|
||||
};
|
||||
addSpeed('Max Speed');
|
||||
addSpeed('Max Weighted Speed');
|
||||
}
|
||||
});
|
||||
tx();
|
||||
setSetting('migr_epa_speed', 'done');
|
||||
}
|
||||
|
||||
+45
-23
@@ -6,6 +6,31 @@ function ensureDay(day) {
|
||||
db.prepare('INSERT OR IGNORE INTO training_days (day) VALUES (?)').run(day);
|
||||
}
|
||||
|
||||
// Write an entry's metric values and bump any personal-best records they beat.
|
||||
// Pushes beaten records onto `newRecords`. Used by both create and update.
|
||||
function writeValues(entryId, values, newRecords) {
|
||||
if (!Array.isArray(values)) return;
|
||||
const ins = db.prepare('INSERT INTO entry_values (entry_id, metric_id, value) VALUES (?, ?, ?)');
|
||||
for (const v of values) {
|
||||
if (!v || v.metric_id == null) continue;
|
||||
const metricId = Number(v.metric_id);
|
||||
const value = Number(v.value) || 0;
|
||||
ins.run(entryId, metricId, value);
|
||||
|
||||
const m = db.prepare(
|
||||
'SELECT id, name, record, higher_is_better, unit FROM category_metrics WHERE id = ? AND track_record = 1'
|
||||
).get(metricId);
|
||||
if (m) {
|
||||
const beats = m.record == null
|
||||
|| (m.higher_is_better ? value > m.record : value < m.record);
|
||||
if (beats) {
|
||||
db.prepare('UPDATE category_metrics SET record = ? WHERE id = ?').run(value, metricId);
|
||||
newRecords.push({ metric_id: m.id, name: m.name, unit: m.unit, value, previous: m.record });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dayPayload(day) {
|
||||
const td = db.prepare('SELECT day, notes FROM training_days WHERE day = ?').get(day)
|
||||
|| { day, notes: '' };
|
||||
@@ -53,34 +78,31 @@ export default async function entryRoutes(app) {
|
||||
const { lastInsertRowid: entryId } = db.prepare(
|
||||
'INSERT INTO entries (day, category_id, note) VALUES (?, ?, ?)'
|
||||
).run(day, cat.id, String(note || ''));
|
||||
if (Array.isArray(values)) {
|
||||
const ins = db.prepare('INSERT INTO entry_values (entry_id, metric_id, value) VALUES (?, ?, ?)');
|
||||
for (const v of values) {
|
||||
if (!v || v.metric_id == null) continue;
|
||||
const metricId = Number(v.metric_id);
|
||||
const value = Number(v.value) || 0;
|
||||
ins.run(entryId, metricId, value);
|
||||
|
||||
// Auto-update personal-best records.
|
||||
const m = db.prepare(
|
||||
'SELECT id, name, record, higher_is_better, unit FROM category_metrics WHERE id = ? AND track_record = 1'
|
||||
).get(metricId);
|
||||
if (m) {
|
||||
const beats = m.record == null
|
||||
|| (m.higher_is_better ? value > m.record : value < m.record);
|
||||
if (beats) {
|
||||
db.prepare('UPDATE category_metrics SET record = ? WHERE id = ?').run(value, metricId);
|
||||
newRecords.push({ metric_id: m.id, name: m.name, unit: m.unit, value, previous: m.record });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entryId;
|
||||
writeValues(entryId, values, newRecords);
|
||||
});
|
||||
tx();
|
||||
return { ...dayPayload(day), newRecords };
|
||||
});
|
||||
|
||||
// Edit an existing entry: replace its values and note (and bump records).
|
||||
app.put('/api/entries/:id', async (req, reply) => {
|
||||
const id = Number(req.params.id);
|
||||
const entry = db.prepare('SELECT id, day FROM entries WHERE id = ?').get(id);
|
||||
if (!entry) return reply.code(404).send({ error: 'Not found' });
|
||||
const { values, note } = req.body || {};
|
||||
|
||||
const newRecords = [];
|
||||
const tx = db.transaction(() => {
|
||||
if (note !== undefined) {
|
||||
db.prepare('UPDATE entries SET note = ? WHERE id = ?').run(String(note || ''), id);
|
||||
}
|
||||
db.prepare('DELETE FROM entry_values WHERE entry_id = ?').run(id);
|
||||
writeValues(id, values, newRecords);
|
||||
});
|
||||
tx();
|
||||
return { ...dayPayload(entry.day), newRecords };
|
||||
});
|
||||
|
||||
app.delete('/api/entries/:id', async (req, reply) => {
|
||||
const id = Number(req.params.id);
|
||||
const row = db.prepare('SELECT day FROM entries WHERE id = ?').get(id);
|
||||
|
||||
+5
-1
@@ -26,7 +26,11 @@ const DEFAULTS = [
|
||||
{ name: 'Effort', unit: '/10', kind: 'score', step: 1 },
|
||||
] },
|
||||
{ name: 'EPA Agility & Speed', emoji: '⚡', color: '#ffc107',
|
||||
metrics: [{ name: 'Minutes', unit: 'min', kind: 'duration', step: 5 }] },
|
||||
metrics: [
|
||||
{ name: 'Minutes', unit: 'min', kind: 'duration', step: 5 },
|
||||
{ name: 'Max Speed', unit: 'mph', kind: 'decimal', step: 1, track_record: 1 },
|
||||
{ name: 'Max Weighted Speed', unit: 'mph', kind: 'decimal', step: 1, track_record: 1 },
|
||||
] },
|
||||
];
|
||||
|
||||
export function seedIfEmpty() {
|
||||
|
||||
Reference in New Issue
Block a user