# 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["운영자
(브라우저)"] Operator -->|":9090 직접"| WebCfg Operator -->|":80 → nginx"| JavaApp subgraph Device["IoT Device — TCC8030 (192.168.55.x)"] WebCfg["Python Web Configurator
systemd: web-configurator.service
:9090"] JavaApp["레거시 Java app-runner
:8080 (nginx :80 프록시)"] dpworld["dpworldapp
CAN/Modbus/OPC-UA 데이터 처리"] DB[("SQLite
~/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 페이지
(home·wifi·wifi-ap·ethernet·io·
can·modbus·opcua·register·log·
firmware·net-apply·…)"] Shared["validator·utils·toast·constants·
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 아님) 로그를 `<로그명>..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 ``` > 디바이스 배포 위치: `/opt/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 ` — 버전 태그 검사 → `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) (릴리스 변경 이력)