대시보드 개발 스펙 (Phase 1~6 지시서) - #9
Draft
eondcom wants to merge 7 commits into
Draft
Conversation
앱을 열면 서버 헬스·도메인별 헬스·등록 현황을 한 화면에서 보고 이상을 감지한다.
핵심 설계 판단
1) 2층 구조 — 대시보드는 열릴 때 SSH 를 쏘지 않는다.
SSH 한 번이 수 초, 여러 개면 수십 초다. 첫 화면에서 그러면 앱이 멈춘 것처럼
보이고 VPN 미연결·서버다운에서는 매번 타임아웃을 기다린다.
즉시층(로컬 scan_cache + 마지막 스냅샷 캐시) / 서버층(버튼 누를 때만)
2) 서버 조회는 스크립트 1개 = SSH 1회. 지표별로 나누면 N배 느려진다
3) 결과는 scan_cache 처럼 디스크에 캐시 — 재시작해도 "어제 09:12 기준" 을 보여준다
4) 시작 화면 설정(start_view, 기본 dashboard)
5) 결과 전달은 기존 HM_DB= 선례를 따라 마커 파싱. LogMsg 에 변체를 추가하지 않는다
도메인 헬스 판정 규칙 — 실측으로 잡은 함정
--resolve 로 서버 자신에게 물으면 vhost 매칭이 실패해도 기본 vhost 가 200 을 준다.
존재하지 않는 도메인이 정상으로 보였다. 그래서 로컬응답(LH)으로는 정상 판정을 하지
않고, DNS=ok 일 때 공개응답(PH)을 주 지표로 쓴다. DNS=other/none 은 그 자체가 이상이며
그때 PH 는 남의 서버 응답이라 판정에 쓰지 않는다. 웹루트 존재도 함께 본다.
인증서는 파일에서 읽어 네트워크 없이 D-day 를 구한다.
검증 — 셸 2개를 실제로 실행하며 결함 4건을 잡아 고쳤다
· /sys/firmware/efi/efivars 가 디스크 최대사용률에 섞여 오탐 → /dev/* 만 집계
· 남은 용량이 KB 숫자로 표시 → df -hP
· 존재하지 않는 도메인이 LH=200 으로 정상 판정 → 판정 규칙 재설계 + 웹루트 확인 추가
· 이상 2건인데 집계 0 → issue_of() 단일 기준으로 통일
· getent 폴백이 IPv6 반환 → IPv4 만 비교
임시파일은 mktemp 2개로 분리(root 실행이므로 예측 가능한 이름을 피한다)
문서에서 다시 추출해 실행까지 확인했다(옮기는 과정에서 깨지지 않음).
Phase 1~4 와 각 DoD, 재사용 자산 12개의 정확한 위치, 회귀 테스트
(DNS=none + LH=200 → 이상) 포함.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
Phase 1~4 구현물(PR #11)을 검증한 결과를 수정 지시서로 정리했다. 구현은 스펙을 충실히 따랐다. 자동 SSH 없음 · domain_issue 가 LH 를 아예 참조하지 않아 기본 vhost 오탐 차단 · 회귀 테스트 존재 · Done{ok:true} 에서만 커밋(실패 시 이전 캐시 유지) · 집계 기준 단일화 · need_update 가 기존 색상 판정과 동일 기준 · 셸 2개가 스펙과 주석 1줄 차이. 문서 §0 에 "건드리지 말 것" 으로 명시했다. 수정 항목 [필수] parse_kst_datetime 이 KST(+9) 를 하드코딩한다. 디스크 감시 last-run 은 서버 로컬 시각인데 앱이 KST 로 가정해, UTC 서버면 9시간 어긋나 27시간만 지나도 "하루 넘게 안 돌았다" 경고가 뜬다(해외 VPS 는 UTC 가 기본인 경우가 많다). → 서버가 date -d 로 epoch 를 계산해 HM_DASH_DISKMON_LAST_TS 로 넘기고, 앱은 그것을 우선 사용. 문자열 파싱은 폴백으로만 두고 이름도 정정. 값이 없을 때 경고를 띄우지 않는 경로까지 테스트로 못 박았다. [권장] local_stats 가 매 프레임 scan_cache 전체를 순회한다(대시보드가 기본 화면이라 항상 켜져 있다). 키 기반 캐시 제안. 단 무효화가 복잡해지면 값이 틀리는 쪽이 더 나쁘므로 건너뛰어도 된다고 명시. [권장] docs/dashboard.md 에 캐시 시각의 의미·자동조회를 안 하는 이유·타임존 주의·소요 시간 추가 [선택] 이상 도메인 행 클릭 → 계정 관리 이동 검증: 지시서의 셸 스니펫 bash -n 통과, date -d 변환과 빈 값 처리 실측 확인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
실제 운영에서 나온 요청 3건을 스펙으로 정리했다.
1) 숫자만으로는 좋은지 나쁜지 모른다 → A~E(우수~위험) 등급 + 안내
2) "디스크 추이를 봐라·/backup 파티션을 확인해라" 같은 조언을 앱이 직접 점검
3) 도메인 헬스 목록이 스크롤도 복사도 안 된다(97개 환경에서 사용 불가)
핵심 판단
· 총합 등급 = 항목 중 '최악'. 평균을 내면 서비스가 죽었는데 B 가 나온다 —
거짓 안심이라 이 기능의 목적을 정면으로 배반한다.
· 미조회를 좋은 등급으로 표시하지 않는다. 도메인 미점검 상태의 A 는 위험하다
(서버 리소스는 멀쩡한데 개별 사이트가 죽어 있던 실제 사례가 있다).
조회 안 한 항목은 등급에서 제외하고 '도메인 미점검' 을 병기한다.
· 자동 감시 항목은 총합을 C 보다 나쁘게 만들지 않는다 — 운영 준비 상태이지
서버 건강 문제가 아니다.
· 등급 계산은 캐시만 읽는다. 등급을 매기려고 SSH 를 새로 쏘지 않는다.
· 새 점검은 SERVER_SNAPSHOT_BODY 안에 추가한다(SSH 1회 유지).
· 권고 문구는 Rust 한 곳에만 둔다. 스크립트는 수치만 출력한다.
항목별 임계값은 근거를 함께 적었다(load/cores 1.0 = 코어 포화 시작점,
메모리 93% = 스왑·OOM 위험, 디스크 88% = 대응 시간 확보선, 도메인 이상 2% =
테스트 도메인 수준의 잡음 등).
추이 스크립트 검증 — 실제 실행으로 4케이스 확인
13일간 66→69% → 하루 0.23%p · 95% 도달 약 113일 후
기록 1행 → '데이터 부족'(빈 값) / 감소 추세 → 하루 -1.10%p, ETA 없음
이미 95% 초과 → ETA 없음
빈 값이 정상값이므로 unwrap_or(0) 금지를 함정으로 명시했다.
UI 는 기존 선례를 지정했다: ui.ctx().copy_text (3곳 사용 중),
ScrollArea + id_salt, Label::selectable. 복사는 TSV(엑셀 붙여넣기).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
운영 디스크와 백업 디스크 2개를 쓰는 환경이라 최대 사용률 하나로는 부족하다.
운영 60%·백업 95%면 "디스크 95%" 하나로는 어느 쪽이 위험한지 알 수 없고,
백업 디스크가 차면 백업이 실패한다는 사실이 드러나지 않는다.
추가한 것
· 디스크별 수집: 사용률·남은용량·inode·FS누적에러·SMART(상태/재할당/대기섹터/
가동시간/온도)·역할(운영/백업/기타). HM_DISK 마커 한 줄에 한 건(13필드).
· 디스크별 등급: 세부 중 최악 → 디스크 항목 등급 = 디스크들 중 최악.
SMART 조회불가(-)는 C 로 둔다. A 로 두면 smartmontools 미설치 서버가 영원히
'우수'가 되고, E 로 두면 조회가 원래 안 되는 환경에서 계속 빨간불이라 무시하게 된다.
· 백업 디스크 가중: D 이하거나 사용률 90%+ 면 권고에 반드시 표시.
백업이 실패하면 계정 삭제·복구 경로가 통째로 막힌다.
· UI: 디스크마다 한 줄, 등급색 점. '-' 는 빈칸이 아니라 '조회불가' 로 적는다
(빈칸은 정상으로 오해된다).
검증 — 2디스크 시나리오를 shim 으로 실제 실행
운영 /home 82% SMART PASSED → 정상 판정
백업 /backup 94% SMART FAILED! 재할당48 대기12 FS에러3 → 전부 감지
SMART 는 물리 디스크 단위라 파티션에서 lsblk -no PKNAME 으로 부모를 찾아 조회하고
같은 디스크를 두 번 묻지 않게 캐시했다. /boot·/efi 는 운영 지표가 아니라 제외.
문서에서 다시 추출해 재실행까지 확인.
한계 명시: 추이(95% 도달 예상)는 history.tsv 의 use% 를 쓰는데 그 값이 그날의
최대치 하나여서 디스크별로 나뉘지 않는다. 디스크별 추이는 감시 스크립트가
disk-usage.tsv 를 따로 남기도록 고쳐야 하며 별도 지시서로 다룬다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
직전 개정에서 "추이는 디스크별로 나뉘지 않는다, 별도 지시서로 다룬다" 고 남겼던 한계를 이번에 해소했다. 디스크별로 볼 수 있어야 한다는 요구에 맞춘다. 원천 추가 — 감시 스크립트가 디스크별로 매일 기록 /var/lib/hm-disk-monitor/disk-usage.tsv (날짜·마운트포인트·장치·사용률·남은용량) 하루 디스크당 1행, 같은 날 재실행이면 그날 줄을 갈아끼운다. 5000줄 초과 시 정리. 기존 history.tsv 는 건드리지 않는다(하위호환). 계산 추가 — HM_DISK 마커에 추이 3필드(13 → 16필드) 마운트포인트별로 disk-usage.tsv 를 읽어 하루당 %p 와 95% 도달 예상일을 낸다. 검증 — 디스크마다 다른 속도를 줘서 실제로 구분되는지 확인 /home 하루 0.21%p → 95% 도달 61일 후 /backup 하루 0.43%p → 95% 도달 2일 후 같은 서버인데 한쪽은 두 달 여유, 한쪽은 이틀 뒤 꽉 참. 최대값 하나로 뭉쳤다면 절대 드러나지 않을 정보다. 감시 기록 조각도 1일차 → 같은 날 재실행(중복 없음) → 누적까지 확인. 문서에서 3개 블록을 다시 추출해 재실행까지 검증했다. 전환기 처리: history.tsv 는 쌓였는데 disk-usage.tsv 는 이제 시작하는 시점이 있다. 둘 다 유지하고 표시는 디스크별을 우선, 없으면 전체 추이로 폴백한다. 감시를 다시 설치해야 적용되고 2일이 지나야 추이가 나오므로, 그전에는 '추이 데이터 쌓이는 중' 으로 표시하도록 명시했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
배경에 '디스크가 2개'라고 적었으나 실제 대상 서버는 물리 디스크 3개다. / (nvme1n1, 14%) · /home (nvme0n1, 69%) · /backup (sda 3.6T, 7%). / 와 /home 이 서로 다른 NVMe 이고 /backup 은 별도 SATA 라, 최대값 하나로는 이 구조가 통째로 사라진다. 또 94% 백업 디스크 예시가 실제 수치로 오해될 수 있어, 그것이 로직 검증용 시나리오임을 명시하고 실제 서버를 같은 스크립트로 돌린 출력을 함께 실었다. tmpfs·efivarfs·/boot/efi 제외와 물리 디스크 매핑을 실측으로 확인했다. '디스크 개수를 가정하지 말 것' 을 덧붙였다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
1) dashboard_page 에 ScrollArea 가 하나도 없어 카드가 화면을 넘으면 잘린다. 등급 카드와 디스크 목록이 추가되면 더 심해지므로 6-C 를 '대시보드 UX 정리' 로 확장하고, 전체 스크롤을 먼저 넣도록 순서를 정했다. 안쪽 목록 스크롤과 중첩되므로 서로 다른 id_salt 를 요구했고, 좁은 창에서 3열이 깨지는 것도 available_width 기준 세로 배치로 처리하게 했다. 2) 이 서버는 인증서 자동 갱신이 돌고 있다. Let's Encrypt 는 90일 유효이고 만료 30일쯤 전에 갱신되므로 정상이면 D-day 가 30~90 을 오간다. 즉 D-14 이하는 '곧 만료' 가 아니라 '자동 갱신이 이미 실패했다' 는 뜻이다. 임박 1건만 있어도 C 로 올리고, 문구를 '자동 갱신이 실패하고 있을 수 있습니다' 로 바꿨다. '곧 만료' 라고만 쓰면 자동 갱신을 믿고 넘겨버린다. DNS=other 와 함께 뜨면 .well-known 검증 실패가 원인일 가능성이 높아 그 조합을 권고에 따로 넣었다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
앱을 열면 서버 헬스와 도메인별 헬스, 등록 현황을 한 화면에서 보고 이상을 감지하는 대시보드. 문서만, 코드 없음. 구현은 Codex 가 맡습니다.
📄
.claude/plans/2026-07-30-dashboard-SPEC.md핵심 설계 판단
1. 2층 구조 — 대시보드는 열릴 때 SSH 를 쏘지 않습니다. SSH 한 번이 수 초, 여러 개면 수십 초입니다. 첫 화면에서 그러면 앱이 멈춘 것처럼 보이고, VPN 미연결·서버 다운 상태에서는 매번 타임아웃을 기다리게 됩니다.
customers+scan_cache+ 마지막 스냅샷 캐시2. 서버 조회는 스크립트 1개 = SSH 1회. 지표별로 나누면 N배 느려집니다.
3. 결과는
scan_cache처럼 디스크 캐시 — 재시작해도 "어제 09:12 기준"을 보여줍니다.4. 시작 화면 설정 (
start_view, 기본dashboard).5. 결과 전달은 마커 파싱 — 계정 삭제의
HM_DB=선례를 따르고LogMsg에 변체를 추가하지 않습니다.도메인 헬스 — 실측으로 잡은 함정
--resolve로 서버 자신에게 물으면 vhost 매칭이 실패해도 기본 vhost 가200을 줍니다. 실제로 존재하지 않는 도메인이 정상으로 나왔습니다. 그래서 판정 규칙을 이렇게 확정했습니다:LH)으로는 정상 판정을 하지 않습니다 (참고값)DNS=ok일 때 공개 응답(PH) 이 주 지표 — 실제 방문자가 보는 것DNS=other/none은 그 자체가 이상이고, 그때PH는 남의 서버 응답이라 판정에 쓰지 않음감지 항목: HTTP 응답 · DNS A 레코드가 이 서버인지 · 인증서 만료 D-day · 웹루트 존재. 도메인은 동시 8개 병렬, 타임아웃 8초.
검증 — 셸 2개를 실제로 실행하며 결함 5건을 잡아 고쳤습니다
/sys/firmware/efi/efivars가 디스크 최대사용률에 섞여 오탐/dev/*만 집계df -hPLH=200으로 정상 판정issue_of()단일 기준으로 통일getent폴백이 IPv6 반환임시파일은
mktemp2개로 분리했습니다(root 실행이므로 예측 가능한 이름을 피함).문서에서 스크립트를 다시 추출해 실행까지 확인했습니다 — 옮기는 과정에서 깨지지 않았습니다.
스펙에 담은 것
재사용 자산 12개의 정확한 위치(
hydrate_scan_cache·ago_text·card등) · Phase 1~4 와 각 DoD · 검증된 셸 2개 전문 · 신규ServerSnapshot/DomainHealth구조체 · 화면 레이아웃 · 경고 배너 조건표 · 함정 7개 · 회귀 테스트(DNS=none + LH=200 → 이상).🤖 Generated with Claude Code
https://claude.ai/code/session_01WxEiSzFMdeAzvohAQ39nyJ