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:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
+210
View File
@@ -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);
});