plc_control/docs/superpowers/plans/2026-04-17-web-split-and-cl...

565 lines
17 KiB
Markdown
Raw Permalink 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.

# Web Page Split And Root Source Cleanup
> **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 the `web/` directory into `core/` + `feeder/` + `ops/` subdirectories, delete the obsolete root `src/` files, and update README to reflect the new workspace layout.
**Architecture:** Web pages split into a shared `web/core/` (platform HTML partials and CSS) and per-app directories (`web/feeder/`, `web/ops/`). Each app's Axum router uses `ServeDir` with fallback: try app-specific dir first, then core. This means no URL changes in HTML/JS — the fallback chain resolves transparently. The root `src/` contains stale copies of files already migrated to crates and must be removed.
**Tech Stack:** Rust (Axum, tower-http ServeDir), HTML/CSS/JS (vanilla, ES modules), Cargo workspace
---
## File Map
### Web core (shared platform pages)
- Move: `web/styles.css``web/core/styles.css`
- Move: `web/html/source-panel.html``web/core/html/source-panel.html`
- Move: `web/html/points-panel.html``web/core/html/points-panel.html`
- Move: `web/html/equipment-panel.html``web/core/html/equipment-panel.html`
- Move: `web/html/chart-panel.html``web/core/html/chart-panel.html`
- Move: `web/html/log-stream-panel.html``web/core/html/log-stream-panel.html`
- Move: `web/html/logs-panel.html``web/core/html/logs-panel.html`
- Move: `web/html/api-doc-drawer.html``web/core/html/api-doc-drawer.html`
- Create: `web/core/html/modals.html` (core modals only — equipment, source, point, binding; unit modal removed)
### Web feeder (feeder-specific pages + all JS)
- Move: `web/index.html``web/feeder/index.html` (add unit-modal partial reference)
- Move: `web/html/topbar.html``web/feeder/html/topbar.html`
- Move: `web/html/ops-panel.html``web/feeder/html/ops-panel.html`
- Create: `web/feeder/html/unit-modal.html` (extracted from old modals.html)
- Move: `web/js/*.js``web/feeder/js/*.js` (all 15 JS files stay together as interconnected module graph)
### Web ops (operation-system pages)
- Move: `crates/app_operation_system/web/index.html``web/ops/index.html` (updated content)
- Create: `web/ops/html/topbar.html`
- Create: `web/ops/js/index.js`
- Create: `web/ops/js/app.js`
### Rust router changes
- Modify: `crates/app_feeder_distributor/src/router.rs` (update ServeDir to use fallback)
- Modify: `crates/app_operation_system/src/router.rs` (update ServeDir to use fallback)
### Root src cleanup
- Delete: all 19 files under `src/` (stale duplicates of files in crates)
### Documentation
- Modify: `README.md`
---
## Task 1: Split modals.html And Create Unit Modal Partial
**Files:**
- Create: `web/core/html/modals.html`
- Create: `web/feeder/html/unit-modal.html`
- [ ] **Step 1: Create core modals (without unit modal)**
Extract everything except the unit modal div from `web/html/modals.html` into a new file:
`web/core/html/modals.html`:
```html
<div class="modal hidden" id="equipmentModal">
<!-- keep entire equipment modal as-is from current modals.html lines 53-87 -->
</div>
<div class="modal hidden" id="pointModal">
<!-- keep entire point modal as-is from current modals.html lines 89-107 -->
</div>
<div class="modal hidden" id="sourceModal">
<!-- keep entire source modal as-is from current modals.html lines 109-135 -->
</div>
<div class="modal hidden" id="pointBindingModal">
<!-- keep entire binding modal as-is from current modals.html lines 137-163 -->
</div>
<div class="modal hidden" id="batchBindingModal">
<!-- keep entire batch binding modal as-is from current modals.html lines 165-187 -->
</div>
```
- [ ] **Step 2: Create feeder unit modal partial**
Extract the unit modal into its own file:
`web/feeder/html/unit-modal.html`:
```html
<div class="modal hidden" id="unitModal">
<!-- keep entire unit modal as-is from current modals.html lines 1-51 -->
</div>
```
- [ ] **Step 3: Verify both files contain all original modal content**
Check that the combined line count of the two new files matches the original `web/html/modals.html` (188 lines total, minus blank lines between sections).
- [ ] **Step 4: Commit**
```bash
git add web/core/html/modals.html web/feeder/html/unit-modal.html
git commit -m "refactor(web): split modals into core and feeder unit-modal"
```
## Task 2: Move Core HTML And CSS Into web/core
**Files:**
- Move: `web/styles.css``web/core/styles.css`
- Move: `web/html/source-panel.html``web/core/html/source-panel.html`
- Move: `web/html/points-panel.html``web/core/html/points-panel.html`
- Move: `web/html/equipment-panel.html``web/core/html/equipment-panel.html`
- Move: `web/html/chart-panel.html``web/core/html/chart-panel.html`
- Move: `web/html/log-stream-panel.html``web/core/html/log-stream-panel.html`
- Move: `web/html/logs-panel.html``web/core/html/logs-panel.html`
- Move: `web/html/api-doc-drawer.html``web/core/html/api-doc-drawer.html`
- [ ] **Step 1: Create core directories and move files**
```bash
mkdir -p web/core/html
git mv web/styles.css web/core/styles.css
git mv web/html/source-panel.html web/core/html/source-panel.html
git mv web/html/points-panel.html web/core/html/points-panel.html
git mv web/html/equipment-panel.html web/core/html/equipment-panel.html
git mv web/html/chart-panel.html web/core/html/chart-panel.html
git mv web/html/log-stream-panel.html web/core/html/log-stream-panel.html
git mv web/html/logs-panel.html web/core/html/logs-panel.html
git mv web/html/api-doc-drawer.html web/core/html/api-doc-drawer.html
```
- [ ] **Step 2: Commit**
```bash
git add web/core
git commit -m "refactor(web): move shared HTML partials and CSS into web/core"
```
## Task 3: Move Feeder-Specific HTML And All JS Into web/feeder
**Files:**
- Move: `web/index.html``web/feeder/index.html`
- Move: `web/html/topbar.html``web/feeder/html/topbar.html`
- Move: `web/html/ops-panel.html``web/feeder/html/ops-panel.html`
- Move: `web/js/*.js``web/feeder/js/*.js`
- Delete: `web/html/modals.html` (replaced by split files in Task 1)
- [ ] **Step 1: Create feeder directories and move files**
```bash
mkdir -p web/feeder/html web/feeder/js
git mv web/html/topbar.html web/feeder/html/topbar.html
git mv web/html/ops-panel.html web/feeder/html/ops-panel.html
git mv web/js/api.js web/feeder/js/api.js
git mv web/js/app.js web/feeder/js/app.js
git mv web/js/chart.js web/feeder/js/chart.js
git mv web/js/docs.js web/feeder/js/docs.js
git mv web/js/dom.js web/feeder/js/dom.js
git mv web/js/equipment.js web/feeder/js/equipment.js
git mv web/js/events.js web/feeder/js/events.js
git mv web/js/index.js web/feeder/js/index.js
git mv web/js/logs.js web/feeder/js/logs.js
git mv web/js/ops.js web/feeder/js/ops.js
git mv web/js/points.js web/feeder/js/points.js
git mv web/js/roles.js web/feeder/js/roles.js
git mv web/js/sources.js web/feeder/js/sources.js
git mv web/js/state.js web/feeder/js/state.js
git mv web/js/units.js web/feeder/js/units.js
```
- [ ] **Step 2: Move index.html and delete old modals**
```bash
git mv web/index.html web/feeder/index.html
git rm web/html/modals.html
```
- [ ] **Step 3: Update feeder index.html to add unit-modal partial**
In `web/feeder/index.html`, change the modals partial line and add a unit-modal partial:
Before:
```html
<div data-partial="/ui/html/modals.html"></div>
```
After:
```html
<div data-partial="/ui/html/modals.html"></div>
<div data-partial="/ui/html/unit-modal.html"></div>
```
- [ ] **Step 4: Verify no files remain in old web/html and web/js directories**
```bash
ls web/html/ 2>/dev/null && echo "ERROR: web/html still has files" || echo "OK: web/html is clean"
ls web/js/ 2>/dev/null && echo "ERROR: web/js still has files" || echo "OK: web/js is clean"
```
Expected: both directories are empty or deleted.
- [ ] **Step 5: Commit**
```bash
git add web/feeder
git commit -m "refactor(web): move feeder HTML, JS, and index into web/feeder"
```
## Task 4: Update Feeder Router To Use Fallback ServeDir
**Files:**
- Modify: `crates/app_feeder_distributor/src/router.rs`
- [ ] **Step 1: Update the static file serving to use fallback chain**
In `crates/app_feeder_distributor/src/router.rs`, replace the current `/ui` nest:
Before:
```rust
.nest(
"/ui",
Router::new()
.fallback_service(ServeDir::new("web").append_index_html_on_directories(true))
.layer(axum::middleware::from_fn(no_cache)),
)
```
After:
```rust
.nest(
"/ui",
Router::new()
.fallback_service(
ServeDir::new("web/feeder")
.append_index_html_on_directories(true)
.fallback(ServeDir::new("web/core")),
)
.layer(axum::middleware::from_fn(no_cache)),
)
```
- [ ] **Step 2: Verify feeder crate compiles**
Run:
```bash
cargo check -p app_feeder_distributor
```
Expected: PASS
- [ ] **Step 3: Commit**
```bash
git add crates/app_feeder_distributor/src/router.rs
git commit -m "refactor(feeder): update static file serving for split web dirs"
```
## Task 5: Create Operation-System Web Pages And Update Router
**Files:**
- Create: `web/ops/index.html`
- Create: `web/ops/html/topbar.html`
- Create: `web/ops/js/index.js`
- Create: `web/ops/js/app.js`
- Modify: `crates/app_operation_system/src/router.rs`
- Delete: `crates/app_operation_system/web/index.html`
- [ ] **Step 1: Create ops web scaffold**
`web/ops/index.html`:
```html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>运转系统</title>
<link rel="stylesheet" href="/ui/styles.css" />
</head>
<body>
<div data-partial="/ui/html/topbar.html"></div>
<main>
<div class="muted" style="padding:2rem;text-align:center">运转系统页面开发中</div>
</main>
<script type="module" src="/ui/js/index.js"></script>
</body>
</html>
```
`web/ops/html/topbar.html`:
```html
<header class="topbar">
<div class="title">运转系统</div>
<div class="topbar-actions">
<div class="status" id="statusText">
<span class="ws-dot" id="wsDot"></span>
<span id="wsLabel">连接中…</span>
</div>
</div>
</header>
```
`web/ops/js/index.js`:
```javascript
async function loadPartial(slot) {
const response = await fetch(slot.dataset.partial);
if (!response.ok) {
throw new Error(`Failed to load partial: ${slot.dataset.partial}`);
}
const html = await response.text();
slot.insertAdjacentHTML("beforebegin", html);
slot.remove();
}
async function bootstrapPage() {
const slots = Array.from(document.querySelectorAll("[data-partial]"));
await Promise.all(slots.map((slot) => loadPartial(slot)));
await import("./app.js");
}
bootstrapPage().catch((error) => {
document.body.innerHTML = `<pre>${error.message || String(error)}</pre>`;
});
```
`web/ops/js/app.js`:
```javascript
function bootstrap() {
console.log("Operation system app initialized");
}
bootstrap();
```
- [ ] **Step 2: Update ops router to use split web dirs**
Replace `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))
.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 3: Delete old ops web placeholder**
```bash
git rm crates/app_operation_system/web/index.html
rmdir crates/app_operation_system/web 2>/dev/null || true
```
- [ ] **Step 4: Verify ops crate compiles**
Run:
```bash
cargo check -p app_operation_system
```
Expected: PASS
- [ ] **Step 5: Update ops router smoke test if needed**
Check `crates/app_operation_system/tests/router_smoke.rs` — if it references the old `WEB_ROOT` constant, update accordingly.
- [ ] **Step 6: Commit**
```bash
git add web/ops crates/app_operation_system
git commit -m "refactor(ops): add ops web scaffold and update router for split dirs"
```
## Task 6: Delete Obsolete Root src/ Files
**Files:**
- Delete: all 19 files under `src/`
- [ ] **Step 1: Verify all root src files are duplicates of crate files**
Run quick checks:
```bash
diff src/config.rs crates/app_feeder_distributor/src/config.rs
diff src/handler.rs crates/app_feeder_distributor/src/handler.rs
diff src/middleware.rs crates/app_feeder_distributor/src/middleware.rs
```
All should show no functional differences (only BOM or whitespace).
- [ ] **Step 2: Remove all root src files from git**
```bash
git rm -r src/
```
- [ ] **Step 3: Verify workspace still builds**
Run:
```bash
cargo check --workspace
```
Expected: PASS (root src/ is not a workspace member, removing it changes nothing for the build)
- [ ] **Step 4: Commit**
```bash
git commit -m "chore: remove obsolete root src/ (migrated to crates)"
```
## Task 7: Update README
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Update the README to reflect the workspace structure**
Replace the outdated "后端结构" and add build instructions. Key sections to update:
- Remove references to `src/main.rs`, `src/handler`, `src/service`
- Add workspace structure overview:
```markdown
## 项目结构
```text
plc_control/
Cargo.toml # Workspace root
crates/
plc_platform_core/ # 共享平台核心库
app_feeder_distributor/ # 投煤器布料机专用版
app_operation_system/ # 运转系统专用版
web/
core/ # 共享 HTML/CSS点位、设备、数据源等
feeder/ # 投煤器布料机页面 + JS
ops/ # 运转系统页面 + JS
```
## 构建
```powershell
# 投煤器布料机
cargo build -p app_feeder_distributor --release
# 运转系统
cargo build -p app_operation_system --release
```
## 部署
将编译产物和 `web/` 目录放在同一级目录下:
```text
deploy/
app_feeder_distributor.exe
web/
core/
feeder/
```
```
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs: update README for workspace and web split layout"
```
## Task 8: Final Verification
- [ ] **Step 1: Run all tests**
```bash
cargo test --workspace
```
Expected: PASS
- [ ] **Step 2: Run release builds**
```bash
cargo build -p app_feeder_distributor --release
cargo build -p app_operation_system --release
```
Expected: both produce binaries successfully.
- [ ] **Step 3: Verify web file layout**
```bash
find web -type f | sort
```
Expected: files organized under `web/core/`, `web/feeder/`, `web/ops/` only. No files remaining directly under `web/html/` or `web/js/`.
## Self-Review
### Spec coverage
- Web split into core + per-app directories: Tasks 15
- Fallback ServeDir for transparent URL resolution: Tasks 45
- Root src cleanup: Task 6
- README update: Task 7
- Build verification: Task 8
### Key design decision: ServeDir fallback
Using `ServeDir::new("web/feeder").fallback(ServeDir::new("web/core"))` means:
- No URL changes needed in any HTML partial references or JS imports
- App-specific files override core files of the same name (app takes priority)
- Browser requests are resolved transparently through the chain
### Spec deviation: web directory location
The original design spec §8.4 suggested per-app web directories inside each crate (`app_feeder_distributor/web`, `app_operation_system/web`). This plan deliberately places web files at the workspace root (`web/core/`, `web/feeder/`, `web/ops/`) instead. Reason: enables the ServeDir fallback chain to share core assets without duplication, and avoids coupling web resources to Rust crate build paths. This is a justified departure from the spec.
### What this plan does NOT cover (deferred)
- `PlatformContext` completion (filling in pool/connection_manager/event_manager/ws_manager)
- `config.rs` migration into shared core
- `control/validator.rs` splitting
- Event namespace migration at call sites
- These are larger refactors that should be planned separately