feat: service console auto-connect, settings panel, and batch of enhancements
- Auto-connect WebSocket on page load in service console - Settings modal for editing sgclaw_config.json (API key, base URL, model, skills dir, etc.) - UpdateConfig/ConfigUpdated protocol messages for remote config save - save_to_path() for SgClawSettings serialization - ConfigUpdated handler in sg_claw_client binary - Protocol serialization tests for new message types - HTML test assertions for auto-connect and settings UI - Additional pending changes: deterministic submit, org units, lineloss xlsx export, browser script tool, and docs 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Helper Page Lifecycle Fix v2 — Same-Connection Close + Open
|
||||
|
||||
> **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:** Prevent orphaned helper pages across process restarts by closing existing ones before opening new ones, all on the same WebSocket connection.
|
||||
|
||||
**Architecture:** In `bootstrap_helper_page`, after registering with the browser WS, send `sgHideBrowerserClosePage` (best-effort, silently ignored if no page exists), then send `sgHideBrowerserOpenPage`. Change `use_hidden_domain` to `true`.
|
||||
|
||||
**Tech Stack:** Rust, tungstenite, SuperRPA browser WS protocol
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add close-before-open in bootstrap_helper_page
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/browser/callback_host.rs:345-374` (bootstrap_helper_page function)
|
||||
|
||||
- [ ] **Step 1: Add close command before open command in bootstrap_helper_page**
|
||||
|
||||
Replace the current `bootstrap_helper_page` function. After `recv_bootstrap_prelude`, send the close command first, then the open command:
|
||||
|
||||
```rust
|
||||
fn bootstrap_helper_page(
|
||||
browser_ws_url: &str,
|
||||
request_url: &str,
|
||||
helper_url: &str,
|
||||
use_hidden_domain: bool,
|
||||
) -> Result<(), PipeError> {
|
||||
let (mut websocket, _) = connect(browser_ws_url)
|
||||
.map_err(|err| PipeError::Protocol(format!("browser websocket connect failed: {err}")))?;
|
||||
configure_bootstrap_socket(&mut websocket)?;
|
||||
websocket
|
||||
.send(Message::Text(
|
||||
r#"{"type":"register","role":"web"}"#.to_string().into(),
|
||||
))
|
||||
.map_err(|err| PipeError::Protocol(format!("browser websocket register failed: {err}")))?;
|
||||
let _ = recv_bootstrap_prelude(&mut websocket);
|
||||
|
||||
// Close any orphaned helper page from a previous process run.
|
||||
// Best-effort: if no page exists, the browser silently ignores this.
|
||||
let (open_action, close_action) = if use_hidden_domain {
|
||||
("sgHideBrowerserOpenPage", "sgHideBrowerserClosePage")
|
||||
} else {
|
||||
("sgBrowerserOpenPage", "sgBrowserClosePage")
|
||||
};
|
||||
let close_payload = json!([request_url, close_action, helper_url]).to_string();
|
||||
let _ = websocket.send(Message::Text(close_payload.into()));
|
||||
|
||||
let payload = json!([
|
||||
request_url,
|
||||
open_action,
|
||||
helper_url,
|
||||
])
|
||||
.to_string();
|
||||
websocket
|
||||
.send(Message::Text(payload.into()))
|
||||
.map_err(|err| PipeError::Protocol(format!("helper bootstrap send failed: {err}")))?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Key changes from current code:
|
||||
- After `recv_bootstrap_prelude`, add the close command (best-effort, ignore errors)
|
||||
- Compute both `open_action` and `close_action` from `use_hidden_domain` flag
|
||||
- Send close first, then open on the same WebSocket connection
|
||||
|
||||
- [ ] **Step 2: Change `use_hidden_domain` to `true` in server.rs**
|
||||
|
||||
In `src/service/server.rs`, at the `start_with_browser_ws_url` call, change `false` to `true`:
|
||||
|
||||
```rust
|
||||
match LiveBrowserCallbackHost::start_with_browser_ws_url(
|
||||
browser_ws_url,
|
||||
&bootstrap_url,
|
||||
Duration::from_secs(15),
|
||||
BROWSER_RESPONSE_TIMEOUT,
|
||||
true, // use_hidden_domain: hidden domain for invisible helper
|
||||
) {
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build**
|
||||
|
||||
Run: `cargo build 2>&1`
|
||||
Expected: 0 errors.
|
||||
|
||||
- [ ] **Step 4: Run callback_host tests**
|
||||
|
||||
Run: `cargo test --lib -- callback_host 2>&1`
|
||||
Expected: 12 tests pass (including `live_callback_host_sends_bootstrap_open_page_command` which still checks for `sgBrowerserOpenPage` because the test passes `false`, and `live_callback_host_hidden_domain_sends_hide_open_page_command` which passes `true`).
|
||||
|
||||
Note: The test passes `false` for `use_hidden_domain`, so the close command will use `sgBrowserClosePage`. The test's fake WebSocket server will receive both the close and open frames. The test only checks that `sgBrowerserOpenPage` is present, which is still true.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/browser/callback_host.rs src/service/server.rs
|
||||
git commit -m "fix(callback_host): close orphaned helper page before opening new one on same WS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Full verification
|
||||
|
||||
**Files:** None (verification only)
|
||||
|
||||
- [ ] **Step 1: Full test suite**
|
||||
|
||||
Run: `cargo test 2>&1`
|
||||
Expected: All tests pass except pre-existing `lineloss_period_resolver_prompts_for_missing_period` failure.
|
||||
|
||||
- [ ] **Step 2: Verify key behavioral changes**
|
||||
|
||||
Manually confirm:
|
||||
1. `bootstrap_helper_page` sends close command before open command (both on same WS connection)
|
||||
2. `use_hidden_domain` is `true` in `server.rs` — helper page opens in hidden domain
|
||||
3. `Drop for LiveBrowserCallbackHost` remains simple (shutdown only, no close attempt)
|
||||
4. `cached_host` is still in `mod.rs` outer loop (process-internal deduplication)
|
||||
Reference in New Issue
Block a user