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:
127
server/db.js
127
server/db.js
@@ -21,6 +21,11 @@ function flatParams(params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
function safeAddColumn(table, col, def) {
|
||||
try { db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`); }
|
||||
catch (e) { /* column exists */ }
|
||||
}
|
||||
|
||||
function createWrapper(sqlDb) {
|
||||
return {
|
||||
prepare(sql) {
|
||||
@@ -72,6 +77,8 @@ function createWrapper(sqlDb) {
|
||||
};
|
||||
}
|
||||
|
||||
// Placeholder wrapper — methods are replaced by initDB() after sql.js loads.
|
||||
// This allows synchronous require('db') in route modules before async init.
|
||||
const db = createWrapper(null);
|
||||
|
||||
async function initDB() {
|
||||
@@ -159,8 +166,85 @@ async function initDB() {
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
score REAL NOT NULL DEFAULT 0,
|
||||
topics TEXT NOT NULL DEFAULT '[]',
|
||||
taken_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS flashcards (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question TEXT NOT NULL,
|
||||
answer TEXT NOT NULL,
|
||||
pdf_id INTEGER REFERENCES pdfs(id) ON DELETE SET NULL,
|
||||
message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
||||
seen INTEGER NOT NULL DEFAULT 0,
|
||||
topic TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS study_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_date TEXT NOT NULL,
|
||||
minutes INTEGER NOT NULL DEFAULT 0,
|
||||
topic TEXT,
|
||||
UNIQUE(session_date, topic)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS topic_relationships (
|
||||
from_topic TEXT NOT NULL,
|
||||
to_topic TEXT NOT NULL,
|
||||
domain TEXT,
|
||||
PRIMARY KEY(from_topic, to_topic)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS flashcard_reviews (
|
||||
flashcard_id INTEGER PRIMARY KEY REFERENCES flashcards(id) ON DELETE CASCADE,
|
||||
ease_factor REAL NOT NULL DEFAULT 2.5,
|
||||
interval_days INTEGER NOT NULL DEFAULT 1,
|
||||
repetitions INTEGER NOT NULL DEFAULT 0,
|
||||
next_review TEXT NOT NULL DEFAULT (date('now')),
|
||||
last_review TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
// Try to create FTS5 virtual tables and sync triggers
|
||||
let fts5Available = false;
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(content, content='messages', content_rowid='id');
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS pdfs_fts USING fts5(content_markdown, content='pdfs', content_rowid='id');
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
|
||||
UPDATE messages_fts SET content = new.content WHERE rowid = new.id;
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE rowid = old.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS pdfs_ai AFTER INSERT ON pdfs BEGIN
|
||||
INSERT INTO pdfs_fts(rowid, content_markdown) VALUES (new.id, new.content_markdown);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS pdfs_au AFTER UPDATE ON pdfs BEGIN
|
||||
UPDATE pdfs_fts SET content_markdown = new.content_markdown WHERE rowid = new.id;
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS pdfs_ad AFTER DELETE ON pdfs BEGIN
|
||||
DELETE FROM pdfs_fts WHERE rowid = old.id;
|
||||
END;
|
||||
`);
|
||||
fts5Available = true;
|
||||
} catch (err) {
|
||||
console.warn('[db] FTS5 setup failed:', err.message, '— search will use LIKE fallback');
|
||||
}
|
||||
db._fts5Available = fts5Available;
|
||||
|
||||
const modelCount = db.prepare('SELECT COUNT(*) as count FROM models').get();
|
||||
if (!modelCount || modelCount.count === 0) {
|
||||
db.prepare(`
|
||||
@@ -174,6 +258,49 @@ async function initDB() {
|
||||
db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run('vlm_endpoint', 'http://localhost:8080/vlm');
|
||||
}
|
||||
|
||||
// Additive schema changes for ai-advanced-batch4
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS embeddings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pdf_id INTEGER NOT NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
vector BLOB NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (pdf_id) REFERENCES pdfs(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_embeddings_pdf ON embeddings(pdf_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared_conversations (
|
||||
token TEXT PRIMARY KEY,
|
||||
conv_id INTEGER NOT NULL,
|
||||
role_label TEXT NOT NULL DEFAULT 'compañero',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (conv_id) REFERENCES conversations(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS conversation_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL,
|
||||
user_name TEXT NOT NULL,
|
||||
joined_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
safeAddColumn('progress', 'wrong_streak', 'INTEGER DEFAULT 0');
|
||||
safeAddColumn('progress', 'global_wrong_streak', 'INTEGER DEFAULT 0');
|
||||
safeAddColumn('progress', 'difficulty_level', 'TEXT DEFAULT \'normal\'');
|
||||
|
||||
safeAddColumn('conversations', 'buddy_meta', 'TEXT');
|
||||
|
||||
safeAddColumn('exams', 'questions', 'TEXT');
|
||||
safeAddColumn('exams', 'answers', 'TEXT');
|
||||
safeAddColumn('exams', 'duration_seconds', 'INTEGER');
|
||||
safeAddColumn('exams', 'started_at', 'TEXT');
|
||||
safeAddColumn('exams', 'status', 'TEXT DEFAULT \'pending\'');
|
||||
safeAddColumn('exams', 'conversation_id', 'INTEGER');
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user