chore: v2.8.21 — update progress steps, clone in background, extra SSH keys inline, today's spend

- Plugin update reports its running step (plugin-update-progress.json →
  plugin_update_progress); a hidden update or clone leaves a progress chip.
- Clone here can run in the background.
- Git identity: extra SSH keys are added inline per host (no More keys tab).
- Users: spend today (viewer's local day, ?tz_offset=).
- Prettier on the files touched since v2.8.20.
This commit is contained in:
2026-10-23 10:44:11 +07:00
parent 62b4520507
commit 9dd97a369c
33 changed files with 906 additions and 393 deletions
+19 -4
View File
@@ -2,9 +2,10 @@
* @file users.js
* @description `/api/users` — server mode's per-user view for an admin or
* analyst (members are refused by STAFF_ONLY_PREFIXES in server/lib/auth.js):
* `GET /` lists every account with its workspaces, sessions and $ spent in the
* last 7 and 30 days (from `token_usage_hourly`, sessions owned through their
* workspace — server/lib/scope.js); `GET /audit` the newest audit entries
* `GET /` lists every account with its workspaces, sessions and $ spent today
* (the viewer's local day, `?tz_offset=` = the browser's getTimezoneOffset())
* and in the last 7 and 30 days (from `token_usage_hourly`, sessions owned
* through their workspace — server/lib/scope.js); `GET /audit` the newest audit entries
* (server/lib/audit.js); `GET /storage` how much disk each user's home takes
* (server/lib/user-storage.js — measured in a worker, `?refresh=1` re-measures).
*
@@ -99,10 +100,22 @@ function usageSince(sinceHour, rules) {
const hourAgo = (days) => new Date(Date.now() - days * 864e5).toISOString().slice(0, 13);
/**
* The UTC hour the viewer's local day began in — `offset` is the browser's
* getTimezoneOffset() (minutes local time is BEHIND UTC; UTC when missing).
* Buckets are whole UTC hours, so a half-hour zone's day starts up to 30 min early.
*/
function todayHour(offset) {
const off = Number.isFinite(offset) && Math.abs(offset) <= 840 ? offset : 0;
const local = new Date(Date.now() - off * 60e3);
local.setUTCHours(0, 0, 0, 0);
return new Date(local.getTime() + off * 60e3).toISOString().slice(0, 13);
}
/** Map(key → n) from rows of `{k, n}`. */
const counts = (rows) => new Map(rows.map((r) => [r.k, r.n]));
router.get("/", (_req, res) => {
router.get("/", (req, res) => {
const rules = stmts.listPricing.all();
// A fixed number of grouped queries, however many accounts there are.
const logins = auth.allLoginStats();
@@ -112,6 +125,7 @@ router.get("/", (_req, res) => {
const sessions = counts(
db.prepare(`SELECT uid AS k, COUNT(*) AS n FROM (${OWNED_SESSIONS}) GROUP BY uid`).all()
);
const today = usageSince(todayHour(parseInt(req.query.tz_offset, 10)), rules);
const last7d = usageSince(hourAgo(7), rules);
const last30d = usageSince(hourAgo(30), rules);
const none = { cost: 0, tokens: 0 };
@@ -120,6 +134,7 @@ router.get("/", (_req, res) => {
...(logins.get(u.id) || { logins: 0, lastLoginAt: null }),
workspaces: workspaces.get(u.id) || 0,
sessions: sessions.get(u.id) || 0,
today: today.get(u.id) || none,
last7d: last7d.get(u.id) || none,
last30d: last30d.get(u.id) || none,
}));