fix(scheduler): implement deadline-based matching with 5-run limit and fix security issues

Security fixes:
- Replace $queryRawUnsafe with parameterized $queryRaw in admin.js to prevent SQL injection
- Use PostgreSQL ANY() operator for safe array parameter handling

Scheduler improvements:
- Add registrationDeadline support - scheduler now waits until deadline before running
- Implement 5-run limit after deadline (runs exactly 5 times with 5-minute intervals)
- Add countScheduledRunsAfterDeadline() to track post-deadline runs
- Add environment variable validation with sensible min/max ranges
- Fix Prisma query syntax (remove invalid endDate null check for non-nullable field)

UI improvements:
- Fix colspan mismatch in MatchingRunsSection (6 → 8 columns)
- Remove duplicate "Uruchom Matching" button, keep only "Run now" with audit tracking
- Simplify MatchingConfigSection to focus on deadline configuration

Logging enhancements:
- Add detailed scheduler logs showing run progress (e.g., "Running post-deadline matching (3/5)")
- Log wait times before deadline and between runs
- Show completion status after 5 runs
This commit is contained in:
Radosław Gierwiało
2025-11-30 14:42:08 +01:00
parent 752d65035a
commit 560ff1edc1
4 changed files with 184 additions and 106 deletions

View File

@@ -108,17 +108,16 @@ router.get('/events/:slug/matching-runs', authenticate, async (req, res, next) =
// Cheap and valuable: shows actual created pairs in this run.
if (runs.length > 0) {
const runIds = runs.map(r => r.id);
// Single SQL query for all listed runs
const placeholders = runIds.join(',');
const aggRows = await prisma.$queryRawUnsafe(
`SELECT origin_run_id AS "originRunId",
COUNT(*)::int AS "totalSuggestions",
COUNT(*) FILTER (WHERE recorder_id IS NOT NULL)::int AS "assignedCount",
COUNT(*) FILTER (WHERE status = 'not_found')::int AS "notFoundCount"
FROM recording_suggestions
WHERE event_id = ${event.id} AND origin_run_id IN (${placeholders})
GROUP BY origin_run_id`
);
// Single SQL query for all listed runs (using parameterized query to prevent SQL injection)
const aggRows = await prisma.$queryRaw`
SELECT origin_run_id AS "originRunId",
COUNT(*)::int AS "totalSuggestions",
COUNT(*) FILTER (WHERE recorder_id IS NOT NULL)::int AS "assignedCount",
COUNT(*) FILTER (WHERE status = 'not_found')::int AS "notFoundCount"
FROM recording_suggestions
WHERE event_id = ${event.id} AND origin_run_id = ANY(${runIds})
GROUP BY origin_run_id
`;
const aggByRun = new Map(aggRows.map(r => [r.originRunId, r]));
for (const r of runs) {
const agg = aggByRun.get(r.id) || { totalSuggestions: 0, assignedCount: 0, notFoundCount: 0 };