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
@@ -0,0 +1,13 @@
import Span from "@moduletrace/twui/layout/Span";
import { Monitor } from "lucide-react";
/**
* A computer's avatar in lists: the monitor icon on a small tile.
*/
export default function ComputerIconTile() {
return (
<Span className="size-8 shrink-0 flex items-center justify-center rounded-default-sm border border-solid border-hairline bg-paper">
<Monitor size={16} aria-hidden />
</Span>
);
}
+23 -5
View File
@@ -1,3 +1,6 @@
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
import userInitials from "@/src/utils/user-initials";
import Img from "@moduletrace/twui/layout/Img";
import Span from "@moduletrace/twui/layout/Span";
import { twMerge } from "tailwind-merge";
@@ -12,28 +15,43 @@ const UserAvatarSizeClasses = {
} as const;
type Props = {
/**
* Their photo and initials come from this user (`grabUsers`' row, with
* `profile_media_id`), unless `src` or `initials` are given.
*/
person?: BUN_SQLITE_COBBLE_USERS_JOIN | null;
/**
* A photo to show instead, e.g. one just picked and not saved yet.
*/
src?: string;
/**
* Tried when `src` fails to load, e.g. a full image behind a thumbnail.
*/
img_err_src?: string;
initials: string;
initials?: string;
alt?: string;
size?: keyof typeof UserAvatarSizeClasses;
className?: string;
};
/**
* A user's round photo, or their initials on sunken paper when there is none.
* A user's round photo (its thumbnail, the full image if that fails), or
* their initials on sunken paper when there is none.
*/
export default function UserAvatar({
src,
img_err_src,
initials,
person,
src: passed_src,
img_err_src: passed_err_src,
initials: passed_initials,
alt = "",
size = "md",
className,
}: Props) {
const photo = person?.profile_media_id ? grabMediaURL({ user: person }) : {};
const src = passed_src || photo.media_thumbnail_url || photo.media_url;
const img_err_src = passed_src ? passed_err_src : photo.media_url;
const initials = passed_initials || userInitials(person || {});
const base_class = twMerge(
"shrink-0 rounded-full bg-sunken",
UserAvatarSizeClasses[size],