# NEW Web Configurator — Architecture Document
> **Version 3.0 | 2026-06-20** — reflects app **v1.11.10** (Network Apply Engine + Wi-Fi AP + Firmware OTA)
> Supersedes: v2.2 (2026-05-28), v2.1 (2026-05-26), v2.0 (2026-05-22), v1.1 (2026-02-19), v1.0 (2026-02-12)
---
## 1. Overview
The **NEW Web Configurator** is a browser-based configuration tool for the
Telechips TCC8030 IoT device. It is a **Python-stdlib HTTP server** plus a
**vanilla ES-module SPA** (no build step), and it replaced a heavier legacy
Java/Spring configurator (~436 MB → ~15–30 MB resident).
It reads and writes the device's `board_config` SQLite store. That store is
also used by two other programs on the device — the legacy Java app and the
`dpworldapp` data-processing binary — which makes **storage ownership** a
central concern of this document (see §6).
---
## 2. System Context
Three independent programs run on the device and **all touch the same SQLite
DB**:
```mermaid
graph LR
Operator["운영자< br / > (브라우저)"]
Operator -->|":9090 직접"| WebCfg
Operator -->|":80 → nginx"| JavaApp
subgraph Device["IoT Device — TCC8030 (192.168.55.x)"]
WebCfg["Python Web Configurator< br / > systemd: web-configurator.service< br / > :9090"]
JavaApp["레거시 Java app-runner< br / > :8080 (nginx :80 프록시)"]
dpworld["dpworldapp< br / > CAN/Modbus/OPC-UA 데이터 처리"]
DB[("SQLite< br / > ~/db/dynamic_data.db")]
WebCfg -->|R/W| DB
JavaApp -->|R/W| DB
dpworld -->|R/W| DB
end
```
- **Python Web Configurator** — this project. systemd-managed service on port
**9090** , reached directly (no reverse proxy in front of it).
- **Legacy Java app-runner** — a separate, still-running legacy app on 8080,
fronted by nginx on :80. Not part of this project, but it **shares
`board_config` ** — see §6.
- **dpworldapp** — the core data-processing binary. Reads config from
`board_config` , writes events to `event_history` .
> The original design docs assumed "nginx :80 → Python :8080". The actual
> device deployment is the layout above (Python direct on :9090, systemd-
> managed). See [`DEPLOY.md`](DEPLOY.md) for the deployment runbook.
> 위 3개 프로그램 외에 사용자의 **별도 프로젝트 Super_Relay**도 같은
> `board_config`에 일부 키(`security_config`·`transport_config`)를 기록한다
> — 소유권 상세는 §6.2.
---
## 3. Component Architecture
```mermaid
graph TB
subgraph Frontend["프론트엔드 — Vanilla ES-module SPA (빌드 없음)"]
AppJS["app.js — 라우팅·초기화·Save/Import/Export"]
ApiJS["api.js — REST 클라이언트"]
StateJS["state.js — 상태관리, flat↔nested 변환"]
ViewMode["view-mode.js — Two Faces (User/Advanced 토글)"]
Pages["pages/ — ~18 페이지< br / > (home·wifi·wifi-ap·ethernet·io·< br / > can·modbus·opcua·register·log·< br / > firmware·net-apply·…)"]
Shared["validator·utils·toast·constants·< br / > country-codes·components/"]
AppJS --> Pages --> ApiJS
AppJS --> ViewMode
Pages --> StateJS
Pages --> Shared
end
subgraph Backend["백엔드 — Python stdlib"]
Server["server.py — HTTP 라우팅·CORS·정적파일·진입점"]
DBMgr["db_manager.py — 3-tier SQLite 접근 레이어"]
Validator["config_validator.py — 서버측 검증·정규화"]
LogMgr["log_manager.py — 로그 관리·자동압축 데몬"]
KernelLog["kernel_log.py — 커널 로그 번들 (.tar.gz)"]
Status["system_status.py — Home 대시보드 상태 집계"]
Bundle["support_bundle.py — 진단 zip 생성"]
NetPkg["network/ — Network Apply Engine + Wi-Fi AP"]
FwPkg["firmware/ — Firmware OTA (TCP 8990)"]
Server --> DBMgr
Server --> Validator
Server --> LogMgr
Server --> KernelLog
Server --> Status
Server --> Bundle
Server --> NetPkg
Server --> FwPkg
end
ApiJS -->|HTTP REST| Server
```
### 3.1 Backend modules (`src/*.py`)
| 모듈 | 책임 |
|------|------|
| `server.py` | stdlib `ThreadedHTTPServer` + `ConfigHandler` . 모든 GET/POST 라우팅, 정적파일 서빙, 앱 진입점(`main()`) |
| `db_manager.py` | 3-tier 설정 저장 레이어 — `board_config` 읽기/쓰기는 전부 이곳을 거침 |
| `config_validator.py` | device/protocol config 검증·정규화 (`None` 제거, 비활성 프로토콜 매핑 처리) |
| `log_manager.py` | dpworldapp 앱 로그(`/opt/log/dpworldapp`) 목록·다운로드 아카이브·수동/자동 압축·정리·통계, 그리고 5분 자동압축 데몬 |
| `kernel_log.py` | ** (v1.1.0)** 커널 로그 번들 빌더 — systemd 저널을 `journalctl` 로 텍스트 export + `/opt/log` 평문 커널 로그(`kernel-follow.log`/`wifi-focus.log`/`boot-history`/`pstore`) 사본 + 매니페스트 → `.tar.gz` 한 파일. journald 디렉터리(`/opt/log/journal/`)는 읽기 전용 (텍스트 export 방식). 시작 시 `sweep_stale_temp_dirs()` 로 잔여 임시디렉터리 정리 |
| `system_status.py` | Home 대시보드용 디바이스 헬스 스냅샷 집계. v1.0.x 기본(core_app·communication·network·system) + ** (v1.2.0)** `hardware_modules` (GNSS FW + WiFi FW, `/lib/firmware/amss.bin` 의 `QC_IMAGE_VERSION_STRING` 토큰 + dpworldapp 로그의 `[GNSS FW VER : ...]` ) + ** (v1.2.1)** chunk-boundary 안전 line-iter 스캐너로 교체 (FWVE 16KB tail 누락 버그 fix) + ** (v1.3.0)** `dpworldapp_status` (7개 startup phase tracker + 5개 result 상태 [healthy/starting/hung/failed/unknown] + 6개 runtime config 필드 `equipment` ·`protocol+endpoint`·`can_input/type/speed`·`speed_data`·`odo_speed.source`·`odo_dir.source`). 모든 신규 필드는 module-level cache + `threading.Lock` 로 lazy-load (서비스 재시작 전까지 영구 캐시) |
| `support_bundle.py` | 진단 zip 생성 (상태 + 마스킹된 config + 최신 로그 + OS 진단) — "빠른 스냅샷" 용도. 전체 커널 저널이 필요하면 `kernel_log.py` 의 별도 번들 사용 |
| `config_validator.py` | device/protocol config 검증·정규화 (`None` 제거, 비활성 프로토콜 매핑 처리) — dpworldapp 가 기대하는 JSON 형식 보장 |
| `enum_normalizer.py` | enum 값의 case-only 입력 정규화(`"ON"→"on"`). 의미 추측은 의도적 제외 |
| `dpworldapp_enums.py` | device/protocol enum 허용집합 — 배포 바이너리 검증 계약과 일치. validator 가 reject 판정에 사용 |
| `dpworldapp_telemetry.py` | ** (v1.5.5)** dpworldapp telemetry 스트림(TCP **8989** ) 파서 — connect→헤더 frame parse→disconnect 의 stateless query 로 정적 정보(firmware version/MAC/IP) 수집 |
| `migrations.py` | 기동 시 1회 실행 마이그레이션 — `schema_meta` 테이블로 게이트(`board_config` 경합 회피). CanSpeed Integer 정합 등 |
### 3.1.1 `src/network/` — Network Apply Engine + Wi-Fi AP (v1.6.0~)
웹에서 입력한 네트워크 22항목을 **dpworldapp 파일 계약과 byte-exact 로 렌더**해 즉시 적용·검증·롤백하고, 상시 watchdog 로 자가복구한다. Wi-Fi AP(소프트 AP, ap0) 서브시스템도 같은 패키지에 있다.
| 모듈 | 책임 |
|------|------|
| `apply_engine.py` | apply 상태머신(STAGING→APPLYING→VERIFYING→CONFIRM_WAIT→COMMITTED). 단일 in-flight, DB-first 쓰기, confirm TTL(90s) 타이머, 크래시 복구, country split-apply(비country 즉시 / country deferred) |
| `netmodel.py` | 22항목 집합 — DB `device_config` ↔ intent ↔ persist JSON 매핑 + dpworldapp 네트워크-필드 비교 정합 의미론 |
| `renderer.py` | intent → dpworldapp byte-호환 파일 렌더(캡처 golden 과 1바이트도 안 틀리게) |
| `validator.py` | §5.2 hard rule — SSID/PSK 바이트 길이·security·IPv4 등 dpworldapp 파서 한계 위반 차단 |
| `snapshot.py` | apply 전 백업 + manifest 해시 검증 + 롤백 복원(last-known-good) |
| `verifier.py` | 사후 검증(존재→carrier→주소·라우트→wpa_state→gateway ping). carrier 없음 = "config staged" WARN |
| `watchdog.py` | 상시 감시·자가복구(30s 틱, 히스테리시스·cooldown·시간당 한도). 적용본(`network_config.json`) 기준 |
| `journal.py` | JSONL 네트워크 이벤트 저널(5MB×3 로테이션, psk/password 마스킹) + forensic 번들 |
| `net_routes.py` | `/api/network/*` 라우트 글루(서버 독립적·dict in/out) |
| `ap_engine.py` | **Wi-Fi AP** apply 오케스트레이션 — hostapd/udhcpd conf 렌더 + `dpworld-ap-apply.service` 트리거 + 상태 persist. SCC(STA 채널 추종)·marker kill-switch |
| `ap_model.py` | `ap_config` DB 키 ↔ 정규화 intent (별도 board_config 키, `device_config` 무관) |
| `ap_renderer.py` | intent → `hostapd-ap0.conf` / `udhcpd-ap0.conf` 렌더(WPA2-PSK) |
| `ap_validator.py` | AP 필드 hard rule(SSID/PSK/채널/country) |
| `ap_routes.py` | `/api/network/ap/*` 라우트 글루. status 응답에서 `ap_passphrase` 제거(미인증 API) |
> 상세는 §5 API 표(Network Apply) 및 Wi-Fi AP는 [`wifi-ap-guide.md`](wifi-ap-guide.md) 참조.
### 3.1.2 `src/firmware/` — Firmware OTA (v1.5.0 Phase 4a~)
dpworldapp 의 FW-MMI 채널(TCP **8990** )로 펌웨어 컴포넌트를 staging→flash 하는 OTA 서브시스템. 플래시 전 config 백업, 재부팅 후 복원 검사까지 오케스트레이션한다.
| 모듈 | 책임 |
|------|------|
| `fw_controller.py` | 상태머신·8단계 phase stepper(upload·verify·preflight·backup·flash·commit·reboot·config) + 컴포넌트별 진행률. `FirmwareController` 단일 인스턴스, thread-safe |
| `staging.py` | 업로드 ZIP 추출·컴포넌트 식별(.rom/.img/.ext4/.dtb)·sha256·free-space 확인 |
| `protocol.py` | dpworldapp FW-MMI 와이어 프로토콜(3바이트 signature + 8바이트 LE size + payload, ACK `SUCCESS` /`FW_FAIL`) |
| `fw_client.py` | TCP 8990 wire client — 컴포넌트 전송 + ACK 분류 + 진행 콜백 |
| `config_safety.py` | 플래시 전 `device_config` /`protocol_config` 백업, 재부팅 후 factory-default reseed 감지 → 원자적 복원(단일 BEGIN IMMEDIATE 트랜잭션) |
| `fw_routes.py` | `/api/firmware/*` 라우트 글루 — 스트리밍 ZIP 업로드(디스크 `/opt` 버퍼, tmpfs 금지) |
> 상세는 [`firmware-ota-guide.md`](firmware-ota-guide.md) 참조.
### 3.2 Frontend (`src/static/js/`)
- **Core**: `app.js` (라우팅·초기화·저장), `api.js` (fetch 래퍼), `state.js` (상태·flat↔nested 변환), `view-mode.js` (**Two Faces** — User 기본 / Advanced 토글, body 클래스로 `.user-only` /`.advanced-only` 구동)
- **Shared**: `validator.js` , `utils.js` (`escapeHtml`), `toast.js` , `constants.js` (기본값 + `APP_VERSION` ), `country-codes.js` (WiFi 국가코드)
- **`components/`**: `crud-table.js` , `ip-input.js`
- **`pages/`** (~18개): `home` (Dashboard), `wifi` , `wifi-ap` , `ssid` , `ethernet` , `network` /`server-setting`, `general-settings` , `io` /`sensor-io`, `can` /`can-bus`, `modbus` , `opcua` , `register` , `log` , `firmware` (OTA), `net-apply` (Apply & Status — **Advanced 전용** )
- **Two Faces (v1.8.0)**: 기본 **User** 보기(운영자용 간소 동선)와 **Advanced** 보기(네트워크 Apply & Status 등 고급 기능)를 토글한다. `net-apply` 같은 Advanced 전용 페이지는 User 모드에서 nav 숨김 + 라우트 가드로 이중 차단
---
## 4. Data Flow
### 4.1 설정 조회 / 저장
```mermaid
sequenceDiagram
participant B as Browser
participant P as Python Web Configurator
participant D as SQLite DB
B->>P: GET /setting/get-device
P->>D: SELECT device_config, log_config
P-->>B: 200 — 두 키를 병합한 단일 JSON
B->>P: POST /setting/device {JSON}
P->>P: 검증 · None 제거 · 로그설정 분리
P->>D: device_config 저장 (로그설정 제외)
P->>D: log_config 저장 (로그설정 4개)
P-->>B: 200 success
```
> **로그 설정 분리(v1.0.2)**: 클라이언트는 변함없이 하나의 device config를
> 주고받지만, `server.py`가 저장 시 로그 자동압축/정리 설정 4개를 `log_config`
> 키로 떼어내고, 조회 시 다시 합쳐서 내려준다. 이유는 §6.3.
### 4.2 로그 자동압축 데몬
`main()` 이 백그라운드 데몬 스레드를 띄운다. 5분마다:
1. `log_config` 에서 `log_auto_compress` / `log_auto_cleanup` 확인
2. 켜져 있으면 — 비활성(active 아님) 로그를 `<로그명>.<mtime>.tar.gz` 로 압축,
오래된 압축 아카이브를 임계치까지 정리
---
## 5. API Contract
기존 Java 앱과 호환되는 `/setting/*` 엔드포인트 + 신규 `/api/*` 엔드포인트.
| Method | Path | 핸들러 | 용도 |
|--------|------|--------|------|
| `GET` | `/setting/get-device` | `_handle_get_device` | device + log config 조회(병합) |
| `GET` | `/setting/get-protocol` | `_handle_get_protocol` | protocol config 조회 |
| `GET` | `/setting/log-files` | `_handle_get_log_files` | 로그 파일 목록 |
| `GET` | `/setting/log-stats` | `_handle_get_log_stats` | 로그 디스크 사용량 통계 |
| `GET` | `/setting/kernel-bundle` | `_handle_get_kernel_bundle` | ** (v1.1.0)** 커널 로그 번들 `.tar.gz` 즉석 생성·스트리밍 (journalctl 텍스트 export + 평문 커널 로그 + boot-history + pstore + manifest) |
| `GET` | `/api/health` | `_handle_health` | 서버 헬스(uptime·DB 백엔드) |
| `GET` | `/api/mac` | `_handle_get_mac` | WiFi MAC (장비 식별) |
| `GET` | `/api/system-status` | `_handle_system_status` | Home 대시보드 종합 상태. 응답: `core_app` ·`communication`·`network`·`system` (v1.0.x) + `hardware_modules` (v1.2.0, GNSS FW + WiFi FW) + `dpworldapp_status` (v1.3.0, 7 phases + result + runtime config). 모두 top-level key, 후방 호환 |
| `GET` | `/api/support-bundle` | `_handle_support_bundle` | 진단 zip 다운로드 |
| `POST` | `/setting/device` | `_handle_post_device` | device config 저장(로그설정 분리) |
| `POST` | `/setting/protocol` | `_handle_post_protocol` | protocol config 저장 |
| `POST` | `/setting/log-download` | `_handle_post_log_download` | 선택 로그 tar.gz 다운로드 |
| `POST` | `/setting/log-compress` | `_handle_post_log_compress` | 수동 압축 |
| `POST` | `/setting/log-delete` | `_handle_post_log_delete` | 선택 로그 삭제 |
| `POST` | `/api/action/test-connections` | `_handle_test_connections` | 설정된 서버 TCP 연결 테스트 |
| `POST` | `/api/action/restart-dpworldapp` | `_handle_restart_dpworldapp` | dpworldapp 재시작 |
#### Firmware OTA (v1.5.0 Phase 4a) — 상세 [`firmware-ota-guide.md`](firmware-ota-guide.md)
| Method | Path | 용도 |
|--------|------|------|
| `GET` | `/api/firmware/status` | OTA 상태 스냅샷(phase stepper + 컴포넌트 진행률 + slot) |
| `POST` | `/api/firmware/preflight` | 플래시 전 게이트 체크리스트(staging·여유공간·FW 포트·DB·slot) |
| `POST` | `/api/firmware/upload` | 펌웨어 ZIP 스트리밍 업로드 → staging(`/opt` 버퍼) |
| `POST` | `/api/firmware/flash` | staged 컴포넌트 플래시 시작(백그라운드 워커) |
| `POST` | `/api/firmware/restore-check` | 재부팅 후 config reseed 감지 → 복원 + dpworldapp 재시작 |
#### Network Apply (v1.6.0) — 상세 per-subsystem 가이드
| Method | Path | 용도 |
|--------|------|------|
| `GET` | `/api/network/state` | 현재 적용 상태머신 스냅샷 |
| `GET` | `/api/network/drift` | DB ↔ `network_config.json` 22항목 미적용 drift 카운트(경량) |
| `GET` | `/api/network/apply/status` (`?id=`) | 특정 apply 진행 상태 |
| `GET` | `/api/network/journal` (`?limit=`) | 네트워크 이벤트 저널 tail |
| `GET` | `/api/network/config` | watchdog 등 효과적 `net_config` 조회 |
| `POST` | `/api/network/apply` | 22항목 적용 시작(즉시 적용 + confirm 대기) |
| `POST` | `/api/network/apply/confirm` | 적용 확정(미확정 시 TTL 만료 → 자동 롤백) |
| `POST` | `/api/network/rollback` | last-known-good 롤백 |
| `POST` | `/api/network/config` | watchdog kill-switch 등 `net_config` 쓰기 |
#### Wi-Fi AP (v1.11.x) — 상세 [`wifi-ap-guide.md`](wifi-ap-guide.md)
| Method | Path | 용도 |
|--------|------|------|
| `GET` | `/api/network/ap/status` | AP 라이브 상태(`ap_passphrase` 제거됨) |
| `POST` | `/api/network/ap/config` | `ap_config` 저장(필드 화이트리스트 merge) |
| `POST` | `/api/network/ap/apply` | AP bring-up/down 적용(또는 `dry_run` ) |
> 네트워크/AP 서브시스템 import 실패 시(예: Windows dev box) 해당 라우트는 503 으로 fail-soft.
`OPTIONS` 는 CORS preflight(204). 알 수 없는 `/api/` ·`/setting/` 경로는 404(SPA 폴백 마스킹 방지, v1.5.0). 그 외 GET은 SPA 정적파일 폴백.
---
## 6. Database — `board_config` 소유권 ⭐
`~/db/dynamic_data.db` 는 **세 프로그램이 공유**한다. 이 절은 **무엇이 누구
소유인지** 명확히 한다 — 잘못 건드리면 다른 앱의 데이터가 깨진다.
### 6.1 테이블
```sql
CREATE TABLE board_config (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL);
CREATE TABLE event_history (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL);
```
- `board_config` — Key-Value 설정 저장소. Python 이 쓰는 키는 **5개**
(`db_manager.ALLOWED_KEYS`), 그 외 다른 프로그램 소유 키도 같은 테이블에 공존
- `event_history` — 이벤트 큐. **dpworldapp이 WRITE** , Web Configurator는 건드리지 않음
### 6.2 `board_config` 키별 소유권 — 우리 / Java / dpworldapp
| key | 쓰기 주체 | 소유 구분 |
|-----|----------|----------|
| `device_config` | **Python Web Configurator** + **레거시 Java 앱** | ⚠️ **공유(경합)** |
| `protocol_config` | **Python Web Configurator** + **레거시 Java 앱** | ⚠️ **공유 계약** |
| `log_config` | **Python Web Configurator** | 🟦 우리 (**v1.0.2 신설**) |
| `net_config` | **Python Web Configurator** | 🟦 우리 (**v1.6.0** — watchdog 등 Network Apply 설정) |
| `ap_config` | **Python Web Configurator** | 🟦 우리 (**v1.11.x** — Wi-Fi AP 설정) |
| `security_config` | **Super_Relay 프로젝트** | 🟧 타 프로젝트 |
| `transport_config` | **Super_Relay 프로젝트** | 🟧 타 프로젝트 |
- 🟦 **우리(Python Web Configurator)가 쓰는 것** — Python의 `db_manager.ALLOWED_KEYS` 는
`{device_config, protocol_config, log_config, net_config, ap_config}` (**5개**). Python 은
이 5개만 쓴다. `device_config` ·`protocol_config`는 dpworldapp/Java 와의 **공유 계약**
(Java schema 그대로 — [board_config 키 ownership 정책]), `log_config` ·`net_config`·`ap_config`는
**Python 전용** (Java/dpworldapp 가 읽지 않음). Python 전용 키를 별도로 둔 이유는 §6.3.
- 🟧 **다른 프로젝트(Super_Relay)가 쓰는 것** — `security_config` , `transport_config` .
사용자의 별도 프로젝트 **Super_Relay** (`c:/Development/Super_Relay`)가 자체
`config_bridge.py` 로 같은 `board_config` 테이블에 기록한다 — 릴레이 전송 설정
(`transport_config`: `relay_enabled` /`type`/`base_url`; `security_config` :
Bearer 인증 토큰). NEW Web Configurator는 이 두 키를 읽지도 쓰지도 않으며,
레거시 Java 앱과도 무관하다. (2026-05-22 검증: DB의 JSON 내용 +
Super_Relay 프로젝트 grep으로 확인.)
- `event_history` — **dpworldapp** 소유.
### 6.3 ⚠️ `device_config`는 공유 행 — 그래서 `log_config`가 생겼다
`device_config` 는 Python Web Configurator와 레거시 Java 앱이 **둘 다 쓴다** .
Java 앱이 자기 데이터 모델로 `device_config` 를 저장하면 **Java가 모르는
Python 전용 필드가 통째로 사라진다.**
이 때문에 로그 자동압축/정리 설정(`log_auto_compress`, `log_auto_cleanup` ,
`log_cleanup_max_files` , `log_cleanup_max_size_mb` )이 실제로 날아갔고
자동압축 데몬이 조용히 멈췄다. **해결(v1.0.2)** : 이 4개를 Java가 안 건드리는
별도 `log_config` 키로 분리. board_config 키 소유권은 `db_manager.ALLOWED_KEYS` 를 참고.
> **원칙**: 앞으로 추가하는 **Python 전용 설정은 `device_config`/`protocol_config`에
> 넣지 말 것.** Java/dpworldapp 가 덮어쓴다. 별도 키를 만들고 `db_manager.ALLOWED_KEYS`에
> 등록한다 — `log_config`(v1.0.2)·`net_config`(v1.6.0)·`ap_config`(v1.11.x)가 이 패턴이다.
### 6.4 db_manager 3-tier 백엔드
`db_manager.py` 는 환경에 따라 백엔드를 자동 선택한다: ① Python `sqlite3`
모듈(기본), ② `sqlite3` CLI(subprocess), ③ JSON 파일 폴백(임시용). `save_config` 은
"database is locked" 시 신선한 커넥션으로 재시도한다(v1.0.1).
---
## 7. Directory Structure
```
NEW_Web_Configurator/
├── src/ ← 애플리케이션 (배포 단위)
│ ├── server.py ← 진입점, HTTP 서버
│ ├── db_manager.py ← SQLite 3-tier 접근
│ ├── config_validator.py ← 서버 검증
│ ├── enum_normalizer.py ← enum case-only 정규화
│ ├── dpworldapp_enums.py ← enum 허용집합 (device 계약)
│ ├── dpworldapp_telemetry.py ← TCP 8989 telemetry 파서
│ ├── migrations.py ← 기동 시 1회 마이그레이션
│ ├── log_manager.py ← 로그 관리 + 자동압축 데몬
│ ├── kernel_log.py ← 커널 로그 번들
│ ├── system_status.py ← 대시보드 상태 집계
│ ├── support_bundle.py ← 진단 zip
│ ├── network/ ← Network Apply Engine + Wi-Fi AP
│ │ ├── apply_engine·netmodel·renderer·validator
│ │ ├── snapshot·verifier·watchdog·journal·net_routes
│ │ └── ap_engine·ap_model·ap_renderer·ap_validator·ap_routes
│ ├── firmware/ ← Firmware OTA (TCP 8990)
│ │ └── fw_controller·fw_client·protocol·staging·config_safety·fw_routes
│ └── static/
│ ├── index.html
│ ├── css/style.css
│ ├── img/dp-world-logo.svg
│ └── js/ app·api·state·view-mode·validator·utils·toast·constants·country-codes
│ ├── components/ crud-table·ip-input
│ └── pages/ home·wifi·wifi-ap·ssid·ethernet·io·can·modbus·
│ opcua·register·log·firmware·net-apply· … (~18)
├── deploy/ ← systemd 유닛 + AP/network apply 셸 스크립트
├── tests/ ← pytest(~1557) + node/jsdom(~392) 스위트
├── scripts/deploy.ps1 ← 버전 태그 기반 디바이스 배포 스크립트
├── docs/ ← 문서 (본 문서, wifi-ap-guide, firmware-ota-guide, specs/, …)
├── CHANGELOG.md README.md .gitignore
```
> 디바이스 배포 위치: `/usr/lib/web-configurator/src/` (systemd가 구동, port 9090).
> `deploy/`의 systemd 유닛·셸 스크립트는 rootfs `/lib/systemd` 등에 설치되며 **flash 마다 wipe**
> 된다(§10 caveat, [`wifi-ap-guide.md`](wifi-ap-guide.md) 참조).
---
## 8. Versioning & Deployment
- **버전 체계**: `vMAJOR.MINOR.PATCH` 시맨틱 버전. 단일 소스 = `APP_VERSION`
(`src/static/js/constants.js`), 사이드바 표시. 현재 **v1.11.10** .
- **릴리스**: `APP_VERSION` 갱신 → `CHANGELOG.md` 항목 → 커밋 → `git tag vX.Y.Z` .
- **배포**: `scripts/deploy.ps1 <device-ip>` — 버전 태그 검사 → `src/` 아카이브
전송 → 장비 백업 → 스왑 → `DEPLOYED_VERSION` 기록 → systemd 재시작 → 검증.
절차·롤백 상세는 [`DEPLOY.md` ](DEPLOY.md ).
---
## 9. 기술 제약사항
| 항목 | 제약 | 대응 |
|------|------|------|
| Python | stdlib만 (pip 없음) | `http.server` ·`sqlite3`·`json`·`tarfile` 등 |
| 메모리 | 목표 경량 | 레거시 Java ~436MB → Python ~15–30MB |
| 빌드 도구 | Node/npm 없음 | Vanilla ES-module JS (빌드 불필요) |
| 공유 DB | Java 앱·dpworldapp과 SQLite 공유 | §6 소유권 규칙 준수, 별도 키 분리 |
| 디바이스 구동 | systemd `web-configurator.service` :9090 | `DEPLOY.md` 참고 |
---
## 10. 보안 고려사항
| 현재 상태 | 비고 |
|-----------|------|
| **인증 없음** | HTTP API(:9090)는 현재 **완전 미인증** — 17개 mutation endpoint + wildcard CORS. 이것이 **문서화된 threat boundary** (폐쇄 LAN 단일 운영자 전제). 운영자 로그인은 설계 완료·구현 백로그 상태. |
| CORS 전체 허용 | 의도된 설계 (IoT LAN 전용) |
| HTTP only | TLS 미구현. 운영자 로그인 spec 의 known-limitation(평문) 참조 |
| SQL Injection | `db_manager` 가 키 화이트리스트(`ALLOWED_KEYS`)로 방어 |
| XSS | 출력은 `escapeHtml` 경유 |
| Support bundle | config 내 WiFi 비밀번호 마스킹 후 포함 |
| API 응답 마스킹 | AP `status` /`apply(dry_run)` 응답에서 `ap_passphrase` 제거. `/api/health` 는 db path/pid 미노출 |
| Wi-Fi AP 접근 게이트 | v1.11.7 이후 AP INPUT 전면 개방(SSH 포함) — **WPA2 PSK 가 게이트** . FORWARD 차단으로 PLC/업링크 격리([`wifi-ap-guide.md`](wifi-ap-guide.md)) |
> **이전 메커니즘 제거됨**: 과거 문서가 언급하던 systemd `WEB_AUTH_USER/PASS`(옵션 Basic 인증)
> 및 `WEB_SSL_CERT/KEY`(옵션 HTTPS) 환경변수 인증은 **현재 코드/유닛에 존재하지 않는다**.
> 인증 방향은 위의 operator-login 설계로 대체되었다(아직 미구현).
---
## 11. 변경 이력
| 일자 | 버전 | 변경 |
|------|------|------|
| 2026-02-12 | doc v1.0 | 초기 작성 |
| 2026-02-19 | doc v1.1 | 디렉터리·보안·건강상태 섹션 |
| 2026-05-22 | doc v2.0 | 앱 v1.0.2 기준 전면 갱신 — 실제 배포 구조(systemd :9090), 6개 백엔드 모듈·9개 페이지, 전체 API 라우트, §6 board_config 소유권(우리/Java/Super_Relay/dpworldapp 구분), `log_config` 분리, 버전관리·배포 섹션 |
| 2026-05-26 | doc v2.1 | 앱 v1.1.0 — Kernel Log Bundle 기능 — 신규 모듈 `kernel_log.py` (§3.1), 신규 엔드포인트 `GET /setting/kernel-bundle` (§5). `/opt/log/journal/` 은 journald 소유로 읽기 전용 처리(텍스트 export). 임시작업 디렉터리는 `/opt` 디스크에 (메모리 제약 장비에서 `/tmp` tmpfs 회피). DB 스키마·소유권 변경 없음 |
| 2026-05-28 | **doc v2.2** | **앱 v1.2.0 → v1.2.1 → v1.3.0 누적 반영** . v1.2.0 Hardware Modules 카드 — Home 대시보드에 GNSS·WiFi 펌웨어 버전 표시 (`amss.bin` `QC_IMAGE_VERSION_STRING` 추출 + dpworldapp 로그 `[GNSS FW VER]` 스캔), `/api/system-status` 에 `hardware_modules` 키 추가. v1.2.1 scanner 견고화 — 64KB chunk-boundary silent miss + 10MB cap 두 결함을 line-iter 기반으로 일괄 해결, locale-independent ISO date 파싱. v1.3.0 dpworldapp Status Tracker — 7 startup phase (FRAM→Steady) + 5 result 상태 + 6 runtime config 필드, `/api/system-status` 에 `dpworldapp_status` 키 추가. 모두 module-level cache + `threading.Lock` . §3.1 `system_status.py` 책임 갱신, §5 payload key 목록 갱신. DB 스키마·소유권 변경 없음 |
| 2026-06-20 | **doc v3.0** | **앱 v1.11.10 전면 동기화** (v1.4~v1.11 누적). 신규 백엔드 서브시스템 2종 — `src/network/` (Network Apply Engine v1.6.0 + Wi-Fi AP v1.11.x)·`src/firmware/`(Firmware OTA v1.5.0 Phase 4a) 및 신규 `src/*.py` (enum_normalizer·dpworldapp_enums·dpworldapp_telemetry·migrations) §3.1 추가. §5 API 표에 firmware/network/AP 엔드포인트 추가. §6 `ALLOWED_KEYS` 5키로 정정(`net_config`·`ap_config` Python 전용). §3.2 프론트 ~18 페이지 + Two Faces(User/Advanced). §7 디렉터리 트리 갱신. §10 보안 — 제거된 `WEB_AUTH_USER/PASS` /`WEB_SSL` 언급 삭제, API 미인증=문서화된 threat boundary 명시 + operator-login 설계(백로그) 링크. 신규 가이드 [`wifi-ap-guide.md`]·[`firmware-ota-guide.md`] 분리 |
> 관련 문서: [`DEPLOY.md`](DEPLOY.md) (배포·버전 런북) ·
> [`CHANGELOG.md`](../CHANGELOG.md) (릴리스 변경 이력)