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
@@ -1,5 +1,5 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_CONTAINERS } from "@/db/types/db";
import type { BUN_SQLITE_COBBLE_COMPUTERS, BUN_SQLITE_COBBLE_CONTAINERS } from "@/db/types/db";
import parseJobJson from "@/src/bots-queue/parse-job-json";
import { AppData } from "@/src/data/app-data";
import createComputerContainer from "@/src/functions/backend/docker/create-computer-container";
@@ -17,15 +17,18 @@ type Params = {
/**
* Sets up a computer's container: prepares its home directory, pulls the
* sandbox image and starts the container, then records it (`containers`:
* docker id, state, and where to reach it outside docker). A failure is
* sandbox image and starts the container with the computer's resources
* (`cpus`, `memory_mb`, else the `AppData` defaults), then records it (`containers`:
* docker id, state, and its IP address on the network). A failure is
* recorded as `failed` with its reason, which the computer lists show. The
* owner's tabs hear about it either way. Never throws.
*/
export default async function provisionComputer({ computer_id, fresh }: Params) {
const row = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_CONTAINERS>({
sql: `SELECT co.* FROM containers co JOIN computers c ON c.container_id = co.id WHERE c.id = ? LIMIT 1`,
await BunSQLite.sql<
BUN_SQLITE_COBBLE_CONTAINERS & Pick<BUN_SQLITE_COBBLE_COMPUTERS, "cpus" | "memory_mb">
>({
sql: `SELECT co.*, c.cpus, c.memory_mb FROM containers co JOIN computers c ON c.container_id = co.id WHERE c.id = ? LIMIT 1`,
values: [computer_id],
})
).payload?.[0];
@@ -41,26 +44,28 @@ export default async function provisionComputer({ computer_id, fresh }: Params)
});
const data_dir = await prepareComputerDataDir({ computer_id, fresh });
const created = await createComputerContainer({ computer_id, name: row.name, data_dir });
const created = await createComputerContainer({
computer_id,
name: row.name,
data_dir,
cpus: Number(row.cpus) || AppData.ComputerDefaultCpus,
memory_mb: Number(row.memory_mb) || AppData.ComputerDefaultMemoryMb,
});
const { error: _error, mcp_url: _mcp_url, ...kept } = metadata;
await BunSQLite.sql({
sql: `UPDATE containers SET docker_id = ?, image = ?, network = ?, hostname = ?, port = ?,
status = ?, metadata = ?, last_seen_at = ?, updated_at = ? WHERE id = ?`,
sql: `UPDATE containers SET docker_id = ?, image = ?, network = ?, hostname = ?, ip_address = ?,
port = ?, status = ?, metadata = ?, last_seen_at = ?, updated_at = ? WHERE id = ?`,
values: [
created.docker_id,
AppData.ComputerImage,
created.network,
row.name,
created.ip_address,
AppData.ComputerPort,
created.status,
JSON.stringify({
...kept,
data_dir,
host_dir: created.host_dir,
...(created.mcp_url ? { mcp_url: created.mcp_url } : {}),
}),
JSON.stringify({ ...kept, data_dir, host_dir: created.host_dir }),
Date.now(),
Date.now(),
row.id,