fix: ServiceAccountInfo selectable + Win10 shadow + window auto-fit (#234 #235 #236)#237
Merged
fix: ServiceAccountInfo selectable + Win10 shadow + window auto-fit (#234 #235 #236)#237
Conversation
App.vue disables user selection globally so the shell feels native,
but the Account Details dialog is read-only data the user frequently
copies into game-support tickets (sid / ssn / sname / sauthtype).
Override user-select on .service-account-info__value so every row
in the dialog can be highlighted and copied.
Status text ("Normal" / "Banned") is covered by the same class; the
WPF original also rendered it as a selectable TextBlock so the
behaviour matches.
Fixes #234
…235) Upstream tauri-apps/tauri#11654 / #13176: on Windows 10 the tao undecorated-shadow inset calculation paints a 1px coloured artefact border around the window after DWM redraws. Maintainer-confirmed workaround is `set_shadow(false)`, but we only want it on Win10 — Windows 11 (build >= 22000) rendered the same shadow correctly since tao #1052 and the shadow there gives the rounded glass panel its depth. Adds `os_info` (Windows-only, default-features off so no transitive serde) to detect the build number. `is_windows_10()` is conservative: non-Windows and non-semantic versions both fall back to "leave shadow alone" so a mis-detection on Win11 can never regress that path. The shadow flip lives in the Tauri `.setup()` callback because `WebviewWindow::set_shadow` is only available after the window is constructed; declaring `"shadow": false` in tauri.conf.json would also disable it on Win11. Fixes #235
`fitWindow` hard-capped the auto-fit height at 900px, which was smaller than several pages when their content expanded (Settings with the full Game section visible, AccountList with a populated service-account list + game grid). The cap forced those pages to paint an inner scrollbar inside `__scroll` — this is the "從設定 返回還有滑條能上下滾動" symptom in issue #236. Use the actual display height (`window.screen.availHeight`) minus a 50px taskbar safety margin instead. The pages already obey an `overflow: hidden` root + flex-child `overflow-y: auto`, so capping at the display height just lets them take the space they naturally need without letting the window chrome eat the taskbar. Falls back to the old 900 constant when `window.screen` is unavailable (jsdom / headless CI) so the existing router spec harness keeps working without environment setup. Refs #236 (bug C)
The previous `setTimeout(80)` + async `height='auto' → setSize().then (height='100vh')` flow had four separate bugs that together produced the symptoms in issue #236: 1. **Timing race**: 80ms wasn't tied to Vue's render flush or the browser's paint, so slow config.xml IPC left the observer attached to a half-rendered DOM. 2. **Navigation storm**: first launch fires `afterEach` twice in rapid succession (`/` → region picker → auto-redirect via `LoginRegionSelection.vue::watch(config.loaded)`) and both timeouts ran, the earlier one measuring the picker DOM just as it was being replaced. 3. **Layout-restore race**: `setSize` is async IPC and the `height='auto'` flip lived for a frame-and-a-half until the `.then()` callback reset it back to `100vh`, producing visible flicker if the new content was taller than the old window. 4. **Observer feedback loop**: the `height='auto'` flip itself tripped the ResizeObserver, which fired `fitWindow` again. Replacing the timeout with a shared double-`requestAnimationFrame` handle (`scheduleOnNextPaint`) addresses all four: - Double rAF straddles Vue's microtask flush so measurement sees the settled DOM. - A single shared `pendingFrame` handle means cancelling the previous callback before scheduling the next one — the afterEach storm collapses to a single fit on the final destination. - `fitWindow` now flips `height: auto` → read `scrollHeight` → flip `height: 100vh` in the same synchronous block (forced- layout read followed by a write, both before the next paint) so the browser never renders the intermediate unclipped state. - Same-frame flip-back coalesces the observer notifications (the computed-style value ends where it started) so there's no feedback loop — the `skipFirstObserverCallback` flag additionally swallows the synthetic `observe()` notification so we don't fit twice in a row when switching routes. jsdom / SSR paths fall through to synchronous execution so the router spec harness keeps working without a rAF polyfill. Refs #236 (bugs A, B, D, E; bug C was the 900px cap fixed in the previous commit)
Follow-up to 85c071b. Users reported the window still paints an inner scrollbar after a language switch, because the ResizeObserver was only watching `[data-window-root]` — that element is locked to `100vh`, so content-only height changes (i18n string swaps, async data arriving, drop-downs) never triggered a re-fit. - Each page now marks its real content wrapper with `data-window-content` (AccountList / Settings / About / ManageAccount `__container`, plus a new wrapper around `<RouterView>` in LoginPage). These wrappers actually grow/shrink with the content, so observing them catches the changes the root misses. - `attachObserver` in `src/router/index.ts` now observes both the root and the content wrapper (when present). - The "skip first ResizeObserver callback" flag was racy with two observed targets (synthetic initials can arrive in one or two callbacks), so it's been replaced with an rAF gate that swallows every notification until the first post-attach animation frame. That reliably excludes the synthetic initials without guessing how they'll be batched. Existing router + LoginPage + AccountList specs stay green.
Reported against the post-#236 auto-fit rework: on screens where the total page height (game bar + quick actions + account list + OTP section) exceeded the OS-available height, the window was capped at `screen.availHeight - 50` and the *outer* `account-list__scroll` container ate the overflow. That pushed the OTP row below the fold and forced the user to mouse-wheel before they could hit "Get OTP" — the legacy Beanfun (B6) client shows everything on a single page instead. Flip the layout so the outer container no longer scrolls and the inner account list is the only scroller: - `.account-list__scroll` → `overflow: hidden` + flex column so it distributes space to its children instead of clipping them with an outer scrollbar. - `.account-list__container` / `.account-list__list` → `flex: 1` + `min-height: 0` so the flex chain actually allows the list to shrink below its natural content height. - `.account-list__list-body` → drop the 300 px cap; `flex: 1` now determines its height from whatever space the surrounding chrome leaves, and `overflow-y: auto` keeps the list itself scrollable. Net effect: header, game bar, quick actions and the OTP footer are always visible regardless of account count; only the list scrolls. Existing AccountList + router specs (88) stay green.
Legacy Beanfun (B6) and the WPF port both treated Enter as a shortcut for the Get-OTP button once a service account was highlighted. The SPA port never wired an equivalent, so users who were used to the keyboard flow had to reach for the mouse every time. Restore the WPF `lstViewAccount.KeyDown` parity. - `src/pages/AccountList.vue` now attaches a `window`-level `keydown` listener on mount and tears it down on unmount. `handleGlobalEnter` routes to the existing `handleGetOtp` flow when all guards pass. - Guards, in order: ignore non-Enter / IME composition / key-repeat, ignore when focus is on a form control (`input` / `textarea` / `select` / `button` / `contenteditable`), ignore when focus is inside `.el-overlay` (so open `ElDialog` / `ElMessageBox` instances own the key), ignore when an OTP fetch is already in flight, ignore when nothing is selected (silent no-op — the button path keeps its `MsgSelectAccount` toast for explicit clicks). See the `handleGlobalEnter` docblock for the rationale behind each branch. - `window.dispatchEvent(keydown)` events in jsdom report the `Window` itself as `event.target`, which has no `closest()`; the handler narrows via `target instanceof HTMLElement` before probing, treating "no focused element" (common on first paint) as safe to forward. New spec `tests/unit/pages/AccountList.EnterHotkey.spec.ts` locks down the behaviour in its own file. The sibling `AccountList.spec.ts` follows the P12 convention of not unmounting wrappers between cases (so its 50+ mounts' listeners stack up on the shared `window`); Vitest's per-file jsdom isolation gives the Enter suite a clean slate without requiring sweeping changes to the sibling file. Six cases cover each guard branch plus an onBeforeUnmount leak check, and the existing 52 AccountList + 36 router specs remain green.
Per user feedback "內文字體可以稍微調大一點點" — every text class in `AccountList.vue` gets ~+0.0625rem (one pixel at the 16 px root base) so the page reads more comfortably on modern high-DPI displays without tipping over into a new visual weight. Breakdown (all numbers in `rem`): | Class | Before | After | |----------------------------|--------|---------| | `__title` | 1.625 | 1.75 | | `__subline` | 0.8125 | 0.875 | | `__game-name` | 1 | 1.0625 | | `__game-status` | 0.75 | 0.8125 | | `__balance-label` | 0.625 | 0.6875 | | `__balance-value` | 0.9375 | 1 | | `__quick-link` | 0.6875 | 0.75 | | `__list-title` | 0.875 | 0.9375 | | `__list-count` | 0.75 | 0.8125 | | `__list-state` | 0.8125 | 0.875 | | `__row-grip` | 0.875 | 0.9375 | | `__row-num` | 0.8125 | 0.875 | | `__row-name` | 0.875 | 0.9375 | | `__row-sub` | 0.6875 | 0.75 | | `__add-btn` | 0.8125 | 0.875 | | `__limit-notice` | 0.75 | 0.8125 | | `__otp-title` | 0.875 | 0.9375 | | `__otp-field` | 1.125 | 1.25 | | `__otp-get` | 0.875 | 0.9375 | Everything keeps its relative proportion so the existing visual hierarchy (title > otp-field > balance-value > row-name > ...) is preserved. All 58 AccountList + EnterHotkey specs remain green.
…scroll Follow-up to #236 feedback. The previous 80px min-height on `.account-list__list-body` was only enough for a single row, so with two accounts the second row was vertically clipped (reported on PR #237 with a screenshot). Bumped min-height to 9rem (~144px), budgeted as: row = 0.625rem * 2 padding + ~28px content ~= 52px 2 rows = 104px gap = 0.25rem between rows = 4px padding = 0.5rem * 2 on __list-body = 16px total ~= 124px plus ~20px buffer for line-height jitter / sub-pixel rounding. Keeps the list-body flexible (still `flex: 1`) so it continues to absorb window growth past two rows, but stops the flex chain from squashing it below the two-row threshold when window height is tight. Trade-off documented in the CSS block: with a single account the list has a small empty strip below the row. User preferred that over a clipped second row.
Follow-up to PR #237. On a narrow Tauri window the `disableHardwareAcceleration` checkbox tooltip rendered as "Disabling h..." because: 1. Element Plus' default popper CSS uses `white-space: normal`, which collapses the `\n` characters our i18n strings already embed to break long explanations into two sentences. The two sentences were getting glued together into one very wide line. 2. With `placement="right"` that single wide line then ran past the right edge of the Tauri window, which clips anything outside the webview bounds, producing the truncation. Added a namespaced popper class `settings__tip-popper` on all four settings tooltips (trad-login / kill-patcher / skip-play-window / disable-hw-accel) and a non-scoped `<style>` block targeting it: .settings__tip-popper.el-popper { max-width: 280px; /* lets flip strategy fall back to */ /* top or bottom when right doesn't fit */ white-space: pre-line; /* honour `\n` from i18n strings */ word-break: break-word; /* protect no-seam English copy */ line-height: 1.5; } The block is intentionally non-scoped because el-popper is teleported to `document.body` and lives outside this component's scoped CSS boundary; the class name prefix keeps the blast radius to these four tooltips. Tests: 595/595 pass, vue-tsc clean.
rustfmt collapses the single-argument `tracing::warn!` call onto one line; CI's `cargo fmt --all -- --check` step was failing on the 3-line form that landed in commit 2 of the #235 window border fix. No behavioural change.
Users have been reporting that the current build drops its
Beanfun session after a few minutes of idle, whereas the legacy
WPF client v5.9.1 could stay logged in for days. Investigation
against the WPF source revealed a `pingWorker` BackgroundWorker
(`MainWindow.xaml.cs` L2322-2368 + `bfClient.cs` L193-212) that
silently hits `echo_token.ashx?webtoken=1` every 60 seconds to
reset the portal's server-side inactivity timer. The Tauri
rewrite never ported this machinery, so the server naturally
reaps idle sessions and the user's next action (Get OTP, launch
game) fails.
This change reproduces the WPF behaviour:
- `BeanfunClient::ping()` issues the GET against `portal_base`
with `webtoken=1` appended as a query pair. The body is
deliberately discarded (not piped through `bounded_text`) —
the request is only useful for its side effect.
- `AuthContext` now owns a `tokio_util::sync::CancellationToken`
so each spawned loop has a matching shutdown signal.
- Every login path (`login_regular`, `login_totp`,
`login_qr_check` on Approved, `complete_gamepass_login`)
routes through a new `install_session_and_start_ping` helper
that installs the auth context, spawns the loop, and — if a
prior context was already installed — cancels its token so we
never leak an orphaned task holding a stale cookie jar.
- `logout` cancels the token before calling `logout_service`
so the keep-alive can't race a server-side logout.
- Error handling matches WPF's `catch { }`: failures are logged
at `tracing::debug!` level and the loop keeps running.
Tests:
- New `tests/ping.rs` (6 integration tests) pins the wire-shape
contract: GET method, `echo_token.ashx` path, `webtoken=1`
query, 2xx→Ok, 5xx→LoginError::Http, TW+HK both routing
through `portal_base`, and a huge-body 200 still resolving
(ping must not read the body).
- New unit tests in `commands::auth::tests` cover
`run_ping_loop` cancellation (pre-cancel returns immediately
without hitting the network, mid-sleep cancel wakes up the
select without waiting the full 60 s, 5xx doesn't kill the
loop via `start_paused` + `tokio::time::advance`) plus two
end-to-end tests that `install_session_and_start_ping`
populates `AppState::auth` with a live loop and that a second
install cancels the previous loop's token.
`tokio-util` was already pulled in transitively via `tauri`;
promoting it to a direct dep with `features = ["rt"]` so the
feature set can't silently regress.
Several users have asked for the launcher to come back up where they last left it — typically a secondary monitor or a specific spot on a multi-display setup. This change wires `tauri-plugin-window-state` (official Tauri plugin) into the main window so the X / Y coordinates survive across restarts. Why position-only (`StateFlags::POSITION`): `tauri.conf.json` ships `resizable: false` and the router's `fitWindow` (`src/router/index.ts`) is the canonical owner of per-route width/height. Persisting SIZE / MAXIMIZED would race those handlers and the user would see the window snap to the saved size before snapping again to the route-driven size on the first navigation. Restricting the state flags to POSITION avoids the conflict cleanly — the plugin only touches `set_position`, fitWindow keeps owning `set_size`, and there's no overlap. The state file lives under the standard `appConfigDir` (Windows: `%APPDATA%\tw.beanfun.app\`), so it's reset by the same uninstall path as the rest of Beanfun's local data and honours the existing data-deletion expectations. Plumbing: - `Cargo.toml`: declare `tauri-plugin-window-state = "2"` - `lib.rs::run`: register the plugin via `Builder::with_state_flags` - `capabilities/default.json`: grant `window-state:default` so the plugin's commands work for the main window Frontend code is untouched — the plugin auto-saves on `WindowEvent::CloseRequested` / `Destroyed` and auto-restores on init, so no JS/TS API call is needed for this feature.
Users with custom drag-and-drop sort orders reported that every
visit to the Settings (or About) page was wiping their order
back to the server default on return. The order *was* still
persisted to `Config.xml` under `AccountOrder_<code>_<region>`
and re-applied by `loadList` after the HTTP response landed —
but the spinner-then-empty-list flash made it look reset.
Root cause: `AccountList` is a routed component, so `/settings`
unmounts it and navigating back remounts it. `setupGameOnMount`
unconditionally re-ran the bootstrap (`game.loadGames` →
`selectActiveGame` → `loadList`) every mount, with no awareness
that the Pinia stores had already cached everything for the
current session.
Fix: a fast-path predicate at the top of `setupGameOnMount`:
if (account.serviceAccounts.length > 0 && auth.session !== null) {
loadState.value = 'ready'
return
}
The skip is safe because every code path that *should* trigger
a re-fetch already clears `serviceAccounts` first:
- `auth.logout()` / `auth.session_required` → `clearAccountSession()`
resets the store.
- Game change via the picker → `setActiveService` clears the store
before the new fetch.
- Cold mount after login → store empty by construction, predicate
is false, full bootstrap runs.
`loadState` would otherwise sit at its initial `'loading'`
sentinel forever (no `loadList` call to flip it), so the skip
branch flips it to `'ready'` inline before returning.
Tests:
- New regression case `skips re-fetch on remount when serviceAccounts
already cached for current session` seeds a populated store + live
session before mount, asserts `listGames` / `getAccounts` /
`setActiveService` are never called, and asserts the user's
`selectedSid` survives the round-trip.
- Companion baseline `still re-fetches on remount when
serviceAccounts cache is empty (cold mount baseline)` re-asserts
the existing post-login first-paint behaviour so a future
too-eager skip predicate fails loudly here too.
- 54/54 AccountList tests pass; `vue-tsc --noEmit` clean.
The previous CI run timed out (`cargo test` ran for 6 h before GitHub cancelled it). Root cause: the `5xx-doesn't-kill-loop` unit test combined `#[tokio::test(start_paused = true)]` with a wiremock `MockServer`. With the runtime clock paused, hyper's internal time wheel stopped advancing too, and on Windows the mocked HTTP request never resolved — the test hung indefinitely inside `client.ping().await` before the first `tokio::time::advance` call could even fire. Locally on a faster Windows host this sometimes reaches the `received_requests` check before timing out, which is why the regression escaped manual smoke testing. Refactor the production loop to expose its sleep interval as a parameter: - `run_ping_loop(client, cancel)` keeps its public signature and always passes `PING_INTERVAL` (60 s, WPF parity). - `run_ping_loop_with_interval(client, cancel, interval)` is the new private inner that callers — i.e. the unit test — can drive with a 50 ms cadence under real time. No `start_paused`, no manual `advance()`, no hyper time-wheel stall. The 5xx test now polls for two requests with a 10 s deadline backstop (expected to land in ~100 ms) and finishes in roughly the same wall-clock time as the other ping-loop tests. Defence-in-depth: also add `timeout-minutes: 25` to the rust job in `.github/workflows/ci.yml`. Healthy runs land in 4-8 min on a warm cache, so 25 leaves comfortable headroom while preventing future hangs from silently consuming the GitHub default 6 h budget. Verified locally: - `commands::auth::tests::run_ping_loop_*` (3 tests) pass in 0.08 s - `tests/ping.rs` integration suite (6 tests) pass in 0.01 s - `cargo fmt --check` + `cargo clippy --all-targets -- -D warnings` clean
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #234, #235, #236.
Summary
#234 — 遊戲帳號詳情欄位可選
src/windows/ServiceAccountInfo.vue—.service-account-info__value加user-select: textoverrideuser-select: none導致帳號 / 編號 / 名稱無法複製;override 後讓使用者能複製貼到客服信件裡ServiceAccountInfo.xaml對應位本來就是 selectable TextBlock,behaviour 一致#235 — Win10 視窗框線
set_shadow(false),只在 Win10 套用(build number < 22000)。Win11 保留預設 shadow 讓圓角玻璃面有立體感os_info = "3"(cfg(windows) scope,default-features = false)做版本偵測is_windows_10()保守:非 Windows / 非 semantic version 一律false(誤判 Win11 會失去 shadow → visible regression,誤判 Win10 沒套 shadow 才是保守選擇)#236 — 視窗自適應
src/router/index.ts::installRouterGuards裡的fitWindow原本有 6 個相關 bug:window.screen.availHeight - 50setTimeout(80)不對齊 Vue render flush/paint 時機,slow IPC 下會抓到未渲染完的 DOM → commit 4 改用 rAFheight='auto'→ asyncsetSize().then(height='100vh')之間 flex layout 會崩 → commit 4 改成 frame flipobserve()會帶一次 fire callback,加上初始化呼叫的 fit 重複觸發 →skipFirstObserverCallbackflag 略過第一次[data-window-root]永遠100vh,ResizeObserver 根本沒有「內容長高」可 fire → commit 5 同時對實際內容 wrapper 的data-window-content一併 observe;skip-initial 從「第一次 callback」改為「第一個 rAF 之前一律略過」以支援多目標額外:
pendingFramehandle 配合afterEach+ observer callback 取消 → afterEach storm(/→/login/連續兩次 redirect)自動合併為單次 fit on 真正 destination。多帳號頁面太長 / OTP 被推下去(#236 延伸)
.account-list__scroll改為overflow: hidden的 flex column;__container/__list/__list-body沿flex: 1 + min-height: 0鏈條,讓內層 list 變唯一的 scroller,OTP footer 永遠 anchored 在視窗底部max-height: 300px的 cap(這是讓 OTP 被擠出視窗外的主因)選中帳號按 Enter = GetOtp(B6 parity)
AccountList.vue加入handleGlobalEnter+ window-level keydown listener<input>/<textarea>/<button>/contenteditable 忽略;.el-overlay內部(ElDialog/ElMessageBox)忽略、進行中 selection 變動忽略AccountList.EnterHotkey.spec.ts涵蓋每條 guard branch + onBeforeUnmount leak checkMsgSelectAccounttoast 不同),減少 fat-finger Enter 被當作 UX內文字體微調(#236 延伸)
__title1.625→1.75,__row-name0.875→0.9375,__otp-field1.125→1.25 等,相對比例不變設定頁 tooltip 換行 / 寬度(用戶回報「關閉硬體加速」說明被截斷)
Settings.vue全部四個el-tooltip加上popper-class="settings__tip-popper".settings__tip-popper.el-popper { max-width: 280px; white-space: pre-line; word-break: break-word; line-height: 1.5; }— 因為 popper 被 teleport 到document.body,scoped 樣式抓不到pre-line讓 i18n 字串裡的\n正常換行,max-width+word-break防止單行擠出視窗Session keep-alive — 連線過一段時間會斷(用戶回報舊版 5.9.1 可放好幾天不斷)
MainWindow.pingWorker_DoWork(xaml.cs L2322-2368) →BeanfunClient.Ping()(bfClient.cs L193-212)BeanfunClient::ping()打echo_token.ashx?webtoken=1(與 WPF wire-shape 完全一致)commands::auth::run_ping_loop用tokio::spawn跑 60 秒 cadence 的 keep-alive taskAuthContext加ping_cancel: CancellationToken欄位;logout直接cancel()立刻收掉 background taskinstall_session_and_start_pinghelper 統一登入完成後的 (clone, install, spawn) pattern;若已有 prevAuthContext也會 cancel 它的舊 token,避免「未 logout 直接 re-login」漏 leaktracing::debug!吞掉,配合 WPFcatch { }的 best-effort 語意(5xx / 連線錯誤不會殺 loop)tests/ping.rsintegration 6 case 鎖定 wire-shape (GET / path /webtoken=1/ 2xx ok / 5xx Error / TW + HK 都走 portal_base);commands::auth::tests4 個 unit 鎖定 cancel-before-first-tick / cancel-during-sleep / 5xx-不殺 loop / install-會-cancel-prev記憶視窗位置(用戶回報希望關掉視窗下次能從同位置開)
tauri-plugin-window-state = "2"(官方 plugin)StateFlags::POSITION— 不存 SIZE / MAXIMIZED:tauri.conf.json是resizable: false且 router 的fitWindow是 size 唯一 owner,存 size 會跟它打架(用戶看到「先回到舊 size,第一次切 route 又跳到新 size」的 flicker)appConfigDir(Win:%APPDATA%\tw.beanfun.app\),跟其他 Beanfun local data 共用 uninstall pathWindowEvent::CloseRequested/Destroyed存、init 時自動 restore從 Settings/About 返回 AccountList 不重 fetch(用戶回報拖曳排序「會被刷掉」)
setupGameOnMount開頭加 fast-path:account.serviceAccounts.length > 0 && auth.session !== null→ fliploadState='ready'後直接 return/settings進去再回來會 unmount + remount,每次都跑完整 bootstrap (game.loadGames+selectActiveGame+loadList)。spinner + 短暫空 list flash 讓拖曳排序「看起來」被重置(實際上Config.xml::AccountOrder_<code>_<region>一直在,只是 re-apply 在 HTTP 回來之後)serviceAccounts:auth.logout()/auth.session_required→clearAccountSessionsetActiveService清掉 storelistGames/getAccounts/setActiveService;store 空時還是要走完整 bootstrap(baseline 防 over-aggressive skip)CI 雜項
cargo fmt修lib.rs的 視窗框線問題 #235 win10 shadow helper(前一輪 CI 失敗根因:多行tracing::warn!被 rustfmt 折成單行)run_ping_loop_keeps_running_after_ping_failure:原本用#[tokio::test(start_paused = true)]+ wiremock,paused runtime time 凍住 hyper 內部 time wheel,Windows CI 一路卡到 GitHub default 6h timeout。改為 inject interval seam (run_ping_loop_with_interval),test 用 50ms 真實時間 cadence。同時把.github/workflows/ci.yml的 rust job 加上timeout-minutes: 25,未來再 hang 不會再吃滿 6 小時 budgetWhy one PR
11 個 issue/feedback (3 issue + 8 follow-up requests) 都屬於 window chrome / AccountList 佈局 / session 行為 / visual polish 同一 surface area;切 11 個 PR 評述成本太高。不過每個 commit 都嚴守 scope 對應一個 bug / concern 以便 revert / cherry-pick。
Test plan
npx vitest run— 597/597 pass(54 AccountList + 6 EnterHotkey + 537 其他)npx vue-tsc --noEmitcleannpx eslint+prettier --checkcleancargo check+cargo clippy --all-targets -- -D warningscleancargo fmt --checkcleancargo test— auth (40) + ping integration (6) + 全 suite green on local + CI run24752238006(8.4 min)tracing::debug!log 看是否 60 秒一次Refs
shadow: false是 workaroundMainWindow.pingWorker_DoWork(xaml.cs L2322-2368) +BeanfunClient.Ping()(bfClient.cs L193-212) — Session keep-alive 對齊原版StateFlags::POSITION只存位置