v2.6.2 — TREK Rebrand, OSM Enrichment, File Management, Hotel Bookings & Bug Fixes

Rebrand:
- NOMAD → TREK branding across all UI, translations, server, PWA manifest
- New TREK logos (dark/light, with/without icon)
- Liquid glass toast notifications

Bugs Fixed:
- HTTPS redirect now opt-in only (FORCE_HTTPS=true), fixes #33 #43 #52 #54 #55
- PDF export "Tag" fallback uses i18n, fixes #15
- Vacay sharing color collision detection, fixes #25
- Backup settings import fix (PR #47)
- Atlas country detection uses smallest bounding box, fixes #31
- JPY and zero-decimal currencies formatted correctly, fixes #32
- HTML lang="en" instead of hardcoded "de", fixes #34
- Duplicate translation keys removed
- setSelectedAssignmentId crash fixed

New Features:
- OSM enrichment: Overpass API for opening hours, Wikimedia Commons for photos
- Reverse geocoding on map right-click to add places
- OIDC config via environment variables (OIDC_ISSUER, OIDC_CLIENT_ID, etc.), fixes #48
- Multi-arch Docker build (ARM64 + AMD64), fixes #11
- File management: star, trash/restore, upload owner, assign to places/bookings, notes
- Markdown rendering in Collab Notes with expand modal, fixes #17
- Type-specific booking fields (flight: airline/number/airports, hotel: check-in/out/days, train: number/platform/seat), fixes #35
- Hotel bookings auto-create accommodations, bidirectional sync
- Multiple hotels per day with check-in/check-out color coding
- Ko-fi and Buy Me a Coffee support cards
- GitHub releases proxy with server-side caching
This commit is contained in:
Maurice
2026-03-28 16:38:08 +01:00
parent 5940b7f24e
commit e78c2a97bd
51 changed files with 3460 additions and 510 deletions
+33 -7
View File
@@ -84,10 +84,10 @@ router.get('/app-config', (_req: Request, res: Response) => {
const isDemo = process.env.DEMO_MODE === 'true';
const { version } = require('../../package.json');
const hasGoogleKey = !!db.prepare("SELECT maps_api_key FROM users WHERE role = 'admin' AND maps_api_key IS NOT NULL AND maps_api_key != '' LIMIT 1").get();
const oidcDisplayName = (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_display_name'").get() as { value: string } | undefined)?.value || null;
const oidcDisplayName = process.env.OIDC_DISPLAY_NAME || (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_display_name'").get() as { value: string } | undefined)?.value || null;
const oidcConfigured = !!(
(db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_issuer'").get() as { value: string } | undefined)?.value &&
(db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_client_id'").get() as { value: string } | undefined)?.value
(process.env.OIDC_ISSUER || (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_issuer'").get() as { value: string } | undefined)?.value) &&
(process.env.OIDC_CLIENT_ID || (db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_client_id'").get() as { value: string } | undefined)?.value)
);
res.json({
allow_registration: isDemo ? false : allowRegistration,
@@ -98,7 +98,7 @@ router.get('/app-config', (_req: Request, res: Response) => {
oidc_display_name: oidcConfigured ? (oidcDisplayName || 'SSO') : undefined,
allowed_file_types: (db.prepare("SELECT value FROM app_settings WHERE key = 'allowed_file_types'").get() as { value: string } | undefined)?.value || 'jpg,jpeg,png,gif,webp,heic,pdf,doc,docx,xls,xlsx,txt,csv',
demo_mode: isDemo,
demo_email: isDemo ? 'demo@nomad.app' : undefined,
demo_email: isDemo ? 'demo@trek.app' : undefined,
demo_password: isDemo ? 'demo12345' : undefined,
});
});
@@ -107,7 +107,7 @@ router.post('/demo-login', (_req: Request, res: Response) => {
if (process.env.DEMO_MODE !== 'true') {
return res.status(404).json({ error: 'Not found' });
}
const user = db.prepare('SELECT * FROM users WHERE email = ?').get('demo@nomad.app') as User | undefined;
const user = db.prepare('SELECT * FROM users WHERE email = ?').get('demo@trek.app') as User | undefined;
if (!user) return res.status(500).json({ error: 'Demo user not found' });
const token = generateToken(user);
const { password_hash, maps_api_key, openweather_api_key, unsplash_api_key, ...safe } = user;
@@ -205,7 +205,7 @@ router.get('/me', authenticate, (req: Request, res: Response) => {
router.put('/me/password', authenticate, rateLimiter(5, RATE_LIMIT_WINDOW), (req: Request, res: Response) => {
const authReq = req as AuthRequest;
if (process.env.DEMO_MODE === 'true' && authReq.user.email === 'demo@nomad.app') {
if (process.env.DEMO_MODE === 'true' && authReq.user.email === 'demo@trek.app') {
return res.status(403).json({ error: 'Password change is disabled in demo mode.' });
}
const { current_password, new_password } = req.body;
@@ -229,7 +229,7 @@ router.put('/me/password', authenticate, rateLimiter(5, RATE_LIMIT_WINDOW), (req
router.delete('/me', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
if (process.env.DEMO_MODE === 'true' && authReq.user.email === 'demo@nomad.app') {
if (process.env.DEMO_MODE === 'true' && authReq.user.email === 'demo@trek.app') {
return res.status(403).json({ error: 'Account deletion is disabled in demo mode.' });
}
if (authReq.user.role === 'admin') {
@@ -497,4 +497,30 @@ router.get('/travel-stats', authenticate, (req: Request, res: Response) => {
});
});
// GitHub releases proxy (cached, avoids client-side rate limits)
let releasesCache: { data: unknown[]; fetchedAt: number } | null = null;
const RELEASES_CACHE_TTL = 30 * 60 * 1000;
router.get('/github-releases', authenticate, async (req: Request, res: Response) => {
const page = parseInt(req.query.page as string) || 1;
const perPage = Math.min(parseInt(req.query.per_page as string) || 10, 30);
if (page === 1 && releasesCache && Date.now() - releasesCache.fetchedAt < RELEASES_CACHE_TTL) {
return res.json(releasesCache.data.slice(0, perPage));
}
try {
const resp = await fetch(
`https://api.github.com/repos/mauriceboe/NOMAD/releases?per_page=${perPage}&page=${page}`,
{ headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } }
);
if (!resp.ok) return res.json([]);
const data = await resp.json();
if (page === 1) releasesCache = { data, fetchedAt: Date.now() };
res.json(data);
} catch {
res.status(500).json({ error: 'Failed to fetch releases' });
}
});
export default router;