Skip to content

feat(dashboard): M8 admin control-plane UI — read-only live dashboard (scale #2 GATE3)#56

Merged
pureliture merged 4 commits into
mainfrom
claude/m8-admin-ui
Jun 21, 2026
Merged

feat(dashboard): M8 admin control-plane UI — read-only live dashboard (scale #2 GATE3)#56
pureliture merged 4 commits into
mainfrom
claude/m8-admin-ui

Conversation

@pureliture

Copy link
Copy Markdown
Contributor

M8 — 관리자 컨트롤플레인 UI (scale #2 / GATE3)

read API(M7, #49) 위에서 운영자가 500-repo 스캔 현황을 한 화면에서 보는 read-only 라이브 관리자 대시보드. 정적 HTML+vanilla JS thin client(빌드툴·node·새 런타임 의존성 0), loopback HTTP 서빙.

원래 silent-staleness 사고(daily scan이 죽었는데 publish가 계속 성공 → 갱신 안 된 DB 재발행, 아무도 못 알아챔)의 구조적 수정: 대시보드가 스토어를 라이브 read하므로 staleness/breach가 항상 1급 시각 신호로 렌더링되어 "침묵이 구조적으로 불가능".

무엇 (아키텍처 — 경계 분리)

  • runtime/dashboard.pyThreadingWSGIServer + 정적 서빙. 가드: null-byte 400(resolve 전), resolve()+is_relative_to traversal 403, ext allowlist, X-Content-Type-Options: nosniff, index.html no-store. SIGTERM/SIGINT 별도 스레드 graceful shutdown.
  • CLI security-scanner dashboard — loopback bind validate(비-loopback은 require_auth 없이 거부 → exit 2), store_from_args 재사용, --host/--port(기본 127.0.0.1:8787).
  • read_api.py GET /findingsapp() 본문 분기, QUERY_STRING disposition 멀티필터, 무효값 사전검증 400, 서버 정렬 권위(order_findings_for_dashboard), redacted(secretHash만, raw secret 0).
  • ui/staticindex.html/app.js/logic.mjs/style.css. 4패널(헬스 rollup·커버리지 org N/M·큐 백로그·findings disposition 필터), 6상태(available=false→"미평가", 0 아님), dark WCAG AA 토큰 + never-color-alone, aria-live 티어, 폴링 5s + timeout(AbortController) + 지수 backoff + keep-last-good + STALE 배너, prefers-reduced-motion.

검증

  • uv run pytest1113 passed, 4 skipped(env-gated live smoke/ddb), 회귀 0
  • node --test (logic.mjs 순수 로직) → 43/43
  • uv run ruff check (M8 파일) → clean
  • 보안: traversal/null-byte 거부, payload raw secret 0(테스트 단언), GitHub 쓰기 경로 0
  • FR-A1..A12 → 코드/테스트 매핑(TRACE.md, tests/test_fr_trace.py)

범위

  • v1: 4패널 + 알림 표시만(dispatch는 M9 sink 소유)
  • v2 이월: per-repo freshness 드릴다운(read API 함수 추가 필요), routable 노출+authn(현재 ssh -L로 충분), wheel force-include(dev는 source 트리)
  • 트리 전역 lint 부채는 이 PR 범위 밖(별도 lint PR) — autopilot이 만든 무관 ~50파일 churn은 격리·revert함

출처

grill-to-spec(requirements.mddesign.md rev2, 멀티에이전트 리뷰 18건+gap 4건 반영) → 오토파일럿 빌드(M1–M6) → UI 파인튜닝. 비주얼: 로컬 security-scanner dashboard(또는 in-memory 프리뷰)로 확인.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hXHR6Uj9nGZq84ksfP8Lt

scale #2 M8/GATE3. read API(M7, PR #49) 위 thin client 운영자 대시보드.
정적 HTML+vanilla JS(빌드·node·새 dep 0), loopback HTTP 서빙. staleness가 항상
1급 신호로 렌더 → silent-staleness 사고의 구조적 수정("침묵 불가능").

- runtime/dashboard.py: ThreadingWSGIServer + 정적 서빙(null-byte/traversal 가드,
  resolve+is_relative_to, ext allowlist, nosniff, index no-store), SIGTERM/SIGINT
  별도 스레드 graceful shutdown.
- cli `dashboard`: loopback bind validate(비-loopback은 require_auth 없이 거부, exit 2),
  store_from_args 재사용, --host/--port.
- read_api.py: GET /findings 라우트(app() 본문 분기, QUERY_STRING disposition 멀티필터,
  무효 400, 서버 정렬 권위 order_findings_for_dashboard, secretHash만/raw secret 0).
- ui/static: index.html/app.js/logic.mjs/style.css — 4패널(헬스 rollup/커버리지 org N·M/
  큐 백로그/findings disposition 필터), 6상태(미평가≠0), dark WCAG AA 토큰+never-color-alone,
  aria-live 티어, 폴링 5s+timeout+backoff+keep-last-good, prefers-reduced-motion.
- 테스트: dashboard server/static/a11y-static, /findings route, FR-A1..A12 trace(TRACE.md),
  node --test 순수 로직(logic.mjs).

게이트: uv run pytest 1113 passed/4 skipped(env-gated smoke), node --test 43/43, ruff clean.
제품/설계 출처: grill-to-spec requirements.md/design.md(rev2, 멀티에이전트 리뷰 반영).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hXHR6Uj9nGZq84ksfP8Lt
@pureliture pureliture added enhancement New feature or request 프로젝트 개선 제안 코드, 아키텍처, 문서, 테스트, 자동화 개선 제안 labels Jun 21, 2026

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the live admin control-plane UI (M8) for the security-scanner, introducing a loopback WSGI dashboard server, a static thin client, and comprehensive test suites including static accessibility checks and requirement tracing. The review feedback highlights a critical thread-safety concern when sharing the store instance across concurrent threads, a polling bug where failing requests trigger exponential backoff without clearing the active interval (causing overlapping requests), and opportunities to improve error state handling in the findings panel logic and UI.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

"""
validate_read_api_server_config(config)

app = make_dashboard_app(store, static_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

🧵 Thread-Safety Issue: 공유 store 인스턴스 사용 위험

ThreadingWSGIServer는 여러 스레드에서 동시에 요청을 처리합니다. 하지만 단일 store 인스턴스가 모든 요청 스레드 간에 공유되고 있습니다.

만약 store 내부에서 스레드 안전하지 않은 boto3 Table 리소스 등을 사용하고 있다면, 동시 요청 발생 시 세션/커넥션 충돌로 인해 예외가 발생하거나 데이터가 오염될 수 있습니다.

해결 방안:

  • 각 요청 스레드별로 독립된 store 인스턴스(또는 boto3 세션 및 테이블)를 생성하여 사용하도록 make_dashboard_app 내부 구조를 개선하거나,
  • 동시성 요구량이 크지 않다면 단일 스레드 기반의 동기식 서버 사용을 검토해 주세요.
References
  1. Do not share boto3 Table resource instances across multiple threads because they are not thread-safe. For concurrent operations, use a low-level DynamoDB client, create a new boto3 Session and Table instance per thread, or fall back to serial execution.

Comment on lines +359 to +385
async function fetchSnapshot() {
if (state.pollInFlight) {
return; // re-entrancy guard (no overlapping polls)
}
state.pollInFlight = true;
setRefreshDisabled(true);
try {
const snapshot = await fetchWithTimeout("/snapshot", FETCH_TIMEOUT_MS);
state.lastGoodSnapshot = snapshot;
state.failures = 0;
state.stale = false;
applySnapshot(snapshot);
renderStaleBanner(false);
} catch (_err) {
state.failures += 1;
state.stale = true;
// keep-last-good: re-render the last successful snapshot, mark stale.
if (state.lastGoodSnapshot) {
applySnapshot(state.lastGoodSnapshot);
}
renderStaleBanner(true);
scheduleBackoff();
} finally {
state.pollInFlight = false;
setRefreshDisabled(false);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

🔄 Bug: 폴링 실패 시 지수 백오프(Exponential Backoff)가 무시되고 요청 폭풍이 발생하는 문제

현재 구조에서는 fetchSnapshot이 실패할 경우 scheduleBackoff()를 통해 재시도 setTimeout을 예약합니다. 하지만 startPolling()에서 생성된 5초 주기의 setInterval이 해제되지 않고 계속 실행됩니다.

이로 인해 서버가 다운되었을 때 정기 폴링(5초 주기)과 백오프 재시도 타이머가 동시에 작동하여 서버에 더 많은 요청을 보내는 '요청 폭풍(Request Storm)' 현상이 발생하며, 지수 백오프 메커니즘이 완전히 무력화됩니다.

해결 방안:
에러 발생 시 기존 setInterval을 중지(clearInterval)하고, 성공적으로 데이터를 복구했을 때 다시 정기 폴링을 시작하도록 개선해야 합니다.

async function fetchSnapshot() {
  if (state.pollInFlight) {
    return; // re-entrancy guard (no overlapping polls)
  }
  state.pollInFlight = true;
  setRefreshDisabled(true);
  try {
    const snapshot = await fetchWithTimeout("/snapshot", FETCH_TIMEOUT_MS);
    state.lastGoodSnapshot = snapshot;
    if (state.failures > 0) {
      state.failures = 0;
      startPolling();
    }
    state.failures = 0;
    state.stale = false;
    applySnapshot(snapshot);
    renderStaleBanner(false);
  } catch (_err) {
    state.failures += 1;
    state.stale = true;
    if (state.intervalId !== null) {
      clearInterval(state.intervalId);
      state.intervalId = null;
    }
    if (state.lastGoodSnapshot) {
      applySnapshot(state.lastGoodSnapshot);
    }
    renderStaleBanner(true);
    scheduleBackoff();
  } finally {
    state.pollInFlight = false;
    setRefreshDisabled(false);
  }
}

Comment on lines +300 to +302
if (!hasFindings(resp)) {
return { fields: [], severity: "muted", state: "loading", rows: [] };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

⚠️ Improvement: Findings 패널의 에러 상태 처리 누락

현재 findingsPanelModelloading, empty, ok 상태만 반환할 수 있으며 error 상태를 처리하는 로직이 없습니다. 이로 인해 최초 로드 시 /findings API 호출이 실패하면 화면이 에러 메시지 대신 평생 '불러오는 중…' 상태에 갇히게 됩니다.

resp에 에러 정보가 포함되어 있을 때 state: "error"를 반환하도록 개선하는 것이 좋습니다.

  if (resp && resp.error) {
    return { fields: [], severity: "muted", state: "error", rows: [] };
  }
  if (!hasFindings(resp)) {
    return { fields: [], severity: "muted", state: "loading", rows: [] };
  }

Comment on lines +406 to +409
} catch (_err) {
// findings are non-critical for the live signal; keep last-good rows.
if (state.lastFindings) {
renderFindingsPanel(state.lastFindings);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

⚠️ Improvement: Findings 패널 에러 발생 시 에러 상태 전달

앞서 logic.mjs에서 제안한 에러 상태 처리를 지원하기 위해, 캐시된 lastFindings가 없는 상태에서 API 호출이 실패하면 { error: true } 객체를 전달하여 화면에 에러 UI가 표시되도록 합니다.

    // findings are non-critical for the live signal; keep last-good rows.
    if (state.lastFindings) {
      renderFindingsPanel(state.lastFindings);
    } else {
      renderFindingsPanel({ error: true });
    }

pureliture and others added 2 commits June 21, 2026 12:37
…ror state

gemini-code-assist 리뷰 후속(M8 UI):
- app.js: 폴링 실패 시 setInterval을 clearInterval하고 backoff 타이머만 재시도를 구동,
  성공 시 정기 폴 재개. 기존엔 5s interval과 backoff가 동시에 돌아 지수 backoff가
  무력화되고 장애 시 요청 폭주(HIGH).
- logic.mjs + app.js: findingsPanelModel에 error 상태 추가 + fetchFindings 실패 시
  캐시 없으면 {error:true} 전달 → 최초 /findings 실패가 'loading' 영구 고착되지 않고
  기존 error UI(index.html/style.css) 노출(MEDIUM ×2).
- test_ui_logic.mjs: findingsPanelModel error 케이스 추가(node --test 44 pass).

thread-safety(공유 boto3 store)는 loopback 단일 운영자 trust model로 위험이 제한되어
별도 추적 이슈로 분리. CodeQL red는 전부 pre-existing non-M8 alert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hXHR6Uj9nGZq84ksfP8Lt
@pureliture

Copy link
Copy Markdown
Contributor Author

리뷰 후속 (gemini-code-assist + CodeQL)

커스텀 github 에이전트로 트리아지 후 처리했습니다. 수정은 22e471d.

# 리뷰 항목 판정 조치
HIGH app.js 폴링 backoff 무력화 / 요청 폭주 VALID Fixed 22e471d — 실패 시 clearInterval로 정기 폴 중지, backoff 타이머만 재시도 구동, 성공 시 정기 폴 재개
MEDIUM logic.mjs findingsPanelModel error 상태 누락 VALID Fixed 22e471d{error:true}state:"error" 분기 추가
MEDIUM app.js findings 실패 시 error 전달 VALID Fixed 22e471d — 캐시 없으면 renderFindingsPanel({error:true}) (기존 error UI 노출) + node --test 케이스 추가
MEDIUM dashboard.py 공유 boto3 store thread-safety VALID Deferred → #57 — loopback 단일 운영자 trust model로 실위험 제한, per-thread store 수정은 시그니처/테스트 변경 동반이라 분리

CodeQL (red): open alert 6건은 전부 pre-existing non-M8(llm/vulnerability/prompt.py, core/policy/gate.py, storage/adapters/nosql_db/store.py:178, 테스트 unused-import). M8 신규 코드(dashboard.py/read_api.py/app.js/logic.mjs)에는 CodeQL alert 0건. store.py:178ResourceInUseException만 흡수하는 의도된 특정-예외 패턴(empty-except 오탐). 이 diff가 유입한 alert 아님 — pre-existing debt는 별도 트랙.

검증: uv run pytest 1149 passed / 4 skipped, node --test 44/44, ruff (M8) clean.

PR #56 리뷰의 마지막 항목(공유 boto3 store thread-safety)을 PR 내에서 해결.
make_dashboard_app/run_dashboard가 단일 store 대신 store_factory를 받아 read 요청마다
자체 store(boto3 resource)를 생성 → ThreadingWSGIServer 요청 스레드 간 boto3
resource/Session 공유 제거. 정적 서빙은 store 미접근(무영향). CLI는
lambda: store_from_args(args) factory 전달. 테스트 호출부 factory로 갱신.

게이트: pytest 1149 passed/4 skipped, node --test 44/44, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hXHR6Uj9nGZq84ksfP8Lt
@pureliture

Copy link
Copy Markdown
Contributor Author

✅ thread-safety도 PR 내에서 해결(47ad080, no defer): make_dashboard_app/run_dashboard가 store 대신 store_factory를 받아 read 요청마다 per-thread store 생성 → 공유 boto3 resource 제거. CLI는 lambda: store_from_args(args) 전달. 게이트: pytest 1149 passed, node 44/44, ruff clean. #57 resolved.

@pureliture pureliture merged commit ab85144 into main Jun 21, 2026
8 of 9 checks passed
@pureliture pureliture deleted the claude/m8-admin-ui branch June 21, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request 프로젝트 개선 제안 코드, 아키텍처, 문서, 테스트, 자동화 개선 제안

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants