Skip to content

Commit 9bb6e7a

Browse files
committed
docs(planning): add web admin completion plan
Document the five-phase web admin completion work and apply rustfmt drift from Phases 2–3 command bridge and ui_events changes. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 781f672 commit 9bb6e7a

3 files changed

Lines changed: 211 additions & 8 deletions

File tree

apps/desktop/src-tauri/src/services/ui_events.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ pub const OAUTH_CLIENT_CHANGED_CHANNEL: &str = "oauth-client-changed";
1717
/// Resolve the admin UI event bus from managed Tauri state, if available.
1818
fn resolve_ui_event_bus<R: Runtime>(app: &AppHandle<R>) -> Option<Arc<AdminUiEventBus>> {
1919
app.try_state::<Arc<tokio::sync::RwLock<AdminServerState>>>()
20-
.and_then(|state| state.try_read().ok().map(|guard| guard.ui_event_bus.clone()))
20+
.and_then(|state| {
21+
state
22+
.try_read()
23+
.ok()
24+
.map(|guard| guard.ui_event_bus.clone())
25+
})
2126
}
2227

2328
/// Emit a UI channel event to the Tauri webview and admin SSE subscribers.
@@ -38,10 +43,5 @@ pub fn emit_ui_channel<R: Runtime>(
3843
/// Emit a UI channel event, resolving the admin SSE bus from app state.
3944
pub fn emit_ui_channel_from_app<R: Runtime>(app: &AppHandle<R>, channel: &str, payload: Value) {
4045
let ui_event_bus = resolve_ui_event_bus(app);
41-
emit_ui_channel(
42-
app,
43-
ui_event_bus.as_deref(),
44-
channel,
45-
payload,
46-
);
46+
emit_ui_channel(app, ui_event_bus.as_deref(), channel, payload);
4747
}

crates/mcpmux-gateway/src/admin/command_bridge/read.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,8 @@ pub async fn export_config_to_file(
10911091

10921092
let path = PathBuf::from(path);
10931093
if let Some(parent) = path.parent() {
1094-
std::fs::create_dir_all(parent).map_err(|err| anyhow!("Failed to create parent dir: {err}"))?;
1094+
std::fs::create_dir_all(parent)
1095+
.map_err(|err| anyhow!("Failed to create parent dir: {err}"))?;
10951096
}
10961097
std::fs::write(&path, &content).map_err(|err| anyhow!("Failed to write config: {err}"))?;
10971098
as_json(path.to_string_lossy().to_string())
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Web Admin Completion
2+
3+
**Last Updated:** Jun 24, 2026
4+
**Status:** Active — pending implementation
5+
**Branch:** `dev-rebased` (HEAD: `17d71ad`)
6+
**Depends on:** Phase 2 lib/api migration complete (`dev-rebased-post-port-completion.md`), admin SSE hub live
7+
**Unblocks:** Web admin at `mux.joe-hassio.com` fully functional with no `transformCallback` errors, config export, live consent flow, and working HMR dev path
8+
9+
---
10+
11+
## Problem
12+
13+
Three phases of `invoke``apiCall` migration landed cleanly. The web admin loads spaces, data syncs, SSE events flow, and the console is clean of `transformCallback` spam. Five gaps remain before the web admin is feature-complete:
14+
15+
**1. `pnpm dev:web:admin` is broken.** `scripts/dev-env.mjs` does not exist (referenced by `dev-web-admin.mjs:34`). Even if the prep call is removed, Vite at `:1420` has no `/api` proxy configured — API calls fall through to the Vite dev server which has no handler.
16+
17+
**2. Config export has zero Rust routes.** Five commands in `lib/api/configExport.ts` (`preview_config_export`, `get_config_paths`, `check_config_exists`, `backup_existing_config`, `export_config_to_file`) are mapped in `fetch-api.routes/` but have no handlers in `router.rs` or `command_bridge/`. All five throw 404 in web admin.
18+
19+
**3. OAuth consent popup is dead on web.** `oauth-consent-request` and `oauth-client-changed` are emitted via `app.emit()` (Tauri IPC only) in `oauth.rs`. The admin SSE hub never receives them, so the consent modal and client-changed events are silently dropped in the browser.
20+
21+
**4. Native file pickers crash on web.** `SpaceBaseDirsModal.tsx` calls `@tauri-apps/plugin-dialog` `openDialog()` with no `isTauri()` guard. `pickPath()` in `shell/index.ts` (used by ServersPage, WorkspacesPage) returns `undefined` in browser context but the callers never expect that.
22+
23+
**5. Dead `apiCall` commands pollute the codebase.** Three commands (`export_config`, `connect_server`, `disconnect_server_v2`) are called in `lib/api/` but have no corresponding feature usage and are superseded by newer commands. They add noise to the transport layer and route coverage tests.
24+
25+
---
26+
27+
## Decisions
28+
29+
| # | Decision | Choice | Rationale |
30+
| - | -------- | ------ | --------- |
31+
| 1 | Dev HMR path | Fix properly — `scripts/dev-env.mjs` + Vite `/api` proxy to `:45819` | HMR is the primary iteration loop. Dead tooling slows down future web admin work. |
32+
| 2 | Config export on web | Full implementation — all 5 Rust command bridges | Config export is same-filesystem on the local machine. No upload/download needed; file-write semantics are identical to desktop. |
33+
| 3 | OAuth consent events | Route through `emit_ui_channel()` | Consent request is an active MCP session flow. Web admin connecting a server must be able to complete OAuth. SSE already carries other session events; reuse the pattern. |
34+
| 4 | File pickers on web | `isTauri()` guard → native picker, else `<input type="text">` | Minimal change. Path entry by text is fully functional and avoids a new multipart upload API. |
35+
| 5 | Dead commands | Delete callers + Tauri commands | No feature code calls them. Deletion is cleaner than wiring unused routes. |
36+
37+
---
38+
39+
## Scope
40+
41+
**In:**
42+
- Write `scripts/dev-env.mjs` (gateway liveness check); add `/api` Vite proxy when `VITE_ADMIN_WEB=1`
43+
- Implement 5 config-export Rust handlers in `command_bridge/read.rs` + `router.rs`
44+
- Route `oauth-consent-request` and `oauth-client-changed` through `emit_ui_channel()`
45+
- Guard `SpaceBaseDirsModal` `openDialog()` and `pickPath()` call sites with text-input fallback
46+
- Delete `export_config`, `connect_server`, `disconnect_server_v2` callers and Tauri commands
47+
- Extend `tests/ts/admin-transport.test.ts` to cover builtins, config-export, and the new routes
48+
- Align `builtin-server-config-changed` SSE channel name (desktop bridge uses this name; `ui_events.rs` maps to `server-changed`)
49+
50+
**Out:**
51+
52+
| Item | Reason |
53+
| ---- | ------ |
54+
| Browser file-upload for base dirs / icons | Punted — text input is sufficient for local paths on the same machine |
55+
| Connect IDE flow on web | Desktop-only by design; requires Tauri deep-link to IDE |
56+
| Meta-tools approval dialog on web | Desktop-only modal; approval via Settings toggle works on web |
57+
| Cloudflare `Permissions-Policy` header noise | External to this repo — fix belongs in CF Zero Trust dashboard (remove `Permissions-Policy: *` from the Access application response headers) |
58+
| Web E2E test authoring for new routes | Deferred — manual verification matrix covers this pass |
59+
60+
---
61+
62+
## Architecture
63+
64+
### Dev tooling
65+
66+
```
67+
pnpm dev:web:admin
68+
└─ node scripts/dev-web-admin.mjs
69+
├─ runPrep() → scripts/dev-env.mjs ← NEW: health-check :45819
70+
└─ execa vite (VITE_ADMIN_WEB=1)
71+
└─ vite.config.ts server.proxy['/api'] → http://127.0.0.1:45819
72+
```
73+
74+
`vite.config.ts` conditionally adds the proxy only when `process.env.VITE_ADMIN_WEB` is set so Tauri dev builds are unaffected.
75+
76+
### Config export Rust bridges
77+
78+
New handlers land in `crates/mcpmux-gateway/src/admin/command_bridge/read.rs` alongside existing read handlers. Each handler calls the same underlying `ApplicationServices` method that the Tauri command already calls. Router mounts them under `/api/v1/config-export/`.
79+
80+
### OAuth SSE fan-out
81+
82+
`oauth.rs` currently calls `app.emit("oauth-consent-request", ...)` and `app.emit("oauth-client-changed", ...)`. Change to `emit_ui_channel(&app, UiEvent::OAuthConsentRequest { ... })` which fans out to both Tauri IPC and the admin SSE hub simultaneously. No new SSE channels needed — SSE already carries `oauth-consent-request` and `oauth-client-changed` mappings via `ui_events.rs`.
83+
84+
---
85+
86+
## Files to create / modify
87+
88+
| Phase | File | Action |
89+
| ----- | ---- | ------ |
90+
| 1 | `scripts/dev-env.mjs` | **Create** — gateway liveness check, port guard |
91+
| 1 | `scripts/dev-web-admin.mjs` | Update `runPrep()` to call `dev-env.mjs` |
92+
| 1 | `apps/desktop/vite.config.ts` | Add `/api` proxy block when `VITE_ADMIN_WEB` |
93+
| 2 | `crates/mcpmux-gateway/src/admin/command_bridge/read.rs` | Add 5 config-export handlers |
94+
| 2 | `crates/mcpmux-gateway/src/admin/router.rs` | Mount `/api/v1/config-export/*` routes |
95+
| 3 | `apps/desktop/src-tauri/src/services/oauth.rs` | `app.emit``emit_ui_channel` for consent + client-changed |
96+
| 3 | `apps/desktop/src-tauri/src/services/ui_events.rs` | Verify `OAuthConsentRequest` / `OAuthClientChanged` variants exist; add if missing |
97+
| 3 | `apps/desktop/src-tauri/src/services/ui_events.rs` | Align `builtin-server-config-changed` channel: emit under same name in both Tauri bridge and SSE |
98+
| 4 | `apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx` | Guard `openDialog()` with `isTauri()`; replace with `<input type="text">` on web |
99+
| 4 | `apps/desktop/src/features/servers/ServersPage.tsx` | Guard `pickPath()` — show text input on web |
100+
| 4 | `apps/desktop/src/features/workspaces/WorkspacesPage.tsx` | Guard `pickPath()` for icon upload — show text input on web |
101+
| 5 | `apps/desktop/src/lib/api/gateway.ts` | Delete `export_config`, `connect_server` callers |
102+
| 5 | `apps/desktop/src/lib/api/serverManager.ts` | Delete `disconnect_server_v2` caller |
103+
| 5 | `apps/desktop/src-tauri/src/commands/` | Remove corresponding Tauri commands if no other callers |
104+
| 5 | `apps/desktop/src/lib/backend/data/fetch-api.routes/` | Remove dead route mappings for deleted commands |
105+
| 5 | `tests/ts/admin-transport.test.ts` | Add builtins, config-export, new SSE event coverage |
106+
107+
---
108+
109+
## Phases
110+
111+
### Phase 1 — Dev tooling (~1 hour)
112+
113+
- Write `scripts/dev-env.mjs`: check `:45819/api/v1/health`, fail fast with a clear message if gateway is not running, else exit 0
114+
- Update `scripts/dev-web-admin.mjs` to call `dev-env.mjs` via `execa`
115+
- Add conditional Vite proxy in `apps/desktop/vite.config.ts`:
116+
```ts
117+
...(process.env.VITE_ADMIN_WEB ? { server: { proxy: { '/api': 'http://127.0.0.1:45819' } } } : {})
118+
```
119+
120+
**Outcome:** `pnpm dev:web:admin` starts Vite HMR at `:1420`. API calls proxy through to the running gateway. Stopping with the gateway down prints a helpful "gateway not running" message instead of a missing-module crash.
121+
122+
---
123+
124+
### Phase 2 — Config export HTTP routes (~half day)
125+
126+
Add five read handlers in `command_bridge/read.rs`:
127+
128+
- `preview_config_export(space_id)``GET /api/v1/config-export/preview?space_id=`
129+
- `get_config_paths(space_id)``GET /api/v1/config-export/paths?space_id=`
130+
- `check_config_exists(client_name)``POST /api/v1/config-export/check`
131+
- `backup_existing_config(client_name)``POST /api/v1/config-export/backup`
132+
- `export_config_to_file(space_id, client_name, path)``POST /api/v1/config-export/export`
133+
134+
Mount all five in `router.rs` under `/api/v1/config-export/`. Follow the existing `BridgeContext` + `ApplicationServices` handler pattern from adjacent read handlers.
135+
136+
**Outcome:** Config export UI fully functional in web admin. Preview shows generated config, paths reports target locations, backup and export write to the server filesystem. `pnpm validate` clean.
137+
138+
---
139+
140+
### Phase 3 — OAuth SSE fan-out + builtin channel alignment (~half day)
141+
142+
- In `apps/desktop/src-tauri/src/services/oauth.rs`, replace `app.emit("oauth-consent-request", ...)` and `app.emit("oauth-client-changed", ...)` with `emit_ui_channel(...)` calls so events reach both Tauri IPC and the admin SSE hub
143+
- Verify `ui_events.rs` has matching enum variants for `OAuthConsentRequest` and `OAuthClientChanged`; add if missing
144+
- Fix `builtin-server-config-changed` channel name: `ui_events.rs` currently emits this as `server-changed` (via `map_domain_event_to_ui`); change the SSE mapping to emit `builtin-server-config-changed` so `BuiltinServersPage` live-refreshes without a page reload
145+
146+
**Outcome:** OAuth consent modal fires in web admin when a server needs authorization. Client grant changes reflect live without refresh. Builtin server enable/disable toggles update the page immediately via SSE.
147+
148+
---
149+
150+
### Phase 4 — Web-native file picker fallback (~half day)
151+
152+
For each call site that uses `@tauri-apps/plugin-dialog` `openDialog()` or `shell/index.ts` `pickPath()`:
153+
154+
- `SpaceBaseDirsModal.tsx` — wrap `openDialog()` call in `if (isTauri())` block; render `<input type="text" placeholder="Enter absolute path" />` in the `else` branch
155+
- `ServersPage.tsx` — wrap `pickPath()` usage similarly; the text input value feeds the same state setter
156+
- `WorkspacesPage.tsx` — same pattern for icon path entry
157+
158+
No new API endpoints. The text input path value is passed to the existing `apiCall` command that was already accepting a string.
159+
160+
**Outcome:** Base dirs modal opens in web admin with a text field instead of a native picker. Server and workspace path fields render a text input. No crash on `openDialog()`. Desktop Tauri behaviour unchanged.
161+
162+
---
163+
164+
### Phase 5 — Dead code cleanup + test coverage (~1 hour)
165+
166+
- Delete `export_config` caller from `gateway.ts` (superseded by `export_config_to_file`)
167+
- Delete `connect_server` caller from `gateway.ts` (superseded by `enable_server_v2`)
168+
- Delete `disconnect_server_v2` caller from `serverManager.ts` (superseded by `disconnect_server`)
169+
- Remove corresponding Tauri commands from `src-tauri/src/commands/` if no remaining callers
170+
- Remove their route mappings from `fetch-api.routes/`
171+
- Extend `tests/ts/admin-transport.test.ts`:
172+
- Add builtins route coverage (`list_builtin_servers`, `set_builtin_server_enabled`, `set_builtin_tool_enabled`)
173+
- Add config-export route coverage (all 5 new routes)
174+
- Add SSE event channel coverage for `oauth-consent-request`, `oauth-client-changed`, `builtin-server-config-changed`
175+
176+
**Outcome:** `pnpm validate` clean, no dead `apiCall` entries. `admin-transport.test.ts` covers all registered routes including the newly added config-export and builtin commands. Transport parity between Tauri and web admin is fully tested.
177+
178+
---
179+
180+
## Key files referenced
181+
182+
| File | Note |
183+
| ---- | ---- |
184+
| [`apps/desktop/src/lib/backend/data/transport.ts`](../../apps/desktop/src/lib/backend/data/transport.ts) | `apiCall` / `isTauri()` dispatcher |
185+
| [`apps/desktop/src/lib/backend/data/fetch-api.routes/`](../../apps/desktop/src/lib/backend/data/fetch-api.routes/) | Command → HTTP route mappings |
186+
| [`crates/mcpmux-gateway/src/admin/router.rs`](../../crates/mcpmux-gateway/src/admin/router.rs) | Rust admin route registry — 5 new config-export routes go here |
187+
| [`crates/mcpmux-gateway/src/admin/command_bridge/read.rs`](../../crates/mcpmux-gateway/src/admin/command_bridge/read.rs) | Read handler pattern for new config-export bridges |
188+
| [`apps/desktop/src-tauri/src/services/oauth.rs`](../../apps/desktop/src-tauri/src/services/oauth.rs) | Phase 3 target — `app.emit``emit_ui_channel` |
189+
| [`apps/desktop/src-tauri/src/services/ui_events.rs`](../../apps/desktop/src-tauri/src/services/ui_events.rs) | `emit_ui_channel` + event channel name mappings |
190+
| [`apps/desktop/src/lib/backend/events/admin-sse-hub.ts`](../../apps/desktop/src/lib/backend/events/admin-sse-hub.ts) | SSE hub — receives domain events for browser clients |
191+
| [`apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx`](../../apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx) | Phase 4 target — unguarded `openDialog()` |
192+
| [`scripts/dev-web-admin.mjs`](../../scripts/dev-web-admin.mjs) | Phase 1 target — broken `runPrep()` reference |
193+
| [`apps/desktop/vite.config.ts`](../../apps/desktop/vite.config.ts) | Phase 1 target — missing `/api` proxy |
194+
| [`tests/ts/admin-transport.test.ts`](../../tests/ts/admin-transport.test.ts) | Phase 5 target — incomplete route coverage |
195+
196+
---
197+
198+
## Related documentation
199+
200+
- [`docs/planning/dev-rebased-post-port-completion.md`](./dev-rebased-post-port-completion.md) — Phase 2 lib/api migration this doc builds on
201+
- [`docs/planning/dev-to-main-port.md`](./dev-to-main-port.md) — original 8-phase port
202+
- [`docs/frontend/technical/backend-facade.md`](../frontend/technical/backend-facade.md)`apiCall` / fetch-api architecture reference

0 commit comments

Comments
 (0)