누수/온습도 경보 쿨다운이 경보 태그만 조회해, 직전에 [누수복구]/[정상복귀]가
있었어도 이전 경보 발송 시각만 보고 억제했다. 그 결과 복구 직후 재발한 새 사고
(별개 에피소드)가 쿨다운 창(기본 30분) 동안 통째로 누락됐다.
- alert_cooldown_suppresses(): 경보/복구 중 "최신"이 경보이고(=미복구) 쿨다운
이내일 때만 억제하는 순수 헬퍼 추가. 최신이 복구면 새 에피소드로 즉시 발송.
- send_leak_alert_sms / send_metric_alert_sms: 쿨다운 조회를 (경보|복구) 최신
1건 기준으로 교체하고 위 헬퍼로 판정. 지속 경보 폭주 방지는 그대로 유지.
- tests/cooldown_episode_test.php: 복구 직후 재발 발송 등 9종 회귀 추가,
run_tests.sh 에 등록.
- tests/security_hardening_test.php: dew_point_c 의 forecast.php 단일출처 이동에
맞춰 낡은 단언 갱신(정의는 forecast.php, sms_send.php 는 사용 확인).
작업 트리에 있던 dew_point_c 중복정의 제거(forecast.php 단일출처화)도 sms_send.php
변경에 함께 포함된다.
상단 개요 위젯이 SMS 발송(leak_sms_log)을 세어 하단 측정 이력·이벤트 로그와
숫자가 어긋나던 문제 수정. 실제 센서 데이터로 소스 일원화:
- 도넛/리더보드/게이지 스파크/추이: leak_sensor_metric.metric_status(비정상 측정)
+ leak_sensor_log leak_alert(누수 이벤트) 기준
- "경보 발생 vs 복구" → "경보 발생 추이(온습도 vs 누수)"로 재정의
(측정 기준엔 복구 측정 개념이 없어 두 위험 유형 발생량 비교로 변경)
- 30일 운영 요약은 SMS 기준 유지(라벨에 "SMS" 명시 — 발송 통계는 별개 지표)
검증: php -l 통과, 스텁DB 전체 렌더 exit 0(경고 0)·태그 균형(div 61/61)·존 4개.
샘플 대시보드(고객지원/방문분석) 콘텐츠를 누수·온습도 도메인으로 이식하고,
평면 나열이던 섹션을 관계·위치가 드러나는 4개 라벨 존으로 재구성한다.
콘텐츠 위젯(전부 기존 DB 집계, 외부 라이브러리 0 · 인라인 SVG · 폐쇄망 안전):
- 가동률 게이지 + 7일 경보 스파크라인 + 상태 배지
- 듀얼 KPI(경보 센서 / 오프라인)
- 경보 종류 도넛(30일, $summary 재사용)
- 경보 최다 센서 리더보드(30일 GROUP BY)
- 경보 발생 vs 복구 7일 이벤트율 라인차트
- 온습도 추이: 기간 토글(오늘/3일/7일, ?range 링크) + 피크 마커
정보구조:
- 4개 존(관제 개요 / 실시간 상태 / 추세 분석 / 운영 이력) — eyebrow 라벨 + 하드라인
- 관제 개요 = 12칼럼 벤토 그리드(기존 ops-grid 대체, 자가진단 편입)
- 중복 카드 제거(등록센서/최근온습도/현재시각 → 게이지·히어로·요약 흡수)
검증: php -l 통과, 스텁DB 전체 렌더 exit 0(경고 0)·태그 균형·빈데이터 우아,
채운데이터 프리뷰 스크린샷 확인. 신규 CSS는 네임스페이스 셀렉터만(타 페이지 영향 0).
tools/make_deploy_bundles.py: deploy/ 아래 서버1+Pi3대 폴더 생성 —
API_KEY 1개 자동 생성·4곳 동일 주입, 관리자 bcrypt(비번 stdin 전달), LF 정규화,
단일 schema.sql, var/ 런타임 상태 제외, CRON_SECRET 사전 계산. 난독화 경로
WEB_DIR 은 소스가 아닌 deploy/.secrets/web_dir.txt 에서 로드(커밋 트리 유출 차단).
시크릿 생성 전에 deploy/.gitignore 선작성 + 루트 .gitignore 에 deploy/ 이중 차단.
tools/harden_pi.sh: SSH 완전 비활성(outbound 전용 설계) · 인바운드 전부 차단
(nftables) · 불필요 서비스 제거(avahi/bluetooth/cups 등) · 자동 보안 패치
(unattended-upgrades, 자동 리부팅 끔) · config_local.py 권한 600 · 검증 증적 출력.
setup.sh(번들 생성): 설치→서비스→검증→하드닝 원샷. 검증 실패 시 set -e 로
하드닝(SSH 차단) 전에 중단 → SSH 유지되어 디버깅 가능.
검색 비노출 하드닝: php/.htaccess X-Robots-Tag noindex + Referrer-Policy
no-referrer, api/photo_upload.php CORS 제거, test_mobile.php 세션 인증 가드.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
3번째 Pi(신규 지점, 누수+카메라)를 코드 수정 없이 배포 가능하게:
- raspberry_pi4/config.py: config_local.py 에서 SENSORS 오버라이드 허용
+ fail-closed 형식 검증(id 1..255/gpio/name, 중복 금지) — 오설정으로
기본 sensor_id 1 로 조용히 기동해 타 지점 상태를 덮어쓰는 사고 방지.
- REPORT_INTERVAL 3600→600: 서버 HEARTBEAT_TIMEOUT_SEC(1200) 절반 —
기존 하트비트 오탐(매시간 오프라인/복귀 SMS) 해소.
- php/config.php LEAK_SENSOR_IDS 상수(기본 [1], 1..255 정규화·빈값 복귀).
- php/dashboard.php: 누수 카드 sensor_id===1 하드코딩 → LEAK_SENSOR_IDS 루프.
- php/api/sensor_data.php: periodic 보고 전면 skip_log(누수 노드 증가 시
이벤트 로그 창 보존) + CORS 헤더 제거(서버간 POST 전용, 도메인 지문 노출 차단).
- test_connection.py(양 모듈): 하드코딩 sensor_id 제거→기기 SENSORS 기반,
서명 200 실패 시 exit 1(setup.sh 원샷 게이트용).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
같은 cafe24 DB의 다른 시스템 테이블과 혼동/충돌을 막기 위해 5개 테이블을
leak_ 네임스페이스로 격리:
sensor_log→leak_sensor_log, sensor_status→leak_sensor_status,
sensor_metric→leak_sensor_metric, sms_log→leak_sms_log,
ingest_nonce→leak_ingest_nonce (leak_incident/leak_photo는 이미 접두사).
컬럼명은 불변(sensor_id, is_leak 등) — 테이블명만 변경.
sql/migration_add_leak.sql, migration_drop_leak.sql 삭제: 리네임 후 구버전
무접두 테이블 대상이라 실행 즉시 에러. 신규 설치는 schema_sht30.sql 하나로 완결.
BREAKING CHANGE: 기존 무접두 테이블 배포와 호환 안 됨(신규 설치 전용).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
다중 에이전트 보안 리뷰 후속 수정:
- (HIGH) config.local.example.php ADMIN_TOTP_SECRET '' 로 교체: 기존 플레이스홀더가
Base32 디코드되어 실제 TOTP 코드를 생성(공개 시드로 MFA 활성화)하던 회귀를 차단.
fail-closed(미설정 시 로그인 차단) + 회귀 테스트 추가.
- (MEDIUM) 레거시 키-접두 sha256 서명 경로를 ALLOW_LEGACY_BODY_SIGNATURE(기본 false)로
게이트: X-Signature(HMAC) 없으면 거부.
- (LOW) 인증 전 본문 크기 상한 INGEST_MAX_BODY_BYTES(8KB) → 413.
- (LOW) heartbeat throttle 를 flock 기반 원자 판정으로 교체(중복 스캔 방지).
- (LOW) CRON_SECRET: md5 → hash_hmac('sha256','cron',API_KEY).