Files
Pete/internal/web/templates/layout.html
prosolis c69fbb63db games: a blackjack table you can actually sit down at
The engine, the escrow and the wire were all in place; nothing had a browser on
the end of it. This is that end: a lobby, a table, and the five endpoints between
them.

The browser holds no game. It sends intents and gets back a view — the cards it
is entitled to see, and the script of how they arrived, one event per card off
the shoe. The dealer's hole card is not in the payload at all until the reveal,
because a field the client is told to ignore is a field somebody reads in
devtools. The shoe lives in game_live_hands, which also means a redeploy
mid-hand no longer costs a player their stake: the hand is still there when they
come back.

The money is ordered so nothing can be spent twice. The stake leaves the stack in
the same statement that checks it exists, before a card is dealt. Every new hand
is seated with a plain INSERT, so a double-clicked Deal is decided by the primary
key rather than by a read that raced — it loses, gets its chips back, and the
hand in progress is untouched. A double takes its raise up front and hands it
straight back if the engine refuses the move.

Cards are dealt rather than swapped in — they fly out of the shoe and turn over,
which was a requirement and not a flourish. The faces and the chips are still
plain; that's next.
2026-07-13 23:20:42 -07:00

328 lines
21 KiB
HTML

{{define "layout"}}<!doctype html>
<html lang="en" data-phase="{{if .Phase}}{{.Phase}}{{else}}day{{end}}"{{if .Phase}} data-phase-lock{{end}} data-season="{{.Weather.Season}}" data-weather="{{.Weather.Variant}}" data-intensity="{{.Weather.Intensity}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" .}}{{.SiteTitle}}{{end}}</title>
{{if .NoIndex}}<meta name="robots" content="noindex">{{end}}
{{if .OGImage}}<meta property="og:image" content="{{.OGImage}}"><meta name="twitter:card" content="summary_large_image">{{end}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/output.css">
<link rel="icon" href="/static/img/pete.avif" type="image/avif">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#fbf3e3">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Pete">
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png">
<link rel="alternate" type="application/rss+xml" title="{{.SiteTitle}} — all stories" href="/feed.xml">
<link rel="alternate" type="application/feed+json" title="{{.SiteTitle}} — all stories" href="/feed.json">
{{if .Active}}{{if ne .Active "bookmarks"}}
<link rel="alternate" type="application/rss+xml" title="{{.SiteTitle}} — {{.Active}}" href="/{{.Active}}/feed.xml">
{{end}}{{end}}
<script>
// Pick a palette phase from the browser clock and update it each minute.
(function () {
function phase(h) {
if (h >= 5 && h < 8) return "dawn";
if (h >= 8 && h < 17) return "day";
if (h >= 17 && h < 20) return "dusk";
return "night";
}
function apply() {
if (document.documentElement.hasAttribute("data-phase-lock")) return;
document.documentElement.dataset.phase = phase(new Date().getHours());
}
apply();
setInterval(apply, 60000);
})();
</script>
</head>
<body class="min-h-screen bg-[color:var(--bg)] text-[color:var(--ink)] transition-colors duration-1000">
<div class="pointer-events-none fixed inset-0 -z-10 bg-[color:var(--bg-grad)] transition-colors duration-1000"></div>
<canvas id="pete-weather" class="pointer-events-none fixed inset-0 -z-[5] h-full w-full" aria-hidden="true"></canvas>
<header class="mx-auto max-w-6xl px-4 pt-8 pb-4 sm:pt-12">
<div class="flex items-center justify-between gap-3 sm:gap-4">
<a href="/" class="flex items-center gap-3 group min-w-0">
<img src="/static/img/pete.avif" alt="Pete" width="48" height="48"
class="h-12 w-12 shrink-0 rounded-2xl bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 object-cover group-hover:-rotate-6 transition-transform">
<span class="font-display text-3xl font-bold tracking-tight">{{.SiteTitle}}</span>
<span class="hidden sm:inline rounded-full bg-[color:var(--accent)]/20 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/70" data-phase-label>day</span>
</a>
{{if .AuthEnabled}}
{{if .User}}
<a href="/auth/logout" data-account
title="Signed in as {{.User.Display}}{{if .User.Email}} · {{.User.Email}}{{end}} — sign out"
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<span class="grid h-6 w-6 place-items-center rounded-full bg-[color:var(--accent)] text-white text-xs font-bold">{{.User.Initial}}</span>
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
</a>
{{else}}
<a href="/auth/login?next={{.Path}}" data-signin
title="Sign in to sync your preferences"
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-[color:var(--ink)]/60">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
<polyline points="10 17 15 12 10 7"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
<span class="hidden sm:inline">Sign in</span>
</a>
{{end}}
{{end}}
</div>
<div class="mt-3 flex flex-wrap items-center gap-2 sm:gap-3">
<button type="button" data-settings-trigger
title="Feed settings"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span class="sr-only">Feed settings</span>
</button>
<button type="button" data-reader-open
title="Feed mode — read one story at a time (press f)"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
<path d="M2 4h6a3 3 0 0 1 3 3v13a2.5 2.5 0 0 0-2.5-2.5H2z"></path>
<path d="M22 4h-6a3 3 0 0 0-3 3v13a2.5 2.5 0 0 1 2.5-2.5H22z"></path>
</svg>
<span class="sr-only">Feed mode</span>
</button>
{{if .Weather.Variant}}
<button type="button" data-weather-toggle aria-pressed="true"
title="Toggle weather animation"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" class="h-5 w-5 transition-colors"
data-weather-star fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
<polygon points="12,2.7 14.85,9.27 21.95,10.05 16.6,14.83 18.18,21.8 12,18.13 5.82,21.8 7.4,14.83 2.05,10.05 9.15,9.27"/>
</svg>
<span class="sr-only">Toggle weather animation</span>
</button>
{{end}}
<button type="button" data-weather-loc
title="Set your location"
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<span data-weather-chip class="tabular-nums">📍 Weather</span>
</button>
<span data-aqi-chip
class="hidden shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 tabular-nums"></span>
<a href="/status" data-status-link
title="Source health"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition{{if eq .Active "status"}} bg-[color:var(--accent)]/20{{end}}">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
<path d="M3 12h4l2 6 4-14 2 8h6"></path>
</svg>
<span class="sr-only">Source health</span>
</a>
{{if .User}}
<a href="/bookmarks" data-bookmarks-link
title="Your bookmarks"
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition{{if eq .Active "bookmarks"}} bg-[color:var(--accent)]/20{{end}}">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
<path d="M6 2h12a1 1 0 0 1 1 1v18l-7-4-7 4V3a1 1 0 0 1 1-1z"></path>
</svg>
<span class="sr-only">Bookmarks</span>
</a>
{{end}}
<div class="flex basis-full items-center gap-2 min-w-0 max-w-full sm:basis-auto sm:ml-auto">
<button type="button" data-search-trigger title="Search (⌘K)"
class="flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-[color:var(--ink)]/60">
<circle cx="11" cy="11" r="7"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<span class="hidden sm:inline text-[color:var(--ink)]/60 font-semibold">Search</span>
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
</button>
<nav class="pete-channel-nav flex min-w-0 items-center gap-1 sm:gap-2 overflow-x-auto rounded-full bg-[color:var(--card)] p-1 shadow-pete border-2 border-[color:var(--ink)]/10">
{{$active := .Active}}
{{if .User}}
<a href="/for-you"
title="Your personalized feed"
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
{{if eq $active "for-you"}}bg-[color:var(--accent)] text-[#1c1305] shadow-sm{{else}}hover:bg-[color:var(--ink)]/5{{end}}">
<span aria-hidden="true" class="mr-1"></span><span class="hidden sm:inline">For you</span>
</a>
{{end}}
{{range .Channels}}
<a href="/{{.Slug}}"
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
{{if eq $active .Slug}}bg-theme-{{.Theme}} text-white shadow-sm{{else}}hover:bg-[color:var(--ink)]/5{{end}}">
<span aria-hidden="true" class="mr-1">{{.Emoji}}</span><span class="hidden sm:inline">{{.Title}}</span>
</a>
{{end}}
</nav>
</div>
</div>
</header>
<div id="pete-search" class="hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
<div class="w-full max-w-2xl rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">
<div class="flex items-center gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
<input type="search" data-search-input autocomplete="off" spellcheck="false"
placeholder="Search headlines…"
class="flex-1 bg-transparent text-lg outline-none placeholder:text-[color:var(--ink)]/40">
</div>
<div data-search-list class="max-h-[60vh] overflow-y-auto p-3 space-y-1"></div>
<div class="px-5 py-3 border-t border-[color:var(--ink)]/10 flex items-center justify-between text-xs text-[color:var(--ink)]/50">
<span><kbd class="font-mono">↑↓</kbd> navigate · <kbd class="font-mono"></kbd> open · <kbd class="font-mono">esc</kbd> close</span>
<span data-search-meta></span>
</div>
</div>
</div>
<div id="pete-settings" class="hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
<div class="w-full max-w-md rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">
<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">
<h2 class="font-display text-lg font-bold">Feed settings</h2>
<button type="button" data-settings-close class="rounded-full px-2 py-1 text-sm text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5">esc</button>
</div>
{{if .PushEnabled}}{{if .User}}
<div data-push-section hidden class="px-5 pt-4 pb-1">
<div class="flex items-center justify-between gap-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-3">
<div class="min-w-0">
<div class="text-sm font-bold">🔔 Notifications</div>
<div data-push-note class="text-xs text-[color:var(--ink)]/60">Get a nudge when new stories land.</div>
</div>
<button type="button" data-push-toggle aria-pressed="false"
class="shrink-0 rounded-full bg-[color:var(--accent)] px-3 py-2 text-sm font-semibold text-[#1c1305] shadow-sm hover:brightness-105 transition disabled:opacity-50 disabled:cursor-not-allowed">
Turn on notifications
</button>
</div>
</div>
{{end}}{{end}}
<p class="px-5 pt-3 text-xs text-[color:var(--ink)]/60">Uncheck a feed to hide its stories. <span data-storage-note>Saved in this browser.</span></p>
<div data-settings-list class="max-h-[55vh] overflow-y-auto p-3 space-y-1"></div>
<div class="px-5 py-3 border-t border-[color:var(--ink)]/10 flex items-center justify-between text-xs">
<button type="button" data-settings-reset class="text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] font-semibold">Enable all</button>
<span data-settings-meta class="text-[color:var(--ink)]/50"></span>
</div>
</div>
</div>
<div id="pete-reader" class="hidden fixed inset-0 z-50" aria-modal="true" role="dialog" aria-label="Feed mode reader">
<div class="pete-reader-backdrop" data-reader-backdrop></div>
<div class="pete-reader-shell">
<div class="pete-reader-bar">
<span class="pete-reader-progress tabular-nums" data-reader-progress></span>
<div class="pete-reader-nav">
<button type="button" data-reader-prev class="pete-reader-btn" title="Previous (←)" aria-label="Previous article"></button>
<button type="button" data-reader-next class="pete-reader-btn" title="Next (→)" aria-label="Next article"></button>
<button type="button" data-reader-listen class="pete-reader-btn" style="display:none" title="Read aloud (r)" aria-label="Read this story aloud" aria-pressed="false">🔊 Listen</button>
<button type="button" data-reader-share class="pete-reader-btn" style="display:none" title="Share (s)" aria-label="Share this story">Share</button>
<div class="pete-reader-type">
<button type="button" data-reader-type class="pete-reader-btn" title="Text options (t)" aria-label="Text options" aria-expanded="false"><span style="font-size:1.05rem">A</span><span style="font-size:.8rem">a</span></button>
<div class="pete-reader-typemenu" data-reader-typemenu hidden>
<div class="pete-reader-typerow">
<span class="pete-reader-typelabel">Size</span>
<div class="pete-reader-typebtns" data-reader-size>
<button type="button" data-size="s" title="Small">S</button>
<button type="button" data-size="m" title="Medium">M</button>
<button type="button" data-size="l" title="Large">L</button>
<button type="button" data-size="xl" title="Extra large">XL</button>
</div>
</div>
<div class="pete-reader-typerow">
<span class="pete-reader-typelabel">Font</span>
<div class="pete-reader-typebtns" data-reader-font>
<button type="button" data-font="sans">Sans</button>
<button type="button" data-font="serif" style="font-family:Georgia,'Times New Roman',serif">Serif</button>
</div>
</div>
<div class="pete-reader-typerow">
<span class="pete-reader-typelabel">Paper</span>
<div class="pete-reader-typebtns" data-reader-paper>
<button type="button" data-paper="default">Default</button>
<button type="button" data-paper="sepia">Sepia</button>
</div>
</div>
<div class="pete-reader-typerow pete-reader-voicerow" data-reader-voicerow hidden>
<span class="pete-reader-typelabel">Voice</span>
<select class="pete-reader-voice" data-reader-voice aria-label="Read-aloud voice"></select>
</div>
</div>
</div>
<button type="button" data-reader-bookmark class="pete-reader-btn" style="display:none" title="Bookmark (b)" aria-label="Bookmark this story" aria-pressed="false">🔖 Save</button>
<a data-reader-link target="_blank" rel="noopener noreferrer" class="pete-reader-btn pete-reader-btn-open" title="Open original (o)">Open&nbsp;</a>
<button type="button" data-reader-close class="pete-reader-btn" title="Close (esc)" aria-label="Close reader">esc</button>
</div>
</div>
<div class="pete-reader-scroll" data-reader-scroll tabindex="-1">
<article class="pete-reader-article" data-reader-article></article>
<div class="pete-reader-related" data-reader-related hidden></div>
</div>
<div class="pete-reader-hint">
<span><kbd></kbd> <kbd></kbd> navigate · <kbd>o</kbd> open · <kbd>s</kbd> share · <kbd>t</kbd> text · <kbd>u</kbd> unread · <kbd>esc</kbd> close</span>
</div>
</div>
</div>
<main class="mx-auto max-w-6xl px-4 pb-24">
{{block "main" .}}{{end}}
</main>
<footer class="mx-auto max-w-6xl px-4 pb-10 text-center text-sm text-[color:var(--ink)]/50">
<img src="/static/img/pete.avif" alt="" width="20" height="20"
class="inline-block h-5 w-5 rounded-full object-cover align-[-4px] mr-1">
served by <span class="font-semibold">Pete</span> · also a Matrix bot · <span data-clock></span>
</footer>
<script>
// Tiny clock + phase label updater for the header pill / footer, plus Pete's
// time-of-day greeting on the home hero (whichever of these exist).
(function () {
function greeting(h) {
if (h >= 5 && h < 8) return "Early start? Here's what came in overnight.";
if (h >= 8 && h < 12) return "Morning! Here's what's fresh.";
if (h >= 12 && h < 17) return "Afternoon. Here's what's new.";
if (h >= 17 && h < 21) return "Evening. Here's what you missed today.";
return "Late one? Here's the quiet-hours roundup.";
}
function tick() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, "0");
var mm = String(now.getMinutes()).padStart(2, "0");
var el = document.querySelector("[data-clock]");
if (el) el.textContent = hh + ":" + mm;
var label = document.querySelector("[data-phase-label]");
if (label) label.textContent = document.documentElement.dataset.phase;
var greet = document.querySelector("[data-pete-greeting]");
if (greet) greet.textContent = greeting(now.getHours());
}
tick();
setInterval(tick, 30000);
})();
</script>
<script>
window.PETE_SOURCES = {{.AllSources}};
window.PETE_USER = {{if .User}}{ name: {{.User.Display}}, email: {{.User.Email}} }{{else}}null{{end}};
window.PETE_PREFS = {{.UserPrefs}};
window.PETE_PUSH = {{if .PushEnabled}}{ enabled: true, publicKey: {{.PushPublicKey}} }{{else}}null{{end}};
window.PETE_TTS = {{.TTS}};
</script>
<script src="/static/js/prefs.js" defer></script>
<script src="/static/js/weather-2d.js" defer></script>
<script src="/static/js/weather-gl.js" defer></script>
<script src="/static/js/weather.js" defer></script>
<script src="/static/js/weather-forecast.js" defer></script>
<script src="/static/js/search.js" defer></script>
<script src="/static/js/settings.js" defer></script>
<script src="/static/js/reader.js" defer></script>
<script src="/static/js/pwa.js" defer></script>
{{block "scripts" .}}{{end}}
</body>
</html>{{end}}