Run computers with resources and health checks, add settings for bots, workspaces, computers and users

- Computers get CPUs and memory (default 1 CPU, 2 GB), set at creation and
  editable; new resources stop, remove and recreate the container, its home
  dir kept. Every computer container has a docker health check and stays
  "creating" until it passes; only healthy computers are reached, by
  hostname, else by their IP on the network (dev outside docker).
- Docker is required: the app creates the cobble network on startup and
  exits without docker.
- Production container: deploy/run-app-container.sh mounts .env, the data
  dir and the docker socket; the database (DB_DIR), media, computers' homes
  and bun-sqlite's live schema now live on the data volume.
- Settings pages for bots, workspaces, computers and users, sharing list
  components; bots can be deleted; users are created with a password and
  confirmation, edited and deleted through RBAC-gated APIs.
- User types trimmed to super_admin, admin, member, read_only and revoked;
  admins manage members. UserAvatar shows profile photos from the user row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 10:26:44 +01:00
co-authored by Claude Opus 5.5
parent 0ce61cdb49
commit f24c0d7e82
91 changed files with 2692 additions and 331 deletions
+12 -17
View File
@@ -8,10 +8,10 @@ import {
filterAssignableUserTypes,
isPassThroughUser,
} from "./";
import {
ALL_USER_TYPES,
makeUserTypes,
} from "./user-type-assignment.test-fixtures";
import { UserTypes } from "@/src/dict/user-types-dict";
import { makeUserTypes } from "./user-type-assignment.test-fixtures";
const ALL_USER_TYPES = UserTypes.map((type) => type.value);
describe("isPassThroughUser", () => {
test("super_admin is pass-through", () => {
@@ -19,7 +19,7 @@ describe("isPassThroughUser", () => {
});
test("regular types are not pass-through", () => {
for (const type of ["admin", "president", "secretary"]) {
for (const type of ["admin", "member", "read_only"]) {
expect(
isPassThroughUser({ user_types: makeUserTypes(type) }),
).toBe(false);
@@ -45,7 +45,7 @@ describe("canListUsers / canCreateUser / canManageUserTypes", () => {
});
test("non-admin types fail", () => {
for (const type of ["auditor", "president", "secretary", "read_only"]) {
for (const type of ["member", "read_only", "revoked"]) {
expect(
canListUsers({ user_types: makeUserTypes(type) }),
).toBe(false);
@@ -63,7 +63,7 @@ describe("canUpdateUser", () => {
test("actors may update their own record", () => {
expect(
canUpdateUser({
actor_user_types: makeUserTypes("auditor"),
actor_user_types: makeUserTypes("member"),
actor_user_id: 5,
target_user_id: 5,
}),
@@ -108,7 +108,7 @@ describe("canUpdateUser", () => {
test("non-admin actors cannot update others", () => {
expect(
canUpdateUser({
actor_user_types: makeUserTypes("auditor"),
actor_user_types: makeUserTypes("member"),
actor_user_id: 1,
target_user_id: 2,
update_fields: ["first_name"],
@@ -155,7 +155,7 @@ describe("canRevokeUser", () => {
expect(
canRevokeUser({
user_types: makeUserTypes("admin"),
target_user_types: makeUserTypes("president"),
target_user_types: makeUserTypes("super_admin"),
}),
).toBe(false);
});
@@ -175,17 +175,12 @@ describe("filterAssignableUserTypes", () => {
actor_user_types: makeUserTypes("admin"),
user_types: ALL_USER_TYPES,
});
expect(assignable).toContain("auditor");
expect(assignable).toContain("read_only");
expect(assignable).not.toContain("admin");
expect(assignable).not.toContain("super_admin");
expect(assignable).not.toContain("president");
expect(assignable).not.toContain("secretary");
expect(assignable).toEqual(["member", "read_only", "revoked"]);
});
test("secretary can assign nothing", () => {
test("a member can assign nothing", () => {
const assignable = filterAssignableUserTypes({
actor_user_types: makeUserTypes("secretary"),
actor_user_types: makeUserTypes("member"),
user_types: ALL_USER_TYPES,
});
expect(assignable).toHaveLength(0);