Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import {
|
||||
getSession,
|
||||
getTeams,
|
||||
getJurors,
|
||||
getScores,
|
||||
addScore,
|
||||
updateSession,
|
||||
getStore,
|
||||
} from './store.js';
|
||||
import { resolveConflict } from './conflict.js';
|
||||
import { calculateAllResults, getCompletionStatus } from './aggregation.js';
|
||||
import { broadcastUpdate } from './sse.js';
|
||||
import { Score } from '../shared/types.js';
|
||||
|
||||
export const apiRouter = Router();
|
||||
// ─── Session Endpoints ───────────────────────────────────────────────
|
||||
|
||||
apiRouter.get('/api/session', (_req: Request, res: Response) => {
|
||||
const session = getSession();
|
||||
res.json(session);
|
||||
});
|
||||
|
||||
apiRouter.post('/api/session/start', (_req: Request, res: Response) => {
|
||||
const session = getSession();
|
||||
if (session.status === 'active') {
|
||||
res.status(400).json({ error: 'Session is already active' });
|
||||
return;
|
||||
}
|
||||
const updated = updateSession({
|
||||
status: 'active',
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
apiRouter.post('/api/session/end', (_req: Request, res: Response) => {
|
||||
const session = getSession();
|
||||
if (session.status !== 'active') {
|
||||
res.status(400).json({ error: 'Session is not active' });
|
||||
return;
|
||||
}
|
||||
const updated = updateSession({
|
||||
status: 'ended',
|
||||
endedAt: new Date().toISOString(),
|
||||
});
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
// ─── Team and Juror Endpoints ────────────────────────────────────────
|
||||
|
||||
apiRouter.get('/api/teams', (_req: Request, res: Response) => {
|
||||
const teams = getTeams();
|
||||
res.json(teams);
|
||||
});
|
||||
|
||||
apiRouter.get('/api/jurors', (_req: Request, res: Response) => {
|
||||
const jurors = getJurors();
|
||||
res.json(jurors);
|
||||
});
|
||||
|
||||
// ─── Score Endpoints ─────────────────────────────────────────────────
|
||||
|
||||
apiRouter.post('/api/scores', (req: Request, res: Response) => {
|
||||
const session = getSession();
|
||||
if (session.status !== 'active') {
|
||||
res.status(403).json({ error: 'Session is not active' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { jurorId, teamId, criterionId, value, force } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!jurorId || !teamId || !criterionId || value === undefined) {
|
||||
res.status(400).json({ error: 'Missing required fields: jurorId, teamId, criterionId, value' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate value is within scale range
|
||||
const { scaleMin, scaleMax } = session.config;
|
||||
if (typeof value !== 'number' || value < scaleMin || value > scaleMax) {
|
||||
res.status(400).json({ error: `Value must be between ${scaleMin} and ${scaleMax}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate jurorId exists
|
||||
const jurors = getJurors();
|
||||
if (!jurors.find((j) => j.id === jurorId)) {
|
||||
res.status(400).json({ error: `Unknown jurorId: ${jurorId}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate teamId exists
|
||||
const teams = getTeams();
|
||||
if (!teams.find((t) => t.id === teamId)) {
|
||||
res.status(400).json({ error: `Unknown teamId: ${teamId}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate criterionId exists
|
||||
const criteria = session.config.criteria;
|
||||
if (!criteria.find((c) => c.id === criterionId)) {
|
||||
res.status(400).json({ error: `Unknown criterionId: ${criterionId}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const incoming: Score = {
|
||||
jurorId,
|
||||
teamId,
|
||||
criterionId,
|
||||
value,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Check for existing score and resolve conflict
|
||||
const existingScores = getScores();
|
||||
const existing = existingScores.find(
|
||||
(s) => s.jurorId === jurorId && s.teamId === teamId && s.criterionId === criterionId
|
||||
);
|
||||
|
||||
const conflict = resolveConflict(incoming, existing, force);
|
||||
if (!conflict.accept) {
|
||||
res.status(409).json({ error: conflict.reason, existing });
|
||||
return;
|
||||
}
|
||||
|
||||
addScore(incoming);
|
||||
broadcastUpdate();
|
||||
res.json(incoming);
|
||||
});
|
||||
|
||||
apiRouter.get('/api/scores/:jurorId', (req: Request, res: Response) => {
|
||||
const { jurorId } = req.params;
|
||||
const scores = getScores();
|
||||
const jurorScores = scores.filter((s) => s.jurorId === jurorId);
|
||||
res.json(jurorScores);
|
||||
});
|
||||
|
||||
apiRouter.get('/api/scores', (_req: Request, res: Response) => {
|
||||
const scores = getScores();
|
||||
res.json(scores);
|
||||
});
|
||||
|
||||
// ─── Results and Progress Endpoints ──────────────────────────────────
|
||||
|
||||
apiRouter.get('/api/results', (_req: Request, res: Response) => {
|
||||
const store = getStore();
|
||||
const criteriaCount = store.session.config.criteria.length;
|
||||
const results = calculateAllResults(store.scores, store.teams, criteriaCount, store.session.config.criteria);
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
apiRouter.get('/api/progress', (_req: Request, res: Response) => {
|
||||
const store = getStore();
|
||||
const criteriaCount = store.session.config.criteria.length;
|
||||
const progress: Record<string, { completed: number; total: number }> = {};
|
||||
|
||||
for (const juror of store.jurors) {
|
||||
let completedTeams = 0;
|
||||
for (const team of store.teams) {
|
||||
const status = getCompletionStatus(juror.id, team.id, store.scores, criteriaCount);
|
||||
if (status === 'complete') {
|
||||
completedTeams++;
|
||||
}
|
||||
}
|
||||
progress[juror.id] = { completed: completedTeams, total: store.teams.length };
|
||||
}
|
||||
|
||||
res.json(progress);
|
||||
});
|
||||
|
||||
apiRouter.get('/api/export/csv', (_req: Request, res: Response) => {
|
||||
const store = getStore();
|
||||
const criteriaCount = store.session.config.criteria.length;
|
||||
const results = calculateAllResults(store.scores, store.teams, criteriaCount);
|
||||
|
||||
// Build header row: Team, then for each juror: juror_criterion1, juror_criterion2, ..., then Total, Average, Rank
|
||||
const criteria = store.session.config.criteria;
|
||||
const headerParts = ['Team'];
|
||||
for (const juror of store.jurors) {
|
||||
for (const criterion of criteria) {
|
||||
headerParts.push(`${juror.name}_${criterion.nameDE}`);
|
||||
}
|
||||
}
|
||||
headerParts.push('Total', 'Average', 'Rank');
|
||||
|
||||
const rows: string[] = [headerParts.join(',')];
|
||||
|
||||
// One row per team (sorted by rank)
|
||||
for (const result of results) {
|
||||
const rowParts: string[] = [`"${result.teamName}"`];
|
||||
|
||||
for (const juror of store.jurors) {
|
||||
for (const criterion of criteria) {
|
||||
const score = store.scores.find(
|
||||
(s) => s.jurorId === juror.id && s.teamId === result.teamId && s.criterionId === criterion.id
|
||||
);
|
||||
rowParts.push(score ? String(score.value) : '');
|
||||
}
|
||||
}
|
||||
|
||||
rowParts.push(String(result.total), result.average.toFixed(2), String(result.rank));
|
||||
rows.push(rowParts.join(','));
|
||||
}
|
||||
|
||||
const csv = rows.join('\n');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="jury-results.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
Reference in New Issue
Block a user