From 2feb5e9c2d80c03a678a6c35464d3542ef789ef4 Mon Sep 17 00:00:00 2001 From: phil Date: Wed, 12 Aug 2026 23:52:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(webui):=20=EA=B4=80=EC=A0=9C=20=EC=BD=98?= =?UTF-8?q?=EC=86=94=20=EC=9D=B4=EA=B4=80=20=E2=80=94=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=B4=20=EB=9C=AC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React 콘솔(web/, Vite)과 정적 서빙 계층을 옮기고 apiserver 에 다시 붙였다. ## 프로토타입의 함정을 구조로 없앴다 번들은 커밋하지 않고 index.html 셸만 커밋한다. 그래서 프론트를 빌드하지 않은 상태에서는 셸이 참조하는 해시 번들이 없어 테스트가 404 로 깨졌다 — 프로토타입 에서 make all 이 항상 실패하던 이유다. 게이트가 늘 빨간불이면 사람들이 게이트 자체를 신뢰하지 않게 된다. 이제 테스트가 번들 존재 여부를 먼저 보고 없으면 건너뛴다. make all 은 Go 만 검사하고, 프론트 검증은 make web-build 를 돌린 뒤에 실제로 수행된다. ## web-build 는 vite 에 맡긴다 vite 의 outDir 이 internal/webui/assets 라 산출물이 바로 embed 대상에 떨어진다. 복사 단계를 두었다가 emptyOutDir 와 순서가 엉켜 방금 만든 번들을 지우는 것을 한 번 겪었다 — 그래서 타깃은 pnpm build 한 줄이다. 실증: 빌드 후 webui 테스트 8/8(해시 자산 검증 포함), 프로세스 기동 시 / 가 앱 셸을 내고 /assets/index-*.js 가 200. --- .gitignore | 5 + Makefile | 12 +- internal/apiserver/server.go | 4 + internal/webui/assets/index.html | 13 + internal/webui/webui.go | 146 + internal/webui/webui_test.go | 393 ++ web/components.json | 20 + web/index.html | 12 + web/package.json | 35 + web/pnpm-lock.yaml | 3223 +++++++++++++++++ web/public/brand/lguplus.svg | 7 + web/public/csp/kakao.svg | 4 + web/public/csp/naver.svg | 4 + web/public/csp/nhn.svg | 6 + web/public/csp/onprem.svg | 6 + web/src/App.tsx | 87 + web/src/auth/AuthContext.tsx | 92 + web/src/components/gpu/ActiveMonitoring.tsx | 676 ++++ web/src/components/gpu/AlertTimeline.tsx | 111 + web/src/components/gpu/AssistantBubble.tsx | 24 + web/src/components/gpu/CollectDonut.tsx | 80 + web/src/components/gpu/ContactBubble.tsx | 167 + web/src/components/gpu/CspLogo.tsx | 60 + web/src/components/gpu/CspRollupCards.tsx | 85 + web/src/components/gpu/EventFeed.tsx | 161 + web/src/components/gpu/EventStream.tsx | 85 + web/src/components/gpu/FaultTypeBar.tsx | 97 + web/src/components/gpu/FleetFilterBar.tsx | 120 + web/src/components/gpu/FleetHeatmap.tsx | 222 ++ web/src/components/gpu/GateDiagram.tsx | 96 + web/src/components/gpu/GpuDeviceCard.tsx | 183 + web/src/components/gpu/GpuModelBar.tsx | 53 + web/src/components/gpu/HealthGauge.tsx | 110 + web/src/components/gpu/IdleDonut.tsx | 85 + web/src/components/gpu/IdleReuseDrawer.tsx | 349 ++ web/src/components/gpu/IncidentActionList.tsx | 97 + web/src/components/gpu/IncidentFlow.tsx | 216 ++ web/src/components/gpu/KpiTile.tsx | 158 + web/src/components/gpu/LiveStrip.tsx | 146 + web/src/components/gpu/ModelUtilChart.tsx | 291 ++ web/src/components/gpu/NodeDrillSheet.tsx | 507 +++ web/src/components/gpu/NodeGpuPanel.tsx | 522 +++ web/src/components/gpu/RemediationStepper.tsx | 170 + web/src/components/gpu/ReuseStepper.tsx | 77 + web/src/components/gpu/ScoringFunnel.tsx | 432 +++ web/src/components/gpu/SituationBoard.tsx | 279 ++ web/src/components/gpu/SloPanel.tsx | 101 + web/src/components/gpu/Sparkline.tsx | 88 + web/src/components/gpu/StatusBanner.tsx | 38 + web/src/components/gpu/TenantDonut.tsx | 106 + web/src/components/gpu/TopGpuTable.tsx | 324 ++ web/src/components/gpu/TopologyGraph.tsx | 577 +++ web/src/components/gpu/UtilLegend.tsx | 42 + web/src/components/gpu/VictimMetricChart.tsx | 119 + web/src/components/layout/HeaderExtras.tsx | 214 ++ web/src/components/layout/Shell.tsx | 407 +++ web/src/components/ui/alert.tsx | 66 + web/src/components/ui/badge.tsx | 48 + web/src/components/ui/button.tsx | 64 + web/src/components/ui/card.tsx | 92 + web/src/components/ui/collapsible.tsx | 31 + web/src/components/ui/command.tsx | 112 + web/src/components/ui/dialog.tsx | 156 + web/src/components/ui/hover-card.tsx | 42 + web/src/components/ui/input.tsx | 21 + web/src/components/ui/label.tsx | 24 + web/src/components/ui/popover.tsx | 40 + web/src/components/ui/progress.tsx | 31 + web/src/components/ui/radio-group.tsx | 43 + web/src/components/ui/select.tsx | 148 + web/src/components/ui/separator.tsx | 26 + web/src/components/ui/sheet.tsx | 143 + web/src/components/ui/sidebar.tsx | 726 ++++ web/src/components/ui/skeleton.tsx | 13 + web/src/components/ui/slider.tsx | 63 + web/src/components/ui/table.tsx | 114 + web/src/components/ui/tabs.tsx | 89 + web/src/components/ui/textarea.tsx | 18 + web/src/components/ui/tooltip.tsx | 55 + web/src/hooks/demoContext.ts | 28 + web/src/hooks/use-mobile.ts | 19 + web/src/hooks/useBuildInfo.ts | 54 + web/src/hooks/useDemoStatus.ts | 27 + web/src/hooks/useFleet.ts | 155 + web/src/hooks/useLiveSeries.ts | 79 + web/src/hooks/usePolledQuery.ts | 67 + web/src/hooks/useVictimSeries.ts | 72 + web/src/index.css | 182 + web/src/lib/api.ts | 154 + web/src/lib/branding.ts | 11 + web/src/lib/demoApi.ts | 731 ++++ web/src/lib/fleetFacts.ts | 288 ++ web/src/lib/gpuMetrics.ts | 153 + web/src/lib/navigation.ts | 270 ++ web/src/lib/nodeAsset.ts | 219 ++ web/src/lib/utils.ts | 6 + web/src/lib/validation.ts | 185 + web/src/main.tsx | 15 + web/src/pages/Explorer.tsx | 553 +++ web/src/pages/Login.tsx | 94 + web/src/pages/Map.tsx | 323 ++ web/src/pages/Overview.tsx | 283 ++ web/src/pages/gpu/GpuAssistant.tsx | 977 +++++ web/src/pages/gpu/GpuEfficiency.tsx | 545 +++ web/src/pages/gpu/GpuHealth.tsx | 568 +++ web/src/pages/gpu/GpuIdle.tsx | 648 ++++ web/src/pages/gpu/GpuIncident.tsx | 468 +++ web/src/pages/gpu/GpuLayout.tsx | 84 + web/src/pages/gpu/GpuOverview.tsx | 601 +++ web/src/pages/gpu/GpuRemediation.tsx | 647 ++++ web/src/pages/gpu/GpuRoadmap.tsx | 480 +++ web/src/pages/gpu/GpuRuleset.tsx | 355 ++ web/src/pages/gpu/GpuServing.tsx | 714 ++++ web/src/pages/gpu/GpuValidation.tsx | 1319 +++++++ web/tsconfig.json | 24 + web/tsconfig.node.json | 15 + web/tsconfig.tsbuildinfo | 1 + web/vite.config.ts | 27 + 118 files changed, 25349 insertions(+), 1 deletion(-) create mode 100644 internal/webui/assets/index.html create mode 100644 internal/webui/webui.go create mode 100644 internal/webui/webui_test.go create mode 100644 web/components.json create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/public/brand/lguplus.svg create mode 100644 web/public/csp/kakao.svg create mode 100644 web/public/csp/naver.svg create mode 100644 web/public/csp/nhn.svg create mode 100644 web/public/csp/onprem.svg create mode 100644 web/src/App.tsx create mode 100644 web/src/auth/AuthContext.tsx create mode 100644 web/src/components/gpu/ActiveMonitoring.tsx create mode 100644 web/src/components/gpu/AlertTimeline.tsx create mode 100644 web/src/components/gpu/AssistantBubble.tsx create mode 100644 web/src/components/gpu/CollectDonut.tsx create mode 100644 web/src/components/gpu/ContactBubble.tsx create mode 100644 web/src/components/gpu/CspLogo.tsx create mode 100644 web/src/components/gpu/CspRollupCards.tsx create mode 100644 web/src/components/gpu/EventFeed.tsx create mode 100644 web/src/components/gpu/EventStream.tsx create mode 100644 web/src/components/gpu/FaultTypeBar.tsx create mode 100644 web/src/components/gpu/FleetFilterBar.tsx create mode 100644 web/src/components/gpu/FleetHeatmap.tsx create mode 100644 web/src/components/gpu/GateDiagram.tsx create mode 100644 web/src/components/gpu/GpuDeviceCard.tsx create mode 100644 web/src/components/gpu/GpuModelBar.tsx create mode 100644 web/src/components/gpu/HealthGauge.tsx create mode 100644 web/src/components/gpu/IdleDonut.tsx create mode 100644 web/src/components/gpu/IdleReuseDrawer.tsx create mode 100644 web/src/components/gpu/IncidentActionList.tsx create mode 100644 web/src/components/gpu/IncidentFlow.tsx create mode 100644 web/src/components/gpu/KpiTile.tsx create mode 100644 web/src/components/gpu/LiveStrip.tsx create mode 100644 web/src/components/gpu/ModelUtilChart.tsx create mode 100644 web/src/components/gpu/NodeDrillSheet.tsx create mode 100644 web/src/components/gpu/NodeGpuPanel.tsx create mode 100644 web/src/components/gpu/RemediationStepper.tsx create mode 100644 web/src/components/gpu/ReuseStepper.tsx create mode 100644 web/src/components/gpu/ScoringFunnel.tsx create mode 100644 web/src/components/gpu/SituationBoard.tsx create mode 100644 web/src/components/gpu/SloPanel.tsx create mode 100644 web/src/components/gpu/Sparkline.tsx create mode 100644 web/src/components/gpu/StatusBanner.tsx create mode 100644 web/src/components/gpu/TenantDonut.tsx create mode 100644 web/src/components/gpu/TopGpuTable.tsx create mode 100644 web/src/components/gpu/TopologyGraph.tsx create mode 100644 web/src/components/gpu/UtilLegend.tsx create mode 100644 web/src/components/gpu/VictimMetricChart.tsx create mode 100644 web/src/components/layout/HeaderExtras.tsx create mode 100644 web/src/components/layout/Shell.tsx create mode 100644 web/src/components/ui/alert.tsx create mode 100644 web/src/components/ui/badge.tsx create mode 100644 web/src/components/ui/button.tsx create mode 100644 web/src/components/ui/card.tsx create mode 100644 web/src/components/ui/collapsible.tsx create mode 100644 web/src/components/ui/command.tsx create mode 100644 web/src/components/ui/dialog.tsx create mode 100644 web/src/components/ui/hover-card.tsx create mode 100644 web/src/components/ui/input.tsx create mode 100644 web/src/components/ui/label.tsx create mode 100644 web/src/components/ui/popover.tsx create mode 100644 web/src/components/ui/progress.tsx create mode 100644 web/src/components/ui/radio-group.tsx create mode 100644 web/src/components/ui/select.tsx create mode 100644 web/src/components/ui/separator.tsx create mode 100644 web/src/components/ui/sheet.tsx create mode 100644 web/src/components/ui/sidebar.tsx create mode 100644 web/src/components/ui/skeleton.tsx create mode 100644 web/src/components/ui/slider.tsx create mode 100644 web/src/components/ui/table.tsx create mode 100644 web/src/components/ui/tabs.tsx create mode 100644 web/src/components/ui/textarea.tsx create mode 100644 web/src/components/ui/tooltip.tsx create mode 100644 web/src/hooks/demoContext.ts create mode 100644 web/src/hooks/use-mobile.ts create mode 100644 web/src/hooks/useBuildInfo.ts create mode 100644 web/src/hooks/useDemoStatus.ts create mode 100644 web/src/hooks/useFleet.ts create mode 100644 web/src/hooks/useLiveSeries.ts create mode 100644 web/src/hooks/usePolledQuery.ts create mode 100644 web/src/hooks/useVictimSeries.ts create mode 100644 web/src/index.css create mode 100644 web/src/lib/api.ts create mode 100644 web/src/lib/branding.ts create mode 100644 web/src/lib/demoApi.ts create mode 100644 web/src/lib/fleetFacts.ts create mode 100644 web/src/lib/gpuMetrics.ts create mode 100644 web/src/lib/navigation.ts create mode 100644 web/src/lib/nodeAsset.ts create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/lib/validation.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/Explorer.tsx create mode 100644 web/src/pages/Login.tsx create mode 100644 web/src/pages/Map.tsx create mode 100644 web/src/pages/Overview.tsx create mode 100644 web/src/pages/gpu/GpuAssistant.tsx create mode 100644 web/src/pages/gpu/GpuEfficiency.tsx create mode 100644 web/src/pages/gpu/GpuHealth.tsx create mode 100644 web/src/pages/gpu/GpuIdle.tsx create mode 100644 web/src/pages/gpu/GpuIncident.tsx create mode 100644 web/src/pages/gpu/GpuLayout.tsx create mode 100644 web/src/pages/gpu/GpuOverview.tsx create mode 100644 web/src/pages/gpu/GpuRemediation.tsx create mode 100644 web/src/pages/gpu/GpuRoadmap.tsx create mode 100644 web/src/pages/gpu/GpuRuleset.tsx create mode 100644 web/src/pages/gpu/GpuServing.tsx create mode 100644 web/src/pages/gpu/GpuValidation.tsx create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/tsconfig.tsbuildinfo create mode 100644 web/vite.config.ts diff --git a/.gitignore b/.gitignore index 78c2659..04372e2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ cover.out *.test .DS_Store + +# 프론트 번들은 커밋하지 않는다 — index.html 셸만 두고 make web-build 가 채운다. +internal/webui/assets/* +!internal/webui/assets/.gitignore +!internal/webui/assets/index.html diff --git a/Makefile b/Makefile index be54e2e..2f1df6c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all fmt vet test build clean +.PHONY: all fmt vet test build clean web-build # 외부 의존성 0 이 이 제품의 전제다 — go.mod 에 require 가 늘면 "차트 하나 = # 파드 하나 = 완결" 이 성립하지 않는다. dep-check 가 그것을 지킨다. @@ -26,3 +26,13 @@ build: clean: rm -rf dist cover.out + +# 프론트를 빌드한다. vite 의 outDir 이 internal/webui/assets 라 산출물이 바로 +# embed 대상에 떨어진다 — 복사 단계를 두면 emptyOutDir 와 순서가 엉켜 방금 +# 만든 번들을 지운다(실제로 그렇게 한 번 깨뜨렸다). +# +# 번들은 커밋하지 않고 index.html 셸만 커밋한다. 빌드 전에는 셸이 참조하는 +# 해시 번들이 없으므로 Go 테스트가 그 상태를 인식해 관련 검사를 건너뛴다 — +# 그래야 프론트를 빌드하지 않은 개발자의 `make all` 이 실패하지 않는다. +web-build: + cd web && pnpm install --frozen-lockfile && pnpm build diff --git a/internal/apiserver/server.go b/internal/apiserver/server.go index 9abe4cd..bc4ea3d 100644 --- a/internal/apiserver/server.go +++ b/internal/apiserver/server.go @@ -30,6 +30,7 @@ import ( "github.com/KeiaiLab/nodevitals-observatory/internal/rules" "github.com/KeiaiLab/nodevitals-observatory/internal/storage" "github.com/KeiaiLab/nodevitals-observatory/internal/tsdb" + "github.com/KeiaiLab/nodevitals-observatory/internal/webui" ) // queryWindow 는 /api/v1/query 의 평가창 폭이다 — [eval-queryWindow, eval]. @@ -75,6 +76,9 @@ func NewServer(d Deps) http.Handler { mux.Handle("POST /api/v1/auth/login", d.Auth.LoginHandler()) mux.Handle("POST /api/v1/auth/logout", d.Auth.LogoutHandler()) mux.HandleFunc("GET /api/", handleUnknownAPI) + // 콘솔 정적 자산. 데이터가 0 이라 인증 예외이고, 위 구체 패턴들이 + // ServeMux 우선순위로 먼저 매치되므로 이 catch-all 이 API 를 삼키지 않는다. + mux.Handle("GET /", webui.Handler()) // 보호 — 질의 q := storage.New(d.DB) diff --git a/internal/webui/assets/index.html b/internal/webui/assets/index.html new file mode 100644 index 0000000..ffe206e --- /dev/null +++ b/internal/webui/assets/index.html @@ -0,0 +1,13 @@ + + + + + + NodeVitals Observatory + + + + +
+ + diff --git a/internal/webui/webui.go b/internal/webui/webui.go new file mode 100644 index 0000000..ea74b32 --- /dev/null +++ b/internal/webui/webui.go @@ -0,0 +1,146 @@ +// Package webui 는 nodevitals-observatory 관제 콘솔의 빌드 산출 정적 자산을 +// embed.FS 로 담아 서빙한다. 데이터는 전부 인증 뒤 /api/v1/* 가 낸다 — 이 +// 패키지는 마크업·번들만 서빙한다. M5: React 콘솔(web/, Vite 빌드)이 +// 클라이언트 라우팅(React Router)을 쓰므로, `/assets/*` 는 정적 자산 전용으로 +// 서빙하고 그 외 비-API 경로는 index.html 로 폴백한다(m5-design.md §2 D2) — +// 새로고침·직접 URL 진입(/overview, /explorer 등)에서도 클라이언트 라우터가 +// 문서를 받도록 보장한다. +package webui + +import ( + "embed" + "io/fs" + "net/http" + "strings" +) + +//go:embed assets +var assets embed.FS + +// FS 는 assets 디렉터리를 루트로 한 서브트리를 반환한다(테스트·외부 소비용). +// go:embed 는 빌드 시점에 고정되므로 fs.Sub 실패는 임베드 자체가 깨졌다는 +// 뜻이라 panic 으로 즉시 드러낸다 — 런타임에 조용히 넘어가면 안 되는 불변식. +func FS() fs.FS { + sub, err := fs.Sub(assets, "assets") + if err != nil { + panic("webui: assets 서브트리 구성 실패: " + err.Error()) + } + return sub +} + +// securityHeaders 는 콘솔 응답에 심층방어 헤더를 붙인다. CSP 로 self 외 +// 스크립트 실행을 원천 차단하고(외부 CDN 0 과 정합) 클릭재킹·MIME 스니핑을 +// 막는다(m4-design.md 보안 적대검토 F-SEC-6 반영). +// +// style-src 만 'unsafe-inline' 으로 완화한다 — m5-design.md D3 이 예고한 +// "인라인 style 주입이 실제로 필요해지는 시점"이 왔다: Radix 의 스크롤 락 +// (react-remove-scroll)이