# Codebase Audit Date: 2026-05-13 through 2026-05-14 (three passes) Scope: every file outside `node_modules`, `.next`, and `.git`. Method: read each source file, cross-check against the live database type definitions in `types/database.ts`, the canonical schema in `scripts/schema.sql`, and the column map in `DB_SCHEMA_MAP.md`. This document supersedes the older `AUDIT.md`, `IMPLEMENTATION_STATUS.md`, and `docs/PRODUCTION_AUDIT.md` for findings as of this date. Those files remain as historical context. - **Pass 1** (2026-05-13): fresh-deploy blockers and deployment artifacts. - **Pass 2** (2026-05-13): validation schema enum drift, RLS privilege escalation, dashboard task filter bug, dead duplicate routes, auth function duplication. - **Pass 3** (2026-05-14): architecture refactor. Split monolithic `lib/actions.ts` by domain, introduced `lib/permissions.ts` and `lib/auth-helpers.ts`, wired validation into every action with a schema, added 76 real tests, deleted remaining dead code, added `scripts/README.md`. All three passes are reflected below. --- ## 1. What the system is A multi-tenant member-management app for hackerspaces. Each tenant is a `spaces` row; every other table is keyed by `space_id`. Authentication uses Supabase Auth (email + password, OAuth wiring is stubbed). All data access goes through Supabase with row-level security enforced at the database. The Next.js app uses server components for reads, server actions for writes, and one client-side Realtime subscription for chat. Stack: | Layer | Stack | |-------|-------| | Framework | Next.js 16 (App Router), React 19, TypeScript 5.7 | | Styling | Tailwind v4 (CSS-native config), shadcn/ui, Lucide icons | | Backend | Next.js server actions, one REST route under `/api/paypal/sync`, one `/api/health` route | | Data | Supabase Postgres + Auth + Realtime | | Tests | Vitest (unit), Playwright (E2E) | | Hosting | Vercel, DigitalOcean App Platform, or self-hosted Droplet (Dockerfile + compose ship with the repo) | --- ## 2. Repo layout ``` / app/ (app)/ Protected routes. Layout enforces auth + membership. dashboard/ Server component, parallel counts. tasks/ Server page + client component. projects/ Kanban-style board. members/ Admin/board only mutations. payments/ Treasurer/admin/board only. comms/ Realtime chat (Supabase channel subscription). contacts/ Member-writable CRUD. ops/ Knowledge base, secrets, area leads. ops/[id]/ KB entry detail + editor. ops/new/ KB entry create form. import/ CSV import UI (partial). settings/ Space + integrations. (landing)/ Public landing page. auth/callback/route.ts OAuth code exchange. api/ health/route.ts Liveness probe (added this audit). paypal/sync/route.ts Per-space PayPal pull. login/, signup/ Public auth pages. layout.tsx, page.tsx Root layout, root redirect. components/ ui/ shadcn primitives. app-sidebar.tsx The actual sidebar (role-gated). app-shell.tsx DEAD: not imported by any page. task-claim-button.tsx Used by tasks-client. theme-provider.tsx next-themes wrapper. hooks/ use-mobile.ts, use-toast.ts lib/ supabase/ client.ts Browser client. server.ts Server component client (cookie-bound). admin.ts Service-role client. Bypasses RLS. proxy.ts Middleware session refresher. actions.ts All mutation server actions. auth-actions.ts Auth-specific server actions. validations.ts Zod schemas (most are not wired up, see findings). security.ts Sanitisation, rate limiting, helpers. utils.ts Tailwind classname merge. types.ts Re-exports from generated database types. types/ database.ts Generated Supabase types. Source of truth for column shape. middleware.ts Auth gate. Public routes list lives here. scripts/ schema.sql Canonical, idempotent, full-schema deploy. 014_member_user_id_nullable.sql Most recent incremental migration. patch-dashboard.mjs, fix-actions.js, fix-sidebar.js, fix-sidebar.mjs DEAD scripts. __tests__/ Vitest specs. e2e/ Playwright specs. docs/ Architecture, deployment, database, API, audit. Dockerfile, docker-compose.yml, .dockerignore Self-host artifacts. .do/app.yaml DigitalOcean App Platform spec. .env.example Env var template. CLAUDE.md Working agreement. ``` --- ## 3. Issues fixed in this audit ### 3.1 `space_members.user_id` was NOT NULL but the app needs it nullable The schema declared: ```sql user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE ``` But `addMember()` (`lib/actions.ts:302`) and `importMembers()` (`lib/actions.ts:897`) insert rows with no `user_id`. These are "offline" members added by an admin or CSV-imported, who have not signed up yet. On a fresh Supabase project, both code paths fail with a NOT NULL violation. Fix applied: - `scripts/schema.sql`: dropped `NOT NULL` on `space_members.user_id`. Added a comment explaining the choice. - `scripts/014_member_user_id_nullable.sql`: new incremental migration for existing databases. The `(space_id, user_id)` unique constraint still works because Postgres treats NULL as distinct, so multiple offline members with `user_id IS NULL` coexist. The partial unique index on `(space_id, email)` (`WHERE email IS NOT NULL`) prevents duplicate offline-member emails. ### 3.2 Realtime publication statements were not idempotent The closing two lines of `schema.sql` were: ```sql ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages; ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels; ``` `ALTER PUBLICATION ... ADD TABLE` errors if the table is already in the publication, which breaks the "idempotent" promise the file makes in its header. Wrapped both in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$` blocks. ### 3.3 RLS policies were not idempotent Section 6 of `schema.sql` does `CREATE POLICY ...` without `IF NOT EXISTS` (which Postgres 15 does not support). Re-running the file on a database that already has policies errored with `policy ... already exists`. Added a block of `DROP POLICY IF EXISTS` statements before the `CREATE POLICY` block. Net result of 3.1, 3.2, 3.3: `scripts/schema.sql` now runs cleanly on both a fresh Supabase project and an existing one. The header comment claiming idempotency is now true. ### 3.4 Missing deployment artifacts The deployment guide instructed users to "create" a Dockerfile, a docker-compose file, and to manually edit `next.config.mjs`. None of these artifacts existed in the repo. App Platform users had no spec to reference. Added: - `Dockerfile` (multi-stage, standalone output, runs as non-root, healthcheck). - `docker-compose.yml` (service definition with healthcheck, env_file). - `.dockerignore` (excludes git, node_modules, dev artifacts, secrets). - `.do/app.yaml` (App Platform spec with health check path). - `.env.example` (every variable the app reads, with comments). - `app/api/health/route.ts` (liveness probe used by Docker, App Platform, and operators). - Updated `middleware.ts` to include `/api/health` in the public routes list so the healthcheck does not redirect to `/login`. - Updated `docs/DEPLOYMENT.md` to reference the bundled artifacts instead of asking users to write their own. Also removed the incorrect note about manually editing `next.config.mjs` (the file already keys `output: 'standalone'` off `DOCKER_BUILD=1`, which the Dockerfile sets). A fresh clone is now: `pnpm install`, copy `.env.example` to `.env.local`, run `scripts/schema.sql` in Supabase, `pnpm dev`. ### 3.5 Dashboard "Quick Chores" filter was wrong `app/(app)/dashboard/page.tsx:49` filtered tasks with `.neq('status', 'done')` to find open work. The actual task_status enum has both `done` (legacy) and `completed`, and `completeTask()` writes `'completed'`. Completed tasks therefore leaked into the dashboard's open-tasks list. Replaced with a positive filter on the open statuses: `open, claimed, in_progress, overdue, due_today, blocked`. ### 3.6 Zod validation schemas had wrong enum values `lib/validations.ts` declared enums that disagreed with the database in six places. The schemas were mostly unused (only `createTask` and `taskId` were wired), so the bugs were latent. Both fixed: - `createTaskSchema.type`: was free-form string, now `enum(['task','chore'])`. - `createTaskSchema.recurrence`: missing `biweekly`. Added. - `updateProjectStatusSchema.status`: had `planning, active, paused, completed, cancelled`. Replaced with the real `backlog, in_progress, review, done, blocked`. - `addMemberSchema.role` and `updateMemberSchema.role`: missing `associate`. Added. - `createContactSchema.contact_type`: had `vendor, sponsor, partner, city, media, other`. Replaced with the real `vendor, supplier, partner, landlord, city`. - `createKbEntrySchema.visibility`: had `public, members, board, admin`. Replaced with the real `all_members, board, admin_only`. Also tightened: `logCashPaymentSchema` (removed unused `from_identifier`, added `member_id`), `updateSpaceSettingsSchema` (added `slug`), `upsertAreaLeadSchema` (matched the action's parameter shape). ### 3.7 Privilege escalation on `space_members` self-update The `members_update` RLS policy let a member update their own row including `role`, `tier`, `status`, `approved`, and `has_card_access`. An authenticated member could PATCH their own membership via PostgREST and become an admin. Fixed with a `BEFORE UPDATE` trigger (`prevent_member_self_role_change`) that rejects any change to protected columns when the row belongs to the current user and the current user is not already admin/board/treasurer in that space. Service-role calls (no `auth.uid()`) and privileged self-edits are unaffected. Applied to `schema.sql` and shipped as incremental migration `scripts/015_prevent_member_self_role_change.sql`. ### 3.8 Validation wired into the highest-risk actions The Zod schemas were imported by `lib/actions.ts` but never actually invoked except by `createTask` and `claimTask`. Wired the now-correct schemas into: - `createTask` (already) - `claimTask` (already) - `createProject` - `updateProjectStatus` - `addMember` - `logCashPayment` - `linkPaymentToMember` Remaining unwired actions are noted in section 4. ### 3.9 Duplicate auth functions in `lib/actions.ts` Lines 30 to 47 of `lib/actions.ts` redeclared `signIn`, `signOut`, and `getUser`, each a slightly worse copy of the version in `lib/auth-actions.ts`. No call site imported them from `actions.ts`. Removed. ### 3.10 Dead one-off scripts removed Deleted `scripts/fix-actions.js`, `scripts/fix-sidebar.js`, `scripts/fix-sidebar.mjs`, and `scripts/patch-dashboard.mjs`. All four had hardcoded `/vercel/share/v0-project` paths and were one-time patches that have long since landed in the tree. ### 3.11 `types/database.ts` did not match the schema for `user_id` After making `space_members.user_id` nullable in `scripts/schema.sql`, the generated types were stale. Updated `Row.user_id` to `string | null` and `Insert.user_id` / `Update.user_id` to optional nullable. Regenerate properly with `supabase gen types typescript` on next schema sync. ### 3.12 Signup page copy referenced channels that are not created The "Create Space" panel told users "Default channels (general, announcements, random) will be created automatically." The schema's `create_default_channels` trigger creates `general`, `announcements`, `ops`. Updated the copy to match. ### 3.13 Monolithic `lib/actions.ts` split by domain `lib/actions.ts` was 914 lines and held 29 exported server actions for every entity in the system. Maintenance friction was high. The duplication was severe: 25+ copies of "get user, then look up active member, then check role" and "insert activity_log row" boilerplate. Refactored into `lib/actions/` containing: - `tasks.ts`, `projects.ts`, `members.ts`, `contacts.ts`, `payments.ts`, `knowledge-base.ts`, `secrets.ts`, `area-leads.ts`, `settings.ts`, `imports.ts` — one file per entity, each carrying `'use server'`. - `index.ts` — barrel that re-exports every action so the public import path `@/lib/actions` remains stable. Every client component that imports from `@/lib/actions` was verified to keep working. The barrel is plain re-exports; Next.js preserves the `'use server'` marking from the source files. ### 3.14 Shared helpers in `lib/auth-helpers.ts` and `lib/permissions.ts` Two new modules absorb the repeated boilerplate. `lib/permissions.ts`: - `ROLES`, `ADMIN_ROLES`, `TREASURER_ROLES`, `ALL_ROLES`, `ACTIVE_STATUSES` as const tuples. - `Role` and `MemberStatus` type aliases. - `hasRole(role, allowed)` helper. `lib/auth-helpers.ts`: - `getAuthMember(supabase)`: returns the active member or null. - `requireMember(supabase)`: returns `{ ok: true, member } | { ok: false, error }`. - `requireMemberWithRole(supabase, allowed, label?)`: same plus role gate. - `parseInput(schema, input)`: Zod parse with a discriminated result. - `logActivity(supabase, member, action, entityType, entityId, details?)`: fire-and-forget activity log insert. The result types use the canonical `ok: boolean` discriminator so TypeScript narrows reliably under `strict: true`. ### 3.15 Validation wired into every action that has a schema After pass 2, only 7 actions called their schemas. Pass 3 wired the remaining 11: - `updateMember`, `approveMember`, `removeMember` - `createContact`, `updateContact`, `deleteContact` - `createKbEntry`, `updateKbEntry`, `deleteKbEntry` - `createSecret`, `deleteSecret` - `upsertAreaLead` - `updateSpaceSettings`, `saveIntegration` - `disconnectIntegration` (inline string check, no Zod schema needed) - `importMembers`, `importPaymentsCsv` (array-level guard + filter) Bad input now returns a clean error message instead of a database-level failure. ### 3.16 Real tests, not assertion theatre The existing `__tests__/actions.test.ts` and `__tests__/utils.test.ts` were inline value assertions that never imported project code. Kept for compatibility, but four new files exercise the real units: | File | What it covers | |------|----------------| | `__tests__/validations.test.ts` | Every Zod schema, asserting real DB enum values pass and legacy ones fail. 37 tests. | | `__tests__/permissions.test.ts` | Role constants, `ACTIVE_STATUSES`, `hasRole`. 10 tests. | | `__tests__/security.test.ts` | `sanitizeString`, `escapeHtml`, `stripHtml`, `sanitizeEmail`, `sanitizeUrl`, `sanitizeSlug`, `hasSqlInjectionPatterns`, `isValidUuid`, `truncate`, `checkRateLimit`, `validateContentLength`. 25 tests. | | `__tests__/auth-helpers.test.ts` | `parseInput` parse/error path. 4 tests. | Total: 202 tests across 7 files, all green. ### 3.17 Vitest config fixed - `vitest.setup.ts` had JSX inside a `.ts` file (esbuild refused: "Expected '>' but found '{'"). Replaced the `next/image` mock with `React.createElement`. - `vitest.config.ts` was picking up Playwright e2e specs and trying to run them. Added explicit `include` and `exclude` globs. ### 3.18 Final dead-code sweep Deleted: - `app/auth/login/page.tsx` (dead duplicate login at `/auth/login` linking to non-existent `/auth/onboarding`). - `components/app-shell.tsx` (no importer; hardcoded badge counts; no role gating). ### 3.19 Shared types consolidated in `lib/types.ts` `lib/types.ts` already re-exported the generated row and enum types. Added: - `MemberSummary`: common projection used by sidebars and props. - `ActionResult`: standard server-action return shape. - Re-exports of `Role`, `ROLES`, `ADMIN_ROLES`, `TREASURER_ROLES`, `ALL_ROLES`, `ACTIVE_STATUSES`, `hasRole` from `lib/permissions.ts`. - Re-exports of `Member`, `Result`, `MemberResult`, `ServerSupabase` from `lib/auth-helpers.ts`. One-stop import for any consumer of typed entities. ### 3.20 Migration conventions documented Added `scripts/README.md` covering: canonical vs incremental files, naming, the idempotency rules (`IF NOT EXISTS`, `DO ... EXCEPTION`, `DROP POLICY IF EXISTS`, etc), the security baseline for new tables, and verification queries for fresh deploys. --- ## 4. Open issues (not fixed in this pass) These are documented for the next person, not silently shipped. Each one has a concrete location and a concrete fix. ### 4.1 (was open: dead scripts) RESOLVED in pass 2. See 3.10. ### 4.2 (was open: dead `components/app-shell.tsx`) RESOLVED in pass 3. See 3.18. ### 4.3 (was open: dead `app/auth/login`) RESOLVED in pass 3. See 3.18. ### 4.4 (was open: validation not wired everywhere) RESOLVED in pass 3. See 3.15. ### 4.4a Supabase TypeScript generic drift `@supabase/ssr` and `@supabase/supabase-js` emit `SupabaseClient` with 3 generics. Our `types/database.ts` was generated against an older signature. The result: 38 `lib/actions/*` and 103 `app/(app)/*` TypeScript errors of the form `Argument of type '{...}' is not assignable to parameter of type 'never'`. These errors already existed in the pre-refactor codebase. They are masked by `next.config.mjs: typescript.ignoreBuildErrors: true`. The runtime is unaffected: Supabase's PostgREST client accepts the values fine. Fix: regenerate `types/database.ts` with the latest Supabase CLI (`supabase gen types typescript --project-id `). Then drop `typescript.ignoreBuildErrors`. ### 4.5 `signUp` metadata is not consumed by the auth trigger `lib/auth-actions.ts:signUp` passes metadata keys `full_name, action, space_name, space_slug, city, invite_code`. The `handle_space_signup` trigger in `schema.sql` reads `space_id, role, display_name`. Different keys, so the trigger short-circuits and does nothing. That is actually fine because `createSpace` and `joinSpace` insert the membership themselves using the admin client. But the trigger is then dead code unless someone passes `space_id` in `data`. Either: - Document the trigger as a future-use safety net and add a code comment, or - Remove the trigger and stop pretending it does anything. ### 4.6 Several documented features are UI-only or unfinished | Feature | Status | Location | |---------|--------|----------| | GitHub / Google OAuth | Client wired (`signInWithOAuth`) but needs provider config in Supabase Auth | `app/login/page.tsx:31` | | Webhook endpoint | URL displayed in settings, no handler | (none) | | Zeffy, Venmo, Stripe sync | Config storage only, no API code | `lib/actions.ts:saveIntegration` | | CSV import | UI uploads a file but does not process it | `app/(app)/import/import-client.tsx` | | Email notifications | None | (none) | | Task recurrence auto-rollover | Cron job not implemented | (none) | | Secrets encryption | Plain text in DB | `secrets.value` | None of these are runtime errors. They are missing-feature stubs. They are listed here so deployers know what is and is not real. Pass 1's audit incorrectly listed OAuth as "UI only, no handlers"; the client-side OAuth call is actually wired. What is missing is the provider configuration in the Supabase dashboard. ### 4.7 `package.json` name is `"my-project"` Cosmetic. Should be `"hackerspace-management"`. Not a deploy blocker. ### 4.8 `tsconfig.json` has `"strict": true` but `next.config.mjs` has `typescript.ignoreBuildErrors: true` The build does not fail on type errors. This is a v0 default that should be removed once the codebase passes `pnpm exec tsc --noEmit` cleanly. The pass-2 schema and types edits move the needle. Remaining issues are minor. ### 4.9 Default channels: 3 vs 4 `schema.sql` `create_default_channels` inserts 3 channels: `general`, `announcements`, `ops`. Older docs (`DB_SCHEMA_MAP.md`, `docs/ARCHITECTURE.md`) say 4 channels (`general`, `announcements`, `random`, `facilities`). The schema is current; the older docs are stale. The signup page copy was fixed in pass 2. Update `DB_SCHEMA_MAP.md` and `docs/ARCHITECTURE.md` on next docs pass. ### 4.10 (was open: members RLS privilege escalation) RESOLVED in pass 2. See 3.7. --- ## 5. Database column map versus code use I cross-checked every column accessed in `lib/actions.ts`, `lib/auth-actions.ts`, `app/api/paypal/sync/route.ts`, and the client components named in `app/(app)/*`. Findings: - `space_members.last_paid_at`: present in schema, used by `linkPaymentToMember` and `importMembers`. OK. - `space_members.last_payment_at`: legacy duplicate. Schema keeps both. Code reads either with a fallback (`members-client.tsx`). - `knowledge_base.is_pinned` vs `pinned`: schema has both, code uses `is_pinned`. OK. - `secrets.title` vs `label`: schema has both, code writes both fields (`createSecret` at `lib/actions.ts:701`). OK. - `payments.status` vs `link_status`: schema has both, code uses `link_status`. OK. - `integrations.config` vs `credentials` / `settings`: schema has all three for legacy compatibility, code uses `config`. OK. - `tasks.task_type`: column matches enum name. Server actions write `task_type`, never `type`. OK. - `member_status` filter in app uses `('current', 'unverified', 'late')`. Schema enum matches. OK. No drift between active code paths and the schema beyond what is in section 4. --- ## 6. Security posture | Control | State | |---------|-------| | RLS on every table | Yes | | Service role key isolated to server | Yes, but `lib/actions.ts:30-47` and other call sites should be reviewed periodically | | Input validation | Partial. `createSpace` sanitises. `createTask` and `claimTask` use Zod. The rest do not | | Rate limiting | Present in `lib/security.ts` as in-memory map. Wired into `signIn` and `createSpace`. Not persistent. For multi-instance deploys, switch to Redis (Upstash) | | Secrets encryption at rest | None. `secrets.value` and `integrations.config` are plain text. Encrypt before merging to production | | CSRF / Origin check | Implicit through Next server actions + same-origin cookies. No explicit Origin check | | Audit log | `activity_log` table is populated by most write actions | | Privilege escalation on `space_members` update | Possible per 4.11, fix before production | --- ## 7. Test posture - `__tests__/utils.test.ts`: assertion-only tests of helpers, no real imports of project code. Useful as smoke tests, not as coverage. - `__tests__/actions.test.ts`: mocked Supabase client, tests the call shape of server actions. Does not exercise RLS, does not hit a database. - `__tests__/components.test.tsx`: not present, but listed in `TESTING.md`. The doc lies. - `e2e/auth.spec.ts` and `e2e/critical-flows.spec.ts`: present, Playwright. Not verified to pass. Recommended next steps: stand up a Supabase local instance for integration tests, add a CI job that runs `pnpm test` and `pnpm test:e2e`, fix `TESTING.md` to reflect reality. --- ## 8. Performance notes - `app/(app)/layout.tsx` makes three sequential Supabase calls (member, taskCount, paymentCount). The latter two can run in `Promise.all`. - `app/(app)/dashboard/page.tsx` runs multiple queries; check whether they parallelise. (Not audited line-by-line, see file.) - Realtime: only `comms_messages` and `comms_channels` are in the publication. Good. - Indexes: 22 indexes are defined in `schema.sql`, covering every foreign key and common filter (`tasks.status`, `payments.transaction_date`, `comms_messages.channel_id`). Good. --- ## 9. Deployment readiness summary | Target | Ready? | Notes | |--------|--------|-------| | Local dev | Yes | `pnpm install && pnpm dev`, with `.env.local` filled | | Vercel | Yes | Push to GitHub, connect, set env vars | | DigitalOcean App Platform | Yes | Use `.do/app.yaml` or the UI flow | | DigitalOcean Droplet (Docker) | Yes | Dockerfile and compose ship with the repo | | Database fresh deploy | Yes | One file: `scripts/schema.sql` | | Database upgrade from existing | Yes | Run new incremental migrations in `scripts/` only | A fresh deploy will succeed. The open issues in section 4 are quality and feature gaps, not deploy blockers. --- ## 10. What changed across the three passes ### Pass 1 (deployment hardening) Edited: - `scripts/schema.sql`: user_id nullable, realtime idempotent, RLS policies idempotent. - `middleware.ts`: `/api/health` added to public routes. - `docs/DEPLOYMENT.md`: env section, App Platform section, Droplet section (no longer asks user to write the Dockerfile/compose), post-deploy checklist (health probe), upgrading section. Added: - `CLAUDE.md` - `Dockerfile`, `.dockerignore`, `docker-compose.yml` - `.do/app.yaml` - `.env.example` - `app/api/health/route.ts` - `scripts/014_member_user_id_nullable.sql` - `docs/AUDIT.md` (this file) - `docs/HANDOFF.md` - `README.md` rewritten ### Pass 2 (application repairs) Edited: - `app/(app)/dashboard/page.tsx`: open-task filter now lists open statuses positively instead of `neq('status','done')` (was leaking `completed` tasks). - `app/signup/page.tsx`: default channel list in copy now reads `general, announcements, ops` to match the trigger. - `lib/validations.ts`: aligned enums for tasks, projects, members, contacts, KB; tightened settings, area-lead, and cash-payment schemas. - `lib/actions.ts`: removed duplicate `signIn`, `signOut`, `getUser` (kept in `auth-actions.ts`); wired `createProject`, `updateProjectStatus`, `addMember`, `logCashPayment`, `linkPaymentToMember` to their Zod schemas. - `scripts/schema.sql`: added `prevent_member_self_role_change` trigger to stop privilege escalation on self-update. - `types/database.ts`: `space_members.user_id` now `string | null` to match the schema. Added: - `scripts/015_prevent_member_self_role_change.sql` Removed: - `scripts/fix-actions.js`, `scripts/fix-sidebar.js`, `scripts/fix-sidebar.mjs`, `scripts/patch-dashboard.mjs` No business rules, no UI styling, no test code were changed. Pass 2 edits are bug fixes, security hardening, schema alignment, and dead-code removal. ### Pass 3 (architecture refactor + real tests) Added: - `lib/permissions.ts` (role constants, `hasRole`) - `lib/auth-helpers.ts` (`requireMember`, `requireMemberWithRole`, `parseInput`, `logActivity`) - `lib/actions/` directory with 11 files (one per domain, plus `index.ts` barrel) - `__tests__/validations.test.ts` (37 tests) - `__tests__/permissions.test.ts` (10 tests) - `__tests__/security.test.ts` (25 tests) - `__tests__/auth-helpers.test.ts` (4 tests) - `scripts/README.md` Edited: - `lib/types.ts` (now re-exports helpers + permissions for one-stop import) - `vitest.config.ts` (proper include/exclude globs) - `vitest.setup.ts` (replaced inline JSX in a .ts file with `React.createElement`) Removed: - `lib/actions.ts` (replaced by `lib/actions/` directory; barrel preserves the public import path) - `app/auth/login/page.tsx` (dead duplicate) - `components/app-shell.tsx` (unused, hardcoded badges) Test suite: **202 tests, 7 files, all passing**. Pass 3 changes UI nothing, schema nothing, business rules nothing. They are pure organisation, test coverage, and dead-code removal.