The app is pre-launch and shared by link with a handful of people at Prime
Intellect. It should not be accumulating a search footprint yet.
Three layers, because each covers a gap the others leave:
- robots.txt asks well-behaved crawlers not to fetch at all.
- The <meta name="robots"> tag covers the HTML document for anything that
fetched anyway.
- X-Robots-Tag covers everything that is NOT the HTML document — og.png,
the manifest, the built assets — which the meta tag cannot reach.
noarchive and nosnippet are there so a cache or an excerpt cannot outlive
the page once this is reversed.
Deliberately NOT stripped: the og:/twitter: tags. Link unfurlers are not
crawlers — they fetch on behalf of the person pasting the link, and a
rendered card is exactly what we want when this is shared.
The real gate remains authentication: / returns the sign-in screen and every
/api/ route returns 401. This only stops the app being indexed.
To go public: delete robots.txt, drop the meta tag, drop the header.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
apps/web — React, Vite, Tailwind, shadcn-idiom components. Mobile Safari is a
first-class target, not an afterthought:
- Two navigation treatments rather than one compromise. A bottom tab bar on
phones, because the top of a large phone is out of thumb reach; a persistent
sidebar from lg upward, so an iPad in portrait gets it too.
- Safe-area insets throughout, so the tab bar clears the home indicator and the
last row of a list is actually reachable.
- Inputs are pinned to a 16px minimum, which is the correct fix for Safari
zooming on focus. user-scalable=no is not used: it breaks pinch-zoom for
everyone and recent iOS ignores it anyway.
- The pipeline board becomes a stage picker on phones. An eight-column board
scrolling horizontally on a 390px screen is technically responsive and
practically useless.
Theming: users pick an accent and the whole interface re-tints. Accent values
live once, in @pig/core, and are written onto the root element at runtime —
there is no CSS copy to drift from the TypeScript. Preferences are stored
server-side so they follow a person between laptop and phone, mirrored into
localStorage only so the pre-paint script can avoid a white flash. Status
colours stay fixed regardless of accent: if "at risk" re-tinted to whatever
someone picked, the signal would be gone.
Seed data is public research, every record carrying a confidence grade and a
source URL. No email addresses are seeded or inferred — none are published, and
guessing them from a name and a domain is unreliable and rude. Authorship is
not promoted to employment: contributors, residency participants and alumni are
recorded as what the evidence actually shows, and a name that could not be
sourced at all is listed as unresolved rather than invented.
Three defects found and fixed by actually running it rather than assuming:
1. The seed was not idempotent. onConflictDoNothing() with no target is a no-op
without a matching unique constraint, so a second run duplicated 27
contacts. There is deliberately no unique index on (account, name) — two
people at one company can share a name — so idempotency is enforced in the
seed instead of by bending the schema.
2. /capacity scrolled sideways on a phone. Grid items default to
min-width:auto and `truncate` sets nowrap, so a long title became
unshrinkable content and widened the track. Fixed with min-w-0 on every
truncating grid child.
3. The idle-capacity alert silently failed to fire at exactly 80% utilisation,
losing a float comparison against a 0.2 threshold. Moved to 0.15, which is
also a more sensible line for "worth attention".
The worked example is tuned to teach rather than to flatter: 70% sold at a 53%
markup lands at +6.7% margin with 20% still idle, so both the healthy number
and the alert are visible. Drop the sold share to 55% and the same block goes
underwater — that sensitivity is the argument for the product.
Verified in a real browser at 393px and 1440px, light and dark: zero horizontal
overflow on every route, zero console errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>