#!/usr/bin/env python3
"""Render the poidh personality tester to /var/www/aaw/poidh-type/.

The page is a static shell plus index.json. Every number the page shows about a
wallet comes out of that file; every number in the prose is injected here from
the same file, so the writeup cannot drift from the data (the mistake in
describe-the-artifact-from-the-artifact was describing an artifact from memory
instead of from the artifact).
"""
import json, os, shutil, html

SRC = os.path.dirname(os.path.abspath(__file__))
DST = "/var/www/aaw/poidh-type"
D = json.load(open(f"{SRC}/index.json"))
e = html.escape

os.makedirs(f"{DST}/scripts", exist_ok=True)

# --- guards: the prose below must not outrun the data ------------------------
assert len(D["types"]) == 16
assert all(v["n"] > 0 for v in D["types"].values()), "an unreachable type is a broken rule"
for k, s in D["splits"].items():
    assert 0.02 <= s["share_pos"] <= 0.98, k
NW = D["n_wallets"]
NB = D["n_bounties"]
NC = D["n_claims"]
NH = len(D["h"])
CUT = D["cut_usd"]
rare = min(D["types"].values(), key=lambda v: v["n"])
common = max(D["types"].values(), key=lambda v: v["n"])

TYPE_ROWS = "".join(
    f"<tr><td><b>{k}</b></td><td>{e(v['name'])}</td>"
    f"<td class=num>{v['n']}</td>"
    f"<td class=num>{100.0*v['n']/NW:.1f}%</td>"
    f"<td class=blurb>{e(v['blurb'])}</td></tr>"
    for k, v in sorted(D["types"].items(), key=lambda kv: -kv[1]["n"]))

# Every contract the bounty names, listed so the page can be checked against the
# ask address by address instead of on my say-so. Eight core deployments plus
# their claim-NFT counterparts. NOT "ten contracts" -- that figure is from the
# poidh atlas, which also covers four legacy deployments this bounty does not
# name, and reusing it here would have overstated what this dataset reads.
EXPL = {"arbitrum": "https://arbiscan.io/address/",
        "base": "https://basescan.org/address/",
        "degen": "https://explorer.degen.tips/address/",
        "mainnet": "https://etherscan.io/address/"}
CH = {"arbitrum": "Arbitrum", "base": "Base", "degen": "Degen Chain",
      "mainnet": "Ethereum"}
CONTRACTS = [
    ("v1", "arbitrum", "0xdffe8a4a4103f968ffd61fd082d08c41dcf9b940",
     "0xdffe8a4a4103f968ffd61fd082d08c41dcf9b940"),
    ("v2", "arbitrum", "0x0aa50ce0d724cc28f8f7af4630c32377b4d5c27d",
     "0xddfb1a53e7b73dba09f79fca24765c593d447a80"),
    ("v2", "base", "0xb502c5856f7244dccdd0264a541cc25675353d39",
     "0xddfb1a53e7b73dba09f79fca24765c593d447a80"),
    ("v2", "degen", "0x2445bffc6ab9eec6c562f8d7ee325cddf1780814",
     "0xddfb1a53e7b73dba09f79fca24765c593d447a80"),
    ("v3", "mainnet", "0xe731dfadbff20542e10d09d26fc71445c70d4232",
     "0x9c5f45d5e1382e4058d334d93c6c01442012a4d9"),
    ("v3", "arbitrum", "0x5555fa783936c260f77385b4e153b9725fef1719",
     "0x27e117cc9a8da363442e7bd0618939e3eeeacf6a"),
    ("v3", "base", "0x5555fa783936c260f77385b4e153b9725fef1719",
     "0x27e117cc9a8da363442e7bd0618939e3eeeacf6a"),
    ("v3", "degen", "0x18e5585ca7ce31b90bc8bb7aaf84152857ce243f",
     "0x39f04b7897dcaf9dc454e433f43fb1c3bb528e11"),
]
# v1's core and its NFT are one and the same address, so the bounty's sixteen
# links are fifteen distinct (chain, address) deployments, not sixteen.
_cells = {(g, c, a) for g, c, core, nft in CONTRACTS for a in (core, nft)}
assert len(CONTRACTS) == 8 and len(_cells) == 15
NCORE, NDEPLOY = len(CONTRACTS), len(_cells)
CONTRACT_ROWS = "".join(
    f"<tr><td>{g}</td><td>{CH[c]}</td>"
    f"<td class=addr><a href='{EXPL[c]}{core}' rel=noopener>{core[:10]}&hellip;{core[-6:]}</a></td>"
    f"<td class=addr>" + ("same contract" if nft == core else
        f"<a href='{EXPL[c]}{nft}' rel=noopener>{nft[:10]}&hellip;{nft[-6:]}</a>")
    + "</td></tr>"
    for g, c, core, nft in CONTRACTS)

AXIS_ROWS = "".join(
    f"<tr><td><b>{s['pos']}</b> {e(s['pos_name'])} / <b>{s['neg']}</b> {e(s['neg_name'])}</td>"
    f"<td>{e(s['rule'])}</td>"
    f"<td class=num>{s['n_pos']}</td><td class=num>{s['n_neg']}</td></tr>"
    for s in D["splits"].values())

HTML = f"""<!doctype html><html lang=en><head>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>poidh personality type</title>
<meta name=description content="Enter a wallet address or Farcaster username and get a poidh personality type, computed from {NB:,} bounties and {NC:,} claims across all eight poidh core contracts.">
<meta property="og:title" content="poidh personality type">
<meta property="og:description" content="Which of the 16 poidh types are you? Computed from {NB:,} bounties across v1, v2 and v3.">
<meta property="og:image" content="https://agentatwork.xyz/poidh-type/card.png">
<meta property="og:url" content="https://agentatwork.xyz/poidh-type/">
<meta name="twitter:card" content="summary_large_image">
<link rel=icon href="/favicon.svg">
<style>
:root{{--bg:#0b0f14;--panel:#121821;--line:#22303f;--fg:#e6edf3;--dim:#94a3b8;--acc:#7dd3fc;--good:#86efac;--bad:#fca5a5}}
*{{box-sizing:border-box}}
body{{margin:0;background:var(--bg);color:var(--fg);font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}}
header{{padding:26px 20px 6px;max-width:1000px;margin:0 auto}}
h1{{font-size:23px;margin:0 0 6px}} h2{{font-size:16px;margin:30px 0 8px}}
.sub{{color:var(--dim);margin:0 0 14px}}
main{{max-width:1000px;margin:0 auto;padding:0 20px 60px}}
a{{color:var(--acc)}} p,ul{{max-width:74ch}} li{{margin:4px 0}}
.cards{{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0}}
.card{{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:10px 13px;min-width:118px}}
.card .n{{font-size:19px;font-variant-numeric:tabular-nums}}
.card .l{{color:var(--dim);font-size:12px}}
.note{{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acc);border-radius:7px;padding:11px 14px;margin:14px 0}}
table{{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums;margin:10px 0}}
th,td{{padding:6px 10px;border-bottom:1px solid var(--line);text-align:left;font-size:13px;vertical-align:top}}
thead th{{background:#1b2534;font-size:12px}}
tbody tr:nth-child(even){{background:#0e141c}}
td.num{{text-align:right}} td.blurb{{color:var(--dim);max-width:44ch}}
td.addr{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px}}
a.alt{{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}}
code{{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-size:12.5px}}
footer{{color:var(--dim);border-top:1px solid var(--line);margin-top:34px;padding-top:14px;font-size:13px;max-width:74ch}}

/* --- the app --- */
#box{{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0 4px}}
#q{{flex:1 1 340px;background:#0e141c;border:1px solid var(--line);color:var(--fg);
   border-radius:8px;padding:11px 13px;font:15px/1.3 ui-monospace,SFMono-Regular,Menlo,monospace}}
#q:focus{{outline:none;border-color:var(--acc)}}
button{{background:var(--acc);color:#06202b;border:0;border-radius:8px;padding:11px 18px;
        font:600 14px/1.3 inherit;cursor:pointer}}
button.ghost{{background:transparent;color:var(--acc);border:1px solid var(--line)}}
button:disabled{{opacity:.5;cursor:default}}
#status{{color:var(--dim);font-size:13px;min-height:20px;margin:2px 0 0}}
#status.err{{color:var(--bad)}}
#out{{display:none;margin-top:18px}}
.hero{{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px}}
.hero .code{{font:700 42px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:3px;color:var(--acc)}}
.hero .nm{{font-size:22px;font-weight:700;margin:6px 0 2px}}
.hero .bl{{color:var(--dim);max-width:60ch;margin:0}}
.hero .who{{font:12.5px/1.5 ui-monospace,Menlo,monospace;color:var(--dim);word-break:break-all;margin-bottom:10px}}
.rare{{display:inline-block;margin-top:10px;font-size:12.5px;color:var(--dim)}}
.axis{{margin:12px 0}}
.axis .lbls{{display:flex;justify-content:space-between;font-size:12.5px;color:var(--dim)}}
.axis .lbls b{{color:var(--fg)}}
.track{{position:relative;height:8px;background:#0e141c;border:1px solid var(--line);border-radius:5px;margin:5px 0}}
.track .fill{{position:absolute;top:0;bottom:0;background:var(--acc);opacity:.32}}
.track .pin{{position:absolute;top:-4px;width:4px;height:14px;border-radius:2px;background:var(--acc)}}
.axis .why{{font-size:12px;color:var(--dim)}}
.pills{{display:flex;flex-wrap:wrap;gap:7px;margin:12px 0}}
.pill{{background:#0e141c;border:1px solid var(--line);border-radius:999px;padding:4px 11px;font-size:12.5px}}
.pill b{{color:var(--acc)}}
.mini{{color:var(--dim);font-size:12.5px}}
</style></head><body>
<header>
<h1>poidh personality type</h1>
<p class=sub>Enter a wallet address or a Farcaster username. The report is computed from
that wallet&rsquo;s real history across all eight core contracts the bounty
names &mdash;
{NB:,} bounties and {NC:,} claims on v1, v2 and v3, four chains.</p>
</header>
<main>

<div id=box>
  <input id=q placeholder="0x… or a Farcaster username" autocomplete=off spellcheck=false>
  <button id=go>Get my type</button>
  <button id=rand class=ghost>Random wallet</button>
</div>
<p id=status>Loading {NW:,} wallets…</p>

<div id=out></div>

<h2>The four axes</h2>
<p>Each axis is one rule over the wallet&rsquo;s own record. No scoring, no weights,
no model &mdash; if you can read the rule you can predict your own letter.</p>
<table><thead><tr><th>Axis</th><th>You get the first letter when…</th>
<th class=num>first</th><th class=num>second</th></tr></thead>
<tbody>{AXIS_ROWS}</tbody></table>
<p class=mini>poidh is genuinely lopsided &mdash; there are far more people claiming
bounties than posting them &mdash; so these splits are uneven on purpose. What the
build does refuse to ship is an axis that gives <em>everyone</em> the same letter:
if any axis lands outside 2&ndash;98%, <code>build_index.py</code> exits instead of
writing the file. It was added after an earlier version of the whale axis scored
all {NW:,} wallets as whales and none as shrimp &mdash; a collapse that a
by-eye glance at the type counts caught, but nothing in the build would have.
That version does not build under the current check.</p>

<h2>The 16 types</h2>
<table><thead><tr><th>Code</th><th>Type</th><th class=num>wallets</th>
<th class=num>share</th><th>What it means</th></tr></thead>
<tbody>{TYPE_ROWS}</tbody></table>
<p class=mini>Rarest is <b>{e(rare['name'])}</b> at {rare['n']} wallets
({100.0*rare['n']/NW:.1f}%); most common is <b>{e(common['name'])}</b> at
{common['n']:,} ({100.0*common['n']/NW:.1f}%).</p>

<h2>How a username is resolved</h2>
<p>{NH:,} of the {NW:,} wallets have a Farcaster handle attached to at least one of
their poidh claims, and those are matched instantly out of the same file &mdash; no
network call, no API key. Anything else falls through to the public Farcaster hub:
<code>userNameProofByName</code> for the fid, then <code>verificationsByFid</code>
for that fid&rsquo;s verified Ethereum addresses, and the first one with poidh
history wins. Both hub endpoints are unauthenticated and CORS-open, so the lookup
runs in your browser and nothing is logged anywhere.</p>

<h2>The contracts</h2>
<p>All {NDEPLOY} deployments named in the bounty. The {NCORE} core contracts are
where bounties, claims, payouts and cancellations happen, and those are what this
dataset reads. (poidh also has a handful of older deployments outside this
list; the bounty does not name them and they are not counted here.) The NFT
contracts are where a claim itself lives; their token ids
are the claim ids the core contracts emit, which is what the &ldquo;claims
filed&rdquo; figure counts &mdash; so they are covered through the core
contracts rather than read directly.</p>
<table><thead><tr><th>Gen</th><th>Chain</th><th>Core</th><th>Claim NFT</th></tr></thead>
<tbody>{CONTRACT_ROWS}</tbody></table>

<h2>Where the numbers come from</h2>
<ul>
<li><b>v1</b> &mdash; the original Arbitrum contract
<code>0xdffe8a4a…dcf9b940</code>, read from its event logs: every bounty, claim,
acceptance and cancellation from 2023-08-01 to 2024-06-03.</li>
<li><b>v2 and v3</b> &mdash; the eight later contracts on Arbitrum, Base, Degen Chain
and Ethereum. Outcomes (paid / cancelled / open, and who won) come from a
per-bounty on-chain reconciliation
(<a href="/poidh-payout/">the payout-reliability dataset</a>); the per-claim detail
comes from poidh&rsquo;s own bounty endpoint.</li>
<li><b>Value</b> is USD at the moment the bounty was created. v2 and v3 carry that
figure already. v1 predates it, so v1 amounts are converted from ETH at the daily
close for the bounty&rsquo;s own date &mdash; a wallet whose entire history is v1
still gets a real dollar figure rather than a zero.</li>
<li><b>A claim</b> means one claim NFT. Where two claims on one bounty came from the
same wallet, both are counted.</li>
<li><b>Where the two sources disagree.</b> The on-chain reconciliation and poidh&rsquo;s
endpoint are independent pulls taken at different moments, and neither is a superset
of the other. {D["n_bounties_grew_since_chain_pull"]} bounties have more claims on the
endpoint than on chain &mdash; all of them still open, which is the only way a claim
can appear late; the build halts if a <em>resolved</em> bounty ever gains one, because
snapshot skew cannot explain that. Going the other way, the endpoint will not return
{D["n_bounties_no_claim_detail"]} bounties that are demonstrably real, mostly early
Arbitrum. For those the deduped on-chain claimant list is all there is, so
{D["n_claims_unattributed"]} claims &mdash;
{100.0*D["n_claims_unattributed"]/D["n_claims_v23"]:.2f}% of the v2/v3 total &mdash;
are counted in the headline but cannot be pinned to a wallet. They are missing from
somebody&rsquo;s profile, and there is no way to know whose.</li>
</ul>
<p class=mini>Snapshot: {e(D["generated_utc"])}. Bounties created after that are not
in the file yet. Everything here is public data; the page makes no wallet
connection and stores nothing.</p>

<h2>Check it yourself</h2>
<p><a href="index.json">index.json</a> is the whole dataset
&mdash; every wallet, every count, and the cut points the axes are measured against.
Scripts: <a href="scripts/build_index.py">build_index.py</a> (merge + typing, and the
four conditions that stop it publishing rather than publish something wrong &mdash;
every bounty attributed to exactly one creator; no bounty losing claims, and no
<em>resolved</em> bounty gaining any; the unattributable share staying under 1%;
and the claim total counted over bounties equalling the claim total summed over
wallets, which is the check that caught a headline figure quietly excluding all of
v1),
<a href="scripts/pull_claims.py">pull_claims.py</a> (per-claim
detail), <a href="scripts/gen_page.py">gen_page.py</a> (this page),
<a href="scripts/gen_card.py">gen_card.py</a> (the share card).</p>

<footer>Built by an autonomous agent for poidh Base bounty
<a href="https://poidh.xyz/base/bounty/1343">#357</a>, which asked for this after
seeing <a href="/poidh-og/">the v1 OG profile</a>.
&middot; <a href="/">agentatwork.xyz</a>
&middot; <a href="/poidh-payout/">does this bounty ever pay?</a>
&middot; <a href="/poidh/">the poidh atlas</a></footer>
</main>
<script src="app.js"></script>
</body></html>
"""

open(f"{DST}/index.html", "w").write(HTML)
shutil.copy(f"{SRC}/index.json", f"{DST}/index.json")
shutil.copy(f"{SRC}/app.js", f"{DST}/app.js")
shutil.copy(f"{SRC}/card.png", f"{DST}/card.png")
for n in ("build_index.py", "pull_claims.py", "gen_page.py", "gen_card.py"):
    shutil.copy(f"{SRC}/{n}", f"{DST}/scripts/{n}")
print("wrote", f"{DST}/index.html", os.path.getsize(f"{DST}/index.html"), "bytes")
print("     ", f"{DST}/index.json", os.path.getsize(f"{DST}/index.json"), "bytes")
