Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2fc18fe
Add implementation instructions for crew sync v2 phases 2-5
claude Jul 24, 2026
f6da8a4
Add Tier 3 structural enforcement: DB invariant gate in CI
claude Jul 24, 2026
1058284
Fix first armed e2e CI run and Sonar new-code security finding
claude Jul 24, 2026
d9441ee
Add missing migration for handle_new_user search_path hotfix
claude Jul 24, 2026
7d230e4
Capture prod schema drift as a migration; sync E2E project to parity
claude Jul 24, 2026
db1b135
Fix latent e2e suite bugs surfaced by the first full armed run
claude Jul 24, 2026
354794f
Address CodeRabbit review findings
claude Jul 24, 2026
9e86e3f
Fix root causes of the remaining e2e spec failures
claude Jul 25, 2026
e0ab203
Fix orphaned-user leak in 27-crew-feedback's setup helper
claude Jul 25, 2026
953baa4
Fix locator-identity bug in 20-help.spec.ts's FAQ-expand test
claude Jul 25, 2026
33e7a79
fix(e2e): clean stale communication_logs rows and dedupe comms-log te…
claude Jul 25, 2026
8af4e48
fix(e2e): root-cause fixes for vendor compliance, crew logout guard, …
claude Jul 25, 2026
20bc74f
fix(e2e): address CodeRabbit findings in crew logout guard's disposab…
claude Jul 25, 2026
3d3e309
fix(e2e): correct vendor-compliance option assertion and grace-period…
claude Jul 25, 2026
f778015
fix(e2e): browser.newContext() silently inherits PM storageState mid-…
claude Jul 25, 2026
6afc7c7
fix(e2e): force-enable and select the bypassed vendor option atomically
claude Jul 25, 2026
7906cea
fix(e2e): raise per-test timeout for disposable-crew-login specs
claude Jul 25, 2026
95f5d18
fix(e2e): force-enable submit button and add reload robustness to ven…
claude Jul 25, 2026
ce95c42
fix: correct stale 31-day threshold in VENDOR_HARD_BLOCKED_ERROR message
claude Jul 25, 2026
27a27d5
fix(ci): cancel superseded workflow runs for the same branch/PR
claude Jul 25, 2026
a0640c9
fix(e2e): paginate auth.admin.listUsers() and sweep orphaned disposab…
claude Jul 25, 2026
4d72aff
Fix crew networkidle hangs and property-select race in e2e specs
claude Jul 25, 2026
12eba1a
Fix 22-crew-logout-guard offline test navigating while still offline
claude Jul 25, 2026
7f4bcc4
Fix seeded e2e vendor missing stripe_connect_charges_enabled
claude Jul 25, 2026
c20df4a
Fix maintenance board stale-after-create, owner portal heading assert…
claude Jul 25, 2026
1f208ff
Reduce cognitive complexity in cleanOrphanedDisposableAuthUsers, use …
claude Jul 25, 2026
0452758
Fail fast on missing Upstash config, surface webServer logs in CI
claude Jul 25, 2026
5334667
Add DB-verification diagnostic to 05/24's still-failing board assertions
claude Jul 25, 2026
ddd7770
Capture on-page error banner text on global-setup login failure
claude Jul 25, 2026
9949c1e
Extract nested template literal in login-failure error message
claude Jul 25, 2026
f0a820b
Fix root cause of 05/24's silent maintenance-board query failure
claude Jul 25, 2026
a493d00
Fill required technician-name field in 21-work-order-offline tests
claude Jul 25, 2026
a92d2b2
Fix the last two known e2e failures: booking source, turnover fetch w…
claude Jul 25, 2026
1aaf448
Scope 26-turnover-crew-assignment's card by a unique marker, not posi…
claude Jul 25, 2026
dcb41d0
Scope 26-turnover-crew-assignment's card by stable ID, not rendered text
claude Jul 25, 2026
f24d2e8
Poll for the just-created turnover's ID instead of a single query
claude Jul 25, 2026
e9447a0
Use exact match for the "Assign" button click in 26-turnover-crew-ass…
claude Jul 25, 2026
fe58982
Extend exact:true to the remaining crew-select locators in 26-turnover
claude Jul 25, 2026
bea3de1
Hide decorative avatar-initial span from accessible name in crew drop…
claude Jul 25, 2026
d40b60d
Widen Booking-added banner timeout to absorb E2E-project DB latency
claude Jul 25, 2026
6bf0204
Document remaining structural-scalability tier items as instructions
claude Jul 25, 2026
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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ on:
push:
branches: [main]

# Cancel a still-running CI run for this branch/PR when a new commit lands
# on it, instead of letting both run to completion. Without this, rapid
# successive pushes spawn overlapping e2e runs that all target the SAME
# live E2E Supabase project (not per-run isolated) — two runs' global-setup/
# global-teardown can race and delete [E2E]-prefixed seed rows out from
# under each other's still-in-progress tests, surfacing as unexplained
# "did not find some options" / element-not-found flakes that have nothing
# to do with the actual code under test.
#
# Not scoped to github.ref — github.ref differs between a pull_request event
# (refs/pull/<pr>/merge) and a push event for the same branch/commit
# (refs/heads/<branch>), so a per-ref group lets a push run and a
# pull_request run for the same change execute concurrently, which is
# exactly the race this block exists to prevent. A single group for the
# whole workflow serializes every run (across all branches and PRs) that
# touches the shared E2E Supabase project.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

jobs:
checks:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -96,3 +116,28 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 14

# ── Database invariants (structural enforcement Tier 3) ──────────────────
# Checks the live schema for what no code-side check can see: RLS enabled
# on every public table, no unexpected policy-less (deny-all) tables, a
# covering index on every FK column, zero anon table grants. Runs against
# the DEDICATED E2E project (same secrets as the e2e job — CI never holds
# production credentials); both projects receive every migration, so
# schema invariants verified there hold for production by construction.
# Self-disarming like the e2e job: secrets absent → warning annotation,
# job passes. See scripts/check-db-invariants.mjs for the check details.
db-invariants:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
steps:
- uses: actions/checkout@v4
with:
# This job only reads the repo and runs one script — no reason to
# leave the checkout token on disk.
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node scripts/check-db-invariants.mjs
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1057,8 +1057,8 @@ item below" as part of the definition of done for any non-trivial change.
## Structural Enforcement — Guardrails

Conventions in this file are enforced in code wherever they can be, so
following them stops being a memory test. Three layers, checked in CI via
`npm run lint` and `vitest run`:
following them stops being a memory test. Four layers, checked in CI via
`npm run lint` and `vitest run` (plus the `db-invariants` CI job for layer 4):

1. **ESLint rules** (`eslint.config.mjs`, the "Structural enforcement"
config block) — AST-level bans scoped to `app/`, `lib/`, `components/`:
Expand Down Expand Up @@ -1087,6 +1087,16 @@ following them stops being a memory test. Three layers, checked in CI via

3. **`check:ui-classes`** — the raw `btn-*`/`badge-*`/`card` class grep.

4. **DB invariant gate** (`scripts/check-db-invariants.mjs`, CI
`db-invariants` job) — the live-schema invariants no code-side check can
see, via `public.db_invariant_report()` against the dedicated E2E
project: RLS enabled on every public table, no policy-less (deny-all)
tables outside the script's shrink-only `SERVICE_ROLE_ONLY_TABLES`
allowlist, a covering index on every FK column, and zero `anon` table
grants (all revoked 2026-07-24 — no client reads tables
unauthenticated). Self-disarms with a warning when the E2E secrets are
absent, same as the e2e job.

**The meta-rule: a new convention ships WITH its guardrail.** If a rule is
worth adding to this file, add its ESLint rule or `unit/guardrails/` test in
the same PR — and the CLAUDE.md prose for mechanically-checkable rules
Expand Down
9 changes: 9 additions & 0 deletions app/(dashboard)/maintenance/CreateWorkOrderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function CreateWorkOrderModal({
vendorCompliance = [],
orgId = '',
onClose,
onSuccess,
onWarning,
}: Readonly<{
properties: PropertyOptionWithCoords[]
Expand All @@ -39,6 +40,7 @@ export function CreateWorkOrderModal({
vendorCompliance?: VendorComplianceRow[]
orgId?: string
onClose: () => void
onSuccess?: () => void
onWarning?: (msg: string) => void
}>) {
const [state, action, pending] = useActionState(createWorkOrder, null)
Expand Down Expand Up @@ -77,6 +79,13 @@ export function CreateWorkOrderModal({
useEffect(() => {
if (!state?.success || !state.workOrderId) return

// revalidatePath() in the createWorkOrder Server Action refreshes the
// Server Component's data on the NEXT navigation, but this modal closes
// without one — the parent board's props otherwise go stale until some
// other navigation happens to trigger a refetch. router.refresh() (via
// onSuccess, wired in maintenance-board.tsx) forces that refetch now.
onSuccess?.()

if (state.warning) onWarning?.(state.warning)

// No photos attached — close immediately
Expand Down
4 changes: 3 additions & 1 deletion app/(dashboard)/maintenance/maintenance-board.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useState, useTransition, useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import {
Plus, ChevronDown, X, Wrench, Calendar, DollarSign,
User, ChevronRight, AlertTriangle, CheckCircle2, Clock,
Expand Down Expand Up @@ -855,6 +855,7 @@ export function MaintenanceBoard({
orgId?: string
role: string
}) {
const router = useRouter()
const searchParams = useSearchParams()
const urlFilter = searchParams.get('filter')

Expand Down Expand Up @@ -1240,6 +1241,7 @@ export function MaintenanceBoard({
vendorCompliance={vendorCompliance}
orgId={orgId}
onClose={() => setShowCreate(false)}
onSuccess={() => router.refresh()}
onWarning={setWarning}
/>
)}
Expand Down
41 changes: 27 additions & 14 deletions app/(dashboard)/maintenance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export default async function MaintenancePage() {
const { supabase, membership } = await requireOrgMember()

const [
{ data: workOrders },
{ data: properties },
{ data: vendors },
{ data: schedules },
{ data: crewMembers },
{ data: propertyAssets },
{ data: vendorCompliance },
workOrdersResult,
propertiesResult,
vendorsResult,
schedulesResult,
crewMembersResult,
propertyAssetsResult,
vendorComplianceResult,
] = await Promise.all([
supabase
.from('work_orders')
Expand Down Expand Up @@ -89,15 +89,28 @@ export default async function MaintenancePage() {
.eq('org_id', membership.org_id),
])

// A query erroring (bad filter value, RLS misconfiguration, etc.) and a
// query legitimately returning zero rows both leave `data` empty — `?? []`
// below can't tell them apart, so without this the board just silently
// renders as if nothing exists instead of surfacing a real outage.
const results = [
['work_orders', workOrdersResult], ['properties', propertiesResult], ['vendors', vendorsResult],
['maintenance_schedules', schedulesResult], ['crew_members', crewMembersResult],
['property_assets', propertyAssetsResult], ['vendor_compliance_status', vendorComplianceResult],
] as const
for (const [name, result] of results) {
if (result.error) console.error(`[MaintenancePage] ${name} query failed:`, result.error)
}

return (
<MaintenanceBoard
workOrders={workOrders ?? []}
properties={properties ?? []}
vendors={vendors ?? []}
schedules={schedules ?? []}
crewMembers={crewMembers ?? []}
propertyAssets={propertyAssets ?? []}
vendorCompliance={vendorCompliance ?? []}
workOrders={workOrdersResult.data ?? []}
properties={propertiesResult.data ?? []}
vendors={vendorsResult.data ?? []}
schedules={schedulesResult.data ?? []}
crewMembers={crewMembersResult.data ?? []}
propertyAssets={propertyAssetsResult.data ?? []}
vendorCompliance={vendorComplianceResult.data ?? []}
orgId={membership.org_id}
role={membership.role}
/>
Expand Down
3 changes: 2 additions & 1 deletion app/(dashboard)/turnovers/turnover-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ function CrewAssignment({
onClick={() => handleAdd(c.id)}
className="w-full text-left px-3 py-2 text-sm hover:bg-canvas-themed transition-colors flex items-center gap-2"
>
<span className="w-6 h-6 rounded-full bg-brand-100 text-brand-700 text-xs font-bold flex items-center justify-center flex-shrink-0">
<span aria-hidden="true" className="w-6 h-6 rounded-full bg-brand-100 text-brand-700 text-xs font-bold flex items-center justify-center flex-shrink-0">
{c.name[0]?.toUpperCase()}
</span>
{c.name}
Expand Down Expand Up @@ -353,6 +353,7 @@ function TurnoverCard({

return (
<div
data-testid={`turnover-card-${turnover.id}`}
className={cn(
'bg-card-themed rounded-xl border transition-shadow',
CARD_BORDER_CLASS[urgencyTone],
Expand Down
Loading
Loading