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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 1 addition & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
DATABASE_URL="postgresql://aoz_wohnen:pass@localhost:5432/aoz_wohnen"

# Direct connection URL (for Prisma migrations). On self-hosted Postgres this
# is the same as DATABASE_URL; kept separate so schema.prisma's directUrl works.
DIRECT_URL="postgresql://aoz_wohnen:pass@localhost:5432/aoz_wohnen"

# =============================================================================
# AUTHENTICATION
Expand All @@ -34,7 +32,7 @@ LOGIN_RATE_LIMIT="10"
# REQUIRED in production — set as Authorization: Bearer <value> header
CRON_SECRET=""

# Initial admin account (used by prisma/seed-admin.ts)
# Initial admin account (used by scripts/db/seed-admin.ts)
# Change these before first deploy!
ADMIN_CODE="AOZ-ADMIN1"
ADMIN_NAME="Admin"
Expand Down
21 changes: 4 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ jobs:

- run: npm ci

- name: Generate Prisma client
run: npx prisma generate

- name: Lint
run: npm run lint

Expand All @@ -49,9 +46,6 @@ jobs:

- run: npm ci

- name: Generate Prisma client
run: npx prisma generate

- name: Run unit tests
run: npm test -- --ci --coverage

Expand Down Expand Up @@ -82,7 +76,6 @@ jobs:
run: npm run build
env:
DATABASE_URL: 'postgresql://fake:fake@localhost:5432/fake'
DIRECT_URL: 'postgresql://fake:fake@localhost:5432/fake'
# Not a real secret: page-data collection imports auth/constants,
# whose production fail-fast requires this to be set (>=32 chars).
SESSION_SECRET: 'ci-build-only-dummy-0123456789abcdef'
Expand Down Expand Up @@ -125,28 +118,23 @@ jobs:

- run: npm ci

- name: Generate Prisma client
run: npx prisma generate

- name: Initialize database schema
run: npx prisma migrate deploy
run: npm run db:migrate
env:
DATABASE_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test'
DIRECT_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test'

# Through the npm scripts, never `npx ts-node prisma/…` directly: the
# Through the npm scripts, never `npx ts-node scripts/db/…` directly: the
# seed imports the product's compatibility algorithm by its `@/` alias,
# and only the scripts carry `-r tsconfig-paths/register`. Invoking
# ts-node here by hand was a second definition of "how to seed", and it
# broke the moment the seed stopped re-implementing the algorithm.
# Guarded by src/lib/__tests__/scoring-ssot.test.ts.
- name: Seed E2E data
run: |
npm run prisma:seed
npm run prisma:seed:admin
npm run db:seed
npm run db:seed:admin
env:
DATABASE_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test'
DIRECT_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test'

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
Expand All @@ -155,7 +143,6 @@ jobs:
run: npm run test:e2e
env:
DATABASE_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test'
DIRECT_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test'
SESSION_SECRET: 'ci-test-secret-not-for-production'
STAFF_INVITE_CODE: '0000'

Expand Down
46 changes: 24 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ The live product is **https://aoz-wohnen.orangecat.ch** on the Hetzner box
`/opt/aoz-wohnen/shared/.env` on the box.

Neon, Vercel and hosted Supabase were decommissioned on 2026-06-12. A laptop
`.env` that still names `neon.tech` is leftover garbage. Prisma will load it
and time out; that is not "production is down". Do not restore those URLs.
`.env` that still names `neon.tech` is leftover garbage. The db client will
load it and time out; that is not "production is down". Do not restore those URLs.
Do not treat gitignored env files as SSOT. This laptop's Postgres is not
`aoz_wohnen`.

Uncommitted work, and any branch that is not `master`, is not what residents
see. Deploy is push to `master` → `.github/workflows/deploy.yml` (waits for
CI, pulls box env, `prisma migrate deploy`, build, rsync). Manual:
CI, applies pending `drizzle/*.sql` via fleetcrown's apply-schema.sh, build,
rsync). Manual:
`gh workflow run deploy.yml -R bitbaum/aoz-housing`.

Full table: `docs/INFRASTRUCTURE.md`.
Expand Down Expand Up @@ -136,7 +137,7 @@ This system serves **vulnerable populations** (asylum seekers). Every decision m
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS (mobile-first) |
| Database | PostgreSQL + Prisma |
| Database | PostgreSQL + Drizzle ORM |
| Validation | Zod (SSOT for types) |
| Testing | Jest + Playwright |

Expand Down Expand Up @@ -449,7 +450,7 @@ guards that specific class.

**Building locally needs one env var, and without it the failure lies about
its cause.** A bare `npm run build` dies with `Failed to collect configuration
for /api/auth/demo` — a route that queries `prisma.user`, so the obvious
for /api/auth/demo` — a route that queries the user table, so the obvious
reading is "this laptop has no `aoz_wohnen` database, builds are impossible
here". That reading is wrong, and it was believed twice on 2026-09-01 before
anyone read far enough down the log to the real `[cause]`:
Expand Down Expand Up @@ -554,7 +555,7 @@ export const RESIDENT_FACTORS = {

**Maximum 2 files to change:**
1. `src/lib/config/resident-factors.ts` - Define factor
2. `prisma/schema.prisma` - Add column
2. `src/lib/db/schema.ts` - Add column (then `npm run db:generate` for the migration)

**If you need to edit more files, the architecture is wrong.**

Expand Down Expand Up @@ -826,7 +827,7 @@ identity. `src/lib/expenses/` holds the pure logic; routes only do I/O.
`simplifyDebts` yields a stable ≤ n−1 transfer plan (greedy, id-tiebreak).
- **Resident FKs are `Restrict`, not `Cascade`** — deleting a payer would
silently change everyone else's balance. Residents exit via status.
- Categories are **config, not a Prisma enum** (`lib/config/expenses.ts`):
- Categories are **config, not a database enum** (`lib/config/expenses.ts`):
a new category is a config change, never a migration.

Who may do what: any current unit member records expenses (also on behalf of
Expand All @@ -842,7 +843,7 @@ portal lets them OPTIONALLY set `displayName`, `bio` and a photo:
the SSOT for display — never inline `displayName || code`, and never render
`resident.code` directly. Both helpers already fall back to the code, so the
privacy default is preserved for free. **Selecting only `{ code: true }` in
Prisma causes the same bug one layer earlier** — spread `RESIDENT_NAME_SELECT`
a query causes the same bug one layer earlier** — spread `RESIDENT_NAME_SELECT`
into any query whose rows reach the UI, and use `ResidentSummary` (which
carries `displayName`) for compatibility cards.
**`NamedResident.displayName` is REQUIRED, not optional — this is the load-
Expand Down Expand Up @@ -883,7 +884,7 @@ profiles.

### Real deployments vs demo

`prisma/seed-real.ts` seeds a REAL apartment from `prisma/real/*.ts` config
`scripts/db/seed-real.ts` seeds a REAL apartment from `scripts/db/real/*.ts` config
(layout + who lives where; login codes are generated at runtime and printed
once — never committed). `--wipe` converts a demo instance in place. A real
instance must run with `DEMO_ACCESS_ENABLED=false` and the reset timer
Expand Down Expand Up @@ -1220,7 +1221,8 @@ two reset scopes).

### Database Model

```prisma
```
// src/lib/db/schema.ts (excerpt, Drizzle; shown here in Prisma-style shorthand)
model User {
id String @id @default(cuid())
code String @unique // AOZ-XXXXXX login code — the identity
Expand Down Expand Up @@ -1266,7 +1268,7 @@ model Account {
| Audit logging | Active | `src/lib/audit.ts` |
| Role switching | Active | UserMenu + PortalNav show cross-links |

**To create initial admin:** Run `npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-admin.ts` (default code: `AOZ-ADMIN1`)
**To create initial admin:** Run `npm run db:seed:admin` (default code: `AOZ-ADMIN1`)

---

Expand Down Expand Up @@ -1334,11 +1336,11 @@ Representative coverage by area (not an exhaustive suite list):
```bash
npm run dev # Development server (port 3001)
npm run build # Production build
npm run prisma:generate # Regenerate Prisma client
npm run prisma:migrate # Run pending migrations (production)
npm run prisma:push # Push schema changes (development only)
npm run prisma:studio # Database browser
npm run prisma:seed # Seed demo data
npm run db:generate # Generate a migration from schema.ts changes
npm run db:migrate # Run pending migrations
npm run db:push # Push schema changes (development only)
npm run db:studio # Database browser
npm run db:seed # Seed demo data
npm run test # Run Jest tests (2558 tests)
npm run test:e2e # Run Playwright tests (173 tests)
```
Expand All @@ -1360,7 +1362,7 @@ npm run test:e2e # Run Playwright tests (173 tests)
| Transfer actions | `src/lib/actions/transfers.ts` |
| Auth guards | `src/lib/auth/index.ts` (`requireStaffAuth()`) |
| Route boundaries | `src/lib/auth/route-boundaries.ts` |
| Prisma schema | `prisma/schema.prisma` |
| Drizzle schema | `src/lib/db/schema.ts` |

---

Expand All @@ -1381,14 +1383,14 @@ npm run test:e2e # Run Playwright tests (173 tests)
## Troubleshooting

### Schema changes workflow
1. Edit `prisma/schema.prisma`
2. Run `npx prisma migrate dev --name describe-change` to create migration
3. Run `npm run prisma:generate` to update client types
1. Edit `src/lib/db/schema.ts`
2. Run `npm run db:generate` to create the migration in `drizzle/`
3. Run `npm run db:migrate` to apply it locally
4. Restart dev server

### "Column not found" errors after schema change
1. Run `npm run prisma:generate`
2. Restart dev server (clears column cache)
1. Run `npm run db:migrate` (the migration may not be applied yet)
2. Restart dev server

### Mobile nav not showing
- Check for `sm:hidden` / `hidden sm:flex` patterns
Expand Down
4 changes: 2 additions & 2 deletions DEMO_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ This demo showcases the intelligent matching system that reduces conflicts and i

### Before Demo:
- [ ] Run `npm run dev` to start server
- [ ] Verify database has demo seed data (`npx ts-node prisma/seed-demo.ts`)
- [ ] Verify database has demo seed data (`npx ts-node scripts/db/seed-demo.ts`)
- [ ] Open http://localhost:3000/matching in browser
- [ ] Have ROI dashboard (http://localhost:3000/analytics/roi) in second tab
- [ ] Test that Ahmed (RES-AH014) appears in resident dropdown
Expand Down Expand Up @@ -270,7 +270,7 @@ If time permits (extra 2 minutes):
**For Questions**:
- Technical: Review codebase at /home/g/dev/aoz-housing
- Business: Review ROI dashboard at /analytics/roi
- Data: Review seed file at prisma/seed-demo.ts
- Data: Review seed file at scripts/db/seed-demo.ts

**Suggested Next Steps**:
1. Pilot with real AOZ data (anonymized)
Expand Down
10 changes: 5 additions & 5 deletions IMPLEMENTATION_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ This document summarizes all implementation work completed for the AOZ Housing i
**File**: `/src/app/matching/page.tsx` (UPDATED)

**What it does**:
- Wraps placement operations in `prisma.$transaction()`
- Wraps placement operations in `db.transaction()`
- Atomic operations: all-or-nothing
- Prevents partial failures leaving database in bad state

Expand Down Expand Up @@ -130,7 +130,7 @@ This document summarizes all implementation work completed for the AOZ Housing i
## 3. Demo Data

### Comprehensive Seed File ✅
**File**: `/prisma/seed-demo.ts` (NEW)
**File**: `/scripts/db/seed-demo.ts` (NEW)

**What it contains**:
- **15 residents**: Including Ahmed (unplaced, demo star) and Maria (transferred 3x)
Expand All @@ -148,7 +148,7 @@ This document summarizes all implementation work completed for the AOZ Housing i

**Real data**: YES - All data exists in database after running seed
**Made up**: NO - Data is realistic but fictional for demonstration
**Run with**: `npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-demo.ts`
**Run with**: `npx ts-node -r tsconfig-paths/register --compiler-options '{"module":"CommonJS","types":["node"]}' scripts/db/seed-demo.ts`

---

Expand Down Expand Up @@ -300,7 +300,7 @@ This document summarizes all implementation work completed for the AOZ Housing i

2. **Seed demo data** (if not already done):
```bash
npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-demo.ts
npx ts-node -r tsconfig-paths/register --compiler-options '{"module":"CommonJS","types":["node"]}' scripts/db/seed-demo.ts
```

3. **Open pages**:
Expand All @@ -322,7 +322,7 @@ This document summarizes all implementation work completed for the AOZ Housing i
4. `/src/lib/analytics/unit-metrics.ts`
5. `/src/app/analytics/roi/page.tsx`
6. `/src/app/analytics/learning/page.tsx`
7. `/prisma/seed-demo.ts`
7. `/scripts/db/seed-demo.ts`
8. `/DEMO_GUIDE.md`

### Modified Files (5):
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ UI — that stays German.

### Config-Driven Design (2-File Changes)

All factor definitions, labels, options, thresholds, and colors live in `src/lib/config/`. Adding a new compatibility factor requires editing at most 2 files: config + Prisma schema. Forms, scoring, display, and validation auto-generate from config.
All factor definitions, labels, options, thresholds, and colors live in `src/lib/config/`. Adding a new compatibility factor requires editing at most 2 files: config + Drizzle schema. Forms, scoring, display, and validation auto-generate from config.

Key config files:
- `resident-factors.ts` (550+ lines) -- 38 resident factors organized by form section (basic, lifestyle, social, practical, household, health, preferences, notes)
Expand Down Expand Up @@ -138,7 +138,7 @@ The system detects when conflicts are likely to emerge and estimates timeframes.
|-------|------------|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict mode) |
| Database | PostgreSQL 17 on Hetzner (`aoz_wohnen`) + Prisma |
| Database | PostgreSQL 17 on Hetzner (`aoz_wohnen`) + Drizzle ORM |
| Styling | Tailwind CSS (mobile-first) |
| Validation | Zod |
| Auth | JWT sessions (bcryptjs + jose) |
Expand All @@ -162,7 +162,7 @@ The system detects when conflicts are likely to emerge and estimates timeframes.
git clone <repo-url> && cd aoz-housing
pnpm install
cp .env.example .env # local Postgres; production is aoz_wohnen on Hetzner — see docs/INFRASTRUCTURE.md
pnpm prisma migrate deploy
pnpm db:migrate
pnpm dev
```

Expand Down Expand Up @@ -225,8 +225,8 @@ src/
audit.ts # Placement audit trail
auth/ # JWT sessions, role policy, rate limiting
components/ # UI components (mobile-first)
prisma/
schema.prisma # Single source of truth for data model
src/lib/db/
schema.ts # Single source of truth for data model (drizzle/ holds its SQL migrations)
tests/
unit/ # 2341 unit tests (135 suites)
e2e/ # 45 Playwright specs (11 files)
Expand Down
12 changes: 6 additions & 6 deletions docs/INFRASTRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ created_date: 2026-08-17
last_modified_date: 2026-08-19
last_modified_summary: Document fleet AI keys (Groq → OpenRouter); staff chat no longer uses Anthropic.

This file exists because a gitignored laptop `.env` still named a decommissioned Neon host, Prisma loaded it, and an agent treated that timeout as "the production database is unreachable". It was never the production database.
This file exists because a gitignored laptop `.env` still named a decommissioned Neon host, the db client loaded it, and an agent treated that timeout as "the production database is unreachable". It was never the production database.

## Production (the only live instance)

Expand Down Expand Up @@ -32,8 +32,8 @@ Neon, Vercel and hosted Supabase were left on 2026-06-12 (see `CHANGELOG.md`). T
Do not:

- Restore those URLs into `.env`
- Treat Prisma's "loaded env from .env" line as proof of where production lives
- Run `prisma migrate` against whatever happens to be in a gitignored file without reading the host
- Treat a client's "loaded env from .env" line as proof of where production lives
- Run migrations against whatever happens to be in a gitignored file without reading the host
- Invent a tunnel and then confuse this laptop's Postgres (`aoz_housing`, old scratch DBs) with `aoz_wohnen` on the box

## How code reaches the box
Expand All @@ -42,7 +42,7 @@ Push to `master` → `.github/workflows/deploy.yml` → reusable
`bitbaum/fleetcrown/.github/workflows/selfhost-deploy.yml`.

That pipeline waits for this commit's CI, pulls `/opt/aoz-wohnen/shared/.env`
from the box (the box stays env SSOT), runs `prisma migrate deploy` against
from the box (the box stays env SSOT), applies pending `drizzle/*.sql` (fleetcrown apply-schema.sh, ledgered in `_deploy_schema_history`) against
`aoz_wohnen` over the deploy tunnel, builds, rsyncs, health-checks.

If CI on `master` is red, deploy is blocked. Auto-merge must set
Expand All @@ -68,10 +68,10 @@ ssh root@167.233.22.31
# then, as the app:
cd /opt/aoz-wohnen/current
# DATABASE_URL is already aoz_wohnen@localhost
npx prisma migrate status
npm run db:migrate # drizzle-kit; no-ops when the journal is current
```

Do not point this laptop's Prisma at Neon. Do not assume `localhost:5432` on the laptop is `aoz_wohnen` — that database lives on the box.
Do not point this laptop's db client at Neon. Do not assume `localhost:5432` on the laptop is `aoz_wohnen` — that database lives on the box.

Local development uses a **local** Postgres and `.env.example` as the template (`aoz_wohnen` as the name so it matches production). Copy credentials from the box only when you are deliberately tunnelling, and rewrite the host/port to the tunnel — never keep a `neon.tech` host "for convenience".

Expand Down
2 changes: 1 addition & 1 deletion docs/ROLE-WALKTHROUGH-2026-08-31.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ rather than leaving the seat quietly empty — an unstaffed domain looks
identical to a staffed one nobody has used yet. The SOCIAL seat is covered by
Franziska's oversight.

Provisioning lives in `prisma/real/aoz-team.ts` (config) +
Provisioning lives in `scripts/db/real/aoz-team.ts` (config) +
`scripts/maintenance/ensure-aoz-team.ts` (idempotent, matches by name, so it
is also how a reach is corrected later). Codes are generated at run time and
printed once — never committed, for the same reason the real apartment's
Expand Down
17 changes: 17 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from 'drizzle-kit'

// Fleet house pattern (see reparaturbonus-zh, vitareba). Migrations in
// ./drizzle are applied two ways:
// - fresh databases (dev, CI service containers): `npm run db:migrate`
// (drizzle-kit's own journal)
// - the live box: fleetcrown's scripts/hetzner/apply-schema.sh on every
// deploy — forward-only, ledgered in public._deploy_schema_history,
// refuses destructive statements
export default defineConfig({
schema: ['./src/lib/db/schema.ts', './src/lib/db/relations.ts'],
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})
Loading
Loading