diff --git a/astro.config.mjs b/astro.config.mjs index 51bd4da..3ab7057 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -367,6 +367,32 @@ export default defineConfig({ }, ], }, + { + label: "Admin panel", + collapsed: true, + items: [ + { + label: "Overview", + link: "/admin-panel/overview/", + }, + { + label: "Users", + link: "/admin-panel/users/", + }, + { + label: "Workspaces", + link: "/admin-panel/workspaces/", + }, + { + label: "Audit log", + link: "/admin-panel/audit-log/", + }, + { + label: "Configuration", + link: "/admin-panel/configuration/", + }, + ], + }, ], }), ], diff --git a/src/assets/screenshots/huly/admin-panel/admin-panel-overview-sidebar.png b/src/assets/screenshots/huly/admin-panel/admin-panel-overview-sidebar.png new file mode 100644 index 0000000..2ed61d4 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/admin-panel-overview-sidebar.png differ diff --git a/src/assets/screenshots/huly/admin-panel/audit-date-presets.png b/src/assets/screenshots/huly/admin-panel/audit-date-presets.png new file mode 100644 index 0000000..07b536d Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/audit-date-presets.png differ diff --git a/src/assets/screenshots/huly/admin-panel/audit-default.png b/src/assets/screenshots/huly/admin-panel/audit-default.png new file mode 100644 index 0000000..87b5061 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/audit-default.png differ diff --git a/src/assets/screenshots/huly/admin-panel/audit-details.png b/src/assets/screenshots/huly/admin-panel/audit-details.png new file mode 100644 index 0000000..5013c5a Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/audit-details.png differ diff --git a/src/assets/screenshots/huly/admin-panel/audit-filters.png b/src/assets/screenshots/huly/admin-panel/audit-filters.png new file mode 100644 index 0000000..87efbf9 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/audit-filters.png differ diff --git a/src/assets/screenshots/huly/admin-panel/users-active-filter.png b/src/assets/screenshots/huly/admin-panel/users-active-filter.png new file mode 100644 index 0000000..1c5b2ad Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/users-active-filter.png differ diff --git a/src/assets/screenshots/huly/admin-panel/users-bulk-bar.png b/src/assets/screenshots/huly/admin-panel/users-bulk-bar.png new file mode 100644 index 0000000..237fe90 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/users-bulk-bar.png differ diff --git a/src/assets/screenshots/huly/admin-panel/users-default.png b/src/assets/screenshots/huly/admin-panel/users-default.png new file mode 100644 index 0000000..9528ead Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/users-default.png differ diff --git a/src/assets/screenshots/huly/admin-panel/users-drawer.png b/src/assets/screenshots/huly/admin-panel/users-drawer.png new file mode 100644 index 0000000..f73ff2a Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/users-drawer.png differ diff --git a/src/assets/screenshots/huly/admin-panel/users-orphan-filter.png b/src/assets/screenshots/huly/admin-panel/users-orphan-filter.png new file mode 100644 index 0000000..953b1ff Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/users-orphan-filter.png differ diff --git a/src/assets/screenshots/huly/admin-panel/users-stat-pills.png b/src/assets/screenshots/huly/admin-panel/users-stat-pills.png new file mode 100644 index 0000000..e4695a7 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/users-stat-pills.png differ diff --git a/src/assets/screenshots/huly/admin-panel/workspaces-default.png b/src/assets/screenshots/huly/admin-panel/workspaces-default.png new file mode 100644 index 0000000..2c1fc08 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/workspaces-default.png differ diff --git a/src/assets/screenshots/huly/admin-panel/workspaces-drawer.png b/src/assets/screenshots/huly/admin-panel/workspaces-drawer.png new file mode 100644 index 0000000..641dd94 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/workspaces-drawer.png differ diff --git a/src/assets/screenshots/huly/admin-panel/workspaces-mass-archive.png b/src/assets/screenshots/huly/admin-panel/workspaces-mass-archive.png new file mode 100644 index 0000000..e5c2a27 Binary files /dev/null and b/src/assets/screenshots/huly/admin-panel/workspaces-mass-archive.png differ diff --git a/src/content/docs/admin-panel/audit-log.mdx b/src/content/docs/admin-panel/audit-log.mdx new file mode 100644 index 0000000..4a683aa --- /dev/null +++ b/src/content/docs/admin-panel/audit-log.mdx @@ -0,0 +1,101 @@ +--- +title: Audit log +description: Review every administrative action with filtering, sortable columns, and batch grouping. +--- + +import { Image } from "astro:assets"; +import auditDefault from "../../../assets/screenshots/huly/admin-panel/audit-default.png"; +import auditFilters from "../../../assets/screenshots/huly/admin-panel/audit-filters.png"; +import auditDatePresets from "../../../assets/screenshots/huly/admin-panel/audit-date-presets.png"; +import auditDetails from "../../../assets/screenshots/huly/admin-panel/audit-details.png"; + +Every administrative action — `create_account`, `disable`, `enable`, `set_workspace_member_role`, `archive_workspace`, `trigger_password_reset`, `add_workspace_member`, `remove_member`, `admin_action_denied`, the four `bulk_*` variants, and more — writes a row to the `admin_audit_log` table. The Audit log page is your read-only view of that table with filtering, sorting, and batch grouping for bulk operations. + +Audit log default view with filter row and grouped batch entries + +## Default view + +The page opens with the **Last 3 days** time range and the latest entries on top. The To-date is inclusive through end-of-day, so entries created earlier today are visible without switching to a Custom range. The render is capped at 200 rows; when more match your filters, a `Showing most recent 200 of N` notice appears at the top. + +When switching the preset dropdown from a fixed window to **Custom range**, the From and To dates inherit the previously-active preset's values rather than clearing. This lets an admin tweak one boundary without re-entering both. (The fall-back is a 3-day window when both inputs are empty.) + +## Filtering + +Audit log filter row with admin name input, target name input, actions multi-select, and date range presets + +The filter row exposes the controls a human reviewer needs, not the raw UUIDs: + +- **Admin name or email** — substring match against the actor's name or primary email. +- **Target name or URL** — substring match against the target account's name or the target workspace's URL. +- **Actions** — multi-select dropdown of every action type that exists in the log. Pick one or more to narrow to those. +- **Date range** — preset dropdown with 1 day / 2 days / 3 days / 1 week / 2 weeks / 1 month / Custom. + +Date range preset dropdown open showing Last 1/2/3 days, 1/2 weeks, 1 month, Custom + +When the preset is anything other than `Custom`, the From/To date inputs are read-only. Switching to `Custom` makes them editable. + +The `Reset` button next to the filter row clears every filter and returns to the `Last 3 days` default. + +If no rows match, the table shows an empty state with a `Clear filter` shortcut. + +## Denied admin actions (V13) + +The audit log captures not only successful admin actions but also denied ones, with action type `admin_action_denied`. Two server-side guards write such rows: + +- **`cannot_self_disable`** — fires when an admin attempts to disable their own account via the Disable button (the UI greys it out, but the audit row also catches direct RPC calls that bypass the UI). +- **`last_admin`** — fires when an admin attempts to disable the only remaining account whose email appears in `ADMIN_EMAILS` (this would lock the instance out of all admin RPCs). + +The audit row's `details` payload includes: + +- `reason` — `self_disable` or `last_admin` +- `method` — `disableAccount` (single-target RPC) or `bulkSetDisabled` (bulk RPC; the per-target audit-write fires inside the same RPC, with the bulk-self case audited once before the bulk loop strips the self-target) +- `target` — the account UUID the denied action was aimed at + +### Rate-limit + +`admin_action_denied` rows are throttled per `(adminUuid, reason, method)` per 1-hour window using an in-memory map. The first denial in a window is audited; subsequent identical denials still throw their platform error to the caller but do not produce duplicate audit rows. This is intentional — the audit log should record the attempt pattern without flooding under an automated probe. The map resets on account-service pod restart (acceptable threat-model trade-off). + +### Pre-auth denials are not audited + +A request that fails `requireAdmin` (no admin claim on the token, expired session, etc.) does NOT produce an audit row. Writing audit rows pre-auth would itself be an attack surface — an attacker could spam audit rows that name no actor uuid. The platform-level Forbidden response is still returned to the caller in that case. + +### Filtering by denial action + +The Actions multi-select filter at the top of the page now includes `admin_action_denied`. Selecting it surfaces all denied-attempt rows; combine with the Admin filter to scope to one actor, or the date-range presets to scope to a window. + +## Sorting + +Click any of the **Time**, **Admin**, **Action**, or **Target** column headers to sort. The arrow indicator only appears on the active sort column. + +Each column also has a small filter icon. Clicking it scrolls the page up and focuses the relevant top-bar input — a convenience for quickly narrowing on something you spotted in the table. + +### Timestamp tooltip (V8) + +Every timestamp cell in the table — both the per-row times and the bulk-action batch-header timestamps — exposes a two-line tooltip on hover: + +``` +Local time: 2026-05-27 14:33:54 (Europe/Vienna, UTC+1) +UTC: 2026-05-27 13:33:54 +``` + +The Local time uses the browser's resolved timezone (via `Intl.DateTimeFormat`); the UTC line gives an unambiguous absolute reference for cross-region investigations. If the browser doesn't expose `Intl` (very rare), the tooltip degrades to "Local time:" without the zone-name parenthetical. + +## Batch grouping + +Bulk operations write one audit row per affected target, all stamped with a single `batch_id` UUID. The audit page groups consecutive same-batch rows under a non-interactive header showing the admin who ran the action, the action name, the entry count, and the timestamp. A `bulk_disable` of 50 accounts therefore shows as one collapsible group, not 50 separate rows polluting the log. + +Individual (non-bulk) actions are rendered as plain rows without a group header. + +## Details column + +Each row has a `Details` column that contains the action's structured JSON payload. By default it is collapsed to a `N keys — expand` link so the table stays scannable. Click to view the full JSON: + +Details column expanded showing JSON payload of an admin action + +The payload includes the diff or arguments of the action — for example, an `enable_account` row carries `{ accountUuid, previousStatus }`, and a `set_workspace_member_role` row carries `{ workspaceUuid, accountUuid, oldRole, newRole }`. + +## Retention + +The audit table is automatically pruned. By default, rows older than 365 days are deleted by a daily background job. To change this, set the `AUDIT_RETENTION_DAYS` environment variable on the account service — see [Configuration](/admin-panel/configuration). + +Setting `AUDIT_RETENTION_DAYS=0` disables the prune entirely. The job auto-disables itself on the legacy MongoDB backend. diff --git a/src/content/docs/admin-panel/configuration.mdx b/src/content/docs/admin-panel/configuration.mdx new file mode 100644 index 0000000..91a6efb --- /dev/null +++ b/src/content/docs/admin-panel/configuration.mdx @@ -0,0 +1,90 @@ +--- +title: Configuration +description: Environment variables and prerequisites for the Huly admin panel. +--- + +The admin panel is wired into the account service. Configuration happens through environment variables on the account pod. + +## Required: ADMIN_EMAILS + +`ADMIN_EMAILS` is a comma-separated list of primary email addresses that are allowed to use the admin panel. Anyone whose Huly account's primary email matches an entry on this list sees the `Admin panel` link in their workbench menu and can call admin RPCs. Everyone else gets a server-side `403`. + +```bash +# docker-compose.yml or .env +ADMIN_EMAILS=ops@example.com,security@example.com +``` + +Notes: + +- Matching is case-insensitive. +- Whitespace around entries is trimmed. +- An empty or unset `ADMIN_EMAILS` means **no one** has admin access. Existing accounts that previously had admin access lose it on the next sign-in. +- Removing an email from the list while that admin is signed in does **not** force-log them out — they keep their session until it expires. Their next admin-only RPC call is rejected. +- The reverse — an admin disabling themselves — does force a logout via the same mechanism used for any disabled account. + +## Optional: AUDIT_RETENTION_DAYS + +`AUDIT_RETENTION_DAYS` controls how long admin audit entries are kept before the daily prune job deletes them. + +```bash +AUDIT_RETENTION_DAYS=365 # default +AUDIT_RETENTION_DAYS=90 # keep 90 days only +AUDIT_RETENTION_DAYS=0 # disable prune entirely +``` + +- Default: `365` (one year). +- `0` disables the prune. Use this if you forward audit rows to an external SIEM before they are deleted, and you want the database to be the source of truth indefinitely. +- The prune job runs once per 24 hours. +- On the legacy MongoDB backend, the timer auto-disables itself the first time the prune call returns `not implemented`, so you do not need to set this to `0` manually. + +V13 also adds an `admin_action_denied` audit action; see [Audit log](/admin-panel/audit-log) for the denial reasons and rate-limit behaviour. + +## Backend prerequisites + +The admin panel only fully works against the PostgreSQL/CockroachDB backend (`CommunityDB`). The implementation uses SQL patterns (typed query builders, partial indexes, batch UUID stamping) that the Mongo-based account store does not implement. + +On the Mongo backend: + +- Listing endpoints return `not implemented`. +- The admin panel UI shows error states for every section. +- The audit-retention timer auto-disables. + +Plan a migration to CockroachDB before relying on the admin panel in production. + +## Migrations + +The admin panel ships with four forward-only migrations on the `account` database: + +| Migration | Adds | +|-----------|------| +| V27 | `account.disabled_at`, `account.token_version`, `account.last_activity_at`, `admin_audit_log` table | +| V28 | `admin_audit_log.target_account` NULLABLE + five query-tuning indexes | +| V29 | `admin_audit_log.batch_id UUID NULL` | +| V30 | Partial index on `admin_audit_log(batch_id) WHERE batch_id IS NOT NULL` | + +All four are `IF NOT EXISTS`-guarded and idempotent. They run on the next account-pod startup after upgrade. No downtime is required. + +There are no down-migrations. If you need to roll back, leave the schema in place and redeploy the previous account image. The new columns are nullable or have defaults that keep older code paths working. + +## Security posture + +The admin panel applies several layers of hardening worth knowing about: + +- **Server-side admin check** on every admin RPC (`assertAdmin`) — the UI only hides the menu, it does not gate access. A non-admin who manually crafts a request gets a `403`. +- **CSV export rate limit:** 5 requests per minute per token. The token is SHA-256-hashed before being used as the limiter key, so a leaked memory dump does not expose a usable bearer. +- **CSV bearer in header, not URL:** the admin UI uses `fetch + Authorization: Bearer + blob download` instead of `window.open(url-with-token)`. The server still accepts the legacy `?token=…` query parameter for one release with a deprecation warning so out-of-band scripts keep working. +- **Filter param hardening:** the base64-JSON filter parameter parser rejects `__proto__` / `constructor` / `prototype` keys recursively and caps recursion at 32 levels, so a crafted filter cannot reach prototype pollution. +- **LIKE wildcard escape:** all substring filters escape `%`, `_`, and `\` with explicit `ESCAPE '\'` clauses, so a search for `100%` matches the literal string and not "any prefix ending in 1". +- **Force-logout on disable:** when an admin disables an account (including their own), every active session for that account is invalidated within seconds via an `AccountDisabled` transaction sent through the workbench websocket. + +### Disable behaviour + +The admin panel exposes one account-state action: **Disable**. Setting the disabled flag: + +- Writes `disabled_at` on the account row. +- Increments `token_version` to invalidate every active session for that account (force-logout takes effect within seconds of the next request). +- Leaves person, social_id, mailbox secrets, workspace memberships, billing subscription, and per-workspace permissions intact. + +Disable is reversible — Enable flips `disabled_at` back to NULL. Re-registration with the same email is implicitly blocked while disabled because the existing account row still occupies the email's `social_id` slot. + +The companion code PR originally included a hard-delete action with a database cascade across account, workspace_members, subscription, workspace_permissions, mailbox secrets, and integration secrets. That action was removed before the PR shipped: a hard-delete that does not also visit workspace-side documents (issues, comments, cards) is unsafe by default — it can orphan workspaces whose sole Owner is removed, and it leaves authored content silently re-attributed to the un-verified social_id of the deleted account. Hard-delete, if it returns, will be a separate smaller PR with a content/reference audit, a sole-owner workspace guard, and an impact preview before confirmation. diff --git a/src/content/docs/admin-panel/overview.mdx b/src/content/docs/admin-panel/overview.mdx new file mode 100644 index 0000000..9fc8faa --- /dev/null +++ b/src/content/docs/admin-panel/overview.mdx @@ -0,0 +1,39 @@ +--- +title: Admin panel overview +description: Manage users, workspaces, and view administrative actions in your self-hosted Huly instance. +--- + +import { Image } from "astro:assets"; +import adminSidebar from "../../../assets/screenshots/huly/admin-panel/admin-panel-overview-sidebar.png"; + +The Huly admin panel gives operators of self-hosted instances a single place to manage accounts, oversee workspaces, and review what other administrators have done. It is intended for self-hosting scenarios where you need to handle account-level operations without dropping to SQL. + +## Who can access it + +Access is restricted to administrators listed in the `ADMIN_EMAILS` environment variable of the account service. See the [Configuration](/admin-panel/configuration) page for details. + +Any account whose primary email matches an entry in `ADMIN_EMAILS` will see an `Admin panel` entry in the workbench menu. Non-admin accounts cannot reach the URL — server-side checks reject every request that is not authenticated as an admin. + +## How to open it + +Sign in to Huly with an admin account and navigate to `/login/admin` on your instance. From a logged-in workbench, you can also click the `Admin panel` link in the user menu. + +The admin panel has its own left sidebar with three sections and a back link to the workbench: + +Admin panel sidebar showing Users, Workspaces, and Audit log sections + +## The three sections + +- **[Users](/admin-panel/users)** — list, search, filter, and bulk-action accounts. Open a drawer per account to edit workspace memberships, trigger password resets, disable or enable, and add users to additional workspaces. +- **[Workspaces](/admin-panel/workspaces)** — list, sort, and filter workspaces. Select rows to mass-archive. Open a drawer to see workspace members and remove or re-role them. +- **[Audit log](/admin-panel/audit-log)** — review every administrative action with full filtering, sortable columns, batch grouping for bulk operations, and CSV export. + +## What it is not + +The admin panel is **not** a workspace-internal settings UI. It does not replace the per-workspace owner settings (people, members, billing). It only exposes the global, instance-wide account operations that previously required direct database access. + +It is also **not** a multi-tenancy console — there is only one admin panel per Huly instance, and all listed accounts and workspaces belong to that instance. + +## Backend support + +The admin panel works against the PostgreSQL/CockroachDB backend (`CommunityDB`). On the legacy MongoDB backend, admin-panel RPCs return a `not implemented` error and the audit-retention timer auto-disables itself to avoid log spam. Self-hosters on Mongo should plan a migration before relying on the admin panel. diff --git a/src/content/docs/admin-panel/users.mdx b/src/content/docs/admin-panel/users.mdx new file mode 100644 index 0000000..fb78361 --- /dev/null +++ b/src/content/docs/admin-panel/users.mdx @@ -0,0 +1,122 @@ +--- +title: Users +description: List, filter, edit, and bulk-manage accounts in your self-hosted Huly instance. +--- + +import { Image } from "astro:assets"; +import usersDefault from "../../../assets/screenshots/huly/admin-panel/users-default.png"; +import usersStatPills from "../../../assets/screenshots/huly/admin-panel/users-stat-pills.png"; +import usersActiveFilter from "../../../assets/screenshots/huly/admin-panel/users-active-filter.png"; +import usersOrphanFilter from "../../../assets/screenshots/huly/admin-panel/users-orphan-filter.png"; +import usersDrawer from "../../../assets/screenshots/huly/admin-panel/users-drawer.png"; +import usersBulkBar from "../../../assets/screenshots/huly/admin-panel/users-bulk-bar.png"; + +The `Users` section is the default landing page of the admin panel. It shows every account on your instance with the columns you need to triage day to day: name, email, authentication methods, workspace count, last activity, and status. + +Users page with stat pills, filter row, and account table + +## Stat pills + +Five clickable pills above the search row summarize the account base and double as quick filters: + +Five clickable stat pills: Total, Active, Disabled, Admins, Orphan + +- **Total** — every account, regardless of status. Clicking resets all filters. +- **Active** — accounts that are enabled and can sign in. Green tint when active. +- **Disabled** — accounts that have been disabled. Red tint. +- **Admins** — accounts whose primary email appears in the `ADMIN_EMAILS` allowlist. Blue tint. +- **Orphan** — active accounts with zero workspace memberships. Amber tint. These are the accounts to triage first because they cannot do anything useful until added to a workspace. + +Pills are toggle filters. Clicking the same pill twice clears it. + +Active pill clicked, table narrowed to active accounts + +Orphan accounts also get a small `orphan` badge inline in the **Workspaces** column, so you can spot them in mixed lists too: + +Orphan filter applied, account shows orphan badge in workspaces column + +## Search and column filters + +The search box matches against name and email (substring, case-insensitive, 300 ms debounce). Each column header also has a small filter icon that opens a popup with column-specific options: + +- **Name** / **Email** — substring filters that combine with the global search via AND. +- **Auth** — multi-select of `email`, `oidc`, `mixed`, `none`. +- **Status** — multi-select of `active`, `disabled`. +- **Workspaces** — count range (min, max). +- **Last activity** — date range, or the special `never` predicate. + +The `All auth methods` and `All statuses` dropdowns next to the search box are convenience shortcuts for the single-value variants. + +## Sorting + +Every column with a small arrow next to the label is sortable. Click to toggle ascending/descending. The arrow appears only on the column that is currently driving the sort, so the header row stays uncluttered. + +## Drawer: per-account actions + +Clicking a row opens a side drawer with the account's full details and the actions you can take against it: + +User drawer with workspace memberships, role pickers, and admin action buttons + +The drawer header shows the account's name, primary email, status pill, and an Admin badge if applicable. Below that you find: + +- **Workspace memberships** — every workspace the account belongs to with a role picker (Owner, Maintainer, Member, Guest, Read-only). Changes take effect on save. +- **Recent audit entries** — the last few admin actions targeting this account, with timestamps and the admin who performed them. +- **Action buttons:** + - `Send password reset` — sends a reset link to the account's primary email. + - `Disable account` / `Enable account` — flips the `disabled_at` flag. Disabled accounts are signed out within seconds of the click via a force-logout transaction. + - `Add to workspace` — opens a workspace picker and assigns a role. + - `Remove from workspace` — removes the account from the selected workspace. + +The drawer has Prev/Next pager controls so you can step through accounts without going back to the table. Click outside the drawer or press `Escape` to dismiss. + +### Self-block + +When the drawer is opened on the calling admin's own row, the **Disable account** button is greyed out and labelled `Cannot disable yourself`. The server-side `cannot_self_disable` guard remains the security primitive — this is a UX layer so the admin sees the impossible-action labelled before the click reaches the server. + +### Workspace-root backlink (V5a) + +Each row in the drawer's **Workspaces** section exposes an `Open ↗` link next to the role picker. Clicking opens the workspace root (`/workbench/`) in a new tab. + +The link is unconditional — there is no archived-state branching, because the admin-side `workspaceMemberships` payload does not carry an archived flag. If a workspace happens to be archived, the workbench-side renders its own "not available" view when the link is followed. + +This is intentionally a workspace-root link rather than an Employee deep-link. A future iteration may add a per-workspace `Ref` enrichment so the link can jump directly to the user's Employee record inside HR; the current version is the zero-backend-changes version of that feature. + +## Bulk actions + +Select rows via the per-row checkboxes (or the select-all checkbox in the header) to enable the bulk action bar. The bar appears as a sticky strip directly above the table (between the filter row and the header row) and stays visible while you scroll — the position matches the standard data-grid convention so the affordance is where the eye expects it. + +Bulk action bar above the table with Add to workspace, Remove from workspace, Disable, Enable, Send password reset + +Available bulk actions: + +- **Disable** — disables every selected account and force-logs them out. +- **Send password reset** — sends a reset link to every selected account's primary email. +- **Add to workspace** — picks one workspace and assigns the chosen role to every selected account. +- **Remove from workspace** — picks one workspace and removes every selected account from it. + +When the selection equals the entire (unfiltered) account list, the panel asks you to type the count to confirm before proceeding — a guard against accidental fleet-wide actions. + +Every bulk action is stamped with a single `batch_id` UUID in the audit log so the audit page can group all the resulting rows under one header. See [Audit log](/admin-panel/audit-log) for details. + +## Add user + +The primary `Add user` button in the toolbar opens a small form to create a new account directly. You provide a first name, last name, and primary email, and choose whether to: + +- send an invite email so the user sets their own password, or +- set the password yourself (useful for service accounts). + +You can optionally pick an initial workspace and role so the account is usable immediately. + +## Export CSV + +The `Export CSV` button next to `Presets` exports the currently filtered account list. The export uses an `Authorization: Bearer` header (no token in the URL) and the server enforces a rate limit of 5 requests per minute per token. If you hit it you will get a 429 with `Retry-After: 60`. The CSV is RFC 4180-compliant: CRLF line endings, UTF-8 BOM for Excel compatibility. + +## Presets + +The `Presets` dropdown groups three operations: + +- **Save current filter** — give the current search/filter combination a name to reuse later. +- **Load preset** — apply a previously saved filter combination. +- **Manage presets** — rename or delete saved presets. + +Presets are scoped to your account and stored locally. diff --git a/src/content/docs/admin-panel/workspaces.mdx b/src/content/docs/admin-panel/workspaces.mdx new file mode 100644 index 0000000..37ba942 --- /dev/null +++ b/src/content/docs/admin-panel/workspaces.mdx @@ -0,0 +1,73 @@ +--- +title: Workspaces +description: List, sort, filter, and bulk-manage workspaces in your self-hosted Huly instance. +--- + +import { Image } from "astro:assets"; +import workspacesDefault from "../../../assets/screenshots/huly/admin-panel/workspaces-default.png"; +import workspacesMassArchive from "../../../assets/screenshots/huly/admin-panel/workspaces-mass-archive.png"; +import workspacesDrawer from "../../../assets/screenshots/huly/admin-panel/workspaces-drawer.png"; + +The `Workspaces` section gives you an instance-wide view of every workspace, with capacity stats, per-workspace details, and bulk actions for the cases when you need to clean up at scale. + +Workspaces page with stats, version/region breakdown, and workspace table + +## Stats + +The header shows two bands: + +- **Counts:** Total · Active · With active sessions · Users · Connections · Created in last 30 days +- **Capacity:** Total storage. If you have never run a backup, the panel shows `0 MB` with a `No backup data yet` hint instead of a misleading zero. + +Below that, a quick breakdown by **version** and **region** helps you spot version drift across the instance. + +## The workspace table + +Each row shows: + +| Column | Meaning | +|--------|---------| +| Name | The workspace's display name. Edit/copy buttons appear on hover. | +| Region | Which region the workspace is hosted in (relevant for multi-region deployments). | +| Last visit | When a member last opened the workspace. | +| Mode | `Active` / `Archived` / `Creating` etc. | +| Attempts | How many times the workspace has been created or restored. | +| Progress | Workspace creation/migration progress (only meaningful for in-flight workspaces). | +| Storage | Total bytes the workspace uses on backup storage. | +| Backup age | How old the most recent backup is. | +| Actions | Quick `Archive` button per row. | + +Every column with a small arrow is sortable. Filter icons on each header open per-column filter popups, the same pattern as on the Users page. + +## Selection and mass actions + +Each row has a checkbox; the header has a select-all checkbox that selects every visible (filtered) row. + +With at least one workspace selected, the toolbar shows: + +- A selection summary (`Selected: N · Clear`) on the left. +- A `Mass Archive N` button that archives every selected active workspace in one batch. + +Two workspaces selected, Mass Archive 2 button visible + +The mass action only operates on rows that are both selected and currently active. Archived workspaces in the selection are skipped silently. + +As with bulk user actions, every mass operation is stamped with a single `batch_id` so the audit log can group it under one header. + +## Drawer: per-workspace details + +Clicking a row opens a side drawer showing: + +Workspace drawer with members tab and audit history + +- **Members tab** — every account that belongs to the workspace, with their role and a remove button. You can also re-role members inline. +- **Audit tab** — recent admin actions targeting this workspace. + +The drawer has the same Prev/Next pager as the Users drawer, refetches when you switch rows, and dismisses on outside click or `Escape`. + +## Toolbar buttons + +- **Top 10 by storage** — quick sort/filter to surface the biggest workspaces. Disabled until backup data exists. +- **Add workspace** — primary CTA, links to the regular workspace creation flow. +- **Export CSV** — client-side CSV export of the currently filtered list. RFC 4180-compliant, no server roundtrip needed for this one. +- **Presets** — save and reload filter combinations, same pattern as on the Users page.