From bff3aad1cacc165e40bbba539d80acc7159e6b7a Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 29 May 2026 22:32:08 +0200 Subject: [PATCH] add error/loading/404 boundaries, responsive tables, Metadata API --- src/app/[locale]/admin/billing/page.tsx | 2 + src/app/[locale]/admin/page.tsx | 5 ++ src/app/[locale]/billing/page.tsx | 5 ++ src/app/[locale]/dashboard/page.tsx | 5 ++ src/app/[locale]/error.tsx | 72 +++++++++++++++++ src/app/[locale]/layout.tsx | 32 +++++--- src/app/[locale]/loading.tsx | 25 ++++++ src/app/[locale]/not-found.tsx | 34 ++++++++ src/app/[locale]/settings/page.tsx | 5 ++ src/app/[locale]/support/page.tsx | 5 ++ src/app/[locale]/team/page.tsx | 5 ++ src/app/global-error.tsx | 78 +++++++++++++++++++ .../admin/billing/custom-invoice-editor.tsx | 2 + src/components/admin/billing/draft-list.tsx | 2 + .../admin/billing/generate-form.tsx | 2 + .../admin/billing/invoice-detail-view.tsx | 4 + .../admin/billing/invoices-table.tsx | 2 + .../admin/billing/org-payment-mode-list.tsx | 2 + .../admin/billing/pricing-editor.tsx | 2 + src/components/admin/cron/cron-controls.tsx | 2 + .../admin/skills/pending-skill-requests.tsx | 2 + .../billing/customer-credit-note-list.tsx | 2 + .../billing/customer-invoice-detail.tsx | 2 + .../billing/customer-invoice-list.tsx | 2 + .../billing/running-total-widget.tsx | 2 + src/messages/de.json | 8 ++ src/messages/en.json | 8 ++ src/messages/fr.json | 8 ++ src/messages/it.json | 8 ++ 29 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 src/app/[locale]/error.tsx create mode 100644 src/app/[locale]/loading.tsx create mode 100644 src/app/[locale]/not-found.tsx create mode 100644 src/app/global-error.tsx 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 ( +