Session 41: P0 audit fixes — MLB stat_types, broken routes, tier mismatch, self-hosted fonts (1940 tests)

- Backend: whitelist MLB stat_types in analyze.js + scan.js gates (mirrors
  python validation.py); fixes MLB scans 400ing.
- Routes: /settings -> /profile, /report -> /blog redirect pages.
- Profile: read tier from useAuth().tier (nav's source) to kill the
  Free-vs-DESK mismatch.
- Fonts: self-host Inter/JetBrains Mono/IBM Plex Mono via next/font, drop the
  503ing fonts.googleapis.com <link>; rewire literal font-family refs to vars.
- Kept /settings/security (real MFA page) intact — NOT clobbered to a redirect.
- +33 tests (1907 -> 1940), 149 suites; web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-17 21:55:40 -04:00
parent e453c24d2c
commit 32069863dc
17 changed files with 318 additions and 41 deletions
+10 -7
View File
@@ -94,9 +94,12 @@
--scan-op: 0.04;
--grade-hero: var(--g-a);
/* Type — Inter for chrome/UI, JetBrains Mono for ALL data */
--sans: 'Inter', system-ui, sans-serif;
--mono: 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
/* Type — Inter for chrome/UI, JetBrains Mono for ALL data.
Session 41: --font-* come from next/font (self-hosted, set on <html>).
Literal family names kept as fallbacks for any non-next/font context. */
--sans: var(--font-sans), 'Inter', system-ui, sans-serif;
--mono: var(--font-mono), 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
--ibm-mono: var(--font-ibm), var(--font-mono), 'IBM Plex Mono', 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
/* ── Legacy aliases — every existing component still resolves ── */
--bg-primary: var(--bg-0);
@@ -605,7 +608,7 @@ body.tex-grain::before {
───────────────────────────────────────────────────────── */
.wordmark {
font-family: 'IBM Plex Mono', 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
font-family: var(--ibm-mono);
font-weight: 800;
letter-spacing: 0.10em;
display: inline-flex;
@@ -717,7 +720,7 @@ body.tex-grain::before {
align-items: center;
gap: 8px;
padding: 4px 10px 4px 4px;
font-family: 'IBM Plex Mono', 'JetBrains Mono', monospace;
font-family: var(--ibm-mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.15em;
@@ -743,7 +746,7 @@ body.tex-grain::before {
padding: 6px 10px;
background: var(--bg-1);
border: 1px solid var(--border);
font-family: 'IBM Plex Mono', 'JetBrains Mono', monospace;
font-family: var(--ibm-mono);
font-size: 10px;
letter-spacing: 0.15em;
text-transform: uppercase;
@@ -761,7 +764,7 @@ body.tex-grain::before {
/* The V watermark — Vendetta nod, hero background */
.v-watermark {
position: absolute;
font-family: 'IBM Plex Mono', 'JetBrains Mono', monospace;
font-family: var(--ibm-mono);
font-weight: 800;
font-size: 70vmin;
line-height: 0.8;
+18 -11
View File
@@ -1,4 +1,5 @@
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';
@@ -21,6 +22,22 @@ 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: {
@@ -110,17 +127,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
const country = hdrs.get(COUNTRY_HEADER) || '';
return (
<html lang={locale} dir={dir} className="dark">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
{/* VYNDR 2.0 (§2): Inter for chrome/UI, JetBrains Mono for ALL data.
IBM Plex Mono + Instrument Sans kept while pages migrate session-by-session. */}
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600;700;800&family=Instrument+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<html lang={locale} dir={dir} className={`dark ${fontVars}`}>
<body className="antialiased tex-grain">
<LocaleProvider locale={locale} country={country}>
<PostHogProvider>
+14 -8
View File
@@ -18,7 +18,7 @@ interface FullProfile {
export default function ProfilePage() {
const router = useRouter();
const { user, signOut, loading: authLoading } = useAuth();
const { user, tier: authTier, signOut, loading: authLoading } = useAuth();
const [profile, setProfile] = useState<FullProfile | null>(null);
const [working, setWorking] = useState(false);
const [error, setError] = useState('');
@@ -69,6 +69,12 @@ export default function ProfilePage() {
);
}
// Session 41 — tier is read from useAuth (the same live-session source the
// nav uses) so the two can't disagree. The /api/user/profile fetch can lag
// or return a stale/null tier; the auth context is authoritative. Profile
// still supplies the other fields (scan_count, subscription_*, founder).
const tier = authTier || profile.tier || 'free';
return (
<section style={{ maxWidth: 600, margin: '0 auto', padding: '24px 16px 120px' }}>
<header style={{ marginBottom: 24 }}>
@@ -88,8 +94,8 @@ export default function ProfilePage() {
{/* Session 27 — always render a tier label. When the profile
API returns null/undefined tier (free users sometimes do),
fall back to 'free' so the field is never blank. */}
<h2 style={{ fontSize: 28, fontWeight: 800, marginTop: 4, textTransform: 'capitalize', color: tierColor(profile.tier || 'free') }}>
{profile.tier || 'free'}
<h2 style={{ fontSize: 28, fontWeight: 800, marginTop: 4, textTransform: 'capitalize', color: tierColor(tier) }}>
{tier}
{profile.founder_pricing && (
<span
className="mono"
@@ -108,27 +114,27 @@ export default function ProfilePage() {
)}
</h2>
</div>
{profile.tier === 'free' && (
{tier === 'free' && (
<a href="/api/checkout?tier=analyst" className="btn-primary" style={{ padding: '10px 18px', fontSize: 13 }}>
Upgrade
</a>
)}
</div>
{profile.tier !== 'free' && (
{tier !== 'free' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<Stat label="Status" value={profile.subscription_status} tone={profile.subscription_status === 'active' ? 'good' : 'warn'} />
<Stat label="Renews" value={profile.subscription_end ? new Date(profile.subscription_end).toLocaleDateString() : '—'} />
</div>
)}
{profile.tier === 'free' && (
{tier === 'free' && (
<Stat label="Reads this month" value={`${profile.scan_count} of 5`} />
)}
</section>
{/* Founder pricing promo for free users */}
{profile.tier === 'free' && (
{tier === 'free' && (
<section className="surface diagonal-cut-strong" style={{ padding: 20, marginBottom: 16, borderColor: 'var(--grade-a)' }}>
<p className="mono" style={{ fontSize: 11, color: 'var(--grade-a)', letterSpacing: '0.08em', marginBottom: 8 }}>
FOUNDER ACCESS
@@ -146,7 +152,7 @@ export default function ProfilePage() {
)}
{/* Subscription actions */}
{profile.tier !== 'free' && !profile.cancel_at_period_end && (
{tier !== 'free' && !profile.cancel_at_period_end && (
<section className="surface" style={{ padding: 20, marginBottom: 16 }}>
<h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 8 }}>Cancel subscription</h3>
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
+11
View File
@@ -0,0 +1,11 @@
import { redirect } from 'next/navigation';
/**
* /report (Session 41 — P0 audit fix).
*
* The MORE dropdown links "THE REPORT" to /report, but the blog lives at
* /blog. Forward there so the link resolves instead of 404ing.
*/
export default function ReportPage() {
redirect('/blog');
}
+1 -1
View File
@@ -32,7 +32,7 @@ export default function ResponsibleGamblingPage() {
<li>
<strong style={{ color: 'var(--text-primary)' }}>National Council on Problem Gambling Helpline</strong>
<br />
<a href="tel:18005224700" style={{ color: 'var(--grade-a)', fontSize: 24, fontWeight: 700, fontFamily: 'JetBrains Mono, monospace' }}>
<a href="tel:18005224700" style={{ color: 'var(--grade-a)', fontSize: 24, fontWeight: 700, fontFamily: 'var(--mono)' }}>
1-800-522-4700
</a>
<br />
+13
View File
@@ -0,0 +1,13 @@
import { redirect } from 'next/navigation';
/**
* /settings (Session 41 — P0 audit fix).
*
* The audit found /settings 404'd. Account/preferences already live on
* /profile (the canonical surface), so forward there rather than build a
* second screen that would drift out of sync. Server-side redirect — no
* flash of an empty page.
*/
export default function SettingsPage() {
redirect('/profile');
}
+2 -2
View File
@@ -149,7 +149,7 @@ export default function DeskUpgradePage() {
border: 'none',
background: cadence === c ? 'var(--bg-3)' : 'transparent',
color: cadence === c ? 'var(--text-0)' : 'var(--text-1)',
fontFamily: 'IBM Plex Mono, monospace',
fontFamily: 'var(--ibm-mono)',
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.08em',
@@ -241,7 +241,7 @@ function TierCard({
<ul style={{ listStyle: 'none', padding: 0, margin: '14px 0 0', display: 'grid', gap: 8 }}>
{features.map((f) => (
<li key={f} style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 14, color: 'var(--text-0)' }}>
<span aria-hidden style={{ color: 'var(--grade-a)', fontFamily: 'IBM Plex Mono, monospace' }}></span>
<span aria-hidden style={{ color: 'var(--grade-a)', fontFamily: 'var(--ibm-mono)' }}></span>
<span>{f}</span>
</li>
))}
+1 -1
View File
@@ -133,7 +133,7 @@ function SportBadgeStrip() {
>
{SPORTS_DISPLAY.map((s) => {
const base: React.CSSProperties = {
fontFamily: 'IBM Plex Mono, JetBrains Mono, monospace',
fontFamily: 'var(--ibm-mono)',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.08em',
+2 -2
View File
@@ -173,7 +173,7 @@ export default function NotificationBell() {
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 8px rgba(0, 212, 160, 0.7)',
fontFamily: 'IBM Plex Mono, monospace',
fontFamily: 'var(--ibm-mono)',
}}
>
{unread > 9 ? '9+' : unread}
@@ -236,7 +236,7 @@ export default function NotificationBell() {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span
style={{
fontFamily: 'IBM Plex Mono, monospace',
fontFamily: 'var(--ibm-mono)',
fontSize: 10,
letterSpacing: '0.08em',
color: TYPE_TINT[n.type],