Add records, Elijah scores, per-session notes, and PWA update prompt

App features:
- Personal-best records per metric: manually settable in Settings and
  auto-updated when a session beats them; shown in the log modal and a
  new dashboard "Personal records" card.
- Juggling now counts by 1 instead of 5.
- 1-on-1 with Elijah gains Technical Skill and Effort scores (out of 10)
  as manual inputs, plus an optional per-session note.
- Service worker now uses a controlled update flow: an in-app
  "new version ready" banner activates the update on tap and reloads.

Data model:
- category_metrics gains track_record + record; entries gains note.
- Idempotent migrations bring existing databases up to date (juggling
  step/record, Elijah score metrics) alongside the updated seed.

StartOS package:
- Bump to 0.1.2:0 with release notes.
- Build x86_64 only (drop aarch64) per deployment target.
This commit is contained in:
Keysat
2026-06-03 08:46:27 -05:00
parent 0265699504
commit 5868852686
17 changed files with 441 additions and 121 deletions
+23 -6
View File
@@ -10,7 +10,7 @@ function dayPayload(day) {
const td = db.prepare('SELECT day, notes FROM training_days WHERE day = ?').get(day)
|| { day, notes: '' };
const entries = db.prepare(
'SELECT id, category_id, created_at FROM entries WHERE day = ? ORDER BY id'
'SELECT id, category_id, note, created_at FROM entries WHERE day = ? ORDER BY id'
).all(day);
const valuesByEntry = new Map();
if (entries.length) {
@@ -42,26 +42,43 @@ export default async function entryRoutes(app) {
});
app.post('/api/entries', async (req, reply) => {
const { day, category_id, values } = req.body || {};
const { day, category_id, values, note } = req.body || {};
if (!ISO.test(day || '')) return reply.code(400).send({ error: 'Bad date' });
const cat = db.prepare('SELECT id FROM categories WHERE id = ?').get(Number(category_id));
if (!cat) return reply.code(400).send({ error: 'Unknown category' });
const newRecords = [];
const tx = db.transaction(() => {
ensureDay(day);
const { lastInsertRowid: entryId } = db.prepare(
'INSERT INTO entries (day, category_id) VALUES (?, ?)'
).run(day, cat.id);
'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) ins.run(entryId, Number(v.metric_id), Number(v.value) || 0);
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;
});
tx();
return dayPayload(day);
return { ...dayPayload(day), newRecords };
});
app.delete('/api/entries/:id', async (req, reply) => {