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

29 KiB
Raw Blame History

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

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

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

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

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

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

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

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

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

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

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

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

  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:

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:

  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:

cargo check -p app_feeder_distributor

Expected: PASS (no Rust changes in this task)

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

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:

pub mod log;

with:

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:

cargo check -p app_feeder_distributor

Expected: PASS

  • Step 6: Commit
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.mddocs/api-feeder.md

  • Create: docs/api-ops.md

  • Step 1: Create core generic markdown serving utility

crates/plc_platform_core/src/handler/doc.rs:

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:

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:

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
git mv API.md docs/api-feeder.md
  • Step 5: Create ops API doc

docs/api-ops.md:

# 运转系统 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:

pub mod doc;

crates/app_operation_system/src/handler/doc.rs:

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:

cargo check -p app_feeder_distributor
cargo check -p app_operation_system

Expected: both PASS

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

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:

cargo check -p app_operation_system
cargo test -p app_operation_system

Expected: both PASS

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

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:

- API 接口说明: `API.md`

After:

- 投煤器布料机 API: `docs/api-feeder.md`
- 运转系统 API: `docs/api-ops.md`
  • Step 3: Commit
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:

cargo test --workspace

Expected: all PASS

  • Step 2: Run release builds

Run:

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:

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:

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.