diff --git a/src/app/[locale]/admin/billing/page.tsx b/src/app/[locale]/admin/billing/page.tsx index 1f0b9c5..b53f43e 100644 --- a/src/app/[locale]/admin/billing/page.tsx +++ b/src/app/[locale]/admin/billing/page.tsx @@ -98,6 +98,7 @@ export default async function AdminBillingPage() {

{t("balancesTitle")}

+
@@ -126,6 +127,7 @@ export default async function AdminBillingPage() { ))}
+
)} diff --git a/src/app/[locale]/admin/page.tsx b/src/app/[locale]/admin/page.tsx index fc05b28..40e101d 100644 --- a/src/app/[locale]/admin/page.tsx +++ b/src/app/[locale]/admin/page.tsx @@ -5,6 +5,11 @@ import { listTenants } from "@/lib/k8s"; import { countPendingSkillActivationRequests } from "@/lib/db"; import { AdminPanel } from "@/components/admin/admin-panel"; +export async function generateMetadata() { + const t = await getTranslations("common"); + return { title: t("admin") }; +} + export default async function AdminPage() { const user = await getSessionUser(); if (!user) redirect("/login"); diff --git a/src/app/[locale]/billing/page.tsx b/src/app/[locale]/billing/page.tsx index 5ef18e3..13828fe 100644 --- a/src/app/[locale]/billing/page.tsx +++ b/src/app/[locale]/billing/page.tsx @@ -26,6 +26,11 @@ import { RunningTotalWidget } from "@/components/billing/running-total-widget"; * Anyone signed in can view this. The data is org-scoped; even * non-owner team members see the same view. */ +export async function generateMetadata() { + const t = await getTranslations("common"); + return { title: t("billing") }; +} + export default async function CustomerBillingPage() { const user = await getSessionUser(); if (!user) redirect("/login"); diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx index 51cf8c2..58a1afa 100644 --- a/src/app/[locale]/dashboard/page.tsx +++ b/src/app/[locale]/dashboard/page.tsx @@ -22,6 +22,11 @@ import { ProvisioningStatus } from "@/components/onboarding/provisioning-status" import { formatDateTime } from "@/lib/format"; import Link from "next/link"; +export async function generateMetadata() { + const t = await getTranslations("common"); + return { title: t("dashboard") }; +} + export default async function DashboardPage() { const user = await getSessionUser(); if (!user) redirect("/login"); diff --git a/src/app/[locale]/error.tsx b/src/app/[locale]/error.tsx new file mode 100644 index 0000000..80eccd9 --- /dev/null +++ b/src/app/[locale]/error.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect } from "react"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/navigation"; + +/** + * Error boundary for the [locale] segment. Catches render/data errors + * thrown by any page below the locale layout (which is where K8s, DB, + * LiteLLM and Stripe calls happen). Renders inside NextIntlClientProvider, + * so translations are available. Root-layout failures fall through to + * global-error.tsx instead. + */ +export default function LocaleError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + const t = useTranslations("errors"); + + useEffect(() => { + // Surface the error for log scraping; the digest correlates with + // the server-side stack in production. + console.error("Portal error boundary:", error); + }, [error]); + + return ( +
+
+
+ +
+

+ {t("title")} +

+

{t("description")}

+ {error?.digest && ( +

+ {error.digest} +

+ )} +
+ + + {t("backToDashboard")} + +
+
+
+ ); +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index bcf9029..229d4a2 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,5 +1,6 @@ +import type { Metadata, Viewport } from "next"; import { NextIntlClientProvider } from "next-intl"; -import { getMessages } from "next-intl/server"; +import { getMessages, getTranslations } from "next-intl/server"; import { routing } from "@/i18n/routing"; import { notFound } from "next/navigation"; import { NavShell } from "@/components/layout/nav-shell"; @@ -8,6 +9,27 @@ export function generateStaticParams() { return routing.locales.map((locale) => ({ locale })); } +// Metadata API (Next 15) instead of a hand-rolled . The title +// template lets each page export a short `title` (e.g. "Dashboard") +// that renders as "Dashboard · PieCed". Pages that export no metadata +// fall back to the default below. +export async function generateMetadata(): Promise { + const t = await getTranslations("common"); + const appName = t("appName"); + return { + title: { + default: `${appName} Portal`, + template: `%s · ${appName}`, + }, + description: "PieCed IT — Multi-tenant AI assistant platform", + }; +} + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, +}; + export default async function LocaleLayout({ children, params, @@ -25,14 +47,6 @@ export default async function LocaleLayout({ return ( - - - PieCed Portal - - {children} diff --git a/src/app/[locale]/loading.tsx b/src/app/[locale]/loading.tsx new file mode 100644 index 0000000..dde24e2 --- /dev/null +++ b/src/app/[locale]/loading.tsx @@ -0,0 +1,25 @@ +/** + * Loading skeleton for the [locale] segment. Shown during navigation + * while a server component fetches (the dashboard, for instance, does + * listTenants() + one K8s GET per provisioning row). Textless on + * purpose so it needs no translations and adds no layout shift. + */ +export default function LocaleLoading() { + return ( +