Demo baseline reset: full DB snapshot/restore (v2.3.0)
Hourly reset now restores entire DB from baseline snapshot instead of just deleting demo trips. This reverts ALL demo user changes including modifications to shared admin trips. Admin credentials (password, API keys) are preserved across resets. Admin can save new baseline via Admin Panel button. Removed demoWriteBlock middleware.
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Demo Baseline Button */}
|
||||
{demoMode && (
|
||||
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-xl flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-amber-900">Demo Baseline</p>
|
||||
<p className="text-xs text-amber-700">Save current state as the hourly reset point. All admin trips and settings will be preserved.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await adminApi.saveDemoBaseline()
|
||||
toast.success('Baseline saved! Resets will restore to this state.')
|
||||
} catch (e) {
|
||||
toast.error(e.response?.data?.error || 'Failed to save baseline')
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-amber-600 text-white rounded-lg text-sm font-semibold hover:bg-amber-700 transition-colors flex-shrink-0 ml-4"
|
||||
>
|
||||
Save Baseline
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4 mb-6">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user