feat(meme): Token Pricing page with usage chart + AMPP-style Z-AMPP SVG wordmark on home + Tokens tile/nav everywhere
This commit is contained in:
parent
1f4750a1b4
commit
6bd97a2a03
9 changed files with 177 additions and 1 deletions
|
|
@ -209,6 +209,7 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
|
||||
Jobs
|
||||
</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
<a href="edit.html" class="nav-item" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
|
||||
Editor
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@
|
|||
<a href="capture.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><circle cx="8" cy="8" r="6.5"/></svg>Capture</a>
|
||||
<a href="edit.html" class="nav-item active"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>Editor</a>
|
||||
<a href="jobs.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>Jobs</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
0%, 100% { opacity: 0.85; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.1); }
|
||||
}
|
||||
.home-wordmark-svg { width: min(380px, 70vw); height: auto; filter: drop-shadow(0 16px 30px oklch(35% 0.18 266 / 0.45)); margin-bottom: -10px; }
|
||||
.home-wordmark {
|
||||
font-size: 36px; font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
|
|
@ -213,7 +214,16 @@
|
|||
<img src="img/ampp-safe.png?v=hardhat3" alt="Zac in hardhat">
|
||||
<span class="home-portrait-dot" title="On duty"></span>
|
||||
</div>
|
||||
<h1 class="home-wordmark">Z<span class="accent">-</span>AMPP</h1>
|
||||
<svg class="home-wordmark-svg" viewBox="0 0 540 132" xmlns="http://www.w3.org/2000/svg" aria-label="Z-AMPP">
|
||||
<defs>
|
||||
<linearGradient id="zwm" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="oklch(82% 0.14 220)"/>
|
||||
<stop offset="0.55" stop-color="oklch(58% 0.18 245)"/>
|
||||
<stop offset="1" stop-color="oklch(32% 0.16 265)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<text x="0" y="108" font-family="Inter, system-ui, sans-serif" font-weight="900" font-size="132" letter-spacing="-8" fill="url(#zwm)">Z-AMPP</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="home-tagline">Please select an option below to get started</div>
|
||||
|
|
@ -357,6 +367,21 @@
|
|||
</div>
|
||||
<div class="home-card-desc">Track proxy generation, thumbnails, and AMPP folder sync as they run.</div>
|
||||
</a>
|
||||
|
||||
<a href="tokens.html" class="home-card" title="Just kidding">
|
||||
<div class="home-card-title">Tokens</div>
|
||||
<div class="home-card-preview">
|
||||
<svg class="preview-art" viewBox="0 0 200 125" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="0" width="200" height="125" fill="oklch(13% 0.02 250)"/>
|
||||
<g stroke="oklch(45% 0.10 266 / 0.5)" stroke-width="0.5"><line x1="14" y1="100" x2="186" y2="100"/><line x1="14" y1="80" x2="186" y2="80" stroke-dasharray="2,3"/><line x1="14" y1="60" x2="186" y2="60" stroke-dasharray="2,3"/></g>
|
||||
<polyline points="14,90 38,72 62,80 86,55 110,62 134,40 158,48 186,28" fill="none" stroke="oklch(70% 0.18 200)" stroke-width="2"/>
|
||||
<polyline points="14,95 38,86 62,90 86,76 110,80 134,68 158,72 186,58" fill="none" stroke="oklch(62% 0.15 145)" stroke-width="2"/>
|
||||
<circle cx="186" cy="28" r="3" fill="oklch(62% 0.22 25)"/>
|
||||
</svg>
|
||||
<span class="home-card-stats"><span class="home-card-stats-dot" style="background:oklch(62% 0.22 25);box-shadow:0 0 6px oklch(62% 0.22 25)"></span><b id="tokenBurn">—</b> burning</span>
|
||||
</div>
|
||||
<div class="home-card-desc">Token-metered pricing parody. Click for a giggle. (You actually pay $0.)</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -402,6 +427,7 @@
|
|||
setText('jobCount', arr.length);
|
||||
setText('ingestCount', arr.filter(x => (x.type || '').toLowerCase().includes('proxy')).length || arr.length);
|
||||
}
|
||||
const tb = document.getElementById('tokenBurn'); if (tb) { tb.textContent = (14000 + Math.round(Math.random() * 8000)).toLocaleString(); }
|
||||
} catch (_) { /* leave dashes */ }
|
||||
}
|
||||
loadStats();
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
|
||||
Jobs
|
||||
</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
<a href="edit.html" class="nav-item" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
|
||||
Editor
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
|
||||
Jobs
|
||||
</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
<a href="edit.html" class="nav-item" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
|
||||
Editor
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@
|
|||
<a href="recorders.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="4" width="10" height="8" rx="1"/><path d="M11 7l4-2v6l-4-2"/></svg>Recorders</a>
|
||||
<a href="capture.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><circle cx="8" cy="8" r="6.5"/></svg>Capture</a>
|
||||
<a href="jobs.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>Jobs</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
<a href="edit.html" class="nav-item" ><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>Editor</a>
|
||||
</nav>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
|
||||
Jobs
|
||||
</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
<a href="edit.html" class="nav-item" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
|
||||
Editor
|
||||
|
|
|
|||
|
|
@ -268,6 +268,50 @@
|
|||
.tok-row > :nth-child(4), .tok-row > :nth-child(5) { display: none; }
|
||||
.tok-title { font-size: 30px; }
|
||||
}
|
||||
|
||||
/* ─ Token burn chart ─ */
|
||||
.tok-chart {
|
||||
background: oklch(11% 0.020 220 / 0.7);
|
||||
border: 1px solid oklch(35% 0.06 215 / 0.5);
|
||||
border-radius: 12px;
|
||||
padding: 20px 22px 22px;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.tok-chart-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tok-stat {
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
padding: 10px 12px;
|
||||
background: oklch(8% 0.015 220 / 0.7);
|
||||
border: 1px solid oklch(35% 0.06 215 / 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tok-stat-label { font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-tertiary); }
|
||||
.tok-stat-value { font-family: var(--font-mono); font-size: 20px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.01em; }
|
||||
.tok-stat-delta { font-size: 10px; font-weight: 600; color: var(--text-tertiary); letter-spacing: 0.04em; }
|
||||
.tok-stat-delta.hot { color: oklch(70% 0.18 25); }
|
||||
.tok-stat-delta.cold { color: oklch(70% 0.14 145); }
|
||||
.tok-chart-frame {
|
||||
position: relative;
|
||||
background: oklch(8% 0.015 220 / 0.7);
|
||||
border: 1px solid oklch(35% 0.06 215 / 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
.tok-chart-svg { width: 100%; height: 260px; display: block; }
|
||||
.tok-chart-legend {
|
||||
display: flex; flex-wrap: wrap; gap: 18px;
|
||||
margin-top: 10px;
|
||||
font-size: 11px; color: var(--text-secondary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.tok-chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.tok-chart-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -478,6 +522,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage chart -->
|
||||
<div class="tok-section-head">
|
||||
<span class="tok-section-title">Current Token Burn</span>
|
||||
<span class="tok-section-hint" id="chartHint">Last 14 days · nightly true-up · TVM applied</span>
|
||||
</div>
|
||||
<div class="tok-chart">
|
||||
<div class="tok-chart-stats">
|
||||
<div class="tok-stat"><span class="tok-stat-label">MTD burn</span><span class="tok-stat-value" id="statMtd">—</span><span class="tok-stat-delta hot" id="statMtdDelta">+0%</span></div>
|
||||
<div class="tok-stat"><span class="tok-stat-label">Forecast EOM</span><span class="tok-stat-value" id="statEom">—</span><span class="tok-stat-delta hot" id="statEomDelta">over plan</span></div>
|
||||
<div class="tok-stat"><span class="tok-stat-label">TVM (live)</span><span class="tok-stat-value" id="statTvm">—</span><span class="tok-stat-delta" id="statTvmTrend">stable</span></div>
|
||||
<div class="tok-stat"><span class="tok-stat-label">Peak draw</span><span class="tok-stat-value" id="statPeak">—</span><span class="tok-stat-delta cold" id="statPeakDay">Wed</span></div>
|
||||
</div>
|
||||
<div class="tok-chart-frame">
|
||||
<svg class="tok-chart-svg" id="burnChart" viewBox="0 0 800 220" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||
<div class="tok-chart-legend">
|
||||
<span><i style="background:oklch(70% 0.18 200)"></i>Ingest</span>
|
||||
<span><i style="background:oklch(62% 0.15 145)"></i>Recorders</span>
|
||||
<span><i style="background:oklch(70% 0.18 80)"></i>Render</span>
|
||||
<span><i style="background:oklch(62% 0.22 25)"></i>Overage</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculator -->
|
||||
<div class="tok-calc">
|
||||
<div class="tok-calc-head">
|
||||
|
|
@ -557,6 +624,82 @@
|
|||
}
|
||||
document.querySelectorAll('.tok-calc-field input').forEach(i => i.addEventListener('input', calc));
|
||||
calc();
|
||||
|
||||
async function renderBurnChart() {
|
||||
let realJobs = 0, realAssets = 0;
|
||||
try {
|
||||
const [jRes, aRes] = await Promise.all([
|
||||
fetch('/api/v1/jobs', { credentials: 'include' }),
|
||||
fetch('/api/v1/assets?limit=1', { credentials: 'include' }),
|
||||
]);
|
||||
if (jRes.ok) realJobs = (await jRes.json()).length;
|
||||
if (aRes.ok) realAssets = (await aRes.json()).total || 0;
|
||||
} catch (_) {}
|
||||
const N = 14;
|
||||
const days = [];
|
||||
const today = new Date();
|
||||
for (let i = N - 1; i >= 0; i--) {
|
||||
const d = new Date(today); d.setDate(d.getDate() - i);
|
||||
days.push(d);
|
||||
}
|
||||
function rng(seed) { let x = Math.sin(seed) * 10000; return x - Math.floor(x); }
|
||||
const series = days.map((d, i) => {
|
||||
const baseline = 8000 + realAssets * 18 + realJobs * 12;
|
||||
const wk = (d.getDay() === 0 || d.getDay() === 6) ? 0.55 : 1.0;
|
||||
const wave = 1 + 0.45 * Math.sin(i * 0.9) + 0.18 * Math.cos(i * 1.7);
|
||||
const noise = 0.8 + 0.4 * rng(i * 13 + 7);
|
||||
const total = Math.round(baseline * wk * wave * noise);
|
||||
const ingest = Math.round(total * (0.35 + 0.05 * rng(i * 3 + 1)));
|
||||
const recorders = Math.round(total * (0.30 + 0.04 * rng(i * 5 + 2)));
|
||||
const render = Math.round(total * (0.20 + 0.04 * rng(i * 7 + 3)));
|
||||
const overage = Math.max(0, total - ingest - recorders - render);
|
||||
return { d, ingest, recorders, render, overage, total };
|
||||
});
|
||||
const max = Math.max(...series.map(s => s.total));
|
||||
const W = 800, H = 220, P = 28;
|
||||
const bw = (W - P * 2) / N;
|
||||
const layers = [
|
||||
{ key: 'ingest', color: 'oklch(70% 0.18 200)' },
|
||||
{ key: 'recorders', color: 'oklch(62% 0.15 145)' },
|
||||
{ key: 'render', color: 'oklch(70% 0.18 80)' },
|
||||
{ key: 'overage', color: 'oklch(62% 0.22 25)' },
|
||||
];
|
||||
const bars = series.map((s, i) => {
|
||||
const x = P + i * bw + 4;
|
||||
let yAcc = H - P;
|
||||
const stack = layers.map(l => {
|
||||
const v = s[l.key] || 0;
|
||||
const h = (v / max) * (H - P * 2);
|
||||
yAcc -= h;
|
||||
return '<rect x="' + x + '" y="' + yAcc + '" width="' + (bw - 8) + '" height="' + h + '" fill="' + l.color + '" opacity="0.92" rx="1"/>';
|
||||
}).join('');
|
||||
const label = s.d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
|
||||
const lbl = i % 2 === 0 ? '<text x="' + (x + (bw-8)/2) + '" y="' + (H - 8) + '" text-anchor="middle" font-size="10" fill="oklch(55% 0.04 215)" font-family="var(--font-mono)">' + label + '</text>' : '';
|
||||
return stack + lbl;
|
||||
}).join('');
|
||||
let grid = '';
|
||||
for (let k = 0; k <= 4; k++) {
|
||||
const y = P + (H - P * 2) * (k / 4);
|
||||
grid += '<line x1="' + P + '" y1="' + y + '" x2="' + (W - P + 12) + '" y2="' + y + '" stroke="oklch(35% 0.06 215 / 0.25)" stroke-width="0.5"/>';
|
||||
const tick = Math.round(max * (1 - k / 4));
|
||||
grid += '<text x="' + (W - P + 16) + '" y="' + (y + 3) + '" font-size="9" fill="oklch(50% 0.04 215)" font-family="var(--font-mono)">' + tick.toLocaleString() + '</text>';
|
||||
}
|
||||
document.getElementById('burnChart').innerHTML = grid + bars;
|
||||
const mtd = series.reduce((a, s) => a + s.total, 0);
|
||||
const eom = Math.round(mtd * 2.3);
|
||||
const peakIdx = series.reduce((max, s, i, arr) => s.total > arr[max].total ? i : max, 0);
|
||||
const tvm = (0.92 + 0.6 * rng(Date.now() / 60000 | 0)).toFixed(2);
|
||||
document.getElementById('statMtd').textContent = mtd.toLocaleString();
|
||||
document.getElementById('statMtdDelta').textContent = '+' + Math.round(rng(1) * 40 + 15) + '% vs prior period';
|
||||
document.getElementById('statEom').textContent = eom.toLocaleString();
|
||||
document.getElementById('statEomDelta').textContent = '+340% over plan';
|
||||
document.getElementById('statTvm').textContent = tvm + 'x';
|
||||
document.getElementById('statTvmTrend').textContent = parseFloat(tvm) > 1.2 ? 'spiking' : 'stable';
|
||||
document.getElementById('statPeak').textContent = series[peakIdx].total.toLocaleString();
|
||||
document.getElementById('statPeakDay').textContent = series[peakIdx].d.toLocaleDateString('en', { weekday: 'short' });
|
||||
}
|
||||
renderBurnChart();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@
|
|||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
|
||||
Jobs
|
||||
</a>
|
||||
<a href="tokens.html" class="nav-item"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6"/><path d="M5.5 9.5h3a1.5 1.5 0 0 0 0-3h-1a1.5 1.5 0 0 1 0-3h3M8 3v1m0 8v1"/></svg>Tokens</a>
|
||||
<a href="edit.html" class="nav-item" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
|
||||
Editor
|
||||
|
|
|
|||
Loading…
Reference in a new issue