chore: document architecture gaps and add dev hot reload

- add npm dev:hot wrapper using cargo-watch and page reload polling

- add mnote-web dev hot reload endpoint and coverage

- record current architecture review and tracked bug findings across realtime, tree, editor, and AI runtimes

Verification:

- node scripts/task-dev-hot-plan-test.js

- node --check scripts/dev-hot.js

- cargo test -p mnote-web dev_hot -- --nocapture
This commit is contained in:
lix-2026
2026-05-17 23:09:56 +08:00
parent ee0643041a
commit 3311bd0366
31 changed files with 1220 additions and 1 deletions
@@ -0,0 +1,51 @@
use axum::http::{HeaderMap, HeaderValue};
use axum::response::IntoResponse;
use axum::Json;
use serde::Serialize;
use std::sync::LazyLock;
use std::time::{SystemTime, UNIX_EPOCH};
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
static DEV_HOT_BOOT_ID: LazyLock<String> = LazyLock::new(|| {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or_default();
format!("{}-{now}", std::process::id())
});
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DevHotReloadPayload {
ok: bool,
enabled: bool,
boot_id: &'static str,
}
pub fn dev_hot_reload_enabled() -> bool {
matches!(
std::env::var("MNOTE_WEB_DEV_HOT_RELOAD")
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
.as_str(),
"1" | "true" | "yes" | "on"
)
}
pub async fn hot_reload() -> impl IntoResponse {
let mut headers = HeaderMap::new();
headers.insert(
HEADER_MNOTE_WEB_OWNER,
HeaderValue::from_static("mnote-web"),
);
(
headers,
Json(DevHotReloadPayload {
ok: true,
enabled: dev_hot_reload_enabled(),
boot_id: DEV_HOT_BOOT_ID.as_str(),
}),
)
}
+16
View File
@@ -1,6 +1,7 @@
mod bridge;
pub(crate) mod command_support;
mod compat;
mod dev_hot;
mod documents;
mod editor;
mod gateway;
@@ -80,6 +81,7 @@ pub fn build_router(state: AppState) -> Router {
)
.route("/api/search/documents", post(search::documents))
.route("/api/gateway/health", get(gateway::gateway_health))
.route("/api/dev/hot-reload", get(dev_hot::hot_reload))
.route("/api/runtime/config", get(session::runtime_config))
.route("/api/auth", post(gateway::auth_api))
.route("/api/auth/session", get(session::session))
@@ -350,4 +352,18 @@ mod tests {
);
}
}
#[tokio::test]
async fn dev_hot_reload_endpoint_is_mounted() {
let response = app(false)
.oneshot(
Request::builder()
.uri("/api/dev/hot-reload")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}
}
@@ -6745,6 +6745,36 @@ const SIDEBAR_TREE_JS: &str = r##"
}
scheduleInitializePageUiSurfaces();
function installMnoteDevHotReload() {
var bootId = '';
var failedOnce = false;
var timer = 0;
function tick() {
fetch('/api/dev/hot-reload', { cache: 'no-store', headers: { accept: 'application/json' } })
.then(function(response) { return response.ok ? response.json() : null; })
.then(function(payload) {
if (!payload || payload.enabled !== true || !payload.bootId) {
if (!bootId && timer) window.clearInterval(timer);
return;
}
document.documentElement.setAttribute('data-mnote-dev-hot-reload', 'enabled');
if (!bootId) {
bootId = String(payload.bootId);
return;
}
if (failedOnce || String(payload.bootId) !== bootId) {
window.location.assign(window.location.href);
}
})
.catch(function() {
if (bootId) failedOnce = true;
});
}
tick();
timer = window.setInterval(tick, 1000);
}
installMnoteDevHotReload();
function readPageDragNodeId(event) {
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
return (fromTransfer || draggingPageNodeId || '').trim();
@@ -7437,6 +7467,14 @@ mod tests {
assert!(!SIDEBAR_TREE_JS.contains("window.location.reload"));
}
#[test]
fn sidebar_tree_runtime_contains_dev_hot_reload_client() {
assert!(SIDEBAR_TREE_JS.contains("installMnoteDevHotReload"));
assert!(SIDEBAR_TREE_JS.contains("/api/dev/hot-reload"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-dev-hot-reload"));
assert!(SIDEBAR_TREE_JS.contains("window.clearInterval(timer)"));
}
#[test]
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu"));