feat: implement 33 nice-to-have features + fix 37 code review bugs
5 SDD batches archived: - Batch 1: UI Polish (10 features, 14 tasks) - Batch 2: Study System (8 features, 23 tasks) - Batch 3: Infrastructure (5 features, 22 tasks) - Batch 4: AI Advanced (5 features, 30 tasks) — RAG with @xenova/transformers - Batch 5: Core Features (5 features, 19 tasks) 37 bugs fixed from comprehensive code review (11 CRITICAL, 12 HIGH, 14 MEDIUM/LOW): - SSE streaming now works (event.token check) - API keys no longer exposed via GET /api/models - FTS5 injection sanitized - DB backup/restore with admin auth - Buddy mode wired (buddy_meta column) - Exam auto-submit stale closure fixed - CSS variables aligned with design tokens - Progress data corruption fixed - WebSocket protocol auto-detection - Tests infrastructure completed (vitest + node:test)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { getProgress, updateProgress, resetProgressTopic } from '../lib/api';
|
||||
|
||||
function calcPercentage(row) {
|
||||
@@ -9,6 +9,9 @@ function calcPercentage(row) {
|
||||
export default function useProgress() {
|
||||
const [progress, setProgress] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const wsRef = useRef(null);
|
||||
const reconnectDelayRef = useRef(1000);
|
||||
const reconnectTimerRef = useRef(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
@@ -28,6 +31,62 @@ export default function useProgress() {
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
|
||||
const connect = () => {
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectDelayRef.current = 1000;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'progress_update' && msg.data) {
|
||||
setProgress((prev) => {
|
||||
const row = msg.data;
|
||||
const enriched = { ...row, percentage: calcPercentage(row) };
|
||||
const idx = prev.findIndex((p) => p.topic === row.topic);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = enriched;
|
||||
return next;
|
||||
}
|
||||
return [...prev, enriched];
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore malformed messages
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
wsRef.current = null;
|
||||
const delay = Math.min(reconnectDelayRef.current, 30000);
|
||||
reconnectDelayRef.current *= 2;
|
||||
reconnectTimerRef.current = setTimeout(connect, delay);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = null;
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
if (reconnectTimerRef.current) {
|
||||
clearTimeout(reconnectTimerRef.current);
|
||||
reconnectTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const updateExercise = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user