Officer is single-user: the server owner is the only account, created once by /auth/bootstrap. Everything that existed to serve additional users was unreachable, so it is gone rather than left looking like it does something. Accounts: drop the invite / resend-invite / delete / list-users routes and the Users settings screen, the inert /auth/signup handler, and the account verification chain it fed (verify, resend-verification, VerifyScreen, the UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token survives for password resets only, and now requires a reset-password token rather than accepting any signed JWT. Roles: drop the users.role column and the four-value USER_ROLES enum. The permissions table granted every role identical methods, and every role === 'Super Admin' check was permanently true. The JWT no longer carries a role claim. Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected only for non-Super-Admin users, so it never ran. It was also not a usable agent jail as written — --share-net, the project root (with .env) bound read-only, and runuser dropping to the server's own uid. Rebuilding it for agent containment would be a different construction, and git history keeps this one. getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the owner's real login home, which is what terminals, chats and task runs use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { Link, useLocation } from 'react-router';
|
|
import type { LucideIcon } from 'lucide-react';
|
|
|
|
export type DockItem = {
|
|
label: string;
|
|
to: string;
|
|
icon: LucideIcon;
|
|
color: string;
|
|
};
|
|
|
|
type DockProps = {
|
|
items: DockItem[];
|
|
className?: string;
|
|
};
|
|
|
|
const ICON_SIZE = 48;
|
|
const ICON_GAP = 24;
|
|
const DOCK_PADDING = 12;
|
|
const MAX_SCALE = 1.5;
|
|
const MAX_DISTANCE = 150;
|
|
const SHOW_THRESHOLD = 24;
|
|
const HIDE_THRESHOLD = 100;
|
|
|
|
const getScale = (mouseX: number | null, iconCenterX: number) => {
|
|
if (mouseX === null) return 1;
|
|
const distance = Math.abs(mouseX - iconCenterX);
|
|
if (distance > MAX_DISTANCE) return 1;
|
|
return 1 + (MAX_SCALE - 1) * Math.cos((distance / MAX_DISTANCE) * (Math.PI / 2));
|
|
};
|
|
|
|
export const Dock = ({ items, className }: DockProps) => {
|
|
const [mouseX, setMouseX] = useState<number | null>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
const dockRef = useRef<HTMLDivElement | null>(null);
|
|
const location = useLocation();
|
|
|
|
const isActive = (to: string) => (to === '/' ? location.pathname === '/' : location.pathname.startsWith(to));
|
|
|
|
useEffect(() => {
|
|
const handleMouseMove = (ev: MouseEvent) => {
|
|
const distFromBottom = window.innerHeight - ev.clientY;
|
|
setVisible((prev) => {
|
|
if (!prev) return distFromBottom <= SHOW_THRESHOLD;
|
|
return distFromBottom <= HIDE_THRESHOLD;
|
|
});
|
|
if (distFromBottom <= HIDE_THRESHOLD) {
|
|
const rect = dockRef.current?.getBoundingClientRect();
|
|
if (rect) setMouseX(ev.clientX - rect.left);
|
|
} else {
|
|
setMouseX(null);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
return () => document.removeEventListener('mousemove', handleMouseMove);
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={dockRef}
|
|
className={`fixed left-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg transition-transform duration-300 ease-in-out ${className ?? 'flex'}`}
|
|
style={{
|
|
backgroundColor: 'var(--dock-bg)',
|
|
borderColor: 'var(--dock-border)',
|
|
bottom: '16px',
|
|
transform: `translateX(-50%) translateY(${visible ? '0' : 'calc(100% + 24px)'})`,
|
|
}}
|
|
>
|
|
{items.map((item, index) => {
|
|
const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2;
|
|
const scale = getScale(mouseX, iconCenter);
|
|
const active = isActive(item.to);
|
|
|
|
return (
|
|
<Link
|
|
key={item.to}
|
|
to={item.to}
|
|
className="group relative flex flex-col items-center"
|
|
style={{
|
|
transform: `scale(${scale})`,
|
|
transformOrigin: 'bottom center',
|
|
transition: 'transform 150ms ease-out',
|
|
}}
|
|
>
|
|
<span
|
|
className="absolute -top-9 px-2 py-1 rounded-md text-white text-xs whitespace-nowrap hidden md:block opacity-0 group-hover:opacity-100 transition-opacity duration-150 pointer-events-none"
|
|
style={{ backgroundColor: 'var(--dock-tooltip-bg)' }}
|
|
>
|
|
{item.label}
|
|
</span>
|
|
<div
|
|
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
|
|
style={{
|
|
background: item.color,
|
|
boxShadow: active ? `0 0 12px ${item.color}40` : 'none',
|
|
}}
|
|
>
|
|
<item.icon className="h-5 w-5 md:h-6 md:w-6 text-white" />
|
|
</div>
|
|
{active && (
|
|
<div className="absolute -bottom-1.5 w-1.5 h-1.5 rounded-full" style={{ background: item.color }} />
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
import {
|
|
Home,
|
|
MessageCircle,
|
|
FileText,
|
|
FolderOpen,
|
|
Code,
|
|
LayoutGrid,
|
|
ScrollText,
|
|
FolderKanban,
|
|
Monitor,
|
|
Mail,
|
|
Globe,
|
|
MonitorSmartphone,
|
|
Workflow,
|
|
} from 'lucide-react';
|
|
|
|
export const ALL_DOCK_ITEMS: DockItem[] = [
|
|
{ label: 'Home', to: '/', icon: Home, color: '#f59e0b' },
|
|
{ label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' },
|
|
{ label: 'Email', to: '/email', icon: Mail, color: '#ef4444' },
|
|
{ label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
|
|
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
|
|
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
|
|
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
|
|
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
|
|
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
|
|
{ label: 'Projects', to: '/projects', icon: FolderKanban, color: '#10b981' },
|
|
{ label: 'Browser', to: '/browser', icon: Globe, color: '#06b6d4' },
|
|
{ label: 'Desktop', to: '/desktop', icon: MonitorSmartphone, color: '#ec4899' },
|
|
{ label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' },
|
|
];
|
|
|
|
export const DEFAULT_DOCK_PATHS = ['/', '/files', '/projects', '/dashboards', '/chat'];
|