diff --git a/apps/web/src/pages/Imports.tsx b/apps/web/src/pages/Imports.tsx
index e77cc97..5a6af92 100644
--- a/apps/web/src/pages/Imports.tsx
+++ b/apps/web/src/pages/Imports.tsx
@@ -145,7 +145,7 @@ export function Imports() {
type="button"
aria-pressed={entity === candidate}
onClick={() => resetForEntity(candidate)}
- className={entity === candidate ? 'tap card min-w-0 border-accent p-4 text-left ring-1 ring-accent' : 'tap card min-w-0 p-4 text-left'}
+ className={entity === candidate ? 'tap card min-w-0 border-primary p-4 text-left ring-1 ring-primary' : 'tap card min-w-0 p-4 text-left'}
>
{candidateDefinition.label}
{candidateDefinition.description}
diff --git a/apps/web/src/pages/Pipeline.tsx b/apps/web/src/pages/Pipeline.tsx
index 39a0acb..e5b83da 100644
--- a/apps/web/src/pages/Pipeline.tsx
+++ b/apps/web/src/pages/Pipeline.tsx
@@ -184,7 +184,7 @@ function PipelineBoard
diff --git a/apps/web/src/pages/Register.tsx b/apps/web/src/pages/Register.tsx
index 2a35b37..4583e50 100644
--- a/apps/web/src/pages/Register.tsx
+++ b/apps/web/src/pages/Register.tsx
@@ -172,7 +172,7 @@ export function Register({
className={[
'flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors',
team === value
- ? 'border-accent bg-accent-subtle'
+ ? 'border-primary bg-accent-subtle'
: 'border-border hover:bg-surface-2',
].join(' ')}
>
diff --git a/apps/web/src/pages/Settings.tsx b/apps/web/src/pages/Settings.tsx
index 9d34d09..8ca9113 100644
--- a/apps/web/src/pages/Settings.tsx
+++ b/apps/web/src/pages/Settings.tsx
@@ -14,6 +14,7 @@ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input } from '
import { useState } from 'react';
import { usePageTitle } from '@/lib/title';
import { AdminSettings } from '@/components/AdminSettings';
+import { toast } from 'sonner';
interface Me {
id: string;
@@ -93,7 +94,7 @@ function Appearance() {
title={option.label}
className={[
'tap relative flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition-colors',
- selected ? 'border-accent bg-accent-subtle' : 'border-border hover:bg-surface-2',
+ selected ? 'border-primary bg-accent-subtle' : 'border-border hover:bg-surface-2',
].join(' ')}
>
{/*
@@ -144,7 +145,11 @@ function Profile({ me }: { me: Me | undefined }) {
void queryClient.invalidateQueries({ queryKey: ['me'] });
setName('');
setTitle('');
+ // The form clears itself on success, which without confirmation reads as
+ // though the input was discarded rather than saved.
+ toast.success('Profile saved');
},
+ onError: () => toast.error('Could not save your profile'),
});
if (!me) return null;
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index 61a0260..433e17c 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -20,8 +20,22 @@ export default {
foreground: 'hsl(var(--fg))',
muted: 'hsl(var(--muted))',
'muted-foreground': 'hsl(var(--muted))',
- accent: 'hsl(var(--accent))',
- 'accent-foreground': 'hsl(var(--accent-on))',
+ /*
+ * shadcn's `accent` is its SUBTLE hover/selected surface — dropdown
+ * items, command rows, ghost-button hover. It is not the brand colour;
+ * that is `primary`, mapped below.
+ *
+ * Mapping `accent` to --accent (the brand) inverted this, so every
+ * hover state painted a full-strength brand block: in dark mode with
+ * the monochrome "pig" accent that is near-white, which made a selected
+ * command row glare. Aliased to the subtle pair instead, so shadcn
+ * primitives tint the way their authors intended while PIG's own brand
+ * fills use `primary`.
+ */
+ accent: 'hsl(var(--accent-subtle))',
+ 'accent-foreground': 'hsl(var(--accent-fg))',
+ /** The brand itself, for PIG's own components that need a solid fill. */
+ brand: 'hsl(var(--accent))',
'accent-fg': 'hsl(var(--accent-fg))',
'accent-on': 'hsl(var(--accent-on))',
'accent-subtle': 'hsl(var(--accent-subtle))',
diff --git a/deploy/README.md b/deploy/README.md
index e5cf3a5..aead6e7 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -98,6 +98,34 @@ applied. Take a dump before a major upgrade anyway:
docker compose -p pig exec db pg_dump -U pig pig | gzip > pig-$(date +%F).sql.gz
```
+## On-premises: using your own identity provider
+
+PIG authenticates against any standards-compliant OIDC provider, which is how
+an install inside your own network works. Set:
+
+```bash
+PIG_OIDC_ISSUER=https://id.yourcompany.internal
+PIG_OIDC_AUDIENCE=pig # the client/app id you registered for PIG
+```
+
+That is usually the whole configuration — the JWKS is discovered from the
+issuer. On an air-gapped network, set `PIG_OIDC_JWKS_URI` too and no discovery
+request is made.
+
+`PIG_OIDC_ISSUER` takes precedence over `SUPABASE_URL`, so the hosted values
+can stay in the environment file without quietly taking over.
+
+**Set the audience.** Without it, any token your provider issued for any
+application in the same tenant verifies here — a token minted for an unrelated
+internal tool would be accepted as a PIG session. PIG warns about this at boot
+but cannot refuse, because some providers legitimately issue single-audience
+tokens.
+
+**Provisioning stays in PIG.** Authenticating proves who someone is; it does
+not make them a member. They still need an invite, and their team and role live
+in PIG's database. That is deliberate — your directory should not have to model
+"supply lead versus demand member" for one application.
+
## A note on the auth project
PIG verifies JWTs but authorizes from its own `users` table. If the Supabase
diff --git a/packages/db/src/seed/demo.ts b/packages/db/src/seed/demo.ts
index cb701cc..adc3d97 100644
--- a/packages/db/src/seed/demo.ts
+++ b/packages/db/src/seed/demo.ts
@@ -41,6 +41,7 @@ import { createDatabase } from '../client';
import {
accounts,
activities,
+ facts,
allocations,
capacityCommitments,
capacityRequests,
@@ -660,7 +661,166 @@ async function seedDemo() {
}
}
+ // -------------------------------------------------- agent-derived facts
+ //
+ // Without these the fact-review queue and every provenance tooltip are
+ // empty, which hides the thing that makes an agent-written CRM trustworthy:
+ // that each claim carries a score, a band, evidence and a source, and that
+ // only verified claims apply themselves.
+ //
+ // The mix is deliberate. Two `applied` facts show what a confident agent
+ // writes unprompted; four `proposed` show what waits for a human; one is a
+ // near-miss that a reviewer should reject, so the queue is not a row of
+ // obvious approvals.
+ const factSeeds: {
+ accountDomain?: string;
+ contactName?: string;
+ field: string;
+ value: string;
+ score: string;
+ band: 'verified' | 'probable' | 'possible';
+ status: 'applied' | 'proposed';
+ method: string;
+ sourceUrl?: string;
+ evidence: Record;
+ }[] = [
+ {
+ accountDomain: 'coreweave.com',
+ field: 'supplierType',
+ value: 'neocloud',
+ score: '0.960',
+ band: 'verified',
+ status: 'applied',
+ method: 'web_search',
+ sourceUrl: 'https://www.coreweave.com/',
+ evidence: {
+ quote: 'Describes itself as an AI hyperscaler providing GPU cloud infrastructure.',
+ corroboration: 2,
+ },
+ },
+ {
+ accountDomain: 'nebius.com',
+ field: 'jurisdiction',
+ value: 'European Union',
+ score: '0.910',
+ band: 'verified',
+ status: 'applied',
+ method: 'web_search',
+ sourceUrl: 'https://nebius.com/',
+ evidence: {
+ quote: 'Operates a datacentre in Finland, inside the EU data-residency perimeter.',
+ matters: 'Determines eligibility for customers with EU residency requirements.',
+ },
+ },
+ {
+ accountDomain: 'crusoe.ai',
+ field: 'certifications',
+ value: 'SOC 2 Type II',
+ score: '0.720',
+ band: 'probable',
+ status: 'proposed',
+ method: 'web_search',
+ sourceUrl: 'https://crusoe.ai/',
+ evidence: {
+ quote: 'A trust page references SOC 2, but the report scope and observation window are not stated.',
+ caution: 'Scope matters — a report can cover only some products.',
+ },
+ },
+ {
+ accountDomain: 'lambda.ai',
+ field: 'supplierType',
+ value: 'neocloud',
+ score: '0.680',
+ band: 'probable',
+ status: 'proposed',
+ method: 'web_search',
+ sourceUrl: 'https://lambda.ai/',
+ evidence: { quote: 'Markets GPU cloud and on-premises clusters.' },
+ },
+ {
+ contactName: 'Dana Whitfield',
+ field: 'title',
+ value: 'VP Infrastructure',
+ score: '0.540',
+ band: 'possible',
+ status: 'proposed',
+ method: 'inference',
+ evidence: {
+ reasoning: 'A conference bio lists a VP title; the CRM records Head of Infrastructure.',
+ conflict: 'Sources disagree, and neither is dated.',
+ },
+ },
+ {
+ accountDomain: 'runpod.io',
+ field: 'customerSegment',
+ value: 'frontier_lab',
+ score: '0.310',
+ band: 'possible',
+ status: 'proposed',
+ method: 'inference',
+ evidence: {
+ reasoning: 'Inferred from a blog post mentioning large training runs.',
+ warning:
+ 'Weak. This is a supply-side provider, not a frontier lab — a reviewer should reject it.',
+ },
+ },
+ ];
+
+ let factsAdded = 0;
+ for (const seed of factSeeds) {
+ let accountId: string | undefined;
+ let contactId: string | undefined;
+
+ if (seed.accountDomain) {
+ const [row] = await db
+ .select({ id: accounts.id })
+ .from(accounts)
+ .where(eq(accounts.domain, seed.accountDomain))
+ .limit(1);
+ accountId = row?.id;
+ }
+ if (seed.contactName) {
+ const [row] = await db
+ .select({ id: contacts.id })
+ .from(contacts)
+ .where(eq(contacts.fullName, seed.contactName))
+ .limit(1);
+ contactId = row?.id;
+ }
+ if (!accountId && !contactId) continue;
+
+ // Idempotent on the natural key: one claim per subject per field per value.
+ const [existing] = await db
+ .select({ id: facts.id })
+ .from(facts)
+ .where(
+ and(
+ accountId ? eq(facts.accountId, accountId) : eq(facts.contactId, contactId!),
+ eq(facts.field, seed.field),
+ eq(facts.value, seed.value),
+ ),
+ )
+ .limit(1);
+ if (existing) continue;
+
+ await db.insert(facts).values({
+ accountId,
+ contactId,
+ field: seed.field,
+ value: seed.value,
+ score: seed.score,
+ band: seed.band,
+ status: seed.status,
+ method: seed.method,
+ sourceUrl: seed.sourceUrl,
+ evidence: seed.evidence,
+ observedAt: at(-Math.round(Math.random() * 6) - 1),
+ });
+ factsAdded += 1;
+ }
+
console.log(' 4 capacity commitments, with sites, MSAs and negotiated SLAs');
+ console.log(` ${factSeeds.length} agent-derived facts (${factsAdded} new) — 2 applied, 4 awaiting review`);
console.log(' 6 demand deals across the pipeline, 5 supply deals');
console.log(' Allocations including one unconverted hold and internal research burn');
console.log('\nEverything is prefixed "DEMO — ". Remove it with: npm run db:demo -- --clear');