Files
AriWeb-Suite/portal/relocation/public/relocation.js
T

23 lines
5.7 KiB
JavaScript

/* portal/relocation/public/relocation.js — 서버 이전 화면(§21). 경계값·조건·진행·취소·되돌리기 */
(() => {
const { api, esc, state, toast, confirmDanger, poll, job, fmt } = window.aw;
const el = document.getElementById('rl-site'); if (!el) return; const siteId = el.dataset.site;
const ST = { awaiting_approval: ['waiting', '승인 대기'], queued: ['waiting', '처리 대기'], running: ['running', '진행 중'], paused: ['warning', '일시 중지'], cancelling: ['warning', '취소 중'], cancelled: ['neutral', '취소됨'], done: ['done', '완료'], failed: ['failed', '실패'], reverting: ['running', '되돌리는 중'], reverted: ['ok', '되돌림 완료'], rejected: ['failed', '거절됨'], needs_user: ['warning', '사용자 확인 필요'] };
const badge = (k) => { const [c, l] = ST[k] || ['neutral', k]; return `<span class="badge ${c}">${esc(l)}</span>`; };
const v = (x, u = '') => (x == null ? '<span class="muted">미확정(후속 상품정책)</span>' : esc(x) + u);
const bounds = (b) => `<dl class="kv"><dt>취소 가능 마지막 단계</dt><dd>${esc(b.lastCancelLabel)}</dd><dt>쓰기 제한</dt><dd>${b.writeFreeze ? `${esc(b.writeFreeze.startsAt)} ~ ${esc(b.writeFreeze.endsAt)} — ${esc(b.writeFreeze.note)}` : '없음(정적 사이트)'}</dd><dt>목표 중단 시간</dt><dd>${v(b.targetDowntimeMin, '분')}</dd><dt>새 위치가 유일한 운영 위치가 되는 시점</dt><dd>${esc(b.soleLocationAt)}</dd><dt>전환 후 되돌리기 기간</dt><dd>${v(b.revertHours, '시간')}</dd><dt>이전 위치 보존·삭제</dt><dd>${b.oldRetentionDays != null ? `전환 후 ${b.oldRetentionDays}일 보존 뒤 삭제` : v(null)}</dd><dt>DNS·인증서 반영 대기 중</dt><dd>${esc(b.dnsWaitHandling)}</dd><dt>비용</dt><dd>${b.chargeable ? v(b.cost) + ' ' + esc(b.currency) : '없음(플랫폼 사유)'}</dd></dl>`;
const render = async () => {
let d; try { d = await api('GET', `/api/relocation/sites/${siteId}`); } catch (e) { state(el, e.status === 403 ? 'forbidden' : e.code === 'hal_unavailable' ? 'stale' : 'failed', e.message, { retry: render }); return false; }
if (!d.managed) { state(el, 'empty', '외부 배포 사이트는 Ariweb 가 서버를 관리하지 않아 서버 이전 대상이 아닙니다'); return false; }
const open = d.relocations.find((r) => !['done', 'failed', 'cancelled', 'reverted', 'rejected'].includes(r.status));
el.innerHTML = `<div class="card"><h2>이전 안내</h2><dl class="kv"><dt>현재 위치</dt><dd>${esc(d.currentLocation)}</dd><dt>실행 형태</dt><dd>${d.runtime === 'static' ? '정적 사이트(조건이 맞으면 플랫폼이 자동 이전할 수 있습니다)' : '동적 사이트(운영자 승인과 작업 시간 확인 뒤 이전)'}</dd><dt>자동 이전 조건</dt><dd>${d.auto.ok ? '충족' : d.auto.reasons.map(esc).join('<br>')}</dd></dl><h3>고객 요청 이전(유료)</h3>${bounds(d.boundaries.customer)}<h3>플랫폼 사유 이전(장애 회피·용량 조정, 무료)</h3>${bounds(d.boundaries.platform)}<div class="actions">${open ? '' : '<button class="btn primary" id="rl-req">서버 이전 요청</button>'}</div></div>
${d.relocations.length ? d.relocations.map((r) => `<div class="card"><h2>${badge(r.status)} ${r.kind === 'customer_paid' ? '고객 요청' : r.kind === 'auto_static' ? '자동(정적)' : '승인형'} · 요청 ${esc(fmt(r.created_at))}</h2><p class="sub">이유: ${esc(r.reason || '')}${r.scheduled_at ? ` · 예정 ${esc(fmt(r.scheduled_at))}` : ''}${r.old_retained_until ? ` · 이전 위치 보존 종료 ${esc(fmt(r.old_retained_until))}` : ''}${r.charged ? ` · 비용 ${r.cost}` : ' · 과금 없음'}</p><div id="rl-job-${r.id}"></div><div class="actions">${['awaiting_approval', 'queued', 'running'].includes(r.status) ? `<button class="btn danger" data-cancel="${r.id}">취소</button>` : ''}${r.status === 'done' ? `<button class="btn danger" data-revert="${r.id}">되돌리기</button>` : ''}</div></div>`).join('') : ''}`;
for (const r of d.relocations) if (r.job) job(`#rl-job-${r.id}`, r.job, { onAction: async (a) => { try { if (a === 'cancel') await api('POST', `/api/relocation/${r.id}/cancel`); toast('처리했습니다'); render(); } catch (e) { toast(e.message); } } });
el.querySelectorAll('[data-cancel]').forEach((b) => (b.onclick = async () => { try { await api('POST', `/api/relocation/${b.dataset.cancel}/cancel`); toast('취소했습니다'); render(); } catch (e) { toast(e.message); } }));
el.querySelectorAll('[data-revert]').forEach((b) => (b.onclick = async () => { if (!(await confirmDanger({ title: '이전 위치로 되돌리기', target: '이 사이트', impacts: ['전환 뒤 새 위치에서 생긴 변경은 사라질 수 있습니다', '되돌리기 기간 안에서만 가능합니다'], confirmLabel: '되돌리기' }))) return; try { await api('POST', `/api/relocation/${b.dataset.revert}/revert`, { confirm: true }); toast('되돌리기를 시작했습니다'); render(); } catch (e) { toast(e.message); } }));
const req = document.getElementById('rl-req'); if (req) req.onclick = async () => { if (!(await confirmDanger({ title: '서버 이전 요청(유료)', target: '이 사이트', impacts: ['운영자 승인 뒤 안내된 시간에 진행됩니다', d.boundaries.customer.writeFreeze ? '최종 반영~전환 사이 쓰기가 제한됩니다' : '정적 사이트라 쓰기 제한은 없습니다', `비용: ${d.boundaries.customer.cost ?? '미확정'} ${d.boundaries.customer.currency}`], confirmLabel: '요청' }))) return; try { await api('POST', `/api/relocation/sites/${siteId}`, { reason: '고객 요청', confirm: true }); toast('이전을 요청했습니다'); render(); } catch (e) { toast(e.message); } };
return true;
};
poll(render, 6000);
})();