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
+25 -1
View File
@@ -10,7 +10,7 @@
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import type { AuthState, ServerUserUsage } from "../../lib/api";
@@ -34,6 +34,7 @@ const row = (id: number, username: string, role: ServerUserUsage["role"]): Serve
const users = vi.hoisted(() => ({
list: vi.fn(),
audit: vi.fn(),
storage: vi.fn(),
create: vi.fn(),
update: vi.fn(),
setPassword: vi.fn(),
@@ -74,6 +75,7 @@ beforeEach(() => {
users: [row(1, "boss", "admin"), row(2, "mia", "developer")],
});
users.audit.mockResolvedValue({ entries: [] });
users.storage.mockResolvedValue({ storage: {}, measuring: false });
users.get.mockImplementation(async (id: number) => ({
user: { id, username: "x", role: "developer", disabled: false },
workspaces: [],
@@ -91,6 +93,28 @@ describe("Users page", () => {
expect(users.list).not.toHaveBeenCalled();
});
it("shows each user's home size, and re-measures on Refresh", async () => {
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
users.storage.mockResolvedValue({
storage: {
1: null,
2: {
bytes: 3 * 1024 ** 3,
files: 1200,
partial: false,
measuredAt: "2026-10-14T09:00:00Z",
},
},
measuring: false,
});
renderPage();
expect(await screen.findByText("3.0 GB")).toBeTruthy();
expect(screen.getByText("Storage")).toBeTruthy();
expect(users.storage).toHaveBeenLastCalledWith(false);
fireEvent.click(screen.getByRole("button", { name: /Refresh/ }));
await waitFor(() => expect(users.storage).toHaveBeenLastCalledWith(true));
});
it("shows an analyst the accounts with no controls", async () => {
authState = { mode: "server", user: { id: 3, username: "ana", role: "analyst" } };
renderPage();