From e4967e1cda876eafeafc5f492612b20a1c9e9b4f Mon Sep 17 00:00:00 2001 From: "Engineering (agent-engineering)" Date: Fri, 15 May 2026 21:50:21 -0700 Subject: [PATCH] fix: sync admin UI schema with plugin (UUID keys, lesson_media table) Schema now matches plugin Data/LessonStore.cs exactly: - id TEXT PRIMARY KEY (UUIDs) - lesson_media table (not media_items) - Full column set with NOT NULL constraints matching plugin Addresses PR #14 review feedback. --- admin-ui/server.js | 82 +++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/admin-ui/server.js b/admin-ui/server.js index 46b3184..14ba129 100644 --- a/admin-ui/server.js +++ b/admin-ui/server.js @@ -16,26 +16,35 @@ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); const db = new Database(DB_PATH); -// Initialize schema if needed +// UUID helper +function generateId() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +} + +// Initialize schema — must match plugin Data/LessonStore.cs +const now = () => new Date().toISOString(); db.exec(` CREATE TABLE IF NOT EXISTS lessons ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id TEXT PRIMARY KEY, title TEXT NOT NULL, - grade INTEGER, - subject TEXT, - lesson_date TEXT, + grade INTEGER NOT NULL, + subject TEXT NOT NULL, + lesson_date TEXT NOT NULL, description TEXT, - source_path TEXT, + source_path TEXT NOT NULL, compiled_html TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS media_items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - lesson_id INTEGER NOT NULL, + CREATE TABLE IF NOT EXISTS lesson_media ( + lesson_id TEXT NOT NULL, jellyfin_item_id TEXT NOT NULL, - sort_order INTEGER DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (lesson_id, jellyfin_item_id), FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE ); `); @@ -68,7 +77,7 @@ app.get('/api/lessons/:id', (req, res) => { try { const lesson = db.prepare('SELECT * FROM lessons WHERE id = ?').get(req.params.id); if (!lesson) return res.status(404).json({ error: 'Lesson not found' }); - const mediaItems = db.prepare('SELECT * FROM media_items WHERE lesson_id = ? ORDER BY sort_order').all(req.params.id); + const mediaItems = db.prepare('SELECT * FROM lesson_media WHERE lesson_id = ? ORDER BY sort_order').all(req.params.id); res.json({ ...lesson, mediaItems }); } catch (err) { res.status(500).json({ error: err.message }); @@ -83,31 +92,30 @@ app.post('/api/lessons', (req, res) => { if (!title) return res.status(400).json({ error: 'Title is required' }); const upsertLesson = db.transaction(() => { - let lessonId = id; - if (lessonId) { - // Update existing - db.prepare(` - UPDATE lessons - SET title = ?, grade = ?, subject = ?, lesson_date = ?, description = ?, - source_path = ?, compiled_html = ?, updated_at = datetime('now') - WHERE id = ? - `).run(title, grade || null, subject || null, lessonDate || null, description || null, - sourcePath || null, compiledHtml || null, lessonId); - // Replace media items - db.prepare('DELETE FROM media_items WHERE lesson_id = ?').run(lessonId); - } else { - // Create new - const result = db.prepare(` - INSERT INTO lessons (title, grade, subject, lesson_date, description, source_path, compiled_html) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(title, grade || null, subject || null, lessonDate || null, description || null, - sourcePath || null, compiledHtml || null); - lessonId = result.lastInsertRowid; - } + let lessonId = id || generateId(); + const ts = now(); + const existing = id ? db.prepare('SELECT created_at FROM lessons WHERE id = ?').get(id) : null; + const createdAt = existing ? existing.created_at : ts; - // Insert media items + db.prepare(` + INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at) + VALUES (@id, @title, @grade, @subject, @lesson_date, @description, @source_path, @compiled_html, @created_at, @updated_at) + ON CONFLICT(id) DO UPDATE SET + title = excluded.title, grade = excluded.grade, subject = excluded.subject, + lesson_date = excluded.lesson_date, description = excluded.description, + source_path = excluded.source_path, compiled_html = excluded.compiled_html, + updated_at = excluded.updated_at + `).run({ + id: lessonId, title, grade: grade || 0, + subject: subject || '', lesson_date: lessonDate || new Date().toISOString().split('T')[0], + description: description || null, source_path: sourcePath || '', + compiled_html: compiledHtml || null, created_at: createdAt, updated_at: ts + }); + + // Replace media items + db.prepare('DELETE FROM lesson_media WHERE lesson_id = ?').run(lessonId); if (mediaItems && Array.isArray(mediaItems)) { - const insertMedia = db.prepare('INSERT INTO media_items (lesson_id, jellyfin_item_id, sort_order) VALUES (?, ?, ?)'); + const insertMedia = db.prepare('INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) VALUES (?, ?, ?)'); for (const item of mediaItems) { insertMedia.run(lessonId, item.jellyfinItemId, item.sortOrder || 0); } @@ -118,7 +126,7 @@ app.post('/api/lessons', (req, res) => { const lessonId = upsertLesson(); const lesson = db.prepare('SELECT * FROM lessons WHERE id = ?').get(lessonId); - const media = db.prepare('SELECT * FROM media_items WHERE lesson_id = ? ORDER BY sort_order').all(lessonId); + const media = db.prepare('SELECT * FROM lesson_media WHERE lesson_id = ? ORDER BY sort_order').all(lessonId); res.json({ ...lesson, mediaItems: media }); } catch (err) { res.status(500).json({ error: err.message });