add SPA config UI, wire media/rss adapters, event-driven layout push

- React SPA: dashboard, data sources CRUD, widgets CRUD, layout builder,
  presets. TanStack Router + Query, shadcn/ui, Vite proxy to :3000
- wire media + rss adapters into polling loop, remove xtb source type
- media adapter: read username/password from headers, proper subsonic auth
- event handler: subscribe to LayoutChanged, push screen update to clients
- fix clippy warnings across workspace (Default impls, collapsible ifs,
  redundant closures, is_none_or, unused imports)
This commit is contained in:
2026-06-19 00:12:42 +02:00
parent 21c08911df
commit 26ebfad3a2
175 changed files with 12338 additions and 801 deletions

View File

@@ -0,0 +1,78 @@
import { Link, useRouterState } from "@tanstack/react-router"
import {
SidebarProvider,
Sidebar,
SidebarContent,
SidebarHeader,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarInset,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { Separator } from "@/components/ui/separator"
import { Toaster } from "@/components/ui/sonner"
import {
LayoutDashboard,
Database,
Box,
Layers,
Save,
} from "lucide-react"
const NAV = [
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
{ to: "/data-sources", label: "Data Sources", icon: Database },
{ to: "/widgets", label: "Widgets", icon: Box },
{ to: "/layout", label: "Layout", icon: Layers },
{ to: "/presets", label: "Presets", icon: Save },
] as const
export function AppShell({ children }: { children: React.ReactNode }) {
const { location } = useRouterState()
return (
<SidebarProvider>
<Sidebar>
<SidebarHeader className="px-4 py-3">
<span className="text-lg font-bold tracking-tight">K-Frame</span>
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
{NAV.map((item) => {
const active =
item.to === "/"
? location.pathname === "/"
: location.pathname.startsWith(item.to)
return (
<SidebarMenuItem key={item.to}>
<SidebarMenuButton asChild isActive={active}>
<Link to={item.to}>
<item.icon />
<span>{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<span className="text-muted-foreground text-sm">
{NAV.find((n) =>
n.to === "/"
? location.pathname === "/"
: location.pathname.startsWith(n.to),
)?.label ?? ""}
</span>
</header>
<main className="flex-1 p-6">{children}</main>
</SidebarInset>
<Toaster />
</SidebarProvider>
)
}