mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
d7a71c0572
Todos already support a due_date field but nothing notifies the user when a deadline is approaching — you'd only remember if you happened to look at the Lists tab. This wires a reminder into the existing notification pipeline so due-date todos behave like trip-start reminders. Details: - New `todo_due` event type alongside trip_reminder; all four channels (in-app, email, webhook, ntfy) supported and toggleable per user in Settings > Notifications. - New daily scheduler task (9 AM local TZ) queries unchecked todos whose due_date is within the next 3 days. Each todo gets at most one reminder per 24 hours, tracked via a new todo_items.reminded_at column (migration 116). - If the todo has an assigned user, only that user is reminded; if not, every member of the trip gets the notification. - Strings added in all 15 UI languages and for all notification carriers. - Gated by app_settings.notify_todo_due (default on) so admins can disable it globally.
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
import 'dotenv/config';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs';
|
|
import { createApp } from './app';
|
|
|
|
// Create upload and data directories on startup
|
|
const uploadsDir = path.join(__dirname, '../uploads');
|
|
const photosDir = path.join(uploadsDir, 'photos');
|
|
const filesDir = path.join(uploadsDir, 'files');
|
|
const coversDir = path.join(uploadsDir, 'covers');
|
|
const avatarsDir = path.join(uploadsDir, 'avatars');
|
|
const backupsDir = path.join(__dirname, '../data/backups');
|
|
const tmpDir = path.join(__dirname, '../data/tmp');
|
|
|
|
[uploadsDir, photosDir, filesDir, coversDir, avatarsDir, backupsDir, tmpDir].forEach(dir => {
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
});
|
|
|
|
const app = createApp();
|
|
|
|
import * as scheduler from './scheduler';
|
|
|
|
const PORT = process.env.PORT || 3001;
|
|
const server = app.listen(PORT, () => {
|
|
const { logInfo: sLogInfo, logWarn: sLogWarn } = require('./services/auditLog');
|
|
const LOG_LVL = (process.env.LOG_LEVEL || 'info').toLowerCase();
|
|
const tz = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
const origins = process.env.ALLOWED_ORIGINS || '(same-origin)';
|
|
const banner = [
|
|
'──────────────────────────────────────',
|
|
' TREK API started',
|
|
` Port: ${PORT}`,
|
|
` Environment: ${process.env.NODE_ENV || 'development'}`,
|
|
` Timezone: ${tz}`,
|
|
` Origins: ${origins}`,
|
|
` Log level: ${LOG_LVL}`,
|
|
` Log file: /app/data/logs/trek.log`,
|
|
` PID: ${process.pid}`,
|
|
` User: uid=${process.getuid?.()} gid=${process.getgid?.()}`,
|
|
'──────────────────────────────────────',
|
|
];
|
|
banner.forEach(l => console.log(l));
|
|
if (process.env.DEMO_MODE === 'true') sLogInfo('Demo mode: ENABLED');
|
|
if (process.env.DEMO_MODE === 'true' && process.env.NODE_ENV === 'production') {
|
|
sLogWarn('SECURITY WARNING: DEMO_MODE is enabled in production!');
|
|
}
|
|
scheduler.start();
|
|
scheduler.startTripReminders();
|
|
scheduler.startTodoReminders();
|
|
scheduler.startVersionCheck();
|
|
scheduler.startDemoReset();
|
|
scheduler.startIdempotencyCleanup();
|
|
scheduler.startTrekPhotoCacheCleanup();
|
|
const { startTokenCleanup } = require('./services/ephemeralTokens');
|
|
startTokenCleanup();
|
|
import('./websocket').then(({ setupWebSocket }) => {
|
|
setupWebSocket(server);
|
|
});
|
|
});
|
|
|
|
// Graceful shutdown
|
|
function shutdown(signal: string): void {
|
|
const { logInfo: sLogInfo, logError: sLogError } = require('./services/auditLog');
|
|
const { closeMcpSessions } = require('./mcp');
|
|
sLogInfo(`${signal} received — shutting down gracefully...`);
|
|
scheduler.stop();
|
|
closeMcpSessions();
|
|
server.close(() => {
|
|
sLogInfo('HTTP server closed');
|
|
const { closeDb } = require('./db/database');
|
|
closeDb();
|
|
sLogInfo('Shutdown complete');
|
|
process.exit(0);
|
|
});
|
|
setTimeout(() => {
|
|
sLogError('Forced shutdown after timeout');
|
|
process.exit(1);
|
|
}, 10000);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
|
|
export default app;
|