diff --git a/diagnostic/build-351208e6.json b/diagnostic/build-351208e6.json
new file mode 100644
index 00000000..84308a90
--- /dev/null
+++ b/diagnostic/build-351208e6.json
@@ -0,0 +1,24 @@
+{
+ "generated_at": "2026-06-23T20:07:49.875563+00:00",
+ "commit": "351208e6",
+ "diagnostic_logd": "diagnostic/build-351208e6.logd",
+ "diagnostic_logd_error": null,
+ "message_blocker": null,
+ "chunked": false,
+ "chunk_size_bytes": null,
+ "password": "07c5642fac692758929f",
+ "decrypt_command": "encryptly unpack diagnostic/build-351208e6.logd --password 07c5642fac692758929f",
+ "total_modules": 1,
+ "passed": 1,
+ "failed": 0,
+ "modules": [
+ {
+ "name": "frontend",
+ "status": "PASS",
+ "elapsed_seconds": 6.434,
+ "artifact": "/repo/frontend/dist",
+ "output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 101 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.62 kB \u2502 gzip: 0.34 kB\ndist/assets/state-D-FhEj_8.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-OPNiyGRr.js 49.47 kB \u2502 gzip: 17.49 kB \u2502 map: 484.52 kB\ndist/assets/index-CDLR7IeC.js 233.01 kB \u2502 gzip: 72.64 kB \u2502 map: 1,048.62 kB\n\u2713 built in 1.72s\nnpm notice\nnpm notice New major version of npm available! 10.9.8 -> 11.17.0\nnpm notice Changelog: https://github.com/npm/cli/releases/tag/v11.17.0\nnpm notice To update run: npm install -g npm@11.17.0\nnpm notice"
+ }
+ ],
+ "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-351208e6.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging."
+}
diff --git a/diagnostic/build-351208e6.logd b/diagnostic/build-351208e6.logd
new file mode 100644
index 00000000..e5912fe8
Binary files /dev/null and b/diagnostic/build-351208e6.logd differ
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 7f23c536..333ed466 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import Header from './Header';
+import { SectionFallback } from './SectionFallback';
import Sidebar from './Sidebar';
interface LayoutProps {
@@ -11,13 +12,31 @@ const Layout: React.FC = ({ children }) => {
return (
diff --git a/frontend/src/components/SectionFallback.tsx b/frontend/src/components/SectionFallback.tsx
new file mode 100644
index 00000000..2abeeeaf
--- /dev/null
+++ b/frontend/src/components/SectionFallback.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+
+export interface SectionFallbackProps {
+ children: React.ReactNode;
+ title: string;
+ message?: string;
+ className?: string;
+ resetLabel?: string;
+}
+
+interface SectionFallbackState {
+ hasError: boolean;
+}
+
+export class SectionFallback extends React.Component<
+ SectionFallbackProps,
+ SectionFallbackState
+> {
+ state: SectionFallbackState = {
+ hasError: false,
+ };
+
+ static getDerivedStateFromError(): SectionFallbackState {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
+ console.error('SectionFallback caught a render error', error, errorInfo);
+ }
+
+ handleReset = (): void => {
+ this.setState({ hasError: false });
+ };
+
+ render(): React.ReactNode {
+ const {
+ children,
+ title,
+ message = 'Something went wrong in this section.',
+ className,
+ resetLabel = 'Try again',
+ } = this.props;
+
+ if (this.state.hasError) {
+ const fallbackClassName = ['section-fallback', className]
+ .filter(Boolean)
+ .join(' ');
+
+ return (
+
+
{title}
+
{message}
+
+
+ );
+ }
+
+ return children;
+ }
+}
+
+export default SectionFallback;
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 447dcfa3..6f6a9e00 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { SectionFallback } from '../components/SectionFallback';
import { useDashboardStats } from '../hooks';
const statCards = [
@@ -39,41 +40,53 @@ const Dashboard: React.FC = () => {
-
- {statCards.map((card) => (
-
-
-
-
{card.label}
-
- {String((stats as any)?.[card.key] ?? ' - ')}
- {card.suffix || ''}
-
+
+
+ {statCards.map((card) => (
+
+
+
+ {card.label}
+
+ {String((stats as any)?.[card.key] ?? ' - ')}
+ {card.suffix || ''}
+
+
-
- ))}
-
+ ))}
+
+
-
-
-
Recent Activity
-
- Activity feed will appear here
+
+
+
+
Recent Activity
+
+ Activity feed will appear here
+
-
-
-
System Health
-
- Health metrics will appear here
+
+
System Health
+
+ Health metrics will appear here
+
-
+
);
};
diff --git a/frontend/src/styles/legacy.css b/frontend/src/styles/legacy.css
index ad0399ac..b653f819 100644
--- a/frontend/src/styles/legacy.css
+++ b/frontend/src/styles/legacy.css
@@ -826,3 +826,58 @@ tbody tr:last-child td {
display: none !important;
}
}
+
+/* ------------------------------------------------------------------ */
+/* SECTION FALLBACKS */
+/* ------------------------------------------------------------------ */
+
+.section-fallback {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ padding: 1rem;
+ color: #f8fafc;
+ background-color: #1e293b;
+ border: 1px solid #334155;
+ border-left: 3px solid #eab308;
+ border-radius: 8px;
+}
+
+.section-fallback-title {
+ margin: 0;
+ color: #f8fafc;
+ font-size: 1rem;
+}
+
+.section-fallback-message {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.875rem;
+}
+
+.section-fallback-action {
+ margin-top: 0.25rem;
+}
+
+.app-header-fallback {
+ margin: 0;
+ border-radius: 0;
+}
+
+.app-sidebar-fallback {
+ align-self: stretch;
+ min-width: 12rem;
+}
+
+.app-content-fallback {
+ width: 100%;
+}
+
+.dashboard-stats-fallback {
+ margin-bottom: 1.5rem;
+}
+
+.dashboard-panels-fallback {
+ margin-top: 1.5rem;
+}