#!/usr/bin/env python3
"""Rebuilds ../101_launch_progress.html (launch work, visual). Run: python3 launch_progress_page.py
Add a new item = add a dict at the TOP of SECTIONS (newest first). Images: put downscaled jpgs in ../101_launch/.
Checklist counts + perf bars are read live from LAUNCH_CHECKLIST.md and the A22 summary files."""
import re, os, html
FB = os.path.expanduser('~/StickBrawl-v3/v4/feedback')
CL = FB + '/LAUNCH_CHECKLIST.md'
NEW = '/Volumes/Workspace/Scratch/coord/a22over2/summary.txt'
OLD = '/Volumes/Workspace/Scratch/coord/t7shader_162cf547d_usb/summary.txt'
IMG = '101_launch/'
E = html.escape

def fig(f, cap, wide=False):
    return f'<figure class="{"w" if wide else ""}"><a href="{IMG}{f}.jpg"><img loading="lazy" src="{IMG}{f}.jpg" alt="{E(cap)}"></a><figcaption>{E(cap)}</figcaption></figure>'

def row_text(rid):
    for l in open(CL, encoding='utf-8'):
        if l.startswith(f'- **{rid} '): return l.strip()
    return ''

def perf(path):
    d, sec = {}, None
    for l in open(path):
        if l.startswith('== '): sec = l[3:].strip()
        m = re.match(r'\[UITOUR\] idle (\w+): \d+ frames p50 ([\d.]+) p95 ([\d.]+)', l)
        if m and sec == 'fast': d[m.group(1)] = float(m.group(3))
    return d

def chart():
    new, old = perf(NEW), perf(OLD)
    pages = [p for p in new if p in old]
    W, rowh, left, scale = 900, 34, 120, 11.0
    H = 60 + rowh * len(pages)
    x33 = left + 33 * scale
    s = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Menu idle p95 frame time per page on the A22, old shader versus baked comic paper, with the 33 ms target line" class="chart">']
    for t in (0, 11, 22, 33, 44, 55):
        x = left + t * scale
        s.append(f'<line x1="{x}" y1="26" x2="{x}" y2="{H-8}" class="grid"/><text x="{x}" y="18" class="ax" text-anchor="middle">{t} ms</text>')
    for i, p in enumerate(pages):
        y = 34 + i * rowh
        s.append(f'<text x="{left-10}" y="{y+19}" class="lab" text-anchor="end">{p}</text>')
        o, n = old[p], new[p]
        s.append(f'<rect x="{left}" y="{y}" width="{o*scale}" height="13" rx="3" class="old"/><text x="{left+o*scale+6}" y="{y+11}" class="val">{o:g}</text>')
        s.append(f'<rect x="{left}" y="{y+15}" width="{n*scale}" height="13" rx="3" class="new"/><text x="{left+n*scale+6}" y="{y+26}" class="val">{n:g}</text>')
    s.append(f'<line x1="{x33}" y1="26" x2="{x33}" y2="{H-8}" class="tgt"/><text x="{x33+6}" y="{H-12}" class="tg">target 33 ms</text></svg>')
    ok = sum(1 for p in new if new[p] <= 33.3)
    return ''.join(s), ok, len(new), sum(1 for p in old if old[p] <= 33.3)

def status_strip():
    c = {' ': 0, '~': 0, 'x': 0, 'U': 0}
    for l in open(CL, encoding='utf-8'):
        m = re.match(r'- \*\*[A-Z0-9-]+ · \[(.)\] · P0', l)
        if m and m.group(1) in c: c[m.group(1)] += 1
    tot = sum(c.values())
    seg = [('x', 'done', c['x']), ('~', 'in progress', c['~']), (' ', 'open', c[' ']), ('U', 'waiting on you', c['U'])]
    bar = ''.join(f'<span class="seg s{k.strip() or "o"}" style="flex:{n}">{n}</span>' for k, _, n in seg if n)
    leg = ''.join(f'<span class="lg"><i class="sw s{k.strip() or "o"}"></i>{n} {lab}</span>' for k, lab, n in seg)
    wait, on = [], False
    for l in open(CL, encoding='utf-8'):
        if l.startswith('## Waiting on the user'): on = True; continue
        if on and l.startswith('## '): break
        if on and l.strip(): wait.append(re.sub(r'\*\*', '', l.strip().lstrip('- ')))
    wl = ''.join(f'<li>{E(w)}</li>' for w in wait)
    return f'<div class="strip">{bar}</div><div class="legend">{leg}<span class="lg">of {tot} P0 rows</span></div><h3>Waiting on the user</h3><ul class="wait">{wl}</ul>'

def sec(tag, title, body, state='ok'):
    return f'<section class="card"><div class="hd"><span class="tag {state}">{tag}</span><h2>{E(title)}</h2></div>{body}</section>'

def build_sections():
    S = []
    svg, ok, n, okold = chart()
    S.append(sec('LAUNCH STATUS', 'Checklist at a glance', status_strip(), 'st'))
    S.append(sec('ADVANCED', 'Bots ADVANCED screen fixed on no-notch screens',
        '<p class="lead">On screens without a notch the controls covered the labels. Now the layout is clean and the grid scrolls.</p>'
        '<div class="pair">' + fig('adv_before', 'BEFORE: controls cover the labels (uic_e6d683da8)', True) + fig('adv_after', 'AFTER: clean, the grid scrolls (uic_120e18c8d)', True) + '</div>', 'ok'))
    S.append(sec('ECONOMY', 'Economy: pass-gift exploit closed, gem changes wait for your go',
        '<p class="lead">Gifting the PREMIUM pass cost <b>480 gems on the server but 900 in the game</b>, so two friends could gift each other passes for free. Per the coordinator, a server deploy closes it. A regular player finishes the season pass on <b>day 22</b> of 30. The proposed gem changes are <b>not applied</b>: they wait for your go.</p>'
        '<table class="tbl"><tr><th>Player</th><th>Finishes the pass</th></tr>'
        '<tr><td>Casual (20 min, 5 days a week)</td><td>day 30 today, day 27 with the proposal</td></tr>'
        '<tr><td>Regular (45 min, 6 days a week)</td><td>day 22</td></tr>'
        '<tr><td>Heavy (2 h, every day)</td><td>day 15</td></tr></table>'
        '<table class="tbl"><tr><th>Proposed change (waiting)</th><th>Before</th><th>After</th></tr>'
        '<tr><td>Pass gift price (gems)</td><td>480 server / 900 game</td><td>1200 both</td></tr>'
        '<tr><td>Catch-up boost</td><td>x1.5 below 80% of pace</td><td>x2.0 below 85%</td></tr>'
        '<tr><td>Login day 4</td><td>10 gems</td><td>20 gems</td></tr>'
        '<tr><td>Free track tiers 26/36/46</td><td>250 coins</td><td>25 gems</td></tr>'
        '<tr><td>Pass kill films (seasons 1-3)</td><td>automatic pick, can be a kill already free</td><td>pinned picks from kills on sale</td></tr></table>'
        '<p class="src">Model: 5 min a match, 45% win rate, expected values. No real-money prices change. Plan: Scratch/coord/economy/ECONOMY_PLAN.md (sections 0, 2, 3).</p>', 'ip'))
    S.append(sec('L2', 'Server tuning proven on the Samsung A22 (TEST only)',
        '<p class="lead">The server changed <b>pistol damage 26 to 27</b> on the TEST channel only. The phone fetched it and applied it at the next match start ("1 fields written, 1 overridden"), then it was rolled back to defaults after 100 s. LIVE was never touched. This is the proof that every weapon and game value can be tuned from the server without a new build.</p>'
        '<div class="steps"><div><b>01:11:02</b>fetch test: ok, rev 2, 1 value</div><div><b>01:12:05</b>match start: 1 fields written, 1 overridden</div><div><b>01:12:37</b>rollback: rev 1, 0 values (defaults)</div></div>'
        '<div class="grid3">' + ''.join(fig(f, t) for f, t in [('l2_1_home', 'Phone home (test build)'), ('l2_2_play', 'Play'), ('l2_3_bots', 'Bots setup'), ('l2_4_match', 'In match (s4, the match that started with rev 2)')]) + '</div>'
        '<p class="src">Log: [TUNING] lines from the A22 unity.log (LIVE-SHOP scratchpad a22rev2). Note: the stills show the phone state, the proof is the log lines above.</p>', 'ok'))
    S.append(sec('U4', 'Menu speed on the Samsung A22: every page idle at or under 33 ms',
        f'<p class="lead"><b>{ok} of {n} pages</b> now hold p95 &lt;= 33 ms (old shader: {okold} of {n}). Fix: static comic paper is baked once per panel into a cached texture, hidden layers are culled. Page OPEN times are still being measured.</p>'
        f'<div class="key"><span class="lg"><i class="sw old"></i>old shader (build 15 line)</span><span class="lg"><i class="sw new"></i>baked comic paper</span><span class="lg"><i class="sw tg"></i>33 ms target</span></div>{svg}'
        '<p class="src">Idle p95 frame time in ms, lower is better. Measured frame times come in steps, so values jump 22 / 33 / 44 / 55. Sources: coord/a22over2/summary.txt, coord/t7shader_162cf547d_usb/summary.txt. Row U4 is still [~].</p>', 'ip'))
    S.append(sec('S9', 'Purchases ask the real store sheet directly (no more TEST PURCHASE)',
        '<p class="lead">Before: every real-money buy first showed our own "TEST PURCHASE / NO MONEY IS CHARGED / BUY (TEST)" sheet, then the real store. Misleading and a review-rejection risk. Now: 6 of 6 real-money paths go straight to the store sheet, 0 test words. Proven on the second Mac, run realstore-43d8, 15 checks pass, 0 fail, merged in train8.</p>'
        '<div class="pair">' + fig('s9_control_mock', 'BEFORE: the old test sheet (control)', True) + fig('s9_pass_premium_answer', 'AFTER: premium pass, store sheet directly', True) + '</div>'
        '<h3>The 6 paths, after</h3><div class="grid3">' + ''.join(fig('s9_' + k + '_answer', t) for k, t in [('gems_pack', 'Gems pack'), ('pass_plus', 'Pass plus (the lab store answered PURCHASE FAILED; the point is the store sheet, no test words)'), ('pass_premium', 'Pass premium'), ('remove_ads', 'Remove ads'), ('starter', 'Starter pack'), ('vip', 'VIP')]) + '</div>'
        '<p class="src">Still to do: strict rescan of the build-16 store artifacts. Row S9 is [~]. Stills: Scratch/mac2/runs/realstore-43d8/.</p>', 'ip'))
    S.append(sec('U1', 'Old MATCH SETUP removed everywhere',
        '<p class="lead">The old setup screen (BOT BASH, CUSTOM presets, RULES / ITEMS / ARENA / FUN / LOBBY) is gone. The Bots screen has ADVANCED instead, and the results SETUP button leads to the new flow. Lab liveshop-oldsetup-16a688747: 11 pass, 0 fail, rebased on train8, compile clean x3, merging.</p>'
        '<div class="grid3">' + ''.join(fig('u1_' + k, t) for k, t in [('1_play', 'Play'), ('2_bots', 'Bots'), ('3_bots_advanced', 'Bots, ADVANCED'), ('4_bots_advanced_setups', 'ADVANCED, setups'), ('5_results_setup', 'Results, SETUP')]) + '</div>'
        '<p class="src">Row U1 is [~] until the merge lands.</p>', 'ip'))
    e4 = row_text('E4')
    ev = e4.split('EVIDENCE 10 Nov:')[-1].strip() if 'EVIDENCE' in e4 else e4
    S.append(sec('E4', 'Stage 5 taunt / emote / win-dance / gloat engine: done',
        '<p class="lead">Taunts, emotes, win dances and gloats now run from the engine. Existing ones (LOSER, THE L STAYS, DAB, BHANGRA) play identically to build 15, proven by draw digests, sim, sounds and events at low and high quality.</p>'
        '<div class="nums"><div><b>614 / 0</b>core tests</div><div><b>47 / 47</b>perfmix vs build 15</div><div><b>9 / 9</b>perfnet</div><div><b>PASS</b>determinism gate</div><div><b>0 x3</b>compile errors</div></div>'
        f'<details><summary>Full evidence line from the checklist</summary><p class="src">{E(ev)}</p></details>', 'ok'))
    S.append(sec('L1', 'Season pass: retired skins removed',
        '<p class="lead">Retired CAMO / CARTOON / NEON / TAPED skins can no longer appear in the season pass.</p>'
        '<div class="nums"><div><b>706</b>catalog items pushed (id 10001)</div><div><b>0</b>retired pickable</div><div><b>34</b>picks per season, seasons 1-3</div><div><b>0</b>retired refs in public config</div></div>'
        '<p class="src">Config v16 published, diff touched only ops.seasons.pass.tables. Git: stickbash-online liveshop-catalog-10001 4e6809d. Row L1 is [x].</p>', 'ok'))
    S.append(sec('LEGAL', 'Legal pages are live on stickbash.com',
        '<p class="lead">Privacy policy, terms and account deletion are published (headless screenshots taken 10 Nov).</p>'
        '<div class="grid3">' + ''.join(f'<figure><a href="https://stickbash.com/{p}">stickbash.com/{p}</a><a href="{IMG}legal_{p}.jpg"><img loading="lazy" src="{IMG}legal_{p}.jpg" alt="{p} page"></a></figure>' for p in ['privacy', 'terms', 'delete-account']) + '</div>', 'ok'))
    return S

CSS = '''
:root{--bg:#17132a;--ink:#f4f0ff;--mute:#b6acd9;--gold:#ffcf3a;--line:#3a3166;--panel:#221c3d;--panel2:#2b2450;
--old:#ff9a3d;--new:#4fd1ff;--green:#3fd97a;--red:#ff6b7a}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.5 "Baloo 2",system-ui,sans-serif}
main{max-width:1180px;margin:0 auto;padding:22px 16px 90px}
h1,h2,h3,.tag{font-family:"Lilita One",system-ui,sans-serif;letter-spacing:.02em;font-weight:400}
h1{color:var(--gold);font-size:2.3em;margin:.1em 0 .1em}h2{margin:0;font-size:1.5em;color:var(--ink)}h3{color:var(--gold);margin:1em 0 .3em}
a{color:var(--new)}.back{color:var(--mute);text-decoration:none}.sub{color:var(--mute);margin:0 0 18px}
.card{background:var(--panel);border:2px solid var(--line);border-radius:18px;padding:16px;margin:20px 0}
.hd{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
.tag{border-radius:10px;padding:2px 12px;font-size:1.25em;color:#1a1200;background:var(--gold)}
.tag.ok{background:var(--green)}.tag.ip{background:var(--new)}.tag.st{background:#c9b6ff}
.lead{color:var(--mute);margin:.2em 0 .8em}.lead b{color:var(--ink)}.src{color:var(--mute);font-size:.88em}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:12px}.grid3{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
figure{margin:0;background:var(--panel2);border:2px solid var(--line);border-radius:14px;padding:8px}
figure img{width:100%;border-radius:10px;display:block}figcaption{color:var(--mute);font-size:.9em;margin-top:6px}
.pair figure:first-child{border-color:var(--old)}.pair figure:last-child{border-color:var(--green)}
@media(max-width:760px){.pair{grid-template-columns:1fr}}
.nums{display:flex;gap:12px;flex-wrap:wrap;margin:8px 0}.nums div{background:var(--panel2);border:2px solid var(--line);border-radius:12px;padding:8px 14px;color:var(--mute);min-width:130px}
.nums b{display:block;font:1.7em "Lilita One";color:var(--green)}
details{margin-top:8px;color:var(--mute)}summary{cursor:pointer}
.chart{width:100%;height:auto;background:var(--panel2);border-radius:12px;border:2px solid var(--line)}
.chart .grid{stroke:#4a4180;stroke-width:1}.chart .ax,.chart .lab,.chart .val{fill:var(--ink);font:12px "Baloo 2",system-ui,sans-serif}.chart .ax{fill:var(--mute)}
.chart .old{fill:var(--old)}.chart .new{fill:var(--new)}.chart .tgt{stroke:var(--gold);stroke-width:2.5;stroke-dasharray:6 4}.chart .tg{fill:var(--gold);font:700 12px system-ui}
.key,.legend{display:flex;gap:16px;flex-wrap:wrap;margin:6px 0 8px;color:var(--mute)}
.lg{display:inline-flex;align-items:center;gap:6px}.sw{width:14px;height:14px;border-radius:4px;display:inline-block;background:#888}
.sw.old{background:var(--old)}.sw.new{background:var(--new)}.sw.tg{background:var(--gold)}
.strip{display:flex;height:40px;border-radius:12px;overflow:hidden;border:2px solid var(--line)}
.seg{display:flex;align-items:center;justify-content:center;font:1.2em "Lilita One";color:#10131a}
.sx,.sw.sx{background:var(--green)}.s~,.sw.s~{background:var(--new)}
.so,.sw.so{background:#8d85b8}.sU,.sw.sU{background:var(--gold)}
.steps{display:flex;gap:12px;flex-wrap:wrap;margin:8px 0}.steps div{background:var(--panel2);border:2px solid var(--line);border-radius:12px;padding:8px 14px;color:var(--mute)}.steps b{display:block;color:var(--new);font:1.2em 'Lilita One'}
.tbl{width:100%;border-collapse:collapse;margin:8px 0;background:var(--panel2);border-radius:12px;overflow:hidden}.tbl th,.tbl td{text-align:left;padding:7px 12px;border-bottom:1px solid var(--line)}.tbl th{color:var(--gold);font-family:'Lilita One';font-weight:400}
.wait{color:var(--mute);padding-left:20px}.wait li{margin:4px 0}
'''.replace('.s~,.sw.s~', '.ssp,.sw.ssp')

def main():
    sx = build_sections()
    body = ''.join(sx).replace('class="seg s~"', 'class="seg ssp"').replace('class="sw s~"', 'class="sw ssp"')
    from datetime import datetime
    page = f'''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Launch progress</title>
<link href="https://fonts.googleapis.com/css2?family=Lilita+One&family=Baloo+2:wght@600;700;800&display=swap" rel="stylesheet">
<style>{CSS}</style></head><body><main>
<a class="back" href="36_index.html">&larr; all pages</a>
<h1>Launch progress</h1><p class="sub">What the launch work looks like, newest first. Rebuilt {datetime.now().strftime("%d %b %H:%M")} from the launch checklist and the lab results. Click any still to enlarge.</p>
{body}</main></body></html>'''
    open(FB + '/101_launch_progress.html', 'w', encoding='utf-8').write(page)
    print('ok')
main()
