Let members view a workspace's dashboard, editing still for managers only
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -85,7 +85,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
|
|||||||
|
|
||||||
### Bots (the chat shell)
|
### Bots (the chat shell)
|
||||||
|
|
||||||
- **Access (the crew is shared):** bots, workspaces and computers belong to the install, not to a user (their `user_id` is only who created them, not a foreign key). Nobody reaches a bot until a super admin or admin grants it: `bot_users` (a worker assigned to a user, for its own chat) and `workspace_users` (a user added to a workspace, to chat with every worker in it there). `crewRole` (`src/utils/rbac/crew-role.ts`): `super_admin` reaches everything and is the only one who ever sees, chats with or edits the coordinator; `admin` reaches every worker and workspace; anyone else (`member`) only their grants. `canManageCrew` (super admins and admins) creates, edits and deletes workers and workspaces, sets their models and pictures, and hands out access (the Bots and Workspaces groups on a user's settings page, `user-access-groups.tsx`, each added or taken away at once by `api/admin/users/add-access` and `remove-access` through `setUserGrant`); the structural DB helpers check it themselves (`assertCrewManager`, `src/functions/backend/db/access/`). The SQL conditions live in `src/utils/crew-access/` (`chatBotsSql`, `workspacesSql`, `workspaceBotsSql`, `computersSql`, tested against SQLite) and back `grabChatBot` (its own chat), `grabUserBot` (anywhere: its own chat or a workspace), `grabUserWorkspace(s)`, `grabWorkspaceBot`, `grabBots` and `grabUserComputer(s)`. Conversations are per user (`bot_sessions.user_id`): a job runs for the user whose session it's in (`runBotJob` reads it), with that user's memory and only what they reach. A bot's face and status line are per user too (`bot_user_faces`, set by `setBotFace` for the job's user, `grabJobUserId`; no row is idle), so needs-you or working only shows to the user the job is for; every bot read for a page overlays the viewer's (`botFaceSql`, `src/utils/bot-face-sql.ts`). Live updates go to the session's user (`pushToSessionUser`), a face change to its user, and a bot change (its computer) to everyone who sees it (`pushBotUpdated`, `grabBotAudience`, each with their own face and active task); its status line never names a task (`BotTaskStatusLine`). Bot and workspace pictures are served to whoever sees them (`grabSharedPictureMedia`). A member with nothing granted lands on an empty `/admin/bots`, and on a workspace's page is sent to its first bot. `src/scripts/share-crew.ts` (safe to rerun) moved an install from one crew per user to this.
|
- **Access (the crew is shared):** bots, workspaces and computers belong to the install, not to a user (their `user_id` is only who created them, not a foreign key). Nobody reaches a bot until a super admin or admin grants it: `bot_users` (a worker assigned to a user, for its own chat) and `workspace_users` (a user added to a workspace, to chat with every worker in it there). `crewRole` (`src/utils/rbac/crew-role.ts`): `super_admin` reaches everything and is the only one who ever sees, chats with or edits the coordinator; `admin` reaches every worker and workspace; anyone else (`member`) only their grants. `canManageCrew` (super admins and admins) creates, edits and deletes workers and workspaces, sets their models and pictures, and hands out access (the Bots and Workspaces groups on a user's settings page, `user-access-groups.tsx`, each added or taken away at once by `api/admin/users/add-access` and `remove-access` through `setUserGrant`); the structural DB helpers check it themselves (`assertCrewManager`, `src/functions/backend/db/access/`). The SQL conditions live in `src/utils/crew-access/` (`chatBotsSql`, `workspacesSql`, `workspaceBotsSql`, `computersSql`, tested against SQLite) and back `grabChatBot` (its own chat), `grabUserBot` (anywhere: its own chat or a workspace), `grabUserWorkspace(s)`, `grabWorkspaceBot`, `grabBots` and `grabUserComputer(s)`. Conversations are per user (`bot_sessions.user_id`): a job runs for the user whose session it's in (`runBotJob` reads it), with that user's memory and only what they reach. A bot's face and status line are per user too (`bot_user_faces`, set by `setBotFace` for the job's user, `grabJobUserId`; no row is idle), so needs-you or working only shows to the user the job is for; every bot read for a page overlays the viewer's (`botFaceSql`, `src/utils/bot-face-sql.ts`). Live updates go to the session's user (`pushToSessionUser`), a face change to its user, and a bot change (its computer) to everyone who sees it (`pushBotUpdated`, `grabBotAudience`, each with their own face and active task); its status line never names a task (`BotTaskStatusLine`). Bot and workspace pictures are served to whoever sees them (`grabSharedPictureMedia`). A member with nothing granted lands on an empty `/admin/bots`; a workspace's dashboard is open to everyone in it, its settings link and computers' settings links only to `canManageCrew`. `src/scripts/share-crew.ts` (safe to rerun) moved an install from one crew per user to this.
|
||||||
- **Tables:** `bots` (`kind` worker|coordinator, `hue`, `role`, `system_prompt` (the bot's core instructions, markdown)), `bot_user_faces` (`face`, `status_line` per bot and user), `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps`. Enums in `src/dict/bot-dict.ts`.
|
- **Tables:** `bots` (`kind` worker|coordinator, `hue`, `role`, `system_prompt` (the bot's core instructions, markdown)), `bot_user_faces` (`face`, `status_line` per bot and user), `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps`. Enums in `src/dict/bot-dict.ts`.
|
||||||
- **Threads and sessions:** each user has one conversation with a bot in its own chat and one per workspace it's in (the coordinator in every workspace). Each is split into `bot_sessions` (`user_id`, `bot_id`, `workspace_id` null for its own chat, `status` active|closed, `title` from the first user message, `summary`, `previous_session_id`, `unread`, `last_activity_at`); `bot_messages`, `bot_tasks` and `bots_queue` carry `session_id` and `workspace_id`. Helpers in `src/functions/backend/db/bot-sessions/`: `ensureBotSession` (the active one, else a new one; `rollover` starts the next once it has `AppData.BotSessionMaxMessages`), `startBotSession`, `reopenBotSession`, `grabBotSessions` (search over title, summary and message text), `grabUserConversation` (access and membership), `grabConversationProps` (the conversation pages' props). A new session's first reply gets the previous one's summary (`grab-session-carry-over.ts`, written once with the text harness). The header's sessions icon (`(partials)/bot-sessions-modal.tsx`) lists, searches, starts and reopens sessions, and the new-session icon beside it (`(partials)/new-session-button.tsx`) starts one (`api/admin/bots/list-sessions`, `new-session`, `reopen-session`). Unread, the rail's last message and activity are per thread, from its sessions. Job results go into the job's own session, even after it closes.
|
- **Threads and sessions:** each user has one conversation with a bot in its own chat and one per workspace it's in (the coordinator in every workspace). Each is split into `bot_sessions` (`user_id`, `bot_id`, `workspace_id` null for its own chat, `status` active|closed, `title` from the first user message, `summary`, `previous_session_id`, `unread`, `last_activity_at`); `bot_messages`, `bot_tasks` and `bots_queue` carry `session_id` and `workspace_id`. Helpers in `src/functions/backend/db/bot-sessions/`: `ensureBotSession` (the active one, else a new one; `rollover` starts the next once it has `AppData.BotSessionMaxMessages`), `startBotSession`, `reopenBotSession`, `grabBotSessions` (search over title, summary and message text), `grabUserConversation` (access and membership), `grabConversationProps` (the conversation pages' props). A new session's first reply gets the previous one's summary (`grab-session-carry-over.ts`, written once with the text harness). The header's sessions icon (`(partials)/bot-sessions-modal.tsx`) lists, searches, starts and reopens sessions, and the new-session icon beside it (`(partials)/new-session-button.tsx`) starts one (`api/admin/bots/list-sessions`, `new-session`, `reopen-session`). Unread, the rail's last message and activity are per thread, from its sessions. Job results go into the job's own session, even after it closes.
|
||||||
- **Coordinator:** the install has exactly one `kind: coordinator` bot, the super admin's alone, that coordinates the crew and delegates to every worker. It is created at signup (`bun run seed`) and, as a fallback, by the super admin's `/admin/bots`, all via `ensureCoordinatorBot` (`src/functions/backend/db/bots/ensure-coordinator-bot.ts`, idempotent). Name in `CoordinatorBotName` (`src/dict/bot-dict.ts`). Its default prompt is `CoordinatorSystemPrompt` (`src/data/coordinator-system-prompt.ts`): attached at creation, and backfilled onto a coordinator whose prompt is empty, never over an edited one.
|
- **Coordinator:** the install has exactly one `kind: coordinator` bot, the super admin's alone, that coordinates the crew and delegates to every worker. It is created at signup (`bun run seed`) and, as a fallback, by the super admin's `/admin/bots`, all via `ensureCoordinatorBot` (`src/functions/backend/db/bots/ensure-coordinator-bot.ts`, idempotent). Name in `CoordinatorBotName` (`src/dict/bot-dict.ts`). Its default prompt is `CoordinatorSystemPrompt` (`src/data/coordinator-system-prompt.ts`): attached at creation, and backfilled onto a coordinator whose prompt is empty, never over an edited one.
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ type Props = {
|
|||||||
* What can be done with it, after the open button.
|
* What can be done with it, after the open button.
|
||||||
*/
|
*/
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Link the title to its settings page. Off for people who can't open it.
|
||||||
|
*/
|
||||||
|
linked?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,14 +28,14 @@ type Props = {
|
|||||||
* settings page from the title. Every list uses it, so the row behaves the
|
* settings page from the title. Every list uses it, so the row behaves the
|
||||||
* same, and the open button shows it in the viewer over the page.
|
* same, and the open button shows it in the viewer over the page.
|
||||||
*/
|
*/
|
||||||
export default function ComputerListRow({ computer, badge, subtitle, children }: Props) {
|
export default function ComputerListRow({ computer, badge, subtitle, children, linked = true }: Props) {
|
||||||
const name = computer.name || `Computer ${computer.id}`;
|
const name = computer.name || `Computer ${computer.id}`;
|
||||||
const ready = computer.status == "running";
|
const ready = computer.status == "running";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsListRow
|
<SettingsListRow
|
||||||
avatar={<ComputerIconTile />}
|
avatar={<ComputerIconTile />}
|
||||||
href={`/admin/settings/computers/${computer.id}`}
|
href={linked ? `/admin/settings/computers/${computer.id}` : undefined}
|
||||||
title={name}
|
title={name}
|
||||||
badge={badge}
|
badge={badge}
|
||||||
subtitle={
|
subtitle={
|
||||||
|
|||||||
+6
-1
@@ -8,13 +8,17 @@ type Props = {
|
|||||||
* The computers linked to the workspace, its default first.
|
* The computers linked to the workspace, its default first.
|
||||||
*/
|
*/
|
||||||
computers: ComputerWithStatus[];
|
computers: ComputerWithStatus[];
|
||||||
|
/**
|
||||||
|
* Whether the viewer can open each computer's settings page.
|
||||||
|
*/
|
||||||
|
linked: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The computers bots work on here, with their state. Display only:
|
* The computers bots work on here, with their state. Display only:
|
||||||
* computers are linked and set up under Settings.
|
* computers are linked and set up under Settings.
|
||||||
*/
|
*/
|
||||||
export default function WorkspaceComputersListGroup({ computers }: Props) {
|
export default function WorkspaceComputersListGroup({ computers, linked }: Props) {
|
||||||
return (
|
return (
|
||||||
<SettingsGroup
|
<SettingsGroup
|
||||||
id="computers"
|
id="computers"
|
||||||
@@ -30,6 +34,7 @@ export default function WorkspaceComputersListGroup({ computers }: Props) {
|
|||||||
key={computer.id}
|
key={computer.id}
|
||||||
computer={computer}
|
computer={computer}
|
||||||
is_default={index == 0}
|
is_default={index == 0}
|
||||||
|
linked={linked}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
|
|||||||
+7
-3
@@ -4,12 +4,16 @@ import type { ComputerWithStatus } from "@/src/types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
computer: ComputerWithStatus;
|
computer: ComputerWithStatus;
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether the viewer can open its settings page.
|
||||||
|
*/
|
||||||
|
linked: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A computer in the workspace dashboard: its state, resources and what
|
* A computer in the workspace dashboard: its state, resources and what
|
||||||
* it's for, opening its settings page.
|
* it's for, opening its settings page for those who manage the crew.
|
||||||
*/
|
*/
|
||||||
export default function WorkspaceDashboardComputerRow({ computer, is_default }: Props) {
|
export default function WorkspaceDashboardComputerRow({ computer, is_default, linked }: Props) {
|
||||||
return <ComputerListRow computer={computer} badge={is_default ? "Default" : undefined} />;
|
return <ComputerListRow computer={computer} badge={is_default ? "Default" : undefined} linked={linked} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default function WorkspaceDashboard() {
|
|||||||
/>
|
/>
|
||||||
<WorkspaceComputersListGroup
|
<WorkspaceComputersListGroup
|
||||||
computers={pageProps?.workspace_computers || []}
|
computers={pageProps?.workspace_computers || []}
|
||||||
|
linked={can_manage}
|
||||||
/>
|
/>
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
|||||||
redirect: {
|
redirect: {
|
||||||
destination: canManageCrew({ user_types })
|
destination: canManageCrew({ user_types })
|
||||||
? `/admin/settings/workspaces/${query.workspace_id}`
|
? `/admin/settings/workspaces/${query.workspace_id}`
|
||||||
: "/admin/bots",
|
: `/admin/workspaces/${query.workspace_id}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ import grabWorkspaceActivity from "@/src/functions/backend/db/workspaces/grab-wo
|
|||||||
import grabWorkspaceBots from "@/src/functions/backend/db/workspaces/grab-workspace-bots";
|
import grabWorkspaceBots from "@/src/functions/backend/db/workspaces/grab-workspace-bots";
|
||||||
import grabWorkspaceComputers from "@/src/functions/backend/db/workspaces/grab-workspace-computers";
|
import grabWorkspaceComputers from "@/src/functions/backend/db/workspaces/grab-workspace-computers";
|
||||||
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
||||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
|
||||||
import type { PagePropsType, PageQueryObject } from "@/src/types";
|
import type { PagePropsType, PageQueryObject } from "@/src/types";
|
||||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workspace's dashboard: its picture, context, latest activity, bots and
|
* A workspace's dashboard: its picture, context, latest activity, bots and
|
||||||
* computers, all read-only. Editing lives under Settings. Only super admins
|
* computers, all read-only. Everyone who reaches the workspace sees it; editing lives
|
||||||
* and admins see it; a member opens its first bot instead.
|
* under Settings, for super admins and admins only.
|
||||||
*/
|
*/
|
||||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||||
const query: PageQueryObject = params.query;
|
const query: PageQueryObject = params.query;
|
||||||
const { user, user_types } = await userAuth({ req: params.req });
|
const { user } = await userAuth({ req: params.req });
|
||||||
|
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
return { redirect: { destination: "/auth/login" } };
|
return { redirect: { destination: "/auth/login" } };
|
||||||
@@ -32,33 +31,6 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
|||||||
|
|
||||||
const workspace_id = Number(workspace.id);
|
const workspace_id = Number(workspace.id);
|
||||||
|
|
||||||
/**
|
|
||||||
* Members don't manage it: they go straight to its first bot.
|
|
||||||
*/
|
|
||||||
if (!canManageCrew({ user_types })) {
|
|
||||||
const workspace_bots = await grabWorkspaceBots({
|
|
||||||
user_id: user.id,
|
|
||||||
workspace_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (workspace_bots[0]?.id) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/admin/workspaces/${workspace_id}/bots/${workspace_bots[0].id}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
workspaces,
|
|
||||||
workspace,
|
|
||||||
bots: workspace_bots,
|
|
||||||
workspace_bots,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const [workspace_bots, workspace_computers, workspace_activity] =
|
const [workspace_bots, workspace_computers, workspace_activity] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
grabWorkspaceBots({ user_id: user.id, workspace_id }),
|
grabWorkspaceBots({ user_id: user.id, workspace_id }),
|
||||||
|
|||||||
Reference in New Issue
Block a user