Add link previews and icons; make PIG_INVITE_CODE actually work

Social and icons. A 1200x630 card, apple-touch-icon, and maskable PWA icons,
generated from an HTML template by a script so the mark, wordmark and tagline
cannot drift from the product. The apple-touch-icon referenced in index.html
was a 404 until now. Icons are drawn on an opaque plate with inset because iOS
rounds corners and Android may apply a circle — an edge-to-edge mark loses its
ears to that crop. og:image is absolute, which is the single most common
reason a card unfurls blank.

PIG_INVITE_CODE was a lie. Signup validates against the invites table, so
setting the variable only flipped a label in the UI — an operator would set it,
hand the code to a colleague, and watch them be rejected. It is now reconciled
into a real invite row at boot: setting it issues, changing it rotates and
revokes the predecessor, and removing it revokes. Verified all three, plus that
a restart with an unchanged code does not duplicate.

Two bugs found while doing that:

- `uses_remaining` was jsonb, so the SQL decrement could never have worked.
  Now integer. The generated migration failed because Postgres has no implicit
  jsonb->integer cast, so the USING clause is hand-written.
- Redemption keyed off `redeemedAt`, which would have made every reusable
  invite single-use — a confusing way to lock a team out. Availability now
  comes from `usesRemaining`, and redemption records who used it most recently
  without consuming it.

Sign-in gains a password option alongside the magic link, defaulting to
password since that is the daily path. PIG stores neither; both are handled by
the identity provider and PIG only ever sees the resulting token.
autocomplete is set so password managers and iOS can fill.

Verified: full migration chain applies to a fresh Postgres, the CSP hash for
the inline theme script is unchanged by the rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:43:10 -07:00
parent 93818a2d2c
commit 0551e8dd6e
19 changed files with 6855 additions and 53 deletions
+125
View File
@@ -0,0 +1,125 @@
<!doctype html>
<!--
Source for the social preview card. Rendered to PNG by `tools/render-assets.mjs`.
Kept as HTML rather than hand-drawn in a design tool so the wordmark, the
tagline and the mark stay in sync with the product when any of them changes —
and so regenerating is one command rather than an afternoon.
1200×630 is the size Twitter/X and most link unfurlers crop to. Anything
important is kept well inside the edges, because several clients crop to
roughly 1.91:1 and Slack shows a smaller centred slice.
-->
<html>
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1200px;
height: 630px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 88px;
background: #ffffff;
color: #09090b;
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Inter, sans-serif;
position: relative;
overflow: hidden;
}
/* A faint grid, suggesting racked capacity without being literal about it. */
.grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(9, 9, 11, 0.045) 1px, transparent 1px),
linear-gradient(to bottom, rgba(9, 9, 11, 0.045) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(ellipse 80% 70% at 70% 40%, #000 20%, transparent 75%);
}
.row { display: flex; align-items: center; gap: 22px; position: relative; }
.mark { width: 92px; height: 92px; flex: none; }
.wordmark {
font-size: 84px;
font-weight: 650;
letter-spacing: -0.045em;
line-height: 1;
}
.expansion {
margin-top: 30px;
font-size: 27px;
font-weight: 500;
color: #52525b;
letter-spacing: -0.01em;
position: relative;
}
h1 {
margin-top: 22px;
font-size: 46px;
font-weight: 600;
letter-spacing: -0.028em;
line-height: 1.16;
max-width: 20ch;
position: relative;
}
.footer {
position: absolute;
left: 88px;
right: 88px;
bottom: 56px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 22px;
color: #71717a;
}
.pill {
border: 1.5px solid #e4e4e7;
border-radius: 999px;
padding: 9px 20px;
font-size: 19px;
font-weight: 500;
color: #3f3f46;
}
</style>
</head>
<body>
<div class="grid"></div>
<div class="row">
<svg class="mark" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M7.4 8.6 5.1 3.9c-.2-.5.3-1 .8-.8l5.2 2.2z" fill="#09090b" />
<path d="M24.6 8.6 26.9 3.9c.2-.5-.3-1-.8-.8l-5.2 2.2z" fill="#09090b" />
<path
d="M16 5.2c6.3 0 11.2 4.3 11.2 10.4 0 6.5-5 11.2-11.2 11.2S4.8 22.1 4.8 15.6C4.8 9.5 9.7 5.2 16 5.2z"
fill="#09090b"
/>
<ellipse cx="11.6" cy="13.4" rx="1.5" ry="1.8" fill="#fff" />
<ellipse cx="20.4" cy="13.4" rx="1.5" ry="1.8" fill="#fff" />
<rect x="11.3" y="17.6" width="9.4" height="6.2" rx="3.1" fill="#fff" />
<ellipse cx="14.2" cy="20.7" rx="1.15" ry="1.5" fill="#09090b" />
<ellipse cx="17.8" cy="20.7" rx="1.15" ry="1.5" fill="#09090b" />
</svg>
<span class="wordmark">pig</span>
</div>
<div class="expansion">Prime Intellect Growth</div>
<h1>The CRM for companies that buy compute and sell it.</h1>
<div class="footer">
<span>primeintellectgrowth.com</span>
<span class="pill">Open source · Apache 2.0</span>
</div>
</body>
</html>