chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+244
View File
@@ -0,0 +1,244 @@
/***********************************************************************************************************************************************************************
* File Name: health.service.js
* Type of Program: Service
* Description: Health check registry — runs all dependency checks and builds two response formats.
*
* runDashboard() → rich human-readable payload (for GET /api/health)
* runReadiness() → compact machine payload (for GET /api/health/ready)
*
* Both call runChecks() internally and share the same check results.
*
* Check registry (CHECKS array):
* Each entry is self-contained — name, criticality, enable condition, metadata, and runner.
* To add a new infrastructure check: push one entry here. Nothing else changes.
*
* Criticality model:
* critical: true → failure → overall "unhealthy" → HTTP 503
* critical: false → failure → overall "degraded" → HTTP 200
*
* Checks included:
* ✓ database — PostgreSQL via Sequelize (CRITICAL)
* ✓ cache — Redis PING; skipped in memory mode (optional)
* ✓ storage — S3/Garage HeadBucket (optional)
* ✓ email — Gmail API profile fetch (OAuth2) (optional)
*
* Checks intentionally excluded:
* ✗ chibisafe — third-party CDN; not owned infrastructure
* ✗ paypal — third-party payment gateway; not owned infrastructure
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 20, 2026
***********************************************************************************************************************************************************************/
"use strict";
const os = require('os');
const sequelize = require('../config/db.config');
const redis = require('../config/redis.config');
const { ping: pingS3 } = require('./s3.service');
const { ping: pingEmail } = require('./email.service');
const PKG = require('../package.json');
const TIMEOUT_MS = 3000;
// ─── Connection status labels (internal status → human label) ─────────────────
const CONNECTION_LABEL = {
healthy: 'established',
unhealthy: 'unavailable',
timeout: 'timeout',
skipped: 'not configured',
};
// ─── Check registry ───────────────────────────────────────────────────────────
//
// Fields:
// name {string} — key used in response JSON
// critical {boolean} — true: 503 on failure; false: degraded (200)
// enabled {boolean} — false: skipped entirely
// skipNote {string} — shown in dashboard when enabled = false
// meta {object} — extra context shown in the dashboard (provider, host, port, etc.)
// run {async fn} — throws on failure, resolves on success
const CHECKS = [
{
name: 'database',
critical: true,
enabled: true,
meta: { dialect: 'postgresql', host: process.env.DB_HOST, port: Number(process.env.DB_PORT) || 5432 },
run: () => sequelize.authenticate(),
},
{
name: 'cache',
critical: false,
enabled: !!redis,
skipNote: 'CACHE_DRIVER=memory — Redis is not used in this environment',
meta: { driver: 'redis', url: process.env.REDIS_URL || 'redis://127.0.0.1:6379' },
run: () => redis.ping(),
},
{
name: 'storage',
critical: false,
enabled: !!(process.env.S3_ENDPOINT || process.env.S3_PUBLIC_URL),
skipNote: 'S3_ENDPOINT is not configured',
meta: { provider: 's3', endpoint: process.env.S3_PUBLIC_URL ?? process.env.S3_ENDPOINT, bucket: process.env.S3_BUCKET },
run: pingS3,
},
{
name: 'email',
critical: false,
enabled: !!(process.env.GOOGLE_CLIENT_ID && process.env.GMAIL_REFRESH_TOKEN),
skipNote: 'GMAIL_REFRESH_TOKEN is not configured',
meta: { provider: 'gmail-api', account: process.env.EMAIL_FROM },
run: pingEmail,
},
];
// ─── Internal helpers ─────────────────────────────────────────────────────────
const mb = (bytes) => Math.round(bytes / 1024 / 1024 * 10) / 10;
function formatUptime(seconds) {
if (seconds < 60) return `${seconds} secs`;
if (seconds < 3600) return `${Math.floor(seconds / 60)} mins`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
if (seconds < 2592000) return `${Math.floor(seconds / 86400)} days`;
if (seconds < 31536000) return `${Math.floor(seconds / 2592000)} months`;
return `${Math.floor(seconds / 31536000)} years`;
}
function memoryInfo() {
const m = process.memoryUsage();
return {
heap_used_mb: mb(m.heapUsed),
heap_total_mb: mb(m.heapTotal),
rss_mb: mb(m.rss),
external_mb: mb(m.external),
};
}
function systemInfo() {
const load = os.loadavg();
return {
platform: process.platform,
arch: process.arch,
cpus: os.cpus().length,
load_avg: {
'1m': Math.round(load[0] * 100) / 100,
'5m': Math.round(load[1] * 100) / 100,
'15m': Math.round(load[2] * 100) / 100,
},
memory: memoryInfo(),
};
}
function appInfo() {
return {
name: PKG.name,
version: PKG.version,
environment: process.env.NODE_ENV || 'development',
node_version: process.version,
pid: process.pid,
};
}
// ─── Check runner ─────────────────────────────────────────────────────────────
async function runCheck(check) {
const { name, critical, enabled, skipNote, run } = check;
if (!enabled) {
return [name, { status: 'skipped', note: skipNote, critical }];
}
const start = Date.now();
try {
await Promise.race([
run(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS)
),
]);
return [name, { status: 'healthy', latency_ms: Date.now() - start, critical }];
} catch (err) {
return [name, {
status: err.message === 'timeout' ? 'timeout' : 'unhealthy',
latency_ms: Date.now() - start,
critical,
}];
}
}
async function runChecks() {
const results = await Promise.all(CHECKS.map(runCheck));
const checks = Object.fromEntries(results);
const criticalFailed = results.some(
([, r]) => r.critical && r.status !== 'healthy' && r.status !== 'skipped'
);
const anyDegraded = results.some(
([, r]) => !r.critical && r.status !== 'healthy' && r.status !== 'skipped'
);
const overallStatus = criticalFailed ? 'unhealthy'
: anyDegraded ? 'degraded'
: 'healthy';
return { checks, criticalFailed, overallStatus };
}
// ─── Dashboard payload (GET /api/health) ─────────────────────────────────────
//
// Human-readable — full context: app info, system info, service connection details.
// Uses "established / unavailable / timeout / not configured" language.
async function runDashboard() {
const { checks, criticalFailed, overallStatus } = await runChecks();
const services = {};
for (const check of CHECKS) {
const result = checks[check.name];
services[check.name] = {
connection: CONNECTION_LABEL[result.status] ?? result.status,
...(result.latency_ms !== undefined && { latency_ms: result.latency_ms }),
...(result.note !== undefined && { note: result.note }),
...(check.meta !== undefined && check.meta),
critical: result.critical,
};
}
return {
httpStatus: criticalFailed ? 503 : 200,
body: {
status: overallStatus,
app: appInfo(),
uptime: formatUptime(Math.floor(process.uptime())),
timestamp: new Date().toISOString(),
system: systemInfo(),
services,
},
};
}
// ─── Readiness payload (GET /api/health/ready) ───────────────────────────────
//
// Compact — intended for machines (Kubernetes probe, deployment scripts, CI gates).
// Uses "healthy / unhealthy / timeout / skipped" language.
async function runReadiness() {
const { checks, criticalFailed, overallStatus } = await runChecks();
return {
httpStatus: criticalFailed ? 503 : 200,
body: {
status: overallStatus,
version: PKG.version,
uptime: formatUptime(Math.floor(process.uptime())),
timestamp: new Date().toISOString(),
checks,
memory: memoryInfo(),
},
};
}
module.exports = { runDashboard, runReadiness };