# Three-Panel Web Restructure And Handler Migration > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Restructure each app's UI into three logical panels (platform-config / app-config / ops), move stateless handlers (log, doc) to the shared core, and split API.md per application. **Architecture:** Each app gets three tabs: "运维" (ops view), "应用配置" (app-specific config), "平台配置" (shared platform config). Core HTML pages are cleaned of unit references. Feeder overrides core pages that need unit-related UI via the ServeDir fallback chain. Stateless handlers (log, doc) move to `plc_platform_core::handler` so both apps can register them. Each app serves its own API doc. Handlers that depend on `AppState` (source, point, equipment, tag, page) stay in the feeder app — they require the `PlatformContext` refactor to move, which is deferred to a follow-up plan. **Tech Stack:** Rust (Axum, tower-http), HTML/CSS/JS (vanilla ES modules), Cargo workspace --- ## File Map ### Core HTML changes (remove unit references) - Modify: `web/core/html/source-panel.html` — remove unit list section, keep data source only - Modify: `web/core/html/equipment-panel.html` — remove batch-unit toolbar - Modify: `web/core/html/modals.html` — remove unit select from equipment modal ### Feeder HTML overrides (add unit references back) - Create: `web/feeder/html/source-panel.html` — override with units + sources stacked - Create: `web/feeder/html/equipment-panel.html` — override with unit batch toolbar - Create: `web/feeder/html/modals.html` — override with unit select in equipment modal - Create: `web/feeder/html/unit-panel.html` — standalone unit config panel for app-config view ### Feeder three-tab UI - Modify: `web/feeder/html/topbar.html` — add third tab - Modify: `web/feeder/index.html` — add unit-panel partial, add grid-app-config layout - Modify: `web/feeder/js/app.js` — three-way view switching - Modify: `web/feeder/js/dom.js` — add tabAppConfig selector - Modify: `web/core/styles.css` — add grid-app-config layout ### Handler migration to core - Create: `crates/plc_platform_core/src/handler.rs` — module declarations - Create: `crates/plc_platform_core/src/handler/log.rs` — moved from feeder - Create: `crates/plc_platform_core/src/handler/doc.rs` — generic markdown serving - Modify: `crates/plc_platform_core/src/lib.rs` — export handler module - Modify: `crates/plc_platform_core/Cargo.toml` — add async-stream if needed - Modify: `crates/app_feeder_distributor/src/handler.rs` — replace log/doc with re-exports - Modify: `crates/app_feeder_distributor/src/handler/doc.rs` — thin wrapper calling core - Delete: `crates/app_feeder_distributor/src/handler/log.rs` — replaced by core ### Ops app handler registration - Modify: `crates/app_operation_system/src/lib.rs` — add handler module - Create: `crates/app_operation_system/src/handler.rs` — module declarations - Create: `crates/app_operation_system/src/handler/doc.rs` — ops-specific doc handler - Modify: `crates/app_operation_system/src/router.rs` — register log/doc routes - Modify: `crates/app_operation_system/Cargo.toml` — add needed deps ### API.md split - Rename: `API.md` → `docs/api-feeder.md` - Create: `docs/api-ops.md` — ops API (health endpoint for now) --- ## Task 1: Clean Core HTML — Remove Unit References **Files:** - Modify: `web/core/html/source-panel.html` - Modify: `web/core/html/equipment-panel.html` - Modify: `web/core/html/modals.html` - [ ] **Step 1: Rewrite core source-panel to data sources only** Replace `web/core/html/source-panel.html` with: ```html

数据源

``` - [ ] **Step 2: Remove batch-unit toolbar from core equipment-panel** Replace `web/core/html/equipment-panel.html` with: ```html

设备

``` - [ ] **Step 3: Remove unit select from core equipment modal** In `web/core/html/modals.html`, remove the unit select label from the equipment modal. Replace the equipment modal section (lines 1-35) with: ```html ``` Keep the rest of modals.html (point, source, binding modals) unchanged. - [ ] **Step 4: Commit** ```bash git add web/core/html/source-panel.html web/core/html/equipment-panel.html web/core/html/modals.html git commit -m "refactor(web): remove unit references from core HTML pages" ``` ## Task 2: Create Feeder Override Pages With Unit References **Files:** - Create: `web/feeder/html/source-panel.html` - Create: `web/feeder/html/equipment-panel.html` - Create: `web/feeder/html/modals.html` - [ ] **Step 1: Create feeder source-panel override (units + sources stacked)** `web/feeder/html/source-panel.html` — same as the original file with both sections: ```html

控制单元

数据源

``` - [ ] **Step 2: Create feeder equipment-panel override (with unit batch toolbar)** `web/feeder/html/equipment-panel.html` — same as original with batch-unit toolbar: ```html

设备

已选 0 台设备
``` - [ ] **Step 3: Create feeder modals override (with unit select in equipment modal)** `web/feeder/html/modals.html` — copy of the ORIGINAL core modals.html (before Task 1 removes the unit select). This file overrides the core version via the ServeDir fallback chain. The equipment modal retains the unit select field that Task 1 removes from core. Write the full file with this content (identical to the original core modals.html): ```html ``` - [ ] **Step 4: Commit** ```bash git add web/feeder/html/source-panel.html web/feeder/html/equipment-panel.html web/feeder/html/modals.html git commit -m "refactor(web): create feeder overrides for unit-dependent pages" ``` ## Task 3: Create Feeder Unit Panel And Add Three-Tab UI **Files:** - Create: `web/feeder/html/unit-panel.html` - Modify: `web/feeder/html/topbar.html` - Modify: `web/feeder/index.html` - Modify: `web/feeder/js/dom.js` - Modify: `web/feeder/js/app.js` - Modify: `web/core/styles.css` - [ ] **Step 1: Create standalone unit-panel for app-config view** `web/feeder/html/unit-panel.html`: ```html

控制单元配置

``` Note: Uses separate IDs (`refreshUnitBtn2`, `newUnitBtn2`, `unitConfigList`) to avoid DOM ID conflicts with the unit list in the source-panel override. The JS wiring will connect these to the same handler functions. - [ ] **Step 2: Update topbar for three tabs** Replace `web/feeder/html/topbar.html`: ```html
投煤器布料机控制系统
连接中…
``` - [ ] **Step 3: Replace feeder index.html `
` contents to include unit-panel partial** Replace the entire `
` block in `web/feeder/index.html`: ```html
``` - [ ] **Step 4: Add grid-app-config layout to styles.css** Add after the existing `.grid-ops` rule in `web/core/styles.css`: ```css .grid-app-config { display: grid; gap: 1px; height: calc(100vh - var(--topbar-h)); grid-template-columns: 1fr; grid-template-rows: 1fr; } .grid-app-config .panel.app-config-main { grid-column: 1; grid-row: 1; } ``` Also add the responsive override inside the existing `@media (max-width: 900px)` block: ```css .grid-app-config { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; } ``` - [ ] **Step 5: Add tabAppConfig to dom.js** In `web/feeder/js/dom.js`, add: ```javascript tabAppConfig: byId("tabAppConfig"), refreshUnitBtn2: byId("refreshUnitBtn2"), newUnitBtn2: byId("newUnitBtn2"), unitConfigList: byId("unitConfigList"), ``` - [ ] **Step 6: Update app.js for three-way view switching** Replace the `switchView` function in `web/feeder/js/app.js`: ```javascript function switchView(view) { state.activeView = view; const main = document.querySelector("main"); main.className = view === "ops" ? "grid-ops" : view === "app-config" ? "grid-app-config" : "grid-config"; dom.tabOps.classList.toggle("active", view === "ops"); dom.tabAppConfig.classList.toggle("active", view === "app-config"); dom.tabConfig.classList.toggle("active", view === "config"); // config-only panels (platform config view) ["top-left", "top-right", "bottom-left", "bottom-right"].forEach((cls) => { const el = main.querySelector(`.panel.${cls}`); if (el) el.classList.toggle("hidden", view !== "config"); }); const logStreamPanel = main.querySelector(".panel.bottom-mid"); if (logStreamPanel) logStreamPanel.classList.toggle("hidden", view !== "config"); // ops-only panels const opsMain = main.querySelector(".panel.ops-main"); const opsBottom = main.querySelector(".panel.ops-bottom"); if (opsMain) opsMain.classList.toggle("hidden", view !== "ops"); if (opsBottom) opsBottom.classList.toggle("hidden", view !== "ops"); // app-config-only panels const appConfigMain = main.querySelector(".panel.app-config-main"); if (appConfigMain) appConfigMain.classList.toggle("hidden", view !== "app-config"); if (view === "config") { startLogs(); if (!_configLoaded) { _configLoaded = true; withStatus((async () => { await Promise.all([loadSources(), loadEquipments(), loadEvents()]); await loadPoints(); })()); } } else { stopLogs(); } if (view === "app-config") { if (!_appConfigLoaded) { _appConfigLoaded = true; withStatus(loadUnits()); } } } ``` Add `let _appConfigLoaded = false;` alongside the existing `let _configLoaded = false;`. Update `bindEvents` to add: ```javascript dom.tabAppConfig.addEventListener("click", () => switchView("app-config")); dom.refreshUnitBtn2.addEventListener("click", () => withStatus(loadUnits())); dom.newUnitBtn2.addEventListener("click", openCreateUnitModal); ``` - [ ] **Step 7: Verify no broken partials** Run feeder locally, check all three tabs load without console errors. Run: ```bash cargo check -p app_feeder_distributor ``` Expected: PASS (no Rust changes in this task) - [ ] **Step 8: Commit** ```bash git add web/feeder/html/unit-panel.html web/feeder/html/topbar.html web/feeder/index.html web/feeder/js/dom.js web/feeder/js/app.js web/core/styles.css git commit -m "feat(feeder): add three-tab UI (ops / app-config / platform-config)" ``` ## Task 4: Move Log Handler To Core **Files:** - Create: `crates/plc_platform_core/src/handler.rs` - Create: `crates/plc_platform_core/src/handler/log.rs` - Modify: `crates/plc_platform_core/src/lib.rs` - Modify: `crates/app_feeder_distributor/src/handler.rs` - Delete: `crates/app_feeder_distributor/src/handler/log.rs` - [ ] **Step 1: Create core handler module** `crates/plc_platform_core/src/handler.rs`: ```rust pub mod log; ``` - [ ] **Step 2: Move log.rs to core** Copy `crates/app_feeder_distributor/src/handler/log.rs` to `crates/plc_platform_core/src/handler/log.rs`. Change the import path from `plc_platform_core::util::response::ApiErr` to `crate::util::response::ApiErr`. - [ ] **Step 3: Export handler module from core lib.rs** Add `pub mod handler;` to `crates/plc_platform_core/src/lib.rs`. - [ ] **Step 4: Update feeder handler.rs to re-export log from core** In `crates/app_feeder_distributor/src/handler.rs`, replace: ```rust pub mod log; ``` with: ```rust pub mod log { pub use plc_platform_core::handler::log::*; } ``` Delete `crates/app_feeder_distributor/src/handler/log.rs`. - [ ] **Step 5: Verify feeder compiles** Run: ```bash cargo check -p app_feeder_distributor ``` Expected: PASS - [ ] **Step 6: Commit** ```bash git add crates/plc_platform_core/src/handler.rs crates/plc_platform_core/src/handler/log.rs crates/plc_platform_core/src/lib.rs crates/app_feeder_distributor/src/handler.rs git rm crates/app_feeder_distributor/src/handler/log.rs git commit -m "refactor(core): move log handler to shared platform core" ``` ## Task 5: Move Doc Handler To Core And Split API.md **Files:** - Create: `crates/plc_platform_core/src/handler/doc.rs` - Modify: `crates/plc_platform_core/src/handler.rs` - Modify: `crates/app_feeder_distributor/src/handler.rs` - Modify: `crates/app_feeder_distributor/src/handler/doc.rs` - Create: `crates/app_operation_system/src/handler.rs` - Create: `crates/app_operation_system/src/handler/doc.rs` - Rename: `API.md` → `docs/api-feeder.md` - Create: `docs/api-ops.md` - [ ] **Step 1: Create core generic markdown serving utility** `crates/plc_platform_core/src/handler/doc.rs`: ```rust use axum::{ http::{header, HeaderMap, HeaderValue, StatusCode}, response::IntoResponse, }; use crate::util::response::ApiErr; pub async fn serve_markdown(path: &str) -> Result { let content = tokio::fs::read_to_string(path) .await .map_err(|err| { tracing::error!("Failed to read {}: {}", path, err); ApiErr::NotFound(format!("{} not found", path), None) })?; let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, HeaderValue::from_static("text/markdown; charset=utf-8"), ); Ok((StatusCode::OK, headers, content)) } ``` - [ ] **Step 2: Add doc to core handler module** In `crates/plc_platform_core/src/handler.rs`, add: ```rust pub mod doc; pub mod log; ``` - [ ] **Step 3: Update feeder doc handler to use core utility** Replace `crates/app_feeder_distributor/src/handler/doc.rs`: ```rust use axum::response::IntoResponse; use plc_platform_core::util::response::ApiErr; pub async fn get_api_md() -> Result { plc_platform_core::handler::doc::serve_markdown("docs/api-feeder.md").await } pub async fn get_readme_md() -> Result { plc_platform_core::handler::doc::serve_markdown("README.md").await } ``` - [ ] **Step 4: Rename API.md to docs/api-feeder.md** ```bash git mv API.md docs/api-feeder.md ``` - [ ] **Step 5: Create ops API doc** `docs/api-ops.md`: ```markdown # 运转系统 API ## 健康检查 - `GET /api/health` — 返回应用名称和状态 ## 日志 - `GET /api/logs` — 拉取日志内容 - `GET /api/logs/stream` — SSE 增量推送 ## 文档 - `GET /api/docs/api-md` — 获取 API 文档 - `GET /api/docs/readme-md` — 获取 README ``` - [ ] **Step 6: Create ops handler module with doc handler** `crates/app_operation_system/src/handler.rs`: ```rust pub mod doc; ``` `crates/app_operation_system/src/handler/doc.rs`: ```rust use axum::response::IntoResponse; use plc_platform_core::util::response::ApiErr; pub async fn get_api_md() -> Result { plc_platform_core::handler::doc::serve_markdown("docs/api-ops.md").await } pub async fn get_readme_md() -> Result { plc_platform_core::handler::doc::serve_markdown("README.md").await } ``` Add `pub mod handler;` to `crates/app_operation_system/src/lib.rs`. - [ ] **Step 7: Verify both apps compile** Run: ```bash cargo check -p app_feeder_distributor cargo check -p app_operation_system ``` Expected: both PASS - [ ] **Step 8: Commit** ```bash git add crates/plc_platform_core/src/handler/doc.rs crates/plc_platform_core/src/handler.rs crates/app_feeder_distributor/src/handler/doc.rs crates/app_operation_system/src/handler.rs crates/app_operation_system/src/handler/doc.rs crates/app_operation_system/src/lib.rs crates/app_operation_system/Cargo.toml docs/api-feeder.md docs/api-ops.md git commit -m "refactor(core): move doc handler to core and split API.md per app" ``` ## Task 6: Register Core Handlers In Ops App Router **Files:** - Modify: `crates/app_operation_system/src/router.rs` - [ ] **Step 1: Add log and doc routes to ops router** Update `crates/app_operation_system/src/router.rs`: ```rust use axum::{extract::State, routing::get, Router}; use tower_http::services::ServeDir; use crate::app::AppState; async fn no_cache( req: axum::extract::Request, next: axum::middleware::Next, ) -> axum::response::Response { let mut response = next.run(req).await; response.headers_mut().insert( axum::http::header::CACHE_CONTROL, axum::http::HeaderValue::from_static("no-store"), ); response } pub fn build_router(state: AppState) -> Router { Router::new() .route("/api/health", get(health_check)) .route("/api/logs", get(plc_platform_core::handler::log::get_logs)) .route("/api/logs/stream", get(plc_platform_core::handler::log::stream_logs)) .route("/api/docs/api-md", get(crate::handler::doc::get_api_md)) .route("/api/docs/readme-md", get(crate::handler::doc::get_readme_md)) .nest( "/ui", Router::new() .fallback_service( ServeDir::new("web/ops") .append_index_html_on_directories(true) .fallback(ServeDir::new("web/core")), ) .layer(axum::middleware::from_fn(no_cache)), ) .with_state(state) } async fn health_check(State(state): State) -> String { format!("{}:ok", state.app_name) } ``` - [ ] **Step 2: Verify ops compiles and tests pass** Run: ```bash cargo check -p app_operation_system cargo test -p app_operation_system ``` Expected: both PASS - [ ] **Step 3: Commit** ```bash git add crates/app_operation_system/src/router.rs git commit -m "feat(ops): register log and doc routes from shared core" ``` ## Task 7: Update Feeder Router API Doc Path Reference **Files:** - Modify: `crates/app_feeder_distributor/src/router.rs` (no change if doc handler wrapper handles path) - [ ] **Step 1: Verify feeder doc route still works with renamed file** The feeder doc handler now reads `docs/api-feeder.md` instead of `API.md`. The route `/api/docs/api-md` stays the same — only the file path changed inside the handler. Run: ```bash cargo test -p app_feeder_distributor ``` Expected: PASS - [ ] **Step 2: Update README reference to API.md** In `README.md`, replace the doc index entry: Before: ```markdown - API 接口说明: `API.md` ``` After: ```markdown - 投煤器布料机 API: `docs/api-feeder.md` - 运转系统 API: `docs/api-ops.md` ``` - [ ] **Step 3: Commit** ```bash git add README.md git commit -m "docs: update API doc references for per-app split" ``` ## Task 8: Final Verification - [ ] **Step 1: Run all workspace tests** Run: ```bash cargo test --workspace ``` Expected: all PASS - [ ] **Step 2: Run release builds** Run: ```bash cargo build -p app_feeder_distributor --release cargo build -p app_operation_system --release ``` Expected: both produce binaries - [ ] **Step 3: Verify web file layout** Run: ```bash find web -type f | sort ``` Expected: core pages have no unit references, feeder overrides contain unit references, both apps have their own topbar and index. - [ ] **Step 4: Verify core HTML has no unit references** Run: ```bash grep -ri "unit" web/core/ --include="*.html" ``` Expected: no matches ## Self-Review ### Coverage - Three-panel UI (ops / app-config / platform-config): Tasks 1–3 - Core HTML cleaned of unit references: Task 1 - Feeder overrides via fallback chain: Task 2 - Log handler to core: Task 4 - Doc handler to core + API.md split: Tasks 5, 7 - Ops app gets log/doc routes: Task 6 - Build verification: Task 8 ### What this plan does NOT cover (deferred) - **PlatformContext completion**: Filling in pool/connection_manager/event_manager/ws_manager in the core context struct. This is a prerequisite for moving the remaining handlers (source, point, equipment, tag, page) to core. - **Remaining handler migration**: source.rs (626 lines), point.rs (693 lines), equipment.rs (335 lines), tag.rs (126 lines), page.rs (169 lines) all depend on `AppState` and require PlatformContext to move to core. - **Unit-panel JS wiring**: The standalone unit-panel in app-config view needs JS to render unit list and handle CRUD. Currently the unit rendering logic in `units.js` targets `#unitList` (in source-panel). Wiring `#unitConfigList` to the same data is a follow-up JS task. ### Key design decisions - **ServeDir fallback override pattern**: Feeder overrides core pages by placing same-named files in `web/feeder/html/`. The fallback chain tries app dir first. This means core files are the "clean" base, feeder adds business-specific UI on top. - **Separate unit-panel IDs**: Uses `refreshUnitBtn2`, `newUnitBtn2`, `unitConfigList` to avoid conflicts with the unit list embedded in the feeder source-panel override. Both are wired to the same `loadUnits()` / `openCreateUnitModal()` functions. - **Log handler is fully stateless**: Reads files from `./logs` directory. No AppState dependency. Trivially movable to core. - **Doc handler split**: Core provides `serve_markdown(path)` utility. Each app wraps it to point to its own API doc file.