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:
@@ -20,8 +20,11 @@ export const MainComputerName = "Main computer";
|
||||
|
||||
/**
|
||||
* Docker container state as reported by `docker inspect` (`State.Status`),
|
||||
* plus `creating` while the app sets it up, `failed` when that didn't
|
||||
* work, and `unknown` for a record that hasn't been inspected yet.
|
||||
* with `running` narrowed by its health check: `starting` until it first
|
||||
* passes, `unhealthy` once it keeps failing. Plus `creating` while the app
|
||||
* sets it up (until the health check passes), `failed` when that didn't
|
||||
* work, and `unknown` for a record that hasn't been inspected yet. Only a
|
||||
* `running` computer is reached.
|
||||
*/
|
||||
export const ContainerStatuses = [
|
||||
{
|
||||
@@ -36,10 +39,18 @@ export const ContainerStatuses = [
|
||||
title: "Created",
|
||||
value: "created",
|
||||
},
|
||||
{
|
||||
title: "Starting",
|
||||
value: "starting",
|
||||
},
|
||||
{
|
||||
title: "Running",
|
||||
value: "running",
|
||||
},
|
||||
{
|
||||
title: "Unhealthy",
|
||||
value: "unhealthy",
|
||||
},
|
||||
{
|
||||
title: "Paused",
|
||||
value: "paused",
|
||||
|
||||
@@ -70,6 +70,7 @@ export const ProcessOnlyEnvKeys = [
|
||||
"ENCRYPTION_SALT",
|
||||
"PORT",
|
||||
"DATA_DIR",
|
||||
"DB_DIR",
|
||||
"MAIN_SERVER",
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import {
|
||||
Bot,
|
||||
CpuAi,
|
||||
KeyRound,
|
||||
Layers,
|
||||
Monitor,
|
||||
Users,
|
||||
SlidersHorizontal,
|
||||
UserRound,
|
||||
Variable,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import canListUsers from "@/src/utils/rbac/can-list-users";
|
||||
|
||||
export type SettingsPage = {
|
||||
title: string;
|
||||
@@ -15,8 +21,14 @@ export type SettingsPage = {
|
||||
* Shown and served to super admins only.
|
||||
*/
|
||||
super_admin_only?: boolean;
|
||||
/**
|
||||
* Who sees it, when that's an RBAC rule rather than super admins only.
|
||||
*/
|
||||
can_view?: (params: { user_types?: BUN_SQLITE_COBBLE_USER_TYPES[] | null }) => boolean;
|
||||
};
|
||||
|
||||
export const SettingsComputersPath = "/admin/settings/computers";
|
||||
|
||||
/**
|
||||
* The `/admin/settings/*` sub pages, listed in the rail on every settings
|
||||
* page. The first one is where `/admin/settings` lands.
|
||||
@@ -33,6 +45,15 @@ export const SettingsPages: SettingsPage[] = [
|
||||
url: "/admin/settings/change-password",
|
||||
Icon: KeyRound,
|
||||
},
|
||||
{ title: "Bots", url: "/admin/settings/bots", Icon: Bot },
|
||||
{ title: "Workspaces", url: "/admin/settings/workspaces", Icon: Layers },
|
||||
{ title: "Computers", url: SettingsComputersPath, Icon: Monitor },
|
||||
{
|
||||
title: "Users",
|
||||
url: "/admin/settings/users",
|
||||
Icon: Users,
|
||||
can_view: canListUsers,
|
||||
},
|
||||
{
|
||||
title: "Models",
|
||||
url: "/admin/settings/models",
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("UserTypes dict", () => {
|
||||
const required = [
|
||||
"super_admin",
|
||||
"admin",
|
||||
"member",
|
||||
"revoked",
|
||||
"read_only",
|
||||
] as const;
|
||||
@@ -48,28 +49,18 @@ describe("Assignment matrices", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("elevated types are pass-through only", () => {
|
||||
const elevated = [
|
||||
"admin",
|
||||
"president",
|
||||
"vice_president",
|
||||
"secretary",
|
||||
] as const;
|
||||
for (const type of elevated) {
|
||||
const entry = UserTypeAssignmentMatrix[type];
|
||||
expect(Array.isArray(entry.allowed_actors)).toBe(true);
|
||||
expect((entry.allowed_actors as string[]).length).toBe(0);
|
||||
test("admin is granted by pass-through actors only", () => {
|
||||
const entry = UserTypeAssignmentMatrix["admin"];
|
||||
expect(Array.isArray(entry.allowed_actors)).toBe(true);
|
||||
expect((entry.allowed_actors as string[]).length).toBe(0);
|
||||
});
|
||||
|
||||
test("admin manages member types, never on admins or the super admin", () => {
|
||||
for (const type of ["member", "read_only", "revoked"] as const) {
|
||||
for (const matrix of [UserTypeAssignmentMatrix, UserTypeDeletionMatrix]) {
|
||||
const rules = (matrix[type].allowed_actors as Record<string, { excluded_targets?: string[] }>).admin;
|
||||
expect(rules?.excluded_targets).toEqual(["super_admin", "admin"]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("admin can manage auditor but not another admin", () => {
|
||||
expect(
|
||||
UserTypeAssignmentMatrix["auditor"].allowed_actors,
|
||||
).toContain("admin");
|
||||
|
||||
expect(
|
||||
(UserTypeAssignmentMatrix["admin"].allowed_actors as string[])
|
||||
.length,
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+33
-53
@@ -1,29 +1,33 @@
|
||||
import type { UserType, UserTypeAssignmentMatrixType } from "../types";
|
||||
|
||||
/**
|
||||
* Every user type on this install. A user needs at least one to sign in.
|
||||
*/
|
||||
export const UserTypes = [
|
||||
{
|
||||
value: "super_admin",
|
||||
title: "Super admin",
|
||||
description: "Runs the install: models, environment, computers and users. The first account.",
|
||||
},
|
||||
{
|
||||
value: "admin",
|
||||
title: "Admin",
|
||||
description: "Adds and manages members.",
|
||||
},
|
||||
{
|
||||
value: "auditor",
|
||||
},
|
||||
{
|
||||
value: "president",
|
||||
},
|
||||
{
|
||||
value: "vice_president",
|
||||
},
|
||||
{
|
||||
value: "secretary",
|
||||
},
|
||||
{
|
||||
value: "revoked",
|
||||
value: "member",
|
||||
title: "Member",
|
||||
description: "Has their own crew, workspaces and main computer.",
|
||||
},
|
||||
{
|
||||
value: "read_only",
|
||||
title: "Read only",
|
||||
description: "Can look around, but can't change anything.",
|
||||
},
|
||||
{
|
||||
value: "revoked",
|
||||
title: "Revoked",
|
||||
description: "Can't sign in. Their crew and files are kept.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -58,36 +62,24 @@ export const UserTypeAssignmentMatrix: UserTypeAssignmentMatrixType = {
|
||||
admin: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
president: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
vice_president: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
secretary: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
auditor: {
|
||||
allowed_actors: ["admin"],
|
||||
member: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
excluded_targets: ["super_admin", "admin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
read_only: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
excluded_targets: ["super_admin"],
|
||||
excluded_targets: ["super_admin", "admin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
revoked: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
allowed_targets: ["read_only"],
|
||||
excluded_targets: [
|
||||
"super_admin",
|
||||
"admin",
|
||||
"president",
|
||||
"vice_president",
|
||||
"secretary",
|
||||
],
|
||||
excluded_targets: ["super_admin", "admin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -98,36 +90,24 @@ export const UserTypeDeletionMatrix: UserTypeAssignmentMatrixType = {
|
||||
admin: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
president: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
vice_president: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
secretary: {
|
||||
allowed_actors: [],
|
||||
},
|
||||
auditor: {
|
||||
allowed_actors: ["admin"],
|
||||
member: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
excluded_targets: ["super_admin", "admin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
read_only: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
excluded_targets: ["super_admin"],
|
||||
excluded_targets: ["super_admin", "admin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
revoked: {
|
||||
allowed_actors: {
|
||||
admin: {
|
||||
allowed_targets: ["read_only"],
|
||||
excluded_targets: [
|
||||
"super_admin",
|
||||
"admin",
|
||||
"president",
|
||||
"vice_president",
|
||||
"secretary",
|
||||
],
|
||||
excluded_targets: ["super_admin", "admin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user