- Custom i18n provider with React Context + localStorage - Hebrew RTL support (dir=rtl on html) - All pages + components use t() calls - FullCalendar + dates locale-aware - Language selector in Settings wired to context Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
// System health & monitoring routes — 2026-03-29
|
|
module.exports = async function systemRoutes(app, opts) {
|
|
const os = require('os');
|
|
|
|
// GET /api/v1/system/health — comprehensive system status
|
|
app.get('/system/health', async (request, reply) => {
|
|
// System info
|
|
const system = {
|
|
hostname: os.hostname(),
|
|
uptime: Math.floor(os.uptime()),
|
|
loadavg: os.loadavg(),
|
|
cpus: os.cpus().length,
|
|
memory: {
|
|
total: Math.round(os.totalmem() / 1024 / 1024),
|
|
free: Math.round(os.freemem() / 1024 / 1024),
|
|
used_pct: Math.round((1 - os.freemem() / os.totalmem()) * 100)
|
|
}
|
|
};
|
|
|
|
// DB check
|
|
let db_status = { status: 'error' };
|
|
try {
|
|
const { rows } = await app.db.query('SELECT NOW() as time, pg_database_size(current_database()) as size');
|
|
db_status = {
|
|
status: 'ok',
|
|
time: rows[0].time,
|
|
size_mb: Math.round(rows[0].size / 1024 / 1024)
|
|
};
|
|
} catch (e) {
|
|
db_status = { status: 'error', message: e.message };
|
|
}
|
|
|
|
// Redis check
|
|
let redis_status = { status: 'error' };
|
|
try {
|
|
const pong = await app.redis.ping();
|
|
const info = await app.redis.info('memory');
|
|
const usedMem = info.match(/used_memory_human:(.+)/)?.[1]?.trim();
|
|
redis_status = {
|
|
status: pong === 'PONG' ? 'ok' : 'error',
|
|
memory: usedMem
|
|
};
|
|
} catch (e) {
|
|
redis_status = { status: 'error', message: e.message };
|
|
}
|
|
|
|
// Task stats
|
|
let task_stats = {};
|
|
try {
|
|
const { rows } = await app.db.query(`
|
|
SELECT status, count(*)::int as count FROM tasks GROUP BY status
|
|
UNION ALL SELECT 'total', count(*)::int FROM tasks
|
|
UNION ALL SELECT 'users', count(*)::int FROM users
|
|
UNION ALL SELECT 'goals', count(*)::int FROM goals
|
|
`);
|
|
task_stats = Object.fromEntries(rows.map(r => [r.status, r.count]));
|
|
} catch (e) {
|
|
task_stats = { error: e.message };
|
|
}
|
|
|
|
// Determine overall status
|
|
const overall = (db_status.status === 'ok' && redis_status.status === 'ok') ? 'ok' : 'degraded';
|
|
|
|
return {
|
|
status: overall,
|
|
timestamp: new Date().toISOString(),
|
|
system,
|
|
database: db_status,
|
|
redis: redis_status,
|
|
tasks: task_stats,
|
|
version: require('../../package.json').version || '1.0.0'
|
|
};
|
|
});
|
|
|
|
// GET /api/v1/system/ping — lightweight liveness check
|
|
app.get('/system/ping', async () => ({ pong: true, ts: Date.now() }));
|
|
};
|