콘텐츠로 이동

내장 WebUI

Continuum Router에는 바이너리에 직접 컴파일된 브라우저 기반 관리 인터페이스가 들어 있습니다. 외부 파일, 빌드 도구, Node.js 파이프라인이 필요하지 않고, 단일 바이너리 배포의 일부로 언제든 사용할 수 있습니다.

개요

WebUI는 Admin REST API에서 제공하는 작업과 같은 그래픽 인터페이스를 제공합니다. 다음 용도에 특히 유용합니다.

  • 백엔드 상태 모니터링 (인터랙티브)
  • API 키 수명 주기 관리 (생성, 교체, 활성화/비활성화, 삭제)
  • 유효성 검사가 포함된 실시간 구성 편집
  • 구성 변경 이력 및 롤백
  • 가드레일 정책 상태, 프로바이더별 튜닝, 라우트별 오버라이드, 드라이런 테스트 콘솔
  • 캐시 및 라우팅 최적화 서브시스템(응답 캐시, KV 캐시 인덱스, Gemini 컨텍스트 캐시, 프리픽스 라우팅) 가시성과 삭제 제어

WebUI 자체 에셋(HTML/CSS/JS)은 인증 없이 로드되므로 로그인 화면은 항상 접근할 수 있습니다. WebUI에서 수행하는 모든 동작은 Admin API를 거치며, 이는 나머지 /admin/*와 동일한 관리자 인증 미들웨어로 보호됩니다. 추가적인 인증 구성은 필요하지 않습니다.

구성

구성 파일의 webui 섹션이 내장 인터페이스를 제어합니다:

webui:
  enabled: true        # 기본값: 관리자가 구성된 경우 true
  path_prefix: /webui  # 기본값: /webui

구성 속성:

속성 타입 기본값 설명
enabled boolean true WebUI 활성화 또는 비활성화
path_prefix string /webui WebUI의 URL 경로 접두사. /로 시작해야 합니다. ..을 포함해서는 안 됩니다.

구성에서 webui를 생략하면 위의 기본값이 적용됩니다 (WebUI는 /webui에서 활성화됨).

WebUI를 완전히 비활성화하려면:

webui:
  enabled: false

WebUI 접속

관리자 인증이 구성된 상태에서 라우터가 실행되면 다음 주소로 이동합니다:

http://<호스트>:<포트>/webui/

예를 들어, 기본 설정을 사용하는 경우:

http://localhost:8080/webui/

처음 로드되면 WebUI는 GET /admin/health를 호출해 라우터가 어떻게 보호되고 있는지 감지한 뒤, 로그인 폼 / 전용 "접근 거부" 화면 / "라우터에 연결할 수 없음" 화면 / 앱 화면 네 가지 중 하나를 보여줍니다.

인증

Admin REST API와 동일하게 admin 섹션에서 관리자 인증을 구성하세요:

admin:
  auth:
    method: bearer
    bearer_token: "${ADMIN_TOKEN}"

HTTP Basic을 사용하는 경우:

admin:
  auth:
    method: basic
    basic_auth:
      username: admin
      password: "${ADMIN_PASSWORD}"

로그인 흐름

WebUI의 시작 시 프로브 결과에 따라 운영자가 보게 되는 화면이 달라집니다:

  • 자격 증명이 필요한 경우 (401) - 로그인 화면이 표시됩니다: "Bearer token"과 "Username / password (Basic)" 사이의 토글, 그에 맞는 입력 필드, "Remember on this device" 체크박스가 있습니다. 제출하면 입력한 자격 증명으로 같은 엔드포인트를 다시 프로브하며, 값이 틀리면 폼을 벗어나지 않고 "Invalid credentials" 인라인 오류를 표시합니다.
  • 자격 증명이 필요 없는 경우 (200) - admin.auth.methodnone이거나(또는 클라이언트 IP가 allowed_ips 목록에 있으면), WebUI는 곧바로 진입하며 닫을 수 있는 배너를 표시합니다: "Admin API authentication is disabled. Anyone who can reach this router can administer it." 배너를 닫으면 현재 브라우저 탭 세션 동안만 숨겨지며, 새 탭을 열거나 브라우저를 다시 시작하면 실제 인증을 구성하기 전까지 다시 표시됩니다.
  • 접근 거부 (403) - 요청이 거부된 이유(예: allowed_ips에 없는 IP)를 설명하는 전용 화면과 재시도 버튼이 표시됩니다. IP 수준 거부는 어떤 자격 증명으로도 해결할 수 없으므로 이 화면에는 로그인 폼이 표시되지 않습니다.
  • 네트워크 오류 - 재시도 버튼이 있는 "라우터에 연결할 수 없음" 화면이 표시됩니다.

자격 증명 저장과 "Remember on this device"

로그인에 성공하면 원본 자격 증명 필드가 아니라 완성된 Authorization 헤더 값(Bearer <token> 또는 Basic <base64(user:pass)>)만 저장됩니다:

  • 기본적으로 sessionStorage에 저장되어 브라우저 탭을 닫으면 사라집니다.
  • "Remember on this device"를 체크하면 대신 localStorage에 저장되어 브라우저를 재시작해도 유지됩니다. 이는 실질적인 트레이드오프입니다: 로그아웃하거나 서버 측에서 자격 증명을 폐기하기 전까지는 해당 브라우저 프로필에 접근할 수 있는 누구나(공유 컴퓨터, 손상된 확장 프로그램 등) 라우터를 관리할 수 있습니다. 공유 컴퓨터나 신뢰할 수 없는 환경에서는 체크하지 마세요.

세션 처리

WebUI가 수행하는 모든 Admin API 호출(js/app.jsadminFetch)은 저장된 Authorization 헤더를 첨부하며, 두 가지 실패 상황을 전역적으로 처리합니다:

  • 401 Unauthorized - 자격 증명이 거부되었거나 서버 측에서 폐기되었습니다. WebUI는 저장된 자격 증명을 지우고 "Session rejected, sign in again" 토스트를 표시한 뒤 로그인 화면으로 돌아갑니다. 다시 로그인하면 이전에 보고 있던 페이지가 자동으로 복원됩니다.
  • 403 Forbidden - 단일 동작에 대한 범위 한정 거부입니다(예: 필요한 스코프가 없는 키). WebUI는 토스트를 표시하지만 현재 페이지에 그대로 머무릅니다. 세션 자격 증명 자체는 여전히 유효하므로 403으로는 로그아웃되지 않습니다.

로그아웃

사이드바 하단에는 현재 인증 상태("Signed in (Bearer)", "Signed in (Basic)", 또는 "Auth disabled")와 Sign out 버튼이 표시됩니다. 로그아웃하면 sessionStoragelocalStorage에 저장된 자격 증명이 모두 지워지고 로그인 화면으로 돌아갑니다. admin.auth.methodnone인 경우에는 단순히 다시 프로브하여 경고 배너와 함께 앱으로 재진입합니다.

자격 증명 값은 브라우저 콘솔, 토스트, URL 어디에도 절대 기록되지 않습니다.

페이지

대시보드

대시보드는 운영 현황을 한눈에 보여주는 랜딩 페이지입니다: 아이덴티티 스트립, 통계 카드, 트래픽 트렌드, 백엔드 카드, 서킷 브레이커 상세 패널로 구성되며, 각 요소는 해당 데이터를 담당하는 전용 페이지로 연결됩니다.

  • 아이덴티티 스트립 - 라우터 버전(캐시된 capabilities 응답에서 가져옴), 틱(tick)하며 증가하는 사람이 읽기 쉬운 가동 시간(GET /admin/healthstarted_at / uptime_seconds를 폴링 사이에도 로컬에서 계속 증가시켜, 5초 폴링 주기 사이에도 멈춰 보이지 않게 함), subsystems.* 기능 플래그마다 하나씩 표시되는 칩. 전용 페이지가 있는 서브시스템(가드레일, 스마트 라우팅, 파일, 캐시, Integrations, 사용량 등)의 칩을 클릭하면 해당 서브시스템이 켜져 있든 꺼져 있든 그 페이지로 이동합니다 - 꺼진 서브시스템의 칩을 클릭해도 반응이 없는 대신, 그 페이지 자체의 "기능 비활성화됨" 패널로 이동합니다.
  • 통계 카드 - 정상 백엔드 수 / 전체 백엔드 수(GET /admin/health), 가장 최근 일별 버킷의 요청 수와 토큰 수(GET /admin/stats/series 전역 시계열의 마지막 항목), 열린 서킷 수. 각 카드는 해당 데이터를 소유한 페이지(백엔드, 사용량, 또는 이 페이지 아래쪽의 서킷 패널)로 연결됩니다.
  • 트렌드 행 - GET /admin/stats/series로 그리는 14일 요청 수 스파크라인(uPlot, 갱신마다 깜빡임 없이 제자리에서 업데이트됨)과, GET /admin/statstotal_requests를 폴링 사이 델타로 클라이언트에서 계산하는 근사 현재 요청률 패널(폴링 기반 근사치임을 명시적으로 표기합니다).
  • 백엔드 카드는 백엔드마다 헬스 상태, 타입, 가중치, (서킷 브레이커가 구성된 경우) 서킷 상태를 GET /admin/backends, GET /admin/config/backends, GET /admin/circuit/all에서 가져와 보여줍니다. healthy가 아닌 백엔드의 카드가 먼저 정렬되고 색이 있는 테두리로 강조됩니다. 각 카드의 케밥 메뉴는 편집(#backends/<name>을 통해 백엔드 페이지의 편집 패널로 딥링크)과 서킷 강제 액션 세 가지를 제공하며, 모두 확인 모달을 거칩니다.
  • 서킷 브레이커 패널 - 일반 LLM 프록시 요청이 여기 표시되는 것과 동일한 서킷 상태를 갱신합니다. 패널은 GET /admin/circuit/all이 보고하는 모든 정보를 백엔드마다 하나씩 보여줍니다: 실패/성공 횟수, 연속 성공 횟수, 반열림 요청 수, 전체 통계 블록(총/성공/실패 요청 수, 성공률, 열림/닫힘 횟수, 평균 열림 지속 시간), 마지막 실패/마지막 성공/마지막 상태 전환 시각(서킷이 열려 있는 동안에는 다음 재시도 시각도 함께). 백엔드 카드와 동일한 확인 모달을 통해 동일한 세 가지 강제 액션을 여기서도 사용할 수 있습니다.

폴링: 헬스, 백엔드, 서킷, 현재 요청률 스냅샷은 5초마다 갱신됩니다. 14일 시계열은 5분마다 갱신됩니다. 두 주기 모두 아이덴티티 스트립의 자동 새로고침 토글을 공유합니다 - 토글을 끄면 모든 네트워크 폴링이 멈추지만, 가동 시간 카운터는 새 데이터 없이도 정확히 계산할 수 있으므로 로컬에서 계속 증가합니다.

빈 상태와 저하 상태: 아직 트래픽이 기록되지 않은 새 라우터는 트렌드 행에 빈 차트 대신 설명 문구를 표시하고, 서킷 브레이커가 구성되지 않은 라우터는 서킷 패널에 빈 표 대신 안내 문구를 표시하며, 컴파일에서 빠졌거나 비활성화된 서브시스템도 죽은(아무 곳으로도 연결되지 않는) 칩을 만들지 않습니다 - 기능 정보와 기능 게이팅을 참고하세요.

API 키

API 키 페이지는 API 키에 대한 전체 수명 주기 관리를 제공합니다:

  • 마스킹된 값과 상태 표시기로 모든 키 목록 보기
  • 구성 가능한 범위, 백엔드/모델 허용 목록, 어노테이션, 속도 제한으로 새 키 생성
  • 기존 키 메타데이터 편집 (이름, 범위, 허용 목록, 어노테이션, 속도 제한)
  • 키 값 교체 (키 ID를 유지하면서 새 시크릿 생성)
  • 삭제하지 않고 키 활성화 또는 비활성화
  • 키 영구 삭제
  • 이름 또는 상태로 검색 및 필터링

범위(Scopes)는 라우터가 실제로 인식하는 범위(read, write, admin, files - 인증 및 admin-auth 미들웨어가 검사하는 것과 동일한 집합)로 구성된 체크박스 그리드로 표시됩니다. 구성 파일에서 불러온 키가 이 집합 밖의 범위를 가지고 있을 수도 있는데, 그런 키를 편집해도 저장 시 조용히 삭제되지 않고 체크박스 옆에 제거 가능한 칩으로 계속 표시됩니다.

백엔드/모델 허용 목록(allowed_backends, allowed_models)은 키가 라우팅할 수 있는 백엔드와 보고 사용할 수 있는 모델을 제한합니다. 백엔드 목록은 GET /admin/backends에서 채워지는 체크박스 그리드이고, 모델 목록은 모델 카탈로그(GET /admin/models)의 제안을 받는 입력 후 Enter 방식의 태그 입력입니다. 빈 목록은 제한 없음을 의미하며, 폼과 키 테이블 모두 이를 "제한 없음" / "Unrestricted"로 명시적으로 표시하여 모호함을 남기지 않습니다. 강제 적용은 프록시 단계에서 이루어집니다: 비어 있지 않은 allowed_models 밖의 모델에 대한 요청은 어떤 백엔드에도 도달하기 전에 403 Forbidden으로 거부됩니다.

어노테이션(Annotations)team, email, 비용 센터 태그 등 운영자 메타데이터를 위한 자유 형식 키/값 맵입니다(행 추가/삭제). 라우터 자체에는 특별한 의미가 없으며 api_key_info 메트릭의 Prometheus 레이블로 노출되는 용도 외에는 사용되지 않습니다.

키 테이블에는 Restrictions 배지("2 backends, 3 models" 또는 "Unrestricted" 등)와 Annotations 배지(개수 표시, 마우스를 올리면 모든 키/값 쌍 표시)가 추가되었고, 화면에 실제로 스크롤되어 보이는 행에 대해서만 GET /admin/stats/api-keys/{id}에서 지연 로드되는 두 개의 숫자(총 요청 수, 총 토큰 수)가 표시됩니다. 이 요청은 일괄 처리되고 캐시되므로 수백 개의 키가 있는 테이블이라도 수백 개의 즉시 요청을 발생시키지 않습니다.

행(액션 버튼 제외)을 클릭하면 읽기 전용 상세 드로어가 열립니다: 모든 필드, 칩으로 표시된 두 허용 목록, 전체 어노테이션 테이블, 생성/만료 타임스탬프, 마스킹된 키 ID, 그리고 GET /admin/stats/api-keys/{id}/series를 기반으로 한 14일간의 요청 스파크라인이 표시됩니다. 각 키 행에는 "사용량 보기" 액션도 있어(드로어에서는 "Open full usage"로도 접근 가능) 해당 키의 사용량 페이지 드릴다운으로 딥링크됩니다 (#usage/api-keys/<id>).

사용량

사용량 페이지는 /admin/stats/* 계열의 요청·토큰 분석과 메트릭 이력 탐색기를 보여줍니다. 6개 탭으로 구성됩니다:

  • 개요: 총 요청 수, 프롬프트 토큰, 완료 토큰, 활성 키 수에 대한 통계 카드(GET /admin/stats), 일별 요청 수 라인 차트, 프롬프트 대 완료 토큰을 쌓아 보여주는 일별 토큰 차트. 두 차트 모두 전역 일별 시계열(GET /admin/stats/series)을 기반으로 합니다.
  • 모델백엔드: 모델별·백엔드별 사용량을 정렬·필터 가능한 표로 보여주며, 각 행에는 요청 수에 비례하는 점유율 막대가 표시됩니다.
  • API 키사용자: 키별·사용자별 합계를 담은 마스터 표. 행을 클릭하면 모델별 분석 표와 일별 사용량 차트가 있는 드릴다운이 열립니다. API 키 드릴다운은 API 키 페이지의 "사용량 보기" 액션의 딥링크 대상이기도 합니다 (#usage/api-keys/<id>).
  • 메트릭: 선택한 시간 범위(1시간 / 6시간 / 24시간 / 7일 / 사용자 지정)에 걸쳐 저장된 Prometheus 시계열을 그리는 메트릭 이력 탐색기. 프리셋 목록(http_requests_total, http_request_duration_seconds, backend_current_load, model_usage_total, model_tokens_processed, errors_total)에서 고르거나 다른 메트릭 이름을 직접 입력할 수 있습니다.

자동 새로고침: 기본으로 켜져 있으며 30초 간격입니다. 차트는 깜빡임 없이 제자리에서 갱신되며, 끄면 일시 중지됩니다.

통계 초기화: 개요 탭에는 누적된 모든 카운터와 시계열을 지우는 위험 액션이 있습니다. 확인 모달에 reset을 입력해야 POST /admin/stats/reset을 호출하며, 이후 페이지 데이터가 다시 로드됩니다.

메트릭 탭 노출 조건: 메트릭 탭은 영속 메트릭 로그가 활성일 때만 표시되며, 이는 metrics-persistence 빌드 기능과 런타임 metrics.persistence 구성을 모두 요구합니다. 탭은 subsystems.metrics_persistence_active 기능 플래그로 게이팅되며, 그렇지 않으면 숨겨집니다. 페이지 로드 후 기능이 꺼진 경우(리빌드 경쟁 상황) 탐색기는 깨진 차트 대신 기능 비활성화 패널을 표시합니다.

백엔드

백엔드 페이지는 구성된 모든 백엔드를 나열하고, 섹션으로 구성된 상세 편집 폼을 통해 백엔드 구성 전체를 노출합니다.

목록은 백엔드마다 카드 하나를 표시하며, 상태 점과 상태 텍스트, 연속 실패 횟수, 마지막 점검 시각, 그리고 인증 타입, internal 플래그, 분산 서빙 role(unified가 아닐 때만), 가장 최근 온디맨드 프로브 결과를 나타내는 배지를 함께 보여줍니다. 각 카드에는 지금 프로브, 편집, 삭제 세 가지 행 동작이 있습니다.

상세 편집 슬라이드오버

백엔드 추가편집은 평면 모달 대신 접을 수 있는 섹션으로 구성된 슬라이드오버 패널을 엽니다:

  • 기본 - 이름(편집 시 변경 불가), 타입, URL, 가중치, 조직 ID. bedrock의 경우 URL은 선택 사항이며 리전에서 URL이 템플릿으로 생성됩니다.
  • 인증 - api key, oauth, service account, AWS SigV4 중 하나를 라디오로 선택하고 각각 고유 필드를 표시합니다. SigV4 옵션은 features.bedrock_sigv4 빌드 기능에 따라 게이팅되며, 그렇지 않으면 "bedrock-sigv4 빌드가 필요함" 안내와 함께 비활성화됩니다. 여기서 OAuth는 방식만 선택하며, 토큰 저장소는 브라우저가 아니라 CLI continuum-router auth login 흐름으로 채웁니다.
  • 모델 - 모델 이름 목록과 모델별 구성을 위한 고급 JSON 편집기.
  • 헬스 체크 - 재정의 토글. 꺼져 있으면 백엔드 타입 기본값이 사용되고, 전역 전용인 간격과 임계값이 참고용으로 표시됩니다. 켜면 백엔드별 엔드포인트, 메서드, 타임아웃, 허용 상태 코드를 편집할 수 있으며 전역 값이 플레이스홀더로 표시됩니다.
  • 재시도 - 최대 시도, 지연, 타임아웃, 백오프 배수, 지터를 노출하는 재정의 토글이며 전역 재시도 정책이 플레이스홀더로 표시됩니다.
  • 고급 - internal 가시성, 분산 서빙 role, Bedrock 엔드포인트 타입과 리전(bedrock에서만 표시), Anthropic auto_cache_controlfast_mode 플래그(anthropic에서만 표시), prefill/decode role을 위한 외부 KV 저장소.

비밀 값은 쓰기 전용입니다. API는 자격 증명을 마스킹하여 반환하므로 폼은 저장된 비밀 값을 절대 표시하지 않습니다. 편집 시 인증 섹션은 기본적으로 보존되며, 인증 교체를 체크하면 편집기가 나타나고 비워 둔 비밀 필드는 전송되지 않습니다. API는 저장된 비밀 값을 되돌려줄 수 없으므로 자격 증명은 교체는 가능하지만 조회는 불가능합니다.

패널 하단의 연결 테스트는 저장된 백엔드에 대해 온디맨드 프로브를 실행하고(추가 폼에서는 "먼저 저장" 힌트와 함께 비활성화됨) 상태, 지연 시간, 점검한 URL, 오류를 인라인으로 렌더링합니다.

온디맨드 헬스 프로브

지금 프로브 행 동작과 패널의 연결 테스트 버튼은 POST /admin/backends/{name}/check를 호출합니다. 프로브는 백그라운드 헬스 모니터가 해당 백엔드에 수행하는 것과 동일한 점검(백엔드 타입별 동일한 엔드포인트 해석, 동일한 인증, 동일한 타임아웃)을 실행하지만 순수 프로브입니다. 즉 헬스 상태 머신을 절대 변경하지 않으므로 백엔드를 프로브해도 헬스 임계값에 가까워지거나 서킷 브레이커를 트립하지 않습니다. 응답은 status(healthy, unhealthy, warming_up), latency_ms, checked_url, 실패 시 error 문자열을 보고합니다.

모델

모델 페이지는 통합 모델 카탈로그와 모델 중심 구성 섹션 두 개를 세 개의 탭으로 다룹니다. 가드레일이나 스마트 라우팅과 달리 기능 게이팅이 없습니다. 카탈로그와 두 편집기 모두 항상 사용할 수 있기 때문입니다. WebUI는 관리자 자격 증명만 보유하고 공개 /v1/models* 엔드포인트는 API 키 인증 뒤에 있어(blocking 모드에서는 접근할 수 없음) Catalog 탭은 전용 관리자 전용 집계 엔드포인트를 읽습니다(src/admin_config/models_api.rs):

  • Catalog - GET /admin/models를 통해 라우터가 알고 있는 모든 모델을 검색 가능한 표로 보여줍니다. 각 행에는 서빙 백엔드 이름, 파생된 상태(서빙 백엔드 중 하나라도 정상이면 available, 하나도 정상이 아니면 degraded, 상태를 판단할 수 없으면 unknown), 컨텍스트 윈도우, 메타데이터가 있는 경우 1K 토큰당 가격이 표시됩니다. internal: true 백엔드로만 서빙되는 모델은 "Show internal" 토글 뒤에 숨겨지고 별도의 배지가 붙습니다. GET /v1/models는 이런 모델을 완전히 숨기므로, 관리자 카탈로그가 이를 계속 나열하는 유일한 화면입니다. 행을 클릭하면 메타데이터 카드(개발사, 기능, 최대 출력, 사고/추론 플래그, 요약, 이 모델을 가리키는 별칭 이름)와 GET /admin/backends와 대조한 백엔드별 상태 칩이 펼쳐집니다. "Refresh models"는 POST /admin/models/refresh(공개 POST /v1/models/refresh가 사용하는 것과 동일한 강제 새로고침)를 호출하고 마지막 새로고침이 얼마나 전에 완료되었는지 표시합니다.
  • Aliases - model_aliases 구성 섹션(GET/PUT /admin/config/model_aliases)을 위한 폼 편집기입니다: 5개의 이름 있는 부분 문자열 슬롯(haiku, sonnet, opus, reasoning, default) 각각은 카탈로그에서 채워진 모델 선택 상자이며, 여기에 exact 오버라이드 표(별칭 이름 → 대상 모델)가 추가됩니다. 저장 시 먼저 POST /admin/config/validate로 검증하고 오류가 있으면 PUT을 보내기 전에 인라인으로 표시합니다.
  • Fallback Chains - fallback.fallback_chains 맵(GET/PUT /admin/config/fallback)을 위한 시각적 편집기입니다: 각 체인은 기본 모델 다음에 순서가 있는 폴백 모델 목록으로 렌더링되며, 체인과 개별 단계 모두에 추가/삭제 컨트롤이 있습니다. 이 엔드포인트는 섹션 전체를 교체하므로 저장 시 불러온 그대로 다른 모든 fallback 필드(enabled, mid_stream_enabled, fallback_policy, model_settings)를 보존합니다. 클라이언트 측 검사는 서버 검증을 그대로 반영합니다 - 자기 참조 없음, 직접적인 순환 참조 없음, 모델 이름은 [A-Za-z0-9._-]{1,128} 패턴을 따라야 함 - 이 조건이 해결될 때까지 저장 버튼을 막습니다. POST /admin/config/validatePUT 자체가 최종 권한을 가지므로, 다른 클라이언트를 통해 강제로 전송된 체인이라도 서버 측에서 거부되고 오류가 표시됩니다.

model_aliasesfallback 모두 점진적으로 핫 리로드됩니다: 기존 연결은 계속 실행되고, 저장에 성공하면 새 연결부터 즉시 변경 사항이 적용되며 재시작이 필요하지 않습니다.

구성

구성 페이지는 Admin Config API가 노출하는 17개 섹션용 스키마 기반 편집기입니다. 세션당 한 번 구성 JSON 스키마(GET /admin/config/schema)를 읽고, 섹션별 메타데이터를 GET /admin/config/sections(SectionInfo { name, description, hot_reload, configured })에서 가져온 뒤, 단일 원시 JSON 텍스트 영역 대신 섹션마다 실제 폼을 생성합니다.

섹션 목록

섹션은 Core(server, backends, health_checks, logging), Traffic(rate_limiting, timeouts, retry, circuit_breaker, fallback), Features(그 외 전부)로 묶입니다. UI가 인식하지 못하는 지원 섹션은 자동으로 Features에 들어갑니다. 각 항목은 섹션 설명, 구성됨인지 기본값으로 동작 중인지 나타내는 점, 그리고 SectionInfo.hot_reload에서 파생된 핫 리로드 배지를 표시합니다. 배지는 immediate(녹색, "즉시 적용"), gradual(파란색, "새 요청에 적용"), requires_restart(주황색, "재시작 필요") 세 가지입니다.

스키마 기반 폼

각 섹션에서 페이지는 스키마 하위 트리로부터 폼을 생성합니다. 객체는 필드 그룹이 되고, 원시 값은 타입별 입력(텍스트, 최소/최대 경계가 있는 숫자, 체크박스, 열거형 선택)이 되며, 원시 값 배열은 태그 입력이 되고, 객체 배열은 추가·삭제 컨트롤이 있는 반복 행이 됩니다. 스키마의 필드 설명은 인라인 힌트로 표시되고, 중첩은 임의 깊이까지 처리됩니다(예: timeouts.request.streaming.chunk_interval). 자유 형식 맵(additionalProperties)이나 유니온처럼 스키마가 필드를 의미 있게 기술하지 못하는 경우, 섹션 전체를 원시 JSON으로 떨어뜨리는 대신 해당 필드 하나만 범위가 한정된 JSON 하위 편집기로 대체됩니다. 루트가 객체가 아닌 섹션(예: 백엔드 페이지에서 전체를 편집하는 backends, 모델 페이지에서 편집하는 model_aliases)은 곧바로 Raw JSON 뷰로 열리고 배열·스칼라·null 값을 정확히 유지하며, 해당 값을 표현할 수 없는 폼 전환 버튼은 제공하지 않습니다.

섹션마다 제공되는 Raw JSON 토글은 생성된 폼과 원시 편집기를 전환합니다. 두 뷰는 동일한 기저 객체에 바인딩되므로 저장하지 않은 편집이 뷰 전환에도 유지됩니다. 잘못된 원시 JSON은 인라인으로 표시되며 폼 뷰로의 전환을 막습니다.

시크릿

API가 마스킹하는 필드(스키마에서 x-sensitive로 표시되었거나 마스크 자리표시자로 반환되는 필드)는 쓰기 전용 비밀번호 입력으로 렌더링되며, 빈 값은 "저장된 값 유지"를 의미합니다. 페이지는 마스크 자리표시자를 라우터로 되돌려 보내지 않습니다. 저장 시 편집 상태를 조회된 상태와 비교하고 변경된 리프만 담아 PATCH를 보내므로, 건드리지 않은 시크릿은 요청에 아예 포함되지 않고 병합 과정에서 저장된 값이 그대로 유지됩니다. 섹션에 시크릿이 없고 키가 삭제되었거나(또는 섹션이 이전에 비어 있던) 경우에만 섹션 전체를 PUT으로 교체합니다. 부분 업데이트에서 JSON 배열은 값 전체가 교체되므로, 변경된 배열에 마스킹된 자리표시자가 남아 있으면 검토 모달이 저장을 차단합니다. 전용 섹션 편집기를 사용하거나 배열 변경을 되돌리거나, 저장하기 전에 배열에 남을 모든 시크릿을 다시 입력해야 합니다.

검토와 저장

Review & save는 구조적 차이(추가·삭제·변경된 리프와 이전/새 값), 섹션의 핫 리로드 결과를 평이한 문장으로, 그리고 모달이 열릴 때 실행되는 자동 유효성 검사(POST /admin/config/validate) 결과를 보여주는 모달을 엽니다. 원시 JSON이나 범위가 한정된 JSON을 통해 입력한 시크릿까지 렌더링된 차이에서 재귀적으로 가립니다. 섹션 범위 요청의 경우 서버는 후보 섹션을 현재 구성 위에 병합하고 완전한 후보 구성을 역직렬화한 뒤 쓰기 경로와 동일한 전역 검증을 실행합니다. 유효성 검사 오류는 저장을 막고, 경고는 표시되지만 저장을 허용합니다. 확인하면 PATCH(또는 PUT)를 실행하며, 이 섹션 변경 자체가 후보 스냅샷을 검증하고 발행합니다. 선택적 "Apply now" 후속 작업과 별도 Apply Changes 버튼은 전체 후보 없이 POST /admin/config/apply를 호출합니다. 섹션 변경은 이미 발행됐으므로 이는 명시적인 no-op이며 빈 이력 항목을 만들거나 리로드했다고 주장하지 않습니다. API 클라이언트는 이 엔드포인트에 전체 config 후보를 전달하여 원자적인 검증/차이/미리보기 또는 발행을 수행할 수 있습니다. 재시작 필요 필드는 여전히 프로세스를 재시작해야 합니다.

내보내기와 가져오기

내보내기는 전체 구성(시크릿 마스킹됨)을 YAML, TOML 또는 JSON으로 다운로드합니다. 가져오기는 붙여 넣은 내용을 먼저 검증하고(POST /admin/config/validate), JSON의 경우 커밋 전에 현재 구성과의 구조적 차이를 보여줍니다. YAML과 TOML은 브라우저가 번들 파서 없이 파싱할 수 없으므로 서버 측에서 검증한 뒤 인브라우저 차이 없이 적용됩니다. 유효성 검사가 성공한 뒤에야 Import & apply 동작이 POST /admin/config/import를 보냅니다.

내보낸 뒤 다시 가져와도 자격 증명은 안전합니다. 마스킹된 자리표시자는 "지금 적용된 값을 그대로 둔다"는 뜻이므로, 왕복해도 비밀 값이 자기 마스크로 덮어써지지 않고 그대로 남습니다. 검증 단계와 가져오기 응답 모두 그대로 둔 경로를 나열하며, 자리표시자를 실행 중인 값과 대응시키지 못한 가져오기는 조용히 추측하는 대신 실패한 경로를 알리며 거부됩니다. 자세한 내용은 관리 API 문서의 '내보내기에서 가져오기로 돌아올 때의 마스킹된 비밀 값' 절을 참고하세요.

스마트 라우팅

스마트 라우팅 페이지는 smart_routing.enabled: true(즉 subsystems.smart_routing 기능 정보)가 아니면 숨겨지며, 비활성화된 상태에서 직접 이동하면 smart_routing 설정 섹션을 명시하는 표준 "기능 비활성화됨" 패널이 표시됩니다. 활성화되면 다섯 개 탭에 걸쳐 전체 admin API 표면을 다룹니다:

  • Status - 활성화 여부, 가상 모델, 기본 티어, intercept_all; 부하 상태 패널(현재 레벨, 최대 티어, 저하 플래그, 실시간 스냅샷 지표); 그리고 집계 통계 패널(프로필/정책 수, 분류기 캐시 크기, metrics 기능이 컴파일된 경우 결정/분류/폴백 카운터). 15초마다 자동 새로 고침되며 일시정지 토글을 제공합니다.
  • Model Profiles - 명시적으로 설정된(explicit_exact) 모델 티어 프로필 테이블입니다. 행을 로컬에서 추가·수정·삭제한 뒤 Save all을 누르면 편집된 전체 집합으로 PUT /admin/smart-routing/model-profiles 벌크 호출을 한 번 실행하며, 추가/변경/삭제된 프로필을 요약하는 확인 단계를 거칩니다. 글로브 패턴 프로필(model_pattern)과 실시간 트래픽에서 자동 추론된 프로필은 벌크 엔드포인트가 목록 전체를 교체하기 때문에 이 테이블에는 나열되지 않으며(개수만 안내 문구로 표시), 패턴 프로필은 저장할 때마다 그대로 다시 제출되어 저장 시 조용히 사라지지 않습니다.
  • Policies - 순서가 있는 라우팅 정책 목록으로, 위/아래 재정렬 버튼과 정책별 인라인 편집기(when 조건, route_to 대상)를 제공하며, Save allPUT /admin/smart-routing/policies 벌크 호출을 한 번 실행합니다. 이 엔드포인트는 유효성 검사 실패를 HTTP 오류가 아니라 200 OK 본문({"status": "error", "errors": [...]})으로 보고하므로, 페이지는 HTTP 상태 대신 응답 본문을 확인해 각 오류를 인라인으로 표시합니다.
  • Playground - 두 개의 창으로 구성된 드라이런 도구입니다: 왼쪽은 요청 빌더(일반 프롬프트 또는 원시 JSON 페이로드), 오른쪽은 결과입니다. ClassifySimulatePOST /admin/smart-routing/classify.../simulate를 호출하며, 실제 트래픽을 라우팅하지 않습니다. 각 결과는 요약 뷰와 접을 수 있는 원시 JSON 응답을 함께 보여줍니다.
  • Cache - LLM 분류기 캐시 통계(항목 수, 용량, TTL)와 확인 절차를 거치는 Clear cache 동작을 제공합니다.

이력

이력 페이지는 구성 변경 타임라인을 표시합니다. 각 GET /admin/config/history 항목은 메타데이터(버전, 타임스탬프, 출처, 행위자, 변경된 섹션, 설명)와 함께 해당 버전 시점의 전체 구성 마스킹 스냅샷을 담습니다.

  • 항목을 펼치면 이전(더 오래된) 항목과의 구조적 차이가 표시되어, 그 변경이 정확히 무엇을 도입했는지 볼 수 있습니다. 가장 오래된 항목은 기준선으로 표시됩니다.
  • 롤백은 현재 구성에서 복원 대상 버전으로의 차이를 보여주는 확인 창을 연 뒤 POST /admin/config/rollback/{version}을 실행합니다.

스냅샷은 admin API의 다른 부분과 동일한 규칙으로 마스킹되므로, 시크릿 교체가 차이로 나타나지 않고 이력 엔드포인트를 통해 시크릿이 노출되지 않습니다.

가드레일

가드레일 페이지는 subsystems.guardrailstrue가 아니면 숨겨집니다(기능 정보와 기능 게이팅 참고). 이 상태에서 직접 이동하면 guardrails 설정 섹션을 명시하는 "기능 비활성화됨" 패널이 대신 표시됩니다. 이 페이지는 여섯 개의 가드레일 관리 엔드포인트(src/admin_config/guardrails_api.rs)를 다룹니다:

  • 상태 스트립 - GET /admin/guardrails로 읽어 온 현재 enabled / mode / on_error / timeout_ms / block_behavior를 보여 줍니다. "Edit mode / on-error"를 누르면 변경의 결과를 명확히 설명하는 확인 모달이 뜨고(강제(enforce) 모드는 검사를 통과하지 못한 트래픽을 차단하며, fail-closed는 프로바이더 오류 시 트래픽을 거부합니다), 확인 후에 PATCH /admin/guardrails를 호출합니다.
  • 프로바이더 - 설정 파일에 구성된 프로바이더마다 카드 하나씩, 타입·단계(stage)·활성화 여부·임계값을 보여 줍니다. 빠른 활성화/비활성화 토글과 전체 편집 모달(활성화 여부, 카테고리별 임계값, 타임아웃 오버라이드, on-error 오버라이드) 모두 PUT /admin/guardrails/providers/{name}을 사용합니다. 이 페이지에서는 프로바이더를 생성하거나 삭제할 수 없습니다. 이는 여전히 설정 파일 변경 사항이며, Configuration 페이지의 guardrails 섹션에서 처리합니다. 프로바이더의 api_key_env가 참조하는 API 키는 마스킹된 읽기 전용 값으로만 표시됩니다. 업데이트 엔드포인트에는 이를 변경할 필드가 없기 때문입니다.
  • 라우트 오버라이드 - 라우트별 정책 오버라이드(모드, 활성화 여부, 프로바이더 부분집합, 임계값, 허용/차단 목록) 표를 보여 줍니다. 추가/편집은 PUT /admin/guardrails/routes/{route}를, 삭제는 확인 모달 뒤에서 DELETE /admin/guardrails/routes/{route}를 사용합니다. /를 포함하는 라우트 패턴(예: 팀 단위 라우트 ID)은 요청 전에 퍼센트 인코딩되므로, 라우터의 단일 동적 {route} 경로 세그먼트와 여전히 매칭됩니다.
  • 테스트 콘솔 - POST /admin/guardrails/test에 대한 드라이런입니다: 샘플 텍스트를 붙여넣고 input/output 단계를 선택한 뒤 실행합니다. 이 엔드포인트는 항상 등록된 모든 프로바이더를 평가합니다. "Show results for" 필터는 이 페이지가 어떤 행을 표시할지만 좁힐 뿐, 실제로 무엇이 실행되었는지는 바꾸지 않습니다. 각 결과는 판정 칩(pass / flag / block / skipped)을 보여 주고, 프로바이더가 타임아웃되거나 오류를 내서 판정이 설정된 폴백으로만 결정된 경우에는 Fail-open 또는 Fail-closed 칩으로 표시합니다. 이런 실패 행에는 API 응답의 정제된 실패 종류와 이유도 함께 보여 주므로, fail-open 대체 허용이 실제 Pass와 더 이상 구분되지 않는 일이 없습니다. 카테고리와 점수는 있을 때 계속 표시되며, "Raw JSON" 접이식 영역에는 전체 응답 항목이 들어 있습니다. 드라이런은 운영 메트릭이나 감사 로그에 영향을 주지 않으며, 엔드포인트가 프로바이더별 지연 시간 값을 보고하지 않으므로 이 페이지는 대신 전체 테스트 호출의 왕복 시간을 표시합니다.

캐시

캐시 페이지는 응답 캐시, KV 캐시 인덱스, Gemini 컨텍스트 캐시, 프리픽스 인지 라우팅이라는 네 가지 캐싱/라우팅 최적화 서브시스템에 대한 가시성과 삭제 제어를 제공합니다. 다른 페이지와 달리 페이지 단위의 단일 기능 게이트가 없습니다. 네 섹션 각각이 자신의 subsystems.* 플래그로 독립적으로 게이팅되는데, 운영자가 이 네 가지 중 한두 개만 활성화하는 경우가 흔하기 때문입니다. 비활성화된 서브시스템은 실시간 카드 대신 해당 설정 섹션 이름을 명시하는 간단한 행으로 표시되며, 네 가지가 모두 비활성화된 경우 페이지는 비활성화 행 네 개 대신 설명이 담긴 단일 빈 상태를 보여 줍니다.

  • 응답 캐시: 적중률, 항목 수(용량 대비), 크기, 축출 횟수, 백엔드 타입(메모리 또는 Redis)을 보여 주고, Redis 백엔드가 활성 상태이면 Redis 연결/오류 요약도 함께 표시합니다. "Invalidate" 작업은 캐시 전체를 비웁니다. 무효화 엔드포인트는 현재 modeltenant_id를 무시하므로 이 버튼은 항상 전체 플러시를 수행합니다. 플러시는 캐시된 모든 응답을 버리는 작업이므로, 확인 대화상자는 flush를 입력해야 버튼이 활성화되도록 요구합니다.
  • KV 캐시 인덱스: 인덱스 전체 통계(항목 수, 추적 중인 프리픽스, 총 적중/축출 수, KV 인지 라우팅 비율)와 함께 이벤트 소스 연결 상태 및 인덱스 이벤트 수를 담은 백엔드별 표를 보여 줍니다. "Clear Index"는 라우터의 로컬 라우팅 인덱스를 비웁니다. 백엔드 측 KV 캐시 데이터는 전혀 삭제하지 않으며, 인덱스가 들어오는 백엔드 이벤트로부터 다시 채워질 때까지 라우팅은 단순히 KV 비인지 방식으로 돌아갑니다.
  • Gemini 컨텍스트 캐시: 추적 중인 항목 수, 진행 중인 생성 작업 수, 적중/실패/생성 카운터, 캐시된 토큰 총량, 백엔드별 항목 분포를 보여 줍니다. "Clear"는 라우터의 로컬 추적 정보를 삭제하고 추적 중인 각 cachedContents 리소스에 대해 최선 노력으로 삭제 요청을 보냅니다. 이 삭제가 미치지 못한 항목은 Gemini 쪽에서 TTL에 따라 스스로 만료되며, 새 요청은 필요에 따라 캐시를 다시 생성할 뿐입니다.
  • 프리픽스 라우팅: 읽기 전용 라우팅 결정 수(프리픽스 해시, 오버플로, 폴백), 오버플로 비율, 고유 프리픽스 수, 백엔드별 진행 중 요청 분포를 보여 줍니다. 여기에는 변경을 가하는 제어가 전혀 없습니다. 엔드포인트 목록 자체에 프리픽스 라우팅용 삭제나 초기화 동작이 없기 때문입니다.

네 섹션 모두 최초 조회 시 로딩 스켈레톤을, 실패 시 재시도 버튼이 있는 인라인 오류를, 표시할 데이터가 없을 때는 빈 표 메시지를 보여 줍니다. 비율(적중률, 오버플로 비율)과 바이트 크기는 가독성을 위해 서식이 적용되며, 정확한 원본 값은 해당 요소의 툴팁에서 확인할 수 있습니다. 자동 새로 고침 토글은 활성화된 각 섹션의 통계를 15초마다 폴링하며, 삭제나 무효화 작업 이후에는 해당 섹션이 즉시 새로 고침되고 결과가 토스트로 보고됩니다.

프롬프트

프롬프트 페이지는 global_prompts 설정 섹션, 즉 라우터의 전역 시스템 프롬프트 주입 서브시스템을 관리합니다. 이 섹션은 항상 파싱되므로 기능 게이트가 없습니다. 대신 global_prompts가 전혀 설정되지 않은 동안에는 일반적인 2단 레이아웃 대신 서브시스템을 설명하는 시작 안내 패널("Configure" 동작 포함)을 보여 줍니다. 이 페이지는 네 개의 프롬프트 파일 관리 엔드포인트(src/admin_config/prompts_api/handlers.rs)와 범용 GET/PUT /admin/config/global_prompts 섹션 엔드포인트 위에 구축되었습니다.

  • 프롬프트 파일 (왼쪽 패널) - Injection Settings가 현재 참조하는 프롬프트 파일(기본 프롬프트와 백엔드/모델별 오버라이드)의 트리를 보여 주며, 파일을 선택하면 GET /admin/config/prompts/{path}로 불러와 일반 모노스페이스 텍스트 영역에 표시합니다. GET /admin/config/prompts는 디렉터리 목록 조회가 아닙니다. 그런 엔드포인트 자체가 없으므로, 설정이 실제로 참조하는 항목만 반환됩니다. 이 페이지 세션 중에 새로 만든 파일은 설정 항목에 연결되어 저장되기 전까지는 로컬에서만 계속 보여 줍니다. 저장은 PUT /admin/config/prompts/{path}를 사용합니다. "New file"도 같은 방식으로 빈 파일을 만들되, 클라이언트 쪽에서 ..와 빈 경로 세그먼트를 먼저 거부합니다(서버도 동일한 제약을 강제합니다). "Reload from disk"는 확인 대화상자 뒤에서 POST /admin/config/prompts/reload를 호출합니다. 설정된 모든 파일을 디스크에서 다시 읽어 올 뿐, UI에 입력한 내용은 아무것도 버리지 않습니다. 파일 삭제 엔드포인트가 없으므로 이 페이지도 삭제 기능을 제공하지 않습니다.
  • Injection Settings (오른쪽 패널, 접을 수 있음) - global_prompts 섹션에 대한 폼입니다: 기본 프롬프트(인라인 텍스트 또는 파일 참조), prompts_dir, 병합 전략(prepend / append / replace)과 구분자(GlobalPromptConfig::merge_prompts를 클라이언트 쪽에서 그대로 반영하는 실시간 미리보기 포함), 그리고 백엔드별/모델별 오버라이드를 추가/삭제할 수 있는 행(각각 인라인 텍스트 또는 파일 참조)으로 구성됩니다. 저장은 PUT /admin/config/global_prompts를 호출합니다. 저장이 거부되면 토스트뿐 아니라 서버의 오류 메시지를 인라인으로도 보여 줍니다. global_prompts는 즉시 핫 리로드되므로, 저장에 성공하면 Configuration 페이지와 동일한 config-applied 이벤트를 발생시킵니다.
  • 미저장 변경 보호 - 파일이나 Injection Settings 폼을 편집하면 dirty 플래그가 설정됩니다. dirty 상태에서 다른 파일이나 다른 페이지로 이동하려 하면 확인을 요청하며, 브라우저 탭을 닫거나 새로 고치면 표준 브라우저의 "사이트 나가기" 경고가 뜹니다.

파일

파일 페이지는 파일 API(/v1/files)에 저장된 파일을 관리합니다. files.enabled: true(subsystems.files 기능)가 아니면 사이드바에서 숨겨지며, 비활성화 상태에서 직접 이동하면 files 설정 섹션을 명시하는 표준 "기능 비활성화" 패널이 표시됩니다.

공개 /v1/files 경로는 라우터 API 키로 인증하고 소유자별 소유권을 강제할 수 있으므로, WebUI의 관리자 세션은 이 경로를 직접 사용할 수 없습니다. 대신 이 페이지는 동일한 파일 서비스를 관리자 자격 증명으로 재사용하고 소유권 검사를 설계상 우회하는 관리자 전용 엔드포인트(src/admin_config/files_api.rs)를 사용합니다. 따라서 모든 API 키/사용자가 업로드한 파일이 한 화면에 함께 나열됩니다. 엔드포인트 레퍼런스와 소유권 우회 의미는 관리자 파일 API를 참고하세요.

  • 파일 표: 저장된 파일마다 한 행씩, 파일 이름, 용도(purpose), 크기, 소유자(저장된 사용자/키 식별자, 소유자 기록이 없으면 -), 생성 시각을 보여 줍니다. 클라이언트 측 검색으로 파일 이름이나 소유자를 필터링하며, 파일 이름/크기/생성 열은 클릭하면 정렬됩니다. 하단 요약행에 총 개수와 누적 크기가 표시됩니다.
  • 다운로드: GET /admin/files/{id}/content에서 원본 바이트를 스트리밍합니다(구성 페이지의 파일 내보내기와 동일하게, 인증 헤더를 붙인 원시 fetch). Content-Disposition으로 저장된 파일 이름을 보존합니다.
  • 상세 정보: 파일 이름(또는 정보 아이콘)을 클릭하면 GET /admin/files/{id}를 다시 조회해 콘텐츠 타입, 소유자, 조직, 소스 IP를 포함한 저장된 모든 필드를 상세 모달로 보여 줍니다.
  • 삭제: 파일 이름을 명시하는 확인 모달을 거쳐 DELETE /admin/files/{id}를 호출하며, 성공 시 목록이 새로 고침됩니다.
  • 업로드: 파일 선택기와 용도 선택기(/v1/files가 허용하는 용도와 동일)를 갖춘 모달입니다. 전송 전에 설정된 max_file_size(목록 엔드포인트가 함께 보고)에 대해 클라이언트 측에서 크기를 확인하고, 서버가 그 한도를 최종적으로 강제합니다(한도를 초과하면 거부되고 서버 오류가 토스트로 표시됩니다). 브라우저 fetch API는 업로드 진행률을 노출하지 않으므로, 업로드 진행 상태는 단순한 무한 스피너로 표시합니다.

이 페이지는 일반적인 로딩/오류/빈 상태를 제공하며, 빈 상태는 파일 API가 무엇을 저장하는지 설명합니다. 다운로드, 삭제, 업로드는 콘텐츠 전용 작업입니다. 이 페이지는 파일 내용을 미리 보지 않으며, 시작 시 보존 정리(files.retention_days, 구성 페이지)를 편집하지 않습니다.

연동(Integrations)

Integrations 페이지는 라우터의 외부 연동 서브시스템, 즉 Continuum Hub 컨트롤 플레인 에이전트, ACP 에이전트, AppProxy 워커에 대한 읽기 전용 상태 화면입니다. 이들 중 적어도 하나가 컴파일되고 설정되어 있지 않으면 숨겨집니다. 내비게이션 항목은 배열 capability(features.control_plane, subsystems.acp, features.appproxy_router, features.appproxy_legacy)를 선언하며, 이는 OR 조건입니다. 즉 기본 빌드에서 이들 중 아무것도 활성화되지 않았다면 내비게이션 항목 자체가 표시되지 않습니다. 아래 각 카드는 자신의 기능 플래그로 추가로, 독립적으로 게이팅됩니다. 하나의 빌드가 둘 이상의 연동을 컴파일했더라도 그중 일부만 실제로 설정할 수 있기 때문입니다. 모든 카드는 헤더의 공유 자동 새로 고침 컨트롤(외관과 자동 새로 고침 참고) 아래에서 15초마다 자신의 엔드포인트를 폴링하며, 가져오기에 실패하면 카드를 통째로 없애는 대신 인라인 오류 행과 재시도 버튼을 보여 줍니다. 이 페이지의 어떤 것도 상태를 변경하지 않습니다. 컨트롤 플레인 동작(등록/등록 해제, 정책 수정)은 허브가 소유하며, ACP 세션 종료와 AppProxy 회로 관리는 이 페이지의 범위 밖입니다.

  • Continuum Hub (features.control_plane가 컴파일된 경우 표시) - GET /admin/control-plane/status를 읽습니다(src/control_plane/status_api.rs, control-plane 기능이 컴파일된 빌드에서만 마운트됨). control_plane.enabled: false이면 엔드포인트는 {"enabled": false}를 반환하고, 카드는 에이전트 데이터 대신 이 설정 스위치를 설명합니다. 그 외에는 상태 점(초록: 등록되었고 하트비트가 신선함, 노랑: 아직 등록 중이거나 하트비트가 보고된 간격의 3배(간격을 아직 모르면 5분)보다 오래됨, 빨강: 등록 또는 하트비트 오류가 있음)과 함께 등록된 라우터 ID 및 허브 호스트(자격 증명과 경로는 제거됨), 하트비트 최신성, 정책 동기화 상태(정책 동기화가 켜져 있으면 리비전과 키 테이블 크기), 사용량 전송 최신성과 대기 중인 큐 깊이, 배치 풀 점유율(배치 디스패치가 켜져 있으면 활성 작업 수/용량)을 보여 줍니다.
  • ACP 에이전트 (subsystems.acp가 true인 경우 표시) - GET /admin/acp/status에서 얻은 상태, 기능, 기본 모델. GET /admin/acp/sessions에서 얻은 확장 가능한 세션 표(세션 추적이 관리자 API에 연결되면 ID와 경과 시간을 표시하며, 현재 이 엔드포인트는 항상 세션 0개를 보고합니다). GET /admin/acp/agent.json의 접힌 원시 뷰.
  • AppProxy (features.appproxy_router 또는 features.appproxy_legacy가 컴파일된 경우 표시) - 모드 배지(ROUTER 또는 legacy)와 인증이 필요 없는 루트 GET /status 응답을 키/값 행으로 렌더링한 것(버전, authority, app mode, 프로토콜, 점유/가용 슬롯). /status/admin 아래에 마운트되지 않으므로 관리자 API가 아니라 직접 가져옵니다.

키보드 단축키와 내비게이션

WebUI는 키보드만으로 완전히 조작할 수 있습니다. 커맨드 팔레트와 몇 가지 단축키로 키보드에서 손을 떼지 않고 빠르게 이동할 수 있습니다. 텍스트 입력, textarea, select에 포커스가 있는 동안에는 모든 단축키가 일시 중단되므로 입력을 가로채지 않습니다.

커맨드 팔레트

어디서든 Ctrl+K(macOS는 Cmd+K)를 누르면 커맨드 팔레트가 열립니다. 볼 수 있는 모든 페이지(기능 게이팅 반영)에 이어 각 페이지가 제공하는 주요 동작("Create API key", "Add backend", "Refresh model catalog", "Flush response cache" 등)을 나열합니다. 입력하면 퍼지 부분 수열 방식으로 필터링되고, 방향키로 이동하며, Enter로 강조된 항목을 실행하고, Esc로 닫습니다. 페이지 동작을 실행하면 해당 페이지에 있지 않은 경우 먼저 그 페이지로 이동한 뒤 동작을 수행합니다. 헤더의 "Search" 버튼도 같은 팔레트를 엽니다.

각 페이지는 registerCommand({ id, title, keywords, page, run }) 훅으로 자신의 커맨드를 등록하고, 컴포넌트의 init()에서 registerPageActions(pageId, handlers)로 그 커맨드가 호출하는 동작을 노출합니다(destroy()에서 다시 해제). 페이지 추가를 참고하세요.

페이지 점프 코드

g를 누른 뒤 페이지의 문자를 누르면 곧바로 그 페이지로 이동합니다(gmail 방식의 2키 코드). 문자는 팔레트 하단과 치트 시트에 표시됩니다:

코드 페이지 코드 페이지
g d 대시보드 g a 캐시
g k API 키 g p 프롬프트
g u 사용량 g f 파일
g b 백엔드 g m 모델
g c 구성 g r 가드레일
g s 스마트 라우팅 g i 연동
g h 이력

기능 게이팅된 페이지의 코드는 그 페이지가 사용 가능할 때만 동작합니다.

치트 시트

?를 누르면 모든 단축키와 페이지 점프 코드를 나열하는 모달이 열립니다.

모달, 포커스, 스크린 리더

모든 대화 상자는 열려 있는 동안 포커스를 가두고, Esc로 닫히며, 닫힐 때 자신을 연 컨트롤로 포커스를 되돌립니다(role="dialog"/alertdialog, aria-modal, 레이블이 지정된 제목). 아이콘 전용 버튼에는 aria-label이 있고, 토스트는 스스로 알림을 냅니다(정보/성공은 role="status", 경고/오류는 role="alert"). 내비게이션 랜드마크(nav, main, header)가 갖춰져 있으며, 키보드 포커스에는 어디서나 눈에 보이는 포커스 링이 그려집니다. 헬스, 서킷, 등록 상태 표시는 색상에 텍스트 레이블이나 아이콘을 함께 두어 상태가 색상에만 의존하지 않도록 합니다.

외관과 자동 새로 고침

다크 모드

사이드바 하단에 auto / light / dark 테마 토글이 있습니다(사이드바가 아이콘 레일로 접히면 순환 방식의 단일 버튼). 선택은 localStoragewebui_theme 키에 저장됩니다:

  • Auto(기본) - 운영 체제의 prefers-color-scheme를 따르며 변경 시 실시간으로 반영됩니다.
  • Light, Dark - OS 설정과 무관하게 테마를 고정합니다.

테마는 첫 페인트 이전에 적용되므로(js/app.js에서 처리, 잘못된 테마가 번쩍이지 않음) Tailwind의 dark: 스타일과 손으로 작성한 스타일(스크롤바, 배지, 구성 diff)을 모두 구동합니다. 두 테마 모두에서 명도 대비를 점검했습니다.

전역 자동 새로 고침

여러 페이지가 타이머로 라우터를 폴링합니다(대시보드 5초, 캐시/스마트 라우팅/연동 15초, 사용량 30초). 헤더의 단일 자동 새로 고침 표시기가 폴링이 진행 중인지(맥동하는 녹색 점) 일시 중지되었는지 보여 주고, 툴팁에 현재 폴링 중인 페이지를 나열하며, 한곳에서 모든 페이지의 폴링을 일시 중지하거나 재개합니다. 일시 중지 상태는 localStoragewebui_refresh_paused 키에 저장됩니다. 페이지는 각자의 주기를 유지하고 표시기는 이를 게이팅만 하므로, 기존의 페이지별 자동 새로 고침 체크박스는 제거되었습니다.

반응형 레이아웃

레이아웃은 뷰포트에 맞춰 적응합니다. 노트북/데스크톱(>= 1024px)에서는 전체 사이드바, 태블릿(768-1023px)에서는 아이콘 레일(데스크톱에서는 접기 토글, 상태는 localStoragewebui_sidebar_collapsed 키에 저장), 휴대폰(< 768px)에서는 햄버거 드로어를 사용합니다. 표는 자체 컨테이너 안에서 가로로 스크롤되고, 통계 카드 그리드는 더 적은 열로 재배치되며, 가운데 정렬 모달은 640px 미만에서 전체 화면 시트가 됩니다. 그 결과 375, 768, 1280px에서 모든 페이지가 잘리는 컨트롤 없이, 그리고 페이지 가로 스크롤 없이 조작 가능합니다.

딥 링크와 알 수 없는 페이지

사용량과 구성 페이지는 새로 고침이나 공유 링크가 상태를 복원하도록 페이지별 상태 일부를 해시 쿼리에 유지합니다. 사용량 탭(#usage?tab=models)과 선택한 구성 섹션(#config?section=timeouts)이 그 예입니다. 어떤 페이지와도 일치하지 않는 해시로 이동하면(오래된 북마크, 오타) 빈 화면 대신 사용 가능한 페이지 링크가 있는 친절한 "Page not found" 패널을 보여 줍니다. 각 페이지는 그에 맞는 브라우저 탭 제목도 설정합니다(예: Continuum Router - Usage).

기능 정보와 기능 게이팅

라우터는 빌드 시점에 활성화된 Cargo 기능에 따라 서브시스템의 부분집합만 컴파일하며, 활성화된 각 옵션형 서브시스템도 해당 설정 섹션을 통해 런타임에 추가로 끌 수 있습니다. GET /admin/capabilities는 두 차원을 모두 보고하므로, WebUI는 실행 중인 바이너리와 설정이 실제로 지원하지 않는 기능의 페이지를 절대 렌더링하지 않습니다. 전체 응답 형식은 기능 정보 API 레퍼런스를 참고하세요.

셸이 이를 사용하는 방식:

  • 로드 시 셸은 /admin/capabilities를 한 번 가져와 Alpine capabilities 스토어에 결과를 캐시합니다.
  • 등록된 각 페이지는 capability 점(dot) 경로를 선언할 수 있습니다(예: "subsystems.guardrails" 또는 "features.control_plane"). 서로 독립된 여러 서브시스템이 뒷받침하는 페이지(예: Integrations 페이지의 ["features.control_plane", "subsystems.acp", "features.appproxy_router", "features.appproxy_legacy"])는 점 경로의 배열을 선언할 수도 있습니다. 배열은 OR로 해석되어, 하나라도 참으로 해석되면 페이지가 표시됩니다. 캐시된 응답에서 (배열이 아닌 경우) 해당 경로가, 또는 (배열인 경우) 모든 경로가 false로 해석되면 페이지는 사이드바에서 숨겨집니다.
  • 숨겨진 페이지로의 직접 해시 내비게이션(북마크, 공유 링크, 브라우저 뒤로/앞으로 가기)은 빈 페이지나 깨진 페이지를 렌더링하지 않습니다. 대신 페이지 이름과, 알 수 있는 경우 확인해야 할 설정 섹션을 명시하는 "기능 비활성화됨" 패널을 보여 줍니다.
  • 구성 적용, 섹션 저장, 가져오기, 롤백이 성공할 때마다 기능 정보 캐시가 자동으로 갱신되므로, 서브시스템의 enabled 플래그를 전환하면(예: guardrails.enabled) 브라우저를 새로 고치지 않아도 사이드바가 갱신됩니다.
  • /admin/capabilities 자체에 접근할 수 없으면 셸은 모든 기능을 사용 가능한 것으로 간주하고(안전 실패, fail open) 브라우저 콘솔에 경고를 남깁니다. 그래야 더 오래된 버전이나 잘못 구성된 라우터 앞에서 실행 중인 WebUI도 모든 것을 숨기는 대신 계속 동작합니다.

About 패널: 사이드바 하단에 라우터 버전과 "About" 버튼이 표시됩니다. 버튼을 열면 컴파일된 모든 기능과 런타임 서브시스템 전체 목록이 각각 켜짐/꺼짐 배지와 함께 표시되고, 수동으로 다시 확인할 수 있는 "Refresh capabilities" 버튼도 함께 제공됩니다.

기술 스택

WebUI는 다음으로 구축되었습니다:

  • Alpine.js - 인터랙티브 컴포넌트를 위한 경량 반응성. CDN이 아니라 js/vendor/alpine.min.js로 벤더링됩니다.
  • uPlot - 대시보드와 분석 페이지에서 쓰는 작고 빠른 차트 라이브러리. js/vendor/uplot.min.jscss/vendor/uplot.min.css로 벤더링됩니다.
  • Tailwind CSS - 유틸리티 우선 스타일링. 표준 Tailwind CLI로 생성한 사전 빌드 스타일시트(css/tailwind.css)를 저장소에 커밋해 사용합니다. 브라우저 런타임도, CDN도 없습니다.
  • Vanilla JavaScript - Alpine.js 외에 프레임워크 의존성 없음. 웹 폰트 없이 시스템 폰트 스택을 사용합니다.

모든 에셋은 벤더링되어 라우터 자체 출처에서 제공되므로, 네트워크를 끈 상태(에어갭)에서도 WebUI가 스타일과 인터랙션이 모두 살아 있는 상태로 로드됩니다. 고정된 버전과 원본 URL은 src/webui/assets/vendor/README.md에 기록되어 있습니다.

에셋은 rust-embed를 사용하여 임베드되며 다음과 함께 제공됩니다:

  • ETag 기반 캐싱 - 콘텐츠 주소 지정 ETag가 304 Not Modified 응답을 가능하게 함
  • Cache-Control 헤더 - HTML의 경우 no-cache (항상 재검증), JS/CSS의 경우 public, max-age=3600
  • 보안 헤더 - X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN
  • 콘텐츠 보안 정책 - self만 허용하며, 제3자 출처를 전혀 포함하지 않습니다 (아래 보안 고려 사항 참고)

보안 고려 사항

  • WebUI의 정적 에셋(HTML/CSS/JS)은 로그인 화면이 로드될 수 있도록 인증 없이 제공됩니다. UI에서 취하는 모든 동작은 Admin API를 거치며, admin.auth로 보호됩니다(인증 참고). 신뢰할 수 없는 네트워크에서 접근 가능한 라우터라면 반드시 실제 admin.auth.method를 구성하세요. 로컬 개발 환경에서는 none도 괜찮지만, 이 경우 WebUI는 계속 미보호 경고 배너를 표시해 알려줍니다.
  • path_prefix는 경로 탐색을 방지하기 위해 /로 시작해야 하며 ..을 포함해서는 안 됩니다.
  • 에셋 서버 내의 파일 경로도 제공하기 전에 탐색 시퀀스(.., null 바이트)에 대해 유효성을 검사합니다.
  • 콘텐츠 보안 정책은 제3자 출처를 전혀 포함하지 않습니다. 모든 에셋을 self에서 제공하므로 WebUI는 에어갭 환경에서도 동작합니다.
  • X-Frame-Options: SAMEORIGIN은 교차 출처 프레임에서의 클릭재킹을 방지합니다.

콘텐츠 보안 정책

index.html 응답은 다음 정책을 설정합니다:

default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'self'

CDN, 폰트 호스트를 비롯한 어떤 원격 출처도 등장하지 않습니다. img-src는 (인라인 SVG 데이터를 위해) data: URI를 허용하고, 나머지는 모두 라우터 자체 출처로 제한됩니다.

두 가지 완화가 남아 있으며, 둘 다 자체 출처 범위 안에서만 유효합니다:

  • script-src 'unsafe-eval' - 표준 Alpine.js 빌드는 반응형 표현식(x-text, @click 등)을 Function 생성자로 컴파일합니다. Alpine CSP 빌드(@alpinejs/csp)는 이 요구를 없애지만, 템플릿의 인라인 표현식을 금지하므로 모든 페이지의 바인딩을 다시 작성해야 합니다. 이 재작성은 벤더링/기반 작업의 범위를 벗어나므로 'unsafe-eval'을 문서화된 트레이드오프로 유지합니다. script-src는 여전히 'self'로 제한되므로 원격 코드 로딩은 결코 허용되지 않습니다.
  • style-src 'unsafe-inline' - Alpine이 인라인 스타일(x-show, x-transition)로 요소 표시 여부를 전환합니다.

개발

WebUI는 번들러가 없고 cargo build에 Node.js 빌드 단계가 필요 없는 모듈형 단일 페이지 앱입니다. 에셋은 src/webui/assets/ 아래에 있으며 rust-embed로 임베드됩니다.

에셋 구성

src/webui/assets/
├── index.html              # 셸: 로그인 게이트, 사이드바, 헤더, 토스트 컨테이너, 페이지 마운트 지점
├── css/
│   ├── tailwind.css        # 생성물, 커밋됨 (아래 참고)
│   ├── style.css           # 손수 작성한 오버라이드
│   └── vendor/uplot.min.css
├── js/
│   ├── app.js              # 셸: 페이지 레지스트리, 라우터, adminFetch, 스토어, 유틸
│   ├── auth.js             # 로그인 게이트: 인증 프로브, `auth`/`login` 스토어, 자격 증명 저장
│   ├── vendor/alpine.min.js
│   ├── vendor/uplot.min.js
│   └── pages/              # 페이지당 파일 하나 (컴포넌트 팩토리 + registerPage)
│       ├── dashboard.js
│       ├── apikeys.js
│       ├── backends.js
│       ├── config.js
│       ├── guardrails.js
│       ├── history.js
│       ├── caches.js
│       ├── smart-routing.js
│       ├── prompts.js
│       └── models.js
├── partials/               # 페이지당 HTML 조각 하나
│   ├── dashboard.html
│   ├── apikeys.html
│   ├── backends.html
│   ├── config.html
│   ├── guardrails.html
│   ├── history.html
│   ├── caches.html
│   ├── smart-routing.html
│   ├── prompts.html
│   └── models.html
└── vendor/README.md        # 고정된 버전과 원본 URL

페이지 레지스트리

js/app.jsregisterPage({ id, title, navLabel, icon, order, capability, componentFactory })를 노출합니다. 각 js/pages/<page>.js 파일이 로드 시점에 이를 호출합니다. 셸은 레지스트리(order 기준 정렬)에서 사이드바를 구성하고 페이지마다 Alpine 컴포넌트를 하나씩 등록하므로, 병렬 작업이 공용 내비게이션 마크업을 건드릴 일이 없습니다.

페이지로 처음 이동하면 셸이 partials/<id>.html을 가져와 페이지 마운트 지점에 주입하고 해당 페이지의 Alpine 서브트리를 초기화합니다. 가져온 조각은 메모리에 캐시되며, 가져오기 실패 시 재시도 버튼이 있는 오류 화면을 보여 줍니다. 없는 조각은 실제 404를 반환합니다(셸로 조용히 대체되지 않습니다).

capability 필드는 GET /admin/capabilities 응답으로 향하는 점(dot) 경로이거나(예: "subsystems.guardrails"), 서로 독립된 여러 서브시스템이 뒷받침하는 페이지를 위한 점 경로의 배열입니다(예: Integrations 페이지의 ["features.control_plane", "subsystems.acp", "features.appproxy_router", "features.appproxy_legacy"]). 배열은 OR로 해석되어, 하나라도 참으로 해석되면 페이지가 표시됩니다. 이 값이 어떻게 해석되는지는 아래 기능 정보와 기능 게이팅을 참고하세요.

페이지 추가

  1. 컴포넌트 팩토리와 registerPage({ id: '<id>', title: '...', order: N, icon: '<svg>...</svg>', componentFactory: myFactory }) 호출이 담긴 js/pages/<id>.js를 만듭니다.
  2. 루트 요소가 컴포넌트에 바인딩되는 partials/<id>.html을 만듭니다: <div x-data="<id>">...</div>.
  3. index.html<script src="../js/pages/<id>.js"></script> 태그를 하나 추가합니다.

사이드바 마크업, 라우터, 다른 페이지는 변경할 필요가 없습니다.

Tailwind 스타일시트 재생성

css/tailwind.css는 표준 Tailwind CLI가 생성한 사전 빌드 스타일시트로, 템플릿과 페이지 스크립트를 훑어 실제로 쓰인 유틸리티 클래스만 담습니다. 커밋되어 있으므로 cargo build와 CI에는 Node.js 도구 체인도 네트워크도 필요 없습니다.

파일에 아직 없는 Tailwind 클래스를 도입하는 방향으로 템플릿이나 페이지 스크립트를 편집한 뒤에는 다음으로 재생성합니다:

make webui-css

이 타깃은 처음 실행할 때 고정된 표준 Tailwind CLI(v4.1.11)를 .cache/tailwindcss에 내려받거나, TAILWINDCSS_BIN으로 로컬 바이너리를 재사용합니다. CLI 버전과 입력이 고정되어 있어, 템플릿이 바뀌지 않았다면 재생성해도 diff가 발생하지 않습니다. Tailwind 소스는 src/webui/tailwind.input.css에 있으며(제공되거나 임베드되지 않습니다) @source 글롭과 테마를 선언합니다. 추가한 클래스가 적용되지 않는다면 대개 make webui-css를 다시 실행하지 않았기 때문입니다.

WebUI 비활성화

브라우저 인터페이스가 필요하지 않은 경우 공격 표면을 줄이기 위해 비활성화하세요:

webui:
  enabled: false

비활성화되면 구성된 path_prefix에 대한 모든 요청은 404 Not Found를 반환합니다.

사용자 정의 경로 접두사

다른 경로에서 WebUI를 제공하려면 (예: 역방향 프록시 뒤에서):

webui:
  enabled: true
  path_prefix: /admin/ui

그러면 WebUI는 다음 주소에서 사용할 수 있습니다:

http://localhost:8080/admin/ui/

역방향 프록시가 이 접두사에 대한 요청을 올바르게 전달하는지 확인하세요:

location /admin/ui/ {
    proxy_pass http://localhost:8080/admin/ui/;
}