From 5f4337a18db3e5e04b6a6593463749e1b9bc91c8 Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 16 Jul 2025 11:11:55 +0200 Subject: [PATCH] fix: resolve multiple production issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix archive page showing "00" instead of "0" for empty photo counts - Fix email queue PostgreSQL "updated_at" column error with migration and fallback - Fix storage calculation discrepancies between system status and archive pages - Add proper storage info to system status endpoint - Fix archive path concatenation in settings endpoint šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/Dockerfile | 3 + .../025_fix_email_queue_updated_at.js | 33 +++++++ backend/scripts/fix-email-queue-schema.js | 99 +++++++++++++++++++ backend/src/routes/adminSettings.js | 14 ++- backend/src/routes/adminSystem.js | 32 ++++++ backend/src/services/emailProcessor.js | 25 +++-- frontend/src/pages/admin/ArchivesPage.tsx | 5 +- frontend/src/services/settings.service.ts | 5 + 8 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 backend/migrations/025_fix_email_queue_updated_at.js create mode 100755 backend/scripts/fix-email-queue-schema.js diff --git a/backend/Dockerfile b/backend/Dockerfile index 23d202f..8a27083 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,8 @@ FROM node:18-alpine AS builder +# Add build argument for cache busting +ARG CACHEBUST=1 + WORKDIR /app # Copy package files diff --git a/backend/migrations/025_fix_email_queue_updated_at.js b/backend/migrations/025_fix_email_queue_updated_at.js new file mode 100644 index 0000000..eab064f --- /dev/null +++ b/backend/migrations/025_fix_email_queue_updated_at.js @@ -0,0 +1,33 @@ +/** + * Fix email_queue table by ensuring it doesn't have updated_at column + * This migration addresses the PostgreSQL error where queries are trying to update + * a non-existent updated_at column + */ + +exports.up = async function(knex) { + // First, check if the column exists + const hasUpdatedAt = await knex.schema.hasColumn('email_queue', 'updated_at'); + + if (hasUpdatedAt) { + console.log('Found updated_at column in email_queue table, removing it...'); + await knex.schema.table('email_queue', (table) => { + table.dropColumn('updated_at'); + }); + } + + // Also ensure the table has all required columns + const hasCreatedAt = await knex.schema.hasColumn('email_queue', 'created_at'); + if (!hasCreatedAt) { + console.log('Adding missing created_at column to email_queue table...'); + await knex.schema.table('email_queue', (table) => { + table.datetime('created_at').defaultTo(knex.fn.now()); + }); + } + + console.log('email_queue table schema fixed'); +}; + +exports.down = async function(knex) { + // In the down migration, we don't add back updated_at since it shouldn't exist + // This is intentionally left minimal +}; \ No newline at end of file diff --git a/backend/scripts/fix-email-queue-schema.js b/backend/scripts/fix-email-queue-schema.js new file mode 100755 index 0000000..825e4c9 --- /dev/null +++ b/backend/scripts/fix-email-queue-schema.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +/** + * Script to diagnose and fix email_queue schema issues + * This helps resolve the "column updated_at does not exist" error + */ + +require('dotenv').config(); +const { db } = require('../src/database/db'); + +async function checkAndFixEmailQueueSchema() { + console.log('Checking email_queue table schema...'); + + try { + // Get column information + const columns = await db('email_queue').columnInfo(); + console.log('\nCurrent email_queue columns:', Object.keys(columns)); + + // Check for updated_at column + if (columns.updated_at) { + console.log('\nāš ļø Found unexpected updated_at column in email_queue table!'); + console.log('This column should not exist and is causing errors.'); + + // Ask for confirmation before removing + console.log('\nRemoving updated_at column...'); + await db.schema.table('email_queue', (table) => { + table.dropColumn('updated_at'); + }); + console.log('āœ… Removed updated_at column from email_queue table'); + } else { + console.log('āœ… No updated_at column found (this is correct)'); + } + + // Verify required columns exist + const requiredColumns = [ + 'id', 'event_id', 'recipient_email', 'email_type', + 'email_data', 'status', 'scheduled_at', 'sent_at', + 'error_message', 'retry_count', 'created_at' + ]; + + const missingColumns = requiredColumns.filter(col => !columns[col]); + if (missingColumns.length > 0) { + console.log('\nāš ļø Missing required columns:', missingColumns); + } else { + console.log('āœ… All required columns are present'); + } + + // Check for any database triggers + if (process.env.DATABASE_CLIENT === 'pg') { + console.log('\nChecking for PostgreSQL triggers on email_queue...'); + const triggers = await db.raw(` + SELECT trigger_name, event_manipulation, action_statement + FROM information_schema.triggers + WHERE event_object_table = 'email_queue' + AND trigger_schema = current_schema() + `); + + if (triggers.rows && triggers.rows.length > 0) { + console.log('āš ļø Found triggers on email_queue table:'); + triggers.rows.forEach(trigger => { + console.log(` - ${trigger.trigger_name} (${trigger.event_manipulation})`); + }); + } else { + console.log('āœ… No triggers found on email_queue table'); + } + } + + // Test update query + console.log('\nTesting update query...'); + const testEmail = await db('email_queue') + .where('status', 'pending') + .first(); + + if (testEmail) { + try { + await db('email_queue') + .where('id', testEmail.id) + .update({ + retry_count: testEmail.retry_count + }); + console.log('āœ… Update query works correctly'); + } catch (error) { + console.log('āŒ Update query failed:', error.message); + } + } else { + console.log('ā„¹ļø No pending emails to test with'); + } + + console.log('\nSchema check complete!'); + + } catch (error) { + console.error('Error checking schema:', error); + } finally { + await db.destroy(); + } +} + +// Run the check +checkAndFixEmailQueueSchema(); \ No newline at end of file diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 010e929..dcfe3b8 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -520,11 +520,15 @@ router.get('/storage/info', adminAuth, async (req, res) => { let archiveStorage = 0; for (const archive of archives) { - try { - const stats = await fs.stat(archive.archive_path); - archiveStorage += stats.size; - } catch (error) { - console.error('Archive file not found:', archive.archive_path); + if (archive.archive_path) { + try { + const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); + const fullArchivePath = path.join(storagePath, archive.archive_path); + const stats = await fs.stat(fullArchivePath); + archiveStorage += stats.size; + } catch (error) { + console.error('Archive file not found:', archive.archive_path, error.message); + } } } diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index b0bb088..9b67325 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -4,6 +4,7 @@ const { adminAuth } = require('../middleware/auth-enhanced-v2'); const fs = require('fs').promises; const path = require('path'); const os = require('os'); +const { formatBoolean } = require('../utils/dbCompat'); const router = express.Router(); // Get system version @@ -75,6 +76,32 @@ router.get('/status', adminAuth, async (req, res) => { // Activity logs count const [activityCount] = await db('activity_logs').count('* as count'); + // Storage info + const [{ totalPhotoStorage }] = await db('photos') + .sum('size_bytes as totalPhotoStorage'); + + const archives = await db('events') + .where('is_archived', formatBoolean(true)) + .whereNotNull('archive_path') + .select('archive_path'); + + let archiveStorage = 0; + const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); + + for (const archive of archives) { + if (archive.archive_path) { + try { + const fullArchivePath = path.join(storagePath, archive.archive_path); + const stats = await fs.stat(fullArchivePath); + archiveStorage += stats.size; + } catch (error) { + console.error('Archive file not found:', archive.archive_path); + } + } + } + + const totalStorage = (parseInt(totalPhotoStorage) || 0) + archiveStorage; + // System info const systemInfo = { platform: os.platform(), @@ -105,6 +132,11 @@ router.get('/status', adminAuth, async (req, res) => { activityLogs: activityCount.count } }, + storage: { + totalUsed: totalStorage, + photoStorage: parseInt(totalPhotoStorage) || 0, + archiveStorage: archiveStorage + }, emailQueue: { pending: pendingEmails.count, sent: sentEmails.count, diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 2d703c2..ffd65a6 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -369,13 +369,24 @@ async function processEmailQueue() { logger.info(`Email ${email.id} sent successfully`); } catch (error) { // Increment retry count - await db('email_queue') - .where('id', email.id) - .update({ - retry_count: email.retry_count + 1, - error_message: error.message, - updated_at: new Date() - }); + try { + await db('email_queue') + .where('id', email.id) + .update({ + retry_count: email.retry_count + 1, + error_message: error.message + }); + } catch (updateError) { + logger.error(`Failed to update email retry count for ${email.id}:`, updateError); + // If update fails due to column issue, try without any potential auto-added fields + if (updateError.message && updateError.message.includes('updated_at')) { + logger.warn('Detected updated_at column issue, attempting raw query...'); + await db.raw( + 'UPDATE email_queue SET retry_count = ?, error_message = ? WHERE id = ?', + [email.retry_count + 1, error.message, email.id] + ); + } + } logger.error(`Failed to send email ${email.id}:`, error); } diff --git a/frontend/src/pages/admin/ArchivesPage.tsx b/frontend/src/pages/admin/ArchivesPage.tsx index 2431511..a2801b5 100644 --- a/frontend/src/pages/admin/ArchivesPage.tsx +++ b/frontend/src/pages/admin/ArchivesPage.tsx @@ -169,7 +169,10 @@ export const ArchivesPage: React.FC = () => {

{t('archives.totalPhotos')}

- {archives.reduce((sum, a) => sum + (a.photoCount || 0), 0).toLocaleString()} + {(() => { + const total = archives.reduce((sum, a) => sum + (parseInt(String(a.photoCount)) || 0), 0); + return total === 0 ? '0' : total.toLocaleString(); + })()}

diff --git a/frontend/src/services/settings.service.ts b/frontend/src/services/settings.service.ts index e3de561..5deed19 100644 --- a/frontend/src/services/settings.service.ts +++ b/frontend/src/services/settings.service.ts @@ -47,6 +47,11 @@ export interface SystemStatus { activityLogs: number; }; }; + storage: { + totalUsed: number; + photoStorage: number; + archiveStorage: number; + }; emailQueue: { pending: number; sent: number;