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:
+1
-1
File diff suppressed because one or more lines are too long
+10
-7
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user