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
@@ -0,0 +1,149 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import type { PendingScore } from '../../shared/types'
const STORAGE_KEY = 'pendingScores'
const MAX_RETRIES = 3
const BASE_DELAY_MS = 1000
interface SubmitScoreParams {
jurorId: string
teamId: string
criterionId: string
value: number
}
interface UseOfflineQueueReturn {
submitScore: (params: SubmitScoreParams) => void
pendingCount: number
isOnline: boolean
}
function loadPendingScores(): PendingScore[] {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (!stored) return []
return JSON.parse(stored) as PendingScore[]
} catch {
return []
}
}
function savePendingScores(scores: PendingScore[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(scores))
}
export function useOfflineQueue(): UseOfflineQueueReturn {
const [pending, setPending] = useState<PendingScore[]>(loadPendingScores)
const [isOnline, setIsOnline] = useState(navigator.onLine)
const retryCountsRef = useRef<Map<string, number>>(new Map())
const processingRef = useRef(false)
// Track online/offline status
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
// Process queue when online or when pending changes
useEffect(() => {
if (isOnline && pending.some(s => !s.synced)) {
processQueue()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOnline, pending])
const getScoreKey = (s: PendingScore) =>
`${s.jurorId}:${s.teamId}:${s.criterionId}:${s.timestamp}`
const processQueue = useCallback(async () => {
if (processingRef.current) return
processingRef.current = true
const unsynced = pending.filter(s => !s.synced)
for (const score of unsynced) {
const key = getScoreKey(score)
const retries = retryCountsRef.current.get(key) || 0
if (retries >= MAX_RETRIES) continue
try {
const res = await fetch('/api/scores', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jurorId: score.jurorId,
teamId: score.teamId,
criterionId: score.criterionId,
value: score.value,
}),
})
if (res.ok || res.status === 409) {
// Success or conflict (server has newer) — remove from queue
setPending(prev => {
const updated = prev.filter(s => getScoreKey(s) !== key)
savePendingScores(updated)
return updated
})
retryCountsRef.current.delete(key)
} else {
throw new Error(`Server returned ${res.status}`)
}
} catch {
const newRetries = retries + 1
retryCountsRef.current.set(key, newRetries)
if (newRetries < MAX_RETRIES) {
// Exponential backoff
const delay = BASE_DELAY_MS * Math.pow(2, newRetries - 1)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
processingRef.current = false
}, [pending])
const submitScore = useCallback((params: SubmitScoreParams) => {
const newScore: PendingScore = {
jurorId: params.jurorId,
teamId: params.teamId,
criterionId: params.criterionId,
value: params.value,
timestamp: new Date().toISOString(),
synced: false,
}
setPending(prev => {
// Replace any existing pending score for same juror/team/criterion
const filtered = prev.filter(
s =>
!(
s.jurorId === params.jurorId &&
s.teamId === params.teamId &&
s.criterionId === params.criterionId
)
)
const updated = [...filtered, newScore]
savePendingScores(updated)
return updated
})
// Reset retry count for this score
const key = getScoreKey(newScore)
retryCountsRef.current.delete(key)
}, [])
const pendingCount = pending.filter(s => !s.synced).length
return { submitScore, pendingCount, isOnline }
}
+61
View File
@@ -0,0 +1,61 @@
import { useState, useEffect, useRef } from 'react';
import { TeamResult } from '../../shared/types';
interface UseSSEReturn {
results: TeamResult[];
connected: boolean;
lastUpdate: string | null;
}
/**
* Custom React hook that connects to the SSE endpoint for live score updates.
* Fetches initial results immediately, then uses EventSource for live updates.
*/
export function useSSE(): UseSSEReturn {
const [results, setResults] = useState<TeamResult[]>([]);
const [connected, setConnected] = useState(false);
const [lastUpdate, setLastUpdate] = useState<string | null>(null);
const eventSourceRef = useRef<EventSource | null>(null);
useEffect(() => {
// Fetch initial results immediately
fetch('/api/results')
.then((res) => res.json())
.then((data) => {
setResults(data);
setLastUpdate(new Date().toISOString());
})
.catch(() => {});
// Then connect to SSE for live updates
const eventSource = new EventSource('/api/results/stream');
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setConnected(true);
};
eventSource.addEventListener('scores_updated', (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
setResults(data.results);
setLastUpdate(data.timestamp);
} catch {
// Ignore malformed events
}
});
eventSource.onerror = () => {
setConnected(false);
// EventSource will automatically attempt to reconnect
};
return () => {
eventSource.close();
eventSourceRef.current = null;
setConnected(false);
};
}, []);
return { results, connected, lastUpdate };
}