diff --git a/src/Web/autopilot-monitor-web/app/globals.css b/src/Web/autopilot-monitor-web/app/globals.css
index 6556caae..c44676bf 100644
--- a/src/Web/autopilot-monitor-web/app/globals.css
+++ b/src/Web/autopilot-monitor-web/app/globals.css
@@ -579,3 +579,47 @@ html:hover ::-webkit-scrollbar-thumb {
.dark ::-webkit-scrollbar-thumb:hover {
background: #94a3b8 !important;
}
+
+/* ===== LANDING PAGE ANIMATIONS ===== */
+@keyframes stepReveal {
+ 0% {
+ transform: translateY(8px);
+ opacity: 0;
+ }
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes dotPulse {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.04);
+ transform: scale(1);
+ }
+ 2% {
+ box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.16);
+ transform: scale(1.01);
+ }
+ 8% {
+ box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.30);
+ transform: scale(1.05);
+ }
+ 14.5% {
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.18);
+ transform: scale(1.02);
+ }
+ 15.87% {
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.04);
+ transform: scale(1);
+ }
+}
+
+.landing-page .step-card {
+ animation: stepReveal 0.45s ease-out both;
+}
+
+.landing-page .dot-pulse {
+ animation: dotPulse 12.6s linear infinite;
+}
diff --git a/src/Web/autopilot-monitor-web/app/page.tsx b/src/Web/autopilot-monitor-web/app/page.tsx
index cf927631..fdf3f683 100644
--- a/src/Web/autopilot-monitor-web/app/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/page.tsx
@@ -1,63 +1,7 @@
-"use client";
-
-import { useAuth } from "../contexts/AuthContext";
-import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
import { PublicSiteNavbar } from "../components/PublicSiteNavbar";
-
-interface PlatformStatsManifest {
- latest: string;
- generatedAtUtc: string;
-}
-
-interface PlatformStatsPayload {
- totalEnrollments?: number;
- totalUsers?: number;
- totalTenants?: number;
- totalSignedUpTenants?: number;
- uniqueDeviceModels?: number;
- totalEventsProcessed?: number;
- successfulEnrollments?: number;
- issuesDetected?: number;
- lastFullCompute?: string;
- lastUpdated?: string;
-}
-
-const DEFAULT_PLATFORM_STATS = {
- totalEnrollments: 123,
- totalTenants: 2,
- totalSignedUpTenants: 2,
- uniqueDeviceModels: 1,
- totalEventsProcessed: 0,
- issuesDetected: 0,
- lastUpdated: null as string | null,
-};
-
-function resolvePlatformStatsManifestUrl(rawUrl?: string): string {
- const trimmed = rawUrl?.trim();
- if (!trimmed) {
- return "/platform-stats.json";
- }
-
- const hashIndex = trimmed.indexOf("#");
- const withoutHash = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
- const hash = hashIndex >= 0 ? trimmed.slice(hashIndex) : "";
-
- const queryIndex = withoutHash.indexOf("?");
- const basePath = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
- const query = queryIndex >= 0 ? withoutHash.slice(queryIndex) : "";
-
- if (/\.json$/i.test(basePath)) {
- return trimmed;
- }
-
- const normalizedBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
- const manifestPath = `${normalizedBasePath}/platform-stats.json`;
- return `${manifestPath}${query}${hash}`;
-}
-
-const PLATFORM_STATS_MANIFEST_URL =
- resolvePlatformStatsManifestUrl(process.env.NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL);
+import { AuthGate } from "../components/landing/AuthGate";
+import { LoginButton } from "../components/landing/LoginButton";
+import { PlatformStats } from "../components/landing/PlatformStats";
const QUICK_START = [
{
@@ -160,82 +104,11 @@ function StepIcon({ icon }: { icon: string }) {
}
export default function LandingPage() {
- const { login, isAuthenticated, isLoading, user, isPreviewBlocked } = useAuth();
- const router = useRouter();
- const [platformStats, setPlatformStats] = useState(DEFAULT_PLATFORM_STATS);
-
- // Redirect after login: preview-blocked → /preview, admins/operators → /dashboard, users → /progress
- useEffect(() => {
- if (isAuthenticated && !isLoading && user) {
- if (isPreviewBlocked) {
- router.push("/preview");
- } else if (user.isTenantAdmin || user.isGalacticAdmin || user.role === 'Operator') {
- router.push("/dashboard");
- } else {
- router.push("/progress");
- }
- }
- }, [isAuthenticated, isLoading, user, isPreviewBlocked, router]);
-
- useEffect(() => {
- let cancelled = false;
-
- const loadPlatformStats = async () => {
- try {
- const manifestResponse = await fetch(PLATFORM_STATS_MANIFEST_URL, { cache: "no-store" });
- if (!manifestResponse.ok) {
- return;
- }
-
- const manifest = (await manifestResponse.json()) as PlatformStatsManifest;
- if (!manifest?.latest) {
- return;
- }
-
- const versionedUrl = new URL(manifest.latest, manifestResponse.url).toString();
- const statsResponse = await fetch(versionedUrl, { cache: "force-cache" });
- if (!statsResponse.ok) {
- return;
- }
-
- const payload = (await statsResponse.json()) as PlatformStatsPayload;
- if (cancelled) {
- return;
- }
-
- setPlatformStats({
- totalEnrollments: payload.totalEnrollments ?? DEFAULT_PLATFORM_STATS.totalEnrollments,
- totalTenants: payload.totalTenants ?? DEFAULT_PLATFORM_STATS.totalTenants,
- totalSignedUpTenants: payload.totalSignedUpTenants ?? DEFAULT_PLATFORM_STATS.totalSignedUpTenants,
- uniqueDeviceModels: payload.uniqueDeviceModels ?? DEFAULT_PLATFORM_STATS.uniqueDeviceModels,
- totalEventsProcessed: payload.totalEventsProcessed ?? DEFAULT_PLATFORM_STATS.totalEventsProcessed,
- issuesDetected: payload.issuesDetected ?? DEFAULT_PLATFORM_STATS.issuesDetected,
- lastUpdated: payload.lastUpdated ?? null,
- });
- } catch {
- // Keep defaults if manifest/versioned files are not reachable.
- }
- };
-
- loadPlatformStats();
- return () => {
- cancelled = true;
- };
- }, []);
-
- if (isLoading) {
- return (
-
- );
- }
-
return (
+ {/* Client component: handles auth redirect + loading overlay */}
+
+
{/* Hero Section */}
@@ -269,8 +142,7 @@ export default function LandingPage() {
Monitor every phase, run customizable analyze rules, and resolve issues faster than ever before.
-
+
Free to use • Open-Source • Sign in to request early access
- {/* Platform Stats - compact inline */}
-
-
- {platformStats.totalSignedUpTenants.toLocaleString()} organisations
-
- {platformStats.uniqueDeviceModels.toLocaleString()} device models
-
- {platformStats.totalEnrollments.toLocaleString()} enrollments monitored
-
- {platformStats.issuesDetected.toLocaleString()} detected issues
-
- {platformStats.totalEventsProcessed.toLocaleString()} events processed
-
- {platformStats.lastUpdated && (
-
- last updated: {new Date(platformStats.lastUpdated).toLocaleString()}
-
- )}
-
+ {/* Platform Stats - client component fetches live data */}
+
{/* Product Preview Showcase */}
@@ -530,12 +385,11 @@ export default function LandingPage() {
One pipeline from deployment to action
-
+
@@ -905,12 +759,11 @@ export default function LandingPage() {
Join organizations using Autopilot Monitor to react faster and monitor more reliably.
-
+
@@ -1004,51 +857,6 @@ export default function LandingPage() {
-
-
);
}
diff --git a/src/Web/autopilot-monitor-web/components/landing/AuthGate.tsx b/src/Web/autopilot-monitor-web/components/landing/AuthGate.tsx
new file mode 100644
index 00000000..ba193714
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/components/landing/AuthGate.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useAuth } from "../../contexts/AuthContext";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+/**
+ * Invisible client component that handles auth redirect logic.
+ * Renders nothing visible — just redirects authenticated users.
+ * When auth is still loading, shows a loading overlay on top of the static page.
+ */
+export function AuthGate() {
+ const { isAuthenticated, isLoading, user, isPreviewBlocked } = useAuth();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (isAuthenticated && !isLoading && user) {
+ if (isPreviewBlocked) {
+ router.push("/preview");
+ } else if (user.isTenantAdmin || user.isGalacticAdmin || user.role === 'Operator') {
+ router.push("/dashboard");
+ } else {
+ router.push("/progress");
+ }
+ }
+ }, [isAuthenticated, isLoading, user, isPreviewBlocked, router]);
+
+ // While auth is loading and we might need to redirect, show overlay.
+ // This prevents a flash of the landing page for authenticated users.
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ // Once loaded, render nothing — the static page shows through.
+ return null;
+}
diff --git a/src/Web/autopilot-monitor-web/components/landing/LoginButton.tsx b/src/Web/autopilot-monitor-web/components/landing/LoginButton.tsx
new file mode 100644
index 00000000..77c25c73
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/components/landing/LoginButton.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { useAuth } from "../../contexts/AuthContext";
+
+export function LoginButton({
+ className,
+ children,
+}: {
+ className?: string;
+ children: React.ReactNode;
+}) {
+ const { login } = useAuth();
+
+ return (
+
+ );
+}
diff --git a/src/Web/autopilot-monitor-web/components/landing/PlatformStats.tsx b/src/Web/autopilot-monitor-web/components/landing/PlatformStats.tsx
new file mode 100644
index 00000000..1eed8a17
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/components/landing/PlatformStats.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+interface PlatformStatsManifest {
+ latest: string;
+ generatedAtUtc: string;
+}
+
+interface PlatformStatsPayload {
+ totalEnrollments?: number;
+ totalUsers?: number;
+ totalTenants?: number;
+ totalSignedUpTenants?: number;
+ uniqueDeviceModels?: number;
+ totalEventsProcessed?: number;
+ successfulEnrollments?: number;
+ issuesDetected?: number;
+ lastFullCompute?: string;
+ lastUpdated?: string;
+}
+
+function resolvePlatformStatsManifestUrl(rawUrl?: string): string {
+ const trimmed = rawUrl?.trim();
+ if (!trimmed) {
+ return "/platform-stats.json";
+ }
+
+ const hashIndex = trimmed.indexOf("#");
+ const withoutHash = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
+ const hash = hashIndex >= 0 ? trimmed.slice(hashIndex) : "";
+
+ const queryIndex = withoutHash.indexOf("?");
+ const basePath = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
+ const query = queryIndex >= 0 ? withoutHash.slice(queryIndex) : "";
+
+ if (/\.json$/i.test(basePath)) {
+ return trimmed;
+ }
+
+ const normalizedBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
+ const manifestPath = `${normalizedBasePath}/platform-stats.json`;
+ return `${manifestPath}${query}${hash}`;
+}
+
+const PLATFORM_STATS_MANIFEST_URL =
+ resolvePlatformStatsManifestUrl(process.env.NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL);
+
+const DEFAULT_PLATFORM_STATS = {
+ totalEnrollments: 123,
+ totalTenants: 2,
+ totalSignedUpTenants: 2,
+ uniqueDeviceModels: 1,
+ totalEventsProcessed: 0,
+ issuesDetected: 0,
+ lastUpdated: null as string | null,
+};
+
+export function PlatformStats() {
+ const [stats, setStats] = useState(DEFAULT_PLATFORM_STATS);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadPlatformStats = async () => {
+ try {
+ const manifestResponse = await fetch(PLATFORM_STATS_MANIFEST_URL, { cache: "no-store" });
+ if (!manifestResponse.ok) {
+ return;
+ }
+
+ const manifest = (await manifestResponse.json()) as PlatformStatsManifest;
+ if (!manifest?.latest) {
+ return;
+ }
+
+ const versionedUrl = new URL(manifest.latest, manifestResponse.url).toString();
+ const statsResponse = await fetch(versionedUrl, { cache: "force-cache" });
+ if (!statsResponse.ok) {
+ return;
+ }
+
+ const payload = (await statsResponse.json()) as PlatformStatsPayload;
+ if (cancelled) {
+ return;
+ }
+
+ setStats({
+ totalEnrollments: payload.totalEnrollments ?? DEFAULT_PLATFORM_STATS.totalEnrollments,
+ totalTenants: payload.totalTenants ?? DEFAULT_PLATFORM_STATS.totalTenants,
+ totalSignedUpTenants: payload.totalSignedUpTenants ?? DEFAULT_PLATFORM_STATS.totalSignedUpTenants,
+ uniqueDeviceModels: payload.uniqueDeviceModels ?? DEFAULT_PLATFORM_STATS.uniqueDeviceModels,
+ totalEventsProcessed: payload.totalEventsProcessed ?? DEFAULT_PLATFORM_STATS.totalEventsProcessed,
+ issuesDetected: payload.issuesDetected ?? DEFAULT_PLATFORM_STATS.issuesDetected,
+ lastUpdated: payload.lastUpdated ?? null,
+ });
+ } catch {
+ // Keep defaults if manifest/versioned files are not reachable.
+ }
+ };
+
+ loadPlatformStats();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ return (
+
+
+ {stats.totalSignedUpTenants.toLocaleString()} organisations
+
+ {stats.uniqueDeviceModels.toLocaleString()} device models
+
+ {stats.totalEnrollments.toLocaleString()} enrollments monitored
+
+ {stats.issuesDetected.toLocaleString()} detected issues
+
+ {stats.totalEventsProcessed.toLocaleString()} events processed
+
+ {stats.lastUpdated && (
+
+ last updated: {new Date(stats.lastUpdated).toLocaleString()}
+
+ )}
+
+ );
+}