mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
0497032ed7
BREAKING: Reservations have been completely rebuilt. Existing place-level reservations are no longer used. All reservations must be re-created via the Bookings tab. Your trips, places, and other data are unaffected. Reservation System (rebuilt from scratch): - Reservations now link to specific day assignments instead of places - Same place on different days can have independent reservations - New assignment picker in booking modal (grouped by day, searchable) - Removed day/place dropdowns from booking form - Reservation badges in day plan sidebar with type-specific icons - Reservation details in place inspector (only for selected assignment) - Reservation summary in day detail panel Day Detail Panel (new): - Opens on day click in the sidebar - Detailed weather: hourly forecast, precipitation, wind, sunrise/sunset - Historical climate averages for dates beyond 16 days - Accommodation management with check-in/check-out, confirmation number - Hotel assignment across multiple days with day range picker - Reservation overview for the day Places: - Places can now be assigned to the same day multiple times - Start time + end time fields (replaces single time field) - Map badges show multiple position numbers (e.g. "1 · 4") - Route optimization fixed for duplicate places - File attachments during place editing (not just creation) - Cover image upload during trip creation (not just editing) - Paste support (Ctrl+V) for images in trip, place, and file forms Internationalization: - 200+ hardcoded German strings translated to i18n (EN + DE) - Server error messages in English - Category seeds in English for new installations - All planner, register, photo, packing components translated UI/UX: - Auto dark mode (follows system preference, configurable in settings) - Navbar toggle switches light/dark (overrides auto) - Sidebar minimize buttons z-index fixed - Transport mode selector removed from day plan - CustomSelect supports grouped headers (isHeader option) - Optimistic updates for day notes (instant feedback) - Booking cards redesigned with type-colored headers and structured details Weather: - Wind speed in mph when using Fahrenheit setting - Weather description language matches app language Admin: - Weather info panel replaces OpenWeatherMap key input - "Recommended" badge styling updated
129 lines
4.0 KiB
JavaScript
129 lines
4.0 KiB
JavaScript
const cron = require('node-cron');
|
|
const archiver = require('archiver');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const dataDir = path.join(__dirname, '../data');
|
|
const backupsDir = path.join(dataDir, 'backups');
|
|
const uploadsDir = path.join(__dirname, '../uploads');
|
|
const settingsFile = path.join(dataDir, 'backup-settings.json');
|
|
|
|
const CRON_EXPRESSIONS = {
|
|
hourly: '0 * * * *',
|
|
daily: '0 2 * * *',
|
|
weekly: '0 2 * * 0',
|
|
monthly: '0 2 1 * *',
|
|
};
|
|
|
|
const VALID_INTERVALS = Object.keys(CRON_EXPRESSIONS);
|
|
|
|
let currentTask = null;
|
|
|
|
function loadSettings() {
|
|
try {
|
|
if (fs.existsSync(settingsFile)) {
|
|
return JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
}
|
|
} catch (e) {}
|
|
return { enabled: false, interval: 'daily', keep_days: 7 };
|
|
}
|
|
|
|
function saveSettings(settings) {
|
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
}
|
|
|
|
async function runBackup() {
|
|
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
const filename = `auto-backup-${timestamp}.zip`;
|
|
const outputPath = path.join(backupsDir, filename);
|
|
|
|
try {
|
|
// Flush WAL to main DB file before archiving
|
|
try { const { db } = require('./db/database'); db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const output = fs.createWriteStream(outputPath);
|
|
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
output.on('close', resolve);
|
|
archive.on('error', reject);
|
|
archive.pipe(output);
|
|
const dbPath = path.join(dataDir, 'travel.db');
|
|
if (fs.existsSync(dbPath)) archive.file(dbPath, { name: 'travel.db' });
|
|
if (fs.existsSync(uploadsDir)) archive.directory(uploadsDir, 'uploads');
|
|
archive.finalize();
|
|
});
|
|
console.log(`[Auto-Backup] Created: ${filename}`);
|
|
} catch (err) {
|
|
console.error('[Auto-Backup] Error:', err.message);
|
|
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
|
|
return;
|
|
}
|
|
|
|
const settings = loadSettings();
|
|
if (settings.keep_days > 0) {
|
|
cleanupOldBackups(settings.keep_days);
|
|
}
|
|
}
|
|
|
|
function cleanupOldBackups(keepDays) {
|
|
try {
|
|
const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1000;
|
|
const files = fs.readdirSync(backupsDir).filter(f => f.endsWith('.zip'));
|
|
for (const file of files) {
|
|
const filePath = path.join(backupsDir, file);
|
|
const stat = fs.statSync(filePath);
|
|
if (stat.birthtimeMs < cutoff) {
|
|
fs.unlinkSync(filePath);
|
|
console.log(`[Auto-Backup] Old backup deleted: ${file}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auto-Backup] Cleanup error:', err.message);
|
|
}
|
|
}
|
|
|
|
function start() {
|
|
if (currentTask) {
|
|
currentTask.stop();
|
|
currentTask = null;
|
|
}
|
|
|
|
const settings = loadSettings();
|
|
if (!settings.enabled) {
|
|
console.log('[Auto-Backup] Disabled');
|
|
return;
|
|
}
|
|
|
|
const expression = CRON_EXPRESSIONS[settings.interval] || CRON_EXPRESSIONS.daily;
|
|
currentTask = cron.schedule(expression, runBackup);
|
|
console.log(`[Auto-Backup] Scheduled: ${settings.interval} (${expression}), retention: ${settings.keep_days === 0 ? 'forever' : settings.keep_days + ' days'}`);
|
|
}
|
|
|
|
// Demo mode: hourly reset of demo user data
|
|
let demoTask = null;
|
|
|
|
function startDemoReset() {
|
|
if (demoTask) { demoTask.stop(); demoTask = null; }
|
|
if (process.env.DEMO_MODE !== 'true') return;
|
|
|
|
demoTask = cron.schedule('0 * * * *', () => {
|
|
try {
|
|
const { resetDemoUser } = require('./demo/demo-reset');
|
|
resetDemoUser();
|
|
} catch (err) {
|
|
console.error('[Demo Reset] Error:', err.message);
|
|
}
|
|
});
|
|
console.log('[Demo] Hourly reset scheduled (at :00 every hour)');
|
|
}
|
|
|
|
function stop() {
|
|
if (currentTask) { currentTask.stop(); currentTask = null; }
|
|
if (demoTask) { demoTask.stop(); demoTask = null; }
|
|
}
|
|
|
|
module.exports = { start, stop, startDemoReset, loadSettings, saveSettings, VALID_INTERVALS };
|