Session 27: PWA autopilot — NetworkFirst cache policy, stale bucket cleanup, offline fallback, push helper, manifest polish, tier fix (1584 tests)

This commit is contained in:
Kev
2026-06-13 10:57:49 -04:00
parent f8a51cd9d0
commit 66fafd8429
10 changed files with 445 additions and 16 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
template: '%s · VYNDR',
},
description:
"Grade NBA, MLB, WNBA, and soccer props with intelligence the books don't want you to have. World Cup 2026 intelligence: xG regression, altitude, referee, penalty taker. Built in Detroit.",
"Grade your props across every sport with intelligence the books don't want you to have. NBA, MLB, WNBA, and soccer today — NFL and more through 2026. World Cup 2026 intelligence: xG regression, altitude, referee, penalty taker. Built in Detroit.",
applicationName: 'VYNDR',
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
manifest: '/manifest.json',
+71
View File
@@ -0,0 +1,71 @@
'use client';
/**
* Offline fallback (Session 27).
*
* Served by the service worker's navigation handler when a page request
* fails on the network AND misses the runtime cache. Pre-cached on SW
* install so it's always available. Kept dependency-free so it renders
* with zero network.
*/
export default function OfflinePage() {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg-0, #06060B)',
color: 'var(--text-0, #F0F0F5)',
padding: 24,
textAlign: 'center',
}}
>
<div
className="mono"
style={{
fontSize: 11,
letterSpacing: '0.18em',
textTransform: 'uppercase',
color: 'var(--grade-a, #00D4A0)',
marginBottom: 16,
}}
>
VYNDR
</div>
<h1 style={{ fontSize: 28, fontWeight: 800, marginBottom: 8, letterSpacing: '-0.02em' }}>
You&apos;re offline
</h1>
<p
style={{
fontSize: 16,
color: 'var(--text-secondary, #8A8A9A)',
maxWidth: 400,
marginBottom: 24,
lineHeight: 1.5,
}}
>
Scores and grades will refresh the moment you reconnect. Anything you
loaded earlier is still cached and available.
</p>
<button
type="button"
onClick={() => window.location.reload()}
style={{
padding: '12px 24px',
background: 'var(--grade-a, #00D4A0)',
color: '#06060B',
border: 'none',
borderRadius: 6,
fontWeight: 700,
fontSize: 14,
cursor: 'pointer',
}}
>
Try again
</button>
</div>
);
}
+5 -2
View File
@@ -85,8 +85,11 @@ export default function ProfilePage() {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 16 }}>
<div>
<p className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>YOUR TIER</p>
<h2 style={{ fontSize: 28, fontWeight: 800, marginTop: 4, textTransform: 'capitalize', color: tierColor(profile.tier) }}>
{profile.tier}
{/* 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'}
{profile.founder_pricing && (
<span
className="mono"