- 개요 = 숫자 카드 넷 + CPU · 메모리 · 디스크 게이지(70 % 주황 · 90 % 빨강) + 7 일 SVG 그래프 - 회사 = 목록(찾기 · ⋯) → 상세(정보 · 사람 · 프로젝트 탭) · 사용자 = 회사 · 역할 · 찾기 거르기 · ⋯ - 설계 기본값 = 설정 카드를 묶음 제목 + 줄로 · 시스템 로그 = 동작 · 기간 · 찾기 거르기 + 이전 기록 더 보기 - 내 정보 = Linear 꼴 설정 줄(기본정보 · 비밀번호 · 테마 · 내 설계 기본값) - 옛 구역(리소스 패널 · 옛 내 정보 폼) 지움 · 지킴 시험 test_549 추가 · test_546 stub 맞춤 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
/* =============================================================================
|
|
* B01_Dashboard_UI_View_SysOverview.ts
|
|
* 시스템 관리 › 개요(549-2) — 숫자 카드(회사 · 사용자 · 활성 프로젝트 · 활성 사용자) +
|
|
* CPU · 메모리 · 디스크 게이지(넘으면 주황 · 빨강) + 7 일 그래프(인라인 SVG).
|
|
* 자료 = 리소스 API(7 일) · 회사 목록 · 전체 사용자 — 시스템 관리자만(서버도 막음).
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
fetchAllCompanies,
|
|
fetchAllUsers,
|
|
fetchSystemResources,
|
|
type ResourceData,
|
|
} from "./B01_Dashboard_Api_Fetch";
|
|
import { pageFrame } from "./B01_Dashboard_UI_PageKit";
|
|
import { el as h, loadPage } from "./B01_Dashboard_UI_View_Parts";
|
|
import type { ViewCtx } from "./B01_Dashboard_UI_Views";
|
|
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
const DAY_MS = 86_400_000;
|
|
/** 게이지 색 바뀌는 선 — 이 % 이상 주황 · 빨강 */
|
|
export const GAUGE_WARN = 70;
|
|
export const GAUGE_BAD = 90;
|
|
|
|
export const gaugeTone = (percent: number | null | undefined): "ok" | "warn" | "bad" =>
|
|
percent == null || percent < GAUGE_WARN ? "ok" : percent < GAUGE_BAD ? "warn" : "bad";
|
|
|
|
function statCard(label: string, value: number | string): HTMLElement {
|
|
const card = h("div", "b01-pg-card b01-sys-stat");
|
|
card.append(
|
|
h("span", "b01-sys-stat__label", label),
|
|
h("span", "b01-sys-stat__value", String(value)),
|
|
);
|
|
return card;
|
|
}
|
|
|
|
function gauge(label: string, percent: number | null | undefined): HTMLElement {
|
|
const card = h("div", `b01-pg-card b01-sys-gauge is-${gaugeTone(percent)}`);
|
|
const top = h("div", "b01-sys-gauge__top");
|
|
top.append(
|
|
h("span", "b01-sys-stat__label", label),
|
|
h("span", "b01-sys-stat__value", percent == null ? "-" : `${Math.round(percent)}%`),
|
|
);
|
|
const bar = h("div", "b01-sys-gauge__bar");
|
|
bar.setAttribute("role", "progressbar");
|
|
bar.setAttribute("aria-valuenow", String(Math.round(percent ?? 0)));
|
|
const fill = h("div", "b01-sys-gauge__fill");
|
|
fill.style.width = `${Math.min(100, Math.max(0, percent ?? 0))}%`;
|
|
bar.append(fill);
|
|
card.append(top, bar);
|
|
return card;
|
|
}
|
|
|
|
const svg = (tag: string, attrs: Record<string, string | number>): SVGElement => {
|
|
const e = document.createElementNS(SVG_NS, tag);
|
|
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
|
|
return e;
|
|
};
|
|
|
|
const SERIES = [
|
|
{ name: "CPU", key: "cpu_usage_percent", color: "var(--b01-s1)" },
|
|
{ name: "메모리", key: "memory_usage_percent", color: "var(--b01-s2)" },
|
|
{ name: "디스크", key: "disk_usage_percent", color: "var(--b01-s3)" },
|
|
] as const;
|
|
|
|
/** 7 일 그래프 — 가로 = 시각 · 세로 0~100 % · 세 시간 넘게 비면 선을 끊음 */
|
|
export function buildWeekChart(data: ResourceData | null, now = Date.now()): HTMLElement {
|
|
const W = 720;
|
|
const H = 180;
|
|
const pad = { l: 28, r: 8, t: 8, b: 20 };
|
|
const start = now - 7 * DAY_MS;
|
|
const x = (t: number): number => pad.l + ((t - start) / (now - start)) * (W - pad.l - pad.r);
|
|
const y = (v: number): number => pad.t + (1 - v / 100) * (H - pad.t - pad.b);
|
|
|
|
const box = h("div", "b01-pg-card b01-sys-chart");
|
|
const legend = h("div", "b01-sys-chart__legend");
|
|
for (const s of SERIES) {
|
|
const item = h("span", "", s.name);
|
|
const dot = h("i");
|
|
dot.style.background = s.color;
|
|
item.prepend(dot);
|
|
legend.append(item);
|
|
}
|
|
const chart = svg("svg", { viewBox: `0 0 ${W} ${H}`, role: "img" });
|
|
chart.setAttribute("aria-label", "최근 7 일 CPU · 메모리 · 디스크 사용률");
|
|
for (const v of [0, 50, 100]) {
|
|
chart.append(svg("line", { x1: pad.l, x2: W - pad.r, y1: y(v), y2: y(v) }));
|
|
const label = svg("text", { x: pad.l - 4, y: y(v) + 3, "text-anchor": "end" });
|
|
label.textContent = `${v}`;
|
|
chart.append(label);
|
|
}
|
|
// 날짜 눈금 — 자정마다
|
|
const midnight = new Date(start);
|
|
midnight.setHours(24, 0, 0, 0);
|
|
for (let t = midnight.getTime(); t <= now; t += DAY_MS) {
|
|
const d = new Date(t);
|
|
const label = svg("text", { x: x(t), y: H - 6, "text-anchor": "middle" });
|
|
label.textContent = `${d.getMonth() + 1}/${d.getDate()}`;
|
|
chart.append(label);
|
|
}
|
|
const points = (data?.history ?? [])
|
|
.filter((p) => p.timestamp)
|
|
.map((p) => ({
|
|
t: new Date(p.timestamp.endsWith("Z") ? p.timestamp : `${p.timestamp}Z`).getTime(),
|
|
p,
|
|
}))
|
|
.filter(({ t }) => t >= start && t <= now)
|
|
.sort((a, b) => a.t - b.t);
|
|
for (const s of SERIES) {
|
|
let d = "";
|
|
let prev = -Infinity;
|
|
for (const { t, p } of points) {
|
|
const v = p[s.key];
|
|
if (v == null) continue;
|
|
d += `${t - prev > 3 * 3_600_000 ? "M" : "L"}${x(t).toFixed(1)} ${y(v).toFixed(1)} `;
|
|
prev = t;
|
|
chart.append(
|
|
svg("circle", { cx: x(t).toFixed(1), cy: y(v).toFixed(1), r: 1.5, fill: s.color }),
|
|
);
|
|
}
|
|
chart.append(svg("path", { d, fill: "none", stroke: s.color, "stroke-width": 1.5 }));
|
|
}
|
|
box.append(legend, chart);
|
|
if (!points.length) box.append(h("p", "b01-pg-note", "7 일 안에 쌓인 기록이 없습니다."));
|
|
return box;
|
|
}
|
|
|
|
export function mountSysOverview(host: HTMLElement, _ctx: ViewCtx): void {
|
|
loadPage(host, async (stage) => {
|
|
const [res, companies, users] = await Promise.all([
|
|
fetchSystemResources(7),
|
|
fetchAllCompanies().catch(() => []),
|
|
fetchAllUsers(null).catch(() => []),
|
|
]);
|
|
const now = res.current;
|
|
const stats = h("div", "b01-sys-stats");
|
|
stats.append(
|
|
statCard("회사", companies.length),
|
|
statCard("사용자", users.length),
|
|
statCard("활성 프로젝트", now?.active_project_count ?? 0),
|
|
statCard("활성 사용자", now?.active_user_count ?? 0),
|
|
);
|
|
const gauges = h("div", "b01-sys-stats");
|
|
gauges.append(
|
|
gauge("CPU", now?.cpu_usage_percent),
|
|
gauge("메모리", now?.memory_usage_percent),
|
|
gauge("디스크", now?.disk_usage_percent),
|
|
);
|
|
const page = pageFrame(
|
|
stage,
|
|
"개요",
|
|
"서버 상태와 회사 · 사용자 · 프로젝트 수를 한눈에 봅니다.",
|
|
);
|
|
page.body.append(stats, gauges, buildWeekChart(res));
|
|
});
|
|
}
|