Loading panel/src/App.tsx +7 −6 Changes for panel/src/App.tsx: 7 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -10,10 +10,8 @@ import { Dashboard } from "@/pages/Dashboard"; import { MyServers } from "@/pages/MyServers"; import { ServerConsole } from "@/pages/ServerConsole"; import { Account } from "@/pages/Account"; import { AdminHome } from "@/pages/admin/AdminHome"; import { ServerAdmin } from "@/pages/admin/ServerAdmin"; import { ImageAdmin } from "@/pages/admin/ImageAdmin"; import { OpsOverview } from "@/pages/ops/OpsOverview"; import { FleetTable } from "@/pages/ops/FleetTable"; // Three UX surfaces over two Zero-Trust tiers (DESIGN-WEB-3SIDES): Loading Loading @@ -45,16 +43,19 @@ export default function App() { <Route path="servers/:name" element={<ServerConsole />} /> <Route path="account" element={<Account />} /> {/* Admin-Side — admin-tier (server & content ops) */} {/* Admin-Side — admin-tier (server & content ops). /admin has no landing page; redirect to the first concrete view so the section root and any stale bookmarks land somewhere useful. */} <Route path="admin" element={<RequireAdmin />}> <Route index element={<AdminHome />} /> <Route index element={<Navigate to="/admin/servers" replace />} /> <Route path="servers" element={<ServerAdmin />} /> <Route path="images" element={<ImageAdmin />} /> </Route> {/* SysAdmin-Side — admin-tier (platform observability) */} {/* SysAdmin-Side — admin-tier (platform observability). Same redirect treatment as /admin — fleet is the meaningful view. */} <Route path="ops" element={<RequireAdmin />}> <Route index element={<OpsOverview />} /> <Route index element={<Navigate to="/ops/fleet" replace />} /> <Route path="fleet" element={<FleetTable />} /> </Route> Loading panel/src/components/AppShell.tsx +43 −28 Changes for panel/src/components/AppShell.tsx: 43 added lines, 28 removed lines. Original line number Diff line number Diff line Loading @@ -5,10 +5,49 @@ import { useTranslation } from "react-i18next"; import * as SelectPrimitive from "@radix-ui/react-select"; import { cn } from "@/lib/utils"; import { useTier } from "@/lib/tier"; import { visibleSections } from "@/lib/nav"; import { visibleSections, type NavSection } from "@/lib/nav"; import { Select, SelectContent, SelectItem } from "@/components/ui/select"; import { useTheme } from "@/lib/theme"; function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: boolean }) { const { t } = useTranslation("navigation"); return ( <div className={cn( "flex flex-col gap-0.5", // Sections after the first get a thin divider with breathing room above, // which reads as a structural break without needing a loud heading. !isFirst && "mt-3 border-t border-border/50 pt-3", )} > {section.titleKey && ( <div className="mb-1 px-3 text-[11px] font-medium text-muted-foreground/70"> {t(section.titleKey)} </div> )} {section.items.map((item) => ( <NavLink key={item.to} to={item.to} end={item.end} className={({ isActive }) => cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", isActive ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-accent hover:text-foreground", ) } > <item.icon className="h-4 w-4" /> {t(item.key)} </NavLink> ))} </div> ); } function LangToggle() { const { i18n } = useTranslation(); Loading Loading @@ -67,33 +106,9 @@ export function AppShell() { <span className="text-lg font-semibold tracking-tight">{t("common:brand_name")}</span> </div> <nav className="flex flex-col gap-4"> {sections.map((section) => ( <div key={section.id} className="flex flex-col gap-1"> {section.titleKey && ( <div className="px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70"> {t(section.titleKey)} </div> )} {section.items.map((item) => ( <NavLink key={item.to} to={item.to} end={item.end} className={({ isActive }) => cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", isActive ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-accent hover:text-foreground", ) } > <item.icon className="h-4 w-4" /> {t(item.key)} </NavLink> ))} </div> <nav className="flex flex-col"> {sections.map((section, i) => ( <SectionGroup key={section.id} section={section} isFirst={i === 0} /> ))} </nav> Loading panel/src/i18n/resources/en-US/navigation.json +0 −2 Changes for panel/src/i18n/resources/en-US/navigation.json: 0 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -3,10 +3,8 @@ "my_servers": "My servers", "account": "Account", "admin_section": "Admin", "admin_overview": "Overview", "admin_servers": "Servers", "admin_images": "Images", "sysadmin_section": "SysAdmin", "sysadmin_overview": "Overview", "sysadmin_fleet": "Fleet" } panel/src/i18n/resources/zh-CN/navigation.json +0 −2 Changes for panel/src/i18n/resources/zh-CN/navigation.json: 0 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -3,10 +3,8 @@ "my_servers": "我的服务器", "account": "账户", "admin_section": "管理", "admin_overview": "概览", "admin_servers": "服务器", "admin_images": "镜像", "sysadmin_section": "系统管理", "sysadmin_overview": "概览", "sysadmin_fleet": "全平台服务器" } panel/src/lib/nav.ts +1 −5 Changes for panel/src/lib/nav.ts: 1 added line, 5 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,8 @@ import { LayoutDashboard, Server, UserRound, ShieldCheck, ServerCog, Boxes, Gauge, Network, type LucideIcon, } from "lucide-react"; Loading @@ -31,7 +29,7 @@ export interface NavItem { export interface NavSection { id: "user" | "admin" | "ops"; /** Section heading key; null renders no heading (User-Side). */ /** Section heading key; null renders no heading (User-Side flat list). */ titleKey: string | null; /** When true the section is shown only to admins (is_admin === true). */ adminOnly: boolean; Loading @@ -54,7 +52,6 @@ export const NAV_SECTIONS: NavSection[] = [ titleKey: "admin_section", adminOnly: true, items: [ { to: "/admin", key: "admin_overview", icon: ShieldCheck, end: true }, { to: "/admin/servers", key: "admin_servers", icon: ServerCog }, { to: "/admin/images", key: "admin_images", icon: Boxes }, ], Loading @@ -64,7 +61,6 @@ export const NAV_SECTIONS: NavSection[] = [ titleKey: "sysadmin_section", adminOnly: true, items: [ { to: "/ops", key: "sysadmin_overview", icon: Gauge, end: true }, { to: "/ops/fleet", key: "sysadmin_fleet", icon: Network }, ], }, Loading Loading
panel/src/App.tsx +7 −6 Changes for panel/src/App.tsx: 7 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -10,10 +10,8 @@ import { Dashboard } from "@/pages/Dashboard"; import { MyServers } from "@/pages/MyServers"; import { ServerConsole } from "@/pages/ServerConsole"; import { Account } from "@/pages/Account"; import { AdminHome } from "@/pages/admin/AdminHome"; import { ServerAdmin } from "@/pages/admin/ServerAdmin"; import { ImageAdmin } from "@/pages/admin/ImageAdmin"; import { OpsOverview } from "@/pages/ops/OpsOverview"; import { FleetTable } from "@/pages/ops/FleetTable"; // Three UX surfaces over two Zero-Trust tiers (DESIGN-WEB-3SIDES): Loading Loading @@ -45,16 +43,19 @@ export default function App() { <Route path="servers/:name" element={<ServerConsole />} /> <Route path="account" element={<Account />} /> {/* Admin-Side — admin-tier (server & content ops) */} {/* Admin-Side — admin-tier (server & content ops). /admin has no landing page; redirect to the first concrete view so the section root and any stale bookmarks land somewhere useful. */} <Route path="admin" element={<RequireAdmin />}> <Route index element={<AdminHome />} /> <Route index element={<Navigate to="/admin/servers" replace />} /> <Route path="servers" element={<ServerAdmin />} /> <Route path="images" element={<ImageAdmin />} /> </Route> {/* SysAdmin-Side — admin-tier (platform observability) */} {/* SysAdmin-Side — admin-tier (platform observability). Same redirect treatment as /admin — fleet is the meaningful view. */} <Route path="ops" element={<RequireAdmin />}> <Route index element={<OpsOverview />} /> <Route index element={<Navigate to="/ops/fleet" replace />} /> <Route path="fleet" element={<FleetTable />} /> </Route> Loading
panel/src/components/AppShell.tsx +43 −28 Changes for panel/src/components/AppShell.tsx: 43 added lines, 28 removed lines. Original line number Diff line number Diff line Loading @@ -5,10 +5,49 @@ import { useTranslation } from "react-i18next"; import * as SelectPrimitive from "@radix-ui/react-select"; import { cn } from "@/lib/utils"; import { useTier } from "@/lib/tier"; import { visibleSections } from "@/lib/nav"; import { visibleSections, type NavSection } from "@/lib/nav"; import { Select, SelectContent, SelectItem } from "@/components/ui/select"; import { useTheme } from "@/lib/theme"; function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: boolean }) { const { t } = useTranslation("navigation"); return ( <div className={cn( "flex flex-col gap-0.5", // Sections after the first get a thin divider with breathing room above, // which reads as a structural break without needing a loud heading. !isFirst && "mt-3 border-t border-border/50 pt-3", )} > {section.titleKey && ( <div className="mb-1 px-3 text-[11px] font-medium text-muted-foreground/70"> {t(section.titleKey)} </div> )} {section.items.map((item) => ( <NavLink key={item.to} to={item.to} end={item.end} className={({ isActive }) => cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", isActive ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-accent hover:text-foreground", ) } > <item.icon className="h-4 w-4" /> {t(item.key)} </NavLink> ))} </div> ); } function LangToggle() { const { i18n } = useTranslation(); Loading Loading @@ -67,33 +106,9 @@ export function AppShell() { <span className="text-lg font-semibold tracking-tight">{t("common:brand_name")}</span> </div> <nav className="flex flex-col gap-4"> {sections.map((section) => ( <div key={section.id} className="flex flex-col gap-1"> {section.titleKey && ( <div className="px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70"> {t(section.titleKey)} </div> )} {section.items.map((item) => ( <NavLink key={item.to} to={item.to} end={item.end} className={({ isActive }) => cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors", isActive ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-accent hover:text-foreground", ) } > <item.icon className="h-4 w-4" /> {t(item.key)} </NavLink> ))} </div> <nav className="flex flex-col"> {sections.map((section, i) => ( <SectionGroup key={section.id} section={section} isFirst={i === 0} /> ))} </nav> Loading
panel/src/i18n/resources/en-US/navigation.json +0 −2 Changes for panel/src/i18n/resources/en-US/navigation.json: 0 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -3,10 +3,8 @@ "my_servers": "My servers", "account": "Account", "admin_section": "Admin", "admin_overview": "Overview", "admin_servers": "Servers", "admin_images": "Images", "sysadmin_section": "SysAdmin", "sysadmin_overview": "Overview", "sysadmin_fleet": "Fleet" }
panel/src/i18n/resources/zh-CN/navigation.json +0 −2 Changes for panel/src/i18n/resources/zh-CN/navigation.json: 0 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -3,10 +3,8 @@ "my_servers": "我的服务器", "account": "账户", "admin_section": "管理", "admin_overview": "概览", "admin_servers": "服务器", "admin_images": "镜像", "sysadmin_section": "系统管理", "sysadmin_overview": "概览", "sysadmin_fleet": "全平台服务器" }
panel/src/lib/nav.ts +1 −5 Changes for panel/src/lib/nav.ts: 1 added line, 5 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,8 @@ import { LayoutDashboard, Server, UserRound, ShieldCheck, ServerCog, Boxes, Gauge, Network, type LucideIcon, } from "lucide-react"; Loading @@ -31,7 +29,7 @@ export interface NavItem { export interface NavSection { id: "user" | "admin" | "ops"; /** Section heading key; null renders no heading (User-Side). */ /** Section heading key; null renders no heading (User-Side flat list). */ titleKey: string | null; /** When true the section is shown only to admins (is_admin === true). */ adminOnly: boolean; Loading @@ -54,7 +52,6 @@ export const NAV_SECTIONS: NavSection[] = [ titleKey: "admin_section", adminOnly: true, items: [ { to: "/admin", key: "admin_overview", icon: ShieldCheck, end: true }, { to: "/admin/servers", key: "admin_servers", icon: ServerCog }, { to: "/admin/images", key: "admin_images", icon: Boxes }, ], Loading @@ -64,7 +61,6 @@ export const NAV_SECTIONS: NavSection[] = [ titleKey: "sysadmin_section", adminOnly: true, items: [ { to: "/ops", key: "sysadmin_overview", icon: Gauge, end: true }, { to: "/ops/fleet", key: "sysadmin_fleet", icon: Network }, ], }, Loading