You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

229 lines
14 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="IoT Device Web Configurator - Network, Protocol, and I/O Configuration">
<link rel="icon" href="data:,">
<title>IoT Web Configurator</title>
<!-- v1.4.6.10 ①: 외부 Google Fonts CDN 제거. 본 앱은 LAN 전용 IoT 장비에 직접 접속하는
오프라인 환경에서 사용 — 외부 CDN은 (a) DNS 타임아웃까지 매달려 첫 페인트 수초 지연,
(b) 폰트 폴백으로 디자인 열화 트리거. style.css의 --font-family 폴백 체인이 시스템 폰트
(-apple-system / Segoe UI 등)로 안전 동작. Inter / JetBrains Mono를 정식 사용하려면
.woff2 파일을 src/static/fonts/에 추가 후 style.css의 @font-face 블록 활성화. -->
<link rel="stylesheet" href="css/style.css">
<!-- v1.5.4: modulepreload hints — browser fetches critical modules in parallel
before app.js fully parses, reducing import waterfall latency -->
<link rel="modulepreload" href="js/app.js">
<link rel="modulepreload" href="js/state.js">
<link rel="modulepreload" href="js/api.js">
<link rel="modulepreload" href="js/utils.js">
<link rel="modulepreload" href="js/icons.js">
<link rel="modulepreload" href="js/constants.js">
<link rel="modulepreload" href="js/page-dirty.js">
<link rel="modulepreload" href="js/nav-guard.js">
<link rel="modulepreload" href="js/pages/home.js">
</head>
<body>
<!-- v1.5.0 P1: Skip-to-content accessibility (WCAG 2.4.1) -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- v1.5.0 P1: Sidebar nested 4-group + 13 leaf (v1.11.14: system/network/interface-protocol/firmware) -->
<aside class="sidebar" id="sidebar" role="navigation" aria-label="Main navigation">
<div class="sidebar__header">
<a href="#" class="sidebar__logo" data-page="home" id="logo-home">
<img class="sidebar__logo-img" src="img/dp-world-logo.svg" alt="DP World">
<span class="sidebar__logo-cap">CONFIGURATOR</span>
</a>
</div>
<nav class="sidebar__nav" id="sidebar-nav" aria-label="Configuration pages">
<!-- Group: System (v1.11.14 — Dashboard + Log) -->
<div class="nav-group" data-group="system">
<button type="button" class="nav-group__toggle"
aria-expanded="true" aria-controls="leaves-system">
<span class="nav-group__icon" id="icon-grp-system"></span>
<span class="nav-group__label">System</span>
<span class="nav-group__chevron" id="icon-chev-system" aria-hidden="true"></span>
</button>
<ul class="nav-group__leaves" id="leaves-system">
<li><a href="#" class="nav-item nav-item--leaf" data-page="home" id="nav-home">
<span class="nav-item__icon" id="icon-nav-home"></span>
<span class="nav-item__label">Dashboard</span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="log" id="nav-log">
<span class="nav-item__icon" id="icon-nav-log"></span>
<span class="nav-item__label">Log</span>
<span class="nav-item__dirty" id="dirty-log" aria-hidden="true"></span>
</a></li>
</ul>
</div>
<!-- Group: Network -->
<div class="nav-group" data-group="network">
<button type="button" class="nav-group__toggle"
aria-expanded="true" aria-controls="leaves-network">
<span class="nav-group__icon" id="icon-grp-network"></span>
<span class="nav-group__label">Network</span>
<span class="nav-group__chevron" id="icon-chev-network" aria-hidden="true"></span>
</button>
<ul class="nav-group__leaves" id="leaves-network">
<li><a href="#" class="nav-item nav-item--leaf" data-page="wifi" id="nav-wifi">
<span class="nav-item__icon" id="icon-nav-wifi"></span>
<span class="nav-item__label">Wi-Fi</span>
<span class="nav-item__dirty" id="dirty-wifi" aria-hidden="true"></span>
</a></li>
<!-- AP: nav leaf -->
<li><a href="#" class="nav-item nav-item--leaf" data-page="wifi-ap" id="nav-wifi-ap">
<span class="nav-item__icon" id="icon-nav-wifi-ap"></span>
<span class="nav-item__label">Wi-Fi AP</span>
<span class="nav-item__dirty" id="dirty-wifi-ap" aria-hidden="true"></span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="ethernet" id="nav-ethernet">
<span class="nav-item__icon" id="icon-nav-ethernet"></span>
<span class="nav-item__label">Ethernet</span>
<span class="nav-item__dirty" id="dirty-ethernet" aria-hidden="true"></span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="server-setting" id="nav-server-setting">
<span class="nav-item__icon" id="icon-nav-server-setting"></span>
<span class="nav-item__label">Server Setting</span>
<span class="nav-item__dirty" id="dirty-server-setting" aria-hidden="true"></span>
</a></li>
<!-- v1.8.0: Apply & Status 는 Advanced/Debug 보기 전용 (User 모드 숨김). -->
<li class="advanced-only"><a href="#" class="nav-item nav-item--leaf" data-page="net-apply" id="nav-net-apply">
<span class="nav-item__icon" id="icon-nav-net-apply"></span>
<span class="nav-item__label">Apply &amp; Status</span>
</a></li>
</ul>
</div>
<!-- Group: Interface & Protocol -->
<div class="nav-group" data-group="interface-protocol">
<button type="button" class="nav-group__toggle"
aria-expanded="true" aria-controls="leaves-interface-protocol">
<span class="nav-group__icon" id="icon-grp-interface"></span>
<span class="nav-group__label">Interface &amp; Protocol</span>
<span class="nav-group__chevron" id="icon-chev-interface" aria-hidden="true"></span>
</button>
<ul class="nav-group__leaves" id="leaves-interface-protocol">
<!-- v1.5.0 P3: register→general, NEW sensor-io, can→can-bus -->
<li><a href="#" class="nav-item nav-item--leaf" data-page="general" id="nav-general">
<span class="nav-item__icon" id="icon-nav-general"></span>
<span class="nav-item__label">General Settings</span>
<span class="nav-item__dirty" id="dirty-general" aria-hidden="true"></span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="sensor-io" id="nav-sensor-io">
<span class="nav-item__icon" id="icon-nav-sensor-io"></span>
<span class="nav-item__label">Sensor I/O</span>
<span class="nav-item__dirty" id="dirty-sensor-io" aria-hidden="true"></span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="can-bus" id="nav-can-bus">
<span class="nav-item__icon" id="icon-nav-can-bus"></span>
<span class="nav-item__label">CAN-BUS</span>
<span class="nav-item__dirty" id="dirty-can-bus" aria-hidden="true"></span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="opcua" id="nav-opcua">
<span class="nav-item__icon" id="icon-nav-opcua"></span>
<span class="nav-item__label">OPC UA</span>
<span class="nav-item__dirty" id="dirty-opcua" aria-hidden="true"></span>
</a></li>
<li><a href="#" class="nav-item nav-item--leaf" data-page="modbus" id="nav-modbus">
<span class="nav-item__icon" id="icon-nav-modbus"></span>
<span class="nav-item__label">Modbus</span>
<span class="nav-item__dirty" id="dirty-modbus" aria-hidden="true"></span>
</a></li>
</ul>
</div>
<!-- Group: Firmware (v1.11.14 — own collapsible group, leaf=Update) -->
<div class="nav-group" data-group="firmware">
<button type="button" class="nav-group__toggle"
aria-expanded="true" aria-controls="leaves-firmware">
<span class="nav-group__icon" id="icon-grp-firmware"></span>
<span class="nav-group__label">Firmware</span>
<span class="nav-group__chevron" id="icon-chev-firmware" aria-hidden="true"></span>
</button>
<ul class="nav-group__leaves" id="leaves-firmware">
<li><a href="#" class="nav-item nav-item--leaf" data-page="firmware" id="nav-firmware">
<span class="nav-item__icon" id="icon-nav-firmware"></span>
<span class="nav-item__label">Update</span>
</a></li>
</ul>
</div>
</nav>
<div class="sidebar__footer">
<!-- v1.7.0: 전역 미저장/미적용 배지 — 클릭 시 Pending Changes 패널 -->
<button type="button" class="pending-badge hidden" id="pending-badge"
data-pending-badge
aria-label="No pending changes" title="View pending changes"></button>
<button class="btn btn--primary btn--full" id="btn-save-all">
<span>💾</span> Save All
</button>
<div class="sidebar__actions">
<button class="btn btn--outline btn--sm" id="btn-import">
<span>📥</span> Import
</button>
<button class="btn btn--outline btn--sm" id="btn-export">
<span>📤</span> Export
</button>
</div>
<!-- v1.8.0: Advanced/Debug 보기 토글 — apply 머신(step·watchdog·drift·journal·
force·Apply&Status·미적용 배지) 노출. 보안 경계 아님(누구나 켤 수 있는 표시 스위치). -->
<label class="view-toggle" for="advanced-view-toggle">
<input type="checkbox" id="advanced-view-toggle" class="view-toggle__input" data-no-dirty>
<span class="view-toggle__label">Advanced / Debug view</span>
<span class="view-toggle__hint">Shows apply steps, watchdog, and diagnostics. Not a security boundary.</span>
</label>
<div class="sidebar__version">
<span class="sidebar__version-name" id="app-name"></span>
<span class="sidebar__version-num" id="app-version"></span>
</div>
</div>
</aside>
<!-- Mobile Header -->
<header class="mobile-header" id="mobile-header">
<button class="mobile-header__menu" id="btn-menu" aria-label="Open menu"></button>
<span class="mobile-header__title">IoT Configurator</span>
<button type="button" class="pending-badge hidden" id="pending-badge-mobile"
data-pending-badge
aria-label="No pending changes" title="View pending changes"></button>
<button class="btn btn--primary btn--sm" id="btn-save-mobile" aria-label="Save changes">💾</button>
</header>
<!-- Overlay for mobile sidebar -->
<div class="sidebar-overlay" id="sidebar-overlay"></div>
<!-- Main Content -->
<main class="main" id="main-content" role="main">
<!-- Loading Spinner -->
<div class="loading" id="loading">
<div class="loading__spinner"></div>
<p>Loading configuration...</p>
</div>
<!-- v1.8.0: Simple Apply host — "Applying…" 진행 + eth1 평이 재접속 배너.
★ page-container 밖이라 페이지 전환에도 살아남는다(eth1 confirm 지속). -->
<div class="apply-flow-host" id="apply-flow-host"></div>
<!-- Page Sections (rendered by JS modules) -->
<div class="page-container" id="page-container" style="display:none;"></div>
</main>
<!-- Toast Container — v1.7.1 Task 4: live region for screen readers -->
<div class="toast-container" id="toast-container" role="status" aria-live="polite" aria-atomic="false"></div>
<!-- M-9: assertive SR-only mirror for error/warning toasts. The polite
#toast-container governs all visual toasts; this hidden region is only
populated (via toast.js) for error/warning so AT interrupts immediately. -->
<div id="toast-sr-assertive" role="alert" aria-live="assertive" class="sr-only"></div>
<!-- Hidden file input for import -->
<input type="file" id="file-import" accept=".json" style="display:none;">
<!-- Theme Toggle (floating) -->
<button class="theme-toggle" id="btn-theme" title="Toggle dark/light mode" aria-label="Toggle theme">🌙</button>
<script type="module" src="js/app.js"></script>
</body>
</html>