plc_control/docs/superpowers/plans/2026-04-17-three-panel-and-...

929 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
<section class="panel bottom-left">
<div class="panel-head">
<h2>数据源</h2>
<button type="button" id="openSourceForm">+ 新增</button>
</div>
<div class="source-panels" id="sourceList"></div>
</section>
```
- [ ] **Step 2: Remove batch-unit toolbar from core equipment-panel**
Replace `web/core/html/equipment-panel.html` with:
```html
<section class="panel top-left">
<div class="panel-head">
<h2>设备</h2>
<button type="button" id="newEquipmentBtn">+ 新增</button>
</div>
<div class="toolbar equipment-toolbar">
<input id="equipmentKeyword" placeholder="搜索编码或名称" />
<button type="button" class="secondary" id="refreshEquipmentBtn">刷新</button>
</div>
<div class="list equipment-list" id="equipmentList"></div>
</section>
```
- [ ] **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
<div class="modal hidden" id="equipmentModal">
<div class="modal-content modal-sm">
<div class="modal-head">
<h3>设备配置</h3>
<button class="secondary" id="closeEquipmentModal">X</button>
</div>
<form id="equipmentForm" class="form">
<input type="hidden" id="equipmentId" />
<label>
编码
<input id="equipmentCode" required />
</label>
<label>
名称
<input id="equipmentName" required />
</label>
<label>
类型
<select id="equipmentKind"></select>
</label>
<label>
说明
<input id="equipmentDescription" />
</label>
<div class="form-actions">
<button type="button" class="secondary" id="equipmentReset">清空</button>
<button type="submit" id="equipmentSubmit">保存</button>
</div>
</form>
</div>
</div>
```
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
<section class="panel bottom-left">
<div class="stack-panel">
<div class="stack-section">
<div class="panel-head">
<h2>控制单元</h2>
<div class="toolbar">
<button type="button" class="secondary" id="refreshUnitBtn">刷新</button>
<button type="button" id="newUnitBtn">+ 新增</button>
</div>
</div>
<div class="list unit-list" id="unitList"></div>
</div>
<div class="stack-section stack-section-bordered">
<div class="panel-head">
<h2>数据源</h2>
<button type="button" id="openSourceForm">+ 新增</button>
</div>
<div class="source-panels" id="sourceList"></div>
</div>
</div>
</section>
```
- [ ] **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
<section class="panel top-left">
<div class="panel-head">
<h2>设备</h2>
<button type="button" id="newEquipmentBtn">+ 新增</button>
</div>
<div class="toolbar equipment-toolbar">
<input id="equipmentKeyword" placeholder="搜索编码或名称" />
<button type="button" class="secondary" id="refreshEquipmentBtn">刷新</button>
</div>
<div class="toolbar equipment-batch-toolbar">
<div class="muted" id="selectedEquipmentSummary">已选 0 台设备</div>
<select id="equipmentBatchUnitId"></select>
<button type="button" class="secondary" id="clearEquipmentSelectionBtn">清空选择</button>
<button type="button" id="applyEquipmentUnitBtn">批量设单元</button>
</div>
<div class="list equipment-list" id="equipmentList"></div>
</section>
```
- [ ] **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
<div class="modal hidden" id="equipmentModal">
<div class="modal-content modal-sm">
<div class="modal-head">
<h3>设备配置</h3>
<button class="secondary" id="closeEquipmentModal">X</button>
</div>
<form id="equipmentForm" class="form">
<input type="hidden" id="equipmentId" />
<label>
所属单元
<select id="equipmentUnitId"></select>
</label>
<label>
编码
<input id="equipmentCode" required />
</label>
<label>
名称
<input id="equipmentName" required />
</label>
<label>
类型
<select id="equipmentKind"></select>
</label>
<label>
说明
<input id="equipmentDescription" />
</label>
<div class="form-actions">
<button type="button" class="secondary" id="equipmentReset">清空</button>
<button type="submit" id="equipmentSubmit">保存</button>
</div>
</form>
</div>
</div>
<div class="modal hidden" id="pointModal">
<div class="modal-content">
<div class="modal-head">
<h3>选择节点创建点位</h3>
<button class="secondary" id="closeModal">X</button>
</div>
<div class="toolbar">
<select id="pointSourceSelect"></select>
<div class="muted" id="pointSourceNodeCount">Nodes: 0</div>
<button id="browseNodes">加载节点</button>
<button class="secondary" id="refreshTree">刷新树</button>
</div>
<div class="tree" id="nodeTree"></div>
<div class="modal-foot">
<div class="muted" id="selectedCount">已选中 0 个节点</div>
<button id="createPoints">创建设备点位</button>
</div>
</div>
</div>
<div class="modal hidden" id="sourceModal">
<div class="modal-content modal-sm">
<div class="modal-head">
<h3>Source 配置</h3>
<button class="secondary" id="closeSourceModal">X</button>
</div>
<form id="sourceForm" class="form">
<input type="hidden" id="sourceId" />
<label>
名称
<input id="sourceName" required />
</label>
<label>
Endpoint
<input id="sourceEndpoint" placeholder="opc.tcp://host:port" required />
</label>
<label class="check-row">
<input type="checkbox" id="sourceEnabled" checked />
<span>启用</span>
</label>
<div class="form-actions">
<button type="button" class="secondary" id="sourceReset">清空</button>
<button type="submit" id="sourceSubmit">保存</button>
</div>
</form>
</div>
</div>
<div class="modal hidden" id="pointBindingModal">
<div class="modal-content modal-sm">
<div class="modal-head">
<h3>绑定点位</h3>
<button class="secondary" id="closePointBindingModal">X</button>
</div>
<form id="pointBindingForm" class="form">
<input type="hidden" id="bindingPointId" />
<label>
点位
<input id="bindingPointName" disabled />
</label>
<label>
设备
<select id="bindingEquipmentId"></select>
</label>
<label>
角色模板
<select id="bindingSignalRole"></select>
</label>
<div class="form-actions">
<button type="button" class="secondary" id="clearPointBinding">清空绑定</button>
<button type="submit" id="savePointBinding">保存</button>
</div>
</form>
</div>
</div>
<div class="modal hidden" id="batchBindingModal">
<div class="modal-content modal-sm">
<div class="modal-head">
<h3>批量绑定点位</h3>
<button class="secondary" id="closeBatchBindingModal">X</button>
</div>
<form id="batchBindingForm" class="form">
<div class="muted" id="batchBindingSummary">已选中 0 个点位</div>
<label>
设备
<select id="batchBindingEquipmentId"></select>
</label>
<label>
角色模板
<select id="batchBindingSignalRole"></select>
</label>
<div class="form-actions">
<button type="button" class="secondary" id="clearBatchBinding">清空设备和角色</button>
<button type="submit" id="saveBatchBinding">批量保存</button>
</div>
</form>
</div>
</div>
```
- [ ] **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
<section class="panel app-config-main">
<div class="panel-head">
<h2>控制单元配置</h2>
<div class="toolbar">
<button type="button" class="secondary" id="refreshUnitBtn2">刷新</button>
<button type="button" id="newUnitBtn2">+ 新增</button>
</div>
</div>
<div class="list unit-config-list" id="unitConfigList"></div>
</section>
```
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
<header class="topbar">
<div class="title">投煤器布料机控制系统</div>
<div class="tab-bar">
<button type="button" class="tab-btn active" id="tabOps">运维</button>
<button type="button" class="tab-btn" id="tabAppConfig">应用配置</button>
<button type="button" class="tab-btn" id="tabConfig">平台配置</button>
</div>
<div class="topbar-actions">
<button type="button" class="secondary" id="openReadmeDoc">README.md</button>
<button type="button" class="secondary" id="openApiDoc">API.md</button>
<div class="status" id="statusText">
<span class="ws-dot" id="wsDot"></span>
<span id="wsLabel">连接中…</span>
</div>
</div>
</header>
```
- [ ] **Step 3: Replace feeder index.html `<main>` contents to include unit-panel partial**
Replace the entire `<main>` block in `web/feeder/index.html`:
```html
<main class="grid-ops">
<div data-partial="/ui/html/ops-panel.html"></div>
<div data-partial="/ui/html/equipment-panel.html"></div>
<div data-partial="/ui/html/points-panel.html"></div>
<div data-partial="/ui/html/source-panel.html"></div>
<div data-partial="/ui/html/log-stream-panel.html"></div>
<div data-partial="/ui/html/chart-panel.html"></div>
<div data-partial="/ui/html/unit-panel.html"></div>
<div data-partial="/ui/html/logs-panel.html"></div>
</main>
```
- [ ] **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<impl IntoResponse, ApiErr> {
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<impl IntoResponse, ApiErr> {
plc_platform_core::handler::doc::serve_markdown("docs/api-feeder.md").await
}
pub async fn get_readme_md() -> Result<impl IntoResponse, ApiErr> {
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<impl IntoResponse, ApiErr> {
plc_platform_core::handler::doc::serve_markdown("docs/api-ops.md").await
}
pub async fn get_readme_md() -> Result<impl IntoResponse, ApiErr> {
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<AppState>) -> 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 13
- 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.