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:
renato97
2026-06-08 18:18:47 -03:00
parent b7d1e7319f
commit 4ff4302a8c
79 changed files with 13667 additions and 389 deletions

View File

@@ -29,6 +29,9 @@ import {
deletePdf,
getProgress,
resetProgressTopic,
backupDatabase,
restoreDatabase,
downloadExamPdf,
} from '../lib/api';
function ToggleSwitch({ checked, onChange }) {
@@ -84,6 +87,8 @@ export default function Settings() {
// Progress tab
const [progress, setProgress] = useState([]);
const [restoreFile, setRestoreFile] = useState(null);
const [restoreLoading, setRestoreLoading] = useState(false);
const refreshModels = useCallback(async () => {
try {
@@ -268,6 +273,27 @@ export default function Settings() {
}
};
const handleDownloadBackup = async () => {
try {
await backupDatabase();
} catch (err) {
alert(err.message);
}
};
const handleRestoreBackup = async () => {
if (!restoreFile) return;
if (!window.confirm('¿Restaurar base de datos? Se reemplazará el estado actual.')) return;
setRestoreLoading(true);
try {
await restoreDatabase(restoreFile);
window.location.reload();
} catch (err) {
alert(err.message);
setRestoreLoading(false);
}
};
const progressChartData = progress.map((p) => ({
name: p.topic,
pct: p.percentage,
@@ -311,6 +337,18 @@ export default function Settings() {
>
Progreso
</button>
<button
className={`settings-tab ${activeTab === 'backup' ? 'active' : ''}`}
onClick={() => setActiveTab('backup')}
>
Backup
</button>
<button
className={`settings-tab ${activeTab === 'datos' ? 'active' : ''}`}
onClick={() => setActiveTab('datos')}
>
Datos
</button>
</div>
{/* Tab 1: Modelos */}
@@ -635,6 +673,70 @@ export default function Settings() {
</div>
)}
{/* Tab 4: Backup */}
{activeTab === 'backup' && (
<div>
<div style={{ marginBottom: 24 }}>
<h2
style={{
fontSize: 14,
fontWeight: 600,
color: 'var(--text-primary)',
marginBottom: 12,
}}
>
Base de datos
</h2>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
<button className="btn btn-secondary" onClick={handleDownloadBackup}>
Download Backup
</button>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
type="file"
accept=".db"
onChange={(e) => setRestoreFile(e.target.files?.[0] || null)}
/>
<button
className="btn btn-primary"
onClick={handleRestoreBackup}
disabled={!restoreFile || restoreLoading}
>
{restoreLoading ? (
<Loader2 size={12} style={{ animation: 'spin 1s linear infinite', marginRight: 6 }} />
) : null}
Restore Backup
</button>
</div>
</div>
</div>
</div>
)}
{/* Tab 5: Datos */}
{activeTab === 'datos' && (
<div>
<div style={{ marginBottom: 24 }}>
<h2
style={{
fontSize: 14,
fontWeight: 600,
color: 'var(--text-primary)',
marginBottom: 12,
}}
>
Exportar examen simulado
</h2>
<p style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 12 }}>
Genera un PDF con los 20 temas más recientes de tu progreso.
</p>
<button className="btn btn-primary" onClick={downloadExamPdf}>
Exportar examen simulado
</button>
</div>
</div>
)}
{/* Model Modal */}
{modelModalOpen && (
<Modal