feat: Single-port deployment with improved error handling and SvelteKit static build
- Frontend now uses @sveltejs/adapter-static for production builds - Backend serves both API and static files from single port (originally port 3000) - Removed all throw statements from services to avoid Elysia prototype errors - Fixed favicon serving and SvelteKit assets path handling - Added ecosystem.config.js for PM2 process management - Comprehensive deployment documentation (PM2 + HAProxy) - Updated README with single-port architecture - Created start.sh script for easy production start
This commit is contained in:
@@ -30,8 +30,7 @@ async function verifyPassword(password, hash) {
|
||||
* @param {string} userData.email - User email
|
||||
* @param {string} userData.password - Plain text password
|
||||
* @param {string} userData.callsign - Ham radio callsign
|
||||
* @returns {Promise<Object>} Created user object (without password)
|
||||
* @throws {Error} If email already exists
|
||||
* @returns {Promise<Object|null>} Created user object (without password) or null if email exists
|
||||
*/
|
||||
export async function registerUser({ email, password, callsign }) {
|
||||
// Check if user already exists
|
||||
@@ -42,7 +41,7 @@ export async function registerUser({ email, password, callsign }) {
|
||||
.limit(1);
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('Email already registered');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hash password
|
||||
@@ -79,13 +78,13 @@ export async function authenticateUser(email, password) {
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Invalid email or password');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValid = await verifyPassword(password, user.passwordHash);
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid email or password');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return user without password hash
|
||||
|
||||
@@ -546,7 +546,7 @@ export async function getAwardProgressDetails(userId, awardId) {
|
||||
const award = definitions.find((def) => def.id === awardId);
|
||||
|
||||
if (!award) {
|
||||
throw new Error('Award not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate progress
|
||||
@@ -572,7 +572,7 @@ export async function getAwardEntityBreakdown(userId, awardId) {
|
||||
const award = definitions.find((def) => def.id === awardId);
|
||||
|
||||
if (!award) {
|
||||
throw new Error('Award not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
let { rules } = award;
|
||||
|
||||
@@ -107,7 +107,12 @@ async function processJobAsync(jobId, userId, type, data) {
|
||||
// Get the processor for this job type
|
||||
const processor = jobProcessors[type];
|
||||
if (!processor) {
|
||||
throw new Error(`No processor registered for job type: ${type}`);
|
||||
await updateJob(jobId, {
|
||||
status: JobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
error: `No processor registered for job type: ${type}`,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Execute the job processor
|
||||
|
||||
@@ -84,7 +84,9 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
||||
for (let attempt = 0; attempt < POLLING_CONFIG.maxRetries; attempt++) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed > POLLING_CONFIG.maxTotalTime) {
|
||||
throw new Error(`LoTW sync timeout: exceeded maximum wait time of ${POLLING_CONFIG.maxTotalTime / 1000} seconds`);
|
||||
return {
|
||||
error: `LoTW sync timeout: exceeded maximum wait time of ${POLLING_CONFIG.maxTotalTime / 1000} seconds`
|
||||
};
|
||||
}
|
||||
|
||||
if (attempt > 0) {
|
||||
@@ -104,9 +106,9 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
||||
await sleep(POLLING_CONFIG.retryDelay);
|
||||
continue;
|
||||
} else if (response.status === 401) {
|
||||
throw new Error('Invalid LoTW credentials. Please check your username and password in Settings.');
|
||||
return { error: 'Invalid LoTW credentials. Please check your username and password in Settings.' };
|
||||
} else if (response.status === 404) {
|
||||
throw new Error('LoTW service not found (404). The LoTW API URL may have changed.');
|
||||
return { error: 'LoTW service not found (404). The LoTW API URL may have changed.' };
|
||||
} else {
|
||||
logger.warn(`LoTW returned ${response.status}, retrying...`);
|
||||
await sleep(POLLING_CONFIG.retryDelay);
|
||||
@@ -117,7 +119,7 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
||||
const adifData = await response.text();
|
||||
|
||||
if (adifData.toLowerCase().includes('username/password incorrect')) {
|
||||
throw new Error('Username/password incorrect');
|
||||
return { error: 'Username/password incorrect' };
|
||||
}
|
||||
|
||||
const header = adifData.trim().substring(0, 39).toLowerCase();
|
||||
@@ -127,7 +129,8 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
||||
await sleep(POLLING_CONFIG.retryDelay);
|
||||
continue;
|
||||
}
|
||||
throw new Error('Downloaded LoTW report is invalid. Check your credentials.');
|
||||
|
||||
return { error: 'Downloaded LoTW report is invalid. Check your credentials.' };
|
||||
}
|
||||
|
||||
logger.info('LoTW report downloaded successfully', { size: adifData.length });
|
||||
@@ -159,7 +162,9 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
||||
}
|
||||
|
||||
const totalTime = Math.round((Date.now() - startTime) / 1000);
|
||||
throw new Error(`LoTW sync failed: Report not ready after ${POLLING_CONFIG.maxRetries} attempts (${totalTime}s). LoTW may be experiencing high load. Please try again later.`);
|
||||
return {
|
||||
error: `LoTW sync failed: Report not ready after ${POLLING_CONFIG.maxRetries} attempts (${totalTime}s). LoTW may be experiencing high load. Please try again later.`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user