diff --git a/playwright-report/index.html b/playwright-report/index.html index eaab490..3733c2b 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index 2249545..3aa4bac 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,6 +11,11 @@ export default defineConfig({ baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, + webServer: { + command: 'npm run dev:full', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, projects: [ { name: 'chromium', diff --git a/server/package-lock.json b/server/package-lock.json index e4afd7f..a5d0347 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,7 +13,7 @@ "@prisma/client": "^7.1.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "3.0.3", - "better-sqlite3": "^12.5.0", + "better-sqlite3": "^11.0.0", "cors": "2.8.5", "dotenv": "17.2.3", "express": "5.1.0", @@ -182,6 +182,20 @@ "better-sqlite3": "^12.4.5" } }, + "node_modules/@prisma/adapter-better-sqlite3/node_modules/better-sqlite3": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", + "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, "node_modules/@prisma/client": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.1.0.tgz", @@ -434,15 +448,15 @@ } }, "node_modules/@types/express": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", - "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^1" + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { @@ -476,13 +490,6 @@ "@types/node": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -535,25 +542,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -671,17 +666,14 @@ } }, "node_modules/better-sqlite3": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", - "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, "node_modules/binary-extensions": { @@ -1404,9 +1396,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -1417,7 +1409,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/foreground-child": { @@ -1672,28 +1668,23 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-status-codes": { @@ -2046,15 +2037,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-response": { @@ -2536,15 +2531,15 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" diff --git a/server/package.json b/server/package.json index cb94432..5176134 100644 --- a/server/package.json +++ b/server/package.json @@ -14,7 +14,7 @@ "@prisma/client": "^7.1.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "3.0.3", - "better-sqlite3": "^12.5.0", + "better-sqlite3": "^11.0.0", "cors": "2.8.5", "dotenv": "17.2.3", "express": "5.1.0", diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db new file mode 100644 index 0000000..2e64a81 Binary files /dev/null and b/server/prisma/prisma/dev.db differ diff --git a/server/prisma/prisma/dev.db-journal b/server/prisma/prisma/dev.db-journal new file mode 100644 index 0000000..c260195 Binary files /dev/null and b/server/prisma/prisma/dev.db-journal differ diff --git a/server/prisma/schema.migrate.prisma b/server/prisma/schema.migrate.prisma new file mode 100644 index 0000000..26c5cd4 --- /dev/null +++ b/server/prisma/schema.migrate.prisma @@ -0,0 +1,118 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + + + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + email String @unique + password String + role String @default("USER") // USER, ADMIN + isFirstLogin Boolean @default(true) + isBlocked Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + profile UserProfile? + sessions WorkoutSession[] + exercises Exercise[] + plans WorkoutPlan[] + weightRecords BodyWeightRecord[] +} + +model BodyWeightRecord { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + weight Float + date DateTime @default(now()) + dateStr String // YYYY-MM-DD for unique constraint + + @@unique([userId, dateStr]) +} + +model UserProfile { + id String @id @default(uuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + weight Float? + height Float? + gender String? + birthDate DateTime? + language String? @default("en") +} + +model Exercise { + id String @id @default(uuid()) + userId String? // Null means system default + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + name String + type String // STRENGTH, CARDIO, BODYWEIGHT, STATIC + bodyWeightPercentage Float? @default(0) + isArchived Boolean @default(false) + isUnilateral Boolean @default(false) + + sets WorkoutSet[] + planExercises PlanExercise[] +} + +model WorkoutSession { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + startTime DateTime + endTime DateTime? + userBodyWeight Float? + note String? + planId String? + planName String? + type String @default("STANDARD") // STANDARD, QUICK_LOG + + sets WorkoutSet[] +} + +model WorkoutSet { + id String @id @default(uuid()) + sessionId String + session WorkoutSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + exerciseId String + exercise Exercise @relation(fields: [exerciseId], references: [id]) + + order Int + weight Float? + 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 { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + name String + description String? + exercises String? // JSON string of exercise IDs (Deprecated, to be removed) + planExercises PlanExercise[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model PlanExercise { + id String @id @default(uuid()) + planId String + plan WorkoutPlan @relation(fields: [planId], references: [id], onDelete: Cascade) + exerciseId String + exercise Exercise @relation(fields: [exerciseId], references: [id]) + order Int + isWeighted Boolean @default(false) +} diff --git a/server/src/index.ts b/server/src/index.ts index e02c2a7..212991d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,3 +1,4 @@ +import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import authRoutes from './routes/auth'; diff --git a/server/src/lib/prisma.ts b/server/src/lib/prisma.ts index f9e70d2..849982e 100644 --- a/server/src/lib/prisma.ts +++ b/server/src/lib/prisma.ts @@ -1,7 +1,5 @@ import { PrismaClient } from '@prisma/client'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; -import BetterSqlite3 from 'better-sqlite3'; -import path from 'path'; // Ensure env vars are loaded import 'dotenv/config'; @@ -18,6 +16,7 @@ if (!dbUrl) { throw new Error("DATABASE_URL environment variable is not set. Please check your .env file."); } +console.log('Initializing Prisma Adapter with URL:', dbUrl); const adapter = new PrismaBetterSqlite3({ url: dbUrl }); const prisma = diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 75efb7d..7d7e770 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -57,6 +57,7 @@ router.post('/login', async (req, res) => { res.json({ success: true, user: userSafe, token }); } catch (error) { + console.error('Login error:', error); res.status(500).json({ error: 'Server error' }); } }); diff --git a/server/src/scripts/backupSporadicSets.ts b/server/src/scripts/backupSporadicSets.ts deleted file mode 100644 index 5096763..0000000 --- a/server/src/scripts/backupSporadicSets.ts +++ /dev/null @@ -1,26 +0,0 @@ -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...'); - // Backup Quick Log sessions and their sets (formerly SporadicSets) - const quickLogSessions = await prisma.workoutSession.findMany({ - where: { type: 'QUICK_LOG' }, - include: { sets: true } - }); - - const backupPath = path.join(__dirname, '../../sporadic_backup.json'); - fs.writeFileSync(backupPath, JSON.stringify(quickLogSessions, null, 2)); - console.log(`Backed up ${quickLogSessions.length} quick log sessions to ${backupPath}`); - } catch (error) { - console.error('Backup failed:', error); - } finally { - await prisma.$disconnect(); - } -} - -backup(); diff --git a/server/src/scripts/migratePlans.ts b/server/src/scripts/migratePlans.ts deleted file mode 100644 index 40d59e6..0000000 --- a/server/src/scripts/migratePlans.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function migrate() { - console.log('Starting migration...'); - const plans = await prisma.workoutPlan.findMany(); - console.log(`Found ${plans.length} plans.`); - - for (const plan of plans) { - if (plan.exercises) { - try { - const steps = JSON.parse(plan.exercises); - console.log(`Migrating plan ${plan.name} (${plan.id}) with ${steps.length} steps.`); - - let order = 0; - for (const step of steps) { - await prisma.planExercise.create({ - data: { - planId: plan.id, - exerciseId: step.exerciseId, - order: order++, - isWeighted: step.isWeighted || false - } - }); - } - } catch (e) { - console.error(`Error parsing JSON for plan ${plan.id}:`, e); - } - } - } - console.log('Migration complete.'); -} - -migrate() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/server/src/scripts/restoreSporadicSets.ts b/server/src/scripts/restoreSporadicSets.ts deleted file mode 100644 index c85726d..0000000 --- a/server/src/scripts/restoreSporadicSets.ts +++ /dev/null @@ -1,77 +0,0 @@ -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();