fitness: add multi-device workout sync via SSE and rest timer improvements

Enables real-time workout synchronization across devices using
Server-Sent Events and an ephemeral MongoDB document (24h TTL).
Rest timers now use absolute timestamps instead of interval-based
countdown for accurate cross-device sync. Adds +/-30s rest timer
adjust buttons.
This commit is contained in:
2026-03-19 09:44:21 +01:00
parent 292ec20320
commit c9e8e9919c
10 changed files with 814 additions and 21 deletions

100
src/models/ActiveWorkout.ts Normal file
View File

@@ -0,0 +1,100 @@
import mongoose from 'mongoose';
export interface IActiveWorkoutSet {
reps: number | null;
weight: number | null;
rpe: number | null;
completed: boolean;
}
export interface IActiveWorkoutExercise {
exerciseId: string;
sets: IActiveWorkoutSet[];
restTime: number;
}
export interface IActiveWorkout {
_id?: string;
userId: string;
version: number;
name: string;
templateId: string | null;
exercises: IActiveWorkoutExercise[];
paused: boolean;
elapsed: number;
savedAt: number;
restStartedAt: number | null;
restTotal: number;
updatedAt?: Date;
}
const ActiveWorkoutSetSchema = new mongoose.Schema({
reps: { type: Number, default: null },
weight: { type: Number, default: null },
rpe: { type: Number, default: null },
completed: { type: Boolean, default: false }
}, { _id: false });
const ActiveWorkoutExerciseSchema = new mongoose.Schema({
exerciseId: { type: String, required: true, trim: true },
sets: { type: [ActiveWorkoutSetSchema], default: [] },
restTime: { type: Number, default: 120 }
}, { _id: false });
const ActiveWorkoutSchema = new mongoose.Schema(
{
userId: {
type: String,
required: true,
unique: true,
trim: true
},
version: {
type: Number,
required: true,
default: 1
},
name: {
type: String,
required: true,
trim: true,
maxlength: 100
},
templateId: {
type: String,
default: null
},
exercises: {
type: [ActiveWorkoutExerciseSchema],
default: []
},
paused: {
type: Boolean,
default: false
},
elapsed: {
type: Number,
default: 0
},
savedAt: {
type: Number,
default: () => Date.now()
},
restStartedAt: {
type: Number,
default: null
},
restTotal: {
type: Number,
default: 0
}
},
{
timestamps: true
}
);
// Auto-delete after 24h of inactivity
ActiveWorkoutSchema.index({ updatedAt: 1 }, { expireAfterSeconds: 86400 });
export const ActiveWorkout = mongoose.model<IActiveWorkout>('ActiveWorkout', ActiveWorkoutSchema);