diff --git a/ROADMAP.md b/ROADMAP.md index cc3c764..303aec0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,7 +17,6 @@ - [x] Interaction / event logging — structured `events` table; bot commands, workout save/update/delete, Mini App opens, and per-set additions all record events. `POST /api/events` endpoint lets the Mini App emit client-side events. Rest-timer prereq done. - [x] Staging via shipyard — Mini-App-only HTTP tenant under `shipyard_poc_bot` (slash-command bot deleted; phantom-ship's Shipyard owns Telegram polling). `fitness-bot-shipyard.service` on sunken-ship watches `origin/staging` from `/home/danny/tg_fitness_bot_shipyard`, listens on `:8081`, fronted by vps-relay Caddy at **https://b3.dannydannydanny.me**, validates initData against `shipyard_poc_bot`'s token (`EnvironmentFile=/home/danny/.secrets/shipyard_poc_bot.env`). Listed in `~/python-projects/26_shipyard/apps.json` as `b3bot-beta`. Workflow: `git push origin :staging` (auto-deploys ~15 min) → tap **B3Bot beta** in shipyard_poc_bot → test → `git push origin :main`. - [x] **feedback #9** Negative weight input — `±` sign-flip button next to the weight input handles iOS numeric keypads that have no minus key; active state indicates a negative value. -- [ ] Last-session recall — when starting an exercise, show the most recent sets/weights logged for it (and pre-fill the weight) so you have a reference for what to beat. ## Later - [ ] **#8** Workout templates — save/load favorite workouts diff --git a/db.py b/db.py index 410097b..5b834c4 100644 --- a/db.py +++ b/db.py @@ -263,39 +263,6 @@ def resolve_user_number(user_id: int, user_number: int) -> int | None: return row["id"] if row else None -def get_last_exercise(user_id: int, name: str) -> dict | None: - """Return the most recent logged entry for an exercise (case-insensitive - name match) from this user's non-deleted workouts, or None. - - The returned dict carries the exercise fields plus the parent workout's - `timestamp`, and `sets_detail` parsed back into a list. - """ - with get_db() as conn: - row = conn.execute( - """SELECT e.name, e.machine_id, e.sets, e.reps, e.weight_kg, - e.sets_detail, w.timestamp - FROM workouts w - JOIN superset_groups sg ON sg.workout_id = w.id - JOIN exercises e ON e.superset_group_id = sg.id - WHERE w.user_id = ? AND w.deleted_at IS NULL - AND LOWER(e.name) = LOWER(?) - ORDER BY w.timestamp DESC, e.id DESC - LIMIT 1""", - (user_id, name), - ).fetchone() - if not row: - return None - d = dict(row) - if d.get("sets_detail"): - try: - d["sets_detail"] = json.loads(d["sets_detail"]) - except json.JSONDecodeError: - d["sets_detail"] = [] - else: - d["sets_detail"] = [] - return d - - def get_workout_count(user_id: int) -> int: with get_db() as conn: row = conn.execute( diff --git a/server.py b/server.py index 71d2f9a..8e0ee77 100644 --- a/server.py +++ b/server.py @@ -17,7 +17,7 @@ from urllib.parse import parse_qs from aiohttp import web -from db import init_db, save_workout, get_workouts, get_workout_count, get_stats_sql, delete_workout, update_workout, export_workouts, get_user_workout_number, get_all_exercise_names, log_event, get_settings, update_settings, get_last_exercise +from db import init_db, save_workout, get_workouts, get_workout_count, get_stats_sql, delete_workout, update_workout, export_workouts, get_user_workout_number, get_all_exercise_names, log_event, get_settings, update_settings from parser import parse_workout, format_workout logging.basicConfig( @@ -283,16 +283,6 @@ async def api_get_exercise_names(request: web.Request): return web.json_response({"exercises": get_all_exercise_names()}) -@require_auth -async def api_get_last_exercise(request: web.Request): - """Return the user's most recent logged entry for a given exercise name.""" - name = request.query.get("name", "").strip() - if not name: - return web.json_response({"error": "Missing name"}, status=400) - last = get_last_exercise(request["user_id"], name) - return web.json_response({"last": last}) - - @require_auth async def api_get_stats(request: web.Request): """Return summary stats for the user.""" @@ -376,7 +366,6 @@ def create_app() -> web.Application: app.router.add_put("/api/workouts/{workout_id}", api_update_workout) app.router.add_delete("/api/workouts/{workout_id}", api_delete_workout) app.router.add_get("/api/exercises", api_get_exercise_names) - app.router.add_get("/api/exercises/last", api_get_last_exercise) app.router.add_get("/api/stats", api_get_stats) app.router.add_get("/api/export/json", api_export_json) app.router.add_get("/api/export/csv", api_export_csv) diff --git a/tests/test_db.py b/tests/test_db.py index d770e8e..82bfd88 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -253,48 +253,6 @@ class TestAllExerciseNames: assert db.get_all_exercise_names() == ["Apple", "Mango", "Zebra"] -# ── get_last_exercise ──────────────────────────────────────────── - - -class TestGetLastExercise: - def test_none_when_no_history(self, tmp_db): - assert db.get_last_exercise(1, "Bench") is None - - def test_returns_most_recent(self, tmp_db): - t = lambda d: datetime(2024, 1, d, tzinfo=timezone.utc) - db.save_workout(1, t(1), [[_make_exercise(name="Squat", weight=80.0)]]) - db.save_workout(1, t(5), [[_make_exercise(name="Squat", weight=90.0)]]) - last = db.get_last_exercise(1, "Squat") - assert last is not None - assert last["weight_kg"] == 90.0 - assert last["timestamp"].startswith("2024-01-05") - - def test_case_insensitive(self, tmp_db): - _save_simple(name="Bench Press") - assert db.get_last_exercise(1, "bench press") is not None - assert db.get_last_exercise(1, "BENCH PRESS") is not None - - def test_sets_detail_parsed(self, tmp_db): - detail = [{"reps": 8, "weight_kg": 25.0}, {"reps": 5, "weight_kg": 35.0}] - ex = { - "name": "Press", "machine_id": None, - "sets": 2, "reps": 8, "weight_kg": 25.0, - "sets_detail": detail, "raw_line": "Press: 8x25, 5x35", - } - db.save_workout(1, datetime.now(timezone.utc), [[ex]]) - last = db.get_last_exercise(1, "Press") - assert last["sets_detail"] == detail - - def test_scoped_to_user(self, tmp_db): - _save_simple(user_id=1, name="Deadlift") - assert db.get_last_exercise(2, "Deadlift") is None - - def test_ignores_deleted(self, tmp_db): - wid = _save_simple(name="Rows") - db.delete_workout(1, wid) - assert db.get_last_exercise(1, "Rows") is None - - # ── events / log_event ─────────────────────────────────────────── diff --git a/webapp/app.js b/webapp/app.js index 7f8577a..6559fac 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -125,7 +125,6 @@ function restoreDraft() { if (Array.isArray(draft.currentSets)) { draft.currentSets.forEach((s) => addSetToDOM(s.reps, s.weight_kg)); } - loadLastSession(currentExercise.name); } // Restore active tab @@ -358,72 +357,10 @@ function startExercise(name) { notesSection.classList.remove("hidden"); stopRestTimer(); syncEditorUI(); - loadLastSession(name); tg.HapticFeedback.selectionChanged(); saveDraft(); } -// ── Last-session recall ───────────────────────────────────────── -function _relativeDay(iso) { - const then = new Date(iso); - if (isNaN(then.getTime())) return ""; - const days = Math.floor((Date.now() - then.getTime()) / 86400000); - if (days <= 0) return "today"; - if (days === 1) return "yesterday"; - if (days < 7) return days + " days ago"; - if (days < 14) return "1 week ago"; - if (days < 30) return Math.floor(days / 7) + " weeks ago"; - return then.toLocaleDateString(); -} - -function _setsSummary(last) { - const detail = last.sets_detail || []; - const varied = detail.length > 0 && !detail.every( - (d) => d.reps === detail[0].reps && d.weight_kg === detail[0].weight_kg - ); - if (varied) { - return detail - .map((d) => (d.weight_kg ? `${d.reps}×${fmtWeight(d.weight_kg)}kg` : `${d.reps}`)) - .join(", "); - } - return last.weight_kg - ? `${last.sets}×${last.reps}×${fmtWeight(last.weight_kg)}kg` - : `${last.sets}×${last.reps}`; -} - -async function loadLastSession(name) { - const hint = document.getElementById("last-session-hint"); - if (hint) { - hint.classList.add("hidden"); - hint.textContent = ""; - } - try { - const data = await api("GET", "/exercises/last?name=" + encodeURIComponent(name)); - const last = data.last; - // Bail if the user has moved on to a different exercise meanwhile. - if (!last || !currentExercise || currentExercise.name !== name) return; - - if (hint) { - const when = _relativeDay(last.timestamp); - hint.textContent = "Last time: " + _setsSummary(last) + (when ? " · " + when : ""); - hint.classList.remove("hidden"); - } - - // Pre-fill the weight input with the last set's weight, but only if the - // user hasn't already started typing or logged a set. - const detail = last.sets_detail || []; - const lastWeight = detail.length - ? detail[detail.length - 1].weight_kg - : last.weight_kg; - if (lastWeight && !weightInput.value.trim() && getCurrentSets().length === 0) { - weightInput.value = String(lastWeight); - syncWeightSignUI(); - } - } catch (e) { - // Silent — recall is a convenience, never block exercise entry. - } -} - function getCurrentSets() { return Array.from(setsList.querySelectorAll(".set-entry")).map((el) => ({ reps: parseInt(el.dataset.reps), @@ -655,14 +592,6 @@ function editExercise(idx) { repsInput.value = ""; repsInput.focus(); - // The set rows in front of you are the reference here — drop any stale - // "last time" hint from an earlier startExercise. - const hint = document.getElementById("last-session-hint"); - if (hint) { - hint.classList.add("hidden"); - hint.textContent = ""; - } - stopRestTimer(); renderWorkout(); tg.HapticFeedback.selectionChanged(); diff --git a/webapp/index.html b/webapp/index.html index 16844cf..dc5f84a 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -37,7 +37,6 @@ -
diff --git a/webapp/style.css b/webapp/style.css index 06d29c2..5e5e144 100644 --- a/webapp/style.css +++ b/webapp/style.css @@ -241,15 +241,6 @@ body { font-variant-numeric: tabular-nums; } -.last-session-hint { - margin-top: 6px; - font-size: 12px; - color: var(--tg-theme-hint-color, #999); - background: var(--tg-theme-bg-color, #fff); - border-radius: 8px; - padding: 6px 10px; -} - .btn-danger { color: #e53935 !important; font-size: 12px !important;