Set logging is now a united. Sporadic set table removed.

This commit is contained in:
AG
2025-12-05 08:55:59 +02:00
parent a632de65ea
commit 41d1d0f16a
19 changed files with 1129 additions and 1232 deletions

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@google/generative-ai": "^0.24.1",
"@prisma/adapter-better-sqlite3": "^7.1.0",
"@prisma/client": "*",
"@prisma/client": "^6.19.0",
"@types/better-sqlite3": "^7.6.13",
"bcryptjs": "*",
"better-sqlite3": "^12.5.0",

View File

@@ -11,7 +11,7 @@
"dependencies": {
"@google/generative-ai": "^0.24.1",
"@prisma/adapter-better-sqlite3": "^7.1.0",
"@prisma/client": "*",
"@prisma/client": "^6.19.0",
"@types/better-sqlite3": "^7.6.13",
"bcryptjs": "*",
"better-sqlite3": "^12.5.0",

Binary file not shown.

View File

@@ -0,0 +1,54 @@
/*
Warnings:
- You are about to drop the `SporadicSet` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropIndex
DROP INDEX "SporadicSet_userId_timestamp_idx";
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "SporadicSet";
PRAGMA foreign_keys=on;
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_WorkoutSession" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"startTime" DATETIME NOT NULL,
"endTime" DATETIME,
"userBodyWeight" REAL,
"note" TEXT,
"planId" TEXT,
"planName" TEXT,
"type" TEXT NOT NULL DEFAULT 'STANDARD',
CONSTRAINT "WorkoutSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_WorkoutSession" ("endTime", "id", "note", "planId", "planName", "startTime", "userBodyWeight", "userId") SELECT "endTime", "id", "note", "planId", "planName", "startTime", "userBodyWeight", "userId" FROM "WorkoutSession";
DROP TABLE "WorkoutSession";
ALTER TABLE "new_WorkoutSession" RENAME TO "WorkoutSession";
CREATE TABLE "new_WorkoutSet" (
"id" TEXT NOT NULL PRIMARY KEY,
"sessionId" TEXT NOT NULL,
"exerciseId" TEXT NOT NULL,
"order" INTEGER NOT NULL,
"weight" REAL,
"reps" INTEGER,
"distanceMeters" REAL,
"durationSeconds" INTEGER,
"height" REAL,
"bodyWeightPercentage" REAL,
"completed" BOOLEAN NOT NULL DEFAULT true,
"side" TEXT,
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WorkoutSet_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "WorkoutSession" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "WorkoutSet_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "Exercise" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_WorkoutSet" ("completed", "distanceMeters", "durationSeconds", "exerciseId", "id", "order", "reps", "sessionId", "side", "weight") SELECT "completed", "distanceMeters", "durationSeconds", "exerciseId", "id", "order", "reps", "sessionId", "side", "weight" FROM "WorkoutSet";
DROP TABLE "WorkoutSet";
ALTER TABLE "new_WorkoutSet" RENAME TO "WorkoutSet";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -25,7 +25,6 @@ model User {
exercises Exercise[]
plans WorkoutPlan[]
weightRecords BodyWeightRecord[]
sporadicSets SporadicSet[]
}
model BodyWeightRecord {
@@ -61,7 +60,6 @@ model Exercise {
isUnilateral Boolean @default(false)
sets WorkoutSet[]
sporadicSets SporadicSet[]
}
model WorkoutSession {
@@ -74,6 +72,7 @@ model WorkoutSession {
note String?
planId String?
planName String?
type String @default("STANDARD") // STANDARD, QUICK_LOG
sets WorkoutSet[]
}
@@ -90,8 +89,11 @@ model WorkoutSet {
reps Int?
distanceMeters Float?
durationSeconds Int?
height Float?
bodyWeightPercentage Float?
completed Boolean @default(true)
side String? // LEFT, RIGHT, or null for bilateral
timestamp DateTime @default(now())
}
model WorkoutPlan {
@@ -105,23 +107,3 @@ model WorkoutPlan {
updatedAt DateTime @updatedAt
}
model SporadicSet {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
exerciseId String
exercise Exercise @relation(fields: [exerciseId], references: [id])
weight Float?
reps Int?
distanceMeters Float?
durationSeconds Int?
height Float?
bodyWeightPercentage Float?
side String? // LEFT, RIGHT, or null for bilateral
timestamp DateTime @default(now())
note String?
@@index([userId, timestamp])
}

128
server/sporadic_backup.json Normal file
View File

@@ -0,0 +1,128 @@
[
{
"id": "afc0252b-81c8-4534-b10c-fd328ead82c8",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "LEFT",
"timestamp": "2025-12-03T21:25:04.297Z",
"note": null
},
{
"id": "e772067e-bbea-4e70-83bf-128e6a2feab4",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "RIGHT",
"timestamp": "2025-12-03T21:25:04.335Z",
"note": null
},
{
"id": "b3b86064-935d-45ee-aab2-b7cf7e1de883",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 4,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "LEFT",
"timestamp": "2025-12-03T21:34:13.194Z",
"note": null
},
{
"id": "688c19fa-2cb2-48b0-a96c-71e894047340",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 4,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "RIGHT",
"timestamp": "2025-12-03T21:34:13.226Z",
"note": null
},
{
"id": "93db2e6c-5cab-41a1-b3b4-a66e00ebca1c",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "RIGHT",
"timestamp": "2025-12-03T21:44:15.119Z",
"note": null
},
{
"id": "7e59647f-a115-47ec-9327-5d46df0e56e8",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "LEFT",
"timestamp": "2025-12-03T21:44:24.122Z",
"note": null
},
{
"id": "4dd11f30-f96b-4f9f-b6fd-1968315e06ec",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "LEFT",
"timestamp": "2025-12-03T21:53:54.535Z",
"note": null
},
{
"id": "308c4ec7-7518-45b7-a066-5db1c7e2229e",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "LEFT",
"timestamp": "2025-12-03T21:54:31.820Z",
"note": null
},
{
"id": "c03a8123-05e9-45c0-aac8-587dd6342c27",
"userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b",
"exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b",
"weight": 12,
"reps": 13,
"distanceMeters": null,
"durationSeconds": null,
"height": null,
"bodyWeightPercentage": null,
"side": "LEFT",
"timestamp": "2025-12-03T21:58:44.945Z",
"note": null
}
]

View File

@@ -6,7 +6,7 @@ import sessionRoutes from './routes/sessions';
import planRoutes from './routes/plans';
import aiRoutes from './routes/ai';
import weightRoutes from './routes/weight';
import sporadicSetsRoutes from './routes/sporadic-sets';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
@@ -63,7 +63,7 @@ app.use('/api/sessions', sessionRoutes);
app.use('/api/plans', planRoutes);
app.use('/api/ai', aiRoutes);
app.use('/api/weight', weightRoutes);
app.use('/api/sporadic-sets', sporadicSetsRoutes);
app.get('/', (req, res) => {
res.send('GymFlow AI API is running');

View File

@@ -29,7 +29,18 @@ router.get('/', async (req: any, res) => {
include: { sets: { include: { exercise: true } } },
orderBy: { startTime: 'desc' }
});
res.json(sessions);
// Map exerciseName and type onto each set for frontend convenience
const mappedSessions = sessions.map(session => ({
...session,
sets: session.sets.map(set => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
}));
res.json(mappedSessions);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
@@ -83,7 +94,11 @@ router.post('/', async (req: any, res) => {
// If creating a new active session (endTime is null), check if one already exists
if (!end) {
const active = await prisma.workoutSession.findFirst({
where: { userId, endTime: null }
where: {
userId,
endTime: null,
type: 'STANDARD' // Only check for standard sessions, not Quick Log
}
});
if (active) {
return res.status(400).json({ error: 'An active session already exists' });
@@ -149,7 +164,8 @@ router.get('/active', async (req: any, res) => {
const activeSession = await prisma.workoutSession.findFirst({
where: {
userId,
endTime: null
endTime: null,
type: 'STANDARD'
},
include: { sets: { include: { exercise: true }, orderBy: { order: 'asc' } } }
});
@@ -228,15 +244,119 @@ router.put('/active', async (req: any, res) => {
}
});
// Get today's quick log session
router.get('/quick-log', async (req: any, res) => {
try {
const userId = req.user.userId;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
},
include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } }
});
if (!session) {
return res.json({ success: true, session: null });
}
// Map exerciseName and type onto sets
const mappedSession = {
...session,
sets: session.sets.map(set => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
};
res.json({ success: true, session: mappedSession });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Log a set to today's quick log session
router.post('/quick-log/set', async (req: any, res) => {
try {
const userId = req.user.userId;
const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
// Find or create today's quick log session
let session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
}
});
if (!session) {
session = await prisma.workoutSession.create({
data: {
userId,
startTime: startOfDay,
type: 'QUICK_LOG',
note: 'Daily Quick Log'
}
});
}
// Create the set
const newSet = await prisma.workoutSet.create({
data: {
sessionId: session.id,
exerciseId,
order: 0,
weight: weight ? parseFloat(weight) : null,
reps: reps ? parseInt(reps) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
side: side || null
},
include: { exercise: true }
});
const mappedSet = {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
};
res.json({ success: true, set: mappedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Log a set to the active session
router.post('/active/log-set', async (req: any, res) => {
try {
const userId = req.user.userId;
const { exerciseId, reps, weight, distanceMeters, durationSeconds } = req.body;
const { exerciseId, reps, weight, distanceMeters, durationSeconds, side } = req.body;
// Find active session
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
where: { userId, endTime: null, type: 'STANDARD' },
include: { sets: true }
});
@@ -257,6 +377,7 @@ router.post('/active/log-set', async (req: any, res) => {
weight: weight ? parseFloat(weight) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
side: side || null,
completed: true
},
include: { exercise: true }
@@ -324,7 +445,7 @@ router.put('/active/set/:setId', async (req: any, res) => {
const { setId } = req.params;
const { reps, weight, distanceMeters, durationSeconds } = req.body;
// Find active session
// Find active session (STANDARD or QUICK_LOG)
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
@@ -358,13 +479,58 @@ router.put('/active/set/:setId', async (req: any, res) => {
}
});
// Update a set in the active session (STANDARD or QUICK_LOG)
router.patch('/active/set/:setId', async (req: any, res) => {
try {
const userId = req.user.userId;
const { setId } = req.params;
const { reps, weight, distanceMeters, durationSeconds, height, bodyWeightPercentage, side, note } = req.body;
// Find active session (STANDARD or QUICK_LOG)
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
if (!activeSession) {
return res.status(404).json({ error: 'No active session found' });
}
const updatedSet = await prisma.workoutSet.update({
where: { id: setId },
data: {
reps: reps !== undefined ? (reps ? parseInt(reps) : null) : undefined,
weight: weight !== undefined ? (weight ? parseFloat(weight) : null) : undefined,
distanceMeters: distanceMeters !== undefined ? (distanceMeters ? parseFloat(distanceMeters) : null) : undefined,
durationSeconds: durationSeconds !== undefined ? (durationSeconds ? parseInt(durationSeconds) : null) : undefined,
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
side: side !== undefined ? side : undefined,
note: note !== undefined ? note : undefined,
},
include: { exercise: true }
});
const mappedUpdatedSet = {
...updatedSet,
exerciseName: updatedSet.exercise.name,
type: updatedSet.exercise.type
};
res.json({ success: true, updatedSet: mappedUpdatedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a set from the active session
router.delete('/active/set/:setId', async (req: any, res) => {
try {
const userId = req.user.userId;
const { setId } = req.params;
// Find active session
// Find active session (STANDARD or QUICK_LOG)
const activeSession = await prisma.workoutSession.findFirst({
where: { userId, endTime: null },
});
@@ -394,7 +560,8 @@ router.delete('/active', async (req: any, res) => {
await prisma.workoutSession.deleteMany({
where: {
userId,
endTime: null
endTime: null,
type: 'STANDARD'
}
});
@@ -419,4 +586,113 @@ router.delete('/:id', async (req: any, res) => {
}
});
// Get today's quick log session
router.get('/quick-log', async (req: any, res) => {
try {
const userId = req.user.userId;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
},
include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } }
});
if (!session) {
return res.json({ success: true, session: null });
}
// Map exercise properties to sets for frontend compatibility
const mappedSession = {
...session,
sets: session.sets.map((set: any) => ({
...set,
exerciseName: set.exercise.name,
type: set.exercise.type
}))
};
res.json({ success: true, session: mappedSession });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Log a set to today's quick log session
router.post('/quick-log/set', async (req: any, res) => {
try {
const userId = req.user.userId;
const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body;
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
// Find or create today's quick log session
let session = await prisma.workoutSession.findFirst({
where: {
userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
}
});
if (!session) {
session = await prisma.workoutSession.create({
data: {
userId,
startTime: startOfDay,
type: 'QUICK_LOG',
note: 'Daily Quick Log'
}
});
}
// Create the set
const newSet = await prisma.workoutSet.create({
data: {
sessionId: session.id,
exerciseId,
order: 0, // Order not strictly enforced for quick log
weight: weight ? parseFloat(weight) : null,
reps: reps ? parseInt(reps) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
height: height ? parseFloat(height) : null,
bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null,
side: side || null,
completed: true,
timestamp: new Date()
},
include: { exercise: true }
});
const mappedSet = {
...newSet,
exerciseName: newSet.exercise.name,
type: newSet.exercise.type
};
res.json({ success: true, newSet: mappedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
export default router;

View File

@@ -1,195 +0,0 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import prisma from '../lib/prisma';
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
const authenticate = (req: any, res: any, next: any) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const decoded = jwt.verify(token, JWT_SECRET) as any;
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
router.use(authenticate);
// Get all sporadic sets for the authenticated user
router.get('/', async (req: any, res) => {
try {
const userId = req.user.userId;
const sporadicSets = await prisma.sporadicSet.findMany({
where: { userId },
include: { exercise: true },
orderBy: { timestamp: 'desc' }
});
// Map to include exercise name and type
const mappedSets = sporadicSets.map(set => ({
id: set.id,
exerciseId: set.exerciseId,
exerciseName: set.exercise.name,
type: set.exercise.type,
weight: set.weight,
reps: set.reps,
distanceMeters: set.distanceMeters,
durationSeconds: set.durationSeconds,
height: set.height,
bodyWeightPercentage: set.bodyWeightPercentage,
timestamp: set.timestamp.getTime(),
note: set.note
}));
res.json({ success: true, sporadicSets: mappedSets });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Create a new sporadic set
router.post('/', async (req: any, res) => {
try {
const userId = req.user.userId;
const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body;
if (!exerciseId) {
return res.status(400).json({ error: 'Exercise ID is required' });
}
// Verify that the exercise exists
const exercise = await prisma.exercise.findUnique({
where: { id: exerciseId }
});
if (!exercise) {
return res.status(400).json({ error: `Exercise with ID ${exerciseId} not found` });
}
const sporadicSet = await prisma.sporadicSet.create({
data: {
userId,
exerciseId,
weight: weight ? parseFloat(weight) : null,
reps: reps ? parseInt(reps) : null,
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
height: height ? parseFloat(height) : null,
bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null,
note: note || null,
side: side || null
},
include: { exercise: true }
});
const mappedSet = {
id: sporadicSet.id,
exerciseId: sporadicSet.exerciseId,
exerciseName: sporadicSet.exercise.name,
type: sporadicSet.exercise.type,
weight: sporadicSet.weight,
reps: sporadicSet.reps,
distanceMeters: sporadicSet.distanceMeters,
durationSeconds: sporadicSet.durationSeconds,
height: sporadicSet.height,
bodyWeightPercentage: sporadicSet.bodyWeightPercentage,
timestamp: sporadicSet.timestamp.getTime(),
note: sporadicSet.note,
side: sporadicSet.side
};
res.json({ success: true, sporadicSet: mappedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Update a sporadic set
router.put('/:id', async (req: any, res) => {
try {
const userId = req.user.userId;
const { id } = req.params;
const { weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body;
// Verify ownership
const existing = await prisma.sporadicSet.findFirst({
where: { id, userId }
});
if (!existing) {
return res.status(404).json({ error: 'Sporadic set not found' });
}
const updated = await prisma.sporadicSet.update({
where: { id },
data: {
weight: weight !== undefined ? (weight ? parseFloat(weight) : null) : undefined,
reps: reps !== undefined ? (reps ? parseInt(reps) : null) : undefined,
distanceMeters: distanceMeters !== undefined ? (distanceMeters ? parseFloat(distanceMeters) : null) : undefined,
durationSeconds: durationSeconds !== undefined ? (durationSeconds ? parseInt(durationSeconds) : null) : undefined,
height: height !== undefined ? (height ? parseFloat(height) : null) : undefined,
bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined,
note: note !== undefined ? note : undefined,
side: side !== undefined ? side : undefined
},
include: { exercise: true }
});
const mappedSet = {
id: updated.id,
exerciseId: updated.exerciseId,
exerciseName: updated.exercise.name,
type: updated.exercise.type,
weight: updated.weight,
reps: updated.reps,
distanceMeters: updated.distanceMeters,
durationSeconds: updated.durationSeconds,
height: updated.height,
bodyWeightPercentage: updated.bodyWeightPercentage,
timestamp: updated.timestamp.getTime(),
note: updated.note,
side: updated.side
};
res.json({ success: true, sporadicSet: mappedSet });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a sporadic set
router.delete('/:id', async (req: any, res) => {
try {
const userId = req.user.userId;
const { id } = req.params;
// Verify ownership
const existing = await prisma.sporadicSet.findFirst({
where: { id, userId }
});
if (!existing) {
return res.status(404).json({ error: 'Sporadic set not found' });
}
await prisma.sporadicSet.delete({
where: { id }
});
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
export default router;

View File

@@ -0,0 +1,21 @@
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
const prisma = new PrismaClient();
async function backup() {
try {
console.log('Starting backup...');
const sporadicSets = await prisma.sporadicSet.findMany();
const backupPath = path.join(__dirname, '../../sporadic_backup.json');
fs.writeFileSync(backupPath, JSON.stringify(sporadicSets, null, 2));
console.log(`Backed up ${sporadicSets.length} sporadic sets to ${backupPath}`);
} catch (error) {
console.error('Backup failed:', error);
} finally {
await prisma.$disconnect();
}
}
backup();

View File

@@ -0,0 +1,77 @@
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import path from 'path';
const prisma = new PrismaClient();
async function restore() {
try {
const backupPath = path.join(__dirname, '../../sporadic_backup.json');
if (!fs.existsSync(backupPath)) {
console.error('Backup file not found!');
return;
}
const sporadicSets = JSON.parse(fs.readFileSync(backupPath, 'utf-8'));
console.log(`Found ${sporadicSets.length} sporadic sets to restore.`);
for (const set of sporadicSets) {
const date = new Date(set.timestamp);
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
// Find or create a QUICK_LOG session for this day
let session = await prisma.workoutSession.findFirst({
where: {
userId: set.userId,
type: 'QUICK_LOG',
startTime: {
gte: startOfDay,
lte: endOfDay
}
}
});
if (!session) {
session = await prisma.workoutSession.create({
data: {
userId: set.userId,
startTime: startOfDay, // Use start of day as session start
type: 'QUICK_LOG',
note: 'Daily Quick Log'
}
});
console.log(`Created new QUICK_LOG session for ${startOfDay.toISOString()}`);
}
// Create the WorkoutSet
await prisma.workoutSet.create({
data: {
sessionId: session.id,
exerciseId: set.exerciseId,
order: 0, // Order doesn't matter much for sporadic sets, or we could increment
weight: set.weight,
reps: set.reps,
distanceMeters: set.distanceMeters,
durationSeconds: set.durationSeconds,
height: set.height,
bodyWeightPercentage: set.bodyWeightPercentage,
side: set.side,
timestamp: new Date(set.timestamp),
completed: true
}
});
}
console.log('Restoration complete!');
} catch (error) {
console.error('Restoration failed:', error);
} finally {
await prisma.$disconnect();
}
}
restore();