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:
53
.dockerignore
Normal file
53
.dockerignore
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
bun.lockb
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Database
|
||||||
|
**/*.db
|
||||||
|
**/*.db-shm
|
||||||
|
**/*.db-wal
|
||||||
|
award.db
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
src/frontend/build/
|
||||||
|
src/frontend/.svelte-kit/
|
||||||
|
src/frontend/dist/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# PM2
|
||||||
|
ecosystem.config.js
|
||||||
|
.pm2/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
*.test.js
|
||||||
|
*.test.ts
|
||||||
|
coverage/
|
||||||
461
README.md
461
README.md
@@ -227,79 +227,422 @@ This gives you:
|
|||||||
- ✅ Simple production deployment
|
- ✅ Simple production deployment
|
||||||
|
|
||||||
### Production Mode
|
### Production Mode
|
||||||
In production, you can either:
|
In production, the application serves everything from a single port:
|
||||||
1. **Build static files** and serve from Elysia
|
- **Backend** on port 3001 serves both API and static frontend files
|
||||||
2. **Keep the proxy setup** with a proper reverse proxy (nginx, caddy)
|
- Frontend static files are served from `src/frontend/build/`
|
||||||
3. **Use SvelteKit adapter** for Node/Bun to serve everything from one process
|
- SPA routing is handled by backend fallback
|
||||||
|
- **Single port** simplifies HAProxy configuration
|
||||||
|
|
||||||
## Production Deployment
|
## Production Deployment
|
||||||
|
|
||||||
### Building for Production
|
This guide covers deployment using **PM2** for process management and **HAProxy** as a reverse proxy/load balancer.
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ HAProxy │ Port 443 (HTTPS)
|
||||||
|
│ (Port 80/443) │ Port 80 (HTTP → HTTPS redirect)
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Backend │ Port 3001
|
||||||
|
│ Managed by │ ├─ API Routes (/api/*)
|
||||||
|
│ PM2 │ ├─ Static Files (/*)
|
||||||
|
│ │ └─ SPA Fallback
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
├─────────────────┬──────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────┐ ┌──────────┐ ┌─────────────┐
|
||||||
|
│ SQLite │ │ Frontend │ │ ARRL LoTW │
|
||||||
|
│ DB │ │ Build │ │ External API│
|
||||||
|
└─────────┘ └──────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Server with SSH access
|
||||||
|
- Bun runtime installed
|
||||||
|
- PM2 installed globally: `bun install -g pm2` or `npm install -g pm2`
|
||||||
|
- HAProxy installed
|
||||||
|
- Domain with DNS pointing to server
|
||||||
|
|
||||||
|
### Step 1: Build the Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the frontend
|
# Clone repository on server
|
||||||
|
git clone <repository-url>
|
||||||
|
cd award
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Build frontend (generates static files in src/frontend/build/)
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Configure Environment Variables
|
||||||
|
|
||||||
|
Create `.env` in the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Application URL
|
||||||
|
VITE_APP_URL=https://awards.dj7nt.de
|
||||||
|
|
||||||
|
# API Base URL (empty for same-domain)
|
||||||
|
VITE_API_BASE_URL=
|
||||||
|
|
||||||
|
# JWT Secret (generate with: openssl rand -base64 32)
|
||||||
|
JWT_SECRET=your-generated-secret-here
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Database path (absolute path recommended)
|
||||||
|
DATABASE_PATH=/path/to/award/award.db
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security**: Ensure `.env` has restricted permissions:
|
||||||
|
```bash
|
||||||
|
chmod 600 .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Initialize Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push database schema
|
||||||
|
bun run db:push
|
||||||
|
|
||||||
|
# Verify database was created
|
||||||
|
ls -la award.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Create PM2 Ecosystem Configuration
|
||||||
|
|
||||||
|
Create `ecosystem.config.js` in the project root:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'award-backend',
|
||||||
|
script: 'src/backend/index.js',
|
||||||
|
interpreter: 'bun',
|
||||||
|
cwd: '/path/to/award',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 3001
|
||||||
|
},
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: 'fork',
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '500M',
|
||||||
|
error_file: './logs/backend-error.log',
|
||||||
|
out_file: './logs/backend-out.log',
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'award-frontend',
|
||||||
|
script: 'bun',
|
||||||
|
args: 'run preview',
|
||||||
|
cwd: '/path/to/award/src/frontend',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 5173
|
||||||
|
},
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: 'fork',
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '300M',
|
||||||
|
error_file: './logs/frontend-error.log',
|
||||||
|
out_file: './logs/frontend-out.log',
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create logs directory:**
|
||||||
|
```bash
|
||||||
|
mkdir -p logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Start Applications with PM2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start all applications
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
|
||||||
|
# Save PM2 process list
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# Setup PM2 to start on system reboot
|
||||||
|
pm2 startup
|
||||||
|
# Follow the instructions output by the command above
|
||||||
|
```
|
||||||
|
|
||||||
|
**Useful PM2 Commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View status
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
pm2 logs
|
||||||
|
|
||||||
|
# Restart all apps
|
||||||
|
pm2 restart all
|
||||||
|
|
||||||
|
# Restart specific app
|
||||||
|
pm2 restart award-backend
|
||||||
|
|
||||||
|
# Stop all apps
|
||||||
|
pm2 stop all
|
||||||
|
|
||||||
|
# Monitor resources
|
||||||
|
pm2 monit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Configure HAProxy
|
||||||
|
|
||||||
|
Edit `/etc/haproxy/haproxy.cfg`:
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
global
|
||||||
|
log /dev/log local0
|
||||||
|
log /dev/log local1 notice
|
||||||
|
chroot /var/lib/haproxy
|
||||||
|
stats socket /run/haproxy/admin.sock mode 660 level admin
|
||||||
|
stats timeout 30s
|
||||||
|
user haproxy
|
||||||
|
group haproxy
|
||||||
|
daemon
|
||||||
|
|
||||||
|
# Default SSL material locations
|
||||||
|
ca-base /etc/ssl/certs
|
||||||
|
crt-base /etc/ssl/private
|
||||||
|
|
||||||
|
# Default ciphers to use
|
||||||
|
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
|
||||||
|
ssl-default-bind-options no-sslv3
|
||||||
|
|
||||||
|
defaults
|
||||||
|
log global
|
||||||
|
mode http
|
||||||
|
option httplog
|
||||||
|
option dontlognull
|
||||||
|
timeout connect 5000
|
||||||
|
timeout client 50000
|
||||||
|
timeout server 50000
|
||||||
|
errorfile 400 /etc/haproxy/errors/400.http
|
||||||
|
errorfile 403 /etc/haproxy/errors/403.http
|
||||||
|
errorfile 408 /etc/haproxy/errors/408.http
|
||||||
|
errorfile 500 /etc/haproxy/errors/500.http
|
||||||
|
errorfile 502 /etc/haproxy/errors/502.http
|
||||||
|
errorfile 503 /etc/haproxy/errors/503.http
|
||||||
|
errorfile 504 /etc/haproxy/errors/504.http
|
||||||
|
|
||||||
|
# Statistics page (optional - secure with auth)
|
||||||
|
listen stats
|
||||||
|
bind *:8404
|
||||||
|
mode http
|
||||||
|
stats enable
|
||||||
|
stats uri /stats
|
||||||
|
stats refresh 30s
|
||||||
|
stats realm HAProxy\ Statistics
|
||||||
|
stats auth admin:your-secure-password
|
||||||
|
|
||||||
|
# Frontend redirect HTTP to HTTPS
|
||||||
|
frontend http-in
|
||||||
|
bind *:80
|
||||||
|
http-request redirect scheme https
|
||||||
|
|
||||||
|
# Frontend HTTPS
|
||||||
|
frontend https-in
|
||||||
|
bind *:443 ssl crt /etc/ssl/private/awards.dj7nt.de.pem
|
||||||
|
default_backend award-backend
|
||||||
|
|
||||||
|
# Backend configuration
|
||||||
|
backend award-backend
|
||||||
|
# Health check
|
||||||
|
option httpchk GET /api/health
|
||||||
|
|
||||||
|
# Single server serving both frontend and API
|
||||||
|
server award-backend 127.0.0.1:3001 check
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSL Certificate Setup:**
|
||||||
|
|
||||||
|
Using Let's Encrypt with Certbot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install certbot
|
||||||
|
apt install certbot
|
||||||
|
|
||||||
|
# Generate certificate
|
||||||
|
certbot certonly --standalone -d awards.dj7nt.de
|
||||||
|
|
||||||
|
# Combine certificate and key for HAProxy
|
||||||
|
cat /etc/letsencrypt/live/awards.dj7nt.de/fullchain.pem > /etc/ssl/private/awards.dj7nt.de.pem
|
||||||
|
cat /etc/letsencrypt/live/awards.dj7nt.de/privkey.pem >> /etc/ssl/private/awards.dj7nt.de.pem
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 600 /etc/ssl/private/awards.dj7nt.de.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Start HAProxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test configuration
|
||||||
|
haproxy -c -f /etc/haproxy/haproxy.cfg
|
||||||
|
|
||||||
|
# Restart HAProxy
|
||||||
|
systemctl restart haproxy
|
||||||
|
|
||||||
|
# Enable HAProxy on boot
|
||||||
|
systemctl enable haproxy
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
systemctl status haproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PM2 processes
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# Check HAProxy stats (if enabled)
|
||||||
|
curl http://localhost:8404/stats
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
curl https://awards.dj7nt.de/api/health
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
pm2 logs
|
||||||
|
tail -f /var/log/haproxy.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest changes
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Install updated dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Rebuild frontend (if UI changed)
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
# Preview the production build locally
|
# Restart PM2
|
||||||
bun run preview
|
pm2 restart award-backend
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deployment Options
|
### Database Backups
|
||||||
|
|
||||||
#### Option 1: Static Site + Backend Server
|
Set up automated backups:
|
||||||
|
|
||||||
1. Build the frontend: `bun run build`
|
|
||||||
2. Serve `src/frontend/build/` with Elysia using `@elysiajs/static`
|
|
||||||
3. Backend runs on one port serving both frontend and API
|
|
||||||
|
|
||||||
#### Option 2: Reverse Proxy (Recommended)
|
|
||||||
|
|
||||||
Use nginx or Caddy to proxy:
|
|
||||||
- `/` → SvelteKit frontend (port 5173 or static files)
|
|
||||||
- `/api` → Elysia backend (port 3001)
|
|
||||||
|
|
||||||
**Example nginx configuration:**
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
server_name awards.dj7nt.de;
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:5173;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://localhost:3001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Option 3: Single Process with SvelteKit Node Adapter
|
|
||||||
|
|
||||||
Use `@sveltejs/adapter-node` to build for Node/Bun:
|
|
||||||
- Everything runs in one process
|
|
||||||
- API routes handled by SvelteKit (need to migrate from Elysia)
|
|
||||||
|
|
||||||
### Environment Variables for Production
|
|
||||||
|
|
||||||
Make sure to set these in your production environment:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VITE_APP_URL=https://awards.dj7nt.de
|
# Create backup script
|
||||||
VITE_API_BASE_URL= # Leave empty for same-domain
|
cat > /usr/local/bin/backup-award.sh << 'EOF'
|
||||||
JWT_SECRET=<strong-random-string>
|
#!/bin/bash
|
||||||
NODE_ENV=production
|
BACKUP_DIR="/backups/award"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
cp /path/to/award/award.db $BACKUP_DIR/award_$DATE.db
|
||||||
|
|
||||||
|
# Keep last 30 days
|
||||||
|
find $BACKUP_DIR -name "award_*.db" -mtime +30 -delete
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /usr/local/bin/backup-award.sh
|
||||||
|
|
||||||
|
# Add to crontab (daily at 2 AM)
|
||||||
|
crontab -e
|
||||||
|
# Add line: 0 2 * * * /usr/local/bin/backup-award.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
**PM2 Monitoring:**
|
||||||
|
```bash
|
||||||
|
# Real-time monitoring
|
||||||
|
pm2 monit
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
pm2 logs --lines 100
|
||||||
|
```
|
||||||
|
|
||||||
|
**HAProxy Monitoring:**
|
||||||
|
- Access stats page: `http://your-server:8404/stats`
|
||||||
|
- Check logs: `tail -f /var/log/haproxy.log`
|
||||||
|
|
||||||
|
**Log Files Locations:**
|
||||||
|
- PM2 logs: `./logs/backend-error.log`, `./logs/frontend-error.log`
|
||||||
|
- HAProxy logs: `/var/log/haproxy.log`
|
||||||
|
- System logs: `journalctl -u haproxy -f`
|
||||||
|
|
||||||
|
### Security Checklist
|
||||||
|
|
||||||
|
- [ ] HTTPS enabled with valid SSL certificate
|
||||||
|
- [ ] Firewall configured (ufw/firewalld)
|
||||||
|
- [ ] JWT_SECRET is strong and randomly generated
|
||||||
|
- [ ] .env file has proper permissions (600)
|
||||||
|
- [ ] Database backups automated
|
||||||
|
- [ ] PM2 stats page secured with authentication
|
||||||
|
- [ ] HAProxy stats page secured (if publicly accessible)
|
||||||
|
- [ ] Regular security updates applied
|
||||||
|
- [ ] Log rotation configured for application logs
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Application won't start:**
|
||||||
|
```bash
|
||||||
|
# Check PM2 logs
|
||||||
|
pm2 logs --err
|
||||||
|
|
||||||
|
# Check if ports are in use
|
||||||
|
netstat -tulpn | grep -E ':(3001|5173)'
|
||||||
|
|
||||||
|
# Verify environment variables
|
||||||
|
pm2 env 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**HAProxy not forwarding requests:**
|
||||||
|
```bash
|
||||||
|
# Test backend directly
|
||||||
|
curl http://localhost:3001/api/health
|
||||||
|
curl http://localhost:5173/
|
||||||
|
|
||||||
|
# Check HAProxy configuration
|
||||||
|
haproxy -c -f /etc/haproxy/haproxy.cfg
|
||||||
|
|
||||||
|
# View HAProxy logs
|
||||||
|
tail -f /var/log/haproxy.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database issues:**
|
||||||
|
```bash
|
||||||
|
# Check database file permissions
|
||||||
|
ls -la award.db
|
||||||
|
|
||||||
|
# Verify database integrity
|
||||||
|
bun run db:studio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features in Detail
|
## Features in Detail
|
||||||
|
|
||||||
### Background Job Queue
|
### Background Job Queue
|
||||||
|
|||||||
22
ecosystem.config.js
Normal file
22
ecosystem.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'award-backend',
|
||||||
|
script: 'src/backend/index.js',
|
||||||
|
interpreter: 'bun',
|
||||||
|
cwd: '/path/to/award',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 3001
|
||||||
|
},
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: 'fork',
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: '500M',
|
||||||
|
error_file: './logs/backend-error.log',
|
||||||
|
out_file: './logs/backend-out.log',
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Main backend application
|
* Main backend application
|
||||||
* Serves API routes
|
* Serves API routes and static frontend files
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get allowed origins from environment or allow all in development
|
// Get allowed origins from environment or allow all in development
|
||||||
@@ -85,30 +85,30 @@ const app = new Elysia()
|
|||||||
.post(
|
.post(
|
||||||
'/api/auth/register',
|
'/api/auth/register',
|
||||||
async ({ body, jwt, set }) => {
|
async ({ body, jwt, set }) => {
|
||||||
try {
|
// Create user
|
||||||
// Create user
|
const user = await registerUser(body);
|
||||||
const user = await registerUser(body);
|
|
||||||
|
|
||||||
// Generate JWT token
|
if (!user) {
|
||||||
const token = await jwt.sign({
|
|
||||||
userId: user.id,
|
|
||||||
email: user.email,
|
|
||||||
callsign: user.callsign,
|
|
||||||
});
|
|
||||||
|
|
||||||
set.status = 201;
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
token,
|
|
||||||
user,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
set.status = 400;
|
set.status = 400;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: 'Email already registered',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = await jwt.sign({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
callsign: user.callsign,
|
||||||
|
});
|
||||||
|
|
||||||
|
set.status = 201;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -136,29 +136,29 @@ const app = new Elysia()
|
|||||||
.post(
|
.post(
|
||||||
'/api/auth/login',
|
'/api/auth/login',
|
||||||
async ({ body, jwt, set }) => {
|
async ({ body, jwt, set }) => {
|
||||||
try {
|
// Authenticate user
|
||||||
// Authenticate user
|
const user = await authenticateUser(body.email, body.password);
|
||||||
const user = await authenticateUser(body.email, body.password);
|
|
||||||
|
|
||||||
// Generate JWT token
|
if (!user) {
|
||||||
const token = await jwt.sign({
|
|
||||||
userId: user.id,
|
|
||||||
email: user.email,
|
|
||||||
callsign: user.callsign,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
token,
|
|
||||||
user,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
set.status = 401;
|
set.status = 401;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid email or password',
|
error: 'Invalid email or password',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = await jwt.sign({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
callsign: user.callsign,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -520,22 +520,21 @@ const app = new Elysia()
|
|||||||
return { success: false, error: 'Unauthorized' };
|
return { success: false, error: 'Unauthorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { awardId } = params;
|
||||||
const { awardId } = params;
|
const progress = await getAwardProgressDetails(user.id, awardId);
|
||||||
const progress = await getAwardProgressDetails(user.id, awardId);
|
|
||||||
|
|
||||||
return {
|
if (!progress) {
|
||||||
success: true,
|
set.status = 404;
|
||||||
...progress,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error calculating award progress', { error: error.message });
|
|
||||||
set.status = 500;
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || 'Failed to calculate award progress',
|
error: 'Award not found',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
...progress,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -548,22 +547,21 @@ const app = new Elysia()
|
|||||||
return { success: false, error: 'Unauthorized' };
|
return { success: false, error: 'Unauthorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { awardId } = params;
|
||||||
const { awardId } = params;
|
const breakdown = await getAwardEntityBreakdown(user.id, awardId);
|
||||||
const breakdown = await getAwardEntityBreakdown(user.id, awardId);
|
|
||||||
|
|
||||||
return {
|
if (!breakdown) {
|
||||||
success: true,
|
set.status = 404;
|
||||||
...breakdown,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error fetching award entities', { error: error.message });
|
|
||||||
set.status = 500;
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || 'Failed to fetch award entities',
|
error: 'Award not found',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
...breakdown,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
@@ -572,6 +570,117 @@ const app = new Elysia()
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Serve static files and SPA fallback for all non-API routes
|
||||||
|
.get('/*', ({ request }) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
|
||||||
|
// Don't intercept API routes
|
||||||
|
if (pathname.startsWith('/api')) {
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common missing assets before trying to open files
|
||||||
|
// This prevents Elysia from trying to get file size of non-existent files
|
||||||
|
const commonMissingFiles = ['/favicon.ico', '/robots.txt'];
|
||||||
|
if (commonMissingFiles.includes(pathname)) {
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SvelteKit assets path - replace %sveltekit.assets% with the assets directory
|
||||||
|
if (pathname.startsWith('/%sveltekit.assets%/')) {
|
||||||
|
// Extract the actual file path after %sveltekit.assets%/
|
||||||
|
const assetPath = pathname.replace('/%sveltekit.assets%/', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to serve from assets directory first
|
||||||
|
const assetsPath = `src/frontend/build/_app/immutable/assets/${assetPath}`;
|
||||||
|
const file = Bun.file(assetsPath);
|
||||||
|
const exists = file.exists();
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
return new Response(file);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Fall through to 404
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in assets, try root directory
|
||||||
|
try {
|
||||||
|
const rootFile = Bun.file(`src/frontend/build/${assetPath}`);
|
||||||
|
const rootExists = rootFile.exists();
|
||||||
|
if (rootExists) {
|
||||||
|
return new Response(rootFile);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Fall through to 404
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to serve the file from the build directory
|
||||||
|
// Remove leading slash for file path
|
||||||
|
const filePath = pathname === '/' ? '/index.html' : pathname;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullPath = `src/frontend/build${filePath}`;
|
||||||
|
|
||||||
|
// Use Bun.file() which doesn't throw for non-existent files
|
||||||
|
const file = Bun.file(fullPath);
|
||||||
|
const exists = file.exists();
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
// Determine content type
|
||||||
|
const ext = filePath.split('.').pop();
|
||||||
|
const contentTypes = {
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'css': 'text/css',
|
||||||
|
'html': 'text/html; charset=utf-8',
|
||||||
|
'json': 'application/json',
|
||||||
|
'png': 'image/png',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'svg': 'image/svg+xml',
|
||||||
|
'ico': 'image/x-icon',
|
||||||
|
'woff': 'font/woff',
|
||||||
|
'woff2': 'font/woff2',
|
||||||
|
'ttf': 'font/ttf',
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
if (contentTypes[ext]) {
|
||||||
|
headers['Content-Type'] = contentTypes[ext];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache headers
|
||||||
|
if (ext === 'html') {
|
||||||
|
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||||
|
} else {
|
||||||
|
headers['Cache-Control'] = 'public, max-age=86400';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(file, { headers });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// File not found or error, fall through to SPA fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPA fallback - serve index.html for all other routes
|
||||||
|
try {
|
||||||
|
const indexFile = Bun.file('src/frontend/build/index.html');
|
||||||
|
return new Response(indexFile, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new Response('Frontend not built. Run `bun run build`', { status: 503 });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
.listen(3001);
|
.listen(3001);
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ async function verifyPassword(password, hash) {
|
|||||||
* @param {string} userData.email - User email
|
* @param {string} userData.email - User email
|
||||||
* @param {string} userData.password - Plain text password
|
* @param {string} userData.password - Plain text password
|
||||||
* @param {string} userData.callsign - Ham radio callsign
|
* @param {string} userData.callsign - Ham radio callsign
|
||||||
* @returns {Promise<Object>} Created user object (without password)
|
* @returns {Promise<Object|null>} Created user object (without password) or null if email exists
|
||||||
* @throws {Error} If email already exists
|
|
||||||
*/
|
*/
|
||||||
export async function registerUser({ email, password, callsign }) {
|
export async function registerUser({ email, password, callsign }) {
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
@@ -42,7 +41,7 @@ export async function registerUser({ email, password, callsign }) {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new Error('Email already registered');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
// Hash password
|
||||||
@@ -79,13 +78,13 @@ export async function authenticateUser(email, password) {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('Invalid email or password');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
const isValid = await verifyPassword(password, user.passwordHash);
|
const isValid = await verifyPassword(password, user.passwordHash);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error('Invalid email or password');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return user without password hash
|
// Return user without password hash
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ export async function getAwardProgressDetails(userId, awardId) {
|
|||||||
const award = definitions.find((def) => def.id === awardId);
|
const award = definitions.find((def) => def.id === awardId);
|
||||||
|
|
||||||
if (!award) {
|
if (!award) {
|
||||||
throw new Error('Award not found');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate progress
|
// Calculate progress
|
||||||
@@ -572,7 +572,7 @@ export async function getAwardEntityBreakdown(userId, awardId) {
|
|||||||
const award = definitions.find((def) => def.id === awardId);
|
const award = definitions.find((def) => def.id === awardId);
|
||||||
|
|
||||||
if (!award) {
|
if (!award) {
|
||||||
throw new Error('Award not found');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { rules } = award;
|
let { rules } = award;
|
||||||
|
|||||||
@@ -107,7 +107,12 @@ async function processJobAsync(jobId, userId, type, data) {
|
|||||||
// Get the processor for this job type
|
// Get the processor for this job type
|
||||||
const processor = jobProcessors[type];
|
const processor = jobProcessors[type];
|
||||||
if (!processor) {
|
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
|
// Execute the job processor
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
|||||||
for (let attempt = 0; attempt < POLLING_CONFIG.maxRetries; attempt++) {
|
for (let attempt = 0; attempt < POLLING_CONFIG.maxRetries; attempt++) {
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
if (elapsed > POLLING_CONFIG.maxTotalTime) {
|
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) {
|
if (attempt > 0) {
|
||||||
@@ -104,9 +106,9 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
|||||||
await sleep(POLLING_CONFIG.retryDelay);
|
await sleep(POLLING_CONFIG.retryDelay);
|
||||||
continue;
|
continue;
|
||||||
} else if (response.status === 401) {
|
} 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) {
|
} 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 {
|
} else {
|
||||||
logger.warn(`LoTW returned ${response.status}, retrying...`);
|
logger.warn(`LoTW returned ${response.status}, retrying...`);
|
||||||
await sleep(POLLING_CONFIG.retryDelay);
|
await sleep(POLLING_CONFIG.retryDelay);
|
||||||
@@ -117,7 +119,7 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
|||||||
const adifData = await response.text();
|
const adifData = await response.text();
|
||||||
|
|
||||||
if (adifData.toLowerCase().includes('username/password incorrect')) {
|
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();
|
const header = adifData.trim().substring(0, 39).toLowerCase();
|
||||||
@@ -127,7 +129,8 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
|||||||
await sleep(POLLING_CONFIG.retryDelay);
|
await sleep(POLLING_CONFIG.retryDelay);
|
||||||
continue;
|
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 });
|
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);
|
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.`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.0",
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.49.1",
|
"@sveltejs/kit": "^2.49.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"svelte": "^5.45.6",
|
"svelte": "^5.45.6",
|
||||||
@@ -134,6 +135,8 @@
|
|||||||
|
|
||||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="],
|
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="],
|
||||||
|
|
||||||
|
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
|
||||||
|
|
||||||
"@sveltejs/kit": ["@sveltejs/kit@2.49.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg=="],
|
"@sveltejs/kit": ["@sveltejs/kit@2.49.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg=="],
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.0",
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.49.1",
|
"@sveltejs/kit": "^2.49.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"svelte": "^5.45.6",
|
"svelte": "^5.45.6",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter({
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: 'index.html',
|
||||||
|
precompress: false,
|
||||||
|
strict: true
|
||||||
|
}),
|
||||||
// Get app URL from environment or default to localhost
|
// Get app URL from environment or default to localhost
|
||||||
// This is used for production builds and CSRF configuration
|
// This is used for production builds and CSRF configuration
|
||||||
// Set via VITE_APP_URL environment variable
|
// Set via VITE_APP_URL environment variable
|
||||||
|
|||||||
Reference in New Issue
Block a user