DS4 (design): billboards — STREAKS row, grade reveal, CLV reframe, /u profile
P0 billboards (the timeline is customer #1). Pixel-level craft: one bold hero among muted context, real entities from DS0. - STREAKS row: rebuilt from a log line into a Bloomberg alert. Streak LENGTH is now the mono/tabular HERO (38px, the largest figure in the row); real PlayerAvatar identity; muted "built vs [opponents]" lens; ONE severity accent (step-up amber / step-down green); grade badge tier-gated (the READ is paid). - Grade reveal: edge is now sign-colored — negative edge uses var(--miss), never green (color contract #3). Fixed in BOTH the confidence strip and the MODEL/LINE/EDGE row; that row is now mono + tabular. Letter stays the hero. - CLV reframe: new pure lib/clvDisplay.js (clvMode flat/spread/none). A near- flat distribution (73/74) now renders a confident VOICE line — "CLV flat — we grade the outcome, not the close" — instead of a broken-looking histogram; bars show only on real spread. - /u profile: hit% is the bold record hero (56px mono tabular) + beat-close secondary; honest CLV-VERIFIED badge (only when closing value is tracked); real PlayerAvatar identity on cards; OG image elevated to carry the real record at 1200x630 social crop (graceful tagline fallback). Tests: +23 (tests/unit/ds4Billboards.test.js). Full suite green (2732). web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { GradePill } from '@/components/GradeCard';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { clvMode, CLV_FLAT_LINE } from '@/lib/clvDisplay';
|
||||
|
||||
/**
|
||||
* The Ledger (Session 58, Phase 1) — the truth surface, now backed by the
|
||||
@@ -191,15 +192,36 @@ function TierCalibration({ agg, minSample }: { agg: ModelAggregate; minSample: n
|
||||
);
|
||||
}
|
||||
|
||||
/** S6 (A1 board) — compact settled-CLV distribution strip. Bars are DATA
|
||||
* (mono, tabular): green = beat-the-close side, red = faded side, dim = flat.
|
||||
* Renders only when the server passed the n≥20 gate (clv_distribution set). */
|
||||
/** S6 (A1 board) · reframed DS4 — settled-CLV display. The n≥20 gate lives
|
||||
* in the server (clv_distribution set). DESIGN-SPEC Part 3 #16: a near-flat
|
||||
* distribution renders as one giant bar + six empty stubs — it reads as
|
||||
* BROKEN. When CLV is flat we SAY so in one confident VOICE line (the record
|
||||
* is the best data on the site; it must look the most premium, never errored)
|
||||
* and show the bars only when there is real spread. */
|
||||
function ClvDistribution({ agg }: { agg: ModelAggregate }) {
|
||||
const dist = agg.clv_distribution;
|
||||
if (!Array.isArray(dist) || dist.length === 0) return null;
|
||||
const total = dist.reduce((n, b) => n + b.count, 0);
|
||||
const { mode } = clvMode(dist as Parameters<typeof clvMode>[0]);
|
||||
if (mode === 'none') return null;
|
||||
|
||||
if (mode === 'flat') {
|
||||
// Flat CLV is a FEATURE, not an error: the model grades outcomes, not the
|
||||
// close. State it plainly — premium, confident, no broken histogram.
|
||||
return (
|
||||
<div style={{ marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
|
||||
<div className="mono" style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--text-tertiary)', marginBottom: 6 }}>
|
||||
CLOSING-LINE VALUE
|
||||
</div>
|
||||
<p className="mono" style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', margin: 0, letterSpacing: '0.01em' }}>
|
||||
{CLV_FLAT_LINE}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const distArr = dist as ClvBucket[];
|
||||
const total = distArr.reduce((n, b) => n + b.count, 0);
|
||||
if (total === 0) return null;
|
||||
const max = Math.max(...dist.map((b) => b.count));
|
||||
const max = Math.max(...distArr.map((b) => b.count));
|
||||
const color = (side: ClvBucket['side']) =>
|
||||
side === 'beat' ? 'var(--g-a, #00D4A0)' : side === 'faded' ? 'var(--miss, #FF6B6B)' : 'var(--text-tertiary)';
|
||||
return (
|
||||
@@ -208,7 +230,7 @@ function ClvDistribution({ agg }: { agg: ModelAggregate }) {
|
||||
CLV DISTRIBUTION · {total} SETTLED
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', maxWidth: 420 }}>
|
||||
{dist.map((b) => (
|
||||
{distArr.map((b) => (
|
||||
<div key={b.label} title={`${b.label}: ${b.count}`} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, minWidth: 0 }}>
|
||||
<span className="mono" style={{ fontSize: 10, fontVariantNumeric: 'tabular-nums', color: b.count > 0 ? 'var(--text-secondary)' : 'var(--text-tertiary)' }}>
|
||||
{b.count}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GradePill } from '@/components/GradeCard';
|
||||
import PlayerAvatar from '@/components/vyndr/PlayerAvatar';
|
||||
|
||||
/**
|
||||
* PublicProfile (A1 Session 10) — the public ledger record for one handle.
|
||||
@@ -144,30 +145,60 @@ export default function PublicProfile({ handle }: { handle: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The record HERO (DS4 · billboard — DESIGN-SPEC Part 3 + Part 6 #4). This is
|
||||
* the shareable surface Kev pitches partners with. One bold figure among muted
|
||||
* context: the HIT RATE as a large mono tabular number, beat-close as the
|
||||
* secondary bold figure, the raw record demoted. CLV-VERIFIED badge only when
|
||||
* closing-line value is actually tracked (honest — never a decorative badge).
|
||||
*/
|
||||
function RecordHeader({ agg, minSample }: { agg: ProfileAggregate | null; minSample: number }) {
|
||||
const ready = Boolean(agg && agg.settled >= minSample && agg.hit_pct != null);
|
||||
const clvVerified = Boolean(ready && agg && agg.beat_close_pct != null);
|
||||
return (
|
||||
<div
|
||||
className="surface diagonal-cut"
|
||||
style={{ padding: 20, marginBottom: 20, border: `1px solid ${ready ? 'var(--g-a, #00D4A0)' : 'var(--border)'}` }}
|
||||
style={{ padding: 24, marginBottom: 20, border: `1px solid ${ready ? 'var(--g-a, #00D4A0)' : 'var(--border)'}` }}
|
||||
>
|
||||
{ready && agg ? (
|
||||
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'baseline' }}>
|
||||
<span className="mono" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--text-tertiary)' }}>
|
||||
RECORD · LAST 30D
|
||||
</span>
|
||||
<span className="mono" style={{ fontSize: 20, fontWeight: 800, color: 'var(--g-a, #00D4A0)' }}>
|
||||
{agg.hits}-{agg.misses} · {agg.hit_pct}% HIT
|
||||
</span>
|
||||
{agg.beat_close_pct != null && (
|
||||
<span className="mono" style={{ fontSize: 20, fontWeight: 800, color: 'var(--text-primary)' }}>
|
||||
{agg.beat_close_pct}% BEAT CLOSE
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', marginBottom: 16 }}>
|
||||
<span className="mono" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', color: 'var(--text-tertiary)' }}>
|
||||
RECORD · LAST 30D
|
||||
</span>
|
||||
)}
|
||||
<span className="mono" style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
|
||||
{agg.pending} pending
|
||||
</span>
|
||||
</div>
|
||||
{clvVerified && (
|
||||
<span className="mono" style={{ fontSize: 10.5, fontWeight: 800, letterSpacing: '0.08em', color: 'var(--g-a, #00D4A0)', border: '1px solid var(--g-a, #00D4A0)', borderRadius: 999, padding: '3px 10px', background: 'color-mix(in srgb, var(--g-a, #00D4A0) 12%, transparent)' }}>
|
||||
✓ CLV-VERIFIED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* HERO figures — hit rate is the largest thing on the page. */}
|
||||
<div style={{ display: 'flex', gap: 40, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||
<div>
|
||||
<div className="mono" style={{ fontSize: 56, fontWeight: 800, lineHeight: 0.95, letterSpacing: '-0.03em', color: 'var(--g-a, #00D4A0)', fontVariantNumeric: 'tabular-nums' }}>
|
||||
{agg.hit_pct}%
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.12em', color: 'var(--text-tertiary)', marginTop: 4 }}>
|
||||
HIT RATE · {agg.hits}-{agg.misses}
|
||||
</div>
|
||||
</div>
|
||||
{agg.beat_close_pct != null && (
|
||||
<div>
|
||||
<div className="mono" style={{ fontSize: 40, fontWeight: 800, lineHeight: 0.95, letterSpacing: '-0.02em', color: 'var(--text-primary)', fontVariantNumeric: 'tabular-nums' }}>
|
||||
{agg.beat_close_pct}%
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.12em', color: 'var(--text-tertiary)', marginTop: 4 }}>
|
||||
BEAT CLOSE
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<div className="mono" style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-tertiary)', fontVariantNumeric: 'tabular-nums' }}>
|
||||
{agg.pending} pending
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<p className="mono" style={{ fontSize: 13, fontWeight: 800, letterSpacing: '0.1em', color: 'var(--amber, #FFB347)', marginBottom: 6 }}>
|
||||
@@ -230,10 +261,16 @@ function ProfileCard({ row, index }: { row: ProfileRow; index: number }) {
|
||||
<GradePill grade={row.grade} />
|
||||
</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 2 }}>{row.player_name}</h3>
|
||||
<p className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 4 }}>
|
||||
{row.side} {row.line} {row.stat.replace(/_/g, ' ')}
|
||||
</p>
|
||||
{/* Real entity identity (DS0) — headshot / team-colored monogram. */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||||
<PlayerAvatar name={row.player_name} sport={row.sport} size={34} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{row.player_name}</h3>
|
||||
<p className="mono" style={{ fontSize: 12, color: 'var(--text-secondary)', textTransform: 'capitalize', margin: 0 }}>
|
||||
{row.side} {row.line} {row.stat.replace(/_/g, ' ')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 12 }}>
|
||||
{row.book || '—'}{row.locked_odds ? ` · ${row.locked_odds}` : ''} · {row.game_date}
|
||||
{row.model_value != null && <span> · MODEL {row.model_value}</span>}
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
// A1 Session 10 — every shared /u/:handle link unfurls as a record card.
|
||||
// Self-hosted standalone build → Node runtime (NOT edge; Session-53 rule —
|
||||
// next/og breaks under edge off Vercel).
|
||||
// A1 Session 10 · elevated DS4 — every shared /u/:handle link unfurls as a
|
||||
// record card at social crop (1200×630). The billboard now carries the REAL
|
||||
// record (hit% + beat-close%) when the handle is published + past the n-gate,
|
||||
// so the timeline (customer #1) sees the proof, not just a tagline. Self-hosted
|
||||
// standalone build → Node runtime (NOT edge; Session-53 rule).
|
||||
export const alt = 'VYNDR Public Ledger';
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = 'image/png';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
|
||||
async function fetchRecord(handle: string): Promise<{ hit_pct: number | null; beat_close_pct: number | null; hits: number; misses: number } | null> {
|
||||
try {
|
||||
const r = await fetch(`${BACKEND_URL}/api/profiles/${encodeURIComponent(handle)}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
const agg = d && d.aggregate;
|
||||
if (!agg || agg.hit_pct == null) return null;
|
||||
return { hit_pct: agg.hit_pct, beat_close_pct: agg.beat_close_pct ?? null, hits: agg.hits, misses: agg.misses };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Image({ params }: { params: Promise<{ handle: string }> }) {
|
||||
const { handle } = await params;
|
||||
const h = decodeURIComponent(handle || '').slice(0, 20).toLowerCase() || 'handle';
|
||||
const rec = await fetchRecord(h);
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
@@ -28,14 +49,29 @@ export default async function Image({ params }: { params: Promise<{ handle: stri
|
||||
<div style={{ display: 'flex', position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: '#00D4A0' }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', fontSize: 26, letterSpacing: '0.22em', color: '#00D4A0', fontWeight: 700 }}>
|
||||
PUBLIC LEDGER
|
||||
{rec ? 'CLV-VERIFIED RECORD · 30D' : 'PUBLIC LEDGER'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', fontSize: 84, fontWeight: 900, letterSpacing: '-0.02em', marginTop: 18, color: '#FFFFFF' }}>
|
||||
<div style={{ display: 'flex', fontSize: 72, fontWeight: 900, letterSpacing: '-0.02em', marginTop: 14, color: '#FFFFFF' }}>
|
||||
@{h}
|
||||
</div>
|
||||
<div style={{ display: 'flex', fontSize: 28, color: '#7A7A8E', marginTop: 20 }}>
|
||||
CLV-verified record · every settled read · misses included
|
||||
</div>
|
||||
{rec ? (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 56, marginTop: 26 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', fontSize: 128, fontWeight: 900, lineHeight: 1, color: '#00D4A0' }}>{rec.hit_pct}%</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, letterSpacing: '0.14em', color: '#7A7A8E', marginTop: 8 }}>HIT RATE · {rec.hits}-{rec.misses}</div>
|
||||
</div>
|
||||
{rec.beat_close_pct != null && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', fontSize: 92, fontWeight: 900, lineHeight: 1, color: '#FFFFFF' }}>{rec.beat_close_pct}%</div>
|
||||
<div style={{ display: 'flex', fontSize: 22, letterSpacing: '0.14em', color: '#7A7A8E', marginTop: 8 }}>BEAT CLOSE</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', fontSize: 28, color: '#7A7A8E', marginTop: 20 }}>
|
||||
CLV-verified record · every settled read · misses included
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', fontSize: 44, fontWeight: 900, letterSpacing: '0.08em' }}>
|
||||
|
||||
Reference in New Issue
Block a user