Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 141acd5736 | |||
| 5f4337a18d |
@@ -1,5 +1,8 @@
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# Add build argument for cache busting
|
||||
ARG CACHEBUST=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.48",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.48",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^5.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.48",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
Executable
+99
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.48",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.48",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-link": "^2.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.47",
|
||||
"version": "1.0.48",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -169,7 +169,10 @@ export const ArchivesPage: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p>
|
||||
<p className="text-2xl font-bold text-neutral-900">
|
||||
{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();
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<FileArchive className="w-8 h-8 text-green-600" />
|
||||
|
||||
@@ -47,6 +47,11 @@ export interface SystemStatus {
|
||||
activityLogs: number;
|
||||
};
|
||||
};
|
||||
storage: {
|
||||
totalUsed: number;
|
||||
photoStorage: number;
|
||||
archiveStorage: number;
|
||||
};
|
||||
emailQueue: {
|
||||
pending: number;
|
||||
sent: number;
|
||||
|
||||
Reference in New Issue
Block a user