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

17 KiB
Raw Permalink Blame History

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.cssweb/core/styles.css
  • Move: web/html/source-panel.htmlweb/core/html/source-panel.html
  • Move: web/html/points-panel.htmlweb/core/html/points-panel.html
  • Move: web/html/equipment-panel.htmlweb/core/html/equipment-panel.html
  • Move: web/html/chart-panel.htmlweb/core/html/chart-panel.html
  • Move: web/html/log-stream-panel.htmlweb/core/html/log-stream-panel.html
  • Move: web/html/logs-panel.htmlweb/core/html/logs-panel.html
  • Move: web/html/api-doc-drawer.htmlweb/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.htmlweb/feeder/index.html (add unit-modal partial reference)
  • Move: web/html/topbar.htmlweb/feeder/html/topbar.html
  • Move: web/html/ops-panel.htmlweb/feeder/html/ops-panel.html
  • Create: web/feeder/html/unit-modal.html (extracted from old modals.html)
  • Move: web/js/*.jsweb/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.htmlweb/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:

<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:

<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
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.cssweb/core/styles.css

  • Move: web/html/source-panel.htmlweb/core/html/source-panel.html

  • Move: web/html/points-panel.htmlweb/core/html/points-panel.html

  • Move: web/html/equipment-panel.htmlweb/core/html/equipment-panel.html

  • Move: web/html/chart-panel.htmlweb/core/html/chart-panel.html

  • Move: web/html/log-stream-panel.htmlweb/core/html/log-stream-panel.html

  • Move: web/html/logs-panel.htmlweb/core/html/logs-panel.html

  • Move: web/html/api-doc-drawer.htmlweb/core/html/api-doc-drawer.html

  • Step 1: Create core directories and move files

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
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.htmlweb/feeder/index.html

  • Move: web/html/topbar.htmlweb/feeder/html/topbar.html

  • Move: web/html/ops-panel.htmlweb/feeder/html/ops-panel.html

  • Move: web/js/*.jsweb/feeder/js/*.js

  • Delete: web/html/modals.html (replaced by split files in Task 1)

  • Step 1: Create feeder directories and move files

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
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:

  <div data-partial="/ui/html/modals.html"></div>

After:

  <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
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
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:

        .nest(
            "/ui",
            Router::new()
                .fallback_service(ServeDir::new("web").append_index_html_on_directories(true))
                .layer(axum::middleware::from_fn(no_cache)),
        )

After:

        .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:

cargo check -p app_feeder_distributor

Expected: PASS

  • Step 3: Commit
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:

<!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:

<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:

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:

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:

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
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:

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
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:

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
git rm -r src/
  • Step 3: Verify workspace still builds

Run:

cargo check --workspace

Expected: PASS (root src/ is not a workspace member, removing it changes nothing for the build)

  • Step 4: Commit
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:
## 项目结构

```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

构建

# 投煤器布料机
cargo build -p app_feeder_distributor --release

# 运转系统
cargo build -p app_operation_system --release

部署

将编译产物和 web/ 目录放在同一级目录下:

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
cargo test --workspace

Expected: PASS

  • Step 2: Run release builds
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
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