From b6cb3059af65c31b385238dba2720eedad667e63 Mon Sep 17 00:00:00 2001 From: AG Date: Thu, 18 Dec 2025 20:49:34 +0200 Subject: [PATCH] Datepicker redesign + DB connection fixes for Prod --- deployment_guide.md | 31 +++ dev.db | 0 server/check_adapter_props.js | 29 +++ server/check_db_perms.js | 45 ++++ server/prisma/test.db | Bin 978944 -> 995328 bytes server/prod.db | Bin 98304 -> 98304 bytes server/reset_prod_db.js | 29 ++- server/src/lib/prisma.ts | 27 ++- src/components/Profile.tsx | 1 + src/components/ui/DatePicker.tsx | 388 +++++++++++++++++++++++++------ 10 files changed, 472 insertions(+), 78 deletions(-) create mode 100644 dev.db create mode 100644 server/check_adapter_props.js create mode 100644 server/check_db_perms.js diff --git a/deployment_guide.md b/deployment_guide.md index 47ad0e5..5d9fdb1 100644 --- a/deployment_guide.md +++ b/deployment_guide.md @@ -142,3 +142,34 @@ Point your domain (e.g., `gym.yourdomain.com`) to the NAS IP and the mapped port - Forward Hostname / IP: `[NAS_IP]` - Forward Port: `3033` - Websockets Support: Enable (if needed for future features). + +## 6. Troubleshooting + +### "Readonly Database" Error +If you see an error like `Invalid prisma.userProfile.upsert() invocation: attempt to write a readonly database`: + +1. **Verify Permissions:** Run the diagnostic script inside your container: + ```bash + docker exec -it node-apps node /usr/src/app/gymflow/server/check_db_perms.js + ``` +2. **Fix Permissions:** If the checks fail, run these commands on your NAS inside the `gymflow/server` directory: + ```bash + sudo chmod 777 . + sudo chmod 666 prod.db + ``` + *Note: SQLite needs write access to the directory itself to create temporary journaling files (`-wal`, `-shm`).* + +3. **Check Docker User:** Alternatively, ensure your Docker container is running as a user who owns these files (e.g., set `user: "1000:1000"` in `docker-compose.yml` if your NAS user has that ID). + +### "Invalid ELF Header" Error +If you see an error like `invalid ELF header` for `better-sqlite3.node`: +This happens because the `node_modules` contains Windows binaries (from your local machine) instead of Linux binaries. + +1. **Fix Inside Container:** Run the following command to force a rebuild of native modules for Linux: + ```bash + docker exec -it node-apps /bin/sh -c "cd /usr/src/app/gymflow/server && npm rebuild better-sqlite3" + ``` +2. **Restart Container:** After rebuilding, restart the container: + ```bash + docker-compose restart nodejs-apps + ``` diff --git a/dev.db b/dev.db new file mode 100644 index 0000000..e69de29 diff --git a/server/check_adapter_props.js b/server/check_adapter_props.js new file mode 100644 index 0000000..fd4b7c6 --- /dev/null +++ b/server/check_adapter_props.js @@ -0,0 +1,29 @@ +const { PrismaBetterSqlite3 } = require('@prisma/adapter-better-sqlite3'); +const path = require('path'); + +async function check() { + console.log('--- Prisma Adapter Diagnostic ---'); + const factory = new PrismaBetterSqlite3({ url: 'file:./dev.db' }); + + console.log('Factory Properties:'); + console.log(Object.keys(factory)); + console.log('Factory.adapterName:', factory.adapterName); + console.log('Factory.provider:', factory.provider); + + try { + const adapter = await factory.connect(); + console.log('\nAdapter Properties:'); + console.log(Object.keys(adapter)); + console.log('Adapter name:', adapter.adapterName); + console.log('Adapter provider:', adapter.provider); + + // Also check if there are hidden/prototype properties + let proto = Object.getPrototypeOf(adapter); + console.log('Adapter Prototype Properties:', Object.getOwnPropertyNames(proto)); + + } catch (e) { + console.error('Failed to connect:', e); + } +} + +check(); diff --git a/server/check_db_perms.js b/server/check_db_perms.js new file mode 100644 index 0000000..7e247db --- /dev/null +++ b/server/check_db_perms.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); + +const dbPath = path.resolve(__dirname, 'prod.db'); +const dirPath = __dirname; + +console.log('--- GymFlow Database Permission Check ---'); +console.log(`Checking directory: ${dirPath}`); +console.log(`Checking file: ${dbPath}`); + +// 1. Check Directory +try { + fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK); + console.log('✅ Directory is readable and writable.'); +} catch (err) { + console.error('❌ Directory is NOT writable! SQLite needs directory write access to create temporary files.'); +} + +// 2. Check File +if (fs.existsSync(dbPath)) { + try { + fs.accessSync(dbPath, fs.constants.R_OK | fs.constants.W_OK); + console.log('✅ Database file is readable and writable.'); + } catch (err) { + console.error('❌ Database file is NOT writable!'); + } +} else { + console.log('ℹ️ Database file does not exist yet.'); +} + +// 3. Try to write a test file in the directory +const testFile = path.join(dirPath, '.write_test'); +try { + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + console.log('✅ Successfully performed a test write in the directory.'); +} catch (err) { + console.error(`❌ Failed test write in directory: ${err.message}`); +} + +console.log('\n--- Recommendation ---'); +console.log('If any checks failed, run these commands on your NAS (in the gymflow/server folder):'); +console.log('1. sudo chmod 777 .'); +console.log('2. sudo chmod 666 prod.db'); +console.log('\nAlternatively, ensure your Docker container is running with a user that owns these files.'); diff --git a/server/prisma/test.db b/server/prisma/test.db index ac39fce788a5fde931bb47c98efeff0c61ca5f69..882938a29e1e51b126794462e9bdfd8d7c63481a 100644 GIT binary patch delta 17371 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 QCC=%_7_j8(^b delta 5564 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 diff --git a/server/prod.db b/server/prod.db index 5f7691d26b1f2c05a9b234e58547ba3a79ca276c..c9b5e78b71f3d553c14cde3008053468ef784056 100644 GIT binary patch delta 393 zcma*hu}Z^07{GA~rE7hIIs_c#!gpW3%QabSK%};|iMC6}OYW{Vn3hV#I5m$XX`v6J zlaCn^#w@ z984V!&ZAtEdGf+@y&h)%*c(sm&UIJGu*ijZj!jZ*qgdZ>BPd77!(z0X&SomQp4?6m zP%w^+_vaihLXe>#R}x+VgaH5QUp##GzPe76*&2@V9f;IQ9%PKTG>nAStS?N;0Da=h i^F8uy6YR#U%liDY>ofstXb%o;__;c@&RQRvMz_DA32v|e delta 393 zcma)%%SyvQ7=9`_+<^al~>mZeBFMYXn^Tm@d zuRW$b3SOorTwpwR{i{ii$ped(R(=Msy183kUc~v)AUik1+5O`xIh&oP%Y)HK_6Ntq zgaH$TGD4#%h25^eE`X0A?g3QkAN_-y@AlW0S4Xylx07+iM8`O#NEBlAiYZDY@_?XW lA#i7n=B!THq0OJmozH5yMqvrf-lhpZ`|pkI#>b+T?Jq1haWDV? diff --git a/server/reset_prod_db.js b/server/reset_prod_db.js index 8be675e..f0f420d 100644 --- a/server/reset_prod_db.js +++ b/server/reset_prod_db.js @@ -54,10 +54,33 @@ async function resetDb() { // 4. Create the Admin user console.log(`Creating fresh admin user...`); - // In Prisma 7, we must use the adapter for better-sqlite3 + // In Prisma 7, PrismaBetterSqlite3 is a factory. + // We use the factory to create the adapter, then we access the internal client + // to disable WAL mode for NAS/Network share compatibility (journal_mode = DELETE). const { PrismaBetterSqlite3 } = require('@prisma/adapter-better-sqlite3'); - const adapter = new PrismaBetterSqlite3({ url: dbPath }); - const prisma = new PrismaClient({ adapter }); + const factory = new PrismaBetterSqlite3({ url: dbPath }); + + const adapterWrapper = { + provider: 'sqlite', + adapterName: '@prisma/adapter-better-sqlite3', + async connect() { + const adapter = await factory.connect(); + if (adapter.client) { + console.log(`Setting journal_mode = DELETE for NAS compatibility`); + adapter.client.pragma('journal_mode = DELETE'); + } + return adapter; + }, + async connectToShadowDb() { + const adapter = await factory.connectToShadowDb(); + if (adapter.client) { + adapter.client.pragma('journal_mode = DELETE'); + } + return adapter; + } + }; + + const prisma = new PrismaClient({ adapter: adapterWrapper }); try { const hashedPassword = await bcrypt.hash(adminPassword, 10); diff --git a/server/src/lib/prisma.ts b/server/src/lib/prisma.ts index bf89831..210327c 100644 --- a/server/src/lib/prisma.ts +++ b/server/src/lib/prisma.ts @@ -35,13 +35,36 @@ console.log('Initializing Prisma Client with database:', dbPath); let prisma: PrismaClient; +// In Prisma 7, PrismaBetterSqlite3 is a factory. +// We use a wrapper to intercept the connection and disable WAL mode +// for NAS/Network share compatibility (journal_mode = DELETE). try { - const adapter = new PrismaBetterSqlite3({ url: dbPath }); + const factory = new PrismaBetterSqlite3({ url: dbPath }); + + const adapterWrapper = { + provider: 'sqlite', + adapterName: '@prisma/adapter-better-sqlite3', + async connect() { + const adapter = (await factory.connect()) as any; + if (adapter.client) { + console.log('[Prisma] Setting journal_mode = DELETE for NAS compatibility'); + adapter.client.pragma('journal_mode = DELETE'); + } + return adapter; + }, + async connectToShadowDb() { + const adapter = (await factory.connectToShadowDb()) as any; + if (adapter.client) { + adapter.client.pragma('journal_mode = DELETE'); + } + return adapter; + } + }; prisma = global.prisma || new PrismaClient({ - adapter: adapter as any, + adapter: adapterWrapper as any, }); } catch (e: any) { console.error('Failed to initialize Prisma Client:', e.message); diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index e9e2e75..8698c41 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -321,6 +321,7 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang value={birthDate} onChange={(val) => setBirthDate(val)} testId="profile-birth-date" + maxDate={new Date()} />
diff --git a/src/components/ui/DatePicker.tsx b/src/components/ui/DatePicker.tsx index 056e259..58c905a 100644 --- a/src/components/ui/DatePicker.tsx +++ b/src/components/ui/DatePicker.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect, useId } from 'react'; import { createPortal } from 'react-dom'; -import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react'; +import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; import { Button } from './Button'; import { Ripple } from './Ripple'; @@ -12,6 +12,7 @@ interface DatePickerProps { icon?: React.ReactNode; disabled?: boolean; testId?: string; + maxDate?: Date; // Optional maximum date constraint } export const DatePicker: React.FC = ({ @@ -21,7 +22,8 @@ export const DatePicker: React.FC = ({ placeholder = 'Select date', icon = , disabled = false, - testId + testId, + maxDate }) => { const id = useId(); const [isOpen, setIsOpen] = useState(false); @@ -30,6 +32,11 @@ export const DatePicker: React.FC = ({ const containerRef = useRef(null); const popoverRef = useRef(null); + // MD3 Enhancement: Calendar view and text input states + const [calendarView, setCalendarView] = useState<'days' | 'months' | 'years'>('days'); + const [textInputValue, setTextInputValue] = useState(''); + const [textInputError, setTextInputError] = useState(''); + // Update popover position when opening or when window resizes const updatePosition = () => { if (containerRef.current) { @@ -100,6 +107,10 @@ export const DatePicker: React.FC = ({ const nextOpen = !isOpen; if (nextOpen) { updatePosition(); + // MD3 Enhancement: Reset to days view when opening + setCalendarView('days'); + setTextInputValue(''); + setTextInputError(''); } setIsOpen(nextOpen); } @@ -122,57 +133,257 @@ export const DatePicker: React.FC = ({ setIsOpen(false); }; + // MD3 Enhancement: Year selection handler + const handleYearSelect = (year: number) => { + setViewDate(new Date(year, viewDate.getMonth(), 1)); + setCalendarView('days'); + }; + + // MD3 Enhancement: Text input validation and handling + const validateDateInput = (input: string): boolean => { + // Check format YYYY-MM-DD + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(input)) { + setTextInputError('Format: YYYY-MM-DD'); + return false; + } + + // Check if date is valid + const date = new Date(input); + if (isNaN(date.getTime())) { + setTextInputError('Invalid date'); + return false; + } + + // Check if the input matches the parsed date (catches invalid dates like 2023-02-30) + const [year, month, day] = input.split('-').map(Number); + if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { + setTextInputError('Invalid date'); + return false; + } + + // Check against maxDate constraint + if (maxDate && date > maxDate) { + setTextInputError('Date cannot be in the future'); + return false; + } + + setTextInputError(''); + return true; + }; + + const handleTextInputSubmit = () => { + if (validateDateInput(textInputValue)) { + onChange(textInputValue); + setIsOpen(false); + setTextInputValue(''); + } + }; + const daysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate(); const firstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay(); + const renderCalendar = () => { const year = viewDate.getFullYear(); const month = viewDate.getMonth(); const daysCount = daysInMonth(year, month); const startingDay = firstDayOfMonth(year, month); - const monthName = viewDate.toLocaleString('default', { month: 'long' }); - - const days = []; - for (let i = 0; i < startingDay; i++) { - days.push(
); - } + const monthName = viewDate.toLocaleString('en-US', { month: 'long' }); + const shortMonthName = viewDate.toLocaleString('en-US', { month: 'short' }); + // Format selected date for header display const selectedDate = value ? new Date(value) : null; - const isSelected = (d: number) => { - return selectedDate && - selectedDate.getFullYear() === year && - selectedDate.getMonth() === month && - selectedDate.getDate() === d; - }; + const headerDateText = selectedDate + ? selectedDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) + : 'Select date'; - const today = new Date(); - const isToday = (d: number) => { - return today.getFullYear() === year && - today.getMonth() === month && - today.getDate() === d; - }; + // Render year selection view + const renderYearsView = () => { + const currentYear = new Date().getFullYear(); + const maxYear = maxDate ? maxDate.getFullYear() : currentYear + 100; + const years = []; + // Generate years from current year going backwards (recent first) + for (let y = maxYear; y >= currentYear - 100; y--) { + years.push(y); + } - for (let d = 1; d <= daysCount; d++) { - days.push( - + const selectedYear = selectedDate?.getFullYear(); + + return ( +
+
+ {years.map(y => ( + + ))} +
+
); - } + }; + + // Render month selection view + const renderMonthsView = () => { + const months = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + return ( +
+
+ {months.map((monthLabel, i) => ( + + ))} +
+
+ ); + }; + + // Render days view + const renderDaysView = () => { + const days = []; + for (let i = 0; i < startingDay; i++) { + days.push(
); + } + + const isSelected = (d: number) => { + return selectedDate && + selectedDate.getFullYear() === year && + selectedDate.getMonth() === month && + selectedDate.getDate() === d; + }; + + const today = new Date(); + const isToday = (d: number) => { + return today.getFullYear() === year && + today.getMonth() === month && + today.getDate() === d; + }; + + // Check if a date is disabled (after maxDate) + const isDisabled = (d: number) => { + if (!maxDate) return false; + const checkDate = new Date(year, month, d); + return checkDate > maxDate; + }; + + for (let d = 1; d <= daysCount; d++) { + const disabled = isDisabled(d); + days.push( + + ); + } + + return ( + <> +
+ {/* Month navigation */} +
+ + + +
+ + {/* Year navigation */} +
+ + + +
+
+ +
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => ( +
+ {day} +
+ ))} +
+ +
+ {days} +
+ + ); + }; const calendarContent = (
= ({ }} className="p-4 w-[320px] bg-surface-container-high rounded-2xl shadow-xl border border-outline-variant animate-in fade-in zoom-in duration-200 origin-top" > -
-
- {monthName} {year} -
-
- - -
-
- -
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => ( -
- {day} -
- ))} -
- -
- {days} -
+ {/* Calendar content based on view */} + {calendarView === 'years' ? ( + renderYearsView() + ) : calendarView === 'months' ? ( + renderMonthsView() + ) : ( + renderDaysView() + )} + {/* Footer with Cancel button */}
+