6cf80747cc2de18c9b8810666ba9fd25cf97d368
35 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6cf80747cc |
Keep PIG out of search results until it is meant to be found
CI / verify (push) Successful in 3m11s
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>
|
||
|
|
e12d27edd1 |
Polish every product workflow across desktop and mobile
CI / verify (push) Successful in 3m32s
Reframe each screen around the decisions compute brokers make: sellable capacity, full-cost margin, pipeline movement, contract deadlines, evidence review, staged imports, and controlled agent access. Group the shell by operating domain, strengthen mobile navigation and sheets, add responsive record treatments, and make loading, error, empty, readiness, and retry states explicit. The visual audit exposed sortable table targets and an unnamed file input only after exercising the rendered app, so this commit also pins those accessibility decisions at their actual interaction boundaries. Manrope is self-hosted as a single Latin variable subset to keep the stronger hierarchy without shipping unused font payloads. |
||
|
|
1318c0b841 |
Ship growth intelligence and demo polish
CI / verify (push) Successful in 3m51s
|
||
|
|
a6167629cc |
Move from npm to pnpm across the workspace, CI and the image
CI / verify (push) Successful in 3m23s
The monorepo was on npm workspaces. pnpm gives it a content-addressed store shared between the eight packages, a lockfile that records the whole graph rather than a flattened view of it, and — the reason this mattered in practice — `workspace:*`, which makes an internal dependency unambiguous instead of a version range that npm may satisfy from the registry. Mechanics: - `packageManager: pnpm@11.21.0` pins the version; corepack installs it in CI and in the image, so all three environments resolve identically. - The npm `workspaces` array is replaced by `pnpm-workspace.yaml`. pnpm ignores the former, and keeping both would leave two sources of truth. - All six internal dependencies moved to `workspace:*`. - Root scripts use `pnpm -r --if-present` and `pnpm -F <pkg>`. Two findings worth recording, both from running it rather than reading it: `tsx` was a devDependency, but the server runs TypeScript directly in production — the container's command is `pnpm exec tsx apps/api/src/server.ts`. Under npm this was concealed by the runtime stage re-installing tsx by hand after pruning dev dependencies. Under `pnpm install --prod` that sleight of hand stops working and the image simply fails to start. tsx is now declared in `dependencies`, which is what it has always actually been. The first image build failed with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY. That is not a pnpm bug: it had decided the modules directory was stale and wanted confirmation before deleting it, which a non-interactive build cannot give. The trigger was the host's `node_modules` reaching the build context — there was no `.dockerignore` at all. pnpm's tree is symlinks into a content-addressed store, so copying it into an image produces dangling links and a directory pnpm rightly considers corrupt. Fixed by adding `.dockerignore` and setting `CI=true`, which is required in any non-interactive pnpm build. `esbuild` is denied install scripts via `allowBuilds`. Its platform binary arrives through the optional dependency `@esbuild/linux-x64` and the postinstall only verifies it; confirmed by running the binary directly, which reports 0.25.12. Verified under pnpm: typecheck clean, 150 tests / 0 failures, e2e passes, web builds. The image was built and booted against a real Postgres — health ok, `/api/dashboard` 401 with an issuer configured, `/` and `/capacity` serve the SPA, `/og.png` serves as image/png, and the migrator runs from the pruned runtime stage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2b50797349 |
Merge remote-tracking branch 'gitea/main' into feat/revenue-intelligence
CI / verify (push) Successful in 2m54s
|
||
|
|
74e37f3e76 |
Lay the HubSpot and customer-lifecycle foundation
Work in progress from the Codex session, committed so nothing sits undeployed. Verified before committing: typecheck clean across all packages, 139 unit tests and the e2e suite green, migrations apply to an empty Postgres. Adds the HubSpot integration boundary (OAuth, client, contracts, webhook signature verification, sync), a growth route, customer-lifecycle service, Piggy lifecycle tools, a Growth page, and shared lifecycle/hubspot types. Two things are deliberately incomplete and should not be mistaken for finished: `packages/db/src/schema/hubspot.ts` is NOT exported from the schema index, so it is inert — no tables, no migration. That is the correct order (the shape can settle before it becomes a migration), but it does mean the HubSpot routes have no persistence behind them yet. `pnpm-workspace.yaml` and `pnpm-lock.yaml` are left uncommitted on purpose. The workspace file contains a literal unanswered placeholder — "esbuild: set this to true or false" — and this repository installs with npm, which is also what CI runs. Committing a second package manager's lockfile would make the install ambiguous. If the move to pnpm is intended it should be a deliberate change that updates CI and the Dockerfile together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c2c7fb9c19 |
Make mutations confirm themselves, and seed the evidence trail
CI / verify (push) Successful in 2m51s
Two demo gaps, both of which made working features look like they were not there. **Toasts fired into nothing.** RecordSheets already called toast.success on every save, but <Toaster /> was never mounted, so nothing appeared. It could not be mounted, either: the shadcn original imports next-themes, which PIG does not use — it has its own provider so a chosen theme is persisted server-side and follows a user between devices. Rewired to PIG's useTheme, mounted inside ThemeProvider, and offset clear of the phone tab bar and the home indicator. Feedback added where the interface otherwise gives none: allocation and hold report the GPU-hours actually written, because the sheet closes on success and the only other evidence is a number moving off-screen; releasing a hold says the capacity is sellable again; fact decisions say what the decision meant, and that approving evidence is not the same as writing it to a record; the profile form confirms rather than just clearing itself, which otherwise reads as the input being discarded. **The fact table was empty**, so the review queue and every provenance tooltip had nothing to show — the mechanism that makes an agent-written CRM trustworthy, invisible. Six agent-derived facts seeded with a deliberate mix: two applied, showing what a confident agent writes unprompted, and four proposed, including one weak claim that a reviewer should reject, so the queue is not a row of obvious approvals. Each carries a score, a band, evidence and where available a source. Idempotent on subject+field+value; verified over two runs. Verified: toast confirmed firing in a real browser on a 393px viewport, 135 unit tests and e2e green, typecheck clean, CSP hash unchanged, 0px horizontal overflow across 12 routes at both breakpoints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2763531ce4 |
Align shadcn's accent token with what shadcn means by it
CI / verify (push) Successful in 2m53s
shadcn uses `bg-accent` for its SUBTLE surfaces — dropdown item hover, command row selection, ghost and outline button hover, the dialog close affordance. The brand colour in shadcn is `primary`. PIG's Tailwind config mapped `accent` to `--accent`, which is the brand. That inverted the meaning, so every shadcn hover and selection state painted a full-strength brand block. With the monochrome "pig" palette in dark mode the brand is near-white, so a selected command row rendered as a white slab against a near-black sheet. Measured before the change: selected row rgb(250,250,250) on a rgb(9,9,11) body. `accent` now aliases `--accent-subtle` and `accent-foreground` aliases `--accent-fg`, which is what those tokens were created for. The eleven places where PIG's own components wanted a solid brand fill — filled chips, selected card borders, progress bars — move to `primary`, which still resolves to `--accent`. A `brand` alias is added for clarity. After: selected row rgb(39,39,42) in dark and rgb(244,244,245) in light, both a subtle tint above the body; the pipeline's active stage chip stays a solid rgb(250,250,250) fill, unchanged. Found by opening overlays, which earlier screenshot sweeps never did — every route had been checked, but a dropdown or a command palette only misbehaves once it is open. Worth remembering: page-level sweeps do not exercise portals. Typecheck clean, 135 unit tests and e2e green, CSP hash unchanged, 0px horizontal overflow across 12 routes at 393px and 1440px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c821b2ca07 |
Authenticate against any OIDC provider, for on-premises installs
CI / verify (push) Successful in 2m55s
The seam existed with only a Supabase implementation, so an on-prem deployment had no way to authenticate. A customer running PIG inside their own network already has Okta, Entra, Keycloak, Auth0 or Google Workspace; asking them to stand up a second identity system is a serious adoption tax and in a regulated environment usually refused outright. Setting PIG_OIDC_ISSUER is normally the whole configuration — the JWKS is discovered from the issuer's well-known document. PIG_OIDC_JWKS_URI skips discovery entirely for an air-gapped network. OIDC takes precedence over Supabase so an on-prem install can leave the hosted values in its environment file without them quietly taking over. Three decisions worth stating: Discovery is resolved lazily and the FAILURE is not cached. Doing it per request would put the customer's identity provider on the critical path of every API call; doing it eagerly at boot would mean their IdP rebooting takes the CRM down with it. So it happens on first use and retries on the next request. The audience check is optional but warned about loudly. Without it, a token the provider issued for ANY other application in the same tenant verifies here — a token minted for an unrelated internal tool would be accepted as a PIG session. It cannot be mandatory because some providers legitimately issue single-audience tokens. Email falls back through email, preferred_username and upn, because providers disagree, but a preferred_username without an "@" is ignored — PIG keys membership on the address, and a bare username must never become an account identity. Also fixed a warning that claimed "authentication is DISABLED" on a correctly configured OIDC deployment. That is worse than silence: an operator who reads it on a secure install learns to ignore the warnings. The dev bypass itself was already correct — it keys on the resolved provider rather than on Supabase. 18 new tests, most of them about what the provider must REFUSE: a foreign signing key, a foreign issuer, a token for a different application, an expired token, a token with no subject, and a discovery outage that must not become permanent. Keys are generated per test and the JWKS is served locally, so they run offline. Verified: production refuses to start with neither provider, starts with OIDC alone, enforces 401 on an unauthenticated request, and warns only about the genuinely missing admin list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
54edee30ed |
Unknown /api paths returned the SPA with HTTP 200
An authenticated GET to any unrecognised API route — a typo, a renamed endpoint, an older client — fell through to the SPA fallback and returned 200 text/html containing the app shell. This is close to the worst failure shape for an API consumer. `response.ok` is true, so nothing treats it as an error; the caller then dies on `JSON.parse` with "Unexpected token '<'" far from the actual cause. The MCP server, the CLI and Piggy all consume this API and would all have hit it. It was masked from casual testing because unauthenticated requests are rejected earlier by the auth middleware, so it only appears once you hold a valid token. Found by probing production with Scott's token: GET /api/keys (the real path is /api/api-keys) returned 200 text/html. The static-file middleware already carried this guard — added for the same reason when og.png was being served as HTML — but the SPA fallback beneath it did not. Same guard, one place missing. Verified: unknown API paths now return 404 application/json, real API paths still answer, client-side routes still receive the shell, and static assets still serve with their own content types. Typecheck clean, 124 unit tests and the e2e suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6bd5526675 |
Give Card min-w-0 so the page stops scrolling sideways on a phone
The Overview page overflowed 80px at 393px wide. Traced to the "The book" card: the grid column was a correct 361px, the card inside it was 457px and refused to shrink. Confirmed by forcing `min-width: 0` on grid children in the live page, which took the overflow to 0. Fixed on the Card base class rather than at the call site, because this is the third time the same trap has been fixed individually — grid and flex children default to `min-width: auto` and cards routinely hold something unshrinkable, a tabular-nums figure or a nowrap badge. `min-width: 0` is inert for a block-level card outside a flex or grid parent, so applying it always costs nothing and removes the whole class of bug. Verified by running the stack locally against the demo data: 0px overflow across all 12 routes at both 393px and 1440px. AGENTS.md updated to say any NEW container primitive needs the same, with the one-line browser check to confirm it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d4d7095605 |
Migrate before starting the application
CI / verify (push) Successful in 2m32s
|
||
|
|
853bde2265 |
Build the agent-native compute CRM platform
CI / verify (push) Successful in 3m6s
|
||
|
|
bfd2f8d95a |
Add AGENTS.md — onboarding for whoever picks this up next
CI / verify (push) Successful in 1m32s
Named by convention so a coding agent finds it without being told. Written to get someone productive from a cold start without having to reconstruct the reasoning from the diff. Four sections carry the weight. The architecture rules that must not be broken, each of which fails silently rather than loudly — intelligence never lives in the API, authentication is not authorization, cost is charged against the full commitment, sold and held are different things. The traps that have already cost time here, with the specific symptom each produces: onConflictDoNothing being a no-op without a constraint, z.coerce.boolean turning "false" into true, grid children needing min-w-0, Drizzle emitting a cast Postgres rejects, the CSP allowing exactly one inline script by hash, and the Prime Intellect API quoting node totals rather than per-GPU prices. The conventions, including that comments explain why rather than what. And an explicit start order. The last section says what not to do, which is the part most easily lost: do not copy component files from the MIT project we borrowed ideas from, do not open self-registration on a shared identity provider, do not put a production key on the CI runner, do not weaken the guard that refuses to serve the CRM unauthenticated, and do not invent email addresses for real people. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cf3117e458 |
Record verified Prime Intellect API facts, and log a real pricing bug
CI / verify (push) Successful in 1m33s
The token was found on cloud-1 after all, in a Claude memory note. Verifying its claims against the live API turned up a defect in code already shipped. **prices.onDemand is the total for the whole node, not per-GPU.** Confirmed: datacrunch lists 1x A100 at 1.79 and 2x A100 at 3.58, and gpuMemory scales the same way (640 for 8x 80GB). packages/prime/src/map.ts stores both as if they were per-GPU, so an 8-GPU node reads eight times too expensive. It would have silently poisoned inventory search, the max-price filter and every margin comparison against bought capacity — and nobody would have noticed, because the numbers still look plausible. Logged rather than fixed, per the instruction to hold; it needs a regression test built from the real 1x/2x pair. **Inference is a different host.** api.primeintellect.ai is compute and pods; inference is api.pinference.ai/api/v1, OpenAI-compatible. PIG's config knows only the first, so A4 and A13 need both. **Piggy's default model** is nvidia/nemotron-3-nano-30b-a3b, and the important detail is that it is a hybrid reasoning model which thinks aloud by default and truncates under a tight max_tokens. `reasoning_effort: "none"` gives ~1s terse output for tool use and extraction, which is what Piggy does nearly all of the time. One claim did NOT reproduce: the note warns of Cloudflare 403ing non-browser user-agents, but PIG's own UA and curl's both returned 200. Recorded as history in case a 403 ever appears. Also logged: the key is a broad, never-expiring credential sitting in plaintext in a memory markdown file. PIG's sync should hold a separate narrower key scoped to availability reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3398345109 |
Plan: add RBAC, capacity tiers, imports, admin settings; expand contracts
CI / verify (push) Successful in 2m36s
Six additions, and one promoted to foundation. RBAC becomes a Wave 0 task rather than something absorbed into each CRUD track. Authorization today stops at "is a member", and eight parallel write tracks plus a bulk-import feature are about to land. Import especially: one bad column mapping can rewrite thousands of records, so it must not ship before there is a real answer to who may run it. Suggested default is team leads and platform admins only — easy to loosen later, unpleasant to tighten. A government/sovereign capacity tier joins secure and community. It is a schema change, so it is cheaper before the tables carry real data. Matching must never satisfy a government requirement with community capacity, and the tier interacts with the export-control predicate already modelled in compliance.ts. Imports are split so the work is reusable: a CSV/Excel framework carrying the upload, mapping, dry-run preview and idempotent commit, with Notion and Google Sheets as thin OAuth front-ends onto the same mapping step. Notion databases are tables with typed properties, so treating them as a separate importer would duplicate the hard part. Admin settings gains Piggy's model selection, defaulting to a Nemotron model on Prime Intellect inference, with the endpoint configurable so an on-prem install can point at the customer's own. Contracts is expanded from "surface the schema" to the full field set — the hierarchy with order-form-beats-MSA precedence, negotiated SLA terms including fee abatement and its trigger, spare-pool scope, maintenance classes and the reasonable-endeavours carve-out, plus take-or-pay and termination tier. Written as a first draft to be corrected by someone who negotiates these for a living. Also records three open questions, including that the Prime Intellect API token could not be found on cloud-1 — searched ~/.prime, ~/.config/prime, /opt and /etc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e4698e4d0c |
Add a build plan, informed by reading Comp AI CRM properly
CI / verify (push) Successful in 1m32s
Cloned trycompai/crm (MIT) and inventoried it rather than assuming. Three findings shaped the plan. Their component library is far deeper: 68 primitives to our 9, including data-table, command, sheet, drawer, combobox, chart, and a set of agent-chat components that map almost exactly onto what Piggy needs. Their SourcedValue/Provenance pattern is worth adopting outright — a dotted underline on any agent-derived value with a tooltip carrying the claim, the reasons, when it was observed and the source URL. PIG already stores all of that in `facts` and surfaces none of it. But we are ahead of them on mobile, not behind. Measured across both repos: 211 responsive utilities across their 329 tsx files (0.6 per file) against 58 across our 16 (3.6 per file); zero safe-area handling to our five; no drawer or sheet used for navigation, no viewport-fit. Their app is effectively desktop-only. So the plan takes depth from them, not mobile behaviour. The plan also says not to copy their component files. Most are shadcn/ui originals — MIT, and designed to be installed from upstream where they are canonical and current. Borrow the compositions as ideas; the debt is already credited in NOTICE. Structured into waves with real dependency edges so the work can be handed to several agents without collision. Two foundation tasks must land alone first (the primitive set, and the API write-path convention) because eight parallel CRUD tracks would otherwise each invent their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
14417e34bc |
Margin table: say "Covered" rather than $0.00 for break-even
CI / verify (push) Successful in 1m51s
Same fix already applied to the capacity cards, missed here. A zero break-even means the block's cost is fully recovered and any further sale is upside; printing "$0.00" is technically true and reads like a rendering bug. The demo book has three such blocks, so it was visible on every screenshot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
468979b303 |
Add a plausible demo dataset
CI / verify (push) Successful in 2m1s
So the product is legible before anyone has entered real data, and so Piggy has something to reason about while it is being built. Kept separate from the base seed because that one is publicly-sourced and cited while this is invented. Two rules, both deliberate: Every record is prefixed "DEMO — ", so a screenshot can never be mistaken for real business. And demand-side customers are fictional. Suppliers are real companies — they are public, and naming the actual market is the point — but inventing customers with invented contract values against real named businesses would be fabricating commercial records about them, which is a different thing and not worth the extra realism. The numbers are tuned to teach rather than to flatter. The book clears +5.4% at 79% utilisation, which is thin and about right for this industry once capacity cost is charged honestly. Underneath, the blocks disagree: the large H200 block carries it, the EU H100 block is underwater at 55% sold because a 46% markup needs ~69% sold to break even, and the community pool holds a large unconverted hold — so the difference between "sold" and "held" is visible rather than theoretical. An earlier tuning left the whole book at -26%. Honest, but it reads as a broken product rather than an under-utilised book, so the totals now open healthy and the problems appear on drill-down. Also exercises parts of the schema nothing had touched yet: ramped capacity shapes, negotiated SLAs with fee abatement and spare-pool scope, renewal obligations with one deliberately near-term, EU data-residency constraints on a capacity request, and internal research burn. Two bugs found while testing it, both the same trap as before: the research allocation duplicated on every run because onConflictDoNothing() is a no-op without a matching unique constraint, and notes were double-prefixed. Verified idempotent over three consecutive runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2a30645c8d |
CI: Postgres on 127.0.0.1, because the runner uses host networking
CI / verify (push) Successful in 1m59s
The runner is configured with `container.network: host`. That one setting
explains all three earlier failures, and the workflow now records them so
nobody repeats the sequence:
services: not resolvable by name from a host-networked
job — "getaddrinfo EAI_AGAIN postgres"
--network container:$HOSTNAME /etc/hostname is the HOST's name, not a
container id, so the join finds nothing
default-gateway addressing wrong idea outright: with host networking the
default route is the real router, not a bridge
Sharing the host's network namespace means a published port is just on
127.0.0.1. Readiness is now checked over TCP from the job itself rather than
with pg_isready inside the container — the latter proves the server started,
not that this job can reach it, which is the thing that actually failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e6b4c1618e |
CI: reach Postgres through the gateway instead of a shared namespace
CI / verify (push) Failing after 5s
Second attempt failed differently: /etc/hostname inside the job reports the HOST's name rather than the container id, so `--network container:$HOSTNAME` found no such container. Rather than hunt for our own container id through /proc, publish the port on the host and connect through the job container's default gateway. That needs no container identity at all. The port is derived from the run id so two concurrent runs cannot collide, and DATABASE_URL is exported through GITHUB_ENV once Postgres is actually accepting connections. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
40f6fd993d |
CI: start Postgres as a step rather than a service container
CI / verify (push) Failing after 5s
The first run got through install, typecheck and all 39 tests, then failed on `getaddrinfo EAI_AGAIN postgres`. This runner does not attach service containers to the job's network, so the `services:` hostname never resolves. Fixed by starting Postgres with `--network container:$HOSTNAME`, sharing the job container's own network namespace so it appears on 127.0.0.1. That works regardless of how the runner is configured — which matters here because the runner is shared with other repositories and should not need reconfiguring to suit this one. Also queries row counts through `docker exec` rather than a local psql, since the runner image is not guaranteed to ship postgresql-client, and removes the container in an `if: always()` step so a failed run does not leave it behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
73231a8944 |
Add CI, a test suite, and a deploy script
CI / verify (push) Failing after 34s
`npm test` did nothing until now. CI that runs no tests is theatre, so the tests came first — 39 of them, over the two places where an error would be silent and expensive. packages/core: the margin arithmetic. Every dashboard figure, idle-capacity alert and agent answer resolves through it, and wrong numbers still look like numbers. The cases pin decisions rather than implementation: cost is charged against the full commitment (a naive version reports the opposite sign on a loss-making block), aggregation sums cents rather than averaging percentages (averaging reports +22% on a book that is losing money), break-even prices the remaining hours and returns null rather than Infinity when there are none, and internal research burn counts as cost with no revenue. packages/prime: the upstream mapping. Rounding rather than truncating cents, because 2.43 is 2.4299999 in binary and a lost cent compounds across millions of GPU-hours. And interconnect normalisation, where an unrecognised fabric maps to Unknown rather than Ethernet — guessing low loses a deal, guessing high sells a training customer a cluster that cannot train. CI runs on push and pull request: typecheck all six packages, unit tests, migrations applied twice to a real Postgres, a seed-idempotency assertion that fails the build if row counts move on a second run, a server boot, the front-end build, and a Docker build. It also asserts the inline theme script's hash still matches the CSP the proxy allows. That script prevents a white flash for dark-mode users; if it changes without the CSP being updated, the browser silently blocks it and nothing anywhere reports an error. Deployment stays a script rather than push-to-deploy. Automating it would put an SSH key with production write access on the CI runner — a real escalation for a project this size. The script takes a database dump before migrating and refuses to finish if an unauthenticated request returns anything but 401. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7719850fc5 |
Let people register with a personal email and an invite code
The invite gate was unreachable. PIG shares its identity provider with another application, and self-registration there is deliberately switched off — so somebody with a personal address and a valid invite code could never obtain a token, and therefore could never reach the endpoint that accepts the code. The gate was real and nothing could ever arrive at it. Opening self-registration on the provider would have opened it for the neighbouring application too, which is precisely why it was closed. So PIG now mints the account itself through the provider's admin API, and only after the invite validates. The provider stays shut; the invite becomes the actual gate. Order is deliberate: validate the invite, create the auth account, create the profile, consume the invite. If the profile write fails the auth account is deleted again — otherwise someone could sign in with no profile and no way to obtain one, because their invite would look spent. An administrator's address does NOT bypass this. The bypass in /api/signup exists to bootstrap the first admin from an account that already exists; here an account is created from nothing, and an ungated version of that is simply an open registration endpoint. Registration signs the person in on success rather than returning them to a login form to retype the password they entered ten seconds earlier. An address that already exists in the provider but has no PIG profile is detected and pointed at sign-in, since /api/signup handles that case properly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
045e51bc5c |
Polish: team page title, and accent swatches that match the theme
The accent swatches always previewed the light-mode value, which made the near-black "Pig" swatch effectively invisible on a dark card — the one place the preview needed to be accurate. Each accent is tuned twice because a colour that reads well on white is usually too dark on near-black; the swatch now shows the value for the theme actually in effect. Team and the placeholder routes also set document titles, so every route in the app is now distinguishable in history and in a tab strip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7c134140ff |
Fix static serving — og.png was returning index.html
The server mounted static files at /assets only, so every top-level file fell through to the SPA catch-all. og.png, apple-touch-icon.png, icon-192.png and manifest.webmanifest each returned `200 text/html` containing the app shell. This is a nearly invisible failure. The app works. The tab shows an icon, because browsers cache the SVG. Nothing errors anywhere. But a link shared to iMessage, Slack or X fetches og.png, receives HTML, and renders a card with no image — which is the entire point of having made one. Now the whole dist directory is served, with /api excluded so routes are never answered from disk, and the SPA fallback still catching client-side routes that have no file behind them. Found by fetching the asset from an outside host instead of trusting that adding the file was enough. Verified: og.png is image/png at 53KB, the icons and manifest carry their real types, /margin and /capacity still receive the shell, and the API is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d154a78d48 |
Polish: per-route titles and a way to sign out
There was no sign-out control anywhere. Easy to miss when you develop permanently signed in, and a stranded feeling for anyone on a shared machine. Clearing the identity provider's session is sufficient — PIG holds no session of its own — and the auth listener returns to sign-in without a reload. A hard redirect follows regardless, so a failed provider call cannot leave a half-signed-out interface. Every route now sets its own document title. A single static title makes browser history and a wall of tabs useless: every entry reads "pig" and nobody can tell the margin view from the pipeline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
46d7d80f1b |
Fix boolean env parsing — PRIME_SYNC_ENABLED=false was reading as true
z.coerce.boolean() calls Boolean(value), so the string "false" is true. So are "0", "no" and "off". Every feature flag set to false was silently on. Caught by reading a startup warning that should not have been there: the deployment logged "PRIME_SYNC_ENABLED is on but PRIME_API_KEY is unset" while the .env plainly said false. Had the key been present, PIG would have started polling a third-party API nobody asked it to poll. Replaced with an explicit parser accepting 1/true/yes/on, treating an empty or absent value as the default. Demonstrated both behaviours side by side before committing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0551e8dd6e |
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> |
||
|
|
93818a2d2c |
Document two proxy traps found by deploying rather than assuming
The deployment came up healthy, served a valid certificate, and returned 200 — and was completely unreachable. Two distinct causes, both invisible from inside the host: 1. Every other site on this proxy binds to a private VNIC address. Caddy groups site blocks into servers BY listen address, so a block without `bind` landed in a separate server on :443. The specific listener wins for traffic arriving on that address, which is all public traffic after NAT, so requests hit the server that had never heard of these hostnames and fell through to an empty 200. Testing from the host with --resolve 127.0.0.1 worked perfectly, which is exactly why this was worth chasing from a third machine instead of trusting a local check. 2. The CSP blocked the inline pre-paint theme script, so dark-mode users would have seen a white flash on every load. Fixed with the script's hash rather than 'unsafe-inline', which would have defeated the policy, and rather than an external file, which would have reintroduced the flash. Editing that script changes its hash and silently breaks it, so that is written down. Verified from an independent host: health returns JSON, the app serves, an unauthenticated API call is refused, the short alias redirects, security headers are present, and the existing sites on the proxy are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7bb8835974 |
Add profile creation — close the gap between signing in and being a member
Deploying and then trying to actually use it surfaced a dead end: /api/signup was exempted from auth but never implemented, so a real person could sign in, receive 403 needs_profile, and have nowhere to go. Authentication worked; joining did not. The route is mounted before the auth middleware, because requiring membership to reach the route that grants membership is circular. It verifies the token itself and then requires one of two things: - A valid invite code. Stored hashed, optionally pinned to an address, optionally expiring, consumed on redemption with the redeemer recorded. - Presence in PIG_ADMIN_EMAILS. The bootstrap path, which exists because on a fresh deployment nobody can issue an invite since nobody can sign in to issue one. The bootstrap path is narrow by construction: the address must be listed in server-side configuration AND match the verified email claim on the token. Admin rights are never read from the request body, so a crafted payload cannot grant them. Two behaviours worth noting. A row that was invited but never signed into is claimed rather than rejected, binding it to the identity that just proved ownership of the address. And a resubmitted form returns the existing user instead of erroring, because a double-tap should be harmless. The front end now treats needs_profile as a step in the flow rather than an error, showing a team picker. Sending someone back to a login screen they have already completed is a loop with no exit. Also: the seed no longer creates the dev@localhost admin row under NODE_ENV=production. It was unreachable (no auth subject, so nobody can sign in as it), but an admin-flagged placeholder in a real deployment is a trap. Verified: rejects a missing token, rejects an invalid body, claims a pre-existing row, and is idempotent on resubmission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c747eb2aa7 |
Add deployment: Dockerfile, compose, proxy config, and docs
One container plus a Postgres behind any TLS-terminating proxy. Nothing is specific to a particular host. The app and API are served from a SINGLE origin. This is not tidiness: browser auth sessions live in per-origin storage, so splitting them across two hostnames makes sign-in loop in a way that presents as a server fault. The short alias redirects rather than serving a second origin. Two safety properties verified by running the image, not by reading the code: - With NODE_ENV=production and no SUPABASE_URL, the process refuses to start and says why. Serving the whole CRM unauthenticated is a worse outcome than failing to deploy, so the failure is deliberate and loud. - In production the development auth bypass does not apply: an unauthenticated request to /api/dashboard returns 401 rather than adopting the first user in the table. The Dockerfile typechecks all six packages as a build gate, so a deploy that does not compile fails at build time rather than in front of a user. Runtime runs unprivileged as `node`, and Postgres is not published to the host. Docs cover the ontology and why it is shaped this way, agent connection for Claude Code / Codex / prime-agent / Buzz, and the provenance rules governing seed data about real people — including how to have your record removed. Verified: image builds, container reports healthy, serves the SPA, enforces auth, and the production guard exits non-zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
de33a03524 |
Add the web app, seed data, and user-selectable theming
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> |
||
|
|
7aeec0c632 |
Add Prime Intellect client, API, and MCP server
packages/prime — a hand-written typed client, because the first-party SDK is Python only. Deliberately narrow: PIG reads availability and nothing else, and the key it holds should be scoped so it could not provision even if the code tried. Rate limits are undocumented upstream, so it backs off empirically with full jitter and honours Retry-After. Unknown fields survive in `raw` rather than being dropped. apps/api — Hono, with authentication and authorization kept firmly apart. A verified JWT proves someone has an account in the identity project, which may be shared with other applications; it does NOT prove they belong here. Access requires a row in PIG's own users table, and a token without one gets 403 needs_profile rather than entry. The capacity service is the business logic: availability counts sold and held separately, so a live hold removes inventory from everyone else's availability without inflating utilisation. Expired holds are ignored at read time, so the numbers stay right even when the sweeper is behind. Matching treats interconnect as a hard filter and excludes Unknown as well as Ethernet — unverified is not the same as adequate. apps/mcp — nine tools over stdio, so a team member drives PIG from Claude Code, Codex, prime-agent, or a Buzz agent. It holds an API key and calls the same HTTP API the browser does, with no database credentials, so an agent can never reach further than the person it acts for. Results are formatted as prose rather than raw JSON. Theme preferences live in the database rather than localStorage, so a chosen accent follows someone from laptop to phone. Status colours stay independent of the accent: if "at risk" re-tinted to whatever a user picked, the signal would be gone. Note on the SDK import: its package exports use a `./*` wildcard whose types entry resolves server/mcp.js to server/mcp.js.d.ts, which does not exist. The runtime specifier must keep the .js suffix, so the types are mapped via tsconfig paths rather than by writing an import that would fail at runtime. Verified: all five packages typecheck; the MCP server constructs and registers its tools. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d36762f264 |
Scaffold PIG and model the compute-GTM ontology
PIG is an agent-native CRM for two-sided AI-compute companies: businesses that buy GPU capacity from providers and resell it. Their business is the spread between two pipelines, which is precisely what a generic CRM cannot represent. The load-bearing decision is the `allocations` table, joining a capacity_commitment (what we bought, at a known cost) to a demand_deal (what we sold, at a known price). Margin, utilisation and idle capacity all fall out of that one join. Cost is charged against the full commitment rather than only the hours that sold, because unsold hours are already paid for and any other treatment flatters a block that is losing money. Domain decisions worth noting, each grounded in how this market operates: - Demand stages put `legal` second, not last. Customers do not hand workloads to an infrastructure provider before paper is executed. - Supply qualification splits technical from financial diligence, recorded attributably. Accepting capacity is a two-key decision. - Capacity carries a time SHAPE (intervals + quantities), not a window. Commitments ramp and step down; a rectangle reports availability that does not exist in the month someone wants it. - SLAs model three distinct shapes: none, a reliability tier plus credits policy, and a negotiated agreement. Aggregators generally cannot promise uptime on resold capacity, but negotiate heavyweight paper upstream. Remedies include fee abatement, which is materially better than a capped credit and is not expressible as one. - Export control is a predicate on the allocation edge, evaluated against the ULTIMATE parent's jurisdiction. Country of incorporation is not a valid key, so this cannot live as a flag on an account. - Agent-derived claims land in `facts` with a confidence band and evidence. Only verified claims self-apply; weaker ones await review. - The API never calls the agent. It writes to a leased queue, guarded by a partial unique index on unfinished work. Verified: typechecks clean, migration generates and applies to Postgres 16 (31 tables, 24 enums, 117 indexes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |