서버 한 대(control) 운영의 web·backup 역할 대체, 초안 사이트 백업 중지 사유 구분, 알려진 제한 갱신

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-26 14:27:24 +00:00
co-authored by Claude Fable 5.1
parent 65f8102b5f
commit 3951be5836
6 changed files with 205 additions and 187 deletions
+4 -3
View File
@@ -99,7 +99,7 @@ export function resumeForSite(deps, siteId, { by = 'system', userConfirmed = fal
deps.store.run('UPDATE backup_options SET status=\'active\', paused_reason=NULL, next_run_at=?, updated_at=? WHERE id=?', nowIso(), nowIso(), o.id);
return optionById(deps, o.id);
}
export function reasonLabel(r) { return { site_paused: '사이트 일시 중지', site_cancelled: '사이트 해지', storage_full: '저장 공간 부족', operator: '운영자 조치', option_ended: '옵션 종료' }[r] || r; }
export function reasonLabel(r) { return { site_paused: '사이트 일시 중지', site_not_deployed: '사이트 미배포(배포 뒤 다시 시작)', site_cancelled: '사이트 해지', storage_full: '저장 공간 부족', operator: '운영자 조치', option_ended: '옵션 종료' }[r] || r; }
/** 옵션 종료(만료·해지): 새 백업 중지, 백업은 7일 보관 뒤 삭제(§20.3·§23.2). 권리 반환. */
export function expireOption(deps, optionId, { by = 'system', reason = 'option_ended' } = {}) {
const o = optionById(deps, optionId); if (!o || o.status === 'ended') return o;
@@ -115,7 +115,8 @@ export function expireOption(deps, optionId, { by = 'system', reason = 'option_e
export async function pickStoreServer(deps, site, option) {
const hal = halOf(deps); let servers = [];
try { servers = await hal.listServers({}); } catch (e) { throw asTransient(e); }
const ok = servers.filter((s) => s.acceptsNew && s.status === 'ready' && ['backup', 'web'].includes(s.role));
// [실제 환경 결함 수정] 서버 한 대(control) 운영: 전용 backup·web 서버가 없으면 control 서버도 저장 후보로 본다(같은 서버 저장은 여전히 운영자 override 가 필요).
const pool = servers.filter((s) => s.acceptsNew && s.status === 'ready'); const ok = pool.some((s) => ['backup', 'web'].includes(s.role)) ? pool.filter((s) => ['backup', 'web'].includes(s.role)) : pool.filter((s) => s.role === 'control');
const pref = ok.filter((s) => s.id !== site.server_id).sort((a, b) => (a.role === 'backup' ? -1 : 1) - (b.role === 'backup' ? -1 : 1) || ((b.capacity?.diskFreeBytes || 0) - (a.capacity?.diskFreeBytes || 0)));
if (pref.length) return pref[0].id;
if (option.same_server_override && ok.some((s) => s.id === site.server_id)) return site.server_id;
@@ -135,7 +136,7 @@ export function runDue(deps, now = nowIso()) {
const started = [];
for (const o of deps.store.all('SELECT * FROM backup_options WHERE status=\'active\' AND next_run_at IS NOT NULL AND next_run_at<=?', now)) {
const site = deps.store.get('SELECT * FROM sites WHERE id=?', o.site_id);
if (!site || !['live', 'needs_domain', 'deploying'].includes(site.lifecycle)) { stopForSite(deps, o.site_id, site && site.lifecycle === 'paused' ? 'site_paused' : 'site_cancelled'); continue; }
if (!site || !['live', 'needs_domain', 'deploying'].includes(site.lifecycle)) { stopForSite(deps, o.site_id, site && site.lifecycle === 'paused' ? 'site_paused' : site && site.lifecycle === 'draft' ? 'site_not_deployed' : 'site_cancelled'); continue; } // 초안(미배포) 사이트는 '해지' 가 아니라 '미배포' 로 표시한다(실제 환경 확인)
const slot = kstDate(now) + ':' + String(o.next_run_at);
const { job } = deps.jobs.enqueue({ kind: 'backup.scheduled', subject: { optionId: o.id, siteId: o.site_id, groupId: o.group_id, slot }, idempotencyKey: `backup:${o.id}:${slot}`, actorType: 'system', groupId: o.group_id, subjectType: 'site', subjectId: o.site_id, label: `정기 백업 (${site.name})` });
deps.store.run('UPDATE backup_options SET next_run_at=?, updated_at=? WHERE id=?', nextRunFrom(parseJson(o.schedule_json, {}), now), now, o.id);