HAL 0.4.0: 통합 설정 관제 — Webmaker·AriMail·Agent 서버 단위 설정을 HAL 이 관리(설정 목록·범위 global/server·비밀값 Vault·유효값 hash, /ops/api/settings·재인증, settings.apply → settings.apply.target → Agent module.configure(HMAC 설정 받기·hash 확인·hal-managed.json 0600·재시작·모듈 적용 확인), /ops/settings 화면·운영 현황 "설정 적용 필요", AI 도구 hal_settings_get/set/apply(비밀값 거부)), 도입 모듈 Webmaker 1.9.0·AriMail 1.1.0(HAL 0.4.0 대응 halCompat, hal-managed 적용·관리 항목 409 managed_by_hal 잠금·"HAL 관리" 표시) 재도입(pin 8f8716c·f959b65, import-sources 미수정 파일 갱신), Agent heartbeat 관리, module-as-user write --mode 600, 개발 지시 v0.4.0(Oz)·docs/HAL-SETTINGS.md·VERSION_HISTORY 0.4.0 — test/hal-040.test.mjs RED → GREEN 5/5, agent 11/11, 전체 31파일 통과
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
54db663531
commit
9f1e490b51
@@ -66,6 +66,8 @@ const PAGES = {
|
||||
server: async (id) => { const draw = async () => { const key = $('#metric-key')?.value || 'cpu'; const days = Number($('#metric-days')?.value || 30); try { const r = await api('GET', `/ops/api/servers/${id}/metrics?since=${encodeURIComponent(new Date(Date.now() - days * 86400000).toISOString())}&limit=5000`); $('#metrics').innerHTML = R.metricsSvg(r.samples, key); } catch (e) { $('#metrics').innerHTML = R.relayError(e); } }; $('#metric-key')?.addEventListener('change', draw); $('#metric-days')?.addEventListener('change', draw); await draw(); },
|
||||
job: async (id) => { try { const r = await api('GET', '/ops/api/jobs/' + id); const j = r.job; if (!['done', 'failed', 'cancelled', 'reverted'].includes(j.status)) { const jb = $('.job', body()); if (jb && jb.outerHTML !== R.jobBlock(j)) jb.outerHTML = R.jobBlock(j); } } catch (_) {} },
|
||||
recovery: async () => { document.querySelectorAll('[data-tab]').forEach((a) => a.onclick = (e) => { e.preventDefault(); document.querySelectorAll('[data-tab]').forEach((x) => x.classList.remove('on')); a.classList.add('on'); document.querySelectorAll('main section').forEach((s) => { s.hidden = s.id !== a.dataset.tab; }); }); if (location.hash === '#dr') $('[data-tab=dr]')?.click(); },
|
||||
// [0.4.0] 통합 설정 관제: 범위 바꾸면 그 범위로 다시 연다
|
||||
settings: () => { const sel = $('#settings-scope'); if (sel) sel.onchange = () => { location.href = `/ops/settings?module=${encodeURIComponent(sel.dataset.module)}&scope=${encodeURIComponent(sel.value)}`; }; },
|
||||
login: () => { const f = $('#login-form'); if (!f) return; f.onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(f); try { const r = await api('POST', '/ops/api/login', { login: fd.get('login'), password: fd.get('password'), totp: fd.get('totp') || undefined }); if (r.mfaRequired) { $('#totp-row').hidden = false; $('#msg').textContent = r.message || '인증 코드를 입력하세요'; return; } location.href = '/ops/'; } catch (err) { $('#msg').textContent = err.message; } }; },
|
||||
setup: () => { const f = $('#setup-form'); if (!f) return; f.onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(f); try { await api('POST', '/ops/api/setup', Object.fromEntries(fd)); toast('첫 운영자를 만들었습니다. 로그인하세요'); location.href = '/ops/login'; } catch (err) { $('#msg').textContent = err.message; } }; },
|
||||
};
|
||||
@@ -87,6 +89,13 @@ const ACT = {
|
||||
const r = await call('POST', '/ops/api/portal/relocate', { fromServerId: cur.server_id, toServerId: v.toServerId, version: v.version, ...c }); location.href = '/ops/jobs/' + r.job.id; },
|
||||
'module-health': async (b) => { const r = await call('POST', `/ops/api/modules/${b.m}/health`, { serverIds: b.s ? [b.s] : null }); location.href = '/ops/jobs/' + r.job.id; },
|
||||
'module-rollback': async (b) => { const c = await threeStep({ title: `${b.m} 되돌리기`, impacts: [`서버 ${b.s} 의 ${b.m} 을(를) 이전 릴리스로 되돌립니다`, '되돌리는 동안 해당 모듈이 잠시 중단될 수 있습니다', '이전 릴리스 디렉터리가 남아 있어야 합니다(최근 3개 보관)'], identifiers: [b.s], revertible: '되돌린 뒤 다시 배포하면 복구할 수 있습니다' }); if (!c) return; const r = await call('POST', `/ops/api/modules/${b.m}/rollback`, { ...c, serverIds: [b.s] }); location.href = '/ops/jobs/' + r.job.id; },
|
||||
// ---- [0.4.0] 통합 설정 관제: 저장(재인증) · 비밀값 삭제 · 서버에 적용(서버 선택·재시작 여부 → 작업)
|
||||
'settings-save': async () => { const f = $('#settings-form'); if (!f) return; const values = {}; for (const el of f.querySelectorAll('[data-key]')) { const k = el.dataset.key; const v = el.value; if (el.dataset.type === 'secret') { if (v !== '') values[k] = v; continue; } values[k] = v === '' ? null : v; }
|
||||
const r = await call('PUT', `/ops/api/settings/${f.dataset.module}`, { scope: f.dataset.scope, values }); toast(r.changed.length ? `저장했습니다(${r.changed.length}개${r.restartNeeded ? ' · 재시작 필요 항목 포함' : ''}) — "서버에 적용"으로 반영하세요` : '바뀐 항목이 없습니다'); reload(); },
|
||||
'settings-delete': async (b) => { const f = $('#settings-form'); if (!f || !confirm(`${b.key} 비밀값을 HAL 에서 지웁니다(서버에는 다음 적용 때 반영)`)) return; await call('DELETE', `/ops/api/settings/${f.dataset.module}/${encodeURIComponent(b.key)}?scope=${encodeURIComponent(f.dataset.scope)}`); reload(); },
|
||||
'settings-apply': async (b) => { const sv = await api('GET', `/ops/api/settings/${b.m}`); const servers = (sv.placements || []).map((p) => ({ id: p.id, name: p.name, status: p.status === 'isolated' ? 'isolated' : 'ready' })); const restartDefault = !!(sv.catalog && sv.catalog.restartDefault);
|
||||
const v = await ask(`${(sv.catalog && sv.catalog.label) || b.m} 설정 적용 — 대상 서버`, [{ name: 'serverIds', label: '대상 서버(이 모듈이 배치된 서버)', type: 'checks', html: R.serverChoiceHtml(servers, { name: 'serverIds', multi: true, eligible: b.s ? [b.s] : null }) }, { name: 'restart', label: '적용 뒤 서비스 재시작(재시작 필요 항목을 바꾼 경우)', type: 'checkbox', value: restartDefault }], { okLabel: '적용 작업 만들기', note: 'Agent 가 HAL 에서 설정을 직접 받아 모듈 데이터 디렉터리의 hal-managed.json 에 쓰고, 모듈이 적용한 결과(hash)를 확인합니다.' });
|
||||
if (!v) return; if (!v.serverIds || !v.serverIds.length) return toast('대상 서버를 하나 이상 선택하세요'); const r = await call('POST', `/ops/api/settings/${b.m}/apply`, { serverIds: v.serverIds, restart: !!v.restart, nonce: Date.now() }); toast(`설정 적용 작업을 만들었습니다(${r.targets.length}대)`); location.href = '/ops/jobs/' + r.job.id; },
|
||||
'module-reconcile': async () => { const r = await call('POST', '/ops/api/modules/reconcile', {}); location.href = '/ops/jobs/' + r.job.id; },
|
||||
'server-new': async () => { const v = await ask('서버 등록 — 1. 역할·위치', [{ name: 'name', label: '서버 이름', required: true }, { name: 'role', label: '역할', type: 'select', options: ['web', 'mail', 'backup', 'control'] }, { name: 'location', label: '위치(예: 서울 IDC-1)' }]); if (!v) return; const r = await call('POST', '/ops/api/servers', v); location.href = '/ops/servers/' + r.server.id; },
|
||||
'server-token': async (b) => { const r = await call('POST', `/ops/api/servers/${b.id}/token`); showOnce('일회용 등록 정보(10분·1회)', r.installCommand || r.token, '대상 서버에서 이 명령을 실행하면 Node Agent 가 설치·등록됩니다. 연결·신원·용량 확인 뒤 "승인"을 누르세요.'); },
|
||||
|
||||
Reference in New Issue
Block a user