Add calendar export, SEO, error monitoring, backups, audit trail, and a starter test suite

Six site improvements picked from a "what could be better" review, plus a Jest
test suite covering the two areas with the trickiest money-handling history
in this project (early-bird pricing tranches, donation-leg accounting):

- "Add to calendar" .ics download on event pages and in confirmation emails
- sitemap.xml, robots.txt, and Open Graph/Twitter metadata for public pages
- Sentry error monitoring (backend + frontend), a no-op until SENTRY_DSN is set
- Nightly local pg_dump backups with a Site Settings tab to browse/trigger/download
- Admin audit trail for refunds, donations, manual registrations, event and
  settings changes, and staff-initiated cancellations
- Jest tests reproducing and guarding against the 1.8.0 tranche-pricing bug
  and the 1.4.2 donation-balance-inflation bug

Wallet passes (Google/Apple) were scoped out of this round — Apple Wallet
needs a paid Apple Developer account the project doesn't have yet, and the
user preferred shipping both together later rather than Google alone now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 14:50:11 +02:00
co-authored by Claude Sonnet 5
parent 98ac26bf70
commit 54b89d4f4b
43 changed files with 8414 additions and 92 deletions
@@ -0,0 +1,53 @@
const path = require('path');
const fs = require('fs');
const { runBackup, listBackups, BACKUP_DIR, BACKUP_FILENAME_RE } = require('../utils/backupUtils');
const { safeErrorMessage } = require('../utils/errorUtils');
// @desc List local database backups (newest first)
// @route GET /api/backups
// @access Admin
const getBackups = async (req, res) => {
try {
const backups = await listBackups();
res.json(backups);
} catch (e) {
res.status(500).json({ message: safeErrorMessage(e) });
}
};
// @desc Manually trigger a backup now
// @route POST /api/backups/run
// @access Admin
const triggerBackup = async (req, res) => {
try {
const result = await runBackup();
res.status(201).json(result);
} catch (e) {
res.status(500).json({ message: safeErrorMessage(e) });
}
};
// @desc Download a backup file
// @route GET /api/backups/:filename/download
// @access Admin
const downloadBackup = async (req, res) => {
try {
const { filename } = req.params;
// Reject anything that isn't exactly the shape this app generates, before ever
// touching the filesystem — same posture as the event-alias path-safety fix.
if (!BACKUP_FILENAME_RE.test(filename)) {
res.status(400);
throw new Error('Invalid backup filename');
}
const filePath = path.join(BACKUP_DIR, filename);
if (!fs.existsSync(filePath)) {
res.status(404);
throw new Error('Backup not found');
}
res.download(filePath, filename);
} catch (e) {
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(e) });
}
};
module.exports = { getBackups, triggerBackup, downloadBackup };