POSA_LEAKSMS/docs/examples/admin_dashboard_capture_source.html
유창욱 90f121e14c chore: import codebase with security hardening
SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트).
전체 코드리뷰에서 도출된 보안 하드닝 10건 반영:
- 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시)
- 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php)
- 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점)
- CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php)
- 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화

_backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
2026-06-20 09:37:40 +09:00

165 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>관리자 대시보드 캡처 소스</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "Noto Sans KR", "Malgun Gothic", sans-serif; background: #f0f4f8; color: #243044; overflow-x: hidden; }
header { background: #1a56db; color: #fff; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; gap: 14px; }
header h1 { font-size: 1.26rem; letter-spacing: 0; }
.top-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.top-actions span { background: rgba(255,255,255,.15); border-radius: 6px; padding: 7px 12px; font-size: .84rem; font-weight: 700; }
.container { max-width: 1120px; margin: 24px auto; padding: 0 16px 40px; }
.hero { border-radius: 8px; color: #fff; padding: 20px 22px; margin-bottom: 18px; display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: center; box-shadow: 0 4px 18px rgba(15,23,42,.14); background: #166534; }
.hero h2 { font-size: 1.3rem; margin-bottom: 4px; }
.hero p { color: rgba(255,255,255,.86); font-size: .9rem; overflow-wrap: anywhere; }
.clock { text-align: right; color: rgba(255,255,255,.78); font-size: .82rem; white-space: nowrap; }
.quick { display: none; }
.ops { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(280px, .85fr); gap: 16px; margin-bottom: 22px; }
.panel { background: #fff; border-radius: 8px; padding: 18px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
.panel h2 { color: #475569; font-size: 1rem; margin-bottom: 12px; }
.trend { display: grid; gap: 12px; }
.trend-row { display: grid; grid-template-columns: 120px 1fr; gap: 12px; align-items: center; font-size: .84rem; font-weight: 800; color: #334155; }
.bar { height: 14px; border-radius: 999px; display: block; }
.bar.temp { background: #fb7185; } .bar.hum { background: #38bdf8; }
.trend-note { color: #64748b; font-size: .76rem; line-height: 1.4; margin-top: 2px; }
.diag-list { display: grid; gap: 8px; }
.diag { border: 1px solid #e2e8f0; border-radius: 7px; padding: 9px 10px; display: grid; grid-template-columns: auto 1fr; gap: 8px; align-items: start; }
.dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 4px; background: #22c55e; }
.diag.warn .dot { background: #f59e0b; }
.diag strong { display: block; font-size: .82rem; }
.diag span { color: #64748b; font-size: .74rem; line-height: 1.35; }
.cards { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-bottom: 24px; }
.card { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
.card span { display: block; color: #64748b; font-size: .78rem; margin-bottom: 5px; }
.card strong { font-size: 1.6rem; }
.sensors { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 24px; }
.sensor { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.1); border-left: 6px solid #22c55e; }
.sensor.offline { border-left-color: #f59e0b; background: #fffbeb; }
.sensor h3 { font-size: 1rem; margin-bottom: 4px; }
.sensor small { color: #64748b; }
.badge { display: inline-block; margin-top: 10px; border-radius: 999px; padding: 3px 10px; background: #dcfce7; color: #166534; font-size: .76rem; font-weight: 800; }
.badge.warn { background: #fef3c7; color: #92400e; }
.metric { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.metric span { border-radius: 999px; background: #eef2ff; color: #3730a3; padding: 5px 9px; font-size: .8rem; font-weight: 900; }
.last { margin-top: 7px; color: #94a3b8; font-size: .76rem; }
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
th { background: #f8fafc; font-size: .8rem; color: #64748b; padding: 10px 14px; text-align: left; border-bottom: 1px solid #e2e8f0; }
td { padding: 9px 14px; border-bottom: 1px solid #f1f5f9; font-size: .85rem; }
.ok { color: #16a34a; font-weight: 800; } .err { color: #dc2626; font-weight: 800; }
.section-title { color: #475569; font-size: 1rem; margin: 24px 0 12px; }
.mobile-links { display: none; }
@media (max-width: 760px) {
header { display: block; padding: 13px 16px; }
header h1 { font-size: 1.05rem; }
.top-actions { margin-top: 10px; overflow-x: auto; flex-wrap: nowrap; padding-bottom: 2px; }
.top-actions span { flex: 0 0 auto; padding: 7px 10px; }
.container { margin: 16px auto; padding: 0 10px 32px; }
.hero { grid-template-columns: 1fr; padding: 16px; margin-bottom: 12px; }
.hero p { word-break: break-all; max-width: 310px; }
.clock { text-align: left; }
.quick { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 12px; }
.quick .card { min-height: 78px; padding: 12px; }
.quick .card strong { font-size: 1.3rem; }
.mobile-links { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; margin-bottom: 12px; }
.mobile-links span { flex: 0 0 auto; color: #1d4ed8; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 999px; padding: 8px 11px; font-size: .78rem; font-weight: 800; }
.ops { grid-template-columns: 1fr; }
.cards { display: none; }
.sensors { grid-template-columns: 1fr; gap: 10px; }
.sensor { padding: 16px; }
.panel { padding: 14px; }
.trend-row { grid-template-columns: 100px 1fr; }
.capture-desktop-only { display: none; }
}
</style>
</head>
<body>
<header>
<h1>온습도 모니터링 관리자 대시보드 v2605</h1>
<div class="top-actions">
<span>월간 보고서</span>
<span>보안 증적</span>
<span>로그아웃</span>
</div>
</header>
<!-- ※ 캡처 이미지(captures/*.png)는 SHT30 온습도 화면으로 재촬영 필요 -->
<main class="container">
<section class="hero">
<div>
<h2>정상 감시 중</h2>
<p>현재 임계 경보와 중요 운영 경고가 없습니다. 1번과 2번 센서가 온라인입니다.</p>
</div>
<div class="clock">2026-05-21 09:30:00<br>30초 자동 새로고침</div>
</section>
<div class="quick">
<div class="card"><span>임계 경보</span><strong class="ok">0건</strong></div>
<div class="card"><span>오프라인</span><strong class="ok">0대</strong></div>
<div class="card"><span>평균 온도</span><strong>24.3C</strong></div>
<div class="card"><span>평균 습도</span><strong>48%</strong></div>
</div>
<div class="mobile-links"><span>센서 상태</span><span>온습도 추이</span><span>임계 SMS</span><span>로그</span><span>SMS</span></div>
<div class="ops">
<div>
<div class="panel">
<h2>온습도 추이 (24시간)</h2>
<div class="trend">
<div class="trend-row"><span>온도 24.3C</span><i class="bar temp" style="width:62%"></i></div>
<div class="trend-row"><span>습도 48.1%</span><i class="bar hum" style="width:48%"></i></div>
<p class="trend-note">임계: 고온 30C / 저온 10C / 고습 70% / 저습 20%. 현재 모든 값이 정상 범위입니다.</p>
</div>
</div>
</div>
<aside class="panel">
<h2>운영 자가진단</h2>
<div class="diag-list">
<div class="diag"><i class="dot"></i><div><strong>API 키 · 정상</strong><span>RPi와 PHP API_KEY 일치</span></div></div>
<div class="diag"><i class="dot"></i><div><strong>SHT30 metric table · 정상</strong><span>sensor_metric 저장 가능</span></div></div>
<div class="diag warn"><i class="dot"></i><div><strong>최근 SMS 실패 · 확인 필요</strong><span>최근 7일 실패 1건</span></div></div>
</div>
</aside>
</div>
<div class="cards capture-desktop-only">
<div class="card"><span>등록 센서</span><strong>2대</strong></div>
<div class="card"><span>임계 경보</span><strong class="ok">0건</strong></div>
<div class="card"><span>오프라인</span><strong class="ok">0대</strong></div>
<div class="card"><span>30일 임계 SMS</span><strong>3건</strong></div>
<div class="card"><span>SMS 실패</span><strong>1건</strong></div>
</div>
<h2 class="section-title">센서 현재 상태</h2>
<div class="sensors" id="sensors">
<article class="sensor">
<h3>1번 센서 (SHT30 온습도)</h3>
<small>서버실 하부 랙 / rpi-sht30-02</small><br>
<span class="badge">정상</span>
<div class="metric"><span>온도 23.8C</span><span>습도 46.5%</span></div>
<div class="last">측정: 2026-05-21 09:29:42</div>
</article>
<article class="sensor">
<h3>2번 센서 (SHT30 온습도)</h3>
<small>서버실 상단 랙 / rpi-sht30-01</small><br>
<span class="badge">정상</span>
<div class="metric"><span>온도 24.3C</span><span>습도 48.1%</span></div>
<div class="last">측정: 2026-05-21 09:29:18</div>
</article>
</div>
<h2 class="section-title">최근 임계 경보 SMS 이력</h2>
<table>
<thead>
<tr><th>#</th><th>시각</th><th>센서</th><th>이벤트</th><th>측정값</th><th>상태</th></tr>
</thead>
<tbody>
<tr><td>224</td><td>09:29:18</td><td>2번 센서</td><td>정기보고</td><td>24.3C / 48.1%</td><td><span class="ok">정상</span></td></tr>
<tr><td>223</td><td>09:28:42</td><td>1번 센서</td><td>정기보고</td><td>23.8C / 46.5%</td><td><span class="ok">정상</span></td></tr>
<tr><td>218</td><td>03:14:05</td><td>2번 센서</td><td>고온 임계 SMS</td><td>31.2C / 41.0%</td><td><span class="err">고온</span></td></tr>
</tbody>
</table>
</main>
</body>
</html>