929 lines
29 KiB
Markdown
929 lines
29 KiB
Markdown
# 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 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.
|