mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-23 16:01:15 +00:00
Contacts: - Contact groups/lists with JMAP members map and composer expansion - vCard import/export with RFC 6350 parser and duplicate detection - Bulk operations (multi-select, delete, group add, export) Search: - Advanced search panel with JMAP filter fields - Search chips for active filters visualization - Debounced inputs with AbortController deduplication Vacation: - JMAP VacationResponse singleton management - Settings tab with date range and message configuration - Sidebar indicator when vacation responder is active Auth: - TOTP 2FA support with Stalwart-compatible password$totp format Infrastructure: - Docker multi-stage build with standalone Next.js output - Structured server-side logger with text/JSON format - CSP Report-Only and security headers via proxy middleware - Layout refactoring (HTML structure in root layout) - Playwright E2E framework setup Testing: 450+ tests (identity, contacts, vCard, threads, headers, components) i18n: All new strings added to all 8 locales
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
|
|
const LEVELS: Record<LogLevel, number> = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
|
|
const COLORS: Record<LogLevel, string> = {
|
|
error: '\x1b[31m',
|
|
warn: '\x1b[33m',
|
|
info: '\x1b[34m',
|
|
debug: '\x1b[90m',
|
|
};
|
|
const RESET = '\x1b[0m';
|
|
|
|
function getLevel(): number {
|
|
const env = process.env.LOG_LEVEL?.toLowerCase() as LogLevel | undefined;
|
|
return env && env in LEVELS ? LEVELS[env] : LEVELS.info;
|
|
}
|
|
|
|
function isJson(): boolean {
|
|
return process.env.LOG_FORMAT?.toLowerCase() === 'json';
|
|
}
|
|
|
|
function log(level: LogLevel, message: string, extra?: Record<string, unknown>): void {
|
|
if (LEVELS[level] > getLevel()) return;
|
|
|
|
if (isJson()) {
|
|
const entry: Record<string, unknown> = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
message,
|
|
...extra,
|
|
};
|
|
const out = JSON.stringify(entry);
|
|
if (level === 'error' || level === 'warn') {
|
|
console.error(out);
|
|
} else {
|
|
console.log(out);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const color = COLORS[level];
|
|
const tag = `${color}[${level.toUpperCase().padEnd(5)}]${RESET}`;
|
|
const ts = new Date().toISOString();
|
|
const suffix = extra && Object.keys(extra).length > 0
|
|
? ` ${COLORS.debug}${JSON.stringify(extra)}${RESET}`
|
|
: '';
|
|
|
|
if (level === 'error' || level === 'warn') {
|
|
console.error(`${tag} ${ts} ${message}${suffix}`);
|
|
} else {
|
|
console.log(`${tag} ${ts} ${message}${suffix}`);
|
|
}
|
|
}
|
|
|
|
export const logger = {
|
|
error: (message: string, extra?: Record<string, unknown>) => log('error', message, extra),
|
|
warn: (message: string, extra?: Record<string, unknown>) => log('warn', message, extra),
|
|
info: (message: string, extra?: Record<string, unknown>) => log('info', message, extra),
|
|
debug: (message: string, extra?: Record<string, unknown>) => log('debug', message, extra),
|
|
request: (method: string, path: string, status: number, durationMs: number) =>
|
|
log('info', `${method} ${path} ${status}`, { method, path, status, durationMs }),
|
|
};
|