diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index 8af35bca..d47af808 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -28,6 +28,7 @@ import { NewAgent } from "./pages/NewAgent";
import { AuthPage } from "./pages/Auth";
import { BoardClaimPage } from "./pages/BoardClaim";
import { InviteLandingPage } from "./pages/InviteLanding";
+import { NotFoundPage } from "./pages/NotFound";
import { queryKeys } from "./lib/queryKeys";
import { useCompany } from "./context/CompanyContext";
import { useDialog } from "./context/DialogContext";
@@ -141,6 +142,7 @@ function boardRoutes() {
} />
} />
} />
+ } />
>
);
}
@@ -240,6 +242,7 @@ export function App() {
}>
{boardRoutes()}
+ } />
diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx
index 9c272b64..3a58e614 100644
--- a/ui/src/components/Layout.tsx
+++ b/ui/src/components/Layout.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState, type UIEvent } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState, type UIEvent } from "react";
import { useQuery } from "@tanstack/react-query";
import { BookOpen, Moon, Sun } from "lucide-react";
import { Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
@@ -24,13 +24,20 @@ import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
+import { NotFoundPage } from "../pages/NotFound";
import { Button } from "@/components/ui/button";
export function Layout() {
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
const { openNewIssue, openOnboarding } = useDialog();
const { togglePanelVisible } = usePanel();
- const { companies, loading: companiesLoading, selectedCompanyId, setSelectedCompanyId } = useCompany();
+ const {
+ companies,
+ loading: companiesLoading,
+ selectedCompany,
+ selectedCompanyId,
+ setSelectedCompanyId,
+ } = useCompany();
const { theme, toggleTheme } = useTheme();
const { companyPrefix } = useParams<{ companyPrefix: string }>();
const navigate = useNavigate();
@@ -39,6 +46,13 @@ export function Layout() {
const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true);
const nextTheme = theme === "dark" ? "light" : "dark";
+ const matchedCompany = useMemo(() => {
+ if (!companyPrefix) return null;
+ const requestedPrefix = companyPrefix.toUpperCase();
+ return companies.find((company) => company.issuePrefix.toUpperCase() === requestedPrefix) ?? null;
+ }, [companies, companyPrefix]);
+ const hasUnknownCompanyPrefix =
+ Boolean(companyPrefix) && !companiesLoading && companies.length > 0 && !matchedCompany;
const { data: health } = useQuery({
queryKey: queryKeys.health,
queryFn: () => healthApi.get(),
@@ -57,30 +71,30 @@ export function Layout() {
useEffect(() => {
if (!companyPrefix || companiesLoading || companies.length === 0) return;
- const requestedPrefix = companyPrefix.toUpperCase();
- const matched = companies.find((company) => company.issuePrefix.toUpperCase() === requestedPrefix);
-
- if (!matched) {
- const fallback =
- (selectedCompanyId ? companies.find((company) => company.id === selectedCompanyId) : null)
- ?? companies[0]!;
- navigate(`/${fallback.issuePrefix}/dashboard`, { replace: true });
+ if (!matchedCompany) {
+ const fallback = (selectedCompanyId ? companies.find((company) => company.id === selectedCompanyId) : null)
+ ?? companies[0]
+ ?? null;
+ if (fallback && selectedCompanyId !== fallback.id) {
+ setSelectedCompanyId(fallback.id, { source: "route_sync" });
+ }
return;
}
- if (companyPrefix !== matched.issuePrefix) {
+ if (companyPrefix !== matchedCompany.issuePrefix) {
const suffix = location.pathname.replace(/^\/[^/]+/, "");
- navigate(`/${matched.issuePrefix}${suffix}${location.search}`, { replace: true });
+ navigate(`/${matchedCompany.issuePrefix}${suffix}${location.search}`, { replace: true });
return;
}
- if (selectedCompanyId !== matched.id) {
- setSelectedCompanyId(matched.id, { source: "route_sync" });
+ if (selectedCompanyId !== matchedCompany.id) {
+ setSelectedCompanyId(matchedCompany.id, { source: "route_sync" });
}
}, [
companyPrefix,
companies,
companiesLoading,
+ matchedCompany,
location.pathname,
location.search,
navigate,
@@ -282,7 +296,14 @@ export function Layout() {
className={cn("flex-1 overflow-auto p-4 md:p-6", isMobile && "pb-[calc(5rem+env(safe-area-inset-bottom))]")}
onScroll={handleMainScroll}
>
-
+ {hasUnknownCompanyPrefix ? (
+
+ ) : (
+
+ )}
diff --git a/ui/src/pages/NotFound.tsx b/ui/src/pages/NotFound.tsx
new file mode 100644
index 00000000..bcaf3898
--- /dev/null
+++ b/ui/src/pages/NotFound.tsx
@@ -0,0 +1,66 @@
+import { useEffect } from "react";
+import { Link, useLocation } from "@/lib/router";
+import { AlertTriangle, Compass } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useBreadcrumbs } from "../context/BreadcrumbContext";
+import { useCompany } from "../context/CompanyContext";
+
+type NotFoundScope = "board" | "invalid_company_prefix" | "global";
+
+interface NotFoundPageProps {
+ scope?: NotFoundScope;
+ requestedPrefix?: string;
+}
+
+export function NotFoundPage({ scope = "global", requestedPrefix }: NotFoundPageProps) {
+ const location = useLocation();
+ const { setBreadcrumbs } = useBreadcrumbs();
+ const { companies, selectedCompany } = useCompany();
+
+ useEffect(() => {
+ setBreadcrumbs([{ label: "Not Found" }]);
+ }, [setBreadcrumbs]);
+
+ const fallbackCompany = selectedCompany ?? companies[0] ?? null;
+ const dashboardHref = fallbackCompany ? `/${fallbackCompany.issuePrefix}/dashboard` : "/";
+ const currentPath = `${location.pathname}${location.search}${location.hash}`;
+ const normalizedPrefix = requestedPrefix?.toUpperCase();
+
+ const title = scope === "invalid_company_prefix" ? "Company not found" : "Page not found";
+ const description =
+ scope === "invalid_company_prefix"
+ ? `No company matches prefix "${normalizedPrefix ?? "unknown"}".`
+ : "This route does not exist.";
+
+ return (
+
+
+
+
+
+
{title}
+
{description}
+
+
+
+
+ Requested path: {currentPath}
+
+
+
+
+
+
+
+
+ );
+}