Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ npm run dev -- -p 3020 # Port 3020 to avoid conflicts
| Layer | Technology |
| ---------- | ------------------------------------------------ |
| Framework | Next.js 16 (App Router), React 19 |
| Language | TypeScript 5.8 |
| Styling | Tailwind CSS 3.3 |
| Language | TypeScript 5.x |
| Styling | Tailwind CSS 4 |
| Database | Self-hosted Supabase (PostgreSQL + Auth + RLS) |
| Bitcoin | Lightning Network, BTCPay, NWC |
| Deployment | Self-hosted on Hetzner (`bitbaum`, behind Caddy) |
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ FleetCrown (rebranded from Cockpit) is a live customer project (see "FleetCrown"

| Layer | Technology |
| ---------- | ----------------------------------------------------------------------------------- |
| Framework | Next.js 16, React 19, TypeScript 5.8 |
| Styling | Tailwind CSS 3.3 |
| Framework | Next.js 16, React 19, TypeScript 5.x |
| Styling | Tailwind CSS 4 |
| Database | Self-hosted Supabase (PostgreSQL + Auth + RLS) — `supabase.orangecat.ch` on Hetzner |
| Bitcoin | Lightning Network, BTCPay, NWC |
| Deployment | Self-hosted on Hetzner (`bitbaum`, behind Caddy) |
Expand Down Expand Up @@ -45,7 +45,8 @@ Multi-commit migration in progress:
- ✅ Warm-accent Button variant; landing + auth + about + header CTAs all use `variant="accent"` (ff9f2ce5, 2cdb0907)
- ⏳ Drop chromatic brand palette (tiffany, orange) from new code; migrate existing classes to semantic tier (`bg-card → bg-surface-base`, `text-foreground → text-fg-primary`)
- ⏳ Display typography (Space Grotesk for headings, IBM Plex Mono for code) replacing Inter-only
- ⏳ Tailwind v4 + OKLCH color space
- ✅ Tailwind v4 (package on 4.x — this line previously claimed 3.3 long after main had moved, which misled agents; keep this table honest)
- ⏳ OKLCH color space for the token tier

End state: monochromatic surfaces + one warm accent (`#ff5c00`) for top-of-funnel conversion + Bitcoin Orange for Bitcoin-specific UI + status colors only for actual status. Everything else stays achromatic.

Expand Down
36 changes: 23 additions & 13 deletions __tests__/unit/config/entity-form-schema-drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,27 @@ const ALLOWLIST = new Set<string>([
'pricing_model', // service: documented UI-only toggle (hourly vs fixed), never persisted — see commerce.ts UserServiceFormData
]);

/** Unwrap ZodEffects (.refine/.transform/.superRefine) to the inner object. */
/**
* Unwrap zod v4 wrappers to the inner object. v4 restructured internals:
* refinements are checks ON a schema (no ZodEffects wrapper), transforms are
* pipes, and the def lives at `_zod.def` with a lowercase `type` tag.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function unwrap(schema: any): any {
let s = schema;
const seen = new Set();
while (s && s._def && !seen.has(s)) {
while (s && s._zod?.def && !seen.has(s)) {
seen.add(s);
const tn = s._def.typeName;
if (tn === 'ZodEffects') s = s._def.schema;
else if (tn === 'ZodDefault') s = s._def.innerType;
else if (tn === 'ZodOptional' || tn === 'ZodNullable') s = s._def.innerType;
else if (tn === 'ZodBranded') s = s._def.type;
const def = s._zod.def;
if (def.type === 'pipe') s = def.in;
else if (
def.type === 'optional' ||
def.type === 'nullable' ||
def.type === 'default' ||
def.type === 'readonly' ||
def.type === 'catch'
)
s = def.innerType;
else break;
}
return s;
Expand All @@ -40,14 +49,15 @@ function unwrap(schema: any): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function schemaInfo(schema: any): { keys: Set<string>; passthrough: boolean } {
const s = unwrap(schema);
if (!s || !s._def || s._def.typeName !== 'ZodObject') {
const def = s?._zod?.def;
if (!def || def.type !== 'object') {
return { keys: new Set(), passthrough: false };
}
const shape = typeof s.shape === 'function' ? s.shape() : s.shape;
const passthrough =
s._def.unknownKeys === 'passthrough' ||
(s._def.catchall && s._def.catchall._def && s._def.catchall._def.typeName !== 'ZodNever');
return { keys: new Set(Object.keys(shape ?? {})), passthrough };
const shape = def.shape ?? {};
// .passthrough()/.loose() set a non-never catchall in v4.
const catchallType = def.catchall?._zod?.def?.type;
const passthrough = catchallType !== undefined && catchallType !== 'never';
return { keys: new Set(Object.keys(shape)), passthrough };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
2 changes: 1 addition & 1 deletion __tests__/unit/validation/base-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('lightningAddressSchema', () => {
const result = lightningAddressSchema.safeParse('justuser');
expect(result.success).toBe(false);
if (!result.success) {
const messages = result.error.errors.map(e => e.message);
const messages = result.error.issues.map(e => e.message);
expect(messages.some(m => /valid Lightning address/i.test(m))).toBe(true);
}
});
Expand Down
Loading
Loading