feat: user preferences sync — server-side storage, whiteboard defaults, dashboard widget visibility

- New table `user_preferences` (user_id PK, JSON blob, updated_at)
- GET/PATCH/DELETE /api/preferences with deep-merge UPSERT
- LS.prefs singleton in api.js: dot-notation get/set, debounced flush (1.5s), server sync
- classroom.html: load wb.color/width/lineStyle/theme from prefs on init; save on change
- dashboard.html: widget configurator panel (gear button) — toggle visibility per-user, persisted server-side

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-14 20:17:25 +03:00
parent ba20a76839
commit 89ba25cd20
7 changed files with 292 additions and 6 deletions
@@ -0,0 +1,52 @@
const db = require('../db/db');
// Recursive deep merge: values from `patch` override `base`, objects are merged
function deepMerge(base, patch) {
const result = Object.assign({}, base);
for (const key of Object.keys(patch)) {
if (
patch[key] !== null &&
typeof patch[key] === 'object' &&
!Array.isArray(patch[key]) &&
typeof result[key] === 'object' &&
result[key] !== null &&
!Array.isArray(result[key])
) {
result[key] = deepMerge(result[key], patch[key]);
} else {
result[key] = patch[key];
}
}
return result;
}
/* ── GET /api/preferences ────────────────────────────────────────────────── */
function getPreferences(req, res) {
const row = db.prepare('SELECT data FROM user_preferences WHERE user_id = ?').get(req.user.id);
res.json(JSON.parse(row?.data || '{}'));
}
/* ── PATCH /api/preferences ──────────────────────────────────────────────── */
function patchPreferences(req, res) {
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
return res.status(400).json({ error: 'Body must be a JSON object' });
}
const current = JSON.parse(
db.prepare('SELECT data FROM user_preferences WHERE user_id = ?').get(req.user.id)?.data || '{}'
);
const merged = deepMerge(current, req.body);
db.prepare(`
INSERT INTO user_preferences (user_id, data, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
`).run(req.user.id, JSON.stringify(merged));
res.json(merged);
}
/* ── DELETE /api/preferences ─────────────────────────────────────────────── */
function resetPreferences(req, res) {
db.prepare('DELETE FROM user_preferences WHERE user_id = ?').run(req.user.id);
res.json({});
}
module.exports = { getPreferences, patchPreferences, resetPreferences };