From 4c9b55147bd5cab58ea0502019e67e8f1f8139dc Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Fri, 15 May 2026 19:01:52 -0700 Subject: [PATCH] chore: clear audit backlog (types sync, email norm, docs, members shortcut) Finishes all remaining tracked work from the full-app audit. - types/database.ts synced: added the 8 missing tables (forum_threads, comments, space_tiers, space_invites, space_onboarding_steps, space_role_labels, space_custom_roles, space_member_custom_roles), the comment_entity_type enum (type + Constants), and the missing columns (space_members.tier_id/onboarding_completed_at/ onboarding_progress, secrets.encrypted_value/encryption_version, knowledge_base.render_markdown). - Email canonicalization: new emailField() helper (trim+lowercase) applied to signIn/signUp/addMember/updateMember/createContact and the import path; prevents case-variant duplicates and sign-in mismatches. - Members directory: per-row "make area lead" picker (admin/board) assigns a member to any area-lead role; mirrors Customize. - Docs refreshed and re-dated: DATABASE_SCHEMA.md (migrations 014-024 summary), DB_SCHEMA_MAP.md (new-table quick map), API_REFERENCE.md (all server actions from migrations 016-024). schema.sql named as source of truth. Backlog is now empty. 271/271 tests pass, build clean. No migration. --- CHANGELOG.md | 5 ++ DB_SCHEMA_MAP.md | 28 +++++++++++- app/(app)/members/members-client.tsx | 32 ++++++++++++- app/(app)/members/page.tsx | 12 +++++ docs/API_REFERENCE.md | 38 +++++++++++++++- docs/DATABASE_SCHEMA.md | 40 ++++++++++++++-- docs/HANDOFF.md | 12 +++-- lib/validations.ts | 17 ++++--- types/database.ts | 68 ++++++++++++++++++++++++++++ 9 files changed, 232 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b35c6..fd5ceef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ All notable changes to this project are documented here. Format follows [Keep a - Customize -> Area leads: create area-lead roles, see Vacant until assigned, assign/unassign a member from a picker. The assigned member effectively holds that role, so any Ops item whose access list includes it opens to them. - SECURITY DEFINER helpers `user_effective_roles` and `user_has_permission`. +### Changed +- All emails are canonicalized to trimmed lowercase (sign-in, sign-up, add/update member, contacts, imports) so case/whitespace variants no longer create duplicate members or fail sign-in. +- `types/database.ts` brought in sync with the schema (8 tables, the `comment_entity_type` enum, and the new columns); docs (`DATABASE_SCHEMA.md`, `DB_SCHEMA_MAP.md`, `API_REFERENCE.md`) refreshed for migrations 016-024. +- The member directory has a per-row "make area lead" assignment for admin/board (mirrors Customize -> Area leads). + ### Security - Closed two member self-escalation paths: a non-privileged member can no longer change their own `tier_id` or `onboarding_completed_at` on the `space_members` row (migration 024 extends the self-change trigger). Onboarding completion now goes through the service client after server-side validation. - Added in-code space scoping to forum/comment/channel mutations (defence-in-depth on top of RLS). diff --git a/DB_SCHEMA_MAP.md b/DB_SCHEMA_MAP.md index 6512409..85fe98c 100644 --- a/DB_SCHEMA_MAP.md +++ b/DB_SCHEMA_MAP.md @@ -1,8 +1,8 @@ # DB Schema Map — Quick Reference -> **Last Updated**: 2026-03-10 +> **Last Updated**: 2026-05-15 > **Full Documentation**: See [docs/DATABASE_SCHEMA.md](./docs/DATABASE_SCHEMA.md) for complete reference -> **Source of Truth**: Live database schema +> **Source of Truth**: `scripts/schema.sql` (canonical, idempotent) --- @@ -390,3 +390,27 @@ Default channels created by trigger on space INSERT: `general`, `announcements`, | 011 | Renamed tasks `type` column to `task_type` (canonical enum column name) | | 012 | Canonical sync — task_type enum, cleaned duplicate columns | | 013 | Added `space_members.last_paid_at` timestamptz — required by importMembers and linkPaymentToMember | + +--- + +## Tables added by migrations 016-024 (quick map) + +| Table | Key columns | Purpose | +|-------|-------------|---------| +| `proposals` / `proposal_votes` | space_id, status, quorum_*, voting_*; member_id, position | Async governance voting | +| `incidents` / `incident_updates` | space_id, status, sla_*; visibility | Incident reports + log | +| `policies` | space_id, slug, version, status, body_* | Versioned policy library | +| `space_areas` | space_id, code, name, sort_order, is_archived | Per-space configurable areas | +| `forum_threads` | space_id, author_id, title, category, pinned, locked, comment_count | Forum | +| `comments` | space_id, entity_type, entity_id, author_id, parent_id, body | Polymorphic comments (forum/proposal/incident/policy) | +| `space_tiers` | space_id, slug, name, monthly_price_cents, billing_cadence, is_system | Custom membership tiers (`space_members.tier_id` FK) | +| `space_role_labels` | space_id, role, display_name, color | Rename/recolor built-in roles | +| `space_custom_roles` / `space_member_custom_roles` | space_id, slug, name, color / member_id, custom_role_id | Custom non-privileged roles + assignment | +| `space_invites` | space_id, code, label, expires_at, max_uses, uses_count, is_enabled | Multi-code invites | +| `space_onboarding_steps` | space_id, step_key, step_type, title, body, config, is_enabled, is_required | Configurable member onboarding | +| `space_role_permissions` | space_id, subject, permission | Role/custom-role permission grants | +| `ops_acl` | space_id, entity_type, entity_id, role | Per-item Ops access list (secret/kb/process/area_lead) | + +Column additions: `space_members.tier_id`, `onboarding_completed_at`, `onboarding_progress`; `secrets.encrypted_value`, `encryption_version`; `knowledge_base.render_markdown`, `is_meeting_minutes`, `meeting_date`. New enum `comment_entity_type`. + +Helper functions: `user_effective_roles(uid,sid)`, `user_has_permission(uid,sid,perm)` (both SECURITY DEFINER, search_path=public), plus governance quorum/tally/SLA functions. diff --git a/app/(app)/members/members-client.tsx b/app/(app)/members/members-client.tsx index 4f81385..fcc7d2c 100644 --- a/app/(app)/members/members-client.tsx +++ b/app/(app)/members/members-client.tsx @@ -2,14 +2,18 @@ import { useState } from 'react' import { Plus, X, ChevronDown } from 'lucide-react' -import { addMember, updateMember, approveMember, removeMember } from '@/lib/actions' +import { addMember, updateMember, approveMember, removeMember, assignAreaLead } from '@/lib/actions' +import { toast } from 'sonner' +import { useRouter } from 'next/navigation' import type { Tables } from '@/types/database' type Member = Tables<'space_members'> +type AreaLeadRole = { id: string; area_name: string; lead_id: string | null } interface Props { members: Member[] currentRole: string + areaLeadRoles?: AreaLeadRole[] } const TIER_COLORS: Record = { @@ -21,8 +25,17 @@ const TIER_COLORS: Record = { const isAdmin = (role: string) => role === 'admin' || role === 'board' -export function MembersClient({ members: initialMembers, currentRole }: Props) { +export function MembersClient({ members: initialMembers, currentRole, areaLeadRoles = [] }: Props) { + const router = useRouter() const [members, setMembers] = useState(initialMembers) + + async function makeAreaLead(memberId: string, areaLeadRoleId: string) { + if (!areaLeadRoleId) return + const result = await assignAreaLead({ area_lead_role_id: areaLeadRoleId, member_id: memberId }) + if (result.error) { toast.error(result.error); return } + toast.success('Assigned as area lead') + router.refresh() + } const [activeTab, setActiveTab] = useState<'all' | 'payment_issues' | 'unverified' | 'inactive'>('all') const [search, setSearch] = useState('') const [tierFilter, setTierFilter] = useState('') @@ -326,6 +339,21 @@ export function MembersClient({ members: initialMembers, currentRole }: Props) { > REMOVE + {areaLeadRoles.length > 0 && ( + + )} )} diff --git a/app/(app)/members/page.tsx b/app/(app)/members/page.tsx index 4a03642..75484f9 100644 --- a/app/(app)/members/page.tsx +++ b/app/(app)/members/page.tsx @@ -57,10 +57,22 @@ export default async function MembersPage() { .eq('space_id', self.space_id) .order('joined_at') + // Area-lead roles, so admin/board can assign a lead straight from a member + // row (the same capability also lives in Customize -> Area leads). + const canManageLeads = self.role === 'admin' || self.role === 'board' + const { data: areaLeadRoles } = canManageLeads + ? await supabase + .from('area_leads') + .select('id, area_name, lead_id') + .eq('space_id', self.space_id) + .order('area_name') + : { data: [] } + return ( } /> ) } diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 5d93969..7ad3655 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -1,7 +1,8 @@ # Hackerspace.sh - API Reference -> **Last Updated**: 2026-03-10 +> **Last Updated**: 2026-05-15 > Complete reference for all server actions, database operations, and API patterns. +> Actions added since the original baseline are listed in "Server actions added (migrations 016-024)" at the end. --- @@ -851,3 +852,38 @@ async function handleSubmit() { | "Treasurer access required" | Payment role check | Show permission error | | "Invalid invite code" | Code not found | Ask user to verify | | "A space with this URL slug already exists" | Duplicate slug | Suggest different slug | + +--- + +## Server actions added (migrations 016-024) + +All are `'use server'` in `lib/actions/*`, re-exported from `lib/actions/index.ts`, +validate input with Zod via `parseInput`, authorize via `requireMember` / +`requireMemberWithRole`, scope writes by `member.space_id`, and `revalidatePath`. + +### Governance — `proposals.ts`, `incidents.ts`, `policies.ts` +`createProposal`, `openProposal`, `castVote`, `decideProposal`, `withdrawProposal`, `deleteProposal`; `fileIncident`, `updateIncidentStatus`, `addIncidentUpdate`, `appealIncident`; `createPolicy`, `supersedePolicy`, `updatePolicyStatus`. + +### Areas — `areas.ts` +`createArea`, `updateArea`, `deleteArea` (admin/board; delete admin). + +### Forum + comments — `forum.ts` +`createForumThread`, `updateForumThread` (pin/lock = admin/board), `deleteForumThread`, `addComment`, `editComment`, `deleteComment`. All space-scoped in code. + +### Chat channels — `comms.ts` +`createChannel` (any member), `renameChannel`, `deleteChannel` (creator or admin/board; default channels protected). + +### Tiers / roles / invites — `tiers.ts`, `roles.ts`, `invites.ts` +`createTier`/`updateTier`/`deleteTier`; `upsertRoleLabel`, `createCustomRole`/`updateCustomRole`/`deleteCustomRole`, `assignCustomRole`/`unassignCustomRole`; `createInvite`/`updateInvite`/`deleteInvite`. + +### Onboarding — `onboarding.ts` +`createOnboardingStep`/`updateOnboardingStep`/`deleteOnboardingStep` (admin/board); `markOnboardingStepDone`, `finishOnboarding`, `skipOnboarding` (member; completion writes via the service client after the server-side required-steps check, so the self-change trigger cannot be bypassed). + +### Permissions / Ops ACLs / area-lead — `permissions.ts` +`setRolePermissions(subject, permissions[])`, `setOpsAcl(entity_type, entity_id, roles[])` (admin/board), `createAreaLeadRole`, `assignAreaLead`, `unassignAreaLead`, `deleteAreaLeadRole`. ACLs are additive: empty list falls back to the item's default visibility. + +### Secrets — `secrets.ts` +`createSecret`, `updateSecret`, `revealSecret` (server-only plaintext path, audit-logged, errors on corrupt ciphertext), `deleteSecret`. The list query never selects `value`/`encrypted_value`. + +### Bulk import — `imports.ts`, `payments.ts` +`importMembers`, `importPaymentsCsv`: per-row Zod validation, lowercased emails, `flexibleDateTime` date normalization, enum-checked platform/tier, positive finite amounts; invalid rows skipped and returned as a `skipped` count. diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index cdb6bdd..37c5a23 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -1,9 +1,12 @@ # Hackerspace.sh - Database Schema Reference -> **Last Updated**: 2026-03-10 -> **Database**: PostgreSQL via Supabase -> **Tables**: 13 -> **Source of Truth**: Live database schema +> **Last Updated**: 2026-05-15 +> **Database**: PostgreSQL via self-hosted Supabase +> **Tables**: ~30 +> **Source of Truth**: `scripts/schema.sql` (canonical, idempotent); numbered +> migrations `scripts/0NN_*.sql` upgrade existing deployments. The 13-table +> reference below is the original baseline; tables added by migrations +> 014-024 are summarized in the "Migrations 014-024" section at the end. --- @@ -680,3 +683,32 @@ CREATE INDEX idx_activity_space ON activity_log(space_id, created_at); | `009_fix_member_insert_rls.sql` | 1.8 | Fixed member INSERT RLS for admin/board | | `010_fix_channel_trigger.sql` | 1.9 | Removed non-existent description column from trigger | | `011_fix_member_status_enum.sql` | 2.0 | Fixed enum values (active→current, pending→unverified) | + +--- + +## Migrations 014-024 (additions since the 13-table baseline) + +`scripts/schema.sql` is the canonical idempotent schema; each numbered +migration is mirrored as a section in it. Tables/columns added: + +| Mig | Adds | +|-----|------| +| 014 | `space_members.user_id` made nullable (offline/imported members) | +| 015 | `prevent_member_self_role_change()` trigger (self-escalation guard) | +| 016 | Governance kernel: `proposals`, `proposal_votes`, `incidents`, `incident_updates`, `policies` + quorum/tally/SLA functions | +| 017 | Governance RLS hardening (cross-tenant edge cases) | +| 018 | `space_members.skills/interests/willing_to/affiliations/coi_last_disclosed_at`; `knowledge_base.is_meeting_minutes/meeting_date`; financial/member-directory visibility settings | +| 019 | Auto-expire open proposals past their voting window | +| 020 | `space_areas` (per-space configurable areas) + seeded defaults | +| 021 | `forum_threads`, `comments` (polymorphic: forum_thread/proposal/incident/policy), `space_tiers` + `space_members.tier_id`, `space_role_labels`, `space_custom_roles`, `space_member_custom_roles`, `space_invites`; `secrets.encrypted_value/encryption_version`; `knowledge_base.render_markdown`; comms_channels member-creatable | +| 022 | `space_onboarding_steps`; `space_members.onboarding_completed_at/onboarding_progress`; seed default steps | +| 023 | `space_role_permissions`, `ops_acl`; `user_effective_roles()`, `user_has_permission()` (SECURITY DEFINER); `secrets`/`knowledge_base` SELECT rewritten as `existing-rule OR ops_acl-match` (additive) | +| 024 | `prevent_member_self_role_change()` extended to also block self-change of `tier_id` and `onboarding_completed_at` | + +New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`. + +Every new table has RLS enabled with SELECT/INSERT/UPDATE/DELETE policies +(writes admin/board, deletes admin, reads space members) and, where it has +`updated_at`, a `touch_updated_at` trigger. The ACL model is additive: +with zero `ops_acl`/`space_role_permissions` rows the app behaves exactly +as the role-based baseline. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 4653aa2..dd7325e 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -28,12 +28,14 @@ Five parallel read-only audits (security/RLS, server actions/validation, routes/ - **KB/process per-item ACL wired.** `OpsAclEditor` renders in the KB/process viewer modal (admin/board); entity type derived from the `process` tag. Per-item Ops ACLs now cover secrets + KB + processes. - `/ops` and `/import` denial paths left as-is: unreachable behind the `(app)` layout gate, or already a clear denial with no data leak. -### Tracked backlog (not security-critical, zero runtime impact) +### Pass 16c — backlog cleared (same day) -- `types/database.ts` still missing 8 prior-pass tables + `comment_entity_type` enum + 3 `space_members` columns (loose casts; build green via `ignoreBuildErrors`). -- `docs/DATABASE_SCHEMA.md`, `DB_SCHEMA_MAP.md`, `docs/API_REFERENCE.md` stale since 2026-03-10 (missing migrations 016-024). -- Email `.toLowerCase()` still missing on the non-import auth/member schemas (import path now normalizes). -- Members-row "Make area lead" shortcut (Customize -> Area leads fully covers assignment). +- **`types/database.ts` synced.** Added the 8 missing tables (forum_threads, comments, space_tiers, space_invites, space_onboarding_steps, space_role_labels, space_custom_roles, space_member_custom_roles), the `comment_entity_type` enum (both type union and Constants array), and the missing columns: `space_members.tier_id/onboarding_completed_at/onboarding_progress`, `secrets.encrypted_value/encryption_version`, `knowledge_base.render_markdown`. +- **Email normalized everywhere.** New `emailField()` helper (trim + lowercase) applied to signIn/signUp/addMember/updateMember/createContact and the import path. Prevents case-variant duplicate members/contacts. +- **Members-row "Make area lead".** A per-row picker in the members directory (admin/board) assigns a member to any area-lead role via `assignAreaLead`; same capability also in Customize -> Area leads. +- **Docs refreshed.** `DATABASE_SCHEMA.md` (header + migrations 014-024 summary), `DB_SCHEMA_MAP.md` (table quick-map + helpers), `docs/API_REFERENCE.md` (all server actions added in migrations 016-024). All re-dated 2026-05-15 with `scripts/schema.sql` named as source of truth. + +Backlog is now empty. Remaining known non-issues: `/ops` + `/import` `return null` are unreachable behind the `(app)` layout gate; `next.config.mjs` keeps `ignoreBuildErrors` (pre-existing legacy app/ TS noise) — the new code is type-clean. ### Verification diff --git a/lib/validations.ts b/lib/validations.ts index 6260ba4..d7b0d25 100644 --- a/lib/validations.ts +++ b/lib/validations.ts @@ -33,13 +33,18 @@ export const flexibleDateTime = () => // ─── Auth ──────────────────────────────────────────────────────────────────── +// Canonicalize every email to trimmed lowercase so case/whitespace variants +// cannot create duplicate members/contacts or fail sign-in. +export const emailField = (msg = 'Invalid email address') => + z.string().max(200).email(msg).transform(s => s.trim().toLowerCase()) + export const signInSchema = z.object({ - email: z.string().email('Invalid email address'), + email: emailField(), password: z.string().min(1, 'Password is required'), }) export const signUpSchema = z.object({ - email: z.string().email('Invalid email address'), + email: emailField(), password: z.string().min(8, 'Password must be at least 8 characters'), fullName: z.string().min(1, 'Full name is required').max(100, 'Name is too long'), action: z.enum(['create', 'join']), @@ -92,7 +97,7 @@ export const updateProjectStatusSchema = z.object({ // ─── Members ───────────────────────────────────────────────────────────────── export const addMemberSchema = z.object({ - email: z.string().email('Invalid email address'), + email: emailField(), display_name: z.string().min(1, 'Display name is required').max(100), phone: z.string().max(20).optional().nullable(), handle: z.string().max(50).optional().nullable(), @@ -105,7 +110,7 @@ export const addMemberSchema = z.object({ export const updateMemberSchema = z.object({ memberId: z.string().uuid('Invalid member ID'), display_name: z.string().min(1).max(100).optional(), - email: z.string().email().optional(), + email: emailField().optional(), phone: z.string().max(20).optional().nullable(), handle: z.string().max(50).optional().nullable(), role: z.enum(['admin', 'board', 'treasurer', 'member', 'associate']).optional(), @@ -121,7 +126,7 @@ export const updateMemberSchema = z.object({ export const createContactSchema = z.object({ name: z.string().min(1, 'Name is required').max(200), contact_type: z.enum(['vendor', 'supplier', 'partner', 'landlord', 'city']), - email: z.string().email().optional().nullable(), + email: emailField().optional().nullable(), phone: z.string().max(20).optional().nullable(), details: z.string().max(500).optional().nullable(), note: z.string().max(2000).optional().nullable(), @@ -524,7 +529,7 @@ export const assignAreaLeadSchema = z.object({ // ─── Bulk import ───────────────────────────────────────────────────────────── -const importEmail = z.string().email('Invalid email').max(200).transform(s => s.trim().toLowerCase()) +const importEmail = emailField('Invalid email') export const importMembersSchema = z.array( z.object({ diff --git a/types/database.ts b/types/database.ts index 445466d..72781e4 100644 --- a/types/database.ts +++ b/types/database.ts @@ -46,6 +46,54 @@ export type Database = { Update: { id?: string; space_id?: string; entity_type?: string; entity_id?: string; role?: string; created_at?: string } Relationships: [] } + forum_threads: { + Row: { id: string; space_id: string; author_id: string | null; title: string; body: string | null; category: string; pinned: boolean; locked: boolean; comment_count: number; last_comment_at: string | null; created_at: string; updated_at: string } + Insert: { id?: string; space_id: string; author_id?: string | null; title: string; body?: string | null; category?: string; pinned?: boolean; locked?: boolean; comment_count?: number; last_comment_at?: string | null; created_at?: string; updated_at?: string } + Update: { id?: string; space_id?: string; author_id?: string | null; title?: string; body?: string | null; category?: string; pinned?: boolean; locked?: boolean; comment_count?: number; last_comment_at?: string | null; created_at?: string; updated_at?: string } + Relationships: [] + } + comments: { + Row: { id: string; space_id: string; entity_type: Database['public']['Enums']['comment_entity_type']; entity_id: string; author_id: string | null; parent_id: string | null; body: string; edited_at: string | null; created_at: string } + Insert: { id?: string; space_id: string; entity_type: Database['public']['Enums']['comment_entity_type']; entity_id: string; author_id?: string | null; parent_id?: string | null; body: string; edited_at?: string | null; created_at?: string } + Update: { id?: string; space_id?: string; entity_type?: Database['public']['Enums']['comment_entity_type']; entity_id?: string; author_id?: string | null; parent_id?: string | null; body?: string; edited_at?: string | null; created_at?: string } + Relationships: [] + } + space_tiers: { + Row: { id: string; space_id: string; slug: string; name: string; description: string | null; monthly_price_cents: number; billing_cadence: string; is_system: boolean; is_archived: boolean; sort_order: number; created_at: string; updated_at: string } + Insert: { id?: string; space_id: string; slug: string; name: string; description?: string | null; monthly_price_cents?: number; billing_cadence?: string; is_system?: boolean; is_archived?: boolean; sort_order?: number; created_at?: string; updated_at?: string } + Update: { id?: string; space_id?: string; slug?: string; name?: string; description?: string | null; monthly_price_cents?: number; billing_cadence?: string; is_system?: boolean; is_archived?: boolean; sort_order?: number; created_at?: string; updated_at?: string } + Relationships: [] + } + space_invites: { + Row: { id: string; space_id: string; code: string; label: string | null; expires_at: string | null; max_uses: number | null; uses_count: number; is_enabled: boolean; created_by: string | null; created_at: string; updated_at: string } + Insert: { id?: string; space_id: string; code: string; label?: string | null; expires_at?: string | null; max_uses?: number | null; uses_count?: number; is_enabled?: boolean; created_by?: string | null; created_at?: string; updated_at?: string } + Update: { id?: string; space_id?: string; code?: string; label?: string | null; expires_at?: string | null; max_uses?: number | null; uses_count?: number; is_enabled?: boolean; created_by?: string | null; created_at?: string; updated_at?: string } + Relationships: [] + } + space_onboarding_steps: { + Row: { id: string; space_id: string; step_key: string; step_type: string; title: string; body: string | null; config: Json; is_enabled: boolean; is_required: boolean; is_system: boolean; sort_order: number; created_at: string; updated_at: string } + Insert: { id?: string; space_id: string; step_key: string; step_type: string; title: string; body?: string | null; config?: Json; is_enabled?: boolean; is_required?: boolean; is_system?: boolean; sort_order?: number; created_at?: string; updated_at?: string } + Update: { id?: string; space_id?: string; step_key?: string; step_type?: string; title?: string; body?: string | null; config?: Json; is_enabled?: boolean; is_required?: boolean; is_system?: boolean; sort_order?: number; created_at?: string; updated_at?: string } + Relationships: [] + } + space_role_labels: { + Row: { id: string; space_id: string; role: string; display_name: string | null; description: string | null; color: string | null; sort_order: number; created_at: string; updated_at: string } + Insert: { id?: string; space_id: string; role: string; display_name?: string | null; description?: string | null; color?: string | null; sort_order?: number; created_at?: string; updated_at?: string } + Update: { id?: string; space_id?: string; role?: string; display_name?: string | null; description?: string | null; color?: string | null; sort_order?: number; created_at?: string; updated_at?: string } + Relationships: [] + } + space_custom_roles: { + Row: { id: string; space_id: string; slug: string; name: string; description: string | null; color: string | null; sort_order: number; created_at: string; updated_at: string } + Insert: { id?: string; space_id: string; slug: string; name: string; description?: string | null; color?: string | null; sort_order?: number; created_at?: string; updated_at?: string } + Update: { id?: string; space_id?: string; slug?: string; name?: string; description?: string | null; color?: string | null; sort_order?: number; created_at?: string; updated_at?: string } + Relationships: [] + } + space_member_custom_roles: { + Row: { member_id: string; custom_role_id: string; assigned_at: string } + Insert: { member_id: string; custom_role_id: string; assigned_at?: string } + Update: { member_id?: string; custom_role_id?: string; assigned_at?: string } + Relationships: [] + } activity_log: { Row: { action: string @@ -563,6 +611,7 @@ export type Database = { id: string is_meeting_minutes: boolean is_pinned: boolean + render_markdown: boolean meeting_date: string | null pinned: boolean space_id: string @@ -585,6 +634,7 @@ export type Database = { id?: string is_meeting_minutes?: boolean is_pinned?: boolean + render_markdown?: boolean meeting_date?: string | null pinned?: boolean space_id: string @@ -607,6 +657,7 @@ export type Database = { id?: string is_meeting_minutes?: boolean is_pinned?: boolean + render_markdown?: boolean meeting_date?: string | null pinned?: boolean space_id?: string @@ -1078,6 +1129,8 @@ export type Database = { notes: string | null space_id: string title: string | null + encrypted_value: string | null + encryption_version: number updated_at: string value: string } @@ -1093,6 +1146,8 @@ export type Database = { notes?: string | null space_id: string title?: string | null + encrypted_value?: string | null + encryption_version?: number updated_at?: string value: string } @@ -1108,6 +1163,8 @@ export type Database = { notes?: string | null space_id?: string title?: string | null + encrypted_value?: string | null + encryption_version?: number updated_at?: string value?: string } @@ -1192,6 +1249,9 @@ export type Database = { status: Database["public"]["Enums"]["member_status"] stripe_customer_id: string | null tier: Database["public"]["Enums"]["member_tier"] + tier_id: string | null + onboarding_completed_at: string | null + onboarding_progress: Json updated_at: string user_id: string | null willing_to: string[] @@ -1222,6 +1282,9 @@ export type Database = { status?: Database["public"]["Enums"]["member_status"] stripe_customer_id?: string | null tier?: Database["public"]["Enums"]["member_tier"] + tier_id?: string | null + onboarding_completed_at?: string | null + onboarding_progress?: Json updated_at?: string user_id?: string | null willing_to?: string[] @@ -1252,6 +1315,9 @@ export type Database = { status?: Database["public"]["Enums"]["member_status"] stripe_customer_id?: string | null tier?: Database["public"]["Enums"]["member_tier"] + tier_id?: string | null + onboarding_completed_at?: string | null + onboarding_progress?: Json updated_at?: string user_id?: string | null willing_to?: string[] @@ -1502,6 +1568,7 @@ export type Database = { Enums: { area_lead_status: "active" | "vacant" | "handoff" channel_type: "general" | "area" | "ops" | "project" + comment_entity_type: "forum_thread" | "proposal" | "incident" | "policy" contact_type: "vendor" | "supplier" | "partner" | "landlord" | "city" directory_visibility: | "board_only" @@ -1685,6 +1752,7 @@ export const Constants = { Enums: { area_lead_status: ["active", "vacant", "handoff"], channel_type: ["general", "area", "ops", "project"], + comment_entity_type: ["forum_thread", "proposal", "incident", "policy"], contact_type: ["vendor", "supplier", "partner", "landlord", "city"], directory_visibility: [ "board_only",