diff --git a/js/web/src/routeTree.gen.ts b/js/web/src/routeTree.gen.ts
index 8aaec9b2..045f1d97 100644
--- a/js/web/src/routeTree.gen.ts
+++ b/js/web/src/routeTree.gen.ts
@@ -13,6 +13,7 @@ import { Route as VideosRouteImport } from './routes/videos'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as SearchRouteImport } from './routes/search'
import { Route as LoginRouteImport } from './routes/login'
+import { Route as DashboardRouteRouteImport } from './routes/dashboard/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SettingsIndexRouteImport } from './routes/settings/index'
import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
@@ -61,6 +62,11 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
+const DashboardRouteRoute = DashboardRouteRouteImport.update({
+ id: '/dashboard',
+ path: '/dashboard',
+ getParentRoute: () => rootRouteImport,
+} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -72,9 +78,9 @@ const SettingsIndexRoute = SettingsIndexRouteImport.update({
getParentRoute: () => SettingsRoute,
} as any)
const DashboardIndexRoute = DashboardIndexRouteImport.update({
- id: '/dashboard/',
- path: '/dashboard/',
- getParentRoute: () => rootRouteImport,
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const UserIndexRoute = UserIndexRouteImport.update({
id: '/$user/',
@@ -132,14 +138,14 @@ const SettingsAboutRoute = SettingsAboutRouteImport.update({
getParentRoute: () => SettingsRoute,
} as any)
const DashboardVideosRoute = DashboardVideosRouteImport.update({
- id: '/dashboard/videos',
- path: '/dashboard/videos',
- getParentRoute: () => rootRouteImport,
+ id: '/videos',
+ path: '/videos',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardUploadRoute = DashboardUploadRouteImport.update({
- id: '/dashboard/upload',
- path: '/dashboard/upload',
- getParentRoute: () => rootRouteImport,
+ id: '/upload',
+ path: '/upload',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const ChatPopoutUserRoute = ChatPopoutUserRouteImport.update({
id: '/chat-popout/$user',
@@ -152,31 +158,31 @@ const EmbedUserIndexRoute = EmbedUserIndexRouteImport.update({
getParentRoute: () => rootRouteImport,
} as any)
const DashboardWebhooksIndexRoute = DashboardWebhooksIndexRouteImport.update({
- id: '/dashboard/webhooks/',
- path: '/dashboard/webhooks/',
- getParentRoute: () => rootRouteImport,
+ id: '/webhooks/',
+ path: '/webhooks/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardStreamIndexRoute = DashboardStreamIndexRouteImport.update({
- id: '/dashboard/stream/',
- path: '/dashboard/stream/',
- getParentRoute: () => rootRouteImport,
+ id: '/stream/',
+ path: '/stream/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardRecommendationsIndexRoute =
DashboardRecommendationsIndexRouteImport.update({
- id: '/dashboard/recommendations/',
- path: '/dashboard/recommendations/',
- getParentRoute: () => rootRouteImport,
+ id: '/recommendations/',
+ path: '/recommendations/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardMultistreamIndexRoute =
DashboardMultistreamIndexRouteImport.update({
- id: '/dashboard/multistream/',
- path: '/dashboard/multistream/',
- getParentRoute: () => rootRouteImport,
+ id: '/multistream/',
+ path: '/multistream/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardKeysIndexRoute = DashboardKeysIndexRouteImport.update({
- id: '/dashboard/keys/',
- path: '/dashboard/keys/',
- getParentRoute: () => rootRouteImport,
+ id: '/keys/',
+ path: '/keys/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const EmbedInfoWidgetUserRoute = EmbedInfoWidgetUserRouteImport.update({
id: '/embed/info-widget/$user',
@@ -201,6 +207,7 @@ const EmbedUserVideoTidRoute = EmbedUserVideoTidRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
+ '/dashboard': typeof DashboardRouteRouteWithChildren
'/login': typeof LoginRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRouteWithChildren
@@ -267,6 +274,7 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
+ '/dashboard': typeof DashboardRouteRouteWithChildren
'/login': typeof LoginRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRouteWithChildren
@@ -302,6 +310,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
+ | '/dashboard'
| '/login'
| '/search'
| '/settings'
@@ -367,6 +376,7 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/'
+ | '/dashboard'
| '/login'
| '/search'
| '/settings'
@@ -401,23 +411,16 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
+ DashboardRouteRoute: typeof DashboardRouteRouteWithChildren
LoginRoute: typeof LoginRoute
SearchRoute: typeof SearchRoute
SettingsRoute: typeof SettingsRouteWithChildren
VideosRoute: typeof VideosRoute
ChatPopoutUserRoute: typeof ChatPopoutUserRoute
- DashboardUploadRoute: typeof DashboardUploadRoute
- DashboardVideosRoute: typeof DashboardVideosRoute
UserIndexRoute: typeof UserIndexRoute
- DashboardIndexRoute: typeof DashboardIndexRoute
UserVideoTidRoute: typeof UserVideoTidRoute
EmbedDanmuObsUserRoute: typeof EmbedDanmuObsUserRoute
EmbedInfoWidgetUserRoute: typeof EmbedInfoWidgetUserRoute
- DashboardKeysIndexRoute: typeof DashboardKeysIndexRoute
- DashboardMultistreamIndexRoute: typeof DashboardMultistreamIndexRoute
- DashboardRecommendationsIndexRoute: typeof DashboardRecommendationsIndexRoute
- DashboardStreamIndexRoute: typeof DashboardStreamIndexRoute
- DashboardWebhooksIndexRoute: typeof DashboardWebhooksIndexRoute
EmbedUserIndexRoute: typeof EmbedUserIndexRoute
EmbedUserVideoTidRoute: typeof EmbedUserVideoTidRoute
}
@@ -452,6 +455,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
+ '/dashboard': {
+ id: '/dashboard'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof DashboardRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/': {
id: '/'
path: '/'
@@ -468,10 +478,10 @@ declare module '@tanstack/react-router' {
}
'/dashboard/': {
id: '/dashboard/'
- path: '/dashboard'
+ path: '/'
fullPath: '/dashboard/'
preLoaderRoute: typeof DashboardIndexRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/$user/': {
id: '/$user/'
@@ -552,17 +562,17 @@ declare module '@tanstack/react-router' {
}
'/dashboard/videos': {
id: '/dashboard/videos'
- path: '/dashboard/videos'
+ path: '/videos'
fullPath: '/dashboard/videos'
preLoaderRoute: typeof DashboardVideosRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/dashboard/upload': {
id: '/dashboard/upload'
- path: '/dashboard/upload'
+ path: '/upload'
fullPath: '/dashboard/upload'
preLoaderRoute: typeof DashboardUploadRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/chat-popout/$user': {
id: '/chat-popout/$user'
@@ -580,38 +590,38 @@ declare module '@tanstack/react-router' {
}
'/dashboard/webhooks/': {
id: '/dashboard/webhooks/'
- path: '/dashboard/webhooks'
+ path: '/webhooks'
fullPath: '/dashboard/webhooks/'
preLoaderRoute: typeof DashboardWebhooksIndexRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/dashboard/stream/': {
id: '/dashboard/stream/'
- path: '/dashboard/stream'
+ path: '/stream'
fullPath: '/dashboard/stream/'
preLoaderRoute: typeof DashboardStreamIndexRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/dashboard/recommendations/': {
id: '/dashboard/recommendations/'
- path: '/dashboard/recommendations'
+ path: '/recommendations'
fullPath: '/dashboard/recommendations/'
preLoaderRoute: typeof DashboardRecommendationsIndexRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/dashboard/multistream/': {
id: '/dashboard/multistream/'
- path: '/dashboard/multistream'
+ path: '/multistream'
fullPath: '/dashboard/multistream/'
preLoaderRoute: typeof DashboardMultistreamIndexRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/dashboard/keys/': {
id: '/dashboard/keys/'
- path: '/dashboard/keys'
+ path: '/keys'
fullPath: '/dashboard/keys/'
preLoaderRoute: typeof DashboardKeysIndexRouteImport
- parentRoute: typeof rootRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/embed/info-widget/$user': {
id: '/embed/info-widget/$user'
@@ -644,6 +654,32 @@ declare module '@tanstack/react-router' {
}
}
+interface DashboardRouteRouteChildren {
+ DashboardUploadRoute: typeof DashboardUploadRoute
+ DashboardVideosRoute: typeof DashboardVideosRoute
+ DashboardIndexRoute: typeof DashboardIndexRoute
+ DashboardKeysIndexRoute: typeof DashboardKeysIndexRoute
+ DashboardMultistreamIndexRoute: typeof DashboardMultistreamIndexRoute
+ DashboardRecommendationsIndexRoute: typeof DashboardRecommendationsIndexRoute
+ DashboardStreamIndexRoute: typeof DashboardStreamIndexRoute
+ DashboardWebhooksIndexRoute: typeof DashboardWebhooksIndexRoute
+}
+
+const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
+ DashboardUploadRoute: DashboardUploadRoute,
+ DashboardVideosRoute: DashboardVideosRoute,
+ DashboardIndexRoute: DashboardIndexRoute,
+ DashboardKeysIndexRoute: DashboardKeysIndexRoute,
+ DashboardMultistreamIndexRoute: DashboardMultistreamIndexRoute,
+ DashboardRecommendationsIndexRoute: DashboardRecommendationsIndexRoute,
+ DashboardStreamIndexRoute: DashboardStreamIndexRoute,
+ DashboardWebhooksIndexRoute: DashboardWebhooksIndexRoute,
+}
+
+const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren(
+ DashboardRouteRouteChildren,
+)
+
interface SettingsRouteChildren {
SettingsAboutRoute: typeof SettingsAboutRoute
SettingsAccountRoute: typeof SettingsAccountRoute
@@ -678,23 +714,16 @@ const SettingsRouteWithChildren = SettingsRoute._addFileChildren(
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ DashboardRouteRoute: DashboardRouteRouteWithChildren,
LoginRoute: LoginRoute,
SearchRoute: SearchRoute,
SettingsRoute: SettingsRouteWithChildren,
VideosRoute: VideosRoute,
ChatPopoutUserRoute: ChatPopoutUserRoute,
- DashboardUploadRoute: DashboardUploadRoute,
- DashboardVideosRoute: DashboardVideosRoute,
UserIndexRoute: UserIndexRoute,
- DashboardIndexRoute: DashboardIndexRoute,
UserVideoTidRoute: UserVideoTidRoute,
EmbedDanmuObsUserRoute: EmbedDanmuObsUserRoute,
EmbedInfoWidgetUserRoute: EmbedInfoWidgetUserRoute,
- DashboardKeysIndexRoute: DashboardKeysIndexRoute,
- DashboardMultistreamIndexRoute: DashboardMultistreamIndexRoute,
- DashboardRecommendationsIndexRoute: DashboardRecommendationsIndexRoute,
- DashboardStreamIndexRoute: DashboardStreamIndexRoute,
- DashboardWebhooksIndexRoute: DashboardWebhooksIndexRoute,
EmbedUserIndexRoute: EmbedUserIndexRoute,
EmbedUserVideoTidRoute: EmbedUserVideoTidRoute,
}
diff --git a/js/web/src/routes/__root.tsx b/js/web/src/routes/__root.tsx
index 3f510659..bf569b7f 100644
--- a/js/web/src/routes/__root.tsx
+++ b/js/web/src/routes/__root.tsx
@@ -1,4 +1,3 @@
-import DashboardChrome from "@/components/dashboard/dashboard-chrome";
import Header from "@/components/header";
import SidebarComponent from "@/components/sidebar";
import { TooltipProvider } from "@/components/ui/tooltip";
@@ -20,9 +19,6 @@ import i18next from "../lib/i18n";
/** Routes that should render without sidebar/header chrome. */
const POPOUT_PREFIXES = ["/chat-popout/", "/embed/"];
-/** Routes that should render the dashboard's own chrome, separate from the main app. */
-const DASHBOARD_PREFIX = "/dashboard";
-
export const Route = createRootRoute({
component: RootLayout,
pendingComponent: RouteLoadingSkeleton,
@@ -83,6 +79,14 @@ function RouteLoadingSkeleton() {
}
function RootLayout() {
+ // Popout routes (chat popout, embeds) skip the regular chrome and
+ // render their own minimal layout. Everything else gets the full
+ // provider tree; the chrome decision (regular vs dashboard) is now
+ // owned by route layouts — see routes/dashboard/route.tsx for the
+ // dashboard chrome layout, and the non-dashboard routes get the
+ // regular chrome via ChromeLayout below. The root no longer switches
+ // chrome based on pathname, which closed a render race where the
+ // dashboard route was mounted without its DashboardStoreContext.
const pathname = useRouterState({
select: (s) => s.resolvedLocation?.pathname ?? "",
});
@@ -92,28 +96,12 @@ function RootLayout() {
const actualPathname = pathname || browserPathname;
- const isPopout = POPOUT_PREFIXES.some((p) => actualPathname.startsWith(p));
- const isDashboard =
- actualPathname === DASHBOARD_PREFIX ||
- actualPathname.startsWith(`${DASHBOARD_PREFIX}/`);
-
- console.log(
- "Rendering RootLayout, browser:",
- actualPathname,
- "router: ",
- pathname,
- "isPopout:",
- isPopout,
- isDashboard,
- );
-
// pause until we can get a valid pathname, to avoid rendering the wrong chrome on initial load
if (!actualPathname) {
return ;
}
- // Popout routes get no chrome — just providers and the outlet.
- if (isPopout) {
+ if (POPOUT_PREFIXES.some((p) => actualPathname.startsWith(p))) {
return (
@@ -123,24 +111,6 @@ function RootLayout() {
);
}
- // Dashboard routes get their own chrome, separate from the main app.
- // See /dashboard/index.tsx and components/dashboard/dashboard-chrome.tsx for details.
- if (isDashboard) {
- // if no pathname from router, we aren't ready yet
- if (!pathname) {
- return ;
- }
- return (
-
-
-
-
-
-
-
- );
- }
-
return (
diff --git a/js/web/src/routes/dashboard/route.tsx b/js/web/src/routes/dashboard/route.tsx
new file mode 100644
index 00000000..08ecab1c
--- /dev/null
+++ b/js/web/src/routes/dashboard/route.tsx
@@ -0,0 +1,17 @@
+import DashboardChrome from "@/components/dashboard/dashboard-chrome";
+import { createFileRoute } from "@tanstack/react-router";
+
+// Layout route for everything under /dashboard. DashboardChrome renders
+// its own for the child route, and provides the
+// DashboardStoreContext, LivestreamProvider, DashboardMetricsProvider,
+// and the dashboard's own SidebarProvider.
+//
+// Having this as a real layout route (instead of conditionally rendering
+// DashboardChrome in __root.tsx based on the current pathname) avoids a
+// render race: the dashboard child route's `useDashboardStore()` call
+// was throwing because the dashboard chrome wasn't always mounted at the
+// time the child was rendered. With a layout route, the chrome and the
+// provider tree are guaranteed to be in place before the child renders.
+export const Route = createFileRoute("/dashboard")({
+ component: DashboardChrome,
+});