28 lines
3.5 KiB
JavaScript
28 lines
3.5 KiB
JavaScript
/* portal/dashboard/public/dashboard.js — 대시보드 요약 카드·주의 항목·최근 활동 렌더(30초마다 갱신). */
|
|||
|
|
(() => {
|
||
|
|
const el = document.getElementById('dash'); if (!el) return;
|
||
|
|
const ACT = { 'deploy.start': '배포 시작', 'deploy.done': '배포 완료', 'order.created': '주문 제출', 'order.approve': '주문 승인', 'member.invited': '구성원 초대', 'member.joined': '구성원 참여', 'member.role': '역할 변경', 'group.settings': '그룹 설정 변경', 'permission.site': '사이트 권한 변경', 'ownership.group_transferred': '그룹 소유권 이전', 'member.suspended': '구성원 정지' };
|
||
|
|
const fmtKrw = (n) => `${Number(n || 0).toLocaleString('ko-KR')}원`;
|
||
|
|
async function load() {
|
||
|
|
try {
|
||
|
|
const d = await aw.api('GET', '/api/dashboard'); const s = d.summary;
|
||
|
|
const card = (title, stat, sub) => `<div class="card"><h2>${aw.esc(title)}</h2><div class="stat">${stat}</div>${sub ? `<div class="sub">${sub}</div>` : ''}</div>`;
|
||
|
|
el.innerHTML = `<div class="cards">
|
||
|
|
${card('운영 사이트', `${s.sites.inUse} <span class="sub">/ 구매 ${s.sites.purchased}개</span>`, `사용 중 ${s.sites.inUse} · 예약됨 ${s.sites.reserved} · 미배정 ${s.sites.unallocated}`)}
|
||
|
|
${card('초안·제작 중', s.drafts, '운영 사이트 수를 사용하지 않습니다')}
|
||
|
|
${card('사이트 상태', `<span class="badge ok">● 정상 ${s.health.ok}</span> <span class="badge warning">! 경고 ${s.health.warn}</span> <span class="badge failed">✘ 장애 ${s.health.bad}</span>`)}
|
||
|
|
${card('배포 방식', `관리형 ${s.deploy.managed} · 외부 ${s.deploy.external}`)}
|
||
|
|
${card('도메인', `${s.domains.total}`, s.domains.action ? `<a href="/domains">조치 필요 ${s.domains.action}개</a>` : '조치 필요 없음')}
|
||
|
|
${card('메일', `서비스 ${s.mail.services} · 메일함 ${s.mail.mailboxes}`)}
|
||
|
|
${card('백업 옵션 사이트', s.backup)}
|
||
|
|
${card('진행 중 작업', s.jobs.length ? s.jobs.map((j) => `${aw.esc(j.label)} ${j.n}`).join(' · ') : '0')}
|
||
|
|
${card('이번 달 예상 비용', fmtKrw(s.cost.estimated), `결제 대기 ${fmtKrw(s.cost.awaiting)} · ${aw.esc(s.cost.note)}`)}
|
||
|
|
</div>
|
||
|
|
<div class="card"><h2>주의 항목</h2>${d.attention.length ? d.attention.map((a) => `<a class="alert ${a.severity}" href="${aw.esc(a.href)}">${a.severity === 'critical' ? '⚠' : '!'} ${aw.esc(a.text)}</a>`).join('') : '<div class="aw-state empty"><b>지금 조치할 항목이 없습니다</b></div>'}</div>
|
||
|
|
<div class="card"><h2>최근 활동</h2>${d.recent.length ? `<table class="r"><thead><tr><th>시각</th><th>내용</th><th>주체</th><th>결과</th></tr></thead><tbody>${d.recent.map((r) => `<tr><td data-l="시각">${aw.esc(aw.fmt(r.at))}</td><td data-l="내용">${aw.esc(ACT[r.action] || r.action)}${r.subjectType ? ` <span class="sub">${aw.esc(r.subjectType)} ${aw.esc(r.subjectId || '')}</span>` : ''}</td><td data-l="주체">${r.automatic ? '<span class="badge">⚙ 자동</span>' : `<span class="badge">👤 ${aw.esc(r.actor)}</span>`}</td><td data-l="결과">${r.outcome === 'ok' ? '<span class="badge ok">✔ 성공</span>' : `<span class="badge failed">✘ ${aw.esc(r.outcome)}</span>`}</td></tr>`).join('')}</tbody></table>` : '<div class="aw-state empty"><b>최근 활동이 없습니다</b></div>'}</div>
|
||
|
|
<p class="sub">마지막 갱신 ${aw.esc(aw.fmt(d.generatedAt))}</p>`;
|
||
|
|
} catch (err) { aw.state(el, err.status === 403 ? 'forbidden' : err.code === 'service_unreachable' ? 'stale' : 'failed', err.message, { retry: load }); }
|
||
|
|
}
|
||
|
|
load(); aw.poll(load, 30000);
|
||
|
|
})();
|