/** * lib/status.mjs — 사용자용 상태 어휘(§2.2)와 전이 규칙. * * 세 종류의 상태를 분리한다(§2.2 "하나의 상태로 합치지 않는다"). * JOB_STATUS 작업(배포·복원·이전·주문…)의 진행 상태 — 작업 엔진(lib/jobs)이 쓴다 * SERVICE_STATE 서비스(사이트·메일)의 생명주기 상태(§23.3) * HEALTH 외부 확인 건강 상태(§31) * 코드(영문)로 저장하고 화면은 label() 로 한국어를 얻는다. */ export const JOB_STATUS = Object.freeze({ NOT_READY: 'not_ready', NEEDS_SETUP: 'needs_setup', AWAITING_APPROVAL: 'awaiting_approval', AWAITING_PAYMENT: 'awaiting_payment', QUEUED: 'queued', RUNNING: 'running', AWAITING_EXTERNAL: 'awaiting_external', VERIFYING: 'verifying', PAUSED: 'paused', DONE: 'done', FAILED: 'failed', REVERTING: 'reverting', REVERTED: 'reverted', NEEDS_USER: 'needs_user', CANCELLED: 'cancelled', }); export const JOB_LABEL = Object.freeze({ not_ready: '준비 전', needs_setup: '설정 필요', awaiting_approval: '승인 대기', awaiting_payment: '결제 대기', queued: '처리 대기', running: '진행 중', awaiting_external: '외부 작업 대기', verifying: '검증 중', paused: '일시 중지', done: '완료', failed: '실패', reverting: '되돌리는 중', reverted: '되돌림 완료', needs_user: '사용자 확인 필요', cancelled: '취소됨', }); /** 끝난 상태(더 바뀌지 않음) */ export const TERMINAL = new Set(['done', 'failed', 'reverted', 'cancelled']); /** 사용자가 취소할 수 있는 상태 */ export const CANCELLABLE = new Set(['not_ready', 'needs_setup', 'awaiting_approval', 'awaiting_payment', 'queued', 'awaiting_external', 'needs_user', 'paused']); export const SERVICE_STATE = Object.freeze({ DRAFT: 'draft', BUILDING: 'building', DEPLOYING: 'deploying', LIVE: 'live', NEEDS_DOMAIN: 'needs_domain', PAUSED: 'paused', CANCEL_SCHEDULED: 'cancel_scheduled', READONLY_RETENTION: 'readonly_retention', PENDING_DELETE: 'pending_delete', DELETED: 'deleted', DELETE_HOLD: 'delete_hold', }); export const SERVICE_LABEL = Object.freeze({ draft: '초안', building: '제작 중', deploying: '배포 중', live: '운영 중', needs_domain: '도메인 설정 필요', paused: '일시 중지', cancel_scheduled: '해지 예약', readonly_retention: '읽기 전용 보존', pending_delete: '삭제 대기', deleted: '삭제 완료', delete_hold: '삭제 보류', }); export const HEALTH = Object.freeze({ OK: 'ok', DEGRADED: 'degraded', PARTIAL: 'partial_outage', DOWN: 'down', UNKNOWN: 'unknown', STALE: 'stale', NOT_PROVIDED: 'not_provided' }); export const HEALTH_LABEL = Object.freeze({ ok: '정상', degraded: '성능 저하', partial_outage: '일부 장애', down: '중단', unknown: '확인 전', stale: '정보 확인 지연', not_provided: '제공되지 않음' }); /** 배포 완료 세분(§11.2) */ export const DEPLOY_VERIFY = Object.freeze({ VERIFIED: 'verified', NEEDS_DOMAIN: 'needs_domain', PUBLIC: 'public' }); export const DEPLOY_VERIFY_LABEL = Object.freeze({ verified: '배포 검증 완료', needs_domain: '도메인 설정 필요', public: '공개 운영 중' }); /** 할당 상태(§13.3) */ export const ALLOC_LABEL = Object.freeze({ available: '사용 가능', reserved: '예약됨', in_use: '사용 중', returning: '반환 중', returned: '반환됨' }); /** 코드 → 한국어. 알 수 없는 코드는 그대로(화면에서 눈에 띄게) */ export function label(kind, code) { const map = { job: JOB_LABEL, service: SERVICE_LABEL, health: HEALTH_LABEL, deploy: DEPLOY_VERIFY_LABEL, alloc: ALLOC_LABEL }[kind] || {}; return map[code] || code || ''; } /** * 상태 정보가 오래됐는지(§2.2 `정보 확인 지연`). 마지막 갱신으로부터 staleAfterMs 가 지났으면 true. * @param {string} updatedAt ISO * @param {number} staleAfterMs 기본 5분 */ export function isStale(updatedAt, staleAfterMs = 5 * 60 * 1000, now = Date.now()) { if (!updatedAt) return true; return now - Date.parse(updatedAt) > staleAfterMs; }