Add exercise numbers, image uploads, version display, session resume, and training sparklines
- Exercise number (UF#): optional field on exercises, displayed in cards, training, and sets - Import training plan numbers via migration 005 (UPDATE by name) - Exercise images: JPG upload with multi-image support per exercise (migration 006) - Version endpoint (GET /api/v1/version) with ldflags injection in Makefile and Dockerfile - Version displayed on settings page - Session resume: GET /api/v1/sessions/active endpoint, auto-resume on training page load - Block new session while one is active (409 Conflict) - e1RM sparkline chart per exercise during training (Epley formula) - Fix CORS: add X-User-ID to allowed headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,19 @@ import (
|
||||
)
|
||||
|
||||
// CreateSession startet eine neue Trainingseinheit für einen Nutzer.
|
||||
// Gibt einen Fehler zurück wenn noch eine offene Session existiert.
|
||||
func (s *Store) CreateSession(userID, setID int64) (*model.Session, error) {
|
||||
// Prüfe ob bereits eine offene Session existiert
|
||||
active, err := s.GetActiveSession(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Aktive Session prüfen: %w", err)
|
||||
}
|
||||
if active != nil {
|
||||
return nil, fmt.Errorf("SESSION_OPEN: Es läuft bereits ein Training (%s)", active.SetName)
|
||||
}
|
||||
|
||||
var setName string
|
||||
err := s.db.QueryRow(`SELECT name FROM training_sets WHERE id = ? AND user_id = ? AND deleted_at IS NULL`, setID, userID).Scan(&setName)
|
||||
err = s.db.QueryRow(`SELECT name FROM training_sets WHERE id = ? AND user_id = ? AND deleted_at IS NULL`, setID, userID).Scan(&setName)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("Set %d existiert nicht", setID)
|
||||
}
|
||||
@@ -96,6 +106,51 @@ func (s *Store) ListSessions(userID int64, limit, offset int) ([]model.Session,
|
||||
return sessions, rows.Err()
|
||||
}
|
||||
|
||||
// GetActiveSession gibt die offene Session eines Nutzers zurück (falls vorhanden).
|
||||
func (s *Store) GetActiveSession(userID int64) (*model.Session, error) {
|
||||
var sessionID int64
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id FROM sessions
|
||||
WHERE user_id = ? AND ended_at IS NULL
|
||||
ORDER BY started_at DESC LIMIT 1`, userID,
|
||||
).Scan(&sessionID)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Aktive Session suchen: %w", err)
|
||||
}
|
||||
return s.GetSession(sessionID)
|
||||
}
|
||||
|
||||
// GetSetExercises gibt die Übungen eines Training-Sets zurück.
|
||||
func (s *Store) GetSetExercises(setID int64) ([]model.Exercise, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT e.id, e.name, e.description, e.muscle_group, e.weight_step_kg, e.exercise_number, e.created_at, e.updated_at
|
||||
FROM exercises e
|
||||
JOIN set_exercises se ON se.exercise_id = e.id
|
||||
WHERE se.set_id = ? AND e.deleted_at IS NULL
|
||||
ORDER BY se.position`, setID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set-Übungen abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var exercises []model.Exercise
|
||||
for rows.Next() {
|
||||
var e model.Exercise
|
||||
if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.MuscleGroup, &e.WeightStepKg, &e.ExerciseNumber, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("Übung scannen: %w", err)
|
||||
}
|
||||
exercises = append(exercises, e)
|
||||
}
|
||||
if exercises == nil {
|
||||
exercises = []model.Exercise{}
|
||||
}
|
||||
return exercises, rows.Err()
|
||||
}
|
||||
|
||||
// CreateLog fügt einen Satz zu einer offenen Session hinzu.
|
||||
func (s *Store) CreateLog(sessionID int64, req *model.CreateLogRequest) (*model.SessionLog, error) {
|
||||
if err := s.checkSessionOpen(sessionID); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user