"""2026-10-06 AI2 documentation snapshot; no credentials or private keys."""
from html import escape

def command(value):
    return '<pre>'+escape(value)+'</pre>'

def table(headers, rows):
    return '<table><thead><tr>'+''.join('<th>'+x+'</th>' for x in headers)+'</tr></thead><tbody>'+''.join('<tr>'+''.join('<td>'+x+'</td>' for x in row)+'</tr>' for row in rows)+'</tbody></table>'

OPERATIONS = [
 ('O1', '평소 사용과 재부팅 후 확인', '''
 <p class="lead">아이서티 내부 테스트용 AI 서버2 · 기본 사용자 <b>si</b><br>Web에서 카메라를 Start/Stop하고, 서버 터미널에서는 ct로 상태를 확인합니다.</p>
 '''+table(['대상','현재 설정'],[
 ('Web 화면','https://192.168.30.18'),('AI 서버 / 로그인 ID','192.168.30.52 / AI-01'),
 ('SSH / WSS / Redis 포트','1022 / 38443 / 16379'),('설치 / 환경','/workspace/contextra / internal'),
 ])+'''<h2>1. 전원을 켠 뒤 상태부터 확인</h2><p>AI 서버의 si 터미널 또는 SSH 터미널에서 실행합니다.</p>'''+command('ct status\nct config status')+'''
 <p><b>정상:</b> analytics, ai-control-sidecar, ai-gateway, redis, postgresql이 running이며 설정 주소가 위 표와 같습니다. 자동기동은 구성 검증을 완료했으며, 실제 재부팅 시험은 별도입니다.</p>
 <h2>2. 서비스가 stopped이고 오류가 없으면</h2>'''+command('ct start')+'''
 <p>필요한 관리 권한은 ct가 sudo로 요청합니다. 비밀번호 입력 중 글자가 안 보이는 것은 정상입니다. 보호 검사에 실패하면 반복 기동하지 말고 O3(3쪽)으로 이동합니다.</p>
 <h2>3. Web에서 영상 확인</h2>
 <p>등록 카메라 1개를 Start → 영상과 분석 표시 확인 → 시험이 끝나면 Stop합니다. 서비스 기동과 카메라 Start는 별개이며, 카메라는 자동 Start하지 않습니다.</p>
 <p class="note">연결 정보가 그대로라면 재부팅 때마다 로그인·인증서 발급을 반복하지 않습니다. 연결 변경은 O2, 장애는 O3, 종료·문의자료는 O4를 보세요.</p>'''),
 ('O2', '주소·계정·CA가 바뀌었을 때', '''
 <p class="lead">일반적인 사용에는 필요 없는 절차입니다. Web 담당자가 변경한 주소·AI 계정·연결 버전을 먼저 확인합니다.</p>
 <h2>1. 카메라를 Web에서 Stop한 뒤 서비스 중지</h2>'''+command('ct stop\nct profile edit')+'''
 <p>편집 원본: <code>/workspace/contextra/setup/profiles/internal.json</code><br>주소·포트·선택 Web·Redis 허용 주소를 수정합니다. 파일 형식 <code>version</code>은 1을 유지합니다. 화면의 Enter 안내를 따르며, Enter를 삭제로 해석하지 않습니다.</p>
 <h2>2. 바뀐 항목만 준비</h2>'''+table(['바뀐 항목','실행 / 주의'],[
 ('로그인 ID·비밀번호','<code>ct profile credentials</code> · 숨김 입력, 저장만 수행'),
 ('공개 CA 인증서','<code>ct ca</code> · 파일 선택, 기간·지문 대조'),
 ('AI 주소·접속정보','<code>ct config version 번호</code> · Web 기존 버전보다 큰 정수; 파일 형식 version과 다름'),
 ])+'''<p>CA 위치: <code>/workspace/contextra/setup/profiles/internal.private/main.ca.crt</code><br>계정 파일·token·private key는 전달하거나 일반 문서에 적지 않습니다.</p>
 <h2>3. 준비 → 발급 → 적용 → 기동</h2>'''+command('ct config prepare\nct config connect')+'''
 <p>확인 입력은 각각 <b>PREPARE</b>, <b>CONNECT</b>입니다. <code>CERTIFICATE_VERIFIED</code>를 확인한 뒤:</p>'''+command('ct config apply\nct start')+'''
 <p>apply 질문에는 <b>APPLY</b>. 실패한 단계에서 멈추고 O3을 확인합니다. 연결 요청은 기존 token을 바꿀 수 있으며 로컬 복구만으로 원격 인증이 복구되지는 않습니다.</p>'''),
 ('O3', '연결·분석 표시가 안 될 때', '''
 <p class="lead">AI 서버에서 확인할 일과 Web 담당자가 확인할 일을 구분합니다.</p>
 '''+table(['증상','먼저 할 일 / 담당'],[
 ('SSH 접속 불가','AI 콘솔에서 <code>ip -br -4 addr</code>. 주소·랜선·1022 포트 확인. Web 로그인 재시도로 해결하지 않음.'),
 ('서비스 기동 실패','AI: <code>ct status</code>, <code>ct config status</code>. 디스크·설정·인증 오류를 확인. 보호 파일의 hash를 직접 수정하지 않음.'),
 ('HTTP 401','Web: AI 계정 존재·활성·비밀번호 확인. AI: <code>ct profile credentials</code>. 버전만 올려서 해결하지 않음.'),
 ('HTTP 400 / CONNECTION_VERSION_CONFLICT','Web 담당자와 기존 연결 정보·버전 확인. AI에서 새 버전 지정 후 새 prepare. 실패마다 자동 증분하지 않음.'),
 ('CERTIFICATE 단계 HTTP 500','Web: CA 인증서·서명 private key 경로/권한·발급 오류 확인. AI의 공개 CA 파일만 교체해 해결되는 문제로 보지 않음.'),
 ('영상은 보이나 bbox 없음','Web: AI 등록 Redis 주소 .52:16379, ACL·방화벽 및 고정 env override 확인. AI: 해당 카메라가 running인지 확인.'),
 ('Event 경고만 없음','시나리오 On·ROI·조건·cooldown 확인. Web Event endpoint·인증 상태 확인. 가짜 Event를 보내지 않음.'),
 ('WSS 미연결','Web→AI .52:38443 접속·TLS·token 확인. Redis 분석 표시와 Event HTTP 송신은 각각 별도 경로임.'),
 ])+'''<h2>원인을 해결한 뒤 재시도</h2>'''+command('ct config status')+'''
 <p>이전 connect 실패 원인이 해결됐고 pending 준비본을 재사용할 때만 <code>ct config retry</code>를 사용합니다. 확인 입력은 <b>RETRY → CONNECT</b>. 결과가 불명확하면 반복 요청하지 않습니다.</p>
 <p class="note">Web 계정이 없으면 연결 버전을 올려도 생성되지 않습니다. Web 담당자가 AI 계정을 먼저 준비해야 합니다.</p>'''),
 ('O4', '안전 종료·USB·문의자료', '''
 <h2>1. 안전하게 종료</h2><p>Web에서 카메라 Stop 후 AI si 터미널:</p>'''+command('ct stop\nsudo poweroff')+'''
 <p><b>ct stop 성공을 확인한 다음</b> poweroff를 실행합니다. 회수 실패가 나오면 전원을 강제로 끄지 말고 출력과 시간을 기록합니다.</p>
 <h2>2. USB의 공개 CA 반입과 해제</h2>'''+command('ct usb mount\nct ca')+'''
 <p>장치 용량·filesystem을 확인하고 번호로 선택합니다. CA 반입은 읽기 전용 USB에서 수행하며 원본을 보존합니다. 사용 후:</p>'''+command('ct usb unmount')+'''
 <p>ct가 관리하는 USB만 선택합니다. <b>/storage와 모델·Event·학습 디스크는 해제하지 않습니다.</b> 사용 중이라고 나오면 파일 탐색·열린 파일을 닫고 재확인합니다. 강제 해제는 하지 않습니다.</p>
 <h2>3. 한글·CPU/RAM·문의자료</h2>'''+table(['상황','명령'],[
 ('물리 콘솔 한글 깨짐','<code>ct lang en</code> / 한국어 복귀 <code>ct lang ko</code>'),
 ('자원 사용이 높음','<code>ct resources</code> · 프로세스 확인, 무차별 종료 금지'),
 ('문제 상황 기록','<code>ct config note</code> · 시간·카메라 번호·오류만 입력'),
 ('지원용 ZIP 생성','<code>ct config export</code> · 비밀정보 없는지 확인 후 EXPORT'),
 ])+'''<p>ZIP 절대 경로가 출력됩니다. 기본 폴더는 <code>/workspace/contextra/setup/outbox/</code>입니다. ZIP은 설정 변경 기록이며 전체 Runtime 로그를 포함하지 않습니다. 오류 화면도 함께 전달하세요.</p>
 <h2>4. 저장공간</h2><p>Event400GB(정리목표360GB), 학습200GB(180GB), filesystem 여유100GB 보호. 진행·전송 중 자료는 보호합니다. 데이터 원본을 수동 삭제하거나 /storage 마운트를 변경하지 않습니다.</p>'''),
]

SCENARIOS = [
 ('S1','시나리오 설정의 공통 원칙','''
 <p class="lead">Web에서 AI로 전달되는 설정의 의미를 정리한 참고서입니다. 설치된 AI 계약 기준이며, Web 화면의 항목명·노출 여부는 담당자 확인이 필요합니다.</p>
 <h2>1. 세 가지를 먼저 확인</h2>
 '''+table(['항목','의미 / 변경 효과'],[
 ('시나리오 On/Off','카메라별 해당 이벤트 판정을 켜거나 끕니다. 다른 시나리오는 별도입니다.'),
 ('ROI','판정할 영상 영역입니다. 현재 같은 카메라·같은 시나리오는 ROI1개를 사용합니다.'),
 ('신뢰도 confidence','0~1. 높이면 더 확실한 검출만 사용하고, 낮추면 약한 검출도 사용합니다.'),
 ('cooldown_seconds','동일 채널·시나리오 이벤트의 최소 간격. 0~300초, 미지정 시 서버 기본10초.'),
 ])+'''<h2>2. 설정을 바꾸는 순서</h2><p>현재 값을 기록 → 한 항목만 변경 → Web에서 저장 후 Stop/Start → 같은 영상에서 표시·경고 확인. Web의 저장만으로 실행 중 AI에 반영됐다고 가정하지 않습니다.</p>
 <h2>3. cooldown은 이벤트 간격</h2><p>판정에 필요한 체류시간과 다릅니다. 10초로 설정해도 조건이 유지되는 동안 반드시 10초마다 재발행되는 것은 아닙니다. 시나리오별 해제·재무장 조건도 함께 적용됩니다.</p>
 <h2>4. 기본값의 의미</h2><p>다음 표의 기본값은 AI 코드 기본값입니다. 기존 카메라 저장값이나 Web이 보내는 유효한 값이 있으면 달라질 수 있습니다. 잘못된 선택 설정은 무시되거나 기존값으로 복귀할 수 있으므로 저장 결과를 확인합니다.</p>
 <p class="note">아래 영문 이름은 Web→AI <code>parameters</code>의 필드명입니다. Web 화면에 없는 항목을 서버 보호 파일에 직접 추가하지 마세요.</p>'''),
 ('S2','침입·배회·쓰러짐·싸움','''
 <p class="lead">기본값에서 시작하고 영상에 맞게 한 항목씩 조절합니다.</p>
 '''+table(['시나리오 / 필드','기본값','의미'],[
 ('침입 INTRUSION<br>person_confidence','0.5','사람 검출 신뢰도'),
 ('dwell_seconds / grace_seconds','0.5초 / 0.5초','ROI 체류 판정시간 / 관측 공백 허용시간'),
 ('rearm_seconds','1초','ROI 밖 관측이 유지되어 새 이벤트를 허용할 시간'),
 ('배회 LOITERING<br>person_confidence','0.5','사람 검출 신뢰도'),
 ('dwell_seconds / grace_seconds','5초 / 1초','배회 판정까지 체류시간 / 관측 공백 허용시간'),
 ('rearm_seconds','1초','ROI 밖 관측 후 재무장 시간'),
 ('쓰러짐 FALL<br>person_confidence / pose_confidence','0.25 / 0.15','사람 / 관절 검출 신뢰도'),
 ('down_angle_deg','60도','몸통이 수직에서 기울어진 각도 기준'),
 ('recovery_hold_seconds','1초','회복 자세의 유지시간'),
 ('싸움 FIGHT<br>person_confidence / pose_confidence','0.25 / 0.15','사람 / 관절 검출 신뢰도'),
 ('pair_distance_ratio','1.2','사람 크기로 정규화한 접근 거리 기준'),
 ('pair_close_hold_seconds','0.5초','근접 상태 유지시간'),
 ('arm_motion_ratio / required_bursts','0.30 / 2회','팔 움직임 기준 / 반복 동작 횟수'),
 ])+'''<h2>짧은 예시</h2><p>배회가 너무 빨리 발생하면 <code>dwell_seconds</code>를 5초에서10초로 높입니다. 쓰러짐은 각도 하나만으로 판정하지 않으므로 임의로 낮추기보다 영상·검출 상태를 먼저 확인합니다.</p>
 <p>싸움은 근접·팔 움직임의 규칙 기반 판정입니다. 실제 싸움 여부에 대한 판단을 대신하지 않습니다.</p>'''),
 ('S3','화재·연기','''
 <p class="lead">불꽃과 연기를 각각 관측하며, 둘 중 하나가 조건을 충족하면 FIRE 이벤트를 판정합니다.</p>
 '''+table(['필드','기본값','의미 / 변경 효과'],[
 ('flame_confidence','0.5','불꽃 검출 신뢰도'),
 ('flame_window_seconds','2초','불꽃 관측 구간. 늘리면 더 긴 관측 필요'),
 ('flame_positive_ratio','0.6','관측 구간의 불꽃 긍정 비율 기준'),
 ('smoke_confidence','0.5','연기 검출 신뢰도'),
 ('smoke_window_seconds','2초','연기 관측 구간'),
 ('smoke_positive_ratio','0.6','관측 구간의 연기 긍정 비율 기준'),
 ('clear_seconds','5초','불꽃·연기 모두 음성으로 연속 관측될 때 해제 시간'),
 ('cooldown_seconds','10초','다음 FIRE 이벤트의 최소 간격'),
 ])+'''<h2>짧은 예시</h2><p>반짝임 때문에 불꽃 판정이 잦다면 불꽃 신뢰도를 0.5에서0.6으로 올리고 같은 영상으로 확인합니다. 불꽃과 연기의 값을 동시에 바꾸지 않습니다.</p>
 <h2>해제와 영상 중단은 다릅니다</h2><p>입력 중단이나 누락 Frame을 “불꽃·연기 없음”으로 세지 않습니다. 영상이 멈췄는데 경고가 즉시 해제되지 않는 경우 입력 상태부터 확인합니다.</p>
 <p class="note">관측 구간·비율 등은 서로의 유효 조건을 만족해야 합니다. 잘못된 조합은 적용되지 않을 수 있습니다. 화재 감지 기능을 법정 안전설비 대신 사용하지 않습니다.</p>'''),
 ('S4','군중 인원·밀도·위험 경고','''
 <p class="lead">ROI 안에서 검출된 head 수가 기준입니다. person 수는 참고용입니다.</p>
 <h2>1. 다섯 개의 필수 설정</h2>
 '''+table(['필드','기본/예시','의미'],[
 ('reference_count','50명','밀도 계산의 기준 인원. 양의 정수'),
 ('safe_ratio','0.25','수신·보존하되 현재 상태 구분에는 사용하지 않음'),
 ('caution_ratio','0.50','주의 시작 기준'),
 ('warning_ratio','0.75','경고 시작 기준'),
 ('danger_ratio','0.90','위험 시작 기준, Event 발생 조건'),
 ])+'''<p>군중 On으로 요청할 때 다섯 값을 모두 보내야 합니다. 누락·잘못된 값이면 군중 이벤트가 적용되지 않습니다. 비율 설정은0~1이며 <b>주의 &lt; 경고 &lt; 위험</b>이어야 합니다.</p>
 <h2>2. 밀도 = ROI head 수 ÷ 기준 인원</h2>
 '''+table(['상태','위 예시에서 밀도 구간','인원 예시'],[
 ('SAFE 안전','0.50 미만','20명 → 0.40'),('CAUTION 주의','0.50 이상 0.75 미만','30명 → 0.60'),
 ('WARNING 경고','0.75 이상 0.90 미만','40명 → 0.80'),('DANGER 위험','0.90 이상','45명 → 0.90'),
 ])+'''<p>60명이라면 밀도1.20입니다. 실제 밀도는1을 넘을 수 있으며 사람/㎡ 단위의 물리적 밀도가 아닙니다.</p>
 <h2>3. 경고와 표시</h2><p>DANGER에서만 Event를 발생시키며, 같은 위험 상태가 계속되면 반복 발행을 억제합니다. 안전·주의·경고는 Frame 상태 정보로 전달됩니다. 신뢰도 기본값은 <code>head_confidence=0.45</code>, <code>person_confidence=0.45</code>입니다.</p>'''),
]
