Skip to content

feat(frontend): P2-B wire views to real API — market/sharing/transactions/dashboard/api-keys/admin (v0.3.2) - #79

Merged
argszero merged 1 commit into
mainfrom
feat/frontend-p2b-views
Aug 18, 2026
Merged

argszero merged 1 commit into
mainfrom
feat/frontend-p2b-views

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

Backend P2-B — wire each page of the UI prototype to the real backend API (rant 2026-08-18T12:02:40.823867, "前端对接 P2-B — 各页面数据对接真实 API(市场/共享/交易/仪表盘/API Key/管理)"). v0.3.2.

Backend:

  • DELETE /api/api-keys/:id — soft revoke (status='revoked', owner-only; GET /api/api-keys now lists only active)
  • Tests 63 → 64 (api_key_delete_revokes_own_only)

Frontend (ui/js/app.js, ui/js/api.js):

  • api.del() added
  • Live data layer: caches for models / sharings / transactions / wallet / dashboard / apiKeys / adminUsers / adminUsage; per-view async loaders (logged-in only); graceful fallback to mock + retry on failure (no white screens)
  • Marketplace: GET /api/models (availability badge from available_keys); "使用/消费" → POST /v1/chat/completions (minimal placeholder, stream=false) → success toast + wallet refresh, backend errors (402/503) shown; guest mode keeps static list + login hint
  • Sharing: GET /api/sharings list + POST /api/sharings create + PATCH /api/sharings/:id pause/resume/delete
  • Transactions: GET /api/transactions?type= paged; tab filter + summary + CSV all use the same source
  • Dashboard: /api/wallet (month_use/month_earn) + /api/dashboard (month aggregation + 7-day series for sparkline)
  • Settings API Keys: GET/POST/DELETE /api/api-keys (full key shown once at creation)
  • Admin: GET /api/admin/users + POST /api/admin/credits (inline top-up) + GET /api/admin/usage; departments / raise-requests / ops remain mock, tagged 「演示数据(P2-C 补齐)」

Related Issue

Rant: 2026-08-18T12:02:40.823867 (P2-B view integration)

Tests

  • 64/64 pass (+1 DELETE api-key)
  • cargo fmt --check + cargo clippy --all-targets -- -D warnings clean; node --check on JS
  • Live smoke (release v0.3.2): models list (10, availability), sharings create→pause→delete, transactions, dashboard (month/series/net), api-key create→list→delete→gone, admin credits (balance 12471→12571) + users + usage, use-model → 503 with backend error message

Checklist

  • Branch per convention (feat/frontend-p2b-views)
  • Conventional Commits message
  • Single purpose, minimal change

后端:
- DELETE /api/api-keys/:id 软删(status→revoked,仅属主;list 只列 active)
- 测试 63→64(api_key_delete_revokes_own_only)

前端(ui/js/app.js + api.js):
- api.js 补 del()
- Live 数据层:models/sharings/transactions/wallet/dashboard/apiKeys/adminUsers/adminUsage 缓存 + loaders(登录拉取,游客/失败降级 mock + 重试)
- 市场页:/api/models(可用性 available_keys>0 绿/繁忙黄);「使用/消费」→ POST /v1/chat/completions(最小占位,成功 toast + 刷新钱包,失败显示后端错误 402/503);游客静态列表 + 登录提示
- 共享页:/api/sharings 列表 + POST 上架 + PATCH 暂停/恢复/删除(软删 off)
- 交易页:/api/transactions?type= 分页 + tab 联动(汇总/导出同源)
- 仪表盘:/api/wallet month_use/month_earn + /api/dashboard month/series(sparkline)
- 设置页:/api/api-keys 列表 + POST 生成(完整 key 仅生成时展示一次)+ DELETE 撤销
- 管理视图:/api/admin/users + 充值(/api/admin/credits)+ /api/admin/usage;部门/加额/ops 保留 mock 并标注「演示数据(P2-C 补齐)」
@argszero
argszero merged commit f525242 into main Aug 18, 2026
1 check passed
argszero added a commit that referenced this pull request Sep 13, 2026
## Summary

Two hand-written rosters in `ui/js/app.js` enumerated the "live" containers by id, and **both omitted the admin models table `#model-body`** — which was added later (PR #97) while the rosters date from PR #69/#79:

1. **`bindLiveRetry("<id>", fn)`** (13 entries, bound once at `DOMContentLoaded`) attached the load-error 重试 button of each container. `#model-body` was never registered, so `renderAdminModels()` rendered a 重试 button that **did nothing when clicked**. The smoking gun is the renderer itself: `loadErrorHtml(emptyLabel, retryFn, retryLabel)` declared a `retryFn` parameter and **never used it** — the renderer was always meant to hand its loader over, and the roster was the only thing wiring it up.
2. **`KBD_TABLE_IDS`** listed the tables reachable by ↑/↓/Enter. `#model-body` was absent, so its rows could not be activated (no `.row-active`) while all seven sibling tables could.

The fix **removes both rosters instead of extending them** — a roster that must be maintained by hand is the defect, not the one missing entry. Both containers are now derived from the DOM, so no future table can be forgotten:

| | Before | After |
|---|---|---|
| retry wiring | `bindLiveRetry(id, fn)` ×13 + per-container `addEventListener` | `setLiveError(container, html, loader)` — the renderer hands over its loader; one-time container delegation (`WeakMap`/`WeakSet`) dispatches `[data-live-retry]` |
| kbd container | `KBD_TABLE_IDS.indexOf(tb.id) >= 0` | `kbdTbodyOf(t) = t.closest("tbody")`, via a single **document-level** click delegation |

Runtime evidence (a jsdom probe booting the real `index.html` + the four scripts, only `fetch` stubbed so the admin endpoints 500):

| check | pre-change | post-change |
|---|---|---|
| `#dept-body` retry re-requests (control) | 5 | 5 |
| `#model-body` retry re-requests (test) | **0** | 5 |
| `#model-body` second retry click | **0** | 5 |
| `#dept-body` row click activates (control) | true | true |
| `#model-body` row click activates (test) | **false** | true |
| raise-requests rows activate (declared side effect) | **false** | true |
| a run-time-injected table's rows activate (no roster can cover it) | **false** | true |
| **verdict** | **DEFECT — 5/7 failed** | **OK — 7/7** |

## Related Issue

None — the repository carries no open issues. Found by reconciling the two enumerated rosters against the tables that actually exist.

## Changes

- [x] `ui/js/app.js`
  - `setLiveError(container, html, loader)` replaces the deleted `bindLiveRetry`; the dead `retryFn` parameter is dropped from `loadErrorHtml`. All **12** `loadErrorHtml`/`loadErrorRow` call sites now pass their loader through it.
  - `KBD_TABLE_IDS` deleted; the keyboard container is derived from the event target (`kbdTbodyOf` = `closest("tbody")`) and the row/table click delegations are merged into a single document-level listener.
  - `kbdRows`/`kbdContainerFrom` treat a container that has been rebuilt (`isConnected === false`) as "no active table" instead of highlighting rows that are gone.
- [x] `ui/index.html` — cache-bust `js/app.js?v=20260913-2` → `-3`.
- [x] `ui/README.md` — the keyboard-nav and degradation-pattern sections named both deleted rosters; they now describe the DOM-derived contract.
- [ ] 涉及配置/数据结构的改动已同步示例文件 — **N/A**: no config or data-structure change.

**Declared side effect** (a consequence of "every data table is navigable"): the JS-built raise-requests table now has keyboard row navigation. It was outside the old roster *and* passed `null` for the retry callback, so its degraded state had no 重试 button at all; it now reloads through `loadAdmin()`.

## Tests

- [x] `cargo test` 全部通过 — **190 passed / 0 failed**, i.e. **unchanged** from `main` (this change is JS + docs only; the count staying put is the evidence that no Rust behaviour moved).
- [x] `cargo fmt --check` 通过
- [x] `cargo clippy --all-targets -- -D warnings` 通过
- [ ] 新增/更新了单元测试 — **N/A**: `ui/` carries no test harness in-repo (the probes live outside it), and the change's assertion is the jsdom A/B above: with the pre-change `app.js` the probe is **red (5/7)**, with this branch it is **green (7/7)**, and the `#dept-body` control passes in both legs.

## Checklist

- [x] 分支命名符合约定 — `fix/ui-live-container-rosters`
- [x] Commit message 使用 Conventional Commits 格式 — `fix(ui): derive the live containers from the DOM, not from rosters`
- [x] 单一职责,改动最小化 — one defect class (enumerated live containers), no opportunistic refactor; `node --check ui/js/app.js` clean.
argszero added a commit that referenced this pull request Sep 13, 2026
…e's x axis is time (#218)

`GET /api/dashboard`'s `series` was a sparse day-bucket list: `GROUP BY
date(time)` emits a row only for days that actually have transactions, and
`ui/js/app.js::renderMonthChanges` feeds that list straight into
`sparkline()`, which places point i at an INDEX-based x
(`pad + i*(w - 2*pad)/(len - 1)`) rather than at its date. Days without
transactions were therefore collapsed instead of being shown as zero:

  * a week with a single active day degenerated to a lone `M` command, i.e.
    no line at all, while the number above it still rendered;
  * a week with two active days was drawn as one straight line across the
    full 7-day width, claiming a trend the data does not support.

Every sibling time series in this tree is zero-filled to its own window and
says why: `routes/ops.rs::runtime` fills hours 0..23 ('若不补零前端柱状图会
整体左移'), `app.js::dashTrendDays` and `app.js::txTrendDays` fill their day
buckets, and the guest branch of this very function always hands over 7
points. This was the only consumer that did not, so treat it as the omission
it is: `f525242` (#79) wired the sparse series up and the siblings were fixed
afterwards (#155/#156 and the ops PR6).

Fix: build the window with a recursive CTE (today plus the previous 6 days,
UTC) and a LEFT JOIN whose ON clause carries the user filter. The filter has
to stay in the JOIN condition: in WHERE it would drop exactly the empty days
this change exists to add. Only `src/routes/wallet.rs` changes; the window
semantics (a 7-day day-key window anchored on date('now', '-6 days'), UTC)
and the `month` / `net` windows of C2049 are untouched.

Evidence, all driven through the real router:
- `dashboard_series_is_a_zero_filled_seven_day_window`: 7 rows, ascending,
  the date set compared against chrono (`Utc::now()`) as an independent
  authority rather than derived from the CTE.
- `dashboard_series_zero_fills_inside_the_user_and_the_window`: per-day
  values equal a plain per-day SUM for the same user; another user's row
  inside the window and this user's row outside it never appear; a positive
  control re-checks a zero-filled day after a later transaction lands on it.
- `wallet_summary_and_dashboard`'s `!series.is_empty()` (trivially true once
  the window is filled) is rewritten into the shape assertion it meant.
- A/B, each mutation applied alone and reverted with the file's md5 restored:
  the pre-change sparse query reddens 3 tests (including both new ones); an
  off-by-one window (`-5 days`) reddens 2, a different set; carrying the user
  filter in WHERE reddens those same 3; comparing a datetime column against a
  date key reddens exactly 1 (the value test). Unmutated tree: 222 passed,
  0 failed.
- jsdom end-to-end (real `ui/index.html` plus the four real scripts, only
  `fetch` stubbed), fed the exact series the patched handler returns: 7
  points / 6 line segments / 7 tooltips, against 1 point / 0 segments for the
  pre-change shape. The 14-day bar chart on the same page keeps rendering 14
  columns in every leg.

Display / API shape only: no ledger, balance or settlement change. No `ui/`
or i18n key touched, so the i18n gate counts are unchanged.
`cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` are clean;
`cargo test` goes from 220 to 222.
argszero added a commit that referenced this pull request Sep 13, 2026
…ow number (#227)

Three tables rebuild their list with .filter() and then tag every row button with
the index *inside the searched list*, while the handlers resolve that index
against the full cached array:

  #api-keys   copyKey / renameKey / deleteKey   -> Live.apiKeys[i]
  #model-body editModelRow / deleteModel        -> Live.adminModels[i]
  #dept-body  openDeptForm / deleteDept         -> Live.departments[i]

So as soon as the search box hides one earlier row, a button acts on a different
record than the row it sits in: the delete removed the wrong API key / model /
department, copy handed out another key's secret, and the edit form opened with
another record's values.

Both correct idioms already exist in the same file — #emp-body tags rows with
users.indexOf(u) and #ops-body with u.id — these three tables were simply never
updated when their search boxes were added (#79, #80, #97).

- api-keys: carry the cached index through the mapping, add data-key-row and
  resolve the row by that locator instead of nth-child
- model-body / dept-body: derive the locator with indexOf, mirroring #emp-body
- bump the app.js cache-bust
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant