From 9293a5503285927a10e8c1f9238965e595aeb1f5 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Mon, 19 Oct 2026 17:43:07 +0100 Subject: [PATCH] Remove the closed people pages, keep their user form in settings --- AGENTS.md | 7 +- src/dict/admin-sections-dict.ts | 3 +- .../logs/(partials)/log-details-modal.tsx | 2 +- .../(hooks)/use-all-people-list-init.tsx | 73 ----------- .../(partials)/all-people-list-search.tsx | 114 ------------------ .../(partials)/all-people-list-selections.tsx | 29 ----- .../all-people-list-title-section.tsx | 38 ------ .../all-people-list-users-type-select.tsx | 39 ------ .../people/(partials)/people-list-card.tsx | 104 ---------------- .../admin/people/(partials)/people-list.tsx | 40 ------ src/pages/admin/people/(sections)/hero.tsx | 28 ----- .../people/(sections)/people-list-section.tsx | 13 -- .../people/[user_id]/(sections)/hero.tsx | 60 --------- .../[user_id]/(sections)/person-info.tsx | 38 ------ .../(sections)/edit-person-form-section.tsx | 18 --- .../people/[user_id]/edit/(sections)/hero.tsx | 15 --- .../people/[user_id]/edit/index.server.ts | 18 --- .../admin/people/[user_id]/edit/index.tsx | 26 ---- .../admin/people/[user_id]/index.server.ts | 32 ----- src/pages/admin/people/[user_id]/index.tsx | 24 ---- .../admin/people/create/(sections)/hero.tsx | 6 - .../create/(sections)/user-form-section.tsx | 10 -- src/pages/admin/people/create/index.server.ts | 17 --- src/pages/admin/people/create/index.tsx | 18 --- src/pages/admin/people/index.tsx | 18 --- .../users}/(functions)/submit-user-form.ts | 2 +- .../users/(partials)/new-user-modal.tsx | 2 +- .../users}/(partials)/user-form/index.tsx | 2 +- .../(partials)/user-form/user-form-action.tsx | 0 .../(partials)/user-form/user-form-email.tsx | 0 .../(partials)/user-form/user-form-image.tsx | 0 .../(partials)/user-form/user-form-name.tsx | 0 .../user-form/user-form-password.tsx | 0 .../user-form/user-form-user-types.tsx | 0 .../user-form/user-form-username.tsx | 0 .../[user_id]/(partials)/user-edit-group.tsx | 2 +- src/utils/is-admin-section-path.test.ts | 3 +- 37 files changed, 10 insertions(+), 791 deletions(-) delete mode 100644 src/pages/admin/people/(hooks)/use-all-people-list-init.tsx delete mode 100644 src/pages/admin/people/(partials)/all-people-list-search.tsx delete mode 100644 src/pages/admin/people/(partials)/all-people-list-selections.tsx delete mode 100644 src/pages/admin/people/(partials)/all-people-list-title-section.tsx delete mode 100644 src/pages/admin/people/(partials)/all-people-list-users-type-select.tsx delete mode 100644 src/pages/admin/people/(partials)/people-list-card.tsx delete mode 100644 src/pages/admin/people/(partials)/people-list.tsx delete mode 100644 src/pages/admin/people/(sections)/hero.tsx delete mode 100644 src/pages/admin/people/(sections)/people-list-section.tsx delete mode 100644 src/pages/admin/people/[user_id]/(sections)/hero.tsx delete mode 100644 src/pages/admin/people/[user_id]/(sections)/person-info.tsx delete mode 100644 src/pages/admin/people/[user_id]/edit/(sections)/edit-person-form-section.tsx delete mode 100644 src/pages/admin/people/[user_id]/edit/(sections)/hero.tsx delete mode 100644 src/pages/admin/people/[user_id]/edit/index.server.ts delete mode 100644 src/pages/admin/people/[user_id]/edit/index.tsx delete mode 100644 src/pages/admin/people/[user_id]/index.server.ts delete mode 100644 src/pages/admin/people/[user_id]/index.tsx delete mode 100644 src/pages/admin/people/create/(sections)/hero.tsx delete mode 100644 src/pages/admin/people/create/(sections)/user-form-section.tsx delete mode 100644 src/pages/admin/people/create/index.server.ts delete mode 100644 src/pages/admin/people/create/index.tsx delete mode 100644 src/pages/admin/people/index.tsx rename src/pages/admin/{people/create => settings/users}/(functions)/submit-user-form.ts (96%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/index.tsx (96%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-action.tsx (100%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-email.tsx (100%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-image.tsx (100%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-name.tsx (100%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-password.tsx (100%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-user-types.tsx (100%) rename src/pages/admin/{people/create => settings/users}/(partials)/user-form/user-form-username.tsx (100%) diff --git a/AGENTS.md b/AGENTS.md index f834816..e7a628f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts ""` - a read-onl ### Auth - **Auth guard:** `src/pages/__root.server.ts` - server function that runs on every request. - - No public pages: anything outside the live admin sections (`AdminSections` in `src/dict/admin-sections-dict.ts`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`) and `/auth` redirects to `AdminHomePath` (`/admin/bots`). `/admin` itself only redirects there. People exists in code but is closed until added back to `AdminSections` + - No public pages: anything outside the live admin sections (`AdminSections` in `src/dict/admin-sections-dict.ts`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`) and `/auth` redirects to `AdminHomePath` (`/admin/bots`). `/admin` itself only redirects there - No users at all: everything redirects to `/auth/signup` (first-run super admin setup, `grabHasUsers`); once a user exists `/auth/signup` redirects away - Redirects unauthenticated `/admin/*` → `/auth/login` - Unknown `/admin/settings/*` pages (not in `SettingsPages`, `src/utils/is-unknown-settings-path.ts`) redirect to `/admin/settings/general` @@ -150,16 +150,15 @@ Envs are set in Settings, under Environment (super admin only), not in `.env`. T ## Admin Route Hierarchy -Live routes under `/admin` (see `AdminSections`): `/admin/bots`, `/admin/workspaces` (the index opens the first workspace), `/admin/routines`, `/admin/logs`, `/admin/help` and `/admin/settings`. `/admin` redirects to `/admin/bots`; a workspace's dashboard is `/admin/workspaces/`. The rest below are built but closed for now (redirect to `/admin/bots`): +Live routes under `/admin` (see `AdminSections`): `/admin/bots`, `/admin/workspaces` (the index opens the first workspace), `/admin/routines`, `/admin/logs`, `/admin/help` and `/admin/settings`. `/admin` redirects to `/admin/bots`; a workspace's dashboard is `/admin/workspaces/`. The routes in detail: -- `/admin/people` - admin users list, plus `create/`, `[user_id]/` (profile), `[user_id]/edit/` - `/admin/bots` (live) - the Cobble chat shell, plus `[bot_id]/` - `/admin/routines` (live) - every schedule the user's bots have (see Schedules), opened from "Routines" in the rail's user popover - `/admin/logs` (live) - Activity, the audit log: super admins only (others are redirected; the popover's "Activity" item shows to super admins only). Rendered in `SettingsShell` with `wide` - `/admin/help` (live) - familiar problems and their fixes, one `SettingsGroup` per topic (`(data)/help-topics.ts`, markdown rendered with the chat's `MessageMarkdownComponents`, the topic id as the group's anchor). First topic: signing in to Google and other strict sites with Cookie-Editor over the Desktop view (`HelpSignInTopicId` in `src/dict/help-dict.ts`; `ask_user_to_take_over` points bots to it) - Routines, Activity and Help are the `RailPages` (`src/dict/rail-pages-dict.ts`), opened from the rail's user popover (which lists them, then Settings and Sign out). On them the rail shows its own aside (`(partials)/rail-pages-list.tsx`): a back link to the bots, then those pages, like settings - `/admin/settings/backups` - super admin only (listed in `SettingsPages`): create/list/download database snapshots in `db/.backups`, plus `restore/` (restore a listed backup, or upload a SQLite database file which is saved to `.backups` then restored). Shells out to the `bun-sqlite` CLI (`backup`, and `import ` since `restore` is interactive) via `src/functions/backend/backups/`; API routes in `api/admin/backups/` -- `/admin/settings` (live) - redirects to `SettingsDefaultPath` (`/admin/settings/general`). Sub pages: `general/` (theme: System/Light/Dark per device via `useThemePreference`, stored in `localStorage[AppData.ThemeStorageKey]`, the same key `ThemeInitScript` and twui's `ColorSchemeSelector` read; applies instantly, no save bar), `profile/` (current user profile), `notifications/` (this device's Web Push switch, a test send, and every device the user subscribed), `change-password/` (set/change own password) `models/` (super admin only, `super_admin_only` in the dict hides it from the rail: model providers and which model each harness uses) and `environment/` (super admin only: app settings and the envs bots and computers use, per scope), each wrapped in `SettingsShell` (`(partials)/settings-shell.tsx`: 64px pane header like the conversation header, then one centered column). Content is built from `SettingsGroup` blocks (label + description left, controls right, hairline-separated, no cards) and ends with the sticky `SettingsSaveBar` (Save enabled only when dirty, Discard, one-line status). Saves don't reload the page. Keep save notices in component state: `useFormInit` clears `status` whenever the form changes. `bots/` (the crew: chat, edit with `BotForm`, models, delete a worker via `api/admin/bots/delete-bot`), `workspaces/` (open, create through the Add flow, delete), `computers/` (state and resources, live; super admins create, edit with `AddComputerForm`, retry, delete), `mcps/` and `tools/` (see MCPs and custom tools: super admins add, edit, refresh or try, delete; admins only set Access, `extension-access-modal.tsx`; add, edit, access and try open in modals (`AddModalShell`)), each row with a status line under it (`extension-status-line.tsx`: connected or not, last run worked or failed, when, and why), those five for `canManageCrew` only (`grab-crew-settings-server-props.ts`, `can_view` in the dict) and `users/` (for `canListUsers`: super admins and admins; the dict's `can_view` gates it in the rail; add with a password (`canCreateUser`), edit (`canUpdateUser`), both with the one `UserForm` (`admin/people/create/(partials)/user-form/`, `useFormInit`, photo included, saved in one request), revoke (adds `revoked`, `canRevokeUser`), delete (`canRevokeUser`'s rules, never yourself), pick their user types from `UserTypes` (`user-types-select.tsx`: only the ones `filterAssignableUserTypes` allows, never `UneditableUserTypes`; at least one, since login needs a type; `saveUser` checks each grant and removal with `userTypeAssignment`; the super admin's types are never shown or changed there, and they can't be revoked; the password is entered twice, `SaveUserRequestSchema` checks they match), delete with everything theirs; `api/admin/users/save-user`, `delete-user`). The lists share `SettingsList`, `SettingsListRow`, `SettingsRowAction` and `use-settings-list`. Sub pages are declared once in `src/dict/settings-pages-dict.ts`; on every `/admin/settings/*` page the rail swaps the crew for a back link plus that list. Add a sub page by adding its folder and a dict entry +- `/admin/settings` (live) - redirects to `SettingsDefaultPath` (`/admin/settings/general`). Sub pages: `general/` (theme: System/Light/Dark per device via `useThemePreference`, stored in `localStorage[AppData.ThemeStorageKey]`, the same key `ThemeInitScript` and twui's `ColorSchemeSelector` read; applies instantly, no save bar), `profile/` (current user profile), `notifications/` (this device's Web Push switch, a test send, and every device the user subscribed), `change-password/` (set/change own password) `models/` (super admin only, `super_admin_only` in the dict hides it from the rail: model providers and which model each harness uses) and `environment/` (super admin only: app settings and the envs bots and computers use, per scope), each wrapped in `SettingsShell` (`(partials)/settings-shell.tsx`: 64px pane header like the conversation header, then one centered column). Content is built from `SettingsGroup` blocks (label + description left, controls right, hairline-separated, no cards) and ends with the sticky `SettingsSaveBar` (Save enabled only when dirty, Discard, one-line status). Saves don't reload the page. Keep save notices in component state: `useFormInit` clears `status` whenever the form changes. `bots/` (the crew: chat, edit with `BotForm`, models, delete a worker via `api/admin/bots/delete-bot`), `workspaces/` (open, create through the Add flow, delete), `computers/` (state and resources, live; super admins create, edit with `AddComputerForm`, retry, delete), `mcps/` and `tools/` (see MCPs and custom tools: super admins add, edit, refresh or try, delete; admins only set Access, `extension-access-modal.tsx`; add, edit, access and try open in modals (`AddModalShell`)), each row with a status line under it (`extension-status-line.tsx`: connected or not, last run worked or failed, when, and why), those five for `canManageCrew` only (`grab-crew-settings-server-props.ts`, `can_view` in the dict) and `users/` (for `canListUsers`: super admins and admins; the dict's `can_view` gates it in the rail; add with a password (`canCreateUser`), edit (`canUpdateUser`), both with the one `UserForm` (`admin/settings/users/(partials)/user-form/`, `useFormInit`, photo included, saved in one request), revoke (adds `revoked`, `canRevokeUser`), delete (`canRevokeUser`'s rules, never yourself), pick their user types from `UserTypes` (`user-types-select.tsx`: only the ones `filterAssignableUserTypes` allows, never `UneditableUserTypes`; at least one, since login needs a type; `saveUser` checks each grant and removal with `userTypeAssignment`; the super admin's types are never shown or changed there, and they can't be revoked; the password is entered twice, `SaveUserRequestSchema` checks they match), delete with everything theirs; `api/admin/users/save-user`, `delete-user`). The lists share `SettingsList`, `SettingsListRow`, `SettingsRowAction` and `use-settings-list`. Sub pages are declared once in `src/dict/settings-pages-dict.ts`; on every `/admin/settings/*` page the rail swaps the crew for a back link plus that list. Add a sub page by adding its folder and a dict entry Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout or aside. The rail header's Add icon (super admins and admins only, like the rail's "+" buttons) opens the Add flow (`(partials)/add-flow.tsx`, `AddFlowContext`): a bot, a workspace or (super admins) a computer, each form in a modal stacked over the choice, so closing it goes back there (the Bots and Workspaces labels have a "+" that opens the form directly); on a workspace's pages new bots and computers join it. The rail shows the crew (`(partials)/rail-crew-list.tsx`, needs `bots` and `workspaces` in page props): the coordinator, the latest `AppData.RailRecentBots` workers (plus any open, unread or needing you, `pick-rail-bots`), "See all bots" with a few stones opening every bot in a modal, then Workspaces with a "+". On `/admin/workspaces/*` it shows the workspace's crew instead (`(partials)/rail-workspace-list.tsx`: back link, the workspace, then only the bots attached to it, "Workspace bots"; the coordinator stays in the main crew rail, and on a workspace bot's page the back link returns to the workspace page), except on `/admin/settings/*` where it shows "Back to bots" plus the settings sub pages (`(partials)/rail-settings-list.tsx`). Page heroes use the shared `AdminHero` component (`src/components/general/admin-hero.tsx`). diff --git a/src/dict/admin-sections-dict.ts b/src/dict/admin-sections-dict.ts index 130d6fc..8e36863 100644 --- a/src/dict/admin-sections-dict.ts +++ b/src/dict/admin-sections-dict.ts @@ -1,7 +1,6 @@ /** * The admin sections that are live. Any other `/admin/*` path redirects to - * `AdminHomePath`. People exists in code but stays closed until it is added - * back here. Logs (Activity) is super admins only. + * `AdminHomePath`. Logs (Activity) is super admins only. */ export const AdminSections = [ "/admin/bots", diff --git a/src/pages/admin/logs/(partials)/log-details-modal.tsx b/src/pages/admin/logs/(partials)/log-details-modal.tsx index 1e4ba55..ae41557 100644 --- a/src/pages/admin/logs/(partials)/log-details-modal.tsx +++ b/src/pages/admin/logs/(partials)/log-details-modal.tsx @@ -45,7 +45,7 @@ export default function LogDetailsModal({ { label: "User", value: log.user_id ? ( - + {user_name || "User"} #{log.user_id} ) : ( diff --git a/src/pages/admin/people/(hooks)/use-all-people-list-init.tsx b/src/pages/admin/people/(hooks)/use-all-people-list-init.tsx deleted file mode 100644 index facc7b8..0000000 --- a/src/pages/admin/people/(hooks)/use-all-people-list-init.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import _ from "lodash"; -import { AppData } from "@/src/data/app-data"; -import { useContext, useRef } from "react"; -import useStatus from "@moduletrace/twui/hooks/useStatus"; -import type { ApiReqParams, UserType } from "@/src/types"; -import { AppContext } from "@/src/pages/__root"; -import checkUserAccess from "@/src/functions/backend/auth/check-user-access"; -import EJSON from "@/src/utils/ejson"; -import { useUsersGet } from "@/src/hooks/use-users-get"; -import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins"; - -type Props = { - users_type?: UserType | null; -}; - -export default function useAllPeopleListInit({ users_type }: Props) { - const { pageProps, query } = useContext(AppContext); - const { user_types, url } = pageProps || {}; - - const limit = AppData["DefaultQueryLimit"]; - const can_view_users = checkUserAccess({ - user_types, - includes: ["admin"], - }); - - let location_query = EJSON.parse(url?.searchParams?.q || "") as - | ApiReqParams - | undefined; - - if (query?.user_type) { - if (!location_query) { - location_query = { - target_user_type: query.user_type as any, - }; - } else { - location_query.target_user_type = query.user_type as any; - } - } - - const { - users: people, - count, - page, - setPage, - setUsersQuery, - setUsers: set_people, - ready, - } = useUsersGet({ - query: location_query, - }); - - const selected = useRef(new Set()); - - const { refresh, setRefresh } = useStatus(); - - return { - user_types, - limit, - can_view_users, - people, - count, - page, - setPage, - setUsersQuery, - set_people, - selected, - refresh, - setRefresh, - users_type, - location_query, - ready, - }; -} diff --git a/src/pages/admin/people/(partials)/all-people-list-search.tsx b/src/pages/admin/people/(partials)/all-people-list-search.tsx deleted file mode 100644 index aa86817..0000000 --- a/src/pages/admin/people/(partials)/all-people-list-search.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import Search from "@moduletrace/twui/elements/Search"; -import Select from "@moduletrace/twui/form/Select"; -import Divider from "@moduletrace/twui/layout/Divider"; -import { useRef } from "react"; -import _ from "lodash"; -import twuiSlugToNormalText from "@moduletrace/twui/utils/slug-to-normal-text"; -import type useAllPeopleListInit from "../(hooks)/use-all-people-list-init"; -import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins"; -import searchableFieldToQueryField from "@/src/utils/searchable-field-to-query-field"; -import type { ApiReqParams } from "@/src/types"; - -export default function AllPeopleListSearch({ - setUsersQuery, - location_query, -}: ReturnType) { - const searchable_fields: (keyof BUN_SQLITE_COBBLE_USERS_JOIN)[] = [ - "first_name", - "last_name", - "email", - "user_types", - ]; - - const initial_search_field = (Object.keys(location_query || {}).filter( - (q) => q.startsWith("search_term"), - )?.[0] || - searchableFieldToQueryField({ - field: searchable_fields[0]!, - })) as keyof ApiReqParams; - - const initial_search = String(location_query?.[initial_search_field] || ""); - - const search_field_ref = useRef(initial_search_field); - const search_value_ref = useRef(initial_search); - const initialized_ref = useRef(false); - - function appendQuery() { - setUsersQuery((prev) => { - let nq = _.cloneDeep(prev) || {}; - - for (let i = 0; i < searchable_fields.length; i++) { - const sf = searchableFieldToQueryField({ - field: searchable_fields[i]!, - }); - delete nq[sf]; - } - - if (search_value_ref.current.match(/./)) { - nq[search_field_ref.current] = search_value_ref.current; - } - - nq.page = 1; - - if (_.isEqual(nq, prev)) { - return prev; - } - - return nq; - }); - } - - return ( - { - search_value_ref.current = value || ""; - - if (!initialized_ref.current) { - initialized_ref.current = true; - if ((value || "") === initial_search) { - return; - } - } - - appendQuery(); - }} - defaultValue={initial_search} - no_search_button - inputProps={{ - suffix: ( - <> - -