29 KiB
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:
<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.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:
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 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
AppStateand 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.jstargets#unitList(in source-panel). Wiring#unitConfigListto 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,unitConfigListto avoid conflicts with the unit list embedded in the feeder source-panel override. Both are wired to the sameloadUnits()/openCreateUnitModal()functions. - Log handler is fully stateless: Reads files from
./logsdirectory. 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.