Skip to content

Commit 38c5835

Browse files
James McHughclaude
andcommitted
Add custom exercises and GitHub-style 16-week activity grid
- exercises table gains created_by_user_id, auto_progress, deleted_at; users can create/rename/soft-delete their own customs from settings, with per-user visibility filtering and an auto-progression toggle. - Home activity tile redrawn as a 16-week (112-day) GitHub-style grid: fixed 13px squares, weekday labels (Mon/Wed/Fri), month labels, and a guaranteed-full first column anchored on Sunday. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ca7f9ec commit 38c5835

15 files changed

Lines changed: 813 additions & 92 deletions

db/migrate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ var columnAdds = []struct {
1717
table, column, decl string
1818
}{
1919
{"workouts", "completed_at", "TEXT"},
20+
{"exercises", "created_by_user_id", "INTEGER REFERENCES users(id) ON DELETE CASCADE"},
21+
{"exercises", "auto_progress", "INTEGER NOT NULL DEFAULT 1"},
22+
{"exercises", "deleted_at", "TEXT"},
2023
}
2124

2225
func Migrate(d *sql.DB) error {

db/models.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

db/queries.sql

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,82 @@ DELETE FROM sessions WHERE expires_at <= ?;
5656
-- =============================================================================
5757

5858
-- name: ListExercises :many
59-
-- Global order. Used by progression code where order does not matter.
60-
SELECT id, slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order
61-
FROM exercises ORDER BY sort_order;
59+
-- Used by progression code. Scoped to one user: includes seeded exercises
60+
-- (created_by_user_id IS NULL) and that user's own customs, excluding
61+
-- soft-deleted rows.
62+
SELECT id, slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order,
63+
created_by_user_id, auto_progress, deleted_at
64+
FROM exercises
65+
WHERE deleted_at IS NULL
66+
AND (created_by_user_id IS NULL OR created_by_user_id = CAST(sqlc.arg(user_id) AS INTEGER))
67+
ORDER BY sort_order;
6268

6369
-- name: ListExercisesForUser :many
6470
-- Per-user order: rows in user_exercise_sort_order override exercises.sort_order.
65-
-- Exercises without a per-user row fall back to the seeded default.
66-
SELECT e.id, e.slug, e.name, e.kind, e.default_sets, e.default_reps, e.default_weight_kg, e.sort_order
71+
-- Exercises without a per-user row fall back to the seeded default. Filters
72+
-- out other users' customs and soft-deleted rows.
73+
SELECT e.id, e.slug, e.name, e.kind, e.default_sets, e.default_reps, e.default_weight_kg, e.sort_order,
74+
e.created_by_user_id, e.auto_progress, e.deleted_at
6775
FROM exercises e
6876
LEFT JOIN user_exercise_sort_order uso
69-
ON uso.exercise_id = e.id AND uso.user_id = ?
77+
ON uso.exercise_id = e.id AND uso.user_id = sqlc.arg(user_id)
78+
WHERE e.deleted_at IS NULL
79+
AND (e.created_by_user_id IS NULL OR e.created_by_user_id = sqlc.arg(user_id))
7080
ORDER BY COALESCE(uso.sort_order, e.sort_order), e.id;
7181

7282
-- name: GetExerciseByID :one
73-
SELECT id, slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order
83+
-- No visibility filter at the SQL layer; handlers enforce ownership before
84+
-- mutation (custom rename/delete) and seeded vs custom is determined by the
85+
-- created_by_user_id column on the returned row.
86+
SELECT id, slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order,
87+
created_by_user_id, auto_progress, deleted_at
7488
FROM exercises WHERE id = ?;
7589

7690
-- name: UpsertExercise :exec
77-
-- sort_order is intentionally only set on initial INSERT so that user
78-
-- reordering on the settings page is not clobbered by the seed on restart.
79-
INSERT INTO exercises (slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order)
80-
VALUES (?, ?, ?, ?, ?, ?, ?)
91+
-- Used only by SeedExercises for the global seeded list. sort_order and
92+
-- auto_progress are intentionally NOT updated on conflict so that user
93+
-- reordering / a future schema-bump for auto_progress aren't clobbered by
94+
-- the seed on restart. created_by_user_id is implicitly NULL for seeded rows.
95+
INSERT INTO exercises (slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order, auto_progress)
96+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
8197
ON CONFLICT(slug) DO UPDATE SET
8298
name = excluded.name,
8399
kind = excluded.kind,
84100
default_sets = excluded.default_sets,
85101
default_reps = excluded.default_reps;
86102

103+
-- name: SetSeededAutoProgress :exec
104+
-- One-shot used by SeedExercises to align the auto_progress flag on the
105+
-- two seeded exercises that are deliberately manual (walking, dumbbell_curls).
106+
-- Only touches seeded rows (created_by_user_id IS NULL).
107+
UPDATE exercises SET auto_progress = ?
108+
WHERE slug = ? AND created_by_user_id IS NULL;
109+
110+
-- name: CreateCustomExercise :execlastid
111+
-- Inserts a user-created custom exercise. sort_order is computed by the
112+
-- caller (typically MAX(sort_order)+1 so the new exercise appears last in
113+
-- the global order; per-user reordering still applies on top).
114+
INSERT INTO exercises (slug, name, kind, default_sets, default_reps, default_weight_kg, sort_order,
115+
created_by_user_id, auto_progress)
116+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
117+
118+
-- name: MaxExerciseSortOrder :one
119+
SELECT CAST(COALESCE(MAX(sort_order), 0) AS INTEGER) AS max_sort_order FROM exercises;
120+
121+
-- name: RenameCustomExercise :exec
122+
-- Defence in depth: only the creator can rename their custom exercise.
123+
-- Seeded rows (created_by_user_id IS NULL) are never affected because the
124+
-- WHERE clause requires a matching user id.
125+
UPDATE exercises SET name = ?
126+
WHERE id = ? AND created_by_user_id = ?;
127+
128+
-- name: SoftDeleteCustomExercise :exec
129+
-- Soft-delete: the row stays for historical sets to reference but is hidden
130+
-- from new workouts and from the per-user exercise list. Only the creator
131+
-- can soft-delete their own custom.
132+
UPDATE exercises SET deleted_at = ?
133+
WHERE id = ? AND created_by_user_id = ?;
134+
87135
-- name: ClearUserExerciseSortOrder :exec
88136
DELETE FROM user_exercise_sort_order WHERE user_id = ?;
89137

db/queries.sql.go

Lines changed: 146 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

db/schema.sql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,16 @@ CREATE TABLE IF NOT EXISTS exercises (
2828
default_sets INTEGER NOT NULL,
2929
default_reps INTEGER NOT NULL,
3030
default_weight_kg REAL NOT NULL,
31-
sort_order INTEGER NOT NULL
31+
sort_order INTEGER NOT NULL,
32+
-- NULL = seeded/global; non-NULL = a user-created custom exercise visible
33+
-- only to that user.
34+
created_by_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
35+
-- 0 = manual weight only; 1 = bumps +2.5 kg after 5 successful sessions.
36+
-- Cardio is always treated as 0 regardless of this column.
37+
auto_progress INTEGER NOT NULL DEFAULT 1,
38+
-- NULL = active; non-NULL ISO timestamp = soft-deleted (hidden from new
39+
-- workouts but historical sets remain readable).
40+
deleted_at TEXT
3241
);
3342

3443
CREATE TABLE IF NOT EXISTS user_exercise_weight (

0 commit comments

Comments
 (0)