Add awards system with progress tracking

Implement awards display with progress calculation based on QSO data.

## Backend
- Add awards service with progress calculation logic
- Support for DXCC, WAS, VUCC, and satellite awards
- Filter QSOs by band, mode, and other criteria
- Calculate worked/confirmed entities per award
- API endpoints:
  - GET /api/awards - List all awards
  - GET /api/awards/:id/progress - Get award progress
  - GET /api/awards/:id/entities - Get detailed entity breakdown

## Frontend
- Create awards listing page with progress cards
- Add Awards link to navigation bar
- Display award progress bars with worked/confirmed counts
- Link to individual award detail pages (to be implemented)
- Copy award definitions to static folder

## Award Definitions
- DXCC Mixed Mode (100 entities)
- DXCC CW (100 entities, CW only)
- WAS Mixed Mode (50 states)
- VUCC Satellite (100 grids)
- RS-44 Satellite Award (100 QSOs)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 08:47:30 +01:00
parent 31488e556c
commit 884bdb436d
10 changed files with 755 additions and 1 deletions

View File

@@ -20,6 +20,11 @@ import {
getUserActiveJob,
getUserJobs,
} from './services/job-queue.service.js';
import {
getAllAwards,
getAwardProgressDetails,
getAwardEntityBreakdown,
} from './services/awards.service.js';
/**
* Main backend application
@@ -478,6 +483,89 @@ const app = new Elysia()
}
})
/**
* GET /api/awards
* Get all available awards (requires authentication)
*/
.get('/api/awards', async ({ user, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const awards = await getAllAwards();
return {
success: true,
awards,
};
} catch (error) {
logger.error('Error fetching awards', { error: error.message });
set.status = 500;
return {
success: false,
error: 'Failed to fetch awards',
};
}
})
/**
* GET /api/awards/:awardId/progress
* Get award progress for user (requires authentication)
*/
.get('/api/awards/:awardId/progress', async ({ user, params, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const { awardId } = params;
const progress = await getAwardProgressDetails(user.id, awardId);
return {
success: true,
...progress,
};
} catch (error) {
logger.error('Error calculating award progress', { error: error.message });
set.status = 500;
return {
success: false,
error: error.message || 'Failed to calculate award progress',
};
}
})
/**
* GET /api/awards/:awardId/entities
* Get detailed entity breakdown for an award (requires authentication)
*/
.get('/api/awards/:awardId/entities', async ({ user, params, set }) => {
if (!user) {
set.status = 401;
return { success: false, error: 'Unauthorized' };
}
try {
const { awardId } = params;
const breakdown = await getAwardEntityBreakdown(user.id, awardId);
return {
success: true,
...breakdown,
};
} catch (error) {
logger.error('Error fetching award entities', { error: error.message });
set.status = 500;
return {
success: false,
error: error.message || 'Failed to fetch award entities',
};
}
})
// Health check endpoint
.get('/api/health', () => ({
status: 'ok',