diff --git a/plugins/music/web/MusicPlayerHost.tsx b/plugins/music/web/MusicPlayerHost.tsx index 5c5a270e..1f46057d 100644 --- a/plugins/music/web/MusicPlayerHost.tsx +++ b/plugins/music/web/MusicPlayerHost.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router'; import { useClient } from 'hooks/useClient'; -import { useCapabilities } from 'hooks/useCapabilities'; +import { usePermissions } from 'hooks/usePermissions'; import { Play, Pause, SkipBack, SkipForward, X, Volume2, VolumeX, Loader2, MicVocal } from 'lucide-react'; import { SeekBar } from 'officerdev'; import { MusicHeart } from './MusicHeart'; @@ -24,7 +24,7 @@ const MUSIC_API = '/api/music'; export const MusicPlayerHost = () => { const { token, get, put, delete: del } = useClient(); - const { can } = useCapabilities(); + const { can } = usePermissions(); const canUseMusic = can('music'); const navigate = useNavigate(); const { pathname } = useLocation(); diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx index 1842358d..0c1727cc 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx @@ -1,7 +1,7 @@ import { useMemo, useRef } from 'react'; import { useLocation } from 'react-router'; import { useDock, usePanelFullscreen } from 'officerdev'; -import { useCapabilities } from 'hooks/useCapabilities'; +import { usePermissions } from 'hooks/usePermissions'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import { ScreenErrorFallback } from './ScreenErrorFallback'; import { Background } from './Background'; @@ -15,7 +15,7 @@ type DashboardLayoutProps = { children?: React.ReactNode; }; export function DashboardLayout({ children }: DashboardLayoutProps) { - const { canVisit, plugins } = useCapabilities(); + const { canVisit, plugins } = usePermissions(); // Filtered BEFORE useDock, so a member's saved dock order cannot resurrect an icon their role no longer // reaches, and so the pinned-item defaults fall back to something they can actually open. Cosmetic // either way — every one of these routes is refused server-side too — but an app that offers a door it diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx index 05656b3b..4a61fdff 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { NavLink } from 'react-router'; import type { LucideIcon } from 'lucide-react'; import { resolveIcon } from 'officerdev'; -import type { PluginManifest } from 'hooks/useCapabilities'; +import type { PluginManifest } from 'hooks/usePermissions'; export type DockItem = { label: string; diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Header/JobsIndicator.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Header/JobsIndicator.tsx index 64924d25..e97b5a29 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Header/JobsIndicator.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Header/JobsIndicator.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router'; import { Loader2, ListOrdered } from 'lucide-react'; import { useClient } from 'hooks/useClient'; -import { useCapabilities } from 'hooks/useCapabilities'; +import { usePermissions } from 'hooks/usePermissions'; type Counts = { running: number; runningJobId: string | null; queued: number }; @@ -14,7 +14,7 @@ type Counts = { running: number; runningJobId: string | null; queued: number }; // links to a screen they cannot open. Neither is a security problem; both are the app lying about what it is. export const JobsIndicator = () => { const client = useClient(); - const { can } = useCapabilities(); + const { can } = usePermissions(); const allowed = can('tasks'); const [counts, setCounts] = useState({ running: 0, runningJobId: null, queued: 0 }); diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx index ea4412f5..cc73d7f9 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx @@ -3,7 +3,7 @@ import { RotateCw } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; -import { useCapabilities } from 'hooks/useCapabilities'; +import { usePermissions } from 'hooks/usePermissions'; type RescanResponse = { ok: boolean; counts: Record }; @@ -14,7 +14,7 @@ const ITEM_QUERY_KEYS = ['tasks', 'task-categories', 'skills', 'tools', 'process export function RescanButton() { const client = useClient(); const qc = useQueryClient(); - const { can } = useCapabilities(); + const { can } = usePermissions(); const [loading, setLoading] = useState(false); // `POST /api/rescan` belongs to the `items` capability — skills, tools, agents and processes on the diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/RouteGate.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/RouteGate.tsx index f785b52a..69e59a2d 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/RouteGate.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/RouteGate.tsx @@ -1,5 +1,5 @@ import { Navigate, useLocation } from 'react-router'; -import { useCapabilities } from 'hooks/useCapabilities'; +import { usePermissions } from 'hooks/usePermissions'; // A screen exists only if this server has the thing behind it and this account may reach it. Otherwise the // path is treated exactly as an unknown one: redirect home, same as App.tsx's `path="*"`. @@ -27,7 +27,7 @@ import { useCapabilities } from 'hooks/useCapabilities'; export function RouteGate({ children }: { children?: React.ReactNode }) { const { pathname } = useLocation(); - const { denialReason } = useCapabilities(); + const { denialReason } = usePermissions(); // `replace`, so Back does not bounce between the denied path and home. if (denialReason(pathname)) return ; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/DockSettings.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/DockSettings.tsx index c1604e83..56538248 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/DockSettings.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/DockSettings.tsx @@ -2,7 +2,7 @@ import { useMemo, useState, useCallback, type DragEvent } from 'react'; import { X, Plus, RotateCcw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useDock } from 'officerdev'; -import { useCapabilities } from 'hooks/useCapabilities'; +import { usePermissions } from 'hooks/usePermissions'; import { CORE_DOCK_ITEMS, dockItemsFromPlugins, DEFAULT_DOCK_PATHS } from '@/Screens/Dashboard/Layout/Dock'; type DockPillProps = { @@ -106,7 +106,7 @@ const DropZone = ({ label, children, onDrop }: DropZoneProps) => { export const DockSettings = () => { // Same composition as the dock itself. Offering a pin for an uninstalled feature would let someone // pin a tile that cannot appear, which reads as the setting being broken. - const { plugins } = useCapabilities(); + const { plugins } = usePermissions(); const allDockItems = useMemo(() => [...CORE_DOCK_ITEMS, ...dockItemsFromPlugins(plugins)], [plugins]); const { items, allItems, setItems, reset } = useDock(allDockItems, DEFAULT_DOCK_PATHS); const [dropTarget, setDropTarget] = useState<{ path: string; side: 'left' | 'right' } | null>(null); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/PermissionsSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/PermissionsSection.tsx index 86fbe11b..d820c5d8 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/PermissionsSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/UserManagement/PermissionsSection.tsx @@ -3,18 +3,18 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { Loader2 } from 'lucide-react'; import { useClient } from 'hooks/useClient'; -import { CAPABILITIES_QUERY_KEY } from 'hooks/useCapabilities'; +import { PERMISSIONS_QUERY_KEY } from 'hooks/usePermissions'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -// What each ROLE may reach. Not each user — see the table comment in schema/capabilities.ts for why. +// What each ROLE may reach. Not each user — see the table comment in schema/permissions.ts for why. // -// The screen is one role at a time on purpose. A grid of every role against every capability is the +// The screen is one role at a time on purpose. A grid of every role against every permission is the // obvious design and it is the wrong one: it invites reading across rows, which is not a question anyone // has, and it makes the destructive action ("uncheck Gitea for Members") a single click among fifty. One // role, an explicit Save, and a visible dirty state instead. -type CapabilityInfo = { +type PermissionInfo = { key: string; label: string; description: string; @@ -22,11 +22,11 @@ type CapabilityInfo = { hasPersonalWrites: boolean; }; -type Grant = { role: string; capability: string; level: 'read' | 'write' }; +type Grant = { role: string; permission: string; level: 'read' | 'write' }; -type CapabilitiesResponse = { +type PermissionsResponse = { /** Grantable AND installed. What this server can currently do. */ - capabilities: CapabilityInfo[]; + permissions: PermissionInfo[]; roles: string[]; grants: Grant[]; @@ -43,9 +43,9 @@ export const PermissionsSection = () => { const [draft, setDraft] = useState>({}); const [saving, setSaving] = useState(false); - const { data, isLoading, isError } = useQuery({ + const { data, isLoading, isError } = useQuery({ queryKey: PERMISSIONS_KEY, - queryFn: () => client.get('/users/capabilities'), + queryFn: () => client.get('/users/permissions'), }); const activeRole = role ?? data?.roles[0] ?? null; @@ -53,9 +53,9 @@ export const PermissionsSection = () => { // What the server currently says, for this role. The comparison baseline for the dirty state below. const saved = useMemo(() => { const levels: Record = {}; - for (const capability of data?.capabilities ?? []) levels[capability.key] = 'none'; + for (const permission of data?.permissions ?? []) levels[permission.key] = 'none'; for (const grant of data?.grants ?? []) { - if (grant.role === activeRole) levels[grant.capability] = grant.level; + if (grant.role === activeRole) levels[grant.permission] = grant.level; } return levels; }, [data, activeRole]); @@ -72,12 +72,12 @@ export const PermissionsSection = () => { try { const grants = Object.entries(draft) .filter(([, level]) => level !== 'none') - .map(([capability, level]) => ({ capability, level })); - await client.put(`/users/capabilities/${encodeURIComponent(activeRole)}`, { grants }); + .map(([permission, level]) => ({ permission, level })); + await client.put(`/users/permissions/${encodeURIComponent(activeRole)}`, { grants }); await queryClient.invalidateQueries({ queryKey: PERMISSIONS_KEY }); // The owner may be editing their own view's inputs — and anyone already signed in needs the dock to // catch up without a reload. - await queryClient.invalidateQueries({ queryKey: CAPABILITIES_QUERY_KEY }); + await queryClient.invalidateQueries({ queryKey: PERMISSIONS_QUERY_KEY }); toast.success(`Saved what ${activeRole}s can reach`); } catch (ex) { toast.error(ex instanceof Error ? ex.message : 'Could not save'); @@ -89,12 +89,12 @@ export const PermissionsSection = () => { if (isLoading) { return (
- Loading capabilities… + Loading permissions…
); } if (isError || !data) { - return
Could not load capabilities.
; + return
Could not load permissions.
; } return ( @@ -134,18 +134,18 @@ export const PermissionsSection = () => {

- {data.capabilities.map((capability) => { - const level = draft[capability.key] ?? 'none'; + {data.permissions.map((permission) => { + const level = draft[permission.key] ?? 'none'; return ( -
+
{/* Label only. The descriptions went because with three rows called Terminal, Chat and Files they explained nothing anyone needed — and the "needs a Linux account" line went with them: every account gets one at creation, so warning about it on every row was noise about a state that no longer occurs on its own. */} -
{capability.label}
+
{permission.label}