From d4a14fd6f29a25ec615ad89ef382789aac6389ff Mon Sep 17 00:00:00 2001 From: aomukai Date: Sat, 8 Nov 2025 01:45:12 +0900 Subject: [PATCH] Implemented backend support for a compendium feature, including database schema updates and frontend integration. --- main.html | 1 + src/app.js | 7 ++++ src/compendium.js | 90 ++++++++++++++++++++++++++++++++++++++++ tests/unit-compendium.js | 64 ++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 src/compendium.js create mode 100644 tests/unit-compendium.js diff --git a/main.html b/main.html index d5d572b..cf9febb 100644 --- a/main.html +++ b/main.html @@ -12,6 +12,7 @@ + diff --git a/src/app.js b/src/app.js index c79c4c9..29fd6c6 100644 --- a/src/app.js +++ b/src/app.js @@ -133,6 +133,13 @@ db.version(3).stores({ // noop migration for now; existing installs will get empty prompts/codex }); +// Add compendium table (v4) +db.version(4).stores({ + compendium: 'id, projectId, category, title, modified, tags' +}).upgrade(async tx => { + // noop migration; new installs will get empty compendium +}); + // Alpine.js App document.addEventListener('alpine:init', () => { Alpine.data('app', () => ({ diff --git a/src/compendium.js b/src/compendium.js new file mode 100644 index 0000000..bece283 --- /dev/null +++ b/src/compendium.js @@ -0,0 +1,90 @@ +// Simple Compendium module backed by Dexie `db` defined in app.js +(function () { + try { + const Compendium = { + async createEntry(projectId, { category = 'lore', title = '', body = '', tags = [] } = {}) { + const id = Date.now().toString() + '-' + Math.random().toString(36).slice(2, 8); + const now = new Date(); + const entry = { id, projectId, category, title, body, summary: '', tags: (tags || []).slice(0, 10), created: now, modified: now, order: 0 }; + await db.compendium.add(entry); + return entry; + }, + + async updateEntry(id, updates) { + updates.modified = new Date(); + await db.compendium.update(id, updates); + return db.compendium.get(id); + }, + + async deleteEntry(id) { + await db.compendium.delete(id); + }, + + async getEntry(id) { + return db.compendium.get(id); + }, + + async listByCategory(projectId, category) { + return db.compendium.where({ projectId, category }).sortBy('order'); + }, + + async search(projectId, q, options = {}) { + q = (q || '').trim().toLowerCase(); + const limit = options.limit || 20; + if (!q) { + return (await db.compendium.where('projectId').equals(projectId).limit(limit).toArray()) || []; + } + const all = await db.compendium.where('projectId').equals(projectId).toArray(); + const results = all.filter(e => { + const hay = ((e.title || '') + '\n' + (e.tags || []).join(' ') + '\n' + (e.body || '')).toLowerCase(); + return hay.indexOf(q) !== -1; + }).slice(0, limit); + return results; + }, + + async summaries(projectId, ids = []) { + const out = {}; + for (const id of ids) { + const e = await db.compendium.get(id); + if (!e) continue; + if (e.summary && e.summary.length > 10) out[id] = e.summary; + else out[id] = (e.body || '').slice(0, 300) + ((e.body || '').length > 300 ? '…' : ''); + } + return out; + }, + + async export(projectId) { + return db.compendium.where('projectId').equals(projectId).toArray(); + }, + + async import(projectId, items = [], opts = { merge: true }) { + const added = []; + for (const it of items) { + const entry = Object.assign({}, it); + entry.projectId = projectId; + entry.id = entry.id || (Date.now().toString() + '-' + Math.random().toString(36).slice(2, 6)); + entry.created = entry.created ? new Date(entry.created) : new Date(); + entry.modified = new Date(); + await db.compendium.put(entry); + added.push(entry); + } + return added; + } + }; + + window.Compendium = Compendium; + + // Test helper + window.__test = window.__test || {}; + window.__test.seedCompendium = async function (projectId, entries) { + const created = []; + for (const e of (entries || [])) { + const en = await Compendium.createEntry(projectId, e); + created.push(en); + } + return created; + }; + } catch (e) { + console.warn('Failed to attach Compendium module:', e && e.message ? e.message : e); + } +})(); diff --git a/tests/unit-compendium.js b/tests/unit-compendium.js new file mode 100644 index 0000000..d87cf11 --- /dev/null +++ b/tests/unit-compendium.js @@ -0,0 +1,64 @@ +const { chromium } = require('playwright'); +const path = require('path'); + +(async () => { + const projectRoot = path.resolve(__dirname, '..'); + const fileUrl = process.env.APP_URL || ('file:///' + path.join(projectRoot, 'main.html').replace(/\\/g, '/')); + + console.log('Opening:', fileUrl); + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + + page.on('console', msg => { try { console.log(`PAGE LOG [${msg.type()}] ${msg.text()}`); } catch (e) { } }); + page.on('pageerror', err => { console.error('PAGE ERROR', err && err.message ? err.message : err); }); + + try { + await page.goto(fileUrl, { waitUntil: 'load', timeout: 15000 }); + await page.waitForSelector('.app-container, .welcome-screen', { timeout: 10000 }); + + // Seed project + const proj = await page.evaluate(async () => await window.__test.seedProject('CompTest')); + + // Seed compendium entries via API + const entries = await page.evaluate(async (pid) => { + return await window.__test.seedCompendium(pid, [ + { category: 'character', title: 'Alice', body: 'Alice is a cautious detective.' }, + { category: 'location', title: 'Old Harbor', body: 'The Old Harbor smells of salt and diesel.' } + ]); + }, proj.id); + + if (!Array.isArray(entries) || entries.length < 2) { + console.error('Compendium seed failed:', entries); + await browser.close(); + process.exit(2); + } + + // Test search + const search = await page.evaluate(async (pid) => { + return await window.Compendium.search(pid, 'Alice'); + }, proj.id); + + if (!(Array.isArray(search) && search.length >= 1 && search[0].title === 'Alice')) { + console.error('Compendium search failed:', search); + await browser.close(); + process.exit(3); + } + + // Test getEntry + const fetched = await page.evaluate(async (id) => await window.Compendium.getEntry(id), entries[0].id); + if (!fetched || fetched.title !== 'Alice') { + console.error('Compendium getEntry failed:', fetched); + await browser.close(); + process.exit(4); + } + + console.log('unit-compendium test passed.'); + await browser.close(); + process.exit(0); + } catch (err) { + console.error('unit-compendium test failed:', err && err.message ? err.message : err); + await browser.close(); + process.exit(1); + } +})();