docs: correct ARCHITECTURE layout/§7, note intentional settings + single-space

Project tree + section 7 module paths now reflect the per-domain
lib/actions/* reality (no monolithic lib/actions.ts); explicit note that
/settings admin secret loading is by-design same-space management;
getAuthMember single-space invariant documented in code. HANDOFF pass-31.
This commit is contained in:
Moheeb Zara
2026-05-16 22:44:21 -07:00
parent d5a71c9448
commit d0aef9a6aa
3 changed files with 48 additions and 15 deletions
+23 -15
View File
@@ -90,15 +90,18 @@ Hackerspace.sh is a comprehensive member management platform designed for hacker
│ │ ├── client.ts # Browser client │ │ ├── client.ts # Browser client
│ │ ├── proxy.ts # Middleware session refresh │ │ ├── proxy.ts # Middleware session refresh
│ │ └── server.ts # Server Component client │ │ └── server.ts # Server Component client
│ ├── actions.ts # All server actions (CRUD) │ ├── actions/ # Server actions, one module per domain
│ ├── auth-actions.ts # Auth-specific actions │ │ ├── index.ts # Barrel (re-exports every domain module)
│ ├── types.ts # TypeScript interfaces │ │ ├── tasks.ts projects.ts members.ts payments.ts …
│ │ └── classes.ts equipment.ts door.ts presence.ts …
│ ├── *-logic.ts # Pure, unit-tested decision logic
│ ├── *-guard.ts # Per-feature server-component guards
│ ├── auth-helpers.ts # requireMember / permission gates
│ └── utils.ts # Utility functions (cn) │ └── utils.ts # Utility functions (cn)
├── scripts/ # SQL migrations ├── scripts/ # SQL
│ ├── 000_reset_schema.sql # Schema reset (dev only) │ ├── schema.sql # Canonical, idempotent (source of truth)
│ ├── 001_create_schema.sql # Initial schema │ ├── 0NN_*.sql # Numbered incremental migrations (→ 038)
│ ├── 002-011_*.sql # Incremental fixes │ └── seed.sql # Optional seed data
│ └── patch-dashboard.mjs # Utility script
├── middleware.ts # Auth middleware ├── middleware.ts # Auth middleware
├── docs/ # This documentation ├── docs/ # This documentation
└── [config files] # next.config, tsconfig, etc. └── [config files] # next.config, tsconfig, etc.
@@ -244,7 +247,7 @@ Admin (full access)
| `createSpace` | spaceName, spaceSlug, spaceCity, displayName | Authenticated | Create new space | | `createSpace` | spaceName, spaceSlug, spaceCity, displayName | Authenticated | Create new space |
| `joinSpace` | inviteCode, displayName | Authenticated | Join existing space | | `joinSpace` | inviteCode, displayName | Authenticated | Join existing space |
### 7.2 Tasks (`lib/actions.ts`) ### 7.2 Tasks (`lib/actions/tasks.ts`)
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
@@ -253,7 +256,7 @@ Admin (full access)
| `completeTask` | taskId | Claimant | | `completeTask` | taskId | Claimant |
| `deleteTask` | taskId | Member | | `deleteTask` | taskId | Member |
### 7.3 Projects (`lib/actions.ts`) ### 7.3 Projects (`lib/actions/projects.ts`)
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
@@ -261,7 +264,7 @@ Admin (full access)
| `updateProjectStatus` | projectId, status | Member | | `updateProjectStatus` | projectId, status | Member |
| `deleteProject` | projectId | Member | | `deleteProject` | projectId | Member |
### 7.4 Members (`lib/actions.ts`) ### 7.4 Members (`lib/actions/members.ts`)
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
@@ -271,7 +274,7 @@ Admin (full access)
| `removeMember` | memberId | Admin | | `removeMember` | memberId | Admin |
| `importMembers` | rows[] | Admin/Board | | `importMembers` | rows[] | Admin/Board |
### 7.5 Payments (`lib/actions.ts`) ### 7.5 Payments (`lib/actions/payments.ts`)
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
@@ -279,7 +282,7 @@ Admin (full access)
| `linkPaymentToMember` | paymentId, memberId | Treasurer/Admin | | `linkPaymentToMember` | paymentId, memberId | Treasurer/Admin |
| `importPaymentsCsv` | rows[] | Treasurer/Admin | | `importPaymentsCsv` | rows[] | Treasurer/Admin |
### 7.6 Knowledge Base & Secrets (`lib/actions.ts`) ### 7.6 Knowledge Base & Secrets (`lib/actions/knowledge-base.ts`, `lib/actions/secrets.ts`)
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
@@ -290,7 +293,7 @@ Admin (full access)
| `deleteSecret` | secretId | Admin | | `deleteSecret` | secretId | Admin |
| `upsertAreaLead` | area_code, area_name, lead_id?, lead_handle?, status? | Admin/Board | | `upsertAreaLead` | area_code, area_name, lead_id?, lead_handle?, status? | Admin/Board |
### 7.7 Contacts (`lib/actions.ts`) ### 7.7 Contacts (`lib/actions/contacts.ts`)
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
@@ -298,7 +301,12 @@ Admin (full access)
| `updateContact` | contactId, updates | Member | | `updateContact` | contactId, updates | Member |
| `deleteContact` | contactId | Member | | `deleteContact` | contactId | Member |
### 7.8 Settings (`lib/actions.ts`) ### 7.8 Settings (`lib/actions/settings.ts`)
> Note: `/settings` is admin-only and intentionally loads the space's own
> `webhook_secret` and integration `config` so an admin can view/rotate
> them. That is by-design same-space admin secret management, not a leak;
> the page redirects non-admins before any of it is fetched.
| Action | Parameters | Role Required | | Action | Parameters | Role Required |
|--------|------------|---------------| |--------|------------|---------------|
+20
View File
@@ -4,6 +4,26 @@ Append-only. Newest entries on top. Keep each entry to one screen.
--- ---
## 2026-05-17 (pass 31) — Audit follow-ups + attendance polish; LOCAL, awaiting deploy
Branch `main`. Continuation of the pass-30 audit. User: handle the deferred items where confident + properly test/doc; "stick to single space for now". 5 commits `2dc0af5..` (this entry).
### Done (each gated + own commit, suite 459, build clean)
- **`2dc0af5` PayPal sync**: `payments` has NO unique index on `external_id`, so the `onConflict:'external_id'` upsert was unreliable and fell back to an insert that stripped `external_id` -> duplicate, non-idempotent rows, not tenant-scoped. Replaced with: look up existing `external_id`s in THIS space, insert only new rows (id preserved). No migration, no schema risk.
- **`1e1b93e` door IPv6 pin**: SSRF pin parser did `pinnedHost.split(':')[0]`, mangling any IPv6 pin (failed closed -> IPv6 controllers unusable). New pure exhaustively-tested `normalizeHost` (scheme/userinfo/port/path strip; `[ipv6]`/`[ipv6]:port`/bare-ipv6 unwrap) applied to both request host and pin. ALL prior behavior preserved (verified: IPv4, host:port, userinfo-spoof reject, trailing-dot reject, metadata always blocked, IPv4-mapped-IPv6 vs IPv4 fails closed). 29 door tests.
- **`d5a71c9` /attendance polish** (user ask): client view with a "Here now" section + inline self check-in / check-in-as-host / check-out + note; history with name search, day picker, grouped-by-day headers, per-visit duration/status/notes, live visits/members summary. New pure tested `dayKey` (tz-stable) + `visitDurationMinutes`.
- **docs (this commit)**: ARCHITECTURE project tree + §7 module paths corrected to `lib/actions/*` reality; explicit note that `/settings` admin secret loading is by-design (not a leak); `getAuthMember` single-space invariant documented in code.
### Decisions / not done (intentional)
- **Single space per user**: kept (user decision). `getAuthMember` `.single()` is intentional and fails closed; documented in code + ARCHITECTURE Known Limitations #7. Not changed.
- **Settings `select('*')`**: NOT narrowed — verified the admin-only client legitimately uses `webhook_secret` + integration `config`; narrowing would be cosmetic and risk breaking the UI. Documented as intentional instead.
- A full ARCHITECTURE §7 prose rewrite remains a nice-to-have (paths now correct; an authoritative top-of-doc note already added pass-30).
### Open
- LOCAL & undeployed: pass-31 (5 commits) + the pass-30 deploy-state docs. **Awaiting deploy approval** (no migration this pass; app + docs only). Default reverted to ASK-before-deploy (the pass-30 deploy-without-asking was one-time). After push: smoke `/attendance` (search/day filter/check-in), PayPal re-sync idempotency, door IPv6 pin if a controller is ever IPv6.
---
## 2026-05-17 (pass 30) — Presence/attendance feature + full app audit pass ## 2026-05-17 (pass 30) — Presence/attendance feature + full app audit pass
Branch `main`. Big session: landing wording fix, a new presence module, a 4-agent read-only audit, and audit fixes. User authorized deploying this work without the per-deploy ask (one-time, for this ready work; default reverts to ASK next session unless reaffirmed). **DEPLOYED**: pushed `4b29280..c12a103`, Actions run `25982428014` success, migration 038 applied. Smoke: `/` `/login` 200; `/dashboard` `/attendance` `/doors` `/classes` `/equipment/manage` 307->login (gated, expected). NOT browser-verified. Branch `main`. Big session: landing wording fix, a new presence module, a 4-agent read-only audit, and audit fixes. User authorized deploying this work without the per-deploy ask (one-time, for this ready work; default reverts to ASK next session unless reaffirmed). **DEPLOYED**: pushed `4b29280..c12a103`, Actions run `25982428014` success, migration 038 applied. Smoke: `/` `/login` 200; `/dashboard` `/attendance` `/doors` `/classes` `/equipment/manage` 307->login (gated, expected). NOT browser-verified.
+5
View File
@@ -43,6 +43,11 @@ export async function getAuthMember(supabase: ServerSupabase): Promise<Member |
} = await supabase.auth.getUser() } = await supabase.auth.getUser()
if (!user) return null if (!user) return null
// INVARIANT: one active membership per user (single-space product model;
// see ARCHITECTURE "Known Limitations"). `.single()` is intentional -- if a
// user somehow has 2+ active memberships it fails closed (no action runs)
// rather than silently picking a space. Multi-space would require an
// explicit active-space selector before this can change.
const { data } = await supabase const { data } = await supabase
.from('space_members') .from('space_members')
.select('id, space_id, user_id, role, display_name, handle') .select('id, space_id, user_id, role, display_name, handle')