feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars

- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 17:21:33 +07:00
parent 8ed5018a2f
commit 7a88b74016
63 changed files with 3833 additions and 354 deletions
+7 -24
View File
@@ -155,29 +155,12 @@ function livePanes(account) {
}
/**
* $ of the Codex sessions this account's runs started since `hours` ago.
* $ and tokens of the last 5 h / 7 d (pool-usage.js).
* ponytail: Codex usage has no hourly table, so a session counts whole once
* its run started in the window; add hourly rows if that skews a ranking.
*/
function spendSince(account, hours) {
const { db, stmts } = require("../db");
const { calculateCost } = require("../routes/pricing");
const since = new Date(Date.now() - hours * 3600e3).toISOString();
const rows = db
.prepare(
`SELECT model, speed, inference_geo, service_tier,
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
SUM(cache_read_tokens) AS cache_read_tokens,
SUM(cache_write_tokens) AS cache_write_tokens
FROM token_usage
WHERE session_id IN (SELECT session_id FROM dashboard_runs
WHERE tool = 'codex' AND account = ?
AND session_id IS NOT NULL AND started_at >= ?)
GROUP BY model, speed, inference_geo, service_tier`
)
.all(account, since);
return rows.length ? calculateCost(rows, stmts.listPricing.all()).total_cost : 0;
}
const usageOf = (account) =>
require("./pool-usage").accountUsage("codex", account, { wholeSession: true });
// ── Quota, from the account's own rollouts ─────────────────────────────────
@@ -262,8 +245,7 @@ function poolStatus() {
kind: a.kind,
addedAt: a.addedAt,
livePanes: livePanes(a.name),
spend5h: spendSince(a.name, 5),
spend7d: spendSince(a.name, 24 * 7),
...usageOf(a.name),
quota: quotas().get(a.name) || null,
quotaPercent: percent,
full: percent != null && percent >= BLOCK_PERCENT,
@@ -374,13 +356,14 @@ function pick(member) {
const ranked = usable
.map((a) => {
const percent = quotaPercent(a.name);
const u = usageOf(a.name);
return {
a,
key: [
percent != null && percent >= BLOCK_PERCENT ? 1 : 0,
livePanes(a.name),
spendSince(a.name, 5),
spendSince(a.name, 24 * 7),
u.spend5h,
u.spend7d,
],
};
})