/** * portal/webhooks/api.mjs — HAL → 포털 이벤트 수신과 포털 건강 확인. * * POST /internal/hal-events HAL 이 작업 상태 변화를 밀어 넣는다(lib/serviceauth, 키 id 'hal'). 포털 쪽 작업은 5초 주기로 HAL 을 다시 묻는 구조이므로 * 여기서는 "그 HAL 작업을 기다리는 포털 작업" 의 next_run_at 을 지금으로 당겨 바로 깨운다(둘 중 빠른 쪽, docs/HAL-JOBS.md §6). * GET /api/health HAL 모듈 설명자가 부르는 건강 확인(버전·DB·작업 엔진). */ import { verifyCtx } from '../../lib/serviceauth.mjs'; import { HttpError } from '../../lib/http.mjs'; import { nowIso } from '../../lib/db.mjs'; export async function register(app, deps) { const { store } = deps; app.get('/api/health', (ctx) => { let db = false; try { db = !!store.get('SELECT 1 AS ok').ok; } catch (_) {} return { ok: db, version: deps.version, db, jobs: deps.jobs?.kinds().length || 0, time: nowIso() }; }); app.post('/internal/hal-events', async (ctx) => { const r = await verifyCtx(ctx, (id) => (id === 'hal' ? deps.vault.get('service:hal') : null)); if (!r.ok) throw new HttpError(401, 'service_auth_failed', `서비스 인증 실패(${r.reason})`); const ev = await ctx.body(); const halJobId = String(ev.halJobId || ev.id || ''); if (!halJobId) return { ok: true, woken: 0 }; // 기다리는 포털 작업: 단계 결과에 halJobId 를 적어 둔 작업(halJobFor) 또는 deployments.hal_job_id 로 연결된 작업 const now = nowIso(); let woken = 0; woken += store.run('UPDATE jobs SET next_run_at=?, updated_at=? WHERE status=\'queued\' AND next_run_at>? AND steps_json LIKE ?', now, now, now, `%${halJobId}%`).changes; const dep = store.get('SELECT job_id FROM deployments WHERE hal_job_id=?', halJobId); if (dep && dep.job_id) woken += store.run('UPDATE jobs SET next_run_at=?, updated_at=? WHERE id=? AND status=\'queued\' AND next_run_at>?', now, now, dep.job_id, now).changes; return { ok: true, woken }; }); }