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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user