Add rate limiting and extend JWT expiry

- Wire in Fastify rate-limit (backend) and add dependency
- Update JWT expiresIn to 24h in config
- Remove legacy auth middleware
- Add healthcheck to deployment (deploy.sh)
- Minor frontend tweaks: include average time per round in logs
- Remove Dashboard link from dashboard.html
This commit is contained in:
2026-01-16 09:53:35 +01:00
parent 7cecc0c775
commit b0e52a778f
14 changed files with 165 additions and 754 deletions

View File

@@ -1,234 +0,0 @@
# Production Release Checklist
Use this checklist before deploying to production. Ensure all items are completed and verified.
## Pre-Deployment (1-2 weeks before)
### Code Quality & Testing
- [ ] All tests pass locally (`npm test` or equivalent)
- [ ] Code review completed for all changes
- [ ] No console warnings or errors in development
- [ ] Dependencies are up-to-date and security scanning passed
- [ ] Linting passes (`npm run lint`)
### Documentation
- [ ] README.md is updated with latest features
- [ ] DEPLOYMENT.md is complete and accurate
- [ ] API documentation is current
- [ ] Configuration options are documented
### Security Review
- [ ] No hardcoded secrets in codebase
- [ ] All API endpoints validate input properly
- [ ] Database queries use parameterized statements (no SQL injection)
- [ ] CORS configuration is restrictive (specific domains)
- [ ] Password hashing is using bcryptjs with 12+ rounds
- [ ] JWT tokens have proper expiration (15 minutes)
- [ ] Rate limiting is configured (enabled in docker-compose.prod.yml)
### Database
- [ ] Database schema is final and tested
- [ ] All migrations are tested and reversible
- [ ] Database views are optimized
- [ ] Indexes are in place for frequently queried columns
- [ ] Backup and restore procedures are documented and tested
### Performance
- [ ] Bundle size is reasonable (< 5MB for frontend)
- [ ] Database queries are optimized
- [ ] API response times are acceptable (< 500ms for 95th percentile)
- [ ] Static assets have caching enabled
- [ ] Gzip compression is enabled
## Deployment Week
### Image Build & Push
- [ ] Version number is incremented (semantic versioning)
- [ ] Git tag is created: `git tag v1.0.0`
- [ ] Deployment script runs successfully: `./deploy.sh 1.0.0`
- [ ] Images are pushed to GitHub Container Registry
- [ ] Image sizes are reasonable (backend < 200MB, frontend < 100MB)
- [ ] Image scanning shows no critical vulnerabilities
### Server Preparation
- [ ] Server has Docker and Docker Compose installed
- [ ] Firewall rules allow necessary ports (80, 443)
- [ ] SSL certificates are ready (if using HTTPS)
- [ ] Domain DNS is configured and resolving
- [ ] Disk space is sufficient (>10GB recommended)
- [ ] Server has adequate resources (2GB RAM minimum, 1 CPU)
### Configuration
- [ ] `.env` file created with all required variables
- [ ] `CORS_ORIGIN` set to correct domain
- [ ] `ALLOW_REGISTRATION` set appropriately (false by default)
- [ ] JWT_SECRET is securely generated and stored
- [ ] Log level is set to 'warn' in production
- [ ] Database path points to persistent volume
### Secrets Management
- [ ] Docker secret 'jwt_secret' is created
- [ ] Secret file is securely stored and deleted after import
- [ ] Docker secret command tested: `docker secret ls`
- [ ] Backup of jwt_secret stored securely (offsite)
## Day Before Deployment
### Final Verification
- [ ] Run latest images locally with `docker-compose up -d`
- [ ] Test all major features work correctly
- [ ] Check database is created and migrations run
- [ ] Verify API endpoints respond correctly
- [ ] Test authentication (login, registration if enabled)
- [ ] Test game logging and statistics
- [ ] Verify frontend loads and is responsive
### Backup Everything
- [ ] Current database backed up (if migrating existing data)
- [ ] Configuration files backed up
- [ ] DNS settings noted and ready to switch
- [ ] Rollback plan documented and tested
### Team Communication
- [ ] Team notified of deployment schedule
- [ ] Maintenance window communicated to users
- [ ] Rollback contact information shared
- [ ] Deployment plan reviewed with team
## Deployment Day
### Pre-Deployment (1 hour before)
- [ ] All services currently running and stable
- [ ] Database integrity verified
- [ ] Recent backups completed
- [ ] Monitoring tools configured and running
- [ ] Team members available for issues
### Deployment Steps
1. [ ] Pull latest images from GHCR
```bash
docker pull ghcr.io/YOUR_USER/edh-stats-backend:1.0.0
docker pull ghcr.io/YOUR_USER/edh-stats-frontend:1.0.0
```
2. [ ] Stop current services (if upgrading)
```bash
docker-compose down
```
3. [ ] Update docker-compose.yml with new versions
4. [ ] Start new services
```bash
docker-compose up -d
```
5. [ ] Wait for services to become healthy (check health checks)
```bash
docker-compose ps
```
### Post-Deployment Verification (immediate)
- [ ] All containers are running: `docker-compose ps`
- [ ] Backend health check passes: `curl http://localhost:3000/api/health`
- [ ] Frontend loads: `curl http://localhost/`
- [ ] No error logs: `docker-compose logs | grep ERROR`
- [ ] Database is accessible and has data
- [ ] API endpoints respond (test authentication)
- [ ] UI loads correctly in browser
- [ ] Forms work (try logging a game if applicable)
### Testing (15-30 minutes after)
- [ ] Test user login functionality
- [ ] Test game logging (if enabled)
- [ ] Test viewing statistics
- [ ] Test editing/deleting records
- [ ] Check browser console for JavaScript errors
- [ ] Verify HTTPS/SSL (if configured)
- [ ] Test on mobile device
### Monitoring (first 24 hours)
- [ ] Monitor error logs every 30 minutes
- [ ] Monitor resource usage (CPU, memory)
- [ ] Check database size and integrity
- [ ] Monitor API response times
- [ ] Review user feedback/issues reported
- [ ] Ensure backups are being created
## If Issues Occur
### Quick Diagnostics
```bash
# Check service status
docker-compose ps
# View recent logs
docker-compose logs --tail 50 backend
docker-compose logs --tail 50 frontend
# Check resource usage
docker stats
# Test backend connectivity
curl -v http://localhost:3000/api/health
# Test database
docker-compose exec backend sqlite3 /app/database/data/edh-stats.db "SELECT COUNT(*) FROM users;"
```
### Rollback Procedure
1. [ ] Stop current version: `docker-compose down`
2. [ ] Update docker-compose.yml to previous version
3. [ ] Restore database backup if needed
4. [ ] Restart with previous version: `docker-compose up -d`
5. [ ] Verify service health
6. [ ] Notify team and users
## Post-Deployment (Next 24-48 hours)
### Stability Monitoring
- [ ] No error spikes in logs
- [ ] Database size is stable
- [ ] API response times are acceptable
- [ ] No memory leaks or increasing CPU usage
- [ ] User authentication working smoothly
- [ ] No reported critical issues
### Documentation & Communication
- [ ] Update version number in documentation
- [ ] Post release notes (what was changed)
- [ ] Thank team for their efforts
- [ ] Respond to any user questions/feedback
### Metrics & Learning
- [ ] Deployment time was within expectations
- [ ] Zero downtime achieved (if applicable)
- [ ] Document any unexpected issues and resolutions
- [ ] Identify improvements for next deployment
- [ ] Update deployment checklist based on learnings
## Success Criteria
Deployment is successful when:
✅ All services are running and healthy
✅ All endpoints respond correctly
✅ Database has all data and is accessible
✅ Users can login and use the application
✅ No critical errors in logs
✅ Performance is acceptable (< 500ms response time)
✅ SSL/HTTPS working (if configured)
✅ Backups are being created regularly
✅ Team is confident in stability
✅ Users are satisfied with functionality
---
**Version**: 1.0.0
**Last Updated**: 2024-01-15
**Maintained by**: [Your Team Name]

View File

@@ -1,216 +0,0 @@
# Quick Fix for EDH Stats Deployment Issues
## Issue 1: Backend - "unable to open database file"
## Issue 2: Frontend - "host not found in upstream 'backend'"
This guide fixes both issues immediately.
## Step 1: Stop Services
```bash
docker-compose down
```
## Step 2: Update nginx.prod.conf
The nginx config needs to dynamically resolve the backend hostname. On your server, edit the nginx config:
Replace the HTTP/HTTPS server blocks with this simpler version:
```nginx
# Use resolver to dynamically resolve backend hostname
resolver 127.0.0.11 valid=10s;
set $backend_backend "backend:3000";
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Rate limited API proxy
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://$backend_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# Rate limited static files
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
limit_req zone=static burst=50 nodelay;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Main application routes
location / {
limit_req zone=static burst=10 nodelay;
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
```
## Step 3: Update docker-compose.yml - Frontend Ports
If other containers are using ports 80 and 443, update the frontend service to use alternate ports:
```yaml
frontend:
image: ghcr.io/zlorfi/edh-stats-frontend:v1.0.5
ports:
- '38080:80' # Changed from 80:80
- '30443:443' # Changed from 443:443
restart: unless-stopped
networks:
- edh-stats-network
```
Then access the app at:
- HTTP: `http://your-server:38080`
- HTTPS: `https://your-server:30443` (when SSL configured)
## Step 4: Update docker-compose.yml - Init Service
Update the `init-db` service to use more permissive permissions:
```yaml
init-db:
image: alpine:latest
volumes:
- sqlite_data:/app/database/data
- app_logs:/app/logs
command: sh -c "mkdir -p /app/database/data /app/logs && chmod 777 /app/database/data /app/logs && touch /app/database/data/.initialized && echo 'Database directories initialized'"
networks:
- edh-stats-network
restart: "no"
```
Key changes:
- Changed `chmod 755` to `chmod 777` for full read/write permissions
- Added `touch /app/database/data/.initialized` to create a test file
- Added `restart: "no"` so the init service doesn't restart
## Step 5: Clear Existing Volumes (Fresh Start)
⚠️ **WARNING: This will DELETE your database if you have data!**
If you want to start fresh:
```bash
docker volume rm edh-stats_sqlite_data edh-stats_app_logs 2>/dev/null || true
```
## Step 6: Start Services
```bash
docker-compose up -d
```
## Step 7: Monitor Startup
```bash
# Watch the init-db service create directories
docker-compose logs -f init-db
# Then watch the backend startup
docker-compose logs -f backend
# Check if all services are running
docker-compose ps
```
## Expected Output
After init-db runs and exits, you should see:
```
backend-1 | Database initialized successfully
backend-1 | Server started on port 3000
```
And:
```
frontend-1 | ... ready for start up
```
## Verify Everything Works
```bash
# Check API is responding (backend runs on port 3000 internally)
curl http://localhost:3000/api/health
# Check frontend is responding (now on port 38080)
curl http://localhost:38080/
# All containers running?
docker-compose ps
# Should show: backend (running), frontend (running), init-db (exited)
# Access the app in your browser
# HTTP: http://your-server-ip:38080
# HTTPS: https://your-server-ip:30443 (when SSL is configured)
```
## What Changed and Why
### Nginx Config Issue
The old config tried to resolve `backend` hostname at startup time before the backend container was ready. By using `resolver` and `set $backend_backend`, nginx defers resolution until request time, when the backend is running.
### Database Permissions Issue
Docker volumes sometimes have restricted permissions. By changing to `chmod 777`, the container has full permission to create and write the database file. The `touch` command creates a test file to verify permissions work.
### Init Service
The `init-db` service runs BEFORE the backend and ensures:
1. Directories exist
2. Directories are writable
3. Backend can then create the database successfully
## Still Having Issues?
### Check volume exists:
```bash
docker volume ls | grep edh-stats
```
### Check volume path is writable:
```bash
VOLUME_PATH=$(docker volume inspect edh-stats_sqlite_data | grep -o '"Mountpoint": "[^"]*' | cut -d'"' -f4)
echo "Volume at: $VOLUME_PATH"
ls -la "$VOLUME_PATH"
```
### Remove and recreate volumes:
```bash
docker-compose down
docker volume rm edh-stats_sqlite_data edh-stats_app_logs
docker-compose up -d
```
### Check logs for specific errors:
```bash
docker-compose logs --tail 100 backend
docker-compose logs --tail 100 frontend
```

17
TODO.md Normal file
View File

@@ -0,0 +1,17 @@
# ToDos/Bugs to check and adjust
## games.html
[ ] export logs to JSON via a button in the top right corner, the file should have the current date in the name i.e: edh_games_16_01_2026.json
## round-counter.html
[ ] check if every function in round-counter.js is being used
[ ] instead of Stop Game -> Pause game and do not reset the time counter
[ ] End Game & Log Results -> reset the counter
[ ] Rest button -> has issues to display on mobile
## login.html
[ ] check the cookie for remembering the login credentials, how many seconds does it work?
[ ] prolong the cookie to stay loged in, when using the Remember me checkbox

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@fastify/cors": "^8.4.0",
"@fastify/jwt": "^7.2.0",
"@fastify/rate-limit": "^10.3.0",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^9.2.2",
"close-with-grace": "^1.2.0",
@@ -184,6 +185,43 @@
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/@fastify/rate-limit": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz",
"integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"fastify-plugin": "^5.0.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/rate-limit/node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",

View File

@@ -17,6 +17,7 @@
"dependencies": {
"@fastify/cors": "^8.4.0",
"@fastify/jwt": "^7.2.0",
"@fastify/rate-limit": "^10.3.0",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^9.2.2",
"close-with-grace": "^1.2.0",

View File

@@ -12,7 +12,7 @@ config({ path: resolve(rootDir, '.env') })
export const jwtConfig = {
secret: process.env.JWT_SECRET || 'fallback-secret-for-development',
algorithm: 'HS512',
expiresIn: '15m',
expiresIn: '24h',
refreshExpiresIn: '7d',
issuer: 'edh-stats',
audience: 'edh-stats-users'
@@ -23,12 +23,6 @@ export const corsConfig = {
credentials: true
}
export const rateLimitConfig = {
max: parseInt(process.env.RATE_LIMIT_MAX) || 100,
timeWindow: parseInt(process.env.RATE_LIMIT_WINDOW) || 15 * 60 * 1000, // 15 minutes
skipOnError: false
}
export const serverConfig = {
port: parseInt(process.env.PORT) || 3000,
host: process.env.HOST || '0.0.0.0',
@@ -37,14 +31,6 @@ export const serverConfig = {
}
}
export const securityConfig = {
bcryptSaltRounds: 12,
passwordMinLength: 8,
usernameMinLength: 3,
commanderNameMinLength: 2,
maxNotesLength: 1000
}
export const registrationConfig = {
allowRegistration: process.env.ALLOW_REGISTRATION !== 'false'
}

View File

@@ -1,119 +0,0 @@
// Verify JWT token
export const verifyJWT = async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
reply.code(401).send({
error: 'Unauthorized',
message: 'Invalid or expired token'
})
}
}
// Optional JWT verification (doesn't fail if no token)
export const optionalJWT = async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
// Token is optional, so we don't fail
request.user = null
}
}
// Check if user exists and is active
export const validateUser = async (request, reply) => {
try {
const user = request.user
if (!user) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User not authenticated'
})
}
// You could add additional user validation here
// e.g., check if user is active, banned, etc.
} catch (err) {
reply.code(500).send({
error: 'Internal Server Error',
message: 'Failed to validate user'
})
}
}
// Extract user ID from token and validate resource ownership
export const validateOwnership = (
resourceParam = 'id',
resourceTable = 'commanders'
) => {
return async (request, reply) => {
try {
const user = request.user
if (!user) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User not authenticated'
})
}
const resourceId = request.params[resourceParam]
if (!resourceId) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Resource ID not provided'
})
}
const db = await import('../config/database.js').then((m) => m.default)
const database = await db.initialize()
// Check if user owns the resource
const query = `SELECT user_id FROM ${resourceTable} WHERE id = ?`
const resource = database.prepare(query).get([resourceId])
if (!resource) {
return reply.code(404).send({
error: 'Not Found',
message: 'Resource not found'
})
}
if (resource.user_id !== user.id) {
return reply.code(403).send({
error: 'Forbidden',
message: 'Access denied to this resource'
})
}
// Add resource to request object for later use
request.resourceId = resourceId
} catch (err) {
reply.code(500).send({
error: 'Internal Server Error',
message: 'Failed to validate ownership'
})
}
}
}
// Rate limiting middleware for sensitive endpoints
export const rateLimitAuth = {
config: {
rateLimit: {
max: 5, // 5 requests
timeWindow: '1 minute', // per minute
skipOnError: false
}
}
}
// Rate limiting for general API endpoints
export const rateLimitGeneral = {
config: {
rateLimit: {
max: 100, // 100 requests
timeWindow: '1 minute', // per minute
skipOnError: false
}
}
}

View File

@@ -1,4 +1,5 @@
import fastify from 'fastify'
import rateLimit from '@fastify/rate-limit'
import cors from '@fastify/cors'
import jwt from '@fastify/jwt'
import closeWithGrace from 'close-with-grace'
@@ -23,6 +24,10 @@ export default async function build(opts = {}) {
secret: jwtConfig.secret
})
await app.register(rateLimit, {
global: false
})
// Authentication decorator
app.decorate('authenticate', async (request, reply) => {
try {

View File

@@ -321,6 +321,14 @@ services:
- '38080:80'
- '30443:443'
restart: unless-stopped
healthcheck:
test:
- CMD
- wget
- http://localhost:80/health
interval: 10s
timeout: 5s
retries: 5
networks:
- edh-stats-network

View File

@@ -87,4 +87,4 @@ http {
internal;
}
}
}
}

View File

@@ -20,11 +20,6 @@
<div class="flex items-center space-x-4">
<h1 class="text-2xl font-bold font-mtg">EDH Stats</h1>
<div class="hidden md:flex space-x-6">
<a
href="/dashboard.html"
class="text-white hover:text-edh-accent transition-colors"
>Dashboard</a
>
<a
href="/commanders.html"
class="text-white hover:text-edh-accent transition-colors"
@@ -318,12 +313,7 @@
x-show="topCommanders.length === 0"
class="text-center py-8 text-gray-500"
>
<p>No commanders added yet.</p>
<a
href="/commanders.html"
class="text-edh-accent hover:text-edh-primary"
>Add your first commander</a
>
<p>No stats yet.</p>
</div>
</div>
</div>

View File

@@ -76,7 +76,8 @@ function gameManager() {
data.date || new Date().toISOString().split('T')[0]
this.newGame.rounds = data.rounds || 8
this.newGame.notes =
`Ended after ${data.rounds} rounds in ${data.duration}` || ''
`Ended after ${data.rounds} rounds in ${data.duration}.\nAverage time/round: ${data.avgTimePerRound}` ||
''
// Show the form automatically
this.showLogForm = true
@@ -166,29 +167,29 @@ function gameManager() {
}
},
async handleCreateGame() {
this.submitting = true
async handleCreateGame() {
this.submitting = true
try {
// Ensure boolean values are actual booleans, not strings
const payload = {
date: this.newGame.date,
commanderId: parseInt(this.newGame.commanderId),
playerCount: parseInt(this.newGame.playerCount),
rounds: parseInt(this.newGame.rounds),
won: this.newGame.won === true || this.newGame.won === 'true',
startingPlayerWon:
this.newGame.startingPlayerWon === true ||
this.newGame.startingPlayerWon === 'true',
solRingTurnOneWon:
this.newGame.solRingTurnOneWon === true ||
this.newGame.solRingTurnOneWon === 'true'
}
// Only include notes if it's not empty
if (this.newGame.notes && this.newGame.notes.trim()) {
payload.notes = this.newGame.notes
}
try {
// Ensure boolean values are actual booleans, not strings
const payload = {
date: this.newGame.date,
commanderId: parseInt(this.newGame.commanderId),
playerCount: parseInt(this.newGame.playerCount),
rounds: parseInt(this.newGame.rounds),
won: this.newGame.won === true || this.newGame.won === 'true',
startingPlayerWon:
this.newGame.startingPlayerWon === true ||
this.newGame.startingPlayerWon === 'true',
solRingTurnOneWon:
this.newGame.solRingTurnOneWon === true ||
this.newGame.solRingTurnOneWon === 'true'
}
// Only include notes if it's not empty
if (this.newGame.notes && this.newGame.notes.trim()) {
payload.notes = this.newGame.notes
}
const response = await fetch('/api/games', {
method: 'POST',
@@ -217,28 +218,28 @@ function gameManager() {
}
},
async handleUpdateGame() {
this.editSubmitting = true
async handleUpdateGame() {
this.editSubmitting = true
try {
const payload = {
date: this.editingGame.date,
commanderId: parseInt(this.editingGame.commanderId),
playerCount: parseInt(this.editingGame.playerCount),
rounds: parseInt(this.editingGame.rounds),
won: this.editingGame.won === true || this.editingGame.won === 'true',
startingPlayerWon:
this.editingGame.startingPlayerWon === true ||
this.editingGame.startingPlayerWon === 'true',
solRingTurnOneWon:
this.editingGame.solRingTurnOneWon === true ||
this.editingGame.solRingTurnOneWon === 'true'
}
// Only include notes if it's not empty
if (this.editingGame.notes && this.editingGame.notes.trim()) {
payload.notes = this.editingGame.notes
}
try {
const payload = {
date: this.editingGame.date,
commanderId: parseInt(this.editingGame.commanderId),
playerCount: parseInt(this.editingGame.playerCount),
rounds: parseInt(this.editingGame.rounds),
won: this.editingGame.won === true || this.editingGame.won === 'true',
startingPlayerWon:
this.editingGame.startingPlayerWon === true ||
this.editingGame.startingPlayerWon === 'true',
solRingTurnOneWon:
this.editingGame.solRingTurnOneWon === true ||
this.editingGame.solRingTurnOneWon === 'true'
}
// Only include notes if it's not empty
if (this.editingGame.notes && this.editingGame.notes.trim()) {
payload.notes = this.editingGame.notes
}
const response = await fetch(`/api/games/${this.editingGame.id}`, {
method: 'PUT',

View File

@@ -51,7 +51,7 @@ function roundCounterApp() {
confirmReset() {
this.resetConfirm.resetting = true
// Small delay to simulate work and show loading state
setTimeout(() => {
this.counterActive = false
@@ -78,11 +78,6 @@ function roundCounterApp() {
}
},
jumpToRound(round) {
this.currentRound = round
this.saveCounter()
},
startTimer() {
this.timerInterval = setInterval(() => {
if (this.counterActive && this.startTime) {
@@ -159,18 +154,6 @@ function roundCounterApp() {
}
},
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch((err) => {
console.log(`Error attempting to enable fullscreen: ${err.message}`)
})
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
},
saveAndGoToGameLog() {
// Save the complete game data to localStorage for the game log page
const now = new Date()

View File

@@ -9,7 +9,7 @@
content="Live round counter for Magic: The Gathering EDH/Commander games"
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body class="h-full flex flex-col" x-data="roundCounterApp()">
@@ -19,28 +19,6 @@
<div class="flex justify-between items-center">
<div class="flex items-center space-x-4">
<h1 class="text-2xl font-bold font-mtg">EDH Stats</h1>
<div class="hidden md:flex space-x-6">
<a
href="/dashboard.html"
class="text-white hover:text-edh-accent transition-colors"
>Dashboard</a
>
<a
href="/commanders.html"
class="text-white hover:text-edh-accent transition-colors"
>Commanders</a
>
<a
href="/games.html"
class="text-white hover:text-edh-accent transition-colors"
>Log Game</a
>
<a
href="/stats.html"
class="text-white hover:text-edh-accent transition-colors"
>Statistics</a
>
</div>
</div>
<div class="flex items-center space-x-4">
@@ -141,37 +119,6 @@
</div>
</div>
<!-- Quick Actions -->
<div class="card mb-8">
<h3 class="text-xl font-semibold mb-4">Quick Actions</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<button
@click="jumpToRound(5)"
class="p-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-center font-semibold transition-colors"
>
Jump to Round 5
</button>
<button
@click="jumpToRound(7)"
class="p-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-center font-semibold transition-colors"
>
Jump to Round 7
</button>
<button
@click="jumpToRound(10)"
class="p-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-center font-semibold transition-colors"
>
Jump to Round 10
</button>
<button
@click="toggleFullscreen()"
class="p-3 bg-gray-100 hover:bg-gray-200 rounded-lg text-center font-semibold transition-colors"
>
Fullscreen
</button>
</div>
</div>
<!-- Game Stats Card -->
<div class="card">
<h3 class="text-xl font-semibold mb-4">Game Statistics</h3>
@@ -209,49 +156,53 @@
End Game & Log Results
</button>
</div>
</div>
</main>
</div>
</main>
<!-- Reset Confirmation Modal -->
<div
x-show="resetConfirm.show"
x-cloak
x-transition
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="resetConfirm.show = false"
>
<div class="bg-white rounded-lg shadow-lg max-w-sm w-full">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Reset Counter</h3>
<p class="text-gray-600 mb-6">
Are you sure you want to reset the counter? This will lose all progress.
</p>
<div class="flex justify-end space-x-3">
<button
@click="resetConfirm.show = false"
class="btn btn-secondary"
>
Cancel
</button>
<button
@click="confirmReset()"
:disabled="resetConfirm.resetting"
class="btn btn-primary bg-red-600 hover:bg-red-700"
>
<span x-show="!resetConfirm.resetting">Reset Counter</span>
<span x-show="resetConfirm.resetting">Resetting...</span>
</button>
</div>
</div>
</div>
</div>
<!-- Reset Confirmation Modal -->
<div
x-show="resetConfirm.show"
x-cloak
x-transition
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="resetConfirm.show = false"
>
<div class="bg-white rounded-lg shadow-lg max-w-sm w-full">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-2">
Reset Counter
</h3>
<p class="text-gray-600 mb-6">
Are you sure you want to reset the counter? This will lose all
progress.
</p>
<div class="flex justify-end space-x-3">
<button
@click="resetConfirm.show = false"
class="btn btn-secondary"
>
Cancel
</button>
<button
@click="confirmReset()"
:disabled="resetConfirm.resetting"
class="btn btn-primary bg-red-600 hover:bg-red-700"
>
<span x-show="!resetConfirm.resetting">Reset Counter</span>
<span x-show="resetConfirm.resetting">Resetting...</span>
</button>
</div>
</div>
</div>
</div>
<!-- Scripts --> <script
defer
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<script src="/js/auth-guard.js"></script>
<script src="/js/round-counter.js"></script>
<script src="/js/footer-loader.js"></script>
<!-- Scripts -->
<script
defer
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<script src="/js/auth-guard.js"></script>
<script src="/js/round-counter.js"></script>
<script src="/js/footer-loader.js"></script>
</body>
</html>