Files
vyndr/web/src/app/layout.tsx
T
builtbykev c8790fde55 Session 57: Phase 0 — Kill the Lies (2309 tests)
Work-order Phase 0 (Jul 10 live audit): every fabricated UI element deleted
or rewired to real data. Deletion sprint — no new product features.

0.1 Fake NBA game: root cause was scheduleService fetching the ESPN
    scoreboard with no ?dates= param or date filter — off-season ESPN
    returns the NEAREST slate (Jun 13 NYK@SA Finals rendered as tonight).
    Now pinned to the requested ET date + defensive filter; undated events
    dropped. Honest month-aware per-sport empty states (lib/emptyState.js).
0.2 Fake header counters: liveTick stripped to a bare 1s pulse (the
    auto-incrementing "247 graded", sin-driven brain-%, aPlus/cascades are
    dead). New GET /api/snapshot/summary (cache-only, before /:sport) +
    Next proxy; HeartbeatBar shows the real graded count and SYNC =
    elapsed since the last pipeline run (amber past 5 min).
0.3 Ticker: hardcoded fallback items deleted (real snapshot exhaust only);
    MOVE kept — computeLineDeltas is real movement. <4 real items → no
    ticker; bar publishes --ticker-h so the fixed header collapses cleanly.
0.4 /terminal retired: route redirects to /dashboard; VVI/injury-wire/
    leaders layouts preserved unrouted as §12 content-engine templates.
    Nav PRIMARY = Slate/Scan/Ledger; BottomTabBar Terminal→Explore; PWA
    shortcut Terminal→Ledger; #terminal alias → /dashboard.
0.5 ›Query nav pill deleted (duplicate /scan link).

Backend 2289 → 2309 tests (199 suites), web build exit 0.
Spec: specs/phase-0-kill-the-lies.md. Next: work-order Phase 1 (ledger
persistence + settlement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:17:49 -04:00

168 lines
6.7 KiB
TypeScript

import type { Metadata, Viewport } from 'next';
import { Inter, JetBrains_Mono, IBM_Plex_Mono } from 'next/font/google';
import PostHogProvider from '@/components/PostHogProvider';
import AuthProvider from '@/contexts/AuthContext';
import ParlayProvider from '@/contexts/ParlayContext';
import ExplainModeProvider from '@/contexts/ExplainModeContext';
import Nav from '@/components/Nav';
import Footer from '@/components/Footer';
import AuthGate from '@/components/AuthGate';
import HashRedirect from '@/components/vyndr/HashRedirect';
import GlobalHosts from '@/components/vyndr/GlobalHosts';
// Session 50 — the Parlay Lab supersedes the legacy ParlayTray.
import ParlayPanel from '@/components/vyndr/ParlayPanel';
import BottomTabBar from '@/components/BottomTabBar';
import InstallPrompt from '@/components/InstallPrompt';
import PushPrompt from '@/components/PushPrompt';
import MFAPrompt from '@/components/MFAPrompt';
import MFAChallenge from '@/components/MFAChallenge';
import CookieConsent from '@/components/CookieConsent';
import SentryInit from '@/components/SentryInit';
import { LocaleProvider } from '@/contexts/LocaleContext';
import { headers } from 'next/headers';
import { LOCALE_HEADER, COUNTRY_HEADER, isLocale, DEFAULT_LOCALE, LOCALE_META } from '@/lib/locales';
import './globals.css';
// Session 41 — self-hosted via next/font (downloaded at build time, served
// from our origin). Replaces the runtime fonts.googleapis.com <link>, which
// returned 503 in production and added ~4s to page load. §2 brand fonts:
// Inter for chrome/UI (--font-sans), JetBrains Mono for ALL data (--font-mono).
// IBM Plex Mono (--font-ibm) kept for the wordmark + legacy mono surfaces while
// pages migrate. globals.css :root maps --sans/--mono onto these variables.
const inter = Inter({ subsets: ['latin'], variable: '--font-sans', display: 'swap' });
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono', display: 'swap' });
const ibmPlexMono = IBM_Plex_Mono({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
variable: '--font-ibm',
display: 'swap',
});
const fontVars = `${inter.variable} ${jetbrainsMono.variable} ${ibmPlexMono.variable}`;
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://vyndr.app'),
title: {
default: 'VYNDR — Sports Prop Intelligence',
template: '%s · VYNDR',
},
description:
'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. NBA, MLB, WNBA, and soccer. Built in Detroit.',
applicationName: 'VYNDR',
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
manifest: '/manifest.json',
keywords: [
'sports prop grading',
'NBA prop bet analysis',
'MLB prop intelligence',
'WNBA prop grading',
'soccer prop intelligence',
'World Cup 2026 props',
'xG regression analysis',
'parlay correlation analysis',
'prop betting tools',
],
openGraph: {
title: "VYNDR — The edge the books don't want you to have",
description:
'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. Built in Detroit.',
url: 'https://vyndr.app',
siteName: 'VYNDR',
locale: 'en_US',
type: 'website',
// og:image is supplied by the file-based opengraph-image.tsx convention.
},
twitter: {
card: 'summary_large_image',
title: "VYNDR — The edge the books don't want you to have",
description: 'Pre-graded player props. Proprietary archetypes. Parlay Lab. Built in Detroit.',
creator: '@getvyndr',
// twitter:image is supplied by the file-based twitter-image.tsx convention.
},
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon.ico', sizes: '32x32' },
{ url: '/favicon-32.png', sizes: '32x32', type: 'image/png' },
{ url: '/favicon-16.png', sizes: '16x16', type: 'image/png' },
],
apple: '/apple-touch-icon.png',
},
appleWebApp: {
capable: true,
statusBarStyle: 'black-translucent',
title: 'VYNDR',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
export const viewport: Viewport = {
themeColor: '#06060B',
width: 'device-width',
initialScale: 1,
maximumScale: 5,
// Session 37 — extend under the iOS notch / home indicator so the
// mobile tab bar's env(safe-area-inset-*) padding has room to work.
viewportFit: 'cover',
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
// Session 12 — resolve locale from the middleware-stamped request
// header so server components render with the right translations
// and the <html> dir attribute is set before paint (no FOUC of
// mis-directional text).
const hdrs = await headers();
const localeHeader = hdrs.get(LOCALE_HEADER);
const locale = isLocale(localeHeader) ? localeHeader : DEFAULT_LOCALE;
const dir = LOCALE_META[locale].dir;
// Session 13 — country from CF-IPCountry (set by middleware).
// Empty string when traffic bypasses Cloudflare (local dev, direct
// origin hits). The Africa-tier gate degrades closed on empty.
const country = hdrs.get(COUNTRY_HEADER) || '';
return (
<html lang={locale} dir={dir} className={`dark ${fontVars}`}>
<body className="antialiased tex-grain">
<LocaleProvider locale={locale} country={country}>
<PostHogProvider>
<AuthProvider>
<ExplainModeProvider>
<ParlayProvider>
<HashRedirect />
<Nav />
{/* Header = 60px nav + 30px heartbeat (+2 slack) + a DYNAMIC
ticker: the Ticker publishes --ticker-h (32px with ≥4 real
items, else 0px — spec §3) so main's offset collapses with
it. Keep the 92px base in sync with Nav/HeartbeatBar. */}
<AuthGate>
<main style={{ paddingTop: 'calc(92px + var(--ticker-h, 0px))', minHeight: '100vh', paddingBottom: 80 }}>{children}</main>
</AuthGate>
<Footer />
<ParlayPanel />
<BottomTabBar />
<InstallPrompt />
<PushPrompt />
<MFAPrompt />
<MFAChallenge />
<CookieConsent />
<SentryInit />
{/* Session 38 — prefs apply + paywall/checkout/prefs globals (§9/§10/§12) */}
<GlobalHosts />
</ParlayProvider>
</ExplainModeProvider>
</AuthProvider>
</PostHogProvider>
</LocaleProvider>
</body>
</html>
);
}