feat: extend /health endpoint with OS metrics for Station cockpit
This commit is contained in:
parent
465c943e0a
commit
8d9f4e22b0
60
server.js
60
server.js
|
|
@ -20,6 +20,8 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const os = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// ── Auth: WebAuthn (ESM dynamic import) ─────────────────────────────
|
||||
let webauthn = null;
|
||||
|
|
@ -181,6 +183,15 @@ function extractSource(url) {
|
|||
} catch { return 'unknown'; }
|
||||
}
|
||||
|
||||
// ── 文字コード正規化 ─────────────────────────
|
||||
function normalizeCharset(raw) {
|
||||
const e = (raw || '').toLowerCase().replace(/['"]/g, '').trim();
|
||||
if (['shift_jis', 'shift-jis', 'sjis', 'x-sjis', 'ms_kanji', 'ms932', 'windows-31j', 'csshiftjis'].includes(e)) return 'shift_jis';
|
||||
if (['euc-jp', 'euc_jp', 'x-euc-jp', 'cseucpkdfmtjapanese'].includes(e)) return 'euc-jp';
|
||||
if (e.startsWith('utf')) return 'utf-8';
|
||||
return 'utf-8';
|
||||
}
|
||||
|
||||
// ── OGP フェッチ ───────────────────────────
|
||||
async function fetchMeta(url) {
|
||||
try {
|
||||
|
|
@ -191,15 +202,27 @@ async function fetchMeta(url) {
|
|||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const buffer = await res.arrayBuffer();
|
||||
|
||||
// 文字コード判定(先頭2000バイトからcharsetを探す)
|
||||
const headSnippet = new TextDecoder('utf-8', { fatal: false }).decode(buffer.slice(0, 2000));
|
||||
// 文字コード判定: 1) Content-Typeヘッダー優先 2) HTMLメタタグ確認
|
||||
// iso-8859-1はバイト値0-255をロスレスでデコードするためcharset検出に最適
|
||||
let encoding = 'utf-8';
|
||||
const charsetMatch = headSnippet.match(/charset=["']?(shift_jis|euc-jp|utf-8)["']?/i);
|
||||
if (charsetMatch && charsetMatch[1]) {
|
||||
encoding = charsetMatch[1].toLowerCase();
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
const ctMatch = contentType.match(/charset=([^\s;]+)/i);
|
||||
if (ctMatch) {
|
||||
encoding = normalizeCharset(ctMatch[1]);
|
||||
}
|
||||
if (encoding === 'utf-8') {
|
||||
const headSnippet = new TextDecoder('iso-8859-1').decode(new Uint8Array(buffer).slice(0, 2000));
|
||||
const metaMatch = headSnippet.match(/charset=["']?([^"'\s;>]+)/i);
|
||||
if (metaMatch) encoding = normalizeCharset(metaMatch[1]);
|
||||
}
|
||||
|
||||
const html = new TextDecoder(encoding).decode(buffer);
|
||||
let html;
|
||||
try {
|
||||
html = new TextDecoder(encoding).decode(buffer);
|
||||
} catch {
|
||||
// TextDecoder が対象エンコーディング非対応の場合は UTF-8 フォールバック
|
||||
html = new TextDecoder('utf-8', { fatal: false }).decode(buffer);
|
||||
}
|
||||
const doc = parse(html);
|
||||
|
||||
const og = (p) => doc.querySelector(`meta[property="${p}"]`)?.getAttribute('content') || '';
|
||||
|
|
@ -571,9 +594,30 @@ async function initDB() {
|
|||
function buildRouter() {
|
||||
const r = express.Router();
|
||||
|
||||
// ヘルスチェック
|
||||
// ヘルスチェック(Station コックピット向けに拡張)
|
||||
r.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', gemini: !!genAI, users: Object.values(KEY_MAP).length });
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
const mem = os.freemem(), total = os.totalmem();
|
||||
let disk = null;
|
||||
try {
|
||||
const df = execSync('df -B1 / 2>/dev/null', { timeout: 2000 }).toString();
|
||||
const p = df.trim().split('\n')[1].split(/\s+/);
|
||||
disk = { total_gb: Math.round(parseInt(p[1])/1e9*10)/10, used_gb: Math.round(parseInt(p[2])/1e9*10)/10, use_pct: Math.round(parseInt(p[2])/parseInt(p[1])*100) };
|
||||
} catch(_) {}
|
||||
res.json({
|
||||
status: 'ok',
|
||||
gemini: !!genAI,
|
||||
users: Object.values(KEY_MAP).length,
|
||||
hostname: os.hostname(),
|
||||
uptime_s: Math.floor(os.uptime()),
|
||||
load_avg: os.loadavg().map(l => Math.round(l * 100) / 100),
|
||||
mem_used_mb: Math.round((total - mem) / 1024 / 1024),
|
||||
mem_total_mb: Math.round(total / 1024 / 1024),
|
||||
disk,
|
||||
platform: os.platform(),
|
||||
node_version: process.version,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// 認証テスト (UI用)
|
||||
|
|
|
|||
Loading…
Reference in New Issue