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 ( -
-
-
-

Loading...

-
-
- ); - } - 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 ( +
+
+
+

Loading...

+
+
+ ); + } + + // 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()} +

+ )} +
+ ); +}