diff --git a/.env.production.template b/.env.production.template
new file mode 100644
index 0000000..7389809
--- /dev/null
+++ b/.env.production.template
@@ -0,0 +1,30 @@
+# Production Configuration Template
+# Copy this file to .env.production and update with your production values
+
+# Application Environment
+NODE_ENV=production
+
+# Log Level (debug, info, warn, error)
+# Recommended: info for production
+LOG_LEVEL=info
+
+# Server Port (default: 3001)
+PORT=3001
+
+# Frontend URL (e.g., https://awards.dj7nt.de)
+VITE_APP_URL=https://awards.dj7nt.de
+
+# API Base URL (leave empty for same-domain deployment)
+VITE_API_BASE_URL=
+
+# Allowed CORS origins (comma-separated)
+# Add all domains that should access the API
+ALLOWED_ORIGINS=https://awards.dj7nt.de,https://www.awards.dj7nt.de
+
+# JWT Secret (REQUIRED - generate a strong secret!)
+# Generate with: openssl rand -base64 32
+JWT_SECRET=REPLACE_WITH_SECURE_RANDOM_STRING
+
+# Database (if using external database)
+# Leave empty to use default SQLite database
+# DATABASE_URL=file:/path/to/production.db
diff --git a/bun.lock b/bun.lock
index 99774a8..0aced01 100644
--- a/bun.lock
+++ b/bun.lock
@@ -12,7 +12,6 @@
"elysia": "^1.4.22",
},
"devDependencies": {
- "@libsql/client": "^0.17.0",
"@types/bun": "latest",
"drizzle-kit": "^0.31.8",
},
diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 0000000..d62d109
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,33 @@
+# Bun Configuration
+# https://bun.sh/docs/runtime/bunfig
+
+[install]
+# Cache dependencies in project directory for faster installs
+cache = true
+# Use global cache for faster reinstalls
+global = true
+
+[run]
+# Enable hot reload in development (enabled with --hot flag)
+hot = true
+
+# Lockfile configuration
+[lockfile]
+# Print the lockfile to console (useful for debugging)
+print = "yarn"
+
+# Test configuration
+[test]
+# Enable test coverage
+# coverage = true
+# Preload files before running tests
+preload = []
+
+# Build configuration
+[build]
+# Target modern browsers for better performance
+target = "esnext"
+# Minify production builds
+minify = true
+# Enable source maps in development
+sourcemap = true
diff --git a/package.json b/package.json
index 0d4afc7..d7a343d 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,6 @@
"db:migrate": "drizzle-kit migrate"
},
"devDependencies": {
- "@libsql/client": "^0.17.0",
"@types/bun": "latest",
"drizzle-kit": "^0.31.8"
},
diff --git a/src/backend/index.js b/src/backend/index.js
index 21fcfa8..8d0161d 100644
--- a/src/backend/index.js
+++ b/src/backend/index.js
@@ -635,6 +635,44 @@ const app = new Elysia()
};
})
+ /**
+ * GET /api/awards/batch/progress
+ * Get progress for ALL awards in a single request (fixes N+1 query problem)
+ */
+ .get('/api/awards/batch/progress', async ({ user, set }) => {
+ if (!user) {
+ set.status = 401;
+ return { success: false, error: 'Unauthorized' };
+ }
+
+ try {
+ const awards = await getAllAwards();
+
+ // Calculate all awards in parallel
+ const progressMap = await Promise.all(
+ awards.map(async (award) => {
+ const progress = await getAwardProgressDetails(user.id, award.id);
+ return {
+ awardId: award.id,
+ ...progress,
+ };
+ })
+ );
+
+ return {
+ success: true,
+ awards: progressMap,
+ };
+ } catch (error) {
+ logger.error('Failed to fetch batch award progress', { error: error.message, userId: user.id });
+ set.status = 500;
+ return {
+ success: false,
+ error: 'Failed to fetch award progress',
+ };
+ }
+ })
+
/**
* GET /api/awards/:awardId/entities
* Get detailed entity breakdown for an award (requires authentication)
diff --git a/src/frontend/src/routes/awards/+page.svelte b/src/frontend/src/routes/awards/+page.svelte
index c76c405..c3ea8b0 100644
--- a/src/frontend/src/routes/awards/+page.svelte
+++ b/src/frontend/src/routes/awards/+page.svelte
@@ -19,57 +19,50 @@
error = null;
// Get awards from API
- const response = await fetch('/api/awards', {
+ const awardsResponse = await fetch('/api/awards', {
headers: {
'Authorization': `Bearer ${$auth.token}`,
},
});
- if (!response.ok) {
+ if (!awardsResponse.ok) {
throw new Error('Failed to load awards');
}
- const data = await response.json();
+ const awardsData = await awardsResponse.json();
- if (!data.success) {
- throw new Error(data.error || 'Failed to load awards');
+ if (!awardsData.success) {
+ throw new Error(awardsData.error || 'Failed to load awards');
}
- // Load progress for each award
- allAwards = await Promise.all(
- data.awards.map(async (award) => {
- try {
- const progressResponse = await fetch(`/api/awards/${award.id}/progress`, {
- headers: {
- 'Authorization': `Bearer ${$auth.token}`,
- },
- });
+ // Get progress for all awards in a single batch request (fixes N+1 problem)
+ const progressResponse = await fetch('/api/awards/batch/progress', {
+ headers: {
+ 'Authorization': `Bearer ${$auth.token}`,
+ },
+ });
- if (progressResponse.ok) {
- const progressData = await progressResponse.json();
- if (progressData.success) {
- return {
- ...award,
- progress: progressData,
- };
- }
- }
- } catch (e) {
- console.error(`Failed to load progress for ${award.id}:`, e);
- }
+ let progressMap = {};
+ if (progressResponse.ok) {
+ const progressData = await progressResponse.json();
+ if (progressData.success && progressData.awards) {
+ // Create a map of awardId -> progress for quick lookup
+ progressMap = Object.fromEntries(
+ progressData.awards.map(p => [p.awardId, p])
+ );
+ }
+ }
- // Return award without progress if it failed
- return {
- ...award,
- progress: {
- worked: 0,
- confirmed: 0,
- target: award.rules?.target || 0,
- percentage: 0,
- },
- };
- })
- );
+ // Combine awards with their progress
+ allAwards = awardsData.awards.map(award => ({
+ ...award,
+ progress: progressMap[award.id] || {
+ worked: 0,
+ confirmed: 0,
+ target: award.rules?.target || 0,
+ percentage: 0,
+ },
+ }));
// Extract unique categories
categories = ['all', ...new Set(allAwards.map(a => a.category).filter(Boolean))];
diff --git a/src/frontend/src/routes/qsos/+page.svelte b/src/frontend/src/routes/qsos/+page.svelte
index 66dc76b..3f0e738 100644
--- a/src/frontend/src/routes/qsos/+page.svelte
+++ b/src/frontend/src/routes/qsos/+page.svelte
@@ -2,6 +2,8 @@
import { onMount, onDestroy } from 'svelte';
import { qsosAPI, jobsAPI } from '$lib/api.js';
import { auth } from '$lib/stores.js';
+ import QSOStats from './components/QSOStats.svelte';
+ import SyncButton from './components/SyncButton.svelte';
let qsos = [];
let stats = null;
@@ -384,28 +386,20 @@
Clear All QSOs
{/if}
-
-
+
+