capabilities: the dock a member sees, and the screen the owner grants from
useCapabilities is the frontend's view of the model and explicitly NOT its enforcement — hiding a dock icon is a courtesy, the 403 in origin-validation is the lock. so it fails OPEN: if the request errors the full dock renders. a member clicking through to a 403 is a bad minute; an owner locked out of their own platform by a transient network error is an incident, and the server refuses what it should refuse either way. the endpoint returns held routes AND denied routes, because absence from the held list cannot distinguish a route this account lacks from one no capability claims at all — `/`, the settings shell — and a guard that cannot tell those apart either blanks the app or guards nothing. i wrote the first version without the second list and it silently permitted everything. `can` and `canVisit` are memoised on the query data. a verb rebuilt every render gets a new identity every render, which is how every playback report in the jellyfin player was disabled for days; the dock filter puts one in a useMemo dependency list, so it would have been the same bug. the permissions screen is one role at a time, with an explicit save and a dirty state, rather than a roles-by-capabilities grid — a grid invites reading across rows, which is not a question anyone has, and makes revoking gitea for every member one click among fifty. it also states plainly why terminal, chat, files and the rest are absent, so their absence reads as a decision rather than as a missing feature. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useRef } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useDock, MusicPlayerHost } from 'officerdev';
|
||||
import { useCapabilities } from 'hooks/useCapabilities';
|
||||
import { Background } from './Background';
|
||||
import { Header } from './Header';
|
||||
import { Dock, ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS } from './Dock';
|
||||
@@ -10,7 +11,13 @@ type DashboardLayoutProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const { items: visibleItems } = useDock(ALL_DOCK_ITEMS, DEFAULT_DOCK_PATHS);
|
||||
const { canVisit } = useCapabilities();
|
||||
// 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
|
||||
// will then slam is worse than one that never showed it.
|
||||
const permitted = useMemo(() => ALL_DOCK_ITEMS.filter((item) => canVisit(item.to)), [canVisit]);
|
||||
const { items: visibleItems } = useDock(permitted, DEFAULT_DOCK_PATHS);
|
||||
const isTouch = useIsTouch();
|
||||
usePageTitleSync();
|
||||
// The content region shrinks when the (in-flow) music dock takes its space; the nav dock measures its
|
||||
|
||||
Reference in New Issue
Block a user