Skip to content

feat: organize global settings into categories, give every page an address and stop list flicker - #74

Merged
zhaozhaozz merged 1 commit into
mainfrom
feat/global-settings-redesign
Oct 28, 2026
Merged

zhaozhaozz merged 1 commit into
mainfrom
feat/global-settings-redesign

Conversation

@zhaozhaozz

Copy link
Copy Markdown
Collaborator

Global settings, the console's addresses and the task-list refresh, redone together.

Global settings

The page used to be one grid of seven unrelated cards. It now uses the project page's layout: categories on the left, one category on the right. Each category has its own address, #/settings/<category>.

Category Cards
常规 running version, default task concurrency
公网接入 public HTTPS address, Cloudflare Tunnel
机器人账号 the GitHub App the wizard uses, then every registered account (add, check permissions, delete)
模型 default shared model, shared models
用户 users (add, edit project-admin grants, delete)
  • The top-level "机器人账号" and "用户" tabs are removed; their content is the two categories above. Their old addresses, #/accounts and #/users, redirect to #/settings/accounts and #/settings/users.
  • The add form is now opened by kind ("新增账号", "新增用户") instead of from the shared toolbar button, which is removed.
  • Each category loads only what its cards show. The categories are drawn at once and the cards follow when their data arrives.

Addresses

Every tab now writes its address: #/projects, #/board, #/costs, #/reviews, #/developments, #/events, #/external, #onboarding and #/settings/.... A reload or a shared link opens the same page; before this, clicking a tab cleared the address, so a reload always returned to the projects list.

  • A tab click writes the address with pushState and draws the page in the same click, so there is no gap waiting for hashchange. Back and forward still work through hashchange.
  • An unknown address keeps the current page, as before.
  • A project administrator who opens a settings address gets the projects page.
  • Signing out returns to the default page, except in the wizard, which keeps its address as before.

Flicker

The review and development lists refreshed every 5 seconds through the full render(). That emptied the list and showed "正在加载…" until the request returned, so the page blinked every 5 seconds.

The periodic refresh is now quiet:

  • the list stays on screen while it loads;
  • nothing changes when the rows are the same;
  • otherwise only the rows whose data changed are replaced. A running task's runtime changes on every refresh, so its row updates while finished rows keep their nodes, hover and focus.
  • The refresh also skips hidden tabs.

Tests

  • test/concurrency.test.mjs: the settings tests walk the categories: navigation, cards per category, the address change flagging the App's webhook in the accounts category, and the accounts list.
  • New e2e/console-addresses.spec.mjs: every tab's address survives a reload; settings categories open and reload in place; the old accounts address redirects; the review list refreshes without being emptied and keeps unchanged rows while a changed row updates; a project admin's settings address falls back to projects; 390px has no sideways scroll.
  • e2e/control-plane, e2e/app-permissions and e2e/admin-grants reach accounts and users through global settings.
  • test/docker-env.mjs had two stale steps:
    • It expected 7 project categories for a project admin; 外部执行 was added since, so it is now 8.
    • It accepted a browser confirm() for reset, but reset now asks in the console's own dialog.

Verification

  • npm run check: passed
  • npm test: 271 passed
  • npm run test:e2e: 17 passed
  • npm run test:docker (real Docker project environments): 18 passed
  • npm run test:agent (real Docker and Pi): 21 passed

@agent-ci-infra-bot agent-ci-infra-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit: 96085b8
Coverage: Snapshot: 200 files, 3398449 bytes; skipped 0 entries; Initial model context: 22 files; other accepted files available through tools; Git workspace contains a fixed snapshot, not full upstream history

Comment thread static/settings.js
},
// Where GitHub's webhooks arrive: a self-hosted HTTPS address, or a Cloudflare Tunnel in front of it.
async access(request) {
const view = settingsView = { request, onboarding: await api('/api/onboarding'), accounts: [], models: [] };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F1 — medium, suggestion, state handling — A superseded category load can win the shared settingsView, so model actions on the 模型 category stop redrawing the cards

  • Where: static/settings.js:49 (same pattern at 57 and 64; staleness check at line 19; use at lines 183-186)
  • Problem: every category loader publishes the module-wide settingsView as soon as its data arrives (const view = settingsView = { request, onboarding: await api(...), ... }), but renderSettings compares request !== renderVersion only after the loader returns. A load that has already been superseded can therefore resolve last and become settingsView. refreshSettingsModels compares the view it was built with against settingsView and returns without drawing, so adding, editing, deleting or re-pricing a shared model on the visible 模型 category leaves the card list unchanged and suppresses the 已保存… flash.
  • Conditions: two category loads overlap and the earlier one resolves after the later one — e.g. 公网接入 is opened and 模型 is clicked before its /api/onboarding returns; that request waits 3 s + 3 s on the tunnel-metrics endpoint (src/onboarding/service.ts:39-49) when a tunnel token is saved but the tunnel is unreachable.
  • Impact: the action succeeds server-side, the page keeps showing the stale list, and no error appears; the user has to press 刷新. (Confirmed by driving renderSettings with an out-of-order respond in test/console-page.mjs.)
  • Fix: only publish the view after the staleness check — have the loaders return { view, cards } and do if (request !== renderVersion) return; settingsView = view; content.replaceChildren(...cards); in renderSettings.

@agent-ci-infra-bot

Copy link
Copy Markdown

This PR splits 全局设置 into five addressed categories (with #/accounts and #/users redirected), gives every console page a hash address that survives reload, and replaces the 5-second full re-render of the task lists with an in-place row diff that leaves unchanged rows alone. I verified npm run check and npm test (271 pass) locally; the change's CI run for 96085b8 is green (unit, e2e, images), although e2e cannot run in this container because the bundled chromium is missing libglib. Two narrow issues are reported below as suggestions; nothing blocks, and the PR still applies to main's head (base 48e7a3e) with no overlapping merged work.

F2 — medium, suggestion, routing — Deleting your own account leaves the settings address, so the next login lands on the previous page

  • Where: static/app.js:220-221 (self-delete branch of deleteUser) versus the new logout handler at static/app.js:306
  • Problem: the new logout path clears the address (if (!onboardingAddress.test(location.hash)) history.replaceState(null, '', location.pathname)) so whoever signs in next starts on the default page, keeping the hash only for the wizard. The self-delete branch ends the session the same way (me = null; start() then returns at if (!me) return;) but never clears location.hash, so #/settings/users survives.
  • Conditions: a super administrator deletes their own account while a settings category is open, and someone logs in as a super administrator in the same browser afterwards.
  • Impact: the next session opens 全局设置 → 用户 instead of 项目 — the carry-over the PR's logout handling explicitly avoids. A project administrator is unaffected because route() forces them to 项目.
  • Fix: clear the address in the self-delete branch the same way (or route it through the logout path), e.g. if (!onboardingAddress.test(location.hash)) history.replaceState(null, '', location.pathname) next to tab = 'projects'.

Reviewed commit: 96085b8
Coverage: Snapshot: 200 files, 3398449 bytes; skipped 0 entries; Initial model context: 22 files; other accepted files available through tools; Git workspace contains a fixed snapshot, not full upstream history

@agent-ci-infra-bot agent-ci-infra-bot Bot added bot-reviewed enhancement New feature or request labels Oct 28, 2026
@zhaozhaozz
zhaozhaozz added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 5e6030f Oct 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-reviewed enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant