Files
studyos/client/src/hooks/useProgress.js
renato97 4ff4302a8c 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)
2026-06-08 18:18:47 -03:00

134 lines
3.5 KiB
JavaScript

import { useState, useCallback, useEffect, useRef } from 'react';
import { getProgress, updateProgress, resetProgressTopic } from '../lib/api';
function calcPercentage(row) {
if (!row || row.exercises_done === 0) return 0;
return Math.round((row.exercises_correct / row.exercises_done) * 100);
}
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 {
const rows = await getProgress();
setProgress(
rows.map((r) => ({
...r,
percentage: calcPercentage(r),
}))
);
setError(null);
} catch (err) {
console.error('[useProgress] refresh error:', err.message);
setError(err.message);
}
}, []);
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(
async (topic, correct) => {
try {
const row = await updateProgress(topic, correct);
setProgress((prev) => {
const idx = prev.findIndex((p) => p.topic === topic);
const enriched = { ...row, percentage: calcPercentage(row) };
if (idx >= 0) {
const next = [...prev];
next[idx] = enriched;
return next;
}
return [...prev, enriched];
});
setError(null);
} catch (err) {
console.error('[useProgress] update error:', err.message);
setError(err.message);
}
},
[]
);
const resetTopic = useCallback(async (topic) => {
try {
await resetProgressTopic(topic);
setProgress((prev) => prev.filter((p) => p.topic !== topic));
setError(null);
} catch (err) {
console.error('[useProgress] reset error:', err.message);
setError(err.message);
}
}, []);
return {
progress,
error,
refresh,
updateExercise,
resetTopic,
};
}