diff --git a/client/src/api/client.js b/client/src/api/client.js
index 991a851..79a8ad7 100644
--- a/client/src/api/client.js
+++ b/client/src/api/client.js
@@ -122,6 +122,7 @@ export const adminApi = {
updateUser: (id, data) => apiClient.put(`/admin/users/${id}`, data).then(r => r.data),
deleteUser: (id) => apiClient.delete(`/admin/users/${id}`).then(r => r.data),
stats: () => apiClient.get('/admin/stats').then(r => r.data),
+ saveDemoBaseline: () => apiClient.post('/admin/save-demo-baseline').then(r => r.data),
}
export const mapsApi = {
diff --git a/client/src/pages/AdminPage.jsx b/client/src/pages/AdminPage.jsx
index 33a8a59..ceeecfa 100644
--- a/client/src/pages/AdminPage.jsx
+++ b/client/src/pages/AdminPage.jsx
@@ -12,6 +12,7 @@ import { Users, Map, Briefcase, Shield, Trash2, Edit2, Camera, FileText, Eye, Ey
import CustomSelect from '../components/shared/CustomSelect'
export default function AdminPage() {
+ const { demoMode } = useAuthStore()
const { t } = useTranslation()
const TABS = [
{ id: 'users', label: t('admin.tabs.users') },
@@ -208,6 +209,29 @@ export default function AdminPage() {
+ {/* Demo Baseline Button */}
+ {demoMode && (
+
+
+
Demo Baseline
+
Save current state as the hourly reset point. All admin trips and settings will be preserved.
+
+
+
+ )}
+
{/* Stats */}
{stats && (
diff --git a/server/package.json b/server/package.json
index 9817990..829d77b 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "nomad-server",
- "version": "2.2.8",
+ "version": "2.3.0",
"main": "src/index.js",
"scripts": {
"start": "node --experimental-sqlite src/index.js",
diff --git a/server/src/demo/demo-reset.js b/server/src/demo/demo-reset.js
index 9043953..eca69d3 100644
--- a/server/src/demo/demo-reset.js
+++ b/server/src/demo/demo-reset.js
@@ -1,34 +1,84 @@
-function resetDemoUser(db) {
- const DEMO_EMAIL = 'demo@nomad.app';
+const fs = require('fs');
+const path = require('path');
- const demo = db.prepare('SELECT id FROM users WHERE email = ?').get(DEMO_EMAIL);
- if (!demo) {
- console.log('[Demo Reset] Demo user not found, skipping');
+const dataDir = path.join(__dirname, '../../data');
+const dbPath = path.join(dataDir, 'travel.db');
+const baselinePath = path.join(dataDir, 'travel-baseline.db');
+
+function resetDemoUser() {
+ if (!fs.existsSync(baselinePath)) {
+ console.log('[Demo Reset] No baseline found, skipping. Admin must save baseline first.');
return;
}
- const demoId = Number(demo.id);
+ const { db, closeDb, reinitialize } = require('../db/database');
- // Delete all trips OWNED by demo user (shared trips from admin stay)
- const demoTrips = db.prepare('SELECT id FROM trips WHERE user_id = ?').all(demoId);
- for (const trip of demoTrips) {
- // CASCADE handles days, places, assignments, packing, budget, reservations, photos, files, day_notes
- db.prepare('DELETE FROM trips WHERE id = ?').run(trip.id);
+ // Save admin's current credentials and API keys (these should survive the reset)
+ const adminEmail = process.env.DEMO_ADMIN_EMAIL || 'admin@nomad.app';
+ let adminData = null;
+ try {
+ adminData = db.prepare(
+ 'SELECT password_hash, maps_api_key, openweather_api_key, unsplash_api_key, avatar FROM users WHERE email = ?'
+ ).get(adminEmail);
+ } catch (e) {
+ console.error('[Demo Reset] Failed to read admin data:', e.message);
}
- // Delete demo user's custom categories and tags
- db.prepare('DELETE FROM categories WHERE user_id = ?').run(demoId);
- db.prepare('DELETE FROM tags WHERE user_id = ?').run(demoId);
+ // Flush WAL to main DB file
+ try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
- // Reset demo user's settings
- db.prepare('DELETE FROM settings WHERE user_id = ?').run(demoId);
+ // Close DB connection
+ closeDb();
- const deletedCount = demoTrips.length;
- if (deletedCount > 0) {
- console.log(`[Demo Reset] Cleaned ${deletedCount} trips from demo user`);
- } else {
- console.log('[Demo Reset] No demo user trips to clean');
+ // Restore baseline
+ try {
+ fs.copyFileSync(baselinePath, dbPath);
+ // Remove WAL/SHM files if they exist (stale from old connection)
+ try { fs.unlinkSync(dbPath + '-wal'); } catch (e) {}
+ try { fs.unlinkSync(dbPath + '-shm'); } catch (e) {}
+ } catch (e) {
+ console.error('[Demo Reset] Failed to restore baseline:', e.message);
+ reinitialize();
+ return;
}
+
+ // Reinitialize DB connection with restored baseline
+ reinitialize();
+
+ // Restore admin's latest credentials (in case admin changed password/API keys after baseline was saved)
+ if (adminData) {
+ try {
+ const { db: freshDb } = require('../db/database');
+ freshDb.prepare(
+ 'UPDATE users SET password_hash = ?, maps_api_key = ?, openweather_api_key = ?, unsplash_api_key = ?, avatar = ? WHERE email = ?'
+ ).run(
+ adminData.password_hash,
+ adminData.maps_api_key,
+ adminData.openweather_api_key,
+ adminData.unsplash_api_key,
+ adminData.avatar,
+ adminEmail
+ );
+ } catch (e) {
+ console.error('[Demo Reset] Failed to restore admin credentials:', e.message);
+ }
+ }
+
+ console.log('[Demo Reset] Database restored from baseline');
}
-module.exports = { resetDemoUser };
+function saveBaseline() {
+ const { db } = require('../db/database');
+
+ // Flush WAL so baseline file is self-contained
+ try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
+
+ fs.copyFileSync(dbPath, baselinePath);
+ console.log('[Demo] Baseline saved');
+}
+
+function hasBaseline() {
+ return fs.existsSync(baselinePath);
+}
+
+module.exports = { resetDemoUser, saveBaseline, hasBaseline };
diff --git a/server/src/demo/demo-seed.js b/server/src/demo/demo-seed.js
index 9d1bc2f..af3f0fe 100644
--- a/server/src/demo/demo-seed.js
+++ b/server/src/demo/demo-seed.js
@@ -42,6 +42,13 @@ function seedDemoData(db) {
console.log('[Demo] Seeding example trips...');
seedExampleTrips(db, admin.id, demo.id);
+
+ // Auto-save baseline after first seed
+ const { saveBaseline, hasBaseline } = require('./demo-reset');
+ if (!hasBaseline()) {
+ saveBaseline();
+ }
+
return { adminId: admin.id, demoId: demo.id };
}
diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js
index 682847d..d74c2ff 100644
--- a/server/src/routes/admin.js
+++ b/server/src/routes/admin.js
@@ -110,4 +110,18 @@ router.get('/stats', (req, res) => {
res.json({ totalUsers, totalTrips, totalPlaces, totalPhotos, totalFiles });
});
+// POST /api/admin/save-demo-baseline (demo mode only)
+router.post('/save-demo-baseline', (req, res) => {
+ if (process.env.DEMO_MODE !== 'true') {
+ return res.status(404).json({ error: 'Not found' });
+ }
+ try {
+ const { saveBaseline } = require('../demo/demo-reset');
+ saveBaseline();
+ res.json({ success: true, message: 'Demo baseline saved. Hourly resets will restore to this state.' });
+ } catch (err) {
+ res.status(500).json({ error: 'Failed to save baseline: ' + err.message });
+ }
+});
+
module.exports = router;
diff --git a/server/src/scheduler.js b/server/src/scheduler.js
index 13ade96..18565e0 100644
--- a/server/src/scheduler.js
+++ b/server/src/scheduler.js
@@ -111,9 +111,8 @@ function startDemoReset() {
demoTask = cron.schedule('0 * * * *', () => {
try {
- const { db } = require('./db/database');
const { resetDemoUser } = require('./demo/demo-reset');
- resetDemoUser(db);
+ resetDemoUser();
} catch (err) {
console.error('[Demo Reset] Error:', err.message);
}