Files
Pete/internal/web/templates/who.html
prosolis 7d2d9910cf web: make the night phase's text colours survive a dark card
The adventure purple was unreadable on night, and measuring the family showed
it was not alone: every one of the eleven .text-theme-* colours lands between
1.08:1 (eu, effectively invisible) and 3.12:1 (finance) on night's #2d365a.
They were all picked against a light card and never checked against a dark
one. Same hue, lifted lightness, saturation floored so the dull ones stay a
colour instead of going grey — all now >=5.5:1.

Night only. Dusk and dawn are lit cards despite their names, so they keep
today's values exactly and the light phases are pixel-identical. Lego can't
stay pillar-box red and be legible on navy; a red light enough to pass reads
as salmon, which is the honest trade against a red nobody can see.

Also adds --warn as a phase variable for the new "inert" chip, for the same
reason --ink is one. Tailwind's dark: variant cannot do this job: darkMode is
unconfigured, so dark: follows the OS's prefers-color-scheme, which knows
nothing about which phase Pete is showing. That mismatch left the chip at
2.34:1 on a dark phase under a light OS, and 1.55:1 on a light phase under a
dark OS — half the combinations unreadable, decided by a setting outside the
page. Now 4.52-7.04 across all four phases, OS irrelevant.

Still open, and NOT fixed here: text-[color:var(--ink)]/NN appears not to
compile — there is no such rule in output.css, and the item description
computes to a fixed rgb(74,46,42) that ignores the phase entirely. If that
holds it is a pre-existing site-wide no-op, not confined to these panels.
Recorded rather than guessed at.
2026-07-17 07:28:05 -07:00

331 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "title"}}{{.Mark.Name}} — {{.SiteTitle}}{{end}}
{{/* One item, wherever it's sitting. Worn, backpack, and vault all show the
same facts; only the panel around them differs. The description and the
effect line come from gogobee already resolved — Pete renders them, it does
not compute them. "inert" is the honest word for a worn item that wants a
bond and hasn't got one: it's on you and doing nothing. */}}
{{define "itemrow"}}
<li>
<div class="flex items-baseline justify-between gap-3">
<span class="flex-1 font-semibold">{{.Name}}{{if .Temper}} <span class="text-theme-adventure">+{{.Temper}}</span>{{end}}</span>
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">{{if .Tier}}T{{.Tier}}{{end}}{{if .Value}}{{if .Tier}} · {{end}}{{.Value}}g{{end}}</span>
</div>
{{if or .Slot .SkillSource .Attunement}}
<div class="flex flex-wrap gap-1.5 mt-1">
{{if .Slot}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/5 px-2 py-0.5 text-[color:var(--ink)]/60">{{.Slot}}</span>{{end}}
{{if .SkillSource}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/5 px-2 py-0.5 text-[color:var(--ink)]/60">{{.SkillSource}}</span>{{end}}
{{if .Attunement}}{{if .Attuned}}<span class="text-[11px] rounded-full bg-theme-adventure/20 px-2 py-0.5 text-theme-adventure font-semibold">bonded</span>{{else if .Worn}}<span class="text-[11px] rounded-full bg-amber-400/20 px-2 py-0.5 text-[color:var(--warn)] font-semibold">inert · no bond free</span>{{else}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/10 px-2 py-0.5 text-[color:var(--ink)]/60">needs a bond</span>{{end}}{{end}}
</div>
{{end}}
{{if .Desc}}<p class="text-xs text-[color:var(--ink)]/55 mt-1 leading-snug">{{.Desc}}</p>{{end}}
{{if .Effect}}<p class="text-xs text-theme-adventure/80 mt-0.5">{{.Effect}}</p>{{end}}
</li>
{{end}}
{{define "main"}}
<article class="mt-2 mb-10 max-w-3xl mx-auto" id="who" data-token="{{.Mark.Token}}">
<nav class="mb-4">
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
<span aria-hidden="true"></span> The board
</a>
</nav>
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden">
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">{{if .Mark.OnRun}}⚔{{else}}🏠{{end}}</div>
<div class="relative">
<p class="text-sm uppercase tracking-[0.2em] opacity-80">adventurer</p>
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">{{.Mark.Name}}</h1>
<p class="mt-2 opacity-90">Level {{.Mark.Level}} {{.Mark.ClassRace}}</p>
<p class="mt-4 text-xs uppercase tracking-wider opacity-75" id="who-where">
{{if .Mark.OnRun}}{{.Mark.Where}}{{else}}In town{{if .Mark.Idle}} · {{.Mark.Idle}}{{end}}{{end}}
</p>
</div>
</header>
{{if .HasDetail}}
<!-- Stats + gear: public, the same anonymity model as the board. -->
<section class="mt-8 grid gap-6 sm:grid-cols-2">
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
<h2 class="font-display text-xl font-bold mb-4">Vitals</h2>
<div class="flex items-baseline justify-between">
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Hit points</span>
<span class="font-semibold" id="who-hp">{{.Detail.HPCurrent}} / {{.Detail.HPMax}}{{if .Detail.TempHP}} <span class="text-theme-adventure">+{{.Detail.TempHP}}</span>{{end}}</span>
</div>
<div class="mt-2 h-2 rounded-full bg-[color:var(--ink)]/10 overflow-hidden">
<div class="h-full rounded-full bg-theme-adventure transition-all" id="who-hp-bar" style="width:0%"></div>
</div>
<div class="mt-4 flex items-baseline justify-between">
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Armor class</span>
<span class="font-semibold" id="who-ac">{{.Detail.ArmorClass}}</span>
</div>
<div class="mt-5 grid grid-cols-3 gap-2">
{{range .Abilities}}
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50">{{.Label}}</div>
<div class="font-display text-lg font-bold leading-none mt-1">{{.Score}}</div>
<div class="text-xs text-[color:var(--ink)]/60">{{.ModStr}}</div>
</div>
{{end}}
</div>
{{if .Mark.OnRun}}
<div class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 space-y-1.5 text-sm" id="who-expedition">
{{if .Detail.Room}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Room</span><span class="font-semibold" id="who-room">{{.Detail.Room}}</span></div>{{end}}
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Supplies</span><span class="font-semibold" id="who-supplies">{{.Detail.Supplies}}</span></div>
{{if .Detail.ThreatLevel}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Threat</span><span class="font-semibold" id="who-threat">{{.Detail.ThreatLevel}}</span></div>{{end}}
</div>
{{end}}
</div>
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
<h2 class="font-display text-xl font-bold mb-4">Gear</h2>
{{if .Detail.Gear}}
<ul class="space-y-2">
{{range .Detail.Gear}}
<li class="flex items-baseline justify-between gap-3">
<span class="text-xs uppercase tracking-wider text-[color:var(--ink)]/45 w-16 shrink-0">{{.Slot}}</span>
<span class="font-semibold flex-1">{{.Name}}{{if .Masterwork}} <span title="masterwork"></span>{{end}}</span>
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
</li>
{{end}}
</ul>
{{else}}
<p class="text-sm text-[color:var(--ink)]/50">Traveling light — nothing equipped.</p>
{{end}}
</div>
</section>
{{else}}
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
<p class="text-sm text-[color:var(--ink)]/60">No detailed sheet on file for this adventurer yet — check back after the next snapshot.</p>
</section>
{{end}}
{{if .HasHistory}}
<!-- The record. Public, like the dispatches it's counted from — this is the
same information the /adventure feed already printed, only as numbers
instead of as forty separate sentences. Not a gogobee snapshot: Pete
counted these itself out of the facts it kept. -->
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
<h2 class="font-display text-xl font-bold mb-4">The record</h2>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.BossKills}}</div>
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">bosses down</div>
</div>
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.ZoneClears}}</div>
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">zones cleared</div>
</div>
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Deaths}}</div>
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">death{{if ne .Trophies.Deaths 1}}s{{end}}</div>
</div>
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Retreats}}</div>
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">walked out</div>
</div>
</div>
{{if or .Trophies.BossFirsts .Trophies.ZoneFirsts}}
<p class="mt-3 text-sm text-theme-adventure font-semibold">
★ {{.Trophies.BossFirsts}} realm-first boss{{if ne .Trophies.BossFirsts 1}}es{{end}}{{if .Trophies.ZoneFirsts}}, {{.Trophies.ZoneFirsts}} first clear{{if ne .Trophies.ZoneFirsts 1}}s{{end}}{{end}} — nobody had done it before.
</p>
{{end}}
<div class="mt-6 grid gap-6 sm:grid-cols-2">
{{if .Trophies.Bosses}}
<div>
<h3 class="font-display text-lg font-bold mb-3">Bosses fought</h3>
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
{{range .Trophies.Bosses}}
<li class="flex items-baseline justify-between gap-3">
<span class="flex-1">{{.Boss}}{{if .First}} <span class="text-theme-adventure" title="first in the realm to clear it"></span>{{end}}</span>
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">×{{.Kills}}</span>
</li>
{{end}}
</ul>
</div>
{{end}}
{{if .Trophies.Zones}}
<div>
<h3 class="font-display text-lg font-bold mb-3">Ground covered</h3>
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
{{range .Trophies.Zones}}
<li class="flex items-baseline justify-between gap-3">
<span class="flex-1">{{.Zone}}{{if .First}} <span class="text-theme-adventure" title="first clear in the realm"></span>{{end}}{{if .Region}} <span class="text-[color:var(--ink)]/45">{{.Region}}</span>{{end}}</span>
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">×{{.Clears}}</span>
</li>
{{end}}
</ul>
</div>
{{end}}
</div>
{{if .Trophies.Milestones}}
<div class="mt-6 pt-4 border-t border-[color:var(--ink)]/10">
<h3 class="font-display text-lg font-bold mb-3">Milestones</h3>
<div class="flex flex-wrap gap-2">
{{range .Trophies.Milestones}}
<span class="rounded-full bg-theme-adventure/10 text-theme-adventure text-xs font-semibold px-3 py-1">🏅 {{.}}</span>
{{end}}
</div>
</div>
{{end}}
</section>
<!-- The trail: every fact we have, newest first, each linking to the dispatch
that told it. -->
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
<h2 class="font-display text-xl font-bold mb-4">The trail</h2>
<ol class="relative border-l-2 border-[color:var(--ink)]/10 ml-3 space-y-4">
{{range .Timeline}}
<li class="relative pl-6">
<span class="absolute -left-[13px] top-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-[color:var(--card)] border-2 {{if .Notable}}border-theme-adventure{{else}}border-[color:var(--ink)]/10{{end}} text-xs" aria-hidden="true">{{.Emoji}}</span>
<a href="{{.Permalink}}" class="group block">
<div class="flex items-baseline justify-between gap-3">
<span class="font-semibold group-hover:text-theme-adventure transition {{if .Notable}}text-theme-adventure{{end}}">{{.Label}}</span>
<span class="text-xs text-[color:var(--ink)]/45 shrink-0">{{.When}}</span>
</div>
{{if .Line}}<p class="text-sm text-[color:var(--ink)]/60 mt-0.5">{{.Line}}</p>{{end}}
</a>
</li>
{{end}}
</ol>
{{if .MoreHistory}}
<p class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 text-xs text-[color:var(--ink)]/45">
Showing the most recent {{len .Timeline}}. The counts above cover everything.
</p>
{{end}}
</section>
{{end}}
{{if .HasSelf}}
<!-- Owner-only: this is you. Inventory, vault, house, pets — private, served
only because your signed-in localpart owns this page's token. -->
<section class="mt-8">
<div class="flex items-center gap-2 mb-3">
<span class="rounded-full bg-theme-adventure text-white text-xs font-semibold px-3 py-1">Your adventurer</span>
<span class="text-xs text-[color:var(--ink)]/45">only you can see the panels below</span>
</div>
<div class="grid gap-6 sm:grid-cols-2">
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
<h2 class="font-display text-xl font-bold mb-4">Home</h2>
<div class="space-y-1.5 text-sm">
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">House tier</span><span class="font-semibold">{{if .Self.House.Tier}}{{.Self.House.Tier}}{{else}}no house{{end}}</span></div>
{{if .Self.House.LoanBalance}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Loan balance</span><span class="font-semibold">{{.Self.House.LoanBalance}}</span></div>{{end}}
{{if .Self.House.Rate}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Rate</span><span class="font-semibold">{{.Self.House.Rate}}</span></div>{{end}}
{{if .Self.House.Autopay}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Autopay</span><span class="font-semibold">on</span></div>{{end}}
</div>
<h3 class="font-display text-lg font-bold mt-6 mb-3">Pets</h3>
{{if .Self.Pets}}
<ul class="space-y-2 text-sm">
{{range .Self.Pets}}
<li class="flex items-baseline justify-between gap-3">
<span class="font-semibold flex-1">{{if .Name}}{{.Name}}{{else}}your {{.Type}}{{end}} <span class="text-[color:var(--ink)]/45 font-normal">{{.Type}}</span></span>
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">lv {{.Level}}{{if .ArmorTier}} · barding T{{.ArmorTier}}{{end}}</span>
</li>
{{end}}
</ul>
{{else}}
<p class="text-sm text-[color:var(--ink)]/50">No pets right now.</p>
{{end}}
</div>
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
{{if .Worn}}
<h2 class="font-display text-xl font-bold mb-1">Worn</h2>
<p class="text-xs text-[color:var(--ink)]/45 mb-3">{{.BondsUsed}} of 3 bonds in use</p>
<ul class="space-y-2.5 text-sm">
{{range .Worn}}{{template "itemrow" .}}{{end}}
</ul>
<h3 class="font-display text-lg font-bold mt-6 mb-3">Backpack</h3>
{{else}}
<h2 class="font-display text-xl font-bold mb-4">Backpack</h2>
{{end}}
{{if .Backpack}}
<ul class="space-y-2.5 text-sm max-h-72 overflow-y-auto pr-1">
{{range .Backpack}}{{template "itemrow" .}}{{end}}
</ul>
{{else}}
<p class="text-sm text-[color:var(--ink)]/50">Backpack's empty.</p>
{{end}}
{{if .VaultRows}}
<h3 class="font-display text-lg font-bold mt-6 mb-3">Vault</h3>
<ul class="space-y-2.5 text-sm max-h-52 overflow-y-auto pr-1">
{{range .VaultRows}}{{template "itemrow" .}}{{end}}
</ul>
{{end}}
</div>
</div>
</section>
{{end}}
</article>
<script>
// The sheet is state, like the board: keep an open tab honest without a reload.
// Public detail only (HP, room, supplies) — the private panels stay as rendered.
(function () {
var root = document.getElementById('who');
if (!root) return;
var token = root.getAttribute('data-token');
function setBar() {
var el = document.getElementById('who-hp-bar');
var hp = document.getElementById('who-hp');
if (!el || !hp) return;
var m = hp.textContent.match(/(-?\d+)\s*\/\s*(\d+)/);
if (!m) return;
var cur = parseInt(m[1], 10), max = parseInt(m[2], 10);
var pct = max > 0 ? Math.max(0, Math.min(100, Math.round(cur / max * 100))) : 0;
el.style.width = pct + '%';
}
setBar();
function txt(id, v) { var el = document.getElementById(id); if (el && v != null) el.textContent = v; }
var timer = null;
function refresh() {
fetch('/api/adventure/who/' + encodeURIComponent(token), { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (!data) return;
if (!data.live) {
// Off the board entirely (expedition ended, opted out): nothing more to
// poll for, so stop the timer rather than hammer a token that's gone.
txt('who-where', 'Back in town');
if (timer) clearInterval(timer);
return;
}
var d = data.detail || {};
var mark = data.mark || {};
if (data.has_detail) {
var temp = d.temp_hp ? ' +' + d.temp_hp : '';
txt('who-hp', d.hp_current + ' / ' + d.hp_max + temp);
txt('who-ac', d.armor_class);
txt('who-room', d.room);
txt('who-supplies', d.supplies);
txt('who-threat', d.threat_level);
setBar();
}
// Keep the location line honest in both directions: a mark that came back
// to town while still on the board must stop showing its old expedition.
if (mark.OnRun) {
txt('who-where', mark.Where);
} else {
txt('who-where', 'In town' + (mark.Idle ? ' · ' + mark.Idle : ''));
}
})
.catch(function () { /* transient — next tick will do */ });
}
timer = setInterval(refresh, 60000);
})();
</script>
{{end}}