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:
Keysat
2026-06-04 08:05:30 -05:00
parent 5868852686
commit cf64a2dc50
8 changed files with 138 additions and 62 deletions
+45 -23
View File
@@ -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);