jupiq
요약. jupiq: 자율 개선 회차 16회, 릴리즈 9건. 최근 릴리즈 v1.6.0 (자산 1개). 건강 B
- 16회차
- 1프로젝트
- 8배포 준비 완료
- 1릴리즈 진행 중
- 2병합 완료
- 3검토 대기
- 0검증 실패
- 1변경 없음
- 1실행 오류
- $58.38비용
- 2시간 9분에이전트 시간
현황
- 저장소
- https://github.com/hkjang/jupiq
- 마지막 회차
- 2026-09-12 18:58 KST — 🚀 릴리즈 merged PR #13, released v1.6.0
- 최근 릴리즈
- v1.6.0 — released · 자산 1개 (이전 v1.5.0: 1개) 전체 릴리즈 →
회차 이력
| 일시 | 프로젝트 | 결과 |
|---|---|---|
| 2026-09-12 18:58 | jupiq | 배포 준비 완료 merged PR #13, released v1.6.0 |
| 2026-09-11 04:56 | jupiq | 검토 대기 review held, PR open PR #12 |
| 2026-09-10 20:08 | jupiq | 배포 준비 완료 merged PR #11, released v1.4.13 |
| 2026-09-10 14:09 | jupiq | 배포 준비 완료 merged PR #10, released v1.4.12 |
| 2026-09-09 15:49 | jupiq | 검토 대기 review held, PR open PR #9 |
| 2026-09-09 15:41 | jupiq | 실행 오류 hold: budget |
| 2026-09-09 08:35 | jupiq | 배포 준비 완료 merged PR #8, released v1.4.10 |
| 2026-09-09 01:28 | jupiq | 배포 준비 완료 merged PR #7, released v1.4.9 |
| 2026-09-08 20:37 | jupiq | 배포 준비 완료 merged PR #6, released v1.4.8 |
| 2026-09-07 10:02 | jupiq | 병합 완료 release-only, release skipped |
| 2026-09-07 01:59 | jupiq | 검토 대기 review held, PR open PR #5 |
| 2026-09-06 16:00 | jupiq | 병합 완료 merged PR #4, release blocked (secrets) |
| 2026-09-05 04:17 | jupiq | 배포 준비 완료 merged PR #3, released v1.4.4 |
| 2026-09-04 08:40 | jupiq | 릴리즈 진행 중 merged PR #2, released v1.4.0, ASSETS MISSING |
| 2026-09-03 23:04 | jupiq | 변경 없음 no change |
| 2026-09-03 14:58 | jupiq | 배포 준비 완료 merged PR #1, released v1.3.0 |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 18:52 | jupiq | 릴리즈 | 2분 | 11 | $0.86 | 511K / 7K | success |
| 18:49 | jupiq | review | 4분 | 20 | $1.72 | 1.3M / 13K | success |
| 18:45 | jupiq | 개선 | 22분 | 97 | $11.10 | 14.0M / 81K | success |
| 16:59 | jupiq | 릴리즈 | 2분 | 16 | $0.90 | 688K / 6K | success |
| 04:56 | jupiq | review | 6분 | 47 | $3.23 | 3.3M / 25K | success |
| 04:50 | jupiq | 개선 | 18분 | 131 | $11.11 | 14.7M / 63K | success |
| 20:02 | jupiq | 릴리즈 | 2분 | 21 | $0.86 | 744K / 6K | success |
| 19:59 | jupiq | review | 2분 | 13 | $0.67 | 296K / 10K | success |
| 19:56 | jupiq | 개선 | 6분 | 42 | $2.48 | 2.3M / 23K | success |
| 14:03 | jupiq | 릴리즈 | 2분 | 22 | $0.99 | 885K / 6K | success |
| 13:59 | jupiq | review | 2분 | 12 | $0.60 | 331K / 9K | success |
| 13:57 | jupiq | 개선 | 6분 | 50 | $3.56 | 3.8M / 24K | success |
| 11:05 | jupiq | 릴리즈 | 2분 | 22 | $0.90 | 692K / 7K | success |
| 15:48 | jupiq | 개선 | 7분 | 48 | $3.12 | 2.9M / 31K | success |
| 08:31 | jupiq | 릴리즈 | 2분 | 19 | $0.82 | 655K / 6K | success |
| 08:27 | jupiq | review | 1분 | 9 | $0.38 | 162K / 5K | success |
| 08:25 | jupiq | 개선 | 5분 | 45 | $2.07 | 2.0M / 19K | success |
| 01:21 | jupiq | 릴리즈 | 2분 | 19 | $0.79 | 605K / 6K | success |
| 01:16 | jupiq | review | 2분 | 15 | $0.57 | 276K / 7K | success |
| 01:14 | jupiq | 개선 | 4분 | 39 | $1.56 | 1.4M / 15K | success |
| 20:31 | jupiq | 릴리즈 | 2분 | 17 | $0.86 | 673K / 5K | success |
| 20:27 | jupiq | review | 1분 | 8 | $0.39 | 202K / 4K | success |
| 20:25 | jupiq | 개선 | 5분 | 29 | $1.50 | 1.2M / 17K | success |
| 10:02 | jupiq | 릴리즈 | 1분 | 11 | $0.55 | 274K / 5K | success |
| 01:58 | jupiq | review | 4분 | 14 | $0.99 | 540K / 14K | success |
| 01:55 | jupiq | 개선 | 5분 | 34 | $1.71 | 1.4M / 19K | success |
| 16:00 | jupiq | 릴리즈 | 2분 | 21 | $0.90 | 756K / 5K | success |
| 15:58 | jupiq | review | 2분 | 18 | $0.68 | 400K / 8K | success |
| 15:55 | jupiq | 개선 | 6분 | 44 | $2.50 | 2.4M / 21K | success |
아이디어 백로그 — 대기 10 / 전체 11
| 아이디어 | 가치/위험/크기 | 상태 | 메모 | 갱신 |
|---|---|---|---|---|
| internal/store 커버리지 중 DB 없이 테스트 가능한 순수 함수 경로 보강 | 3/1/M | 대기 | settings.go의 buildLiveHubCards·gpuWasteAssessment·sessionSnapshotStale·topLiveUsers·summarizeLLMItems·buildLLMBreakdown·observedSuccessRate, metrics.go의 cloneMetricLabels·metricSafeLabels·finiteNonNegative·roundedMetricCount·podPhasePriority·selectPodCandidate·firstLabel, hubs.go의 managedUserRoles·safeJupyterServerSnapshot·numericJSONValue. 한 세션에 3~4개. | 2026-09-12 |
| 방문 추적 탭·차단 출처 카드 화면 캡처를 manifest에 추가하고 관리자 가이드 3.5절에 싣기 | 3/1/S | 대기 | 이번 회차는 Playwright 캡처 비용 때문에 생략. 캡처 조건은 analytics 설정 PUT(enabled, momento, 프록시) 한 번과 csp-report POST 한 번이면 카드에 항목이 보인다. 새 항목이 manifest·갤러리·홈 카운트(30→32)에 함께 들어가야 check-screenshots가 통과한다. | 2026-09-12 |
| 로그인 리미터 succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 | 2/1/S | 대기 | login_limiter.go:89-94의 의도가 코드에 없어 '버그'로 오해될 위험. login_limiter_test.go 케이스 하나와 주석 한 줄. 관리자 가이드 7.3절 임계값 명시는 완료. | 2026-09-12 |
| internal/api 통합 테스트를 로컬에서 돌리는 방법 문서화와 make test-integration 타깃 | 2/1/S | 대기 | JUPIQ_INTEGRATION_TEST_DSN 없으면 조용히 skip. postgres:16-alpine 컨테이너 + DSN 한 줄과 Makefile 타깃. 이번 회차에도 같은 방식으로 컨테이너를 띄워 검증했으므로 절차는 재확인됨. | 2026-09-12 |
| auth/oidc.go가 RandomToken 오류를 무시하고 state·nonce·verifier를 만드는 부분 정리 | 2/1/S | 대기 | oidc.go:109-111의 `_ :=`. 현재 도달 불가능하지만 오류를 올려 흐름을 중단시키는 편이 계약상 명확. 이번 회차의 trackedPage는 같은 상황에서 추적 없이 응답하도록 처리했다. | 2026-09-12 |
| 감사 로그 캡처가 캡처 스크립트 자신의 settings.update로 가득 차는 문제 | 2/1/S | 대기 | capture-screenshots.mjs가 화면마다 PUT /settings를 하므로 audit.webp가 단조로움. 캡처 직전 무해한 작업을 섞거나 데모 fallback. | 2026-09-12 |
| queryInt가 잘못된 page·page_size·limit을 조용히 기본값으로 바꾸는 부분을 400으로 돌려주기 | 2/2/S | 대기 | helpers.go의 queryInt. 프런트엔드가 빈 문자열을 보내는 경로가 있으면 회귀이므로 web/src의 쿼리 조립을 먼저 확인. | 2026-09-12 |
| 쿼리로만 GPU로 분류되는 별칭 지표가 feature_enabled 없이 빈 목록을 받는 잔여 간극 | 2/2/M | 대기 | 정확히 고치려면 kind 필터로 걸러진 행의 존재를 알아야 해 조회가 하나 더 필요. 기본 지표 이름은 이미 덮이므로 비용 대비 가치를 먼저 따질 것. | 2026-09-12 |
| 가이드 캡처를 릴리스마다 자동으로 다시 찍는 절차 | 2/2/M | 대기 | 캡처 시점 버전을 manifest에 기록하고 check-screenshots가 VERSION과 비교해 경고하는 수준부터. | 2026-09-12 |
| analyticsConfig가 index.html 요청마다 settings 한 행을 조회하는 것을 짧은 TTL 캐시로 줄이기 | 2/2/S | 대기 | analytics_handlers.go의 analyticsConfig. 현재는 단일 PK 조회라 부담이 작고 설정 저장 즉시 반영이 장점이므로, 페이지 요청량이 문제가 될 때만. liveSnapshot 캐시와 같은 invalidate 패턴을 쓰면 즉시 반영을 유지할 수 있다. | 2026-09-12 |
| 관리자가 화면에서 방문 추적 스크립트를 붙일 수 있는 체계(캠페인 tracking-2026-09) | 4/2/M | 완료 | internal/analytics 신설, serveSPA nonce 주입과 정책 조립, momento 같은 오리진 프록시 기본, CSP 신고 수신·차단 출처 목록, 관리자 설정 '방문 추적' 탭, 설정 검증, OpenAPI, 관리자 가이드 3.5절과 PDF. 기본 꺼짐. 실제 기동해 curl로 전 흐름 확인. | 2026-09-12 |
원장 (에이전트가 남긴 기록)
2026-09-03
- 선택: OpenAPI 문서와 등록 경로 계약 검증 테스트 추가 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약:
register*함수 시그니처를*http.ServeMux에서 최소router인터페이스로 바꿔 테스트가 등록 경로를 수집할 수 있게 하고,internal/api/openapi_contract_test.go에서 openapi.yaml의 paths와 양방향(경로→문서, 문서→경로)으로 대조하도록 했다. probe·별칭·항상 405인 승인 쓰기 경로는 이유를 적은 예외 목록으로 관리하며, 이 검증으로 드러난 누락GET /auth/oidc/callback을 문서에 추가했다. 임시로 가짜 경로를 등록해 테스트가 실제로 드리프트를 잡는지 확인했고,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(16파일 49개) 모두 통과했다. - 보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(boundedTimeRange로 대체됨) /internal/collector커버리지 8.5% 보강 /serveSPA의 해시 자산에 장기 Cache-Control 부여 / 로그인 리미터succeeded가 ip 키를 정리하지 않는 동작에 대한 테스트·문서화 - 릴리즈: v1.3.0 (2026-09-03)
2026-09-04
- 선택: SPA 정적 자산 캐시 정책과 serveSPA 테스트 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
serveSPA가 정적 파일에 Cache-Control을 전혀 붙이지 않아 브라우저 heuristic 캐시에 맡겨져 있었다. Vite가 content hash를 붙여 내보내는/assets/*는public, max-age=31536000, immutable로, public/에서 이름 그대로 복사되는 favicon 같은 파일은public, max-age=0, must-revalidate로 응답하게 하고(index.html은 기존no-store유지) 근거를 주석과 README에 남겼다. 지금까지 테스트가 없던serveSPA에 대해 캐시 헤더·/api·/mcpJSON 404·dist 밖 경로 차단·빌드 산출물 부재 404를 덮는internal/api/spa_test.go를t.Chdir기반으로 추가했고,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(16파일 49개) 모두 통과했다. - 보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(boundedTimeRange로 대체됨) /internal/collector커버리지 8.5% 보강 / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /Collector.prune이 실패해도lastPrune을 갱신해 24시간 재시도하지 않는 문제 수정 - 릴리즈: v1.4.0 (2026-09-04)
2026-09-05
- 선택: 보존 정책 prune 실패 재시도와 설정 읽기 실패 시 삭제 보류 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
Collector.prune이 시도 시각(lastPrune)을 먼저 기록해PruneMetrics가 실패해도 24시간 동안 재시도하지 않던 문제를 고쳐, 실패한 주기는pruneRetryInterval(30분) 뒤 재시도하고 성공하면 하루 주기로 복귀하도록pruneDue/pruneFailed로 분리했다. 함께 무시되던 retention 설정 읽기 오류도 처리해,ErrNotFound가 아닌 오류로 설정을 알 수 없을 때는PruneMetrics의 기본값 30일을 적용해 운영자가 더 길게 보관하도록 설정한 샘플을 지우는 대신 삭제를 건너뛰고 재시도하게 했다. 새 순수 함수 기반 테스트 2개(TestPruneRetriesSoonAfterFailure,TestRetentionReadableOnlyToleratesMissingSettings)를 추가해 collector 커버리지가 8.5%→13.9%로 올랐고,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 58개) 모두 통과했다. - 보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(boundedTimeRange로 대체됨) /internal/collector의collectPrometheus·collectKubernetes경로 커버리지 추가 보강 / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /collectHubs가 goroutine을 제한 없이 띄우는 부분에 동시성 상한 도입 - 릴리즈: v1.4.4 (2026-09-05)
2026-09-06
- 선택: 응답 보안 헤더 보강(CSP 지시자 추가 + TLS 한정 HSTS)과 middleware 테스트 신설 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
internal/api/middleware.go의 CSP가default-src로는 대체되지 않는frame-ancestors·base-uri·form-action·object-src를 지정하지 않아 clickjacking과 주입된<base>·<form>을 통한 외부 전송이 열려 있었고, HSTS는 전혀 없었다. 네 지시자를 추가하고 평문 폐쇄망 배포에서 접속이 영구히 막히지 않도록auth.IsSecureRequest가 참인 요청에만max-age=31536000(includeSubDomains·preload 없음)을 붙이도록setSecurityHeaders로 분리했다. 그동안 테스트가 하나도 없던 middleware에 보안 헤더·HSTS 조건·동일 출처 변경 요청 거부(Origin 호스트/스킴, Sec-Fetch-Site, GET 예외)·요청 ID 생성과 에코·panic 복구를 덮는middleware_test.go(5개 테스트)를 추가했고,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 58개) 모두 통과했다. - 보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(boundedTimeRange로 대체됨) /internal/secure의EncryptString·DecryptString·Derive·RandomToken테스트 공백 보강(현재 0%) /collectHubs가 goroutine을 제한 없이 띄우고 종료 시 기다리지 않는 부분에 동시성 상한과 대기 도입 /collectPrometheus가 metric마다 features 설정을 다시 읽는 중복 조회 제거
2026-09-07
- 선택: 허브 수집 goroutine 동시 실행 상한과 종료 대기 도입 (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
collectHubs가 수집 대상 허브 수만큼 goroutine을 제한 없이 띄우고 아무도 기다리지 않아, 허브가 많으면 30초 주기마다 그만큼의 아웃바운드 HTTP·DB 연결이 동시에 열리고 종료 시에는main의defer database.Close()가 진행 중인 상태 쓰기 아래에서 풀을 닫아 버렸다.updateHubHealth가context.WithoutCancel로 쓰기 컨텍스트를 분리해 둔 의도가 무산되던 지점이다.goHub(용량 8 세마포어, 취소된 컨텍스트면 대기 중인 프로브를 버림)와waitForHubs(10초 상한 대기)를 추가하고Run이 반환할 때만 대기하도록 해 kubernetes·prometheus 수집이 주기마다 지연되지 않게 했으며,main은 HTTP 종료 후 collector 종료를 기다린 뒤 DB를 닫는다. 동시 실행 상한·종료 대기·취소 시 대기열 폐기를 덮는 테스트 3개를 추가해 collector 커버리지가 13.9%→18.7%로 올랐고, 세마포어를 제거한 변형에서 테스트가 실제로 실패하는지 확인했다.go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 58개) 모두 통과했다. - 보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(boundedTimeRange로 대체됨) /internal/secure의EncryptString·DecryptString·Derive·RandomToken테스트 공백 보강(현재 39.5%) /collectPrometheus가 metric마다 features 설정을 다시 읽는 중복 조회 제거 / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /collectHubs의due가 프로브 성공 여부와 무관하게 시각을 선기록해 실패한 허브가 전체 간격만큼 재시도되지 않는 문제 검토
2026-09-08
- 선택: internal/secure의 문자열 암복호화·파생키·토큰 생성 테스트 공백 보강 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
EncryptString·DecryptString·Derive·RandomToken·HashToken은 OIDC state 쿠키, 허브 자격증명, JWT 서명키, API 키 해시가 모두 의존하는데 테스트가 라운드트립 하나뿐이었다. 잘못된 base64·표준 패딩·nonce보다 짧은 blob·변조·절단 ciphertext를DecryptString이 모두 거부하고 오류 시 빈 문자열을 돌려주는지,Derive가 label과 키로 분리되며 결정적인지,RandomToken이 요청한 바이트 수를 그대로 디코딩하고 재사용되지 않는지 덮는 테스트 6개를 추가해 커버리지가 39.5%→87.5%로 올랐다. 겸사겸사RandomToken이 크기 0 이하에 빈 문자열을 조용히 돌려주던 계약을 오류로 바꿔(현재 호출자는 모두 상수라 동작 변화 없음) 나중에 크기를 계산해 넘기는 호출자가 빈 state·nonce를 비밀값으로 쓰지 못하게 했다. 1바이트 토큰 유일성 검사는 256개 값에서 16회 추출 시 ~37% 확률로 충돌해 플레이키하므로 12바이트 이상에만 적용했고,-count=20으로 반복 확인했다.go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(호출자 없음을 이번에도 재확인) /collectPrometheus가 metric마다featureEnabled로 features 설정을 다시 읽는 중복 조회 제거 /internal/config의Load()테스트 신설(현재ParseEncryptionKey만 덮여 있고 필수 환경변수 누락 집계·비밀번호 최소 길이·오류 우선순위는 미검증) / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 - 릴리즈: v1.4.8 (2026-09-08, run 2026-09-08-202139-jupiq-improve)
2026-09-09
- 선택: 큰 페이지 번호가 OFFSET을 음수로 뒤집지 않도록 pageBounds 제한 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
queryInt가 쿼리스트링의page를 상한 없이 그대로 넘겨store.pageBounds의(page-1)*pageSize가 int를 넘겨 음수 OFFSET으로 감쌌고, PostgreSQL이 이를 거부해 인증된 사용자가?page=99999999999999999하나로 목록·감사로그·사용자 상세 API를 500으로 만들 수 있었다.page를 offset이 표현 가능한 마지막 페이지로 잘라(실제 테이블 끝을 지난 값이라 빈 페이지가 정직한 답) 모든pageBounds호출자와 자체적으로 offset을 다시 계산하는user_detail.go경로까지 한 번에 막았고, 지금까지 테스트가 없던 이 순수 함수에 클램프·오버플로 방지·경계 페이지 보존을 덮는 테스트 3개를 추가했다. 클램프를 빼면 두 테스트가 실제로 실패하는 것을 확인했으며go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어:
internal/api/helpers.go의 사용되지 않는parseTimeQuery제거(이번에도 호출자 없음 재확인) /collectPrometheus가 metric마다featureEnabled로 features 설정을 다시 읽는 중복 조회 제거 /internal/config의Load()테스트 신설(현재 커버리지 23.1%, 필수 환경변수 누락 집계·비밀번호 최소 길이·오류 우선순위 미검증) / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 - 릴리즈: v1.4.9 (2026-09-09, run 2026-09-09-011109-jupiq-improve)
2026-09-09
- 선택: 기동 설정 오류 일괄 보고와 config.Load() 테스트 신설 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
config.Load()는 누락 환경변수만 모아 보고하고ENCRYPTION_KEY파싱 실패·BOOTSTRAP_ADMIN_PASSWORD최소 길이는 하나씩 순차 반환해, 운영자가 첫 기동에서 문제마다 컨테이너를 다시 띄워야 알 수 있었다. 세 종류를 하나의 오류로 합쳐 한 번에 고칠 수 있게 하고(값이 비어 있으면 이미 누락 목록에 있으므로 중복 보고하지 않음), 테스트가ParseEncryptionKey하나뿐이던 이 패키지에 트리밍 규칙(비밀번호는 의도적으로 미트리밍)·누락 4개 동시 보고·12자 경계·거부 시 빈 Config 반환·미사용 값 동시 보고를 덮는 테스트 5개를 추가해 커버리지가 23.1%→100%로 올랐다. 집계를 되돌린 변형에서 새 테스트가 실제로 실패하는 것을 확인했고, README의 환경변수 표에 최소 12자 조건을 명시했다. 겸사겸사 다섯 회차 동안 보류돼 있던 호출자 없는parseTimeQuery를 별도 커밋으로 제거했다(time임포트도 함께 정리).go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어:
collectPrometheus가 metric마다featureEnabled로 features 설정을 다시 읽는 중복 조회 제거 //api/v1/metrics의 GPU 게이트가store.IsGPUMetricName과 달리cuda·nvidia를 빼먹어 응답 모양(feature_enabled)이 달라지는 불일치 정리 / 로그인 리미터succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /internal/store커버리지 16.7% 중 DB 없이 테스트 가능한 순수 함수 경로 보강 - 릴리즈: v1.4.10 (2026-09-09, run 2026-09-09-082108-jupiq-improve)
2026-09-09
- 선택: GPU 기능 게이트를 store 판정으로 일원화하고 수집 주기의 중복 설정 조회 제거 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
/api/v1/metrics핸들러가gpu·vram·dcgm세 substring만 자체 목록으로 검사해store.IsGPUMetricName이 함께 보는cuda·nvidia별칭이 게이트를 지나쳤고, 데이터는 store 필터가 막았지만 응답에서feature_enabled가 빠져 소비자가 ‘표본 없음’과 ‘기능 꺼짐’을 구분할 수 없었다.Store.Metrics가 게이트 적용 여부를 함께 돌려주게 해 판정을 한 곳에 모으고 핸들러의 중복 설정 조회도 없앴으며, 판정을 순수 함수GPUFeatureBlocked로 분리해 단위 테스트로 덮고 실제 PostgreSQL에서 다섯 별칭이 모두feature_enabled=false를 받는지 확인하는 통합 테스트를 추가했다(수정 전 코드에서cuda_cores·nvidia_smi_temperature로 실제 실패하는 것을 확인). 이어서collectPrometheus가 지표마다featureEnabled로 설정을 다시 읽던 부분을 주기당 스냅샷 하나로 바꿔(쓰기 시점의 권위 있는 게이트는 store가 트랜잭션 안에서 확인한다) 순수 함수prometheusQueries로 분리하고 테스트를 추가했다. 임시 postgres:16-alpine 컨테이너를 띄워go vet ./...,go test -race ./..., 통합 테스트(-run Integration ./internal/store ./internal/api),scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어: 로그인 리미터
succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /internal/store커버리지 16.7% 중 DB 없이 테스트 가능한 순수 함수 경로 보강 / API 계층에서page·page_size파싱 실패를 400으로 돌려주기 /auth/oidc.go가RandomToken오류를 무시하고 state·nonce·verifier를 만드는 부분 정리 / 쿼리로만 GPU로 분류되는 별칭 지표는 여전히feature_enabled없이 빈 목록을 받는 잔여 간극 검토 - 릴리즈: v1.4.11 (2026-09-10, run 2026-09-10-110255-jupiq-approve)
2026-09-10
- 선택: 오류 메시지 자르기가 한글 문자를 쪼개지 않게 수정 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
store.truncate(1000바이트)와integration.safeConnectionError(500바이트)가 바이트 단위로 슬라이스해, 한글 오류 메시지가 상한을 넘으면 자르는 지점이 UTF-8 시퀀스 중간에 떨어졌다. 앞의 값은hubs.last_error·integration_health.last_error에 그대로 들어가는데 PostgreSQL이 잘못된 바이트 시퀀스를 거부하므로 실패를 기록하려는 UPDATE 자체가 실패해 망가진 Hub가 계속 healthy로 남고, 뒤의 값은 연결 검증 Drawer에 그대로 보이므로 JSON 인코더가 U+FFFD로 바꿔 관리자에게 깨진 글자가 보인다. 두 곳 모두 룬 경계까지 되돌아가 자르고 애초에 유효하지 않은 입력은 대체 문자로 치환하도록 고쳤으며, 상한 근처 모든 오프셋에서 결과가 유효한 UTF-8이고 원본의 접두사이며 한 룬(3바이트) 넘게 버리지 않는지 검증하는 테스트 4개를 추가했다. 수정 전 코드로 되돌린 변형에서 새 테스트가 실제로 실패하는 것을 확인했고,gofmt -l,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어: 로그인 리미터
succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /internal/store커버리지 중 DB 없이 테스트 가능한 순수 함수 경로 보강 /auth/oidc.go가RandomToken오류를 무시하고 state·nonce·verifier를 만드는 부분 정리 /internal/api통합 테스트를 로컬에서 돌리는 방법 문서화와make test-integration타깃 / 쿼리로만 GPU로 분류되는 별칭 지표가feature_enabled없이 빈 목록을 받는 잔여 간극 - 릴리즈: v1.4.12 (2026-09-10, run 2026-09-10-135106-jupiq-improve)
2026-09-10
- 선택: 연동 요청 경로의 이중 percent 인코딩 수정 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
integration.joinURL이 호출자가 이미url.PathEscape로 세그먼트를 이스케이프해 넘긴 경로를URL.Path(디코딩된 값을 담는 필드)에 그대로 넣어,URL.String()이%를 한 번 더 이스케이프했다. 그 결과 JupyterHub 서버 시작·정지·재시작 요청이홍길동을%25ED%2599%258D…로,john doe를john%2520doe로 보내 존재하지 않는 사용자 경로를 때렸고(한국어 사용자명·공백 포함 사용자명·이름 있는 서버가 모두 영향), Kubernetes namespace 경로에도 같은 잠재 결함이 있었다. 이스케이프된 경로를RawPath에, 디코딩한 값을Path에 넣어 호출자의 인코딩이 그대로 나가게 고치고, 경로 조립을 검증할 수 있도록ServerAction의 경로 계산을serverEndpoint로 분리했다. 단일 인코딩·기본 URL의 경로 접두사 보존과 쿼리 제거·이스케이프된%2F가RequestURI에서 구분자로 풀리지 않는지(경로 탈출 방지 유지)를 덮는 테스트 4개를 추가했고, 수정 전 코드로 되돌린 변형에서 3개가 실제로 실패하는 것을 확인했다.gofmt -l,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했다. -
보류 아이디어: 로그인 리미터
succeeded가 ip 키를 의도적으로 유지하는 동작에 대한 테스트·문서화 /internal/store커버리지 중 DB 없이 테스트 가능한 순수 함수 경로 보강 /internal/api통합 테스트를 로컬에서 돌리는 방법 문서화와make test-integration타깃 /auth/oidc.go가RandomToken오류를 무시하고 state·nonce·verifier를 만드는 부분 정리 / 쿼리로만 GPU로 분류되는 별칭 지표가feature_enabled없이 빈 목록을 받는 잔여 간극 /queryInt가 잘못된page·page_size·limit을 조용히 기본값으로 바꾸는 부분을 400으로 돌려주기 - 릴리즈: v1.4.13 (2026-09-10, run 2026-09-10-195118-jupiq-improve)
2026-09-11
- 선택: 화면 캡처가 들어간 사용자·관리자 가이드 정본 작성(캠페인 guides-2026-09) (가치 4 / 위험 1 / 작업량 L)
- 결과: 성공
- 요약: 가이드가 GitHub Pages HTML 두 장뿐이고 저장소 안에 정본이 없어, GUIDE-STANDARD.md에 맞춰
docs/USER_GUIDE.md·docs/ADMIN_GUIDE.md와 공용md2pdf.mjs로 만든 PDF 두 개를 추가했다. 캡처는 지어내지 않았다 — 로컬 PostgreSQL(postgres:16-alpine)과 v1.4.13 빌드(npm run build+go build)로 앱을 8080에 띄우고 저장소의scripts/capture-screenshots.mjs로 manifest 29장을 모두 다시 찍었으며(기존 파일은 v1.0.0~v1.1.0 시점 캡처였다), 관리자 가이드의 “계정과 권한” 절에 실을 역할 화면이 없어admin-roles.webp(관리자 설정 → 보안·키 권한) 한 장을 manifest·캡처 조건·갤러리·홈 카운트(29→30)와 함께 추가했다. 사용자 가이드는 캡처 26장과 함께 처음 5분·화면별 사용법·자주 하는 작업·막혔을 때·용어로 구성했고 오류 표는 코드의 실제 문구를 옮겼다. 관리자 가이드의 환경 변수 표는internal/config/config.go에서, 장애 대응의 로그 문구는internal/collector에서, API 메서드·경로는 라우트 등록 자리에서 읽어 썼다. 정본이 갈라지지 않도록 README 문서 링크를 새 파일로 옮기고 기존 Pages 가이드 두 장에는 “요약이며 정본은 이것”이라는 안내를 넣었다.gofmt -l,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 59개) 모두 통과했고, PDF는 표지·표·코드 블록·그림이 정상 렌더링되는지 페이지를 이미지로 뽑아 확인했다(사용자 30쪽/그림 26, 관리자 17쪽/그림 3). 임시 컨테이너·빌드 산출물·로컬 비밀값은 커밋 전에 모두 제거했다. -
보류 아이디어: 로그인 리미터 succeeded가 ip 키를 유지하는 의도에 대한 테스트·주석 / internal/store의 DB 없이 테스트 가능한 순수 함수 경로 커버리지 보강 / internal/api 통합 테스트 실행법 문서화와 make test-integration 타깃 / auth/oidc.go가 RandomToken 오류를 무시하는 부분 정리 / queryInt가 잘못된 page·page_size를 조용히 기본값으로 바꾸는 부분을 400으로 / 감사 로그 캡처가 캡처 스크립트 자신의 settings.update로 가득 차는 문제(캡처 전 시드 이벤트 다양화)
- 릴리즈: v1.4.14 (2026-09-11, run 2026-09-11-165625-jupiq-approve)
2026-09-12
- 선택: 관리자가 화면에서 방문 추적 스크립트를 붙일 수 있는 체계(캠페인 tracking-2026-09, TRACKING-STANDARD.md 준수) (가치 4 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: jupiq는 사람이 쓰는 SPA 화면이 있으므로 캠페인 대상이며, 화면 응답의 CSP가
script-src 'self'로 잠겨 있어 스니펫을 그냥 넣으면 조용히 차단되는 구조였다. kanpic을 참조해internal/analytics(설정·provider별 스니펫·정책 출처 추출·메모리 차단 기록기 100건)를 이 저장소의 설정 문서 방식(settings테이블의analyticsJSON)에 맞게 새로 만들고,serveSPA가 요청마다 nonce를 만들어 스니펫의 모든<script>에 붙이고 같은 값을script-src에 넣은 정책으로index.html을 응답하게 했다('unsafe-inline'없음, 꺼지면 원래 정책 그대로, API·MCP·probe·프록시 응답은default-src 'none'으로 더 좁힘). provider는 momento를 첫 자리에 두고 같은 오리진 프록시(GET /momento/tracker.js,POST /momento/collect/*만 통과, 세션 쿠키·Authorization 제거, 수집기 Set-Cookie 차단, 다른 연동과 같은 SafeHTTPClient 제한)를 기본으로 삼아 외부 출처가 정책에 등장하지 않게 했으며, 추적이 켜진 동안만report-uri를 넣어POST /api/v1/analytics/csp-report로 신고를 받고GET/DELETE /api/v1/analytics/violations(settings:read/write)로 보여 준다. 관리자 설정에 “방문 추적” 탭(사용 스위치·수집 도구·Momento/GA4/GTM/Matomo/직접 붙여넣기 필드·추가 허용 출처·관리 화면 포함·삽입 위치)과 “정책이 막은 출처” 카드(허용 목록에 추가 → 설정 저장, 기록 지우기)를 더했고, 설정 검증에 analytics 영역(8KB 스니펫 상한, provider별 필수값, loopback·metadata 주소 거부)을 넣었으며 OpenAPI 문서와 계약 테스트 예외를 갱신했다. 기본값은 꺼짐이라 새 설치에서는 아무것도 달라지지 않는다(마이그레이션 없음, 문서 부재 = 꺼짐). 테스트는 analytics 패키지 13개(기본 꺼짐·프록시/직접 스니펫·admin 제외·검증·nonce 부착·스니펫 출처 추출·기록기 중복/축출/허용 표시)와 api 패키지 10개(무변경·nonce와 정책 일치·body 삽입·admin 제외·신고 기록·프록시 전달과 자격 증명 제거·프록시 폐쇄와 경로 제한·설정 검증)에 프런트addAllowedHost4개를 더했고, 임시 postgres:16-alpine과 docker 브리지 주소의 가짜 Momento 수집기로 실제 기동해 꺼짐→켜짐(프록시)→로더·수집이 쿠키 없이 수집기에 도달→CSP 신고→목록·허용 표시→직접 모드 정책→끄면 정책 원복까지 curl로 확인했다(그 과정에서 Go mux가 메서드 없는/momento/와GET /의 충돌로 기동 시 panic하는 것을 잡아 GET/POST로 나눠 등록). 관리자 가이드 3.2 표·3.5절(설정·CSP 설명·막힌 출처 허용 절차·SPA 진입 경로 기준 주의)·7.3 표를 더하고 공용 md2pdf로 PDF를 다시 구웠다(17→20쪽).gofmt -l,go vet ./...,go test -race ./...,scripts/check-version.sh,scripts/check-screenshots.mjs,npm run lint,npm test(18파일 65개) 모두 통과했고 임시 컨테이너·프로세스·빌드 산출물은 커밋 전에 제거했다. -
보류 아이디어: 방문 추적 탭·차단 출처 카드의 화면 캡처를 manifest에 추가하고 가이드에 싣기(이번엔 Playwright 캡처 비용 때문에 생략, 캡처 조건은 analytics 설정 PUT 한 번이면 됨) /
analyticsConfig가 index.html 요청마다 settings를 한 번 조회하는 것을 짧은 TTL 캐시로 줄이기(현재는 단일 행 조회라 부담이 작음) / internal/store의 DB 없이 테스트 가능한 순수 함수 경로 커버리지 보강 / 로그인 리미터 succeeded가 ip 키를 유지하는 의도에 대한 테스트·주석 / internal/api 통합 테스트 실행법 문서화와 make test-integration 타깃 - 릴리즈: v1.6.0 (2026-09-12, run 2026-09-12-182533-jupiq-improve)