© A MoneyFund Company
← Dashboard

Problems & Observations

529 unique open issues ranked high → medium → low, then by fix effort (trivial first). 104 high-severity · 164 flagged risky to fix.

104 high164 risky529 open529 total

Fix effort key: trivial (minutes) → easy (small PR) → moderate (half-day+) → hard (multi-day / monolith) → major (architectural) · Fix risk flags changes that may break kiosks, adjacent flows, or data contracts.

  • ·Service-role updates without company_id checks
  • ·Public routes exposing ODBC, snapshots, or PII
  • ·Session/API paths that trust client-supplied IDs
  • ·Email and agent notification open relays
  • ·Client-only auth with no middleware
  • ·Infinite spinners on logged-out or missing session
  • ·Signup/domain validation and auto-company creation
  • ·Open redirect and admin-password oracle risks
  • ·Dashboard card hiding without route enforcement
  • ·Viewer-accessible mutating APIs
  • ·Fail-open permission queries
  • ·Tool-page gates missing on sibling Greco tools
  • ·GRECO_COMPANY_ID env pin across warehouse suite
  • ·Tables or APIs without tenant scoping
  • ·Cross-tenant reads in memory before JS filter
  • ·Global localStorage keys on shared PCs
  • ·checkCompanyAIAccess() not enforced on UI routes
  • ·Agent tools bypass calendar/document/chat RLS
  • ·Default agent_settings grant full data-source access
  • ·v1 agent hardcoded admin role
  • ·v1 analyze/search/filters validation gaps
  • ·Batch localhost fallback and parallel thundering herd
  • ·Email template whitelist drift and HTML injection
  • ·Entrée panel polish: MapPanel cache, StockVoo /port, BridgeRelay poll
  • ·greco_daily_stats, firmas_entries, thruput, driver_scan_reports
  • ·ide_user_settings missing company_id
  • ·RLS policies incomplete on new tables
  • ·Destructive migrations without idempotency guards
  • ·sessionStorage scan redirect never consumed
  • ·History/team-chat deep links drop parameters
  • ·Orphaned Supabase helpers never imported
  • ·Notification paths that never read configured recipients
  • ·getUnifiedHistory(0) and uncapped report getters
  • ·Multi-megabyte JSONB rows in Postgres
  • ·Aggressive 3–30s polling without visibility gates
  • ·N+1 query patterns in GodView and anomalies
  • ·greco_daily_stats / analytics-log tool_name mismatches
  • ·UTC report_date stamping across tool grid
  • ·GrecoOnlyGuard without greco_card_permissions
  • ·Thruput, DockSheet, and anomaly detection gaps
  • ·Parallel BFC xlsx and Shanetrée ODBC planes
  • ·Gold-standard joins: Low Slots, Return Diffs, Shorts↔PICK
  • ·Duplicate reconcilers and identifier namespace drift
  • ·Read-only snapshots with no write-back
  • ·Relay claim global dequeue and stale modal state
  • ·Public driver-chat and invoice-status exposure
  • ·Will-call / NOE print queue races and UI gaps
  • ·UTC date pickers seeding ODBC modal ranges
  • ·Full DockSheet roster on public /timer
  • ·Will-call admin without date picker or role delete gate
  • ·English-only inbound/sales kiosks
  • ·Missing PWA manifest and iOS safe-area viewport
  • ·IDE routes with zero web callers (~1,100 lines)
  • ·Keystroke-save storms and unbounded settings sync
  • ·Build preview localStorage not namespaced per user
  • ·Custom-tools POST without role gate
  • ·team_activity table name typo → activeToday always 0
  • ·Unbounded metrics collectors
  • ·AI worker marks completed when DB save fails
  • ·QStash-down false-success stubs
  • ·Dashboard tab state and back-navigation
  • ·Duplicate entry points (cards vs tab bar)
  • ·Report email links to dead tab values
  • ·Route-level loader theme flash
  • ·Blocking alert() on dashboard
  • ·English-only members/support/history/counting
  • ·FancyLoader and HistoryPanel missing ARIA
  • ·Inconsistent denial cards vs Greco guard
  • ·Bridge PC binaries beside Next.js app root
  • ·Flat app/ and lib/supabase/ without domain grouping
  • ·Missing .github CI, tests, and docs/ hub
  • ·Stale migration README and misleading combined SQL
  • ·Unused lib/* exports and duplicate implementations
  • ·xlsx@0.18.5 CVE and cdnjs ExcelJS without SRI
  • ·TOOL_REGISTRY covering only 10 of 29 tools
  • ·Smart Reports ghosts in history export
H

High severity

104
1

Analytics chat can wedge its send button forever

Wiring & Broken Flows

highOpenTrivial fix

InlineAnalyticsChat in app/chat/page.tsx returns early when res.body has no reader — before the setLoading(false) at the bottom of the function. A bodyless or interrupted stream response permanently disables the send button for that tab until a refresh. The reset belongs in a finally block.

app/chat/page.tsx

Fix effort: Same finally-block fix.

2

Analytics chat wedges send button on bodyless stream

Data Flows & Wiring

highOpenTrivial fix

app/chat/page.tsx InlineAnalyticsChat sendMessage (~156–157): const reader = res.body?.getReader(); if (!reader) return; — early return skips setLoading(false) at line 192 (only reached after while loop). Bodyless or opaqueredirect responses wedge Send disabled until refresh. Belongs in finally { setLoading(false) }.

app/chat/page.tsx

Fix effort: Wrap sendMessage body in try/finally { setLoading(false) } in app/chat/page.tsx.

3

Anomaly acknowledge/dismiss has no tenant check (IDOR)

Security & Trust Boundaries

highOpenTrivial fixMay affect neighbors

lib/anomaly/service.ts acknowledgeAnomaly() and dismissAnomaly() update anomaly_detections by UUID only via the service-role client — no .eq('company_id', …). Any authenticated user or v1 API-key holder who knows another tenant's anomaly ID can change its status. Callers: app/api/anomalies/route.ts POST and app/api/v1/anomalies/route.ts PATCH.

lib/anomaly/service.tsapp/api/anomalies/route.tsapp/api/v1/anomalies/route.ts

Fix risk: Tenant guard may reject legitimate cross-service calls that used bare UUID today.

Fix effort: Add .eq('company_id', callerCompany) to two service-role updates in lib/anomaly/service.ts; pass company from route handlers.

4

Anomaly acknowledge/dismiss never checks the tenant

Security & Trust Boundaries

highOpenTrivial fix

acknowledgeAnomaly() and dismissAnomaly() in lib/anomaly/service.ts update anomaly_detections by id alone, through the service-role client — no company_id filter. Any authenticated user (or v1 API-key holder, via /api/anomalies POST and /api/v1/anomalies PATCH) who knows or guesses another tenant's anomaly UUID can acknowledge or dismiss it. Classic IDOR: the fix is one .eq('company_id', callerCompany) on both updates.

lib/anomaly/service.ts

Fix effort: Same IDOR fix as above — duplicate audit entry.

5

Login ?redirect= is an open redirect

Security & Trust Boundaries

highOpenTrivial fix

app/login/page.tsx reads params.get('redirect') unchanged and passes it to router.push after sign-in (line 56). A crafted ?redirect=https://evil.com or //evil.com sends users to an external site immediately after they authenticate. Should validate same-origin relative paths only.

app/login/page.tsx

Fix effort: Validate redirect starts with / and has no // protocol-relative trick in app/login/page.tsx (~10 lines).

6

Shanetrée relay-fulfill updates rows by UUID with no tenant guard

Security & Trust Boundaries

highOpenTrivial fix

app/api/internal/shanetree-relay-fulfill/route.ts updates shanetree_relay_requests with .eq('id', body.request_id).eq('status', 'claimed') only — no company_id check. Parallel to willcall-print-done: shared secret + UUID lets any tenant's claimed relay be closed out.

app/api/internal/shanetree-relay-fulfill/route.ts

Fix effort: Add company_id to fulfill body + .eq('company_id', …) like willcall-print-claim.

7

v1 batch self-fetch falls back to http://localhost:3000

Email, v1 API & Entrée Panel Polish

highOpenTrivial fixMay affect neighbors

app/api/v1/batch/route.ts (~133) sets baseUrl from NEXT_PUBLIC_SITE_URL || NEXT_PUBLIC_APP_URL || 'http://localhost:3000'. On Vercel/serverless without those env vars, batch sub-requests hit localhost and fail silently for API consumers — distinct from tracked batch path allowlist gap.

app/api/v1/batch/route.ts

Fix risk: Changing base URL breaks batch in Vercel preview unless VERCEL_URL always set — integrators depend on current fallback in dev.

Fix effort: Fail closed when SITE_URL unset, or derive from request.headers.get('host') in batch/route.ts.

8

willcall-print-done updates rows by UUID with no tenant guard

Security & Trust Boundaries

highOpenTrivial fix

app/api/internal/willcall-print-done/route.ts authenticates INTERNAL_API_SECRET then updates will_call_print_requests with .eq('id', body.id) only — no company_id filter. Contrast willcall-print-claim which does filter by company. Anyone with the shared secret can mark any tenant's print job done/failed if they know the UUID.

app/api/internal/willcall-print-done/route.ts

Fix effort: Add company_id to DoneBody + .eq('company_id', …) like willcall-print-claim; bridge caller must send company.

9

Window slot-load failures render every time as available

Window, Sales & Kiosk Scheduling

highOpenTrivial fix

loadSlots() in app/window/WindowClient.tsx (~194–205) catch sets setSlots({}) on fetch/parse failure. Empty slots means no booked/requested flags — all times look open during API outages, inviting bookings into already-accepted slots. No error banner distinguishes outage from genuinely open grid.

app/window/WindowClient.tsx

Fix effort: Show error state in WindowClient when loadSlots fails instead of setSlots({}).

10

/api/greco/analytics-chat skips AI quota controls and Greco company pin

Greco Warehouse Operations

highOpenEasy fix

POST /api/greco/analytics-chat authenticates any user with profile.company_id and canAccessAgent (~248–258) but never calls checkCompanyAIAccess() and never compares companyId to NEXT_PUBLIC_GRECO_COMPANY_ID. Non-Greco tenants with agent access can invoke GRECO_TOOL_DEFINITIONS (queryDailyStats, executeReadOnlySql against greco_daily_stats, firmas_entries, report tables). app/chat/page.tsx also posts here — widens the bypass beyond /daily-stats.

app/chat/page.tsx

Fix effort: Add checkCompanyAIAccess + GRECO_COMPANY_ID guard before streaming; same pattern as v1 AI routes.

11

/quotes-admin accept and reject fail against quote_submissions RLS

Controls, Admin & Collaboration

highOpenEasy fix

confirmAccept/handleReject in app/quotes-admin/page.tsx (~269–310) use browser createSupabaseClient().update({ status }). Migration 012 UPDATE policy requires user_id = auth.uid() AND status = 'pending' in both USING and WITH CHECK — accepting sets status='accepted', which violates WITH CHECK. Admin cannot accept another user's submission via this page. Dashboard correctly uses /api/admin/submissions (service role) at dashboard/page.tsx ~1387.

app/quotes-admin/page.tsx

Fix effort: Route accept/reject through /api/admin/submissions like dashboard does, or add admin UPDATE policy.

12

/sales print-once treats pending jobs as permanently consumed

Window, Sales & Kiosk Scheduling

highOpenEasy fix

CONSUMED_STATUSES = ['pending','printing','done'] in app/api/public/will-call-print-request/route.ts (~50, 146–147). GET printed list and POST alreadyPrinted guard count pending as consumed. SalesClient.tsx (~825–835, ~927–930) hides the print button immediately and reload keeps it hidden even when the bridge PC is offline — client re-enables only on poll failed/timeout (~965–982), not on stuck pending forever in DB.

app/api/public/will-call-print-request/route.ts

Fix effort: Exclude pending from CONSUMED_STATUSES; only count printing/done as consumed.

13

Agent autonomy cron skips company AI access policy

API & Operations

highOpenEasy fixHigh blast radius

app/api/cron/agent-autonomy/route.ts (vercel.json */10) enqueues autonomous digest agent_task jobs for companies with auto_summarize / auto_email_digest enabled but never calls checkCompanyAIAccess(). Companies with AI disabled or exhausted quota still get background Gemini work — parallel to the port-ai-summary cron gap.

app/api/cron/agent-autonomy/route.ts

Fix risk: Adding checkCompanyAIAccess may stop digests companies think they enabled — pairs with decorative AgentSettings toggles.

Fix effort: Add checkCompanyAIAccess before enqueue in agent-autonomy/route.ts.

14

AI analysis worker marks jobs completed when DB save fails

Public API, Jobs, Onboarding & Greco Gaps

highOpenEasy fix

app/api/jobs/workers/ai-analysis/route.ts — on tool_ai_analyses insert error (~167–173) logs warn but still calls markJobCompleted() (~211–216). Pollers see status completed with no persisted analysis — distinct from tracked QStash stub (that path never calls Gemini either, but this worker did run AI).

app/api/jobs/workers/ai-analysis/route.ts

Fix effort: Call markJobFailed() when tool_ai_analyses insert errors — don't markJobCompleted.

15

Any Greco member (including viewers) can accept or reject appointments

Window, Sales & Kiosk Scheduling

highOpenEasy fix

supabase/migrations/133_create_will_call_appointments.sql lines 62–67: UPDATE policy allows any profiles.company_id member with no role check. decideWillCallAppointment() in lib/supabase/window-appointments.ts (~107–117) updates by id only. /windowadmin is gated by GrecoOnlyGuard only (app/windowadmin/page.tsx) — not founder/admin like /controls — so viewers with a bookmark can accept/reject will-call requests via the browser client.

supabase/migrations/133_create_will_call_appointments.sqllib/supabase/window-appointments.tsapp/windowadmin/page.tsx

Fix effort: Tighten will_call_appointments UPDATE RLS to manager+; gate /windowadmin like /controls.

16

custom-tools/versions GET leaks private tool source to any coworker

Build, IDE & Custom Tools

highOpenEasy fix

app/api/custom-tools/versions/route.ts (~66–90) checks only company_id, not created_by or is_public. Unlike custom-tools/data verifyToolAccess, any company member who knows a private tool UUID gets full version history including code snapshots.

app/api/custom-tools/versions/route.ts

Fix effort: Reuse verifyToolAccess from data route; filter versions by created_by or is_public.

17

Dashboard per-tool Email Settings modal is unwired dead UI

Controls, Admin & Collaboration

highOpenEasy fix

openEmailSettingsModal() is defined in app/dashboard/page.tsx (~575) but grep finds zero callers — only setEmailSettingsModal(null) closes the modal (~2856, ~3142). The full per-tool recipient picker + AI-filter editor (~2837+) can never open. Tracked 'UI report emails ignore per-tool recipient lists' describes the symptom; this is the root cause — founders configure email in Controls member permissions but have no working dashboard surface to manage lists.

app/dashboard/page.tsx

Fix effort: Wire openEmailSettingsModal to Reports/Tools gear buttons or delete modal and consolidate in Controls.

18

Documents postgres subscription overwrites unsaved local edits

Data Flows & Wiring

highOpenEasy fixHigh blast radius

app/documents/page.tsx subscribeToDocument callback applies remote content/title whenever it differs from lastContentRef with no hasUnsavedChanges guard. A collaborator's postgres_changes save can overwrite another editor's in-progress unsaved typing. The broadcast content_diff path checks hasUnsavedChanges; the DB subscription path does not.

app/documents/page.tsx

Fix risk: Ignoring remote events while dirty is correct but users collaborating may never see others' edits until save — behavior change.

Fix effort: Skip subscribeToDocument content apply when hasUnsavedChanges; show conflict banner instead.

19

Driver-chat AI skips company AI quota controls

Shanetrée, Routing & Driver Comms

highOpenEasy fix

generateDriverChatReply() in lib/driver-chat/ai.ts (~157–165) calls gatewayChatCompletion with no checkCompanyAIAccess(). Callers include POST /api/public/driver-chat (auto-reply on every driver message), GET /api/driver-chat/ai?test=1 and POST /api/driver-chat/ai-respond. Session AI kill-switch is tracked for agent/greco routes but driver-chat is a separate high-volume public path that can burn tokens even when AI is disabled or over quota.

lib/driver-chat/ai.ts

Fix effort: Call checkCompanyAIAccess in generateDriverChatReply and public driver-chat POST before gateway.

20

Forklift reports are stamped with the UTC day, not the warehouse day

Data Integrity & Races

highOpenEasy fixHigh blast radius

lib/tools/processors/forklift.ts computes report_date as new Date().toISOString().split('T')[0] — UTC. Any upload after 7-8pm Eastern lands on tomorrow's date, disagreeing with the operator's expectation and with the other Greco tools that use local/bridge dates. That skews the leaderboard, date filters and the anomaly baselines keyed on report_date.

lib/tools/processors/forklift.ts

Fix risk: Switching to local/bridge date reorders historical leaderboards, anomaly baselines, and greco_daily_stats keys — needs backfill or dual-read period.

Fix effort: Same timezone fix.

21

Forklift reports stamped with UTC date, not warehouse local

Backend & Database

highOpenEasy fixHigh blast radius

lib/tools/processors/forklift.ts (~171–192): const reportDate = new Date().toISOString().split('T')[0] stamps report_date in UTC. Eastern uploads after ~7–8pm local land on tomorrow's date vs operator expectation and bridge-local dates in other Greco tools. Skews leaderboard sort, /dashboard date filters and anomaly baselines keyed on report_date.

lib/tools/processors/forklift.ts

Fix risk: Same UTC report_date semantics — downstream stats already keyed on UTC days.

Fix effort: Use existing bridge/local date helper (or America/New_York) in lib/tools/processors/forklift.ts — one line + verify leaderboard filters.

22

GodView message stats count the dead team_chat_messages table

Agent, API & Docs Integrity

highOpenEasy fix

app/api/admin/godview/route.ts counts team_chat_messages for overview (~226, ~238), per-company stats (~312) and message listings (~387). Live /team-chat uses chat_rooms / chat_room_messages. Same stale table in app/api/admin/analytics/route.ts (~66). GodView 'Team Messages' can read 0 while team-chat is active — misleading ops dashboard.

app/api/admin/godview/route.tsapp/api/admin/analytics/route.ts

Fix effort: Retarget godview + admin/analytics counts to chat_room_messages.

23

Hourly port-ai-summary cron skips company AI access policy

API & Operations

highOpenEasy fixHigh blast radius

vercel.json runs /api/cron/port-ai-summary every hour. The cron loops companies and calls generatePortAISummaryForCompany with no checkCompanyAIAccess import or call — multi-step Gemini work runs even when AI is disabled or over quota.

Fix risk: Enforcing policy stops summaries for companies that rely on cron today while UI AI still works.

Fix effort: Add checkCompanyAIAccess in cron loop before generatePortAISummaryForCompany.

24

item_email_alerts lets the anon key insert rows for any company

Security & Trust Boundaries

highOpenEasy fixHigh blast radius

supabase/migrations/130_create_item_email_alerts.sql enables RLS then adds FOR INSERT WITH CHECK (true) for anon. company_id is NOT NULL but has no REFERENCES companies(id). Anyone holding the public anon key can insert subscription rows pinned to any tenant UUID. /api/public/item-alert-subscribe pins Greco correctly in app code, but the database itself doesn't enforce that boundary.

supabase/migrations/130_create_item_email_alerts.sql

Fix risk: Tightening anon INSERT policy can break /api/public/item-alert-subscribe if CHECK is too strict for kiosk flow.

Fix effort: New migration: tighten anon INSERT policy to Greco company_id + add FK on company_id; verify /api/public/item-alert-subscribe still works.

25

Logged-out visitors get an infinite spinner instead of the login page

Wiring & Broken Flows

highOpenEasy fixHigh blast radius

team-chat, documents, account, developer, build (and /vibe) all handle a missing session with router.push('/login'); return; — without ever clearing their loading state. If the client-side redirect is slow or interrupted, the user sits on a permanent FancyLoader / “Authenticating…” screen. Each guard should flip its loading flag (or render nothing) when it bails.

Fix risk: Fixing one page's guard without middleware can leave others spinning; dashboard bootstrap is shared across tabs.

Fix effort: Same spinner fix across auth-guard pages.

26

Logged-out visitors stuck on infinite loading screens

Auth & Session

highOpenEasy fixHigh blast radius

dashboard (router.replace at line 1203), account, team-chat, documents, build, developer, history and /vibe all call router.push/replace('/login') and return without clearing loading/auth state. Users sit on FancyLoader / 'Authenticating…' indefinitely. quote/page.tsx does it right (finally { setLoading(false) }) — the others should match.

Fix risk: Duplicate entry — same guard/bootstrap coupling as dashboard auth wall.

Fix effort: Copy quote/page.tsx pattern: setLoading(false) or setAuthChecked(true) before return on login redirect — ~8 pages, mechanical.

27

Missing agent_settings row grants every data source and viewer chat access

Agent, Collaboration & Platform APIs

highOpenEasy fixHigh blast radius

lib/agent/permissions.ts getAgentSettings() (~89–97) returns DEFAULT_SETTINGS when no DB row exists — enabled_data_sources: ALL_DATA_SOURCES (picker_reports through team_chat) and viewer role with chat + memory_read enabled (~38–43, ~53–54). New companies get unrestricted agent access until a founder manually configures /controls agent settings. Enterprise default should be deny-by-default with explicit opt-in per source.

lib/agent/permissions.ts

Fix risk: Fail-closed default denies agent for new companies — opposite of today's permissive first-run behavior.

Fix effort: Seed agent_settings on company create with deny-by-default sources; change getAgentSettings fallback to restrictive defaults.

28

No .github/ — missing CI, PR template, and issue templates

Repository Structure Audit (Pass 23)

highOpenEasy fix

Glob .github/ returns nothing. No GitHub Actions for lint, tsc --noEmit, or build on PR; no PULL_REQUEST_TEMPLATE.md; no dependabot.yml. AGENTS.md says validation is manual lint + typecheck — but nothing enforces it on push. First signal a serious product repo sends is automated CI green on main.

Fix effort: Add .github/workflows/ci.yml (lint + tsc + build), PULL_REQUEST_TEMPLATE.md, dependabot.yml.

29

Profile-less users get silently assigned to Greco

Security & Trust Boundaries

highOpenEasy fixCritical — test everything

lib/supabase/tool-reports.ts getCompanyId() (and the same pattern in shanetree-snapshots, shanetree-pull-log, shanetree-manual-subs) auto-creates a profile pointing at whichever company matches ILIKE '%greco%' when a user has no company_id. A second tenant's user hitting a warehouse tool before profile sync could be written into Greco — cross-tenant data corruption. Should fail closed instead of defaulting to one tenant.

lib/supabase/tool-reports.ts

Fix risk: Fail-closed throws break first-login warehouse tools unless onboarding creates profile first; wrong default today is the only reason some tenants work at all.

Fix effort: Change getCompanyId() to throw instead of defaulting to Greco; ~4 lib files, but must verify onboarding flow won't break legitimate first-time users.

30

Public tool writes use a nil UUID created_by against auth.users FK

Build, IDE & Custom Tools

highOpenEasy fix

app/api/public-tools/data/route.ts (~143) upserts created_by:'00000000-0000-0000-0000-000000000000'. Migration 076 defines created_by UUID NOT NULL REFERENCES auth.users(id). Public ToolAPI saves either fail at insert unless a system user was manually seeded, or depend on undocumented DB setup — broken or fragile public persistence.

app/api/public-tools/data/route.ts

Fix effort: Seed system user migration or make created_by nullable for anonymous public writes.

31

SumatraPDF.exe (~11 MB) is committed at the repository root

Repository Structure Audit (Pass 23)

highOpenEasy fix

git ls-files includes SumatraPDF.exe at repo root. entree-bridge.js resolves it via join(__dirname, 'SumatraPDF.exe') (~1178). Every clone/pull ships a Windows PDF binary inside the Next.js SaaS tree — inflates GitHub/Vercel checkout, triggers security scanners, and looks unprofessional in a TypeScript product repo. Belongs in a bridge deployment bundle (S3/installer), gitignored vendor/ path, or documented optional install — not version control beside package.json.

Fix effort: git rm SumatraPDF.exe; document install in SHANETREE_REMOTE_SETUP; bridge reads env SUMATRA_PATH or vendor/ gitignored path.

32

v1 /agent and /agent/stream skip checkCompanyAIAccess

API & Operations

highOpenEasy fixHigh blast radius

Sibling /api/v1/ai/chat calls checkCompanyAIAccess(apiKey.companyId) before invoking the gateway. agent/route.ts and agent/stream/route.ts only gate on requireFeature: 'ai_analysis' — companies with AI disabled or exhausted quota can still burn tokens via v1 agent endpoints.

Fix risk: API-key agent consumers may exceed quota with no warning today — enforcement changes billing behavior.

Fix effort: Two-line import + guard at top of each v1 agent route, mirroring v1/ai/chat.

33

v1 GET /tools/:id returns any company tool's full HTML source

Build, IDE & Custom Tools

highOpenEasy fix

app/api/v1/tools/[id]/route.ts handleGet (~28–52) .select('*').eq('id', id).eq('company_id', apiKey.companyId) — returns tool.code full HTML (~52) with no .eq('created_by', apiKey.id) or is_public check. Any tenant API key with reports:read fetches private coworker tool source. Contrasts internal custom-tools/data route which calls verifyToolAccess.

app/api/v1/tools/

Fix effort: Add created_by or visibility check matching session custom-tools routes.

34

v1 POST /reports/analyze skips company AI kill switch and quota

Public API, Jobs, Onboarding & Greco Gaps

highOpenEasy fix

app/api/v1/reports/analyze/route.ts calls geminiFetch() (~185) with no checkCompanyAIAccess() import or call. Tracked gaps cover session AI routes and v1 /agent, but this first-class v1 analyze endpoint (documented in openapi.json) bypasses ai_enabled, daily quota and burst limits in lib/ai/access-policy.ts.

app/api/v1/reports/analyze/route.tslib/ai/access-policy.ts

Fix effort: Add checkCompanyAIAccess(apiKey.companyId) before geminiFetch — same pattern as v1 /ai/chat.

35

v1 PUT and DELETE /tools/:id lack ownership checks

Build, IDE & Custom Tools

highOpenEasy fix

app/api/v1/tools/[id]/route.ts handlePut/handleDelete verify .eq('company_id', apiKey.companyId) (~81, ~104) — any API key with reports:write in the tenant can overwrite or delete any custom_tools row, including private tools created by other members. No created_by === key creator check or role gate. Intra-tenant IDOR for multi-user companies.

app/api/v1/tools/

Fix effort: Require tool.created_by === apiKey.createdByUser or admin role on mutating routes.

36

Viewers can list company API keys and webhook endpoints

Security & Trust Boundaries

highOpenEasy fix

GET /api/settings/api-keys and /api/settings/webhooks require only a valid session; POST/DELETE are admin-gated but GET is not. Any company viewer can enumerate key prefixes, permissions, webhook URLs, delivery stats and creator emails via the Developer page.

Fix effort: Gate GET on api-keys and webhooks routes with admin/founder role check like POST.

37

Viewers can permanently delete will-call appointments on /windowadmin

Kiosk Surfaces, Greco Tools & Platform Polish

highOpenEasy fix

supabase/migrations/133_create_will_call_appointments.sql DELETE policy (~70–73) allows any company member with no role check. deleteWillCallAppointment() in lib/supabase/window-appointments.ts (~124–130) deletes by id only; WindowAdminClient.tsx exposes Delete (~317–318). Viewer accept/reject is tracked — hard delete by viewers is not.

supabase/migrations/133_create_will_call_appointments.sqllib/supabase/window-appointments.ts

Fix effort: Migration: DELETE policy requires role IN (admin, founder) or is_founder — match accept/reject gate.

38

Webhook test endpoints bypass isInternalUrl() SSRF protection

Security & API Gaps (Pass 24)

highOpenEasy fixMay affect neighbors

lib/api/webhooks.ts isInternalUrl() (~113) blocks private IPs in production sendWebhook() (~147), but both test routes call fetch(webhook.url) directly with no check: app/api/v1/webhooks/[id]/test/route.ts (~60–68) and app/api/settings/webhooks/[id]/test/route.ts (~107–115). Both spread ...webhook.headers into the outbound request — user-supplied header injection. API key holder registers webhook → http://169.254.169.254/ → test endpoint probes internal infra immediately.

lib/api/webhooks.tsapp/api/v1/webhooks/app/api/settings/webhooks/

Fix risk: Blocking private IPs breaks webhooks pointing at internal bridge PCs that are intentional today — need allowlist for bridge subnet.

Fix effort: Call isInternalUrl() in both test routes before fetch; strip or whitelist webhook.headers keys.

39

/api/email/send is an authenticated open relay

Security & Trust Boundaries

highOpenModerate effortHigh blast radius

app/api/email/send/route.ts verifies a Bearer session, then sends to whatever to address the client supplies with a client-chosen template and data payload. No recipient allowlist (company members, own address), no role gate — any logged-in account can drive Resend as a spam/phishing relay under FileDisplay's sending domain.

app/api/email/send/route.ts

Fix risk: Recipient allowlist may block legitimate report-notification flows that email arbitrary addresses today (picker, AI summary, agent).

Fix effort: Add recipient allowlist (company members or own email), template whitelist, and optional admin role gate; ~1 route file + policy helper.

40

/api/email/send will email anyone, from any logged-in user

Security & Trust Boundaries

highOpenModerate effortHigh blast radius

The route verifies a Bearer session, then sends to whatever `to` address the client supplies, with a client-chosen template and data payload. There's no recipient allowlist (own company members, own address) and no role gate — any account can drive the Resend integration as a spam/phishing relay under FileDisplay's sending domain. Rate limiting alone doesn't close this.

Fix risk: Duplicate — notification triggers may depend on unrestricted to: field.

Fix effort: Same as open-relay fix above.

41

/api/notifications/trigger accepts forged support/mention/project events

Security & Trust Boundaries

highOpenModerate effort

app/api/notifications/trigger/route.ts lets any authenticated company member POST arbitrary event + data. For support_reply, mention and project_update it emails victim UUIDs from client-supplied data.userId / data.mentionedUserId with no admin role check or proof the event occurred. notifySupportReply etc. exist in trigger.ts but have zero callers outside the route.

app/api/notifications/trigger/route.ts

Fix effort: Stop trusting client event payloads: verify rows exist server-side, require admin for support_reply, derive mentionedUserId from DB — route rewrite ~50–80 lines.

42

Accepting an appointment never checks for slot conflicts

Window, Sales & Kiosk Scheduling

highOpenModerate effort

POST /api/public/window-appointments blocks only when an accepted row exists at date+time (~229–242). decideWillCallAppointment() flips pending→accepted with no second check for another accepted row at the same appt_date+appt_time. Multiple pending requests per slot are allowed; two staffers accepting two pendings double-books. Migration 133 indexes (company_id, appt_date, appt_time) but has no partial UNIQUE WHERE status='accepted'.

Fix effort: Check for existing accepted row in decideWillCallAppointment; add partial UNIQUE index on slot.

43

Admin mention replies insert into a chat table the UI never reads

Dead Code & Orphan Features

highOpenModerate effort

/api/admin/reply (app/api/admin/reply/route.ts:95–104) inserts into legacy team_chat_messages after updating team_mentions.admin_reply. Live /team-chat reads chat_rooms / chat_room_messages only. Admin replies show on dashboard/account mention cards but the parallel chat-row insert is invisible — split-brain between two table families, ~50 lines of dead insert path per reply.

app/api/admin/reply/route.ts

Fix effort: Stop inserting team_chat_messages from admin/reply or migrate replies into chat_room_messages the UI reads.

44

Agent queryCalendarEvents bypasses calendar visibility rules

Agent, Collaboration & Platform APIs

highOpenModerate effort

lib/agent/tools.ts queryCalendarEvents (~805–820) uses the service-role client and selects all calendar_events for a company_id with no visibility, created_by, or participant filter (filterUserId is optional and agent-chosen). UI RLS in supabase/migrations/091_calendar_visibility_public.sql hides coworkers' personal events from viewers — the agent tool does not. Any agent-enabled user can ask for private event titles, descriptions, locations and links.

lib/agent/tools.tssupabase/migrations/091_calendar_visibility_public.sql

Fix effort: Mirror calendar RLS in queryCalendarEvents — filter visibility/participants before service-role select; ~1 file in lib/agent/tools.ts.

45

Agent queryDocuments lists private and DM-shared documents

Agent, Collaboration & Platform APIs

highOpenModerate effort

lib/agent/tools.ts queryDocuments (~678–683) selects all shared_documents for a company via service role with no sharing_mode, created_by, or shared_with_user_id filter. Bypasses document-sharing RLS from migrations 027 and 055. Agent can enumerate DM-only and private docs a viewer cannot open in /documents.

lib/agent/tools.ts

Fix effort: Apply sharing_mode + shared_with_user_id filters in queryDocuments using caller userId — match /documents page query logic.

46

Agent queryTeamChat returns all company messages with emails

Agent, Collaboration & Platform APIs

highOpenModerate effort

lib/agent/tools.ts queryTeamChat (~864–881) loads every chat_rooms row for the company, then all matching chat_room_messages including user_email — no room-membership check. Any agent-enabled viewer can pull company-wide chat history via a single tool call, including DMs if stored in shared room tables.

lib/agent/tools.ts

Fix effort: Restrict queryTeamChat to rooms the caller is a member of; redact user_email for non-admin roles.

47

Agent sendEmail tool has no recipient allowlist

Security & Trust Boundaries

highOpenModerate effortHigh blast radius

lib/agent/tools.ts sendEmailNotification() sends to whatever to address the model supplies. Guard is only userRole !== 'viewer', and v1 agent routes hardcode userRole: 'admin' — a second open-relay path beside /api/email/send with no company-member allowlist or template whitelist.

lib/agent/tools.ts

Fix risk: Tightening agent sendEmail breaks autonomous digest prompts that email external stakeholders.

Fix effort: Add company-member allowlist + template whitelist in sendEmailNotification; tie v1 agent role to API key creator's profile role.

48

Analytics-log exclude sync misses mismatched tool_name keys

Greco Warehouse Operations

highOpenModerate effort

PATCH /api/greco/analytics-log mirrors excluded flags into report tables via TOOL_TABLE_MAP[row.tool_name] (~116). Truck Errors page logs tool_name 'Returns' but the map key is 'Truck Errors' — exclude toggles update team_activity_log only, not truck_errors_reports. Same class for 'Damages' vs 'Damages Tool', 'Weekly Returns' vs 'Returns Intel', and tools absent from the map (Transfers, Routing, Thruput…). Operators think they hid a report from Daily Stats; the underlying report row stays visible.

Fix effort: Alias map Returns→Truck Errors, Damages→Damages Tool, etc.; extend TOOL_TABLE_MAP to all logged tools.

49

Anomaly detection only runs on Forklift saves

Greco Warehouse Operations

highOpenModerate effort

grep detectAndNotifyAnomalies across the repo finds only app/forklift/page.tsx (~724) as a caller. Picker, Loader, Shorts, Returns and the rest call logReportGeneration / notifyReportGenerated but never extract metrics or run detectAndNotifyAnomalies — so warehouse anomaly alerts and baselines are Forklift-only despite lib/supabase/anomalies.ts exporting extractPickerMetrics and extractLoaderMetrics with zero imports.

app/forklift/page.tsxlib/supabase/anomalies.ts

Fix effort: Wire detectAndNotifyAnomalies + extract*Metrics into picker/loader/shorts save paths; share one canonical metrics module.

50

Auto-Summarize New Reports toggle does not hook uploads

Agent, API & Docs Integrity

highOpenModerate effortMay affect neighbors

AgentSettingsPanel (~592–593) promises 'Automatically generate AI insights for newly uploaded reports.' Grep: auto_summarize is read only in app/api/cron/agent-autonomy/route.ts (~35–36), which enqueues a generic digest agent_task — same path as auto_email_digest, not per-upload. No forklift/picker/shorts save path reads the flag. Toggle implies per-report AI; cron delivers a periodic digest stub.

app/api/cron/agent-autonomy/route.ts

Fix risk: Wiring toggle to uploads suddenly enables AI spend and emails users thought were decorative — product surprise.

Fix effort: Call summarize hook from saveReport paths or rename toggle to match periodic digest behavior.

51

Chat and document uploads use public storage buckets

Agent, API & Docs Integrity

highOpenModerate effortHigh blast radius

supabase/migrations/043_add_file_attachments.sql creates chat-attachments and document-images buckets with public: true (~31–41). lib/supabase/chat-rooms.ts (~327–339) and documents.ts upload then getPublicUrl(). Public buckets serve files to anyone holding the URL regardless of authenticated SELECT policies — multi-tenant attachment leakage if links spread outside the company.

supabase/migrations/043_add_file_attachments.sqllib/supabase/chat-rooms.ts

Fix risk: Moving to private buckets breaks existing public URLs embedded in chat history and document shares.

Fix effort: Migrate buckets to private + signed URLs in chat-rooms.ts and documents.ts upload paths.

52

Daily Items fetchAllPoDetails has no PO count cap or parallel throttle

Kiosk Surfaces, Greco Tools & Platform Polish

highOpenModerate effort

app/tools/greco/lib/items-engine.ts fetchAllPoDetails (~1128–1149) sequentially fetches every unique PURNO via /api/entree/query?report=po-doc. A busy DockSheet day with 40–80 POs issues dozens of bridge ODBC round-trips per PDF click with no max-PO guard, timeout UX, or concurrency limit — can stall the browser and stack bridge jobs.

app/tools/greco/lib/items-engine.ts

Fix effort: Cap max POs (e.g. 30), batch with concurrency limit, show progress/cancel — items-engine.ts + UI.

53

Dashboard mount fires parallel loaders on every visit and every 30s

Monolith Files & Bundle Weight

highOpenModerate effortMay affect neighbors

On mount, dashboard/page.tsx (~446–449) calls loadDashboardData(), loadAllTabData() and loadAdminHiddenTabs() together. loadAllTabData (~432–442) kicks off 10 parallel fetches (user files ×2, org members, team activity, mentions ×2, support unread, documents, AI conversations, custom cards, custom tools) regardless of which tab is visible. A 30s interval (~498–505) and visibility handler (~509–516) re-run loadAllTabData() — background tab churn even when the user only wants the tool grid.

Fix risk: Changing refresh intervals trades server load for stale kiosk data operators may rely on.

Fix effort: Lazy-fetch per active tab; gate 30s/visibility refresh to visible tab; combine related Supabase calls — ~1 day, medium regression risk.

54

entree-bridge.js (~588 KB) is a root-level monolith beside the Next.js app

Repository Structure Audit (Pass 23)

highOpenModerate effortHigh blast radius

Single 601 KB entree-bridge.js sits at /workspace root with start-shanetree-autoprint.bat and SumatraPDF.exe — three bridge-PC artifacts sharing top billing with README and package.json. No bridge/ or ops/ namespace separates warehouse runtime from Vercel app code. New contributors cannot tell which files deploy to Vercel vs which install on the ODBC PC. Move to bridge/entree-bridge.js (or packages/bridge) with a one-line README pointer.

Fix risk: Large single-file page — extracting or refactoring risks breaking embedded business logic.

Fix effort: Move to bridge/entree-bridge.js; update bat/ps1 paths and README; keep out of Vercel build context.

55

Geronimo parses uploads with vulnerable npm xlsx on the client

Kiosk Surfaces, Greco Tools & Platform Polish

highOpenModerate effortHigh blast radius

app/geronimo/GeronimoClient.tsx (~240) await import('xlsx') on user-supplied BFC Dakota files. Frozen xlsx@0.18.5 CVE is tracked generically — Geronimo is another client-side surface still using the vulnerable parser for warehouse slot data instead of exceljs.

app/geronimo/GeronimoClient.tsx

Fix risk: Spreadsheet parser changes affect every upload tool — cell types and dates may shift.

Fix effort: Switch GeronimoClient to exceljs parse path — same migration as meat-counts/forklift xlsx CVE fix.

56

Greco guard verdict cache can render tools without a live session

Auth & Session

highOpenModerate effort

app/components/GrecoOnlyGuard.tsx useGrecoCheck() optimistically sets status='allowed' when fd_greco_guard_verdict_v1 is in localStorage, then returns early on missing session without downgrading if cache says allowed (lines 101–139). Deliberate warehouse UX trade-off (RLS still protects data), but /inbox then hits if (!user) return and never sets ready — infinite FancyLoader on shared PCs after the previous operator's cache.

app/components/GrecoOnlyGuard.tsx

Fix effort: Product trade-off: either clear cache on explicit sign-out only (partial), or downgrade cached-allowed when session truly absent after retries — touches warehouse UX on shared PCs.

57

Greco tool hiding only filters the dashboard grid

Greco Warehouse Operations

highOpenModerate effort

grecoDeniedTools from getMyGrecoCardDenials() is consulted only in dashboard/page.tsx (~1562) when rendering GRECO_TOOLS cards. GrecoOnlyGuard on each /forklift, /picker, /thruput route checks company_id === GRECO_COMPANY_ID only — it never reads greco_card_permissions. An operator denied 'Forklift' in /controls can still deep-link or bookmark the route and use the tool; hiding is cosmetic on the Tools tab.

Fix effort: Pass denied tool routes into GrecoOnlyGuard or shared hook; block direct URL access when tool:<route> denied.

58

greco_cindyvue_sessions has no migration

Scalability & Performance

highOpenModerate effortHigh blast radius

The DockSheet session table — arguably the most load-bearing table in the app — has no CREATE TABLE in supabase/migrations/, so its indexes and RLS aren't versioned or reviewable, and a fresh environment can't be reproduced from the repo. A snapshot migration (even retroactive) would fix that. A couple of composite indexes are also missing elsewhere (shanetree_pull_log and shanetree_snapshots order by created_at within a company without a matching composite).

supabase/migrations/

Fix risk: Same as versioned-migration gap — fresh env vs prod parity; altering live table affects all dock kiosks.

Fix effort: Same retroactive migration task.

59

greco_cindyvue_sessions has no versioned migration

Backend & Database

highOpenModerate effortHigh blast radius

grep 'create table.*greco_cindyvue_sessions' in supabase/migrations/ returns zero CREATE TABLE — only comments in 115_create_return_diffs_snapshots.sql and 129_create_driver_chat.sql reference the name. CindyVueClient.tsx (~4749–4763) and POST /api/greco/session assume the table exists. Fresh supabase db reset from repo migrations never creates it; indexes and RLS aren't reviewable in git history.

supabase/migrations/

Fix risk: Retroactive migration on a live JSONB table risks downtime; dock floor depends on this table every shift.

Fix effort: Retroactive CREATE TABLE migration from prod schema dump + document indexes/RLS — no app code change but needs careful SQL review.

60

Internal webhook trigger accepts forged report events from editors

Build, IDE & Custom Tools

highOpenModerate effort

app/api/internal/webhooks/trigger/route.ts (~80–87) allows founder/admin/editor roles. Body fields (tool_name, report_id, summary, ai_analysis) are client-supplied with no proof a report was generated. Any editor can enqueue signed webhook deliveries. Tracked rate-limit gap names the route; this is the forgery/trust angle parallel to /api/notifications/trigger.

app/api/internal/webhooks/trigger/route.ts

Fix effort: Verify report_id exists server-side before enqueue; restrict to service-role internal path only.

61

Member permission edit flags are saved but never enforced

Controls, Admin & Collaboration

highOpenModerate effortHigh blast radius

MemberPermissionsSection and account/page.tsx persist can_edit_email_settings, can_edit_ai_filters and can_edit_report_settings via lib/supabase/member-permissions.ts. getMyFullPermissions() returns canEditEmailSettings, canEditAIFilters and canEditReportSettings, but grep across app/ finds zero reads of those fields. Dashboard only consumes toolAccess (~1566); tool pages use canViewAnalytics/canViewAI only. Restricting 'Email Settings' in Controls has no effect anywhere.

lib/supabase/member-permissions.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Gate email/AI/report settings UI with canEdit* from getMyFullPermissions() on dashboard and tool pages.

62

member_permissions ALL_TOOLS lists 10 tools, Greco has 29

Permissions, Settings & Misleading UI

highOpenModerate effort

lib/supabase/member-permissions.ts ALL_TOOLS (~48–59) seeds only ten legacy analytics tools. lib/greco/greco-tools.ts registers 29 routes (Thruput, Driver Chat, Routing, Negative Slots, etc.). Controls cannot restrict access to tools outside the stale list — getDefaultToolAccess() only knows the ten names. Permission UI appears comprehensive but covers <35% of the warehouse grid.

lib/supabase/member-permissions.tslib/greco/greco-tools.ts

Fix effort: Generate ALL_TOOLS from GRECO_TOOLS + non-Greco tools; migrate existing permission rows.

63

Most warehouse tools stamp report_date in UTC, not warehouse local

Agent, API & Docs Integrity

highOpenModerate effortHigh blast radius

Passes 1–8 track Forklift processor only, but the same new Date().toISOString().split('T')[0] / .slice(0,10) pattern is verified in picker (~1167, ~2158), shorts (~3934), skips (~556, ~727, ~1149), replen (~567, ~856), loader (~343, ~872), returns (~911), truck-errors (~660, ~976), damages (~375), reconciliation (~344, ~772), returns-intel (~420, ~917), thruput (~177) and forklift page (~597, ~1209). Eastern evening uploads land on tomorrow's calendar day for leaderboards, filters and anomaly baselines across the grid.

Fix risk: Cross-tool date alignment touches picker, shorts, loader, forklift processors — partial fix leaves tools disagreeing on 'today'.

Fix effort: Shared warehouseLocalDate() helper; replace ~15 tool save paths (beyond forklift processor).

64

No test directory, typecheck script, Prettier, or EditorConfig

Repository Structure Audit (Pass 23)

highOpenModerate effort

package.json scripts: dev, build, start, lint only — no test, typecheck, or format. No test/, e2e/, vitest.config, playwright.config, .prettierrc, or .editorconfig. tsconfig strict is good, but repo offers no automated safety net before refactor. Pairs with EntreeClient 41k-line monolith — structure without tests reads as high-risk to external reviewers.

Fix effort: Add npm run typecheck, vitest scaffold, prettier + editorconfig — even smoke tests raise professionalism.

65

Only Forklift triggers UI webhook delivery on report complete

Data Flows & Wiring

highOpenModerate effort

lib/notifications/webhooks.ts notifyReportCompleted() is only called from app/forklift/page.tsx. Other warehouse tools (picker, shorts, skips, replen, etc.) call notifyReportGenerated() for email only — Developer-configured report.completed webhooks never fire for those tools.

lib/notifications/webhooks.tsapp/forklift/page.tsx

Fix effort: Call notifyReportCompleted/Failed from each warehouse tool save path (or centralize in notifyReportGenerated).

66

Public /timer downloads the full DockSheet roster before any PO filter

Kiosk Surfaces, Greco Tools & Platform Polish

highOpenModerate effort

app/timer/TimerClient.tsx loadSession (~327–333) fetches greco_cindyvue_sessions.list_data via unauthenticated GET /api/greco/session?date=…. Every saved appointment (vendor, carrier, dock, door, notes, times) lands in browser memory; only the search UI narrows to rows with PO#s (~437–440). World-readable session GET is tracked separately — this is timer-specific disclosure of the entire receiving schedule to anyone with the public link.

app/timer/TimerClient.tsx

Fix effort: Server-side PO-only projection endpoint for /timer, or auth-gate session GET — reduce public roster exposure.

67

Public driver-chat GET returns full threads without authentication

Shanetrée, Routing & Driver Comms

highOpenModerate effortCritical — test everything

GET /api/public/driver-chat?sessionDate=…&apptId=… (app/api/public/driver-chat/route.ts ~62–100) returns the full thread plus all messages with no auth — by design for /timer, but any holder of a dock appointment id can read driver↔Greco chat history. appt_id is a small integer from DockSheet rows, so enumeration across session dates is feasible. Separate from tracked public relay rate limits — this is an unauthenticated read disclosure.

app/api/public/driver-chat/route.ts

Fix risk: Driver kiosk /inbox public path depends on unauthenticated read — auth breaks driver workflow on shared phones.

Fix effort: Require kiosk token, signed appt token, or rate-limited short-lived read secret; document threat model.

68

Session AI routes skip the company AI kill switch

Security & Trust Boundaries

highOpenModerate effortHigh blast radius

checkCompanyAIAccess() in lib/ai/access-policy.ts is enforced on /api/v1/ai/chat and the IDE assistant but not on /api/agent/chat, ai-analyze, ai-summary, routing/ai-build, greco/analytics-chat, port/ai-summary or custom-tools/generate. The policy itself fails open on read errors (returns allowed: true at lines 41–44), so disabled companies or quota exhaustion may not block UI-driven AI calls.

lib/ai/access-policy.ts

Fix risk: Enforcing checkCompanyAIAccess() on UI AI routes may disable AI customers already rely on while policy read errors still fail open today.

Fix effort: Import checkCompanyAIAccess into ~7 session AI routes; also fix fail-open read error in access-policy.ts — repetitive but each route needs a quick test.

69

Seventeen Greco tool pages inject ExcelJS from cdnjs without SRI

Kiosk Surfaces, Greco Tools & Platform Polish

highOpenModerate effortMay affect neighbors

Only Driver Scan CDN load is tracked in pass 18. Same pattern in app/licenses, negative-slots, empty-slots, forklift, picker, shorts, skips, returns, truck-errors, replen, reconciliation, loader, damages, shrink-meeting, returns-intel, scan and build pages — all inject https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js with no integrity attribute. Dock-floor xlsx parsing depends on a third-party CDN across the warehouse suite.

app/licenses

Fix risk: Bundling ExcelJS locally increases tool page JS weight — tablets on slow Wi-Fi may timeout.

Fix effort: Bundle exceljs via npm in shared hook or add SRI to all 17 script injections — wide touch, test each tool export.

70

Shanetrée and auto-pull default every date picker to UTC today

Greco Warehouse Operations

highOpenModerate effortHigh blast radius

EntreeClient.tsx todayStr() (~545) returns new Date().toISOString().slice(0,10) and seeds dozens of modal date ranges (adjustments, routes, will-calls, venpic…). autoPullEngine.ts duplicates the same helper (~54–55) for daily-gate localStorage keys. ODBC pulls opened after 7–8pm Eastern default to tomorrow's calendar day before the operator touches the picker — mismatches dock-floor 'today' and CindyVue session_date.

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Shared warehouseLocalDateStr() in EntreeClient + autoPullEngine; replace toISOString().slice(0,10).

71

Shanetrée relay claim RPC has no company scope

Security & Trust Boundaries

highOpenModerate effort

supabase/migrations/123 shanetree_relay_claim() selects oldest pending rows globally with no company_id filter. Any bridge holding INTERNAL_API_SECRET can dequeue another tenant's ODBC relay jobs — multi-tenant isolation depends on there being only one bridge today.

supabase/migrations/123

Fix effort: Add company_id param to shanetree_relay_claim() RPC + filter pending rows; bridge must send tenant context.

72

Tab visibility toggles target dashboard tabs that no longer exist

Permissions, Settings & Misleading UI

highOpenModerate effortMay affect neighbors

Account ALL_DASHBOARD_TABS (~161–168) and Controls TabVisibilitySection (~15–23) let admins hide projects, tools, reports, members, chat, history and stats. Current dashboard tab strip uses overview, developer, build, tools, workspace, reports and godview — members/chat/history moved under Workspace or separate routes. Five of seven visibility toggles name tabs that are not in the tab bar anymore; admins think they hid Chat while /team-chat links remain.

Fix risk: Rewiring to Workspace folder changes which cards Controls hides — saved JSON keys may orphan.

Fix effort: Realign ALL_DASHBOARD_TABS / CONTROLLABLE_TABS to overview|developer|build|tools|workspace|reports|godview + workspace sub-routes.

73

Thruput actively saves to tables that were never created

Greco Warehouse Operations

highOpenModerate effortMay affect neighbors

app/thruput/page.tsx calls saveThruputWeek/saveThruputDay/getThruputWeeks/getThruputDays from lib/supabase/tool-reports.ts (~2075–2165), which upsert into thruput_days and thruput_weeks. supabase/migrations/132_create_thruput_tables.sql is an empty comment stub (tracked in Hygiene) — no CREATE TABLE ever ran. Operators can edit the throughput board for a session, but persistence either fails silently or depends on tables created outside the repo.

app/thruput/page.tsxlib/supabase/tool-reports.tssupabase/migrations/132_create_thruput_tables.sql

Fix risk: Running stub migration 132 on prod may conflict with hand-created tables or wrong alphabetical apply order.

Fix effort: Fill 132_create_thruput_tables.sql with thruput_days/weeks DDL + RLS; verify saveThruputDay/Week against fresh DB.

74

UI report emails ignore per-tool recipient lists

Data Flows & Wiring

highOpenModerate effort

Dashboard lets admins configure per-tool email recipients (report_email_recipients via getEmailRecipients in lib/supabase/report-email-recipients.ts), but UI-generated reports call notifyReportGenerated() → /api/notifications/trigger which loops every company member with only the global report_notifications opt-in. Per-tool recipient config is never read on the UI notification path.

lib/supabase/report-email-recipients.ts

Fix effort: Wire getEmailRecipients(toolName) into /api/notifications/trigger report_generated case instead of looping all members.

75

v1 agent endpoints hardcode admin role for every API key

Security & Trust Boundaries

highOpenModerate effortHigh blast radius

app/api/v1/agent/route.ts and agent/stream/route.ts pass userRole: 'admin' to runAgent/runAgentSync regardless of who created the key or their profile role. External API consumers get full agent tool permissions through any valid API key.

app/api/v1/agent/route.ts

Fix risk: Mapping API key to real role breaks integrators that depend on admin-level agent tools via any key.

Fix effort: Map API key creator's profile role (or scoped key permissions) into runAgent; may need new api_keys.permission_level column.

76

v1 Export API only supports 10 tools

Agent, API & Docs Integrity

highOpenModerate effort

TOOL_TABLE_MAP in app/api/v1/export/route.ts (~24–35) lists forklift, picker, replen, returns, shorts, skips, truck_errors, reconciliation, loader and returns_intel. lib/greco/greco-tools.ts registers 29 routes. Exporting damages, cyclecount, drivers, thruput, etc. returns 400 'Unknown tool.' OpenAPI /api/v1/export documents no such limitation — API contract overpromises warehouse coverage.

app/api/v1/export/route.tslib/greco/greco-tools.ts

Fix effort: Expand TOOL_TABLE_MAP to all report tables or document allowed tools in OpenAPI enum.

77

v1 POST /reports/analyze is an API-key email open relay

Public API, Jobs, Onboarding & Greco Gaps

highOpenModerate effortHigh blast radius

app/api/v1/reports/analyze/route.ts — send_email defaults true (~254); email_recipients accepts arbitrary addresses (~262–264); emails send via direct Resend fetch (~290–301) with no domain allowlist. Any API key with reports:write can email AI summaries to external addresses under the platform From domain.

app/api/v1/reports/analyze/route.ts

Fix risk: External API consumers may rely on emailing non-member addresses — contract change needs version bump.

Fix effort: Restrict email_recipients to company members or configured per-tool list; disallow arbitrary external addresses.

78

Weekly AI Digest Email toggle never sends email

Agent, API & Docs Integrity

highOpenModerate effortMay affect neighbors

AgentSettingsPanel (~608–609) says 'Send weekly summaries to configured recipients.' Cron runs every 10 minutes (vercel.json */10), not weekly. agent-autonomy/route.ts only changes the prompt string (~88–90); agent-task worker persists agent_tasks.output with no Resend/sendEmail call. digest_recipients is never read in cron or worker. Users enable weekly email; they get at most a background task row.

Fix risk: Implementing cron email may flood recipients who enabled toggle expecting silence — digest_recipients unused today.

Fix effort: Wire agent-task digest completion to Resend using digest_recipients; run cron weekly not */10.

79

Customer ID normalization differs per tool — no shared BFC ↔ Entrée crosswalk

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenHard / risky

Return Diffs uses normCust stripping leading zeros (EntreeClient.tsx ~20748). /reconciliation keys on raw customer strings from fixed xlsx column positions (~249, ~282). Returns Intel keys on BFC Client ID. Employee names on BFC forklift/picker exports have no mapping to Entrée SECID. Enterprise software needs lib/identifiers/customer.ts (and employee.ts) consumed by every join — today each tool reinvents string cleanup.

lib/identifiers/customer.ts

Fix effort: lib/identifiers/customer.ts + employee.ts with normCust, bfcClientToCustno, bfcNameToSecid — wide refactor across join sites.

80

DockSheet session autosave is last-writer-wins per date

Greco Warehouse Operations

highOpenHard / riskyHigh blast radius

POST /api/greco/session upserts greco_cindyvue_sessions on onConflict: 'session_date' only (~176–186) — no row-level version or editor lock. CindyVueClient runs a 30-second autosave setInterval (~1389–1398) calling syncToBackendRef. Two receivers editing the same session_date overwrite list_data/grid_data wholesale; combined with world-readable GET (tracked elsewhere) any kiosk can also clobber in-flight edits.

Fix risk: Adding versioning/locks changes autosave semantics; two receivers editing same day is common — naive fix can block saves or duplicate rows.

Fix effort: Optimistic locking (updated_at check), editor presence, or merge strategy before upsert on session_date.

81

Dual permission stacks use incompatible keys

Controls, Admin & Collaboration

highOpenHard / riskyHigh blast radius

/controls Greco Tools matrix (ToolsAccessSection.tsx ~12–16) keys denials by route (/forklift, /picker) in greco_card_permissions. Legacy Member Permissions keys by display name ('Forklift Productivity') in member_permissions.tool_access. Same member can be denied in one system and allowed in the other. Distinct from tracked ALL_TOOLS 10-vs-29 completeness gap — both systems are live with different namespaces.

Fix risk: Merging Controls matrix with greco_card_permissions changes which gate wins — latent denies or grants flip.

Fix effort: Unify on route keys everywhere or map display names ↔ routes in one permission service.

82

Dual picker pipelines — BFC /picker and Window Picker Production never meet

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenHard / riskyHigh blast radius

app/picker/page.tsx is BFC EPM picker-report upload only — grep finds zero entree/ODBC/picker-report references. Window Picker Production modal pulls live /picker-report via ODBC and autoPullEngine writes pickerProduction snapshots to shanetree_snapshots (~298–300). picker_reports (BFC metrics) and shanetree_snapshots (ODBC production) sit in separate buckets on Daily Stats with no merge, employee crosswalk, or deep-link between the two surfaces.

app/picker/page.tsx

Fix risk: Unifying pipelines touches Window ODBC path and BFC picker_reports schema — operators use both independently today.

Fix effort: Unified picker analytics: merge picker_reports + pickerProduction snapshots on employee crosswalk; add ODBC fetch option to /picker or deep-link from dashboard.

83

greco_daily_stats has no CREATE TABLE migration

Greco Warehouse Operations

highOpenHard / risky

lib/supabase/tool-reports.ts upsertDailyStat/getDailyStats and app/api/cron/greco-digest/route.ts read greco_daily_stats extensively; lib/agent/greco-tools.ts treats it as a first-class agent query target. Grep greco_daily_stats in supabase/migrations/ returns zero CREATE statements — schema exists only in hosted Supabase or manual DDL. Fresh environments cannot reproduce Daily Stats, dockvoo rollups or the digest cron from migrations alone.

lib/supabase/tool-reports.tsapp/api/cron/greco-digest/route.tslib/agent/greco-tools.tssupabase/migrations/

Fix effort: Author migration with company_id, RLS, indexes and dockvoo JSONB columns matching tool-reports.ts; backfill from hosted schema.

84

History page fetches everything in 'All' mode

Performance & Scale

highOpenHard / riskyHigh blast radius

app/history/page.tsx (~177) calls getUnifiedHistory(0) when loadLimit is 'all'. lib/supabase/unified-history.ts (~217) documents limit<=0 as intentional zero-cap across four sources: team_activity_log (getTeamActivity), DockSheet dock events (greco session API), shanetree_print_log (listPrintLog), shanetree_pull_log (listPullLog) — merged in browser newest-first. handleExportExcel (~175–177) also forces getUnifiedHistory(0) even when UI shows 50 rows. At warehouse volume → multi-megabyte fetch and frozen tab.

app/history/page.tsxlib/supabase/unified-history.ts

Fix risk: Same unbounded ledger fetch — export and deep-link flows depend on zero cap.

Fix effort: Server-side cursor pagination across four merged sources in unified-history.ts + history page UX — non-trivial merge semantics.

85

Pass 23 structure audit — repo layout undermines a clean, professional first impression

Repository Structure Audit (Pass 23)

highOpenHard / risky

Layout pass on Jul 2026 checkout: ~11 MB SumatraPDF.exe + 588 KB entree-bridge.js + Windows kiosk scripts committed at repo root; 79 flat app/ routes with no domain grouping; dual component trees (components/ vs app/components/); 43 flat lib/supabase modules; four nested app/**/lib/ folders beside top-level lib/; seven scattered root markdown guides with no docs/ directory; no .github/ CI or templates; no test/, typecheck script, Prettier, or EditorConfig; supabase/migrations/README still documents migrations 001–011 while 150 SQL files exist; misleading ALL_MIGRATIONS_COMBINED.sql (11 migrations only). Clone size, onboarding friction, and 'is this a product or a bridge PC dump?' signals stack up before a reviewer reads application code.

app/components/lib/supabasesupabase/migrations/README

Fix effort: Phased restructure: bridge/ ops bundle, docs/ hub, .github CI, delete SumatraPDF from git, route groups — multi-PR layout pass.

86

PO and invoice namespaces don't unify across WMS exports and Entrée

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenHard / risky

Reconciliation normalizes 7-digit POs with SS/RMR/FPD rules (app/reconciliation/page.tsx ~189–201). Daily Items/DockSheet use Greco PURNO matching ^6\d{5}$ (shanetree-whitepaper glossary ~163). BFC returns use Cust Order as original invoice, not PURNO. Return Diffs parses ORIG_INVNO from credit-memo PONUM/REFNO. No lib/identifiers/po.ts resolver spans all three namespaces — enterprise merge needs one canonical PO/invoice crosswalk.

app/reconciliation/page.tsxlib/identifiers/po.ts

Fix effort: lib/identifiers/po.ts with normalizePo(), parseOrigInvno(), grecoPurno() — consumed by reconciliation, Return Diffs, Daily Items, DockSheet.

87

Public /t/{share_id} tool data is world-writable via share_id

Build, IDE & Custom Tools

highOpenHard / risky

GET/POST /api/public-tools/data authenticate only by share_id (no user token). Anyone with a published link can read all keys (key=_all) and upsert up to 1MB per key into custom_tool_data for that tenant via service role. Enables data poisoning, PII stuffing and storage-cost abuse against public tools wired through app/t/[id]/page.tsx.

app/t/

Fix effort: Require signed write tokens, rate-limit writes harder, or separate read-only public data plane.

88

Return Diffs three-way reconcile works — /reconciliation uses a different join model

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenHard / risky

Window Return Diffs (EntreeClient.tsx ~9890–9903) matches BFC returns xlsx (drag-drop) to live /return-diffs ODBC credit memos on (ORIG_INVNO, item) with (custNo, item) fallback; snapshots land in return_diffs_snapshots. Standalone /reconciliation instead joins two manual xlsx files on customer|item (app/reconciliation/page.tsx ~257–305) with different PO normalization rules (~189–201). Operators must pick one reconciliation philosophy; neither tool reads the other's output or snapshots.

app/reconciliation/page.tsx

Fix effort: Deprecate /reconciliation or redirect to Window Return Diffs join keys; migrate customer|item users to ORIG_INVNO|item model — operator training + data parity testing.

89

Shanetrée panel imports are all eager — ~20k lines ship on first paint

Monolith Files & Bundle Weight

highOpenHard / risky

EntreeClient.tsx statically imports DiamondReportPanel (~2,532 lines, recharts), InvoicePanel, CustomerPanel, MapPanel, ProducePanel, StockVooPanel, SalesDistributionPanel (~recharts), PullLogPanel, BridgeRelayPanel, PortAISummaryModal, and more — none behind dynamic(). Roughly half the Entree file count is pulled in before the operator taps a tile. Splitting panels behind dynamic(() => import(...), { ssr: false }) per modal would cut initial JS substantially.

Fix effort: Wrap each panel in next/dynamic() with ssr:false — ~15 imports in EntreeClient.tsx; test every modal open path on relay and full modes.

90

The history page fetches everything, forever

Scalability & Performance

highOpenHard / riskyHigh blast radius

/history's “All” mode calls getUnifiedHistory(0), which deliberately applies no .limit() across four sources (team_activity_log, dock events, print log, pull log) and merges them in the browser. Fine today; at a year or two of warehouse volume it becomes a multi-megabyte fetch and a frozen tab. Needs server-side pagination or a cursor — same pattern exists in getTeamActivity, listPullLog, and the session ?list=history&limit=0 API path.

Fix risk: Capping getUnifiedHistory breaks 'Download entire history' contract and deep-link highlight that forces load-all.

Fix effort: Same pagination refactor.

91

The npm xlsx package is frozen at a vulnerable version

Dependencies & Build Hygiene

highOpenHard / riskyHigh blast radius

package.json pins xlsx@^0.18.5 — the last version SheetJS ever published to npm, affected by CVE-2023-30533 (prototype pollution when reading crafted spreadsheets). Patched builds exist only on the SheetJS CDN. It's used server-side in ai-summary and the forklift processor and client-side in meat-counts — all parsing user-supplied files. Migrate those call sites to exceljs (already installed) or the CDN build.

Fix risk: exceljs migration changes parsed cell types and date serials across forklift, meat-counts, ai-summary — all user uploads need regression pass.

Fix effort: Same exceljs migration.

92

TOOL_REGISTRY covers 10 tools — /scan and API miss 19+ Greco routes

Warehouse Tool Coverage, Monitoring & Collaboration

highOpenHard / risky

lib/tools/registry.ts ends at 10 entries (forklift through returns_intel). Missing damages, shrink-meeting, transfers, cyclecount, drivers, thruput, licenses, geronimo, empty_slots, etc. app/scan/page.tsx and v1 tool-detection cannot route uploads to those tools. Distinct from tracked /scan re-implementation — even a fixed detector cannot target unregistered tools.

lib/tools/registry.tsapp/scan/page.tsx

Fix effort: Register remaining Greco tools in lib/tools/registry.ts with column signatures — wide effort, enables scan routing.

93

Warehouse tool names drift across cards, permissions and activity log

Greco Warehouse Operations

highOpenHard / riskyMay affect neighbors

Three naming layers disagree: lib/greco/greco-tools.ts card 'Returns' (route /truck-errors) vs member-permissions ALL_TOOLS 'Truck Errors' vs logReportGeneration('Returns') in truck-errors/page.tsx (~733). 'Damages' card vs analytics-log TOOL_TABLE_MAP key 'Damages Tool'. 'Weekly Returns' card vs logReportGeneration('Weekly Returns') vs TOOL_TABLE_MAP 'Returns Intel' vs returns-intel save toolName 'Returns Intel'. Returns+ logs both 'Returns+' and 'Returns Tool'. Filters, permission matrices and analytics-log PATCH mirroring cannot stay aligned without a single canonical tool_name registry.

lib/greco/greco-tools.ts

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Single TOOL_NAME registry consumed by greco-tools, logReportGeneration, analytics-log TOOL_TABLE_MAP and member-permissions.

94

xlsx@0.18.5 is frozen at a vulnerable npm release

Code Quality & Hygiene

highOpenHard / riskyHigh blast radius

package.json pins xlsx@^0.18.5 — last SheetJS npm publish, affected by prototype-pollution CVEs on crafted spreadsheets. Patched builds exist only on SheetJS CDN. Used server-side (ai-summary, forklift) and client-side (meat-counts) parsing user files. exceljs is already installed as alternative.

Fix risk: Same CVE pin — SheetJS API differences break header detection if rushed.

Fix effort: Migrate every xlsx call site to exceljs (already installed): forklift processor, ai-summary, meat-counts, any others — API differences, regression-test all upload flows.

95

.env.local is committed to the repo — with a live secret inside

Security & Trust Boundaries

highOpenMajor refactor

.gitignore whitelists .env.local (“Safe because this GitHub repo is PRIVATE”), verify-architecture.ps1 actively fails unless it's tracked, and the file contains a real 64-char INTERNAL_API_SECRET used by /api/internal/*, tool generation and the bridge relay. That's a deliberate trade-off for the SentinelOne-wipes-the-bridge-PC problem, but one repo-visibility mistake, fork, or leaked clone exposes service-to-service auth. The secret should live in Vercel/bridge env stores and be rotated.

Fix effort: Same ops overhaul as above.

96

.env.local is whitelisted in .gitignore and lives in the repo

Security & Trust Boundaries

highOpenMajor refactor

.gitignore lines 37–41 explicitly un-ignore .env.local ('Safe because this GitHub repo is PRIVATE'). The file is present in the checkout and carries live secrets (INTERNAL_API_SECRET for /api/internal/*, tool generation, bridge relay). One visibility change, fork, or leaked clone exposes service-to-service auth. Secrets belong in Vercel/bridge env stores with rotation, not version control.

Fix effort: Ops/process change: rotate INTERNAL_API_SECRET, move secrets to Vercel + bridge env, un-whitelist .gitignore, update verify-architecture.ps1 — not just a code diff.

97

All WMS↔ERP joins stop at read-only snapshots — no write-back to BFC or Entrée

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenMajor refactor

lib/shanetree/shanetree-whitepaper.ts dataBoundaries (~99–123): Window never streams live ODBC to the internet; snapshots are point-in-time sign-off. whitepaper-unified.ts Return Diffs bullets say weekly reconciliation 'without writing back to Entrée.' BFC has no API hook at all. Low Slots PICK lists, Return Diffs resolutions, and Shorts comparisons inform operators but cannot close the loop in either system — enterprise merge eventually needs governed write-back or at least export formats both systems ingest.

lib/shanetree/shanetree-whitepaper.ts

Fix effort: Product decision + bridge work: governed export formats, Entrée adjustment import path, or BFC API if available — months, not a code-only fix.

98

BFC (WMS) and Entrée (ERP) are two parallel data planes with no shared spine

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenMajor refactor

Greco warehouse tools overwhelmingly ingest manual BFC xlsx exports (documented per-tool in app/docs/page.tsx 'How to Pull This from BFC'). Shanetrée/Window reaches Entrée only via entree-bridge.js ODBC (/api/entree/query). There is no scheduled BFC ingest, no item-master sync, and no canonical crosswalk table linking BFC export schemas to Entrée tables (arinvt01, arslot, arcusto). Enterprise merge requires a shared identifier spine — today every join is bespoke per tool.

app/docs/page.tsx

Fix effort: Platform architecture: lib/identifiers registry, BFC ingest layer, Entrée ODBC facade, and join contracts — weeks of product + schema design before tool-by-tool wiring.

99

ensure_user_profile auto-creates companies for any email domain

Public API, Jobs, Onboarding & Greco Gaps

highOpenMajor refactor

supabase/migrations/014_update_ensure_user_profile.sql (~47–61) — unrecognized domains get a new companies row (INSERT INTO companies (name, domain)). Any signup with @gmail.com, @competitor.com, etc. spawns a tenant and makes the first user founder. Enterprise SaaS expects allowlisted domains or invite-only onboarding, not auto-provisioning.

supabase/migrations/014_update_ensure_user_profile.sql

Fix effort: Product decision: domain allowlist, invite-only signup, or pending-approval state — migration + signup flow change.

100

EntreeClient.tsx is a ~42k-line client bundle with no code splitting

Monolith Files & Bundle Weight

highOpenMajor refactorCritical — test everything

app/entree/EntreeClient.tsx is 41,917 lines — one 'use client' component holding Shanetrée routing, ODBC pulls, will-calls, print queues, relay mode, and a dozen modals. No next/dynamic() panel imports; ExcelJS/xlsx are dynamically imported at call sites but the component tree itself is monolithic. Every /entree visit parses and hydrates the full module before the home grid renders.

app/entree/EntreeClient.tsx

Fix risk: Code-splitting or moving hooks changes /port, /noe, /adrian kiosk mounts that import the full chunk — regression surface is the entire ERP UI.

Fix effort: Architectural decomposition: extract panels/routes into lazy chunks — weeks of careful work with high regression risk on dock-floor Shanetrée.

101

Five job worker routes are mapped but missing

Dead Code & Orphan Features

highOpenMajor refactor

lib/jobs/service.ts WORKER_ROUTES lists report_processing → /api/jobs/workers/report, embedding_generation → /embedding, scheduled_report → /scheduled-report, data_export → /export and cleanup → /cleanup. Glob finds only six workers (email, webhook, batch-email, anomaly, ai-analysis, agent-task). v1 export and schedules enqueue data_export / scheduled_report jobs that will 404; /api/jobs/enqueue allows embedding_generation and report_processing with the same fate. Broken async surface for API consumers.

lib/jobs/service.ts

Fix effort: Implement five worker route handlers or remove job types from WORKER_ROUTES, enqueue allowlist and v1 export/schedules.

102

job_schedules rows are never executed

Agent, API & Docs Integrity

highOpenMajor refactor

POST /api/v1/schedules inserts into job_schedules with next_run_at, cron_expression and job_type scheduled_report (app/api/v1/schedules/route.ts ~162–177). Grep finds only the v1 schedules routes touch job_schedules — vercel.json has no cron that polls due schedules. Combined with the missing /api/jobs/workers/scheduled-report worker (pass 7), the Schedules API is write-only: API consumers can create schedules that never fire.

app/api/v1/schedules/route.ts

Fix effort: Add cron to poll due job_schedules + implement scheduled-report worker, or remove v1 schedules API.

103

Low Slots + Dakota merge is the gold standard — most tools don't reach that bar

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenMajor refactorHigh blast radius

Window Low Slots pulls Entrée demand via bridge /low-slots (artrans/arytran/arslot + arinvt01 on-hand) and optionally merges BFC Dakota Slot Listing (R08921s) in-browser (lib/lowslots/dakota-merge.ts). PICK list exports save to shanetree_snapshots (modal_key lowSlotsPickList); Shorts reads those snapshots for next-morning comparison. This ODBC+demand+BFC-physical+snapshot pattern is exactly what enterprise WMS↔ERP intimacy looks like — but Forklift, Picker, Replen, Returns Intel, Transfers and most other Greco tools are upload-and-forget BFC analytics with zero Entrée join.

lib/lowslots/dakota-merge.ts

Fix risk: Extending Dakota merge pattern to other tools requires EntreeClient coupling — partial port breaks parity.

Fix effort: Replicate ODBC+demand+BFC-physical+snapshot pattern across Forklift/Picker/Replen/etc. — per-tool ODBC endpoints + snapshot keys; use Low Slots as template.

104

No explicit enterprise-merge roadmap — integration is opportunistic per tool

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

highOpenMajor refactor

lib/shanetree/shanetree-whitepaper.ts and app/docs/page.tsx document individual tools well. SHANETREE_REMOTE_SETUP.md covers bridge tunnels only. app/developer/page.tsx mentions 'WMS/ERP integrations' via API keys but implements no unified merge layer. Started wins (Low Slots+Dakota, Return Diffs, Shorts↔PICK) prove the pattern works — what's missing is a platform plan: identifier registry, shared join library, ODBC+BFC ingestion matrix per Greco tool, and deprecation path for duplicate reconcilers (/reconciliation vs Return Diffs, /picker vs Picker Production).

lib/shanetree/shanetree-whitepaper.tsapp/docs/page.tsxapp/developer/page.tsx

Fix effort: Document merge matrix (tool × BFC input × ODBC endpoint × snapshot × write-back) and deprecation list — product/engineering planning artifact.

M

Medium severity

320
105

/api/admin/projects has zero callers (~150 lines)

Agent, API & Docs Integrity

mediumOpenTrivial fix

grep '/api/admin/projects' across app code finds only app/api/admin/projects/route.ts. Dashboard loads quotes via /api/admin/submissions (~1387); quotes-admin uses direct Supabase. Orphan admin route with project-message join logic maintained beside the live submission path.

app/api/admin/projects/route.ts

Fix effort: Delete route or wire dashboard projects tab to it instead of submissions-only path.

106

/api/internal/webhooks/trigger has no rate limit

API & Operations

mediumOpenTrivial fix

Only app/forklift/page.tsx calls notifyReportCompleted() today; when invoked, /api/internal/webhooks/trigger has no withRateLimit (unlike /api/notifications/trigger). A buggy Forklift loop or future wider adoption could enqueue unbounded webhook delivery jobs.

app/forklift/page.tsx

Fix effort: Wrap with withRateLimit like notifications/trigger.

107

/api/public/will-call-items has no callers (~108 lines)

Permissions, Settings & Misleading UI

mediumOpenTrivial fix

Route exists with bridge proxy logic and .env.example still lists it as a live proxy, but grep finds no fetch('/api/public/will-call-items') in app code. /sales loads line items via /api/public/sales-relay (SalesClient.tsx ~878+). Comment in sales still references will-call-items 'when missing' but code path never calls it — stale docs + dead route.

Fix effort: Delete route and remove from .env.example / protected-paths, or wire SalesClient fallback.

108

/noe and /adrian kiosks are absent from operational navigation

Agent, API & Docs Integrity

mediumOpenTrivial fix

Public will-calls (/noe) and routes/vendor-pickups (/adrian) kiosks are documented in /docs and ArchitectureDiagram but grep finds no entries in lib/greco/greco-tools.ts GRECO_TOOLS cards. Same discoverability class as tracked /port gap — floor staff must know URLs; no dashboard or tools-grid entry for two heavily documented public kiosks.

lib/greco/greco-tools.ts

Fix effort: Add GRECO_TOOLS cards or Workspace kiosk folder linking /noe and /adrian.

109

/quotes-admin analytics cards show company metrics as platform totals

Controls, Admin & Collaboration

mediumOpenTrivial fix

loadStats (~162–166) counts profiles and companies via user-scoped Supabase client — RLS returns one company. Cards labeled like platform-wide signups/client IDs are actually company-scoped counts on a page styled as global admin.

Fix effort: Relabel stat cards or fetch via GodView service-role endpoint.

110

132_create_thruput_tables.sql is an empty stub

Backend & Database

mediumOpenTrivial fix

supabase/migrations/132_create_thruput_tables.sql is a single-line comment — no CREATE TABLE. Prefix 132 collides with 132_add_item_alert_stages.sql (alphabetical apply order picks _add_ before _create_). app/thruput/page.tsx + lib/supabase/tool-reports.ts saveThruputWeek/Day (~2075–2165) target thruput_days/thruput_weeks. Fresh migration runs never create those tables; throughput board edits fail or rely on hand-run SQL outside repo history.

supabase/migrations/132_create_thruput_tables.sqlapp/thruput/page.tsxlib/supabase/tool-reports.ts

Fix effort: Delete stub or fill with real CREATE TABLE; resolve duplicate 132 prefix.

111

Activity-log cleanup archives at most 10,000 rows per day

Agent, API & Docs Integrity

mediumOpenTrivial fix

app/api/cron/activity-log-cleanup/route.ts ~42 uses .limit(10_000) per daily run. If more than 10k rows exceed the 365-day cutoff, remainder waits for subsequent days with no overflow logging in the JSON response. High-volume warehouse tenants can leave a growing tail of expired activity.

app/api/cron/activity-log-cleanup/route.ts

Fix effort: Loop until batch empty or log remaining count in cron JSON response.

112

ai-usage-rollup cron ignores upsert failures

API & Operations

mediumOpenTrivial fix

app/api/cron/ai-usage-rollup/route.ts monthly rollup loop awaits ai_usage_monthly_rollups upsert without checking error. Returns { success: true, companiesRolledUp: N } even when individual upserts fail — GodView AI usage dashboards can drift from raw ai_usage_logs.

app/api/cron/ai-usage-rollup/route.ts

Fix effort: Check upsert error per company; increment failed count in response.

113

ALL_MIGRATIONS_COMBINED.sql is misnamed — contains only migrations 001–011

Repository Structure Audit (Pass 23)

mediumOpenTrivial fix

File header says 'COMPLETE MIGRATION FILE' and 'combines all 11 migrations' (~403 lines) while 139+ numbered migrations exist with 13 duplicate prefixes (027, 028, … 132). Filename implies one-shot fresh install; reality is a historical artifact from early multi-tenancy bootstrap. Rename to 001-011_legacy_combined.sql or delete to avoid false confidence on new environments.

Fix effort: Rename to 001-011_legacy_combined.sql or delete; add warning header if kept.

114

Architecture diagram claims greco-digest emails stakeholders

Agent, API & Docs Integrity

mediumOpenTrivial fix

app/docs/ArchitectureDiagram.tsx ~1117: 'Nightly /api/cron/greco-digest rolls up the day and emails stakeholders.' app/api/cron/greco-digest/route.ts only calls addMemoryEntry/searchMemory (~161–237) — grep finds no email import or send in that file. Docs overstate nightly email delivery; operators configuring around that flow are misled.

app/docs/ArchitectureDiagram.tsxapp/api/cron/greco-digest/route.ts

Fix effort: Fix ArchitectureDiagram copy or add email step to greco-digest cron.

115

Architecture diagram misdocuments Return Diffs — undermines integration truth

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenTrivial fix

app/docs/ArchitectureDiagram.tsx (~1102–1106) claims /returns-intel ingests weekly BFC truck-errors and feeds return_diffs_snapshots. Actual flow: BFC xlsx drag-drop in Window Return Diffs modal + /return-diffs ODBC → return_diffs_snapshots (EntreeClient.tsx ~9890+). /returns-intel writes returns_intel_reports only. Wrong docs slow enterprise merge work — new integrators will wire the wrong tables.

app/docs/ArchitectureDiagram.tsx

Fix effort: Fix ArchitectureDiagram.tsx Return Diffs flow arrow — point at Window modal + return_diffs_snapshots, not /returns-intel.

116

Build page truncates generation errors on mobile

UI, Flow & Density

mediumOpenTrivial fix

app/build/page.tsx error banner uses max-w-[100px] truncate on mobile (~1695–1696) while desktop gets max-w-xs. Failed generation messages (API errors, validation text) are clipped to ~100px — operators on phones see 'Gener…' instead of the actionable error string.

app/build/page.tsx

Fix effort: Remove max-w-[100px] on mobile error banner; use multi-line text or expandable error panel.

117

Calendar event form initializes date in UTC

Controls, Admin & Collaboration

mediumOpenTrivial fixHigh blast radius

EventForm in app/calendar/page.tsx (~128) sets date via defaultDate.toISOString().split('T')[0]. Evening local times can show tomorrow in the date input before the operator adjusts — distinct from warehouse report_date UTC issues but same operator confusion on a collaboration surface.

app/calendar/page.tsx

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Use warehouse-local date helper in EventForm default date state.

118

Controls shows 'admin-only' to logged-out visitors

Auth & Session

mediumOpenTrivial fix

app/controls/ControlsClient.tsx sets role='viewer' when getUser() returns null (lines 87–88) and renders the 'Controls are admin-only' card — no /login redirect. Logged-out visitors see a misleading access-denied screen instead of the login page.

app/controls/ControlsClient.tsx

Fix effort: router.push('/login') when !user instead of setRole('viewer') in ControlsClient.tsx.

119

Custom-tool AI routes lack maxDuration for multi-phase generation

Build, IDE & Custom Tools

mediumOpenTrivial fix

app/api/custom-tools/generate/route.ts and generate-from-file/route.ts run multi-phase gateway calls (timeoutMs up to ~55s per phase, up to 4 phases) but export no maxDuration. On Vercel, platform defaults can kill mid-pipeline → flaky 504s from Build and /vibe file/prompt modes.

app/api/custom-tools/generate/route.ts

Fix effort: export const maxDuration = 90 on generate + generate-from-file routes.

120

Daily Items skips notifyReportGenerated on PDF export

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenTrivial fix

app/items/page.tsx calls logReportGeneration('Daily Items', …) (~349) with blob size 0 and never notifyReportGenerated. Print Schedule (app/schedule/page.tsx ~244–252) does both. Per-tool email recipients and webhook subscribers never fire for Daily Items runs despite being a core receiving document.

app/items/page.tsxapp/schedule/page.tsx

Fix effort: Call notifyReportGenerated after logReportGeneration in app/items/page.tsx.

121

Dashboard Build hub omits the /build custom-tool builder

UI, Flow & Density

mediumOpenTrivial fix

activeTab === 'build' (~2162–2193) only links to /vibe and /ide. The standalone /build page (file/prompt custom-tool generation, refine, publish) is the third build surface but has no card in the Build hub — users who land on the dashboard Build tab never discover the main tool builder unless they know the URL.

Fix effort: Add third Build hub card linking to /build with description distinct from Vibe and IDE.

122

Dashboard navigating tabs lost their ↗ marks (regression)

UI, Flow & Density

mediumOpenTrivial fix

Audit tab 'Tab bar mixes tabs with links…' is marked fixed with ↗ on Account, AI, Docs, Shanetrée, DockSheet and Support. app/dashboard/page.tsx tab strip (~1768–1910) has zero ↗ characters — in-place tabs and <Link> navigations look identical again. Users can't tell Overview/Tools/Workspace/Reports (stay on dashboard) from Account/Chat/Docs/Entree (leave page) before clicking.

app/dashboard/page.tsx

Fix effort: Re-add ↗ suffix to dashboard tab-bar <Link> entries (Account, Chat, Docs, Entree, DockSheet, etc.) per audit fix.

123

DockSheet custom-appointments panel swallows load failures

Agent, API & Docs Integrity

mediumOpenTrivial fix

app/tools/greco/CindyVueClient.tsx ~334–338: fetch('/api/greco/custom-appointments') uses .catch(() => {}) with no error state. Failed load renders empty appointment list indistinguishable from 'no custom appointments' — operators may think the feature is unused when the API failed.

app/tools/greco/CindyVueClient.tsx

Fix effort: Surface fetch error inline in CindyVueClient custom-appointments section.

124

DockSheet dockvoo daily-stat upsert swallows errors

Greco Warehouse Operations

mediumOpenTrivial fix

CindyVueClient.tsx (~855) calls upsertDailyStat(dateStr, 'dockvoo', statPayload).catch(() => {}) with no toast or retry. Failed greco_daily_stats writes leave Daily Stats and greco-digest blind to dock metrics while the DockSheet UI still shows live counts — silent divergence between floor view and analytics rollups.

Fix effort: Surface upsertDailyStat failure in CindyVue sync banner instead of .catch(() => {}).

125

Document notification route counts failed Resend sends as delivered

API & Operations

mediumOpenTrivial fix

app/api/notifications/document/route.ts increments emailsSent after fetch('https://api.resend.com/emails') without checking response.ok. A 401/422 from Resend still returns { success: true, emailsSent: N } to the caller.

app/api/notifications/document/route.ts

Fix effort: if (!response.ok) skip emailsSent++ in notifications/document/route.ts.

126

Driver Scan silently swallows Supabase persistence failures

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenTrivial fix

app/driver-scan/DriverScanClient.tsx — saveDriverScanReport(...).catch(() => {}) and upsertDailyStat(...).catch(() => {}) (~231–232). Cloud save/My Reports paths surface warnings (~212–214), but driver_scan_reports and greco_daily_stats fail with zero UI feedback — analytics blind to failed saves.

app/driver-scan/DriverScanClient.tsx

Fix effort: Surface toast on saveDriverScanReport/upsertDailyStat catch — match cloud-save warning pattern.

127

Email service logs Resend API key prefix to server console

Security & Trust Boundaries

mediumOpenTrivial fix

lib/email/service.ts sendWithResend() logs [EMAIL] RESEND_API_KEY prefix: with the first six characters on every send attempt. Server logs (Vercel drains) become a partial secret oracle — separate from the Sentry replay issue.

lib/email/service.ts

Fix effort: Delete the RESEND_API_KEY prefix console.log in lib/email/service.ts line 172.

128

Four member-permissions wrapper exports are never called

Dead Code Hunt (Pass 22)

mediumOpenTrivial fixHigh blast radius

lib/supabase/member-permissions.ts exports canAccessToolAnalytics() (~335), canAccessToolAI() (~363), canEditEmailSettings() (~391) and canEditAIFilters() (~419) with zero external callers. Tool pages use getToolPagePermissions() instead; canEdit* fields on getMyFullPermissions() are already tracked as unread — these standalone async wrappers are a second dead layer on top.

lib/supabase/member-permissions.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Remove canAccessToolAnalytics, canAccessToolAI, canEditEmailSettings, canEditAIFilters from member-permissions.ts; callers already use getToolPagePermissions / getMyFullPermissions.

129

Geronimo, Licenses and Negative Slots never write greco_daily_stats

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenTrivial fix

app/geronimo/GeronimoClient.tsx logs + notifyReportGenerated (~673–674) but never upsertDailyStat. app/licenses/page.tsx (~440–447) and app/negative-slots/page.tsx (~461–468) same pattern. Empty Slots gap is tracked separately. All three are absent from DailyStatsClient TOOL_META (~62–80) — digest cron and Daily Stats grid stay blind.

app/geronimo/GeronimoClient.tsxapp/licenses/page.tsxapp/negative-slots/page.tsx

Fix effort: Add upsertDailyStat on save + TOOL_META entries for all three — mirror driver_scan pattern.

130

GET /api/greco/po-doc skips Greco company gate when env is unset

Agent, Collaboration & Platform APIs

mediumOpenTrivial fix

app/api/greco/po-doc/route.ts (~64–73) wraps the GRECO_COMPANY_ID check in if (GRECO_COMPANY_ID). When NEXT_PUBLIC_GRECO_COMPANY_ID is empty (misconfigured deploy, staging), any authenticated user can enqueue bridge/ODBC po-doc relay jobs — ODBC cost and Entrée load without tenant pin.

app/api/greco/po-doc/route.ts

Fix effort: Fail closed when GRECO_COMPANY_ID unset — return 503 instead of skipping gate.

131

getTruckErrorsReportsRaw is orphaned after Return Diffs decoupled from /truck-errors

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenTrivial fix

lib/supabase/tool-reports.ts (~750–808) exports getTruckErrorsReportsRaw() to feed Return Diffs from truck_errors_reports JSONB. EntreeClient Return Diffs comment (~9899–9903) says BFC xlsx drag-drop replaced that path so operators can diff any file without uploading through /truck-errors first. Grep shows zero callers besides the definition — dead integration code that signals the truck-errors → Return Diffs pipeline was abandoned mid-merge.

lib/supabase/tool-reports.ts

Fix effort: Delete dead helper or rewire as optional Return Diffs BFC source — document chosen path in ArchitectureDiagram.

132

GodView admin list queries accept unbounded limit parameter

API & Operations

mediumOpenTrivial fixMay affect neighbors

app/api/admin/godview/route.ts parseInt(searchParams.get('limit') || '100') has no max cap on users, activity, reports, email_logs, etc. One authenticated admin session can pull multi-megabyte cross-tenant payloads — separate from the tracked internal /api/anomalies limit gap.

app/api/admin/godview/route.ts

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Clamp limit to e.g. 500 in godview/route.ts parseInt.

133

GodView list endpoints return total without requesting a count

Controls, Admin & Collaboration

mediumOpenTrivial fix

getUsers, getProjects and sibling handlers in app/api/admin/godview/route.ts (~334–356, ~382) destructure count from .select(...) without { count: 'exact' }. total is always null/undefined — GodView pagination UI is broken across users, projects, chats and logs tabs. Separate from tracked unbounded limit cap — this is missing count plumbing even at normal limits.

app/api/admin/godview/route.ts

Fix effort: Add { count: 'exact' } to godview list .select() calls.

134

Health check probes GOOGLE_AI_API_KEY, not GOOGLE_API_KEY

API & Operations

mediumOpenTrivial fix

app/api/health/route.ts checkAIService (~219) gates on process.env.GOOGLE_AI_API_KEY. Grep GOOGLE_API_KEY across app/api shows ai-summary, ai-analyze, routing/ai-build and agent/chat all read GOOGLE_API_KEY per .env.example. Production with only GOOGLE_API_KEY set: Gemini works, ?detailed=true health reports AI degraded — false alarm in ops runbooks.

app/api/health/route.tsapp/api

Fix effort: One env var name fix in app/api/health/route.ts line 219.

135

HistoryPanel never refreshes after first open

Data Flows & Wiring

mediumOpenTrivial fix

app/components/HistoryPanel.tsx loads only when open && !loaded; loaded is set true on first fetch and never reset on close/re-open (comment says it refreshes but code does not). Operators who close the slide-over, generate a report, and reopen see stale data until full page refresh.

app/components/HistoryPanel.tsx

Fix effort: Reset loaded=false on panel close, or reload whenever open transitions false→true.

136

Internal routes accept unbounded limit params

API & Operations

mediumOpenTrivial fixMay affect neighbors

app/api/anomalies/route.ts:72 parses parseInt(searchParams.get('limit') || '50') with no Math.min cap and passes limit to getRecentAnomalies RPC. Sibling /api/v1/anomalies/route.ts:35–36 clamps days to 1–90 and limit to 200. app/api/jobs/route.ts:121 has same hole — limit flows into .range() on JSONB job payloads. Authenticated session holders can request ?limit=999999.

app/api/anomalies/route.tsapp/api/jobs/route.ts

Fix risk: Capping limit may break internal dashboards or GodView scripts using ?limit=5000 today.

Fix effort: Copy clamp logic from v1 twins into anomalies + jobs routes (~5 lines each).

137

Internal routes accept unbounded limit params their v1 twins clamp

API Consistency

mediumOpenTrivial fixMay affect neighbors

/api/anomalies GET parses ?limit= and ?days= with no cap and passes them straight to the get_recent_anomalies RPC, while /api/v1/anomalies clamps days to 1-90 and limit to 200. /api/jobs GET has the same hole — limit flows uncapped into .range(), payload JSON blobs and all. Anyone authenticated can request limit=999999.

Fix risk: Duplicate — anomalies/jobs large pulls may be intentional for exports.

Fix effort: Same limit caps.

138

lib/agent/policy-verification.ts is an entire unused module

Dead Code Hunt (Pass 22)

mediumOpenTrivial fix

Exports verifyPolicyPrecedence() and verifyEventCompleteness() (~62 lines) for agent policy precedence checks. Grep across the repo finds zero imports of policy-verification or either function — lib/agent/policy.ts and orchestrator resolve effective policy without these test helpers. Built for policy QA that was never wired into CI or runtime.

lib/agent/policy.ts

Fix effort: Delete lib/agent/policy-verification.ts or wire into agent policy tests if QA value remains.

139

lib/routing/geocoder.ts is an entire unused module

Dead Code Hunt (Pass 22)

mediumOpenTrivial fix

Exports geocodeAddress(), batchGeocode(), getOSRMRoute() and unit helpers (~109 lines). app/tools/greco/routing/RoutingClient.tsx calls fetch('/api/routing/geocode') and inline OSRM instead. Grep finds zero imports of @/lib/routing/geocoder — duplicate geocoding logic maintained in a dead client module beside the live API route.

app/tools/greco/routing/RoutingClient.tsxlib/routing/geocoder

Fix effort: Delete lib/routing/geocoder.ts; RoutingClient already uses /api/routing/geocode.

140

lib/supabase/api-keys.ts is an orphan client module

Permissions, Settings & Misleading UI

mediumOpenTrivial fix

The ~377-line module documents CRUD for API keys 'used by both API routes and settings UI,' but grep finds zero imports of lib/supabase/api-keys anywhere. Developer page uses fetch('/api/settings/api-keys') over HTTP instead. Entire typed client layer is dead weight beside the live route handlers.

lib/supabase/api-keys

Fix effort: Delete ~377-line module or wire developer page through it instead of raw fetch.

141

Members 'Ask AI' writes aiChatPrefill that nothing reads

Data Flows & Wiring

mediumOpenTrivial fix

app/members/page.tsx line 80 sessionStorage.setItem('aiChatPrefill', question) before navigating to /chat, but grep across the entire app finds zero readers for that key. The button appears to wire a prefill flow that was never finished on the chat page.

app/members/page.tsx

Fix effort: Read aiChatPrefill in app/chat/page.tsx on mount (or remove the dead sessionStorage write).

142

Monitoring activeToday queries non-existent team_activity table

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenTrivial fix

app/api/monitoring/metrics/route.ts getUserMetrics (~259–268) selects from team_activity, but migrations only define team_activity_log (016_create_team_activity_log.sql). Query silently returns empty — activeToday always reads 0 in GodView/monitoring dashboards even when warehouse staff are active.

app/api/monitoring/metrics/route.ts

Fix effort: Change .from('team_activity') to team_activity_log with correct timestamp column in monitoring/metrics/route.ts.

143

Per-tool recipient helper exports are never called

Dead Code & Orphan Features

mediumOpenTrivial fix

lib/supabase/report-email-recipients.ts exports getRecipientUserIds() and getAllRecipientEmails() (~50 lines) with zero imports. Dashboard loads recipients via getEmailRecipients for the settings UI, and v1 analyze uses getAllRecipientsForCompany — but the two helper wrappers built to drive notification sends were never wired. Reinforces that per-tool email config is decorative on the UI path.

lib/supabase/report-email-recipients.ts

Fix effort: Delete getRecipientUserIds/getAllRecipientEmails or wire into notifyReportGenerated path.

144

Picker pipeline logs report metadata to the browser console

Code Quality & Hygiene

mediumOpenTrivial fix

lib/supabase/tool-reports.ts savePickerReport and getPickerReports console.log company_id, user_id, report data payloads and auth email. Visible on shared warehouse machines — debug logging that should have been stripped.

lib/supabase/tool-reports.ts

Fix effort: Delete console.log lines in lib/supabase/tool-reports.ts.

145

POST /api/custom-tools has no role gate — viewers can create tools via API

Agent, Collaboration & Platform APIs

mediumOpenTrivial fix

app/api/custom-tools/route.ts POST (~92–130) authenticates any company user and rate-limits, but never checks profile.role. UI may restrict the Build page to editors, but the API accepts up to 500KB code payloads from viewers. Enterprise SaaS should mirror UI role gates on mutating routes.

app/api/custom-tools/route.ts

Fix effort: Reject POST when profile.role === 'viewer' — match Build page gate.

146

Projects project-chat send failures are silent

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenTrivial fix

app/projects/page.tsx sendProjectChatMessage() (~61–100) catches errors with console.error only — no toast, inline error, or disabled-state feedback. Users believe a message sent when the Supabase insert failed. Missing session gate on mount is tracked separately.

app/projects/page.tsx

Fix effort: Toast or inline error in sendProjectChatMessage catch — app/projects/page.tsx.

147

Public item-alert subscribe sends email with no rate limit

Rate Limits & Abuse Surface

mediumOpenTrivial fix

/api/public/item-alert-subscribe is an unauthenticated POST that inserts a subscription row and fires a confirmation email through Resend on every call — with no withRateLimit. Repeated { item, email } posts mean unbounded outbound email cost and DB writes from anyone on the internet.

Fix effort: Add withRateLimit to item-alert-subscribe route.

148

Public kiosk routes are absent from GRECO_TOOLS

Greco Warehouse Operations

mediumOpenTrivial fix

lib/greco/greco-tools.ts GRECO_TOOLS ends at /windowadmin — no entries for /inbound, /timer, /live/greco, /sales or /window (public booking). These routes are documented in ArchitectureDiagram and wired to greco session APIs, but founders cannot hide them via the /controls tool matrix and operators won't find them from the Tools tab without memorizing URLs (same discoverability class as tracked /port, /noe, /adrian gaps).

lib/greco/greco-tools.ts

Fix effort: Add optional kiosk folder entries for /inbound, /timer, /live/greco, /sales, /window in greco-tools.

149

Public tool info endpoint has no rate limiting

Build, IDE & Custom Tools

mediumOpenTrivial fix

app/api/public-tools/info/route.ts returns full tool name + code with zero withRateLimit, while sibling /api/public-tools/data is rate-limited. Enables share_id enumeration and scraping of generated tool source at higher volume.

app/api/public-tools/info/route.ts

Fix effort: Add withRateLimit to public-tools/info like data route.

150

Relay mode hides the DockSheet tile in Shanetrée

Greco Warehouse Operations

mediumOpenTrivial fix

RELAY_HIDDEN_MODALS in EntreeClient.tsx (~446–447) includes 'dockVoo' alongside print, itemSearch and guide. Relay/tablet Shanetrée home (~9162, ~29421) filters modal tiles with !(relayMode && RELAY_HIDDEN_MODALS.has(key)) — dock-floor relay devices cannot open DockSheet from the Entree launcher even though /cindyvue and dashboard DockSheet links still work.

Fix effort: Remove dockVoo from RELAY_HIDDEN_MODALS or add explicit DockSheet shortcut on relay home.

151

Returns-family card labels don't match their routes

UI, Colors & Aesthetics

mediumOpenTrivial fixHigh blast radius

lib/greco/greco-tools.ts GRECO_TOOLS: line 65 name 'Returns' → /truck-errors (truck error upload), line 67 'Returns+' → /returns (ledger), line 74 'Weekly Returns' → /returns-intel. greco_card_permissions keys by route — renames break denials. Labels mislead new users; safe fix is display-name only ('Truck Errors', 'Returns Ledger', 'Returns Intel').

lib/greco/greco-tools.ts

Fix risk: greco_card_permissions keys by route path — renaming display labels is safe but changing routes breaks denials and saved permissions JSON.

Fix effort: Rename card labels in greco-tools.ts only — routes/permission keys stay.

152

Returns-family names don't match their routes

Naming & Labels

mediumOpenTrivial fixHigh blast radius

lib/greco/greco-tools.ts GRECO_TOOLS maps three confusing pairs: line 65 name 'Returns' → route /truck-errors (truck error returns upload), line 67 'Returns+' → /returns (customer returns ledger), line 74 'Weekly Returns' → /returns-intel (weekly intel). greco_card_permissions and member_permissions.tool_access key by route string — renaming routes would break saved denials. Card labels could say 'Truck Errors', 'Returns Ledger', 'Returns Intel' without touching permission keys.

lib/greco/greco-tools.ts

Fix risk: Same route-keyed permissions — '/returns' vs 'Returns+' label confusion is safer than route renames.

Fix effort: Same label rename.

153

Routing load and export failures often fail silently

Shanetrée, Routing & Driver Comms

mediumOpenTrivial fix

loadRouteList ignores non-OK fetch responses (RoutingClient.tsx ~891–894). loadRoute returns false with no banner (~901–928). Export swallows saveOutputFile, logReportGeneration and notifyReportGenerated via .catch(() => {}) (~1439–1441). Operators can believe a route saved or exported when the API failed — empty load menu looks like 'no saved routes' not an error.

Fix effort: Surface res.ok failures and export .catch errors inline in RoutingClient.

154

routing/ai-build missing maxDuration for 90s AI call

API & Operations

mediumOpenTrivial fix

app/api/routing/ai-build/route.ts calls gateway with timeoutMs: 90_000 (~445) but exports no export const maxDuration. Sibling app/api/port/ai-summary/route.ts:36 sets maxDuration = 90. Vercel platform default function timeout can kill mid-cluster generation → intermittent 504 on route planner AI builds.

app/api/routing/ai-build/route.tsapp/api/port/ai-summary/route.ts

Fix effort: export const maxDuration = 90 — one line, copy from port/ai-summary.

155

routing/ai-build waits 90s for AI but never declares maxDuration

API Consistency

mediumOpenTrivial fix

The route calls the gateway with timeoutMs: 90_000 yet exports no maxDuration — on Vercel the platform default can kill the function mid-generation, surfacing as flaky 504s. Sibling AI routes (port/ai-summary at 90, driver-chat/ai at 60) declare it; this one just forgot.

Fix effort: Same maxDuration export.

156

Sentry session replay records unmasked page text

Security & Trust Boundaries

mediumOpenTrivial fix

sentry.client.config.ts sets maskAllText: false and blockAllMedia: false with replaysSessionSampleRate 0.1 and replaysOnErrorSampleRate 1.0. Warehouse metrics, customer names and tool output can land readable in Sentry. Masking should be on by default with targeted unmasking.

Fix effort: Flip maskAllText: true in sentry.client.config.ts; unmask specific selectors if needed.

157

Server-side request errors may never reach Sentry

API & Operations

mediumOpenTrivial fix

instrumentation.ts exports only register() (~8–16) importing sentry.server.config and sentry.edge.config — no export async function onRequestError. @sentry/nextjs v10 App Router docs require onRequestError to capture route-handler exceptions. Client path has app/global-error.tsx; grep onRequestError across repo returns zero matches. API route throws likely never become Sentry events unless manually captured.

app/global-error.tsx

Fix effort: Export onRequestError from instrumentation.ts per @sentry/nextjs v10 docs.

158

Shanetrée autopull hardcodes a fallback user UUID

Backend & Database

mediumOpenTrivial fix

app/api/internal/shanetree-autopull/route.ts attributes snapshots and pull-log rows to SHANETREE_AUTOPULL_USER_ID, falling back to hardcoded f528c8d6-f7db-4d34-a656-680a3d3741de. On a fresh deploy without that env var, activity ledger, pull log and snapshots are stamped with one fixed user regardless of who operates the bridge.

app/api/internal/shanetree-autopull/route.ts

Fix effort: Require SHANETREE_AUTOPULL_USER_ID env var (fail if missing) instead of hardcoded fallback UUID.

159

Shrink Meeting and Counting skip greco_daily_stats integration

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenTrivial fix

app/shrink-meeting/page.tsx logs + notifyReportGenerated (~370) but never upsertDailyStat. app/counting/CountingClient.tsx logs via logTeamActivity (~531–542) only — no upsertDailyStat, logReportGeneration, or notifyReportGenerated. Meat-count ledger entries appear in /history but Daily Stats grid and greco-digest stay blind.

app/shrink-meeting/page.tsxapp/counting/CountingClient.tsx

Fix effort: Add upsertDailyStat + TOOL_META keys for shrink_meeting and counting on save.

160

StockVoo disables live refresh in portMode without stale-data warning

Email, v1 API & Entrée Panel Polish

mediumOpenTrivial fix

app/entree/StockVooPanel.tsx (~733) sets disabled={loading || portMode} on Refresh. /port relay tablet users cannot re-pull live inventory ODBC; UI does not explain they are viewing a frozen snapshot — operators may assume stock counts are current.

app/entree/StockVooPanel.tsx

Fix effort: Show 'snapshot only on /port' banner when portMode disables Refresh — StockVooPanel.tsx.

161

supabase/migrations/README.md is stale — documents 11 migrations, repo has 150

Repository Structure Audit (Pass 23)

mediumOpenTrivial fix

supabase/migrations/README.md 'Migration Files' section ends at 011_create_truck_errors_reports_table.sql with hand-run SQL Editor instructions. git ls-files counts 150 migration SQL files including 136_create_account_geocodes.sql. Fresh DBA following README applies the wrong subset. Professional fix: auto-generate migration index from filenames or delete README body and point to Supabase CLI only.

supabase/migrations/README.md

Fix effort: Rewrite README to point at Supabase CLI only or auto-generate file list from migrations/.

162

Team-chat ?dm= deep link fails silently when room creation errors

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenTrivial fix

app/team-chat/page.tsx (~162–172) calls createChatRoom('', [dmUserId], 'direct') when no existing DM exists. On !result.success there is no toast, error state, or fallback — user stays on room list with no explanation. Broken DM links from mentions or dashboard look like a no-op.

app/team-chat/page.tsx

Fix effort: Show toast/setError when createChatRoom returns !success in team-chat/page.tsx.

163

Team-chat create-room failure is silent

Controls, Admin & Collaboration

mediumOpenTrivial fix

handleCreateChat in app/team-chat/page.tsx (~262–283): createChatRoom failure has no else branch — no toast, no error state. Modal stays open with no feedback when room creation fails (RLS denial, duplicate direct message, network blip).

app/team-chat/page.tsx

Fix effort: Show error toast in handleCreateChat else branch when result.success is false.

164

Team-chat never triggers chat email notifications

Data Flows & Wiring

mediumOpenTrivial fix

notifyChatMessage() exists in lib/notifications/trigger.ts but has zero callers outside its definition. team-chat handleSendMessage only inserts via sendChatRoomMessage — users who enable chat_notifications in account settings never get emails from team-chat activity.

lib/notifications/trigger.ts

Fix effort: Call notifyChatMessage() after successful sendChatRoomMessage in team-chat/page.tsx.

165

The health check tests an env var that nothing else uses

API Consistency

mediumOpenTrivial fix

/api/health's AI probe checks GOOGLE_AI_API_KEY, but every actual AI route reads GOOGLE_API_KEY (which is what .env.example documents). Production can have Gemini fully working while health reports AI as degraded — false alarms baked into the runbook.

Fix effort: Same env var rename.

166

Thruput load failures render an empty board with no error state

Greco Warehouse Operations

mediumOpenTrivial fix

Initial week/day hydration in app/thruput/page.tsx (~171–207) catches errors with console.error only — no setError banner. A missing thruput_days table or RLS denial looks identical to 'no data yet this week,' so night-shift leads may rebuild staffing from scratch while the real rows failed to load.

app/thruput/page.tsx

Fix effort: setError banner in thruput/page.tsx catch blocks when getThruputWeeks/Days fails.

167

Thruput ships hardcoded real employee names as defaults

Greco Warehouse Operations

mediumOpenTrivial fix

DEFAULT_ACTIVE in app/thruput/page.tsx (~114–129) seeds early500, loaders, runners, forklift areas, management and sick/off lists with real Greco staff names (e.g. 'Chilchoa, Moises', 'Acabal, Ramon', 'Cisneros, Rodolfo'). New weeks/days inherit that roster until manually cleared — a privacy and onboarding hazard if the page is ever shared outside the warehouse or reused for demos.

app/thruput/page.tsx

Fix effort: Replace DEFAULT_ACTIVE with empty placeholders or load from last saved week.

168

Transfers and Cycle Count swallow upsertDailyStat failures

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenTrivial fix

app/transfers/TransfersClient.tsx (~128, 136) and app/cyclecount/CycleCountClient.tsx (~128–129) call upsertDailyStat(...).catch(() => {}) and notifyReportGenerated(...).catch(() => {}) with no UI feedback. Driver Scan silent-save pattern is tracked — Transfers/Cycle Count have the same analytics divergence risk.

app/transfers/TransfersClient.tsxapp/cyclecount/CycleCountClient.tsx

Fix effort: Toast on catch instead of empty .catch(() => {}) — match driver-scan fix.

169

v1 /tools/generate always publishes tools (is_public: true)

Build, IDE & Custom Tools

mediumOpenTrivial fix

app/api/v1/tools/generate/route.ts (~103) hardcodes is_public:true on every API-generated tool, minting a public /t/{share_id} surface regardless of builder intent. Pairs with tracked 'no in-app public link' discoverability gap — API path auto-publishes.

app/api/v1/tools/generate/route.ts

Fix effort: Accept is_public param defaulting false; document public share opt-in in OpenAPI.

170

v1 batch API blocks AI chat routes that exist as first-class endpoints

Build, IDE & Custom Tools

mediumOpenTrivial fix

ALLOWED_PATH_PREFIXES in app/api/v1/batch/route.ts (~38–51) includes /api/v1/agent but not /api/v1/ai/chat or /api/v1/ai/chat/stream despite both being live in openapi.json. Batch clients get 400 for AI chat operations. Calendar batch gap is tracked separately.

app/api/v1/batch/route.ts

Fix effort: Add /api/v1/ai/chat and /api/v1/ai/chat/stream to ALLOWED_PATH_PREFIXES.

171

v1 batch API blocks calendar routes that exist as first-class endpoints

API & Operations

mediumOpenTrivial fix

app/api/v1/batch/route.ts ALLOWED_PATH_PREFIXES (~38–51) lists /api/v1/reports, /tools, /schedules, etc. but omits /api/v1/calendar. First-class routes /api/v1/calendar/events and /api/v1/calendar/available-slots accept API keys independently. Batch POST returns 400 'path not allowed' for { method:'GET', path:'/api/v1/calendar/events' } — integrators must special-case calendar outside batch.

app/api/v1/batch/route.ts

Fix effort: Add '/api/v1/calendar' to ALLOWED_PATH_PREFIXES in batch/route.ts.

172

v1 export 202 response documents the wrong poll URL

Email, v1 API & Entrée Panel Polish

mediumOpenTrivial fix

app/api/v1/export/route.ts (~236) tells clients to 'Poll GET /api/v1/export with the job ID'. Actual status endpoint is GET /api/v1/export/:id (app/api/v1/export/[id]/route.ts). API integrators following the message get 400/405 loops until they discover the correct path.

app/api/v1/export/route.tsapp/api/v1/export/

Fix effort: Fix message to GET /api/v1/export/{job_id} in export/route.ts.

173

v1 POST /reports/analyze has no request body size cap

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenTrivial fix

Sibling app/api/v1/reports/route.ts rejects bodies >5MB via content-length (~43–52). v1/reports/analyze parses JSON with no size guard — only truncates data to 15k chars for the model (~137–141) after the full body is already in memory. Large API payloads can OOM serverless functions.

app/api/v1/reports/route.ts

Fix effort: Mirror v1 /reports content-length guard (~5MB) before JSON parse.

174

v1 POST /tools has no code size limit

Build, IDE & Custom Tools

mediumOpenTrivial fix

app/api/v1/tools/route.ts handlePost (~88–96) accepts body.code string with no .length cap. Internal app/api/custom-tools/route.ts (~112–115) rejects code > 500KB. API-key consumers can INSERT multi-megabyte HTML into custom_tools.code — Postgres row bloat, slow list queries, potential Vercel body-size failures at edge.

app/api/v1/tools/route.tsapp/api/custom-tools/route.ts

Fix effort: Apply same 500KB code cap as session custom-tools route.

175

v1 schedules PUT skips cron expression validation

Security & API Gaps (Pass 24)

mediumOpenTrivial fix

app/api/v1/schedules/route.ts defines isValidCron() (~23–26, 5-field check) and calls it in handlePost (~130). app/api/v1/schedules/[id]/route.ts handlePut (~106) assigns body.cron to cron_expression with zero validation — empty string, 'not-a-cron', or 100-field strings persist. job_schedules.cron_expression is plain text with no DB CHECK. Scheduler computing next_run_at from garbage crons fails silently for that row.

app/api/v1/schedules/route.tsapp/api/v1/schedules/

Fix effort: Share isValidCron() in PUT handler; add DB CHECK or cron parser library for semantic validation.

176

Will-Call Window card opens staff admin, not public scheduler

Permissions, Settings & Misleading UI

mediumOpenTrivial fixMay affect neighbors

lib/greco/greco-tools.ts line 83 routes 'Will-Call Window' to /windowadmin. Customer/driver self-service booking lives at /window (WindowClient.tsx). Operators picking 'Window' from the tools grid land on the authenticated review UI; public /window is only linked from a small header on windowadmin. Misleading card for floor staff hunting the booking kiosk.

lib/greco/greco-tools.ts

Fix risk: Changing card href affects trained operators; /windowadmin vs /window is a product decision tied to permissions.

Fix effort: Point greco-tools card at /window or add second 'Window (public)' card.

177

/api/admin/verify is an unauthenticated password oracle

Security & Trust Boundaries

mediumOpenEasy fix

app/api/admin/verify/route.ts accepts public POST { password } and returns { valid: true|false } against ADMIN_PASSWORD. Only generic per-IP rate limiting — no session requirement, lockout, or CAPTCHA. Credential stuffing against a shared admin password.

app/api/admin/verify/route.ts

Fix effort: Require authenticated session before comparing password, or remove endpoint and use server-only admin gate.

178

/api/agent/chat accepts any sessionId without ownership verification

Agent, Collaboration & Platform APIs

mediumOpenEasy fix

app/api/agent/chat/route.ts (~168–188) inserts into ai_chat_messages using client-supplied sessionId after auth, without verifying the session exists or belongs to userInfo.companyId / userInfo.userId. Known or guessed UUIDs allow cross-session message injection and updated_at bumps on foreign sessions.

app/api/agent/chat/route.ts

Fix effort: Verify session belongs to userId + companyId before insert — one guard in agent/chat/route.ts.

179

/api/email/send template whitelist rejects eight live EmailService templates

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fix

app/api/email/send/route.ts validTemplates (~60–64) lists 10 names; lib/email/service.ts EmailTemplate (~13–30) also defines ai_summary, anomaly_alert, agent_notification, agent_digest, dockvoo_checkin, item_scheduled, item_checked_in, item_received. Internal or agent callers using those template names get 400 Invalid template even though NotificationService can render them.

app/api/email/send/route.tslib/email/service.ts

Fix effort: Expand validTemplates in email/send/route.ts to match EmailTemplate union in lib/email/service.ts.

180

/chat fetches AI sessions before checking auth — and never redirects

Auth & Session

mediumOpenEasy fix

app/chat/page.tsx loadData() calls getChatSessions(50) unconditionally whether or not a session exists. No /login redirect anywhere in the file. Logged-out visitors get the full chat shell with an empty list and a Supabase query on mount.

app/chat/page.tsx

Fix effort: Gate loadData on session + router.push('/login') — mirrors other pages.

181

/confirmation shows verified-success UI without checking auth

Auth & Session

mediumOpenEasy fix

app/confirmation/page.tsx always renders 'Your email has been verified and your account is ready' with a dashboard link. No getSession() check, no Supabase auth listener — anyone who visits the URL sees a success screen whether or not they just confirmed email.

app/confirmation/page.tsx

Fix effort: Check for fresh recovery session or redirect; avoid showing success to random visitors.

182

/inbox driver-chat queries ignore Supabase errors

Data Flows & Wiring

mediumOpenEasy fix

app/inbox/InboxClient.tsx loadThreads and loadMessages destructure only { data } and never inspect error. A failed query (RLS denial, network blip, stale token) renders as an empty thread list or empty conversation with no error banner — the page's error state is only used for send/AI failures.

app/inbox/InboxClient.tsx

Fix effort: Check error in loadThreads/loadMessages and set error state — InboxClient.tsx.

183

/noe allows unlimited Loading Sheet reprints

Window, Sales & Kiosk Scheduling

mediumOpenEasy fix

Print-once guard in will-call-print-request/route.ts (~221–239) applies only when requester='sales'. /noe posts requester:'noe' (EntreeClient.tsx ~26503–26507) with only 20s dedupe. UI clears 'Queued' after 5s (~26520–26530) with no GET ?id= outcome polling — unlike /sales which polls until done/failed (~939–982). Operators can re-queue the same invoice indefinitely from /noe.

Fix effort: Extend print-once semantics to noe or track per-invno consumption like sales.

184

/noe never polls print outcome despite bridge watcher existing

Window, Sales & Kiosk Scheduling

mediumOpenEasy fixMay affect neighbors

requestNoeLsPrint in EntreeClient.tsx (~26497–26530) sets 'Queued' then clears after 5s — no status poll. Bridge-side noe-print watcher (~27227+) fulfills rows but /noe UI never surfaces done/failed like /sales does. Comment at ~27238 claims /noe can show final status; client code does not implement it.

Fix risk: Adding poll changes kiosk CPU/network; removing watcher breaks silent print failures.

Fix effort: Poll will-call-print-request ?id= in requestNoeLsPrint like SalesClient requestPrint.

185

/picture is a public unauthenticated internal-architecture diagram

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenEasy fix

app/picture/page.tsx (~127) labels the page PUBLIC • NO LOGIN. Documents ODBC/bridge data flow with PDF export via jspdf + html2canvas (~40–109). No auth, rate limit, or tenant gate — internal infra diagram exposed to anyone with the URL (linked from /port relay docs).

app/picture/page.tsx

Fix effort: Require session or move to /docs; rate-limit PDF export — product decision on public relay collateral.

186

/port snapshot Shanetrée mirror is undiscoverable

Permissions, Settings & Misleading UI

mediumOpenEasy fix

/port wraps EntreeClient in portMode for ODBC-less devices. Grep for nav links to /port finds only picture/page.tsx, docs and an internal EntreeClient reference — not in GRECO_TOOLS, dashboard or greco-tools cards. Documented in /docs but absent from operational navigation; useful fallback with no way to find it.

Fix effort: Add GRECO_TOOLS card or dashboard Workspace link labeled 'Port mode (no ODBC)'.

187

/produce standalone page duplicates Shanetrée ProducePanel

Dead Code & Orphan Features

mediumOpenEasy fixMay affect neighbors

app/produce/page.tsx (~800 lines) implements produce-item browsing via ODBC relay, but app/entree/ProducePanel.tsx line 6 says it was 'Ported from the standalone /produce page.' Grep finds no href/router links to /produce from live app pages — only the Entree panel and API paths remain. Entire route is a duplicate entry point with no navigation.

app/produce/page.tsxapp/entree/ProducePanel.tsx

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Delete app/produce/page.tsx and redirect /produce → /entree with produce panel open, or add nav link if standalone is intentional.

188

/projects chat has no realtime subscription

Controls, Admin & Collaboration

mediumOpenEasy fixMay affect neighbors

app/projects/page.tsx loadProjectMessages (~48–58) runs once when a project expands — no postgres_changes subscription. Admin replies via GodView or dashboard won't appear until re-expand/refresh. Send failures are console.error only (~96–97).

app/projects/page.tsx

Fix risk: Realtime filter changes alter what live updates appear — easy to hide needed events.

Fix effort: Subscribe to project_messages postgres_changes when project expands.

189

/sales search auto-triggers live ODBC invoice-status lookups

Window, Sales & Kiosk Scheduling

mediumOpenEasy fix

SalesClient.tsx (~1130–1148): when filtered rows are empty and the query looks like an invno, 600ms debounce calls runInvoiceLookup() which POSTs /api/public/invoice-status (~1017–1022) — direct ODBC or relay enqueue. Combined with unauthenticated, un-rate-limited endpoint, typo-sweeping on a shared kiosk can stack bridge jobs.

Fix effort: Require explicit Search button or throttle invoice-status POSTs per kiosk session.

190

/scan has no authentication gate

Build, IDE & Custom Tools

mediumOpenEasy fix

app/scan/page.tsx never checks session or redirects to /login — only optionally loads getUserCompanyId for Greco tool filtering. Unauthenticated visitors can upload spreadsheets, run detection and router.push into warehouse tool routes (which may gate later).

app/scan/page.tsx

Fix effort: Redirect unauthenticated users to /login or require session before file upload.

191

/scan mis-wires sessionStorage for three destination tools

Data Flows & Wiring

mediumOpenEasy fixMay affect neighbors

app/scan/page.tsx stores scannerFile in sessionStorage and redirects, but /reconciliation, /loader and /returns-intel never read that key (grep returns no matches). Nine other tools do consume it — users routed through scan to those three arrive with nothing loaded.

app/scan/page.tsx

Fix risk: Wiring reconciliation/loader/returns-intel to scannerFile changes upload path for users who worked around missing key.

Fix effort: Add scannerFile reader to reconciliation, loader, returns-intel pages (copy from forklift pattern) OR remove those three from scan routing table.

192

/scan redirect can exceed sessionStorage quota

Build, IDE & Custom Tools

mediumOpenEasy fixMay affect neighbors

app/scan/page.tsx (~294–306) stores the entire file as a base64 data URL in sessionStorage scannerFile with no size check or try/catch around setItem. Large .xlsx files commonly exceed ~5MB quota → silent redirect with nothing loaded. Tracked mis-wire to three tools is a separate issue.

app/scan/page.tsx

Fix risk: Moving to IndexedDB or Storage changes large-file scan flow timing.

Fix effort: Size-check file before base64; use IndexedDB or server staging for large uploads.

193

/windowadmin is not admin-gated despite the name

Window, Sales & Kiosk Scheduling

mediumOpenEasy fix

Route comment says 'Greco staff review' but access is GrecoOnlyGuard only (app/windowadmin/page.tsx). Unlike /controls founder/admin gate (ControlsClient.tsx), any Greco editor/viewer with a bookmark opens the appointment review console. Pairs with RLS allowing any member to accept/reject (#1 above).

app/windowadmin/page.tsx

Fix effort: Wrap /windowadmin in founder/admin role check like ControlsClient.

194

Account chat and mention notification toggles are no-ops

Dead Code & Orphan Features

mediumOpenEasy fixHigh blast radius

Account settings expose chat_notifications and mention_notifications toggles (app/account/page.tsx) persisted to user_notification_preferences. lib/email/service.ts maps them to templates, but notifyChatMessage() and notifyMention() in lib/notifications/trigger.ts have zero callers — team-chat never triggers either. Users enable 'email me on mentions' and nothing ever sends. report_notifications still works via notifyReportGenerated; chat/mention prefs are decorative (~4 exported notify helpers, ~45 lines, never called).

app/account/page.tsxlib/email/service.tslib/notifications/trigger.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Call notifyChatMessage/notifyMention after send/mention, or hide toggles until wired.

195

Account security_alerts toggle is decorative

Agent, API & Docs Integrity

mediumOpenEasy fixHigh blast radius

account/page.tsx exposes security_alerts in NOTIFICATION_OPTIONS (~43–46) with copy 'You will always receive critical security notifications' (~1199–1200). grep sendSecurityAlert finds only the definition in lib/email/service.ts (~306) — zero callers. Toggle persists to user_notification_preferences but nothing ever sends security_alert emails.

lib/email/service.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Wire sendSecurityAlert on password change/login anomalies, or hide toggle.

196

account_geocodes has no foreign key on company_id

Backend & Database

mediumOpenEasy fixMay affect neighbors

supabase/migrations/136_create_account_geocodes.sql:20 defines company_id uuid not null with no REFERENCES companies(id) — header comment promises multi-tenant scoping but FK omitted. RLS policies (~37–56) filter by profiles.company_id; orphaned UUIDs from bad imports pass INSERT and pollute Window Map geocode cache keys (company_id, custno) unique index.

supabase/migrations/136_create_account_geocodes.sql

Fix risk: Adding FK fails migration if orphan rows exist — must clean Window Map cache first.

Fix effort: New migration: ADD CONSTRAINT REFERENCES companies(id) — verify no orphan rows first.

197

Agent policy scheduling windows evaluate in UTC only

Agent, API & Docs Integrity

mediumOpenEasy fixHigh blast radius

lib/agent/policy.ts isWithinPolicyWindow() (~167–177) uses getUTCDay(), getUTCHours() and getUTCMinutes(). Policy carries scheduling.timezone (~114) and dry-run returns it (app/api/agent/policy/schedules/dry-run/route.ts ~40), but window checks ignore the configured timezone. Autonomy cron + policy dry-run can disagree with US warehouse hours operators think they configured.

lib/agent/policy.tsapp/api/agent/policy/schedules/dry-run/route.ts

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Convert at to policy.scheduling.timezone before day/hour/minute check in isWithinPolicyWindow.

198

Agent query tools accept uncapped limit parameters

Performance & Scale

mediumOpenEasy fix

lib/agent/tools.ts queryReports() passes args.limit straight to .limit(limit) with no clamp (executeTool default 50 only when arg omitted). A single agent tool call with limit: 50000 pulls full JSONB report rows for a tenant via service-role client — parallel to tracked internal API limit gaps.

lib/agent/tools.ts

Fix effort: Math.min(args.limit ?? 50, 200) in executeTool before queryReports and sibling query tools.

199

agent_notification emails embed unvalidated actionUrl in button href

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fix

lib/email/service.ts (~491–493) uses ${data.actionUrl} in an anchor with no same-origin or https allowlist. Agent-supplied or forged actionUrl values can become open redirects or phishing links inside FileDisplay-branded notification emails.

lib/email/service.ts

Fix effort: Allowlist actionUrl to same-origin paths or https://filedisplay.com/* before rendering button.

200

Agent-autonomy cron silently caps at 100 companies per run

API & Operations

mediumOpenEasy fix

app/api/cron/agent-autonomy/route.ts (~33–37) selects agent_settings rows with auto_summarize or auto_email_digest, then .limit(100) — no .range() pagination, no overflow counter in JSON response. vercel.json schedules */10; companies alphabetically beyond row 100 silently never enqueue digest agent_tasks. GodView tenant list can exceed 100 without any cron metric surfacing the skip.

app/api/cron/agent-autonomy/route.ts

Fix effort: Paginate settings query or raise limit with overflow logging in JSON response.

201

Analytics-log date filters use UTC midnight boundaries

Greco Warehouse Operations

mediumOpenEasy fixHigh blast radius

GET /api/greco/analytics-log applies date_from/date_to as `${date}T00:00:00.000Z` and `T23:59:59.999Z` (~35–39). Eastern evening report saves (local Jul 9 ~8pm) store created_at as Jul 10 UTC and fall outside a Jul 9 filter on the Daily Stats Analytics Log — separate from report_date stamping but same operator confusion on date pickers.

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Filter on report_date or warehouse-local day bounds instead of created_at Z midnight.

202

API key cache invalidation flushes all tenants

API & Operations

mediumOpenEasy fixMay affect neighbors

lib/cache/service.ts apiKeyCache.invalidateCompany(companyId) (~426–428) accepts companyId but calls cacheDeletePattern('fd:api_key:*') — wipes every tenant's cached API keys on one company's key rotation. Comment at ~402 admits 'can't easily find all users in a company'. invalidateApiKeyCache(keyHash) correctly deletes one hash (~510–511). Cross-tenant cache flush on single-key revoke is a multi-tenancy footgun.

lib/cache/service.ts

Fix risk: Duplicate — global pattern delete affects every API consumer simultaneously.

Fix effort: Scope cacheDeletePattern to companyId in lib/cache/service.ts; add company check on invalidate_user.

203

Build page progress timers leak on failure

Wiring & Broken Flows

mediumOpenEasy fix

The tool-generation flows in app/build/page.tsx stage fake pipeline progress with four chained setTimeouts, cleared only on the success path — the catch blocks never clear them, and neither does unmount. A failed generation (or navigating away mid-request) leaves orphan timers calling setCurrentStep on a dead flow. Same pattern in generateFromPrompt and refineCode.

app/build/page.tsx

Fix effort: Same as build/unmount timer leak — clear timeout refs in catch + useEffect cleanup.

204

Build page progress timers leak on failure/unmount

Data Flows & Wiring

mediumOpenEasy fix

app/build/page.tsx chains four setTimeout calls for fake pipeline progress in generateFromFile, generateFromPrompt and refineCode. Timeouts cleared only on success path; catch blocks don't clear them and there's no unmount cleanup. Failed generation or navigating away leaves orphan timers calling setCurrentStep on dead state.

app/build/page.tsx

Fix effort: Store timeout IDs in ref, clear in catch + useEffect cleanup — ~3 functions in build/page.tsx.

205

Calendar has no auth wall

Auth & Session

mediumOpenEasy fixCritical — test everything

app/calendar/page.tsx never redirects unauthenticated users. It loads events, renders the full calendar UI, and only skips setting currentUserId when getUser() returns null. Logged-out visitors see an empty calendar shell and can open the event form — createCalendarEvent will fail at RLS, but the page shouldn't render at all.

app/calendar/page.tsx

Fix risk: Central auth changes affect every protected route and public kiosk exception list.

Fix effort: Add same auth redirect pattern as account/page.tsx at top of calendar load effect.

206

Calendar realtime ignores Personal vs Company tab filter

Controls, Admin & Collaboration

mediumOpenEasy fixMay affect neighbors

getCalendarEvents(..., calendarTab) filters personal vs company in lib/supabase/calendar.ts (~93–108). subscribeToCalendarEvents in calendar/page.tsx (~542–546) inserts every calendar_events row for the company with no calendarTab, date-range or visibility filter. On Personal tab, colleagues' company events appear live incorrectly.

lib/supabase/calendar.ts

Fix risk: Filtering subscription may hide legitimate cross-tab updates users currently see (and complain about).

Fix effort: Filter realtime inserts/updates in calendar/page.tsx by calendarTab visibility rules.

207

Calendar shows Edit/Delete on every visible event, not just owned events

Agent, Collaboration & Platform APIs

mediumOpenEasy fix

app/calendar/page.tsx EventDetail (~421–427) renders edit/delete for all expanded events with no created_by === currentUserId check. RLS allows DELETE only for creator (088_create_calendar_events.sql ~70–76); handleDelete (~571–574) calls deleteCalendarEvent and reloads without surfacing RLS failure — operators think they deleted a coworker's event when nothing changed.

app/calendar/page.tsx

Fix effort: Hide edit/delete unless event.created_by === user.id; surface RLS error toast on failed delete.

208

Calendar sub-toolbar overflows on narrow screens

UI, Flow & Density

mediumOpenEasy fix

app/calendar/page.tsx sub-nav (~627–680) is flex justify-between with calendar tabs, view-mode toggle and month prev/next in one row with no flex-wrap. On narrow viewports the month navigation and view controls collide with Personal/Company tabs — horizontal overflow or clipped controls.

app/calendar/page.tsx

Fix effort: flex-wrap sub-nav or collapse month nav into a compact dropdown below md breakpoint.

209

Calendar swallows every failure

Data Flows & Wiring

mediumOpenEasy fixMay affect neighbors

lib/supabase/calendar.ts getCalendarEvents returns [] on error. handleSave catches with console.error only; handleDelete has no try/catch. Network blip during save/delete looks like success; failed load looks like an empty calendar.

lib/supabase/calendar.ts

Fix risk: Surfacing errors changes empty-state UX — operators may think calendar is broken when network blips.

Fix effort: Add error state banner + surface calendar.ts errors instead of returning [].

210

calls table (WebRTC) was never integrated

Dead Code & Orphan Features

mediumOpenEasy fix

supabase/migrations/081_create_calls.sql creates a calls table with realtime publication. Grep for .from('calls') across the entire repo returns zero matches. calendar_events.call_type (migration 088) is similarly unused in app code. ~45 lines of schema + RLS for a video-call feature that was scaffolded and abandoned.

supabase/migrations/081_create_calls.sql

Fix effort: Drop calls table + call_type column in a migration, or scope a minimal WebRTC spike before keeping schema.

211

Chat Build mode iframes /vibe with a misleading title

UI, Flow & Density

mediumOpenEasy fix

app/chat/page.tsx Greco-only viewMode === 'vibe' renders <iframe src='/vibe' title='Build — Tool Builder'> (~1008–1014) while the tab label says Build. /vibe is the web studio; /build is the custom-tool file/prompt builder — the iframe title and tab name imply the wrong product surface.

app/chat/page.tsx

Fix effort: Point iframe at /build or rename tab to Vibe; fix title attribute to match actual src.

212

client_events telemetry table has no retention or cleanup path

Agent, Collaboration & Platform APIs

mediumOpenEasy fix

supabase/migrations/107_create_ide_context_index_and_client_events.sql creates client_events with INSERT RLS. Writer is app/api/telemetry/client-event/route.ts only. No cron, TTL index, partition strategy or cleanup migration — table grows unbounded as IDE/desktop extension telemetry accumulates.

supabase/migrations/107_create_ide_context_index_and_client_events.sqlapp/api/telemetry/client-event/route.ts

Fix effort: Cron deleting rows older than 90d or partition by month — migration + vercel.json cron entry.

213

companyCache and statsCache are write-only — only invalidate is called

Dead Code Hunt (Pass 22)

mediumOpenEasy fix

lib/cache/service.ts defines companyCache.get/.set (~351–365) and statsCache.get/.set (~458–467). Grep companyCache|statsCache across the repo finds only invalidateCompanyCache() calling .invalidate / .invalidateAll (~500–502). No .get or .set callers anywhere — ~120 lines of cache objects that never store or read data; invalidation runs against empty caches.

lib/cache/service.ts

Fix effort: Wire .get/.set at read sites or delete cache objects and simplify invalidateCompanyCache.

214

CONTROLLABLE_TABS preset list is also stale

Permissions, Settings & Misleading UI

mediumOpenEasy fix

CONTROLLABLE_TABS (~64–74) still lists members, chat, history and stats as dashboard tabs and feeds getDefaultDashboardTabs() for permission presets. Any future wiring of dashboard_tabs would gate the wrong tab IDs — same IA drift as the Account/Controls visibility toggles.

Fix effort: Same realignment as tab visibility — single source of truth in lib/dashboard/tabs.ts.

215

Controls member-permissions UI omits dashboard_tabs entirely

Permissions, Settings & Misleading UI

mediumOpenEasy fix

MemberPermissionsSection saves tool_access, tool_features and three edit flags (~79–85) but never renders or writes dashboard_tabs even though updateMemberPermissions supports it. Founders cannot configure per-member tab ACL in the admin UI that was built for it — paired with the dashboard never reading the field.

Fix effort: Add dashboard_tabs matrix to MemberPermissionsSection using updated CONTROLLABLE_TABS.

216

Daily Stats date picker navigates in UTC, not warehouse local

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fixMay affect neighbors

app/daily-stats/DailyStatsClient.tsx fmtDate() uses d.toISOString().split('T')[0] (~84–85) for selectedDate, week navigation (~731–733) and Today (~866). After ~7–8pm Eastern, 'today' becomes tomorrow UTC — parallel to tracked Shanetrée/DockSheet UTC issues but specific to /daily-stats operator date picking.

app/daily-stats/DailyStatsClient.tsx

Fix risk: Aligning to local TZ must match report_date columns in greco_daily_stats or grid shows empty days.

Fix effort: Use America/New_York (or company timezone setting) for fmtDate — same fix class as Greco UTC dates.

217

Dashboard email and report modals lack dialog accessibility

UI, Flow & Density

mediumOpenEasy fix

Email Settings modal (~2837+) and Per-Report Settings modal (~3154+) are fixed inset-0 overlays with no role='dialog', aria-modal, aria-labelledby, Escape-to-close or focus trap. Same gap class as team-chat/developer modals already tracked — but these two high-traffic dashboard modals were missed.

Fix effort: Add role=dialog, aria-modal, Escape and focus trap to emailSettingsModal and reportSettingsModal overlays.

218

Dashboard mentions have no realtime subscription

Agent, API & Docs Integrity

mediumOpenEasy fixMay affect neighbors

dashboard/page.tsx calls loadMyMentions() / loadUnreadMentionCount() once on mount (~436–437, ~914–932). No postgres_changes on team_mentions (contrast team-chat subscribeToChatRoomMessages and calendar subscriptions). New @mentions require manual refresh or navigating away — badge can stay stale on a long-lived dashboard session.

Fix risk: Realtime filter changes alter what live updates appear — easy to hide needed events.

Fix effort: postgres_changes on team_mentions filtered by mentioned_user_id; refresh badge on insert.

219

Dashboard never subscribes to AI conversation comments

Data Flows & Wiring

mediumOpenEasy fix

subscribeToAIConversationComments() exists in lib/supabase/ai-conversations.ts but is never imported. Dashboard subscribes to new conversations via subscribeToAIConversations only — new comments on existing conversations won't appear until manual refresh or the 30s polling interval.

lib/supabase/ai-conversations.ts

Fix effort: Import and wire subscribeToAIConversationComments alongside existing conversation subscription.

220

Dashboard uses blocking alert() for errors

UI, Flow & Density

mediumOpenEasy fix

app/dashboard/page.tsx still calls alert() for AI-filter save failures, external-email add/remove, file download errors, AI summary failures and quote submission accept/reject (~619–1409). Blocking native dialogs interrupt warehouse workflows, can't be styled or translated consistently, and feel out of place beside the polished glass UI elsewhere.

app/dashboard/page.tsx

Fix effort: Replace ~10 alert() calls with inline toast/banner state — reuse existing dashboard error UI patterns.

221

Developer page load failures show empty lists with no error

Data Flows & Wiring

mediumOpenEasy fix

app/developer/page.tsx loadApiKeys and loadWebhooks catch errors with console.error only. UI renders 'No API Keys' / 'No Webhooks' with no error banner — identical to a real empty integration setup. Tier banner failures are also swallowed.

app/developer/page.tsx

Fix effort: Surface fetch errors inline on developer/page.tsx instead of empty-state copy.

222

Developer settings modals lack dialog accessibility

UI, Colors & Aesthetics

mediumOpenEasy fix

app/developer/page.tsx modals at ~1039+ (showCreateKeyModal, webhook create/details) use fixed inset-0 bg-black/50 overlays with inner white panel — backdrop click and X close only. grep role='dialog' / aria-modal / onKeyDown Escape in developer/page.tsx returns zero. Same mouse-only pattern as calendar EventFormModal and members overlay.

app/developer/page.tsx

Fix effort: Same dialog pattern on three developer/page.tsx modals.

223

Developer webhook create accepts arbitrary URL schemes

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fix

app/developer/page.tsx (~358–369) sends newWebhookUrl.trim() to POST /api/settings/webhooks with no https:// requirement or SSRF blocklist. javascript: or internal host URLs can be stored as webhook targets — delivery worker may fail or hit unexpected endpoints when triggered.

app/developer/page.tsx

Fix effort: Require https:// and block private IP ranges on POST /api/settings/webhooks + developer UI validation.

224

digest_recipients and digest_day_of_week are persisted but unused

Agent, API & Docs Integrity

mediumOpenEasy fix

Fields exist in lib/agent/permissions.ts (~29–30, 114–115) and lib/agent/settings-shared.ts, and appear in OpenAPI agent settings schema. AgentSettingsPanel has no recipient picker or day-of-week control; cron/worker ignore both. OpenAPI types digest_day_of_week as string; code uses number — schema drift on top of dead fields.

lib/agent/permissions.tslib/agent/settings-shared.ts

Fix effort: Add recipient/day UI in AgentSettingsPanel + read fields in cron/worker; fix OpenAPI type to number.

225

Dock check-in notifications can be spammed

Rate Limits & Abuse Surface

mediumOpenEasy fix

/api/dockvoo/notify loops the company's notification preferences and sends an email per subscriber (plus per-PO bridge relay calls for time_in/docked events) on every POST — authenticated, but with no rate limit. Any company member can hammer colleagues' inboxes and the bridge with repeated check-in events.

Fix effort: Per-user rate limit on /api/dockvoo/notify.

226

DockSheet bootstraps workspace date in UTC

Greco Warehouse Operations

mediumOpenEasy fixHigh blast radius

CindyVueClient.tsx session bootstrap (~1277–1332) sets const today = new Date().toISOString().slice(0, 10) when picking the active workspace tab. Combined with global LS_DATE_KEY ('cindyVueLastDate' in lib/tools/greco/lib/constants.ts), a receiver opening DockSheet near midnight UTC boundary can land on the wrong session_date relative to the warehouse clock.

lib/tools/greco/lib/constants.ts

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Use warehouse-local date helper in CindyVueClient bootstrap; align with session_date convention.

227

DockSheet Live defaults to UTC today, not warehouse local date

Data Flows & Wiring

mediumOpenEasy fixHigh blast radius

app/live/greco/page.tsx picks today via new Date().toISOString().slice(0, 10) (UTC). Sibling /timer uses localTodayStr() (browser local). After ~7–8pm Eastern, Live can open yesterday's session while Timer opens today's — inconsistent kiosk behavior.

app/live/greco/page.tsx

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Reuse localTodayStr() from timer or America/New_York helper for initial selectedDate.

228

DockSheet Live swallows session load failures as empty data

Data Flows & Wiring

mediumOpenEasy fixCritical — test everything

app/live/greco/page.tsx loadSession() and the dates bootstrap use if (!res.ok) return and empty catch blocks. Network/500 failures render 'No data for this date' — indistinguishable from a genuinely empty dock day. No error banner or retry beyond manual refresh.

app/live/greco/page.tsx

Fix risk: DockSheet session stack — autosave, grid, and public kiosks depend on current save semantics.

Fix effort: Add error state + retry banner in app/live/greco/page.tsx when fetch fails.

229

DockSheet notif-prefs GET leaks coworker auth emails

Security & Trust Boundaries

mediumOpenEasy fix

app/api/dockvoo/notif-prefs/route.ts GET returns companyLog with every watched PO plus user_id, user_email and user_name for all enabled company members. Any authenticated tenant user can enumerate colleagues' auth emails via admin.auth.admin.getUserById in the loop at lines 82–88.

app/api/dockvoo/notif-prefs/route.ts

Fix effort: Return only caller's prefs on GET; move companyLog to admin-only endpoint or strip emails.

230

DockSheet workspace localStorage is not namespaced per operator

Greco Warehouse Operations

mediumOpenEasy fixMay affect neighbors

Beyond tracked kiosk theme keys: CindyVue persists cindyVueLastDate (LS_DATE_KEY), per-day session blobs and workspace tab lists in localStorage with no userId suffix. Shared receiving PCs inherit the previous receiver's active date and offline backup payload — can restore the wrong day's grid after shift change.

Fix risk: Receiver desk PCs share layout/date prefs — namespacing changes which session date opens by default.

Fix effort: Prefix LS_DATE_KEY and per-day keys with userId or shift token on shared PCs.

231

Documents create modal lacks dialog accessibility

UI, Flow & Density

mediumOpenEasy fix

app/documents/page.tsx Create Document overlay (~1162–1286) is a fixed inset-0 div with backdrop click and X to close — no role='dialog', aria-modal, Escape handler or focus trap. Fourth high-traffic modal in the same accessibility gap class (after calendar, members, team-chat, developer).

app/documents/page.tsx

Fix effort: Add role=dialog, aria-modal, Escape and focus trap to showCreateModal overlay.

232

Documents editor: debounce timers survive unmount

Data Flows & Wiring

mediumOpenEasy fix

app/documents/page.tsx schedules typing-indicator, content-diff and history snapshots through timeout refs with no unmount cleanup. Leaving mid-edit can fire broadcasts and setState on an unmounted component. Unsaved-changes confirm() guards in-app nav only — no beforeunload for tab close/refresh.

app/documents/page.tsx

Fix effort: useEffect cleanup clearing three timeout refs + optional beforeunload — documents/page.tsx only.

233

Documents editor: debounce timers survive unmount, and only in-app nav guards unsaved edits

Wiring & Broken Flows

mediumOpenEasy fix

app/documents/page.tsx schedules typing-indicator, content-diff and history snapshots through three timeout refs with no unmount cleanup — leaving the page mid-edit can fire broadcasts and setState on an unmounted component. Separately, the unsaved-changes confirm() only guards in-app doc switching and back-to-list; there's no beforeunload handler, so a tab close or refresh silently discards long-form edits.

app/documents/page.tsx

Fix effort: Same cleanup + beforeunload.

234

Driver Scan loads ExcelJS from unpinned cdnjs without SRI

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fixMay affect neighbors

app/driver-scan/DriverScanClient.tsx (~67) injects https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js with no integrity attribute. Parallel supply-chain risk to tracked Supabase CDN pin in app/layout.tsx — dock-floor machines parse driver scan xlsx through a third-party script tag.

app/driver-scan/DriverScanClient.tsxapp/layout.tsx

Fix risk: Pinning/SRI is safe; swapping loader breaks offline dock tablet if CDN blocked.

Fix effort: Bundle exceljs via npm import or add SRI + version pin — match Entree dynamic import pattern.

235

driver_chat_settings UPDATE RLS allows any company member

Shanetrée, Routing & Driver Comms

mediumOpenEasy fix

supabase/migrations/129_create_driver_chat.sql lines 132–135: UPDATE policy checks only company_id membership, not role. /inbox AI toggles write driver_chat_settings.ai_enabled_all and response_mode — UI implies an operational control, but schema lets any member including viewers change global driver-chat AI behavior via Supabase client or inbox UI.

supabase/migrations/129_create_driver_chat.sql

Fix effort: Tighten UPDATE policy to manager+ roles; mirror in API route checks.

236

Driver-chat AI controls have no role gate

Shanetrée, Routing & Driver Comms

mediumOpenEasy fix

requireGrecoUser() in lib/driver-chat/db.ts (~86–104) only checks profile.company_id === GRECO_COMPANY_ID — any Greco member including viewers can hit /api/driver-chat/ai, /api/driver-chat/settings and manual ai-respond. Combined with driver_chat_settings UPDATE RLS allowing any company member, floor viewers can flip global AI auto-reply without founder/admin role.

lib/driver-chat/db.ts

Fix effort: Restrict /api/driver-chat/settings and ai-respond to founder/admin/manager roles.

237

Driver-chat unread counters are a lost-update waiting to happen

Data Integrity & Races

mediumOpenEasy fix

postMessage() in lib/driver-chat/db.ts sets greco_unread / driver_unread to (staleThreadObject.unread || 0) + 1 — a read-modify-write from a thread object fetched earlier in the request. Two messages arriving close together on one thread can produce a count of 1 instead of 2. Should be an atomic UPDATE … SET greco_unread = greco_unread + 1 (or an RPC).

lib/driver-chat/db.ts

Fix effort: Same atomic increment.

238

Driver-chat unread counters use stale read-modify-write

Backend & Database

mediumOpenEasy fix

lib/driver-chat/db.ts postMessage() (~186–215) inserts message then sets greco_unread = (thread.greco_unread || 0) + 1 using thread object passed from caller's earlier SELECT. Concurrent messages on one thread (two drivers, AI + human) race read-modify-write — count can stay at 1 when 2 arrived. Fix: atomic UPDATE driver_chat_threads SET greco_unread = greco_unread + 1.

lib/driver-chat/db.ts

Fix effort: Atomic UPDATE SET greco_unread = greco_unread + 1 in lib/driver-chat/db.ts or small RPC.

239

Drivers, Transfers and Cycle Count skip getToolPagePermissions

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenEasy fix

app/drivers/DriversClient.tsx, app/transfers/TransfersClient.tsx and app/cyclecount/CycleCountClient.tsx have no getToolPagePermissions import. Viewers get full generate/export. Geronimo permission gap is tracked — these three warehouse tools share the same inconsistency with the /controls permission matrix.

app/drivers/DriversClient.tsxapp/transfers/TransfersClient.tsxapp/cyclecount/CycleCountClient.tsx

Fix effort: Wire getToolPagePermissions gate on export/generate buttons — copy negative-slots pattern per client.

240

Dual React component roots — components/ vs app/components/

Repository Structure Audit (Pass 23)

mediumOpenEasy fix

app/components/ holds 24+ live UI modules (FancyLoader, GodView, GrecoOnlyGuard, etc.) imported across the app. Root components/ holds only ShanetreeWhitepaperSections.tsx, ShanetreeWhitepaperGuideSections.tsx and components/docs/ShanetreeDocsSection.tsx — imported via @/components/ from EntreeClient and app/docs. tsconfig paths map @/* to ./* so both work, but the split confuses every new contributor ('where do I add a component?'). Consolidate under app/components/ or src/components/ with one rule.

app/components/app/docs

Fix effort: Move three root components/ files into app/components/docs/; update @/ imports.

241

Duplicate entry points to the same destinations

Navigation & Flow

mediumPartly fixedEasy fixMay affect neighbors

DockSheet, Guide and Shanetrée each exist as an overview card AND a tab-bar link; AI and Docs are Workspace-folder cards and tab-bar links. The non-Greco overview no longer duplicates AI/Docs/Reports (they live in the Workspace folder now), and the remaining tab-bar links are marked with ↗ as shortcuts. The card is the canonical home; the tab strip is a quick-jump row.

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: IA cleanup — mostly done; remaining overview card vs tab-bar shortcut duplication.

242

Forklift embedding index fails silently

Permissions, Settings & Misleading UI

mediumOpenEasy fix

Only app caller of /api/embeddings is forklift/page.tsx (~699–710): fetch with .catch(() => {}) and no response.ok check. Missing OpenAI key or route errors mean vector indexing fails invisibly — semantic search/RAG for forklift reports never updates with no operator feedback.

Fix effort: Check response.ok in forklift fetch; optional toast when embedding index fails.

243

Four team-activity query helpers are unused

Dead Code & Orphan Features

mediumOpenEasy fix

lib/supabase/team-activity.ts exports getTeamActivityByTool, getTeamActivityByUser, getAnalyticsLog and toggleActivityExcluded (~120 lines) with zero imports. AnalyticsLogPanel bypasses getAnalyticsLog and fetches /api/greco/analytics-log directly (app/components/AnalyticsLogPanel.tsx:104). Built for admin drill-downs that were never surfaced in UI.

lib/supabase/team-activity.tsapp/components/AnalyticsLogPanel.tsx

Fix effort: Delete helpers or expose in Controls/History admin UI if still wanted.

244

generate-from-file accepts unbounded fileProfile JSON

Build, IDE & Custom Tools

mediumOpenEasy fixMay affect neighbors

app/api/custom-tools/generate-from-file/route.ts (~281) parses body with no content-length cap — unlike generate/route.ts (~386–395) which caps at 1MB. Build sends the full client profile (app/build/page.tsx ~643) enabling oversized payloads and memory pressure server-side.

app/api/custom-tools/generate-from-file/route.tsapp/build/page.tsx

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Cap request body size like generate route; reject oversized profiles early.

245

Geocoding route is an open proxy

Security & Trust Boundaries

mediumOpenEasy fixMay affect neighbors

app/api/routing/geocode/route.ts GET (~6–34) accepts any ?q= string, forwards to nominatim.openstreetmap.org with FileDisplay User-Agent, no auth, no withRateLimit. Used by app/tools/greco/routing/RoutingClient.tsx. Abusable as free geocoding relay under FileDisplay IP; Nominatim ToS requires rate limiting — this route has none.

app/api/routing/geocode/route.tsapp/tools/greco/routing/RoutingClient.tsx

Fix risk: Auth or 1 req/sec limit breaks Window Map modal batch geocode on account roster open — Nominatim throttle already painful.

Fix effort: Same rate limit + auth on /api/routing/geocode.

246

Geocoding route is an open, unauthenticated proxy

Security & Trust Boundaries

mediumOpenEasy fixMay affect neighbors

app/api/routing/geocode/route.ts GET (~6–34): no withRateLimit, no getUser()/session check — any caller passes ?q=, server forwards to nominatim.openstreetmap.org with FileDisplay User-Agent. RoutingClient.tsx and account-geocodes Window map depend on it. Abusable as free geocoding relay under FileDisplay egress IP; Nominatim usage policy expects ≤1 req/sec per app.

app/api/routing/geocode/route.ts

Fix risk: Same routing geocode proxy — RoutingClient and account-geocodes depend on unauthenticated access.

Fix effort: Add withRateLimit + requireAuth (or internal token); optional response caching.

247

Geronimo skips getToolPagePermissions — viewers get full export

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenEasy fix

Unlike app/negative-slots/page.tsx (~112–117) and app/licenses/page.tsx (~127–130), app/geronimo/GeronimoClient.tsx has no analytics/AI permission gate. Every Greco member including viewers always gets full generate/print/export — inconsistent with permission matrix on sibling slot tools.

app/negative-slots/page.tsxapp/licenses/page.tsxapp/geronimo/GeronimoClient.tsx

Fix effort: Wire getToolPagePermissions('Geronimo') like negative-slots — gate export buttons.

248

GET /api/dashboard/custom-cards loads every tenant's cards into memory

Agent, Collaboration & Platform APIs

mediumOpenEasy fix

app/api/dashboard/custom-cards/route.ts (~48–66) runs select('*') on custom_dashboard_cards with no company_id SQL filter, then filters in JS by target_user_ids / target_company_ids. Service-role query pulls cross-tenant rows on every dashboard load — multi-tenancy leak in memory plus unnecessary scale cost as card count grows.

app/api/dashboard/custom-cards/route.ts

Fix effort: Add .or(target_user_ids.cs.{id},target_company_ids.cs.{companyId}) SQL filter — stop full-table select.

249

GET /api/greco/session?list=dates leaks up to 200 session dates without auth

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenEasy fixHigh blast radius

app/api/greco/session/route.ts (~23–33) returns session_date + updated_at for the 200 most recent DockSheet days with no authentication. /timer bootstrap calls this (~343–347). Remote callers can fingerprint which warehouse days had receiving activity without loading appointment payloads.

app/api/greco/session/route.ts

Fix risk: Removing or gating list=dates breaks date-picker UX in timer/inbound that enumerates sessions without login.

Fix effort: Require session or CRON_SECRET for list=dates; or return only last 7 days for public timer.

250

GodView project and support chat filters run client-side after fetch

Controls, Admin & Collaboration

mediumOpenEasy fix

getProjectChats and getSupportThreads (~437–494) query without company_id, then filteredData = filteredData.filter(...) in JS. Fetches up to limit cross-tenant rows then discards — wrong total, wasted payload when a company filter is active.

Fix effort: Filter project_messages/support_messages by company_id in SQL before range().

251

GodView updateUserRole has no target validation

Controls, Admin & Collaboration

mediumOpenEasy fix

app/api/admin/godview/route.ts updateUserRole (~893–900): service-role .from('profiles').update({ role: params.role }).eq('id', params.userId) — no verify target belongs to caller's company, no role enum whitelist in function (only HTTP handler trusts client JSON). Platform admin typo on userId can re-role another tenant's account.

app/api/admin/godview/route.ts

Fix effort: Verify target profile.company_id and whitelist allowed roles before update.

252

Greco digest cron uses UTC 'yesterday,' not warehouse local

API & Operations

mediumOpenEasy fixHigh blast radius

app/api/cron/greco-digest/route.ts derives yesterdayStr from new Date().toISOString().split('T')[0] in UTC. For an Eastern warehouse, the 05:00 UTC cron (vercel.json) can digest the wrong calendar day relative to dock operations.

app/api/cron/greco-digest/route.ts

Fix risk: Changing digest window shifts which stats rows email includes — ops runbooks assume current behavior.

Fix effort: Use America/New_York date helper in greco-digest cron (same as forklift fix).

253

greco_card_permissions fails open on query errors

Greco Warehouse Operations

mediumOpenEasy fixHigh blast radius

getMyGrecoCardDenials() in lib/supabase/greco-card-permissions.ts (~113–117) returns empty denial sets (full access) when the Supabase select errors — deliberate comment 'never accidentally hide tools.' Inverse risk: a transient RLS/network failure shows every denied tool/card again, defeating permission matrices until the query succeeds.

lib/supabase/greco-card-permissions.ts

Fix risk: Fail-closed denies entire tool grid when Supabase blips — dock shift starts with empty Tools tab.

Fix effort: Fail closed to last-known denials in sessionStorage, or show 'permissions unavailable' and block tools tab.

254

greco_routes.company_id has no foreign key to companies

Shanetrée, Routing & Driver Comms

mediumOpenEasy fixMay affect neighbors

supabase/migrations/114_create_greco_routes.sql line 18 defines company_id uuid not null with no REFERENCES companies(id). Same migration-hygiene class as tracked account_geocodes FK gap but greco_routes is the live routing planner table — orphaned UUIDs would pass RLS checks that compare to profiles.company_id only.

supabase/migrations/114_create_greco_routes.sql

Fix risk: Adding FK on live data fails if orphan rows exist — needs cleanup migration first.

Fix effort: Migration: REFERENCES companies(id) ON DELETE CASCADE after verifying live UUID types.

255

greco-digest cron TOOL_COLUMNS omits half the live Greco tool suite

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenEasy fix

app/api/cron/greco-digest/route.ts TOOL_COLUMNS (~27–31) lists dockvoo through driver_scan but omits geronimo, licenses, negative_slots, empty_slots, thruput, meat_count, incentives, items, schedule, etc. Daily digest agent_memory and anomaly baselines under-report which tools actually ran — even when those tools write greco_daily_stats.

app/api/cron/greco-digest/route.ts

Fix effort: Expand TOOL_COLUMNS to match DailyStatsClient TOOL_META keys — config change in greco-digest route.

256

Greco-digest silently skips days with no greco_daily_stats row

Agent, API & Docs Integrity

mediumOpenEasy fix

app/api/cron/greco-digest/route.ts (~70–74) returns { success: true, message: 'No stats found… skipping' } when yesterday's stats row is missing. No retry queue or alert. If stats aggregation lags the cron, that day's digest/anomalies never land in agent_memory — quiet data loss.

app/api/cron/greco-digest/route.ts

Fix effort: Retry next run or alert when stats missing; don't return success: true on skip.

257

History load errors render as 'no activity'

Data Flows & Wiring

mediumOpenEasy fix

app/history/page.tsx load() catch (~103–104) and changeLoadLimit catch (~120–121) only console.error — never setLoadError state. HistoryLedger renders 'No activity recorded yet' (HistoryLedger.tsx ~545) when teamActivity=[]. Transient Supabase/RLS failure looks like quiet day. Export path (~181) sets exportError banner — load errors should match.

app/history/page.tsx

Fix effort: Add error banner like export path already has — history/page.tsx.

258

History page load errors render as “no activity”

Wiring & Broken Flows

mediumOpenEasy fix

The initial and paginated loads in app/history/page.tsx catch errors with console.error only, then render the empty ledger. A transient Supabase failure is indistinguishable from a quiet day. (Export errors ARE surfaced — load errors should get the same treatment.) Project-chat sends in app/projects/page.tsx fail equally silently.

app/history/page.tsxapp/projects/page.tsx

Fix effort: Same as History load errors — add error banner instead of empty ledger.

259

HistoryPanel load errors render as empty ledger

Data Flows & Wiring

mediumOpenEasy fix

HistoryPanel catches load failures with console.error only; teamActivity stays [] and HistoryLedger shows empty feed. Transient failures inside embedded tool panels are indistinguishable from quiet activity — separate from the full /history page load issue.

Fix effort: Add error state banner in HistoryPanel.tsx like full /history page should.

260

HistoryPanel slide-over lacks dialog accessibility semantics

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fix

app/components/HistoryPanel.tsx (~154–161) is a fixed overlay with Escape handler (~126–130) but no role='dialog', aria-modal, or aria-labelledby. Embedded in CindyVue, Shorts and Shanetrée modals — screen-reader users may not perceive it as a modal surface. Other modals' a11y gaps are tracked separately.

app/components/HistoryPanel.tsx

Fix effort: Add role='dialog' aria-modal aria-labelledby to HistoryPanel overlay container.

261

IDE file saves have no content size cap

Build, IDE & Custom Tools

mediumOpenEasy fix

PUT app/api/ide/workspaces/[id]/files/route.ts (~139) accepts unbounded content strings. Combined with Monaco onChange calling saveFileContent on every keystroke (app/ide/page.tsx ~500), a large paste can POST megabytes per edit.

app/api/ide/workspaces/app/ide/page.tsx

Fix effort: Cap content length on PUT; debounce Monaco saves (tracked separately).

262

IDE saves on every keystroke with no debounce

Performance & Scale

mediumOpenEasy fix

app/ide/page.tsx MonacoEditor onChange calls saveFileContent immediately for every character typed — a PUT per keystroke (optimistic local update + network). Creates API storms, version-contention reloads and unnecessary DB writes during normal typing.

app/ide/page.tsx

Fix effort: 300ms debounce on saveFileContent in ide/page.tsx onChange.

263

IDE workspace/file operations fail silently

Data Flows & Wiring

mediumOpenEasy fix

app/ide/page.tsx initial loads use .catch(() => {}). Create/delete file paths do if (!response.ok) return with no user feedback. Failed workspace creation also returns quietly. Operators get no toast or inline error when saves, deletes or loads fail.

app/ide/page.tsx

Fix effort: Toast or inline error on !response.ok paths in ide/page.tsx.

264

Inbound kiosk exposes operator and debug schedule UI

UI, Flow & Density

mediumOpenEasy fix

app/inbound/InboundClient.tsx Schedule Outlook section (~704–762) shows upload history (details/summary), PO diff counts (+added / -removed vs previous upload) and refresh/hide controls on the public /inbound kiosk. Floor operators see backend scheduling metadata meant for admins — adds visual noise and leaks operational churn on a scan-first screen.

app/inbound/InboundClient.tsx

Fix effort: Hide upload history, PO diff and refresh behind admin flag or move to /controls; kiosk shows search results only.

265

internal shanetree-autopull accepts unbounded pulled_data JSON

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fixMay affect neighbors

app/api/internal/shanetree-autopull/route.ts parses full JSON body (~124) and writes pulled_data to shanetree_snapshots/pull log (~144+) with no payload size cap. Tracked issue covers hardcoded SHANETREE_AUTOPULL_USER_ID; this is separate — compromised INTERNAL_API_SECRET could POST multi-megabyte ODBC payloads into snapshot rows.

app/api/internal/shanetree-autopull/route.ts

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Reject bodies >4MB before snapshot insert — align with relay payload cap comments.

266

Invalidating one company's API keys flushes every tenant's

Data Integrity & Races

mediumOpenEasy fixMay affect neighbors

apiKeyCache.invalidateCompany(companyId) in lib/cache/service.ts accepts a companyId and then ignores it — it runs cacheDeletePattern('fd:api_key:*'), evicting all companies' cached key hashes. Revoking one tenant's key causes a global cache stampede of re-auth DB lookups. Relatedly, /api/cache's invalidate_user action accepts any target userId without checking it belongs to the caller's company.

lib/cache/service.ts

Fix risk: Per-tenant cache key is correct fix but changes thundering herd timing — all tenants re-auth on any revoke.

Fix effort: Same scoped cache invalidation.

267

Job enqueue has no throttle

Rate Limits & Abuse Surface

mediumOpenEasy fix

/api/jobs/enqueue lets any authenticated session enqueue ai_analysis, batch_email, embedding_generation and data_export jobs with arbitrary payloads, with no rate limit — a single misbehaving client can flood QStash and background_jobs with expensive work while sibling routes are throttled.

Fix effort: Add withRateLimit to /api/jobs/enqueue.

268

Legacy team-chat message CRUD is orphaned

Dead Code & Orphan Features

mediumOpenEasy fix

lib/supabase/team-chat.ts still exports sendTeamMessage, getTeamMessages, getRecentTeamMessages and deleteTeamMessage (~175 lines) targeting team_chat_messages, but grep finds zero imports outside that file. Live /team-chat uses lib/supabase/chat-rooms.ts (sendChatRoomMessage, subscribeToChatRoomMessages). Leftover from pre–chat_rooms migration — confuses readers and duplicates mention-insert logic.

lib/supabase/team-chat.tslib/supabase/chat-rooms.ts

Fix effort: Delete sendTeamMessage/getTeamMessages/getRecentTeamMessages/deleteTeamMessage from team-chat.ts or migrate last caller; keep mention helpers.

269

lib/supabase/anomalies.ts duplicates live service wrappers

Dead Code & Orphan Features

mediumOpenEasy fixMay affect neighbors

The 271-line client module mirrors lib/anomaly/service.ts, but only detectAndNotifyAnomalies and extractForkliftMetrics are imported (app/forklift/page.tsx). getRecentAnomalies, acknowledgeAnomaly, dismissAnomaly, extractPickerMetrics and extractLoaderMetrics in the client file have zero callers — API routes use service.ts directly. ~185 lines of duplicate dead wrappers.

lib/anomaly/service.tsapp/forklift/page.tsx

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Keep only detectAndNotifyAnomalies + extractForkliftMetrics in client module; delete duplicate wrappers.

270

MapPanel caches full customer roster in global localStorage

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fixMay affect neighbors

app/entree/MapPanel.tsx CACHE_KEY = filedisplay:accounts-map:v1 (~49) with no userId or companyId suffix. Shared bridge PCs inherit another operator's cached Entrée accounts roster, geocodes and salesman filters — wrong map pins until manual refresh.

app/entree/MapPanel.tsx

Fix risk: Namespacing keys resets shared-PC preferences until users reconfigure.

Fix effort: Suffix CACHE_KEY with userId or companyId in MapPanel.tsx — match dashboard hidden-cards pattern.

271

Meat-count range queries lack row caps on browser loads

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fix

lib/supabase/meat-counts.ts listMeatCountsForRange() (~314–329) has no .limit(); listMeatCountUploads() pulls .limit(5000) rows then reduces client-side (~350–354). Large date ranges can pull entire meat_counts partitions into browser memory on /meat-counts analytics tabs.

lib/supabase/meat-counts.ts

Fix effort: Add .limit() on listMeatCountsForRange; paginate analytics tabs.

272

Members load failures masquerade as empty team

Data Flows & Wiring

mediumOpenEasy fix

app/members/page.tsx loadOrganizationMembers and loadLeaderboard catch errors with console.error only. UI renders 'No team members found' / 'No activity data yet' with no distinction from a real empty roster or a transient Supabase failure.

app/members/page.tsx

Fix effort: Distinguish error state from empty roster in members/page.tsx.

273

Members quick-action modal is mouse-only

UI, Colors & Aesthetics

mediumOpenEasy fix

app/members/page.tsx member detail overlay is a fixed inset-0 div closed only by backdrop click or X button. No role='dialog', aria-modal, Escape handler or focus trap — keyboard users can't dismiss it or tab through actions safely. Same accessibility gap class as the calendar grid.

app/members/page.tsx

Fix effort: Add Escape handler, focus trap, role=dialog on members overlay.

274

Memory-cleanup cron can skip companies with few expired rows

API & Operations

mediumOpenEasy fix

app/api/cron/memory-cleanup/route.ts discovery loop breaks when newCompanyIds.length === 0 even though more expired agent_memory may exist. If one tenant dominates the first 500-row batch, other tenants' expired rows are never discovered that run.

app/api/cron/memory-cleanup/route.ts

Fix effort: Don't break on newCompanyIds.length === 0 until query returns zero rows; or DISTINCT company_id query.

275

Nine Vercel crons, two of them sub-5-minute

Dependencies & Build Hygiene

mediumOpenEasy fix

vercel.json schedules job-retries every minute and stale-jobs every two minutes — ~2,900 authenticated invocations a day before the other seven jobs. All routes exist and all check CRON_SECRET, but the density is real function-invocation and Supabase load for queues that are usually empty. Worth stretching the intervals or moving retry logic into the worker itself.

Fix effort: Stretch job-retries and stale-jobs intervals in vercel.json if ops agrees.

276

Nine Vercel crons, two sub-5-minute

Code Quality & Hygiene

mediumOpenEasy fix

vercel.json lists 9 crons: job-retries */1 * * * * (~line 4) and stale-jobs */2 * * * * (~line 9) fire every 1–2 minutes; plus agent-autonomy */10, webhooks */5, port-ai-summary hourly, etc. ~2,900 invocations/day from the two fast crons alone. Routes check CRON_SECRET but queues are often empty — real serverless + Supabase load for retry sweeps that could run inside workers or at 15-min intervals.

Fix effort: Stretch job-retries to 5m, stale-jobs to 5m in vercel.json — config-only if ops agrees.

277

No CONTRIBUTING.md or CODEOWNERS — contributor and review norms undocumented

Repository Structure Audit (Pass 23)

mediumOpenEasy fix

LICENSE.md and AGENTS.md exist (Cursor Cloud oriented) but no human CONTRIBUTING.md (branch naming, commit style, moneyfund push remote, problems-page workflow). No CODEOWNERS for app/api/, supabase/migrations/, or entree-bridge.js. External collaborators or future hires lack a single 'how we work' entry point beyond scattered .cursorrules.

app/api/supabase/migrations/

Fix effort: Add CONTRIBUTING.md from .cursorrules + commit style; CODEOWNERS for migrations and api/.

278

No security headers anywhere

Security & Trust Boundaries

mediumOpenEasy fix

There's no middleware.ts and next.config.ts defines no headers() — the app ships without CSP, HSTS, X-Frame-Options, X-Content-Type-Options or Referrer-Policy. For a multi-tenant SaaS with admin routes and public kiosk pages, a baseline headers() block in next.config is cheap insurance against clickjacking and MIME sniffing.

Fix effort: Same next.config.ts headers block.

279

No security headers configured

Security & Trust Boundaries

mediumOpenEasy fix

No middleware.ts and next.config.ts defines no headers() — the app ships without CSP, HSTS, X-Frame-Options, X-Content-Type-Options or Referrer-Policy. Baseline headers() in next.config is cheap insurance for a multi-tenant SaaS with admin routes and public kiosk pages.

Fix effort: Add headers() block in next.config.ts (CSP may need iteration) — ~20 lines, test on Vercel preview.

280

notification_settings updates return success when the table is missing

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fix

lib/supabase/notifications.ts updateNotificationSetting() (~181–183) and updateNotificationSettings() (~217–220) return { success: true } when the relation does not exist. Account email toggles look saved on fresh/partial DBs but persist nothing — operators think notifications are configured when the table was never migrated.

lib/supabase/notifications.ts

Fix effort: Return success:false when relation missing; surface 'notifications not configured' in account UI.

281

OpenAPI spec omits live Calendar endpoints

Agent, API & Docs Integrity

mediumOpenEasy fix

/api/v1/calendar/events and /api/v1/calendar/available-slots work with API keys, but grep 'calendar' in app/api/v1/openapi.json/route.ts returns zero matches — no Calendar tag or paths. Developer docs treat OpenAPI as the v1 contract; calendar is invisible there (batch blocking is tracked separately in API & Operations).

app/api/v1/openapi.json/route.ts

Fix effort: Add /api/v1/calendar/events and available-slots paths to openapi.json route.

282

Picker header shows four icon-only toggles on mobile

UI, Flow & Density

mediumOpenEasy fix

app/picker/page.tsx header packs AI, Analytics, Filters and Test toggles with labels hidden sm:inline (~1377–1438). On warehouse phones operators see four identical-looking icon buttons with no text — easy to tap the wrong mode (e.g. Test vs Filters).

app/picker/page.tsx

Fix effort: Show abbreviated labels on xs, tooltips with aria-label, or collapse secondary toggles into one menu.

283

port_summary_memory expires_at entries are never pruned

Backend & Database

mediumOpenEasy fix

supabase/migrations/121_create_port_summary_memory.sql adds expires_at with a comment that a prune task is coming, but daily memory-cleanup cron only calls pruneExpiredMemory() on agent_memory. port_summary_memory rows with past expires_at accumulate and keep feeding /port AI context.

supabase/migrations/121_create_port_summary_memory.sql

Fix effort: Extend memory-cleanup cron or add pruneExpiredPortSummary() for port_summary_memory table.

284

POST /api/jobs lets any company member cancel or retry jobs

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fix

app/api/jobs/route.ts POST (~148–192) verifies session + company_id ownership but has no role gate — viewers can cancel/retry background jobs. No withRateLimit on cancel/retry path. Expensive ai_analysis or export jobs can be disrupted or re-queued by any authenticated seat.

app/api/jobs/route.ts

Fix effort: Require editor+ role for cancel/retry; add withRateLimit on POST.

285

Presence display names never update after async profile load

Data Flows & Wiring

mediumOpenEasy fix

lib/realtime/useCompanyPresence.ts loads full_name async (~43–61) into userName state, but presence channel join effect deps are [companyId, userId] only (~77) — initial track uses userName || 'User' at ~85 before profile resolves. Auto-update effect (~153–157) syncs page/resourceId/mode but not userName. updatePresence callback deps [userId] only (~149). Collaborators who connect before profile fetch completes stay labeled 'User' for the session.

lib/realtime/useCompanyPresence.ts

Fix effort: Add userName to useCompanyPresence effect deps or re-trackPresence after profile loads.

286

project_updates email toggle is a no-op

Permissions, Settings & Misleading UI

mediumOpenEasy fixHigh blast radius

Account exposes project_updates (app/account/page.tsx ~67). notifyProjectUpdate() exists in lib/notifications/trigger.ts but grep finds zero callers outside its definition — no project status change anywhere triggers it. Third decorative notification category beside the tracked chat/mention gaps.

app/account/page.tsxlib/notifications/trigger.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Call notifyProjectUpdate on status change in projects flow, or hide toggle.

287

Projects page loads without a session gate

Auth & Session

mediumOpenEasy fix

app/projects/page.tsx useEffect calls loadQuoteSubmissions() on mount with no auth check. getUserQuoteSubmissions() throws 'You must be logged in' which is caught and console.error'd — logged-out visitors see an empty projects list with no redirect. Auth is only checked inside sendProjectChatMessage().

app/projects/page.tsx

Fix effort: Redirect to /login when getUserQuoteSubmissions throws or on mount session check.

288

Public GET leaks every invoice printed from /sales

Window, Sales & Kiosk Scheduling

mediumOpenEasy fix

GET /api/public/will-call-print-request with no id param (route.ts ~138–165) returns up to 5000 invno values where requester='sales' and status IN (pending, printing, done) — unauthenticated. Reveals operational print history to anyone on the internet; complements tracked print-once UX bug with a separate data-disclosure angle.

Fix effort: Require kiosk token or remove unscoped GET; scope printed list to authenticated staff.

289

Public kiosk localStorage keys are not namespaced per session

Permissions, Settings & Misleading UI

mediumOpenEasy fixCritical — test everything

Beyond homepage-bg-theme and greco guard cache (tracked elsewhere): /window persists window-my-requests + window-theme (WindowClient.tsx ~35–36), /sales uses sales-theme (~54), /timer uses timer-theme (~27). Shared dock PCs inherit prior visitor appointment lists and dark/light preferences — no user or company suffix on keys.

Fix risk: Locking down public kiosk APIs without token replacement breaks wall-mounted workflows.

Fix effort: Suffix window/sales/timer theme and request keys with session id or kiosk device label.

290

Public shanetree-latest exposes operator identity in snapshot payloads

Shanetrée, Routing & Driver Comms

mediumOpenEasy fix

app/api/public/shanetree-latest/route.ts selects user_id on every row (~81–83) and returns the full snapshot including payload. useShanetreeSnapshot.ts embeds user_email and user_id inside payload.__meta (~60–61, 170–172). Kiosk readers on /adrian, /noe and /inbound therefore see who captured routes, will-calls or schedule data — PII on a world-readable endpoint.

app/api/public/shanetree-latest/route.ts

Fix effort: Strip user_id and payload.__meta from public response; keep full row on authenticated snapshot API.

291

Public tool share URLs (/t/{share_id}) have no in-app surfacing

Permissions, Settings & Misleading UI

mediumOpenEasy fix

v1 tools/generate returns share_url at /t/{share_id} and app/t/[id]/page.tsx renders public tools, but build/page.tsx links to /custom-tools/{id} (auth required) and never shows 'copy public link.' Public-share infrastructure works; product UI never exposes it to builders.

app/t/

Fix effort: Show copy-share-link on build/custom-tools success; surface share_url from v1 generate.

292

Public window API accepts past dates and off-grid times

Window, Sales & Kiosk Scheduling

mediumOpenEasy fix

POST /api/public/window-appointments validates DATE_RE and TIME_RE only (~216–220) — no comparison to today and no whitelist against buildTimeSlots() in lib/window/constants.ts (~101–108). Client uses min={localTodayStr()} (WindowClient.tsx ~362) but direct API calls can book 2020-01-01 at 03:17 outside the 7:00–16:30 grid the UI shows.

lib/window/constants.ts

Fix effort: Reject date < today and time not in buildTimeSlots() whitelist server-side.

293

Quote uploads have no file-size cap before base64 JSONB insert

Performance & Scale

mediumOpenEasy fix

app/quote/page.tsx accepts arbitrary file count/size via <input type='file' multiple /> with no size check. lib/supabase/quote-submissions.ts encodes every file to data_base64 in quote_submissions.file_attachments JSONB with no server guard — large spreadsheets can blow request size and Postgres row weight.

app/quote/page.tsxlib/supabase/quote-submissions.ts

Fix effort: Client max size check + server rejection in quote-submissions.ts (e.g. 5MB/file).

294

Reports tab footer crowds actions on narrow screens

UI, Flow & Density

mediumOpenEasy fix

Each report card footer (~2577–2633) packs timestamp, 'include input file?' checkbox, settings gear and Send Email button into one flex justify-between row. On mobile the checkbox label (text-[9px]) and Send Email button compete for width — actions wrap awkwardly or truncate beside the timestamp instead of stacking.

Fix effort: Stack footer on sm breakpoint — timestamp row, then checkbox, then action buttons; or move include-input into settings modal.

295

Reset-password accepts any active session, not just recovery

Auth & Session

mediumOpenEasy fix

app/reset-password/page.tsx onAuthStateChange sets sessionReady on PASSWORD_RECOVERY or SIGNED_IN, and getSession() also sets sessionReady for any existing session (lines 36–39). A normally logged-in user visiting /reset-password can set a new password with no old-password confirmation — blurs recovery vs in-session password change.

app/reset-password/page.tsx

Fix effort: Only set sessionReady on PASSWORD_RECOVERY event, not generic SIGNED_IN/getSession.

296

Routing APIs accept any authenticated tenant, not Greco-only

Shanetrée, Routing & Driver Comms

mediumOpenEasy fixHigh blast radius

/api/routing/routes, /api/routing/routes/[id] and /api/routing/ai-build authenticate any user with profiles.company_id (routes/route.ts ~12–33) — no NEXT_PUBLIC_GRECO_COMPANY_ID pin unlike driver-chat requireGrecoUser(). On a multi-tenant deploy, non-Greco companies can store routes in greco_routes and invoke routing AI. Concrete unguarded surface beyond the broad Greco-hardcode note in older audit sections.

Fix risk: Pinning to Greco UUID breaks routing tool for any future non-Greco tenant already using /routing.

Fix effort: Pin routing routes to GRECO_COMPANY_ID like driver-chat requireGrecoUser.

297

Routing planner defaults new routes to UTC today

Shanetrée, Routing & Driver Comms

mediumOpenEasy fixHigh blast radius

freshRoute() in app/routing/RoutingClient.tsx (~96) sets date: new Date().toISOString().split('T')[0]. Eastern evening dispatchers opening a blank route get tomorrow's route_date before touching the picker — same warehouse-clock skew as Entree todayStr() (pass 10) but /routing is a separate daily workflow.

app/routing/RoutingClient.tsx

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Use shared warehouseLocalDateStr() in freshRoute() like other Greco date fixes.

298

sales-relay POST creates a new relay row on every call

Window, Sales & Kiosk Scheduling

mediumOpenEasy fix

app/api/public/sales-relay/route.ts (~116–134) inserts into shanetree_relay_requests with no per-date/invno dedupe or cooldown. Day-switching, refresh loops or multiple /sales kiosks can stack duplicate /will-calls jobs for the same date. Claim RPC global scope is tracked separately — this is enqueue-side duplication.

app/api/public/sales-relay/route.ts

Fix effort: Dedupe pending jobs for same endpoint+date/invno before insert.

299

scripts/fake-lockscreen and scripts/popup-killer are kiosk hacks inside the SaaS repo

Repository Structure Audit (Pass 23)

mediumOpenEasy fix

git tracks scripts/fake-lockscreen/ (PowerShell + VBS lock-screen overlay) and scripts/popup-killer/ (auto-close print dialogs) — 8 files with autostart installers. Legitimate for a dedicated bridge PC playbook, but jarring in a multi-tenant SaaS repository a customer or auditor might clone. Relocate to bridge-deploy/ or a private ops repo; keep only scripts/refresh-platform-stats.mjs and migration helpers in the product tree.

Fix effort: Move to bridge-deploy/ or private ops repo; remove from main product tree.

300

Seven operator/setup markdown files sit at repo root without a docs/ folder

Repository Structure Audit (Pass 23)

mediumOpenEasy fix

AGENTS.md, MULTI_TENANCY_SETUP.md, SHANETREE_REMOTE_SETUP.md, TOOL_REPORTS_SETUP.md, COMPANY_ID_VS_EMAIL_DOMAIN.md, QUICK_START.md and LICENSE.md share root with package.json. No docs/ index, no mkdocs/vitepress site, no cross-links. Professional OSS/SaaS repos use docs/ or a single DOCUMENTATION.md hub — root looks cluttered to GitHub visitors and Vercel importers.

Fix effort: Create docs/; move setup markdowns; add docs/README.md hub linking AGENTS + QUICK_START.

301

Shanetrée autopull logs pull success before snapshot insert completes

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenEasy fix

app/api/internal/shanetree-autopull/route.ts (~177–190) inserts status:success into shanetree_pull_log before shanetree_snapshots insert (~222+). On snapshot failure it writes error log (~242–256) but leaves the earlier success pull row — Pull Log shows completed pull with no matching snapshot for operators debugging auto-pull.

app/api/internal/shanetree-autopull/route.ts

Fix effort: Insert pull_log only after snapshot succeeds, or update status to failed on snapshot error.

302

Shanetrée modal permissions fail open on query errors

Shanetrée, Routing & Driver Comms

mediumOpenEasy fixHigh blast radius

getMyDeniedModals() in lib/supabase/shanetree-modal-permissions.ts (~92–96) returns an empty Set (full modal access) when the Supabase read fails — mirror of greco_card_permissions fail-open. A transient RLS/network error unlocks modals an admin denied in /controls until the query succeeds.

lib/supabase/shanetree-modal-permissions.ts

Fix risk: Fail-closed change denies access on transient errors — opposite of today's permissive behavior.

Fix effort: Fail closed to last-known denials in sessionStorage or block Entree until permissions load.

303

shanetree_pull_log / shanetree_snapshots lack composite indexes

Backend & Database

mediumOpenEasy fix

lib/supabase/shanetree-pull-log.ts and shanetree-snapshots.ts order by created_at DESC scoped to a company. Migrations 116 and 118 index company_id and created_at separately, not as (company_id, created_at). Postgres must combine two indexes or sort a larger slice as pull-log and snapshot volume grows.

lib/supabase/shanetree-pull-log.ts

Fix effort: Migration: CREATE INDEX ON (company_id, created_at DESC) for both tables.

304

Six ai-conversations exports beyond subscribeToAIConversationComments are orphaned

Dead Code Hunt (Pass 22)

mediumOpenEasy fix

lib/supabase/ai-conversations.ts exports logAIConversation() (~35), getAIConversationsWithComments() (~128), getAIConversationsByUser() (~178), addConversationComment() (~204), deleteConversationComment() (~258) and getConversationComments() (~282) — all with zero external callers. Dashboard uses getAIConversations + subscribeToAIConversations only; comment CRUD and enriched list queries were built but never surfaced (~250 lines). subscribeToAIConversationComments orphan is tracked separately in Hygiene.

lib/supabase/ai-conversations.ts

Fix effort: Delete comment CRUD exports or add dashboard conversation thread UI.

305

Team-chat create modal lacks dialog accessibility

UI, Colors & Aesthetics

mediumOpenEasy fix

app/team-chat/page.tsx New Chat overlay is a fixed inset-0 div with no role='dialog', aria-modal, Escape-to-close or focus trap. Keyboard users cannot dismiss it safely or tab through member selection — same class as the Members modal gap.

app/team-chat/page.tsx

Fix effort: Add role=dialog, aria-modal, Escape handler and focus trap to team-chat New Chat overlay.

306

Thruput never logs to the analytics log or sends report notifications

Greco Warehouse Operations

mediumOpenEasy fix

grep logReportGeneration and notifyReportGenerated under app/thruput/ returns zero matches. Every other major warehouse report tool emits team_activity_log rows and optional email/webhook notifications on save. Thruput night-shift data stays invisible to Daily Stats Analytics Log, leaderboard scoring and notifyReportGenerated subscribers.

app/thruput/

Fix effort: Call logReportGeneration + notifyReportGenerated on saveThruputDay like other warehouse tools.

307

ToolAPI bridge reports success when cloud save failed

Data Flows & Wiring

mediumOpenEasy fix

lib/custom-tools/bridge.ts responds { success: true, fallback: true } to the iframe when /api/custom-tools/data returns an error or throws (lines 648–656). Custom tools and Build preview show success while data exists only in browser localStorage — silent data loss on shared machines or after cache clear.

lib/custom-tools/bridge.ts

Fix effort: Return success: false to iframe on API failure; let tool show error instead of silent localStorage fallback.

308

ToolAPI cloud-save fallback localStorage is not namespaced per user

Build, IDE & Custom Tools

mediumOpenEasy fixMay affect neighbors

lib/custom-tools/bridge.ts (~434–435) uses fd_toolapi_fallback_${toolId} when cloud save fails. Tracked preview keys (fd_build_preview_tool_data_v1) are separate — this persisted-fallback path bleeds across operators on shared warehouse PCs.

lib/custom-tools/bridge.ts

Fix risk: Cloud-save failure path bleeds tool state across logins — fix changes offline fallback semantics.

Fix effort: Prefix fd_toolapi_fallback keys with userId like preview storage fix.

309

triggerItemReceivedAlert is never wired — only checked-in stage fires

Dead Code Hunt (Pass 22)

mediumOpenEasy fix

lib/inbound/item-alerts.ts:172 exports triggerItemReceivedAlert() for the 'item received' email stage. app/api/dockvoo/notify/route.ts imports and calls triggerItemCheckedInAlert only (~148). The received-stage alert path and its email template wiring are dead — operators never get the intermediate notification even if item_received template exists in EmailService.

lib/inbound/item-alerts.tsapp/api/dockvoo/notify/route.ts

Fix effort: Call triggerItemReceivedAlert from dockvoo notify earlier stage or remove item_received template path.

310

v1 batch executes every operation in parallel via Promise.all

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fixMay affect neighbors

app/api/v1/batch/route.ts (~136–179) runs all operations concurrently with no concurrency cap. Large batches (reports + search + files) can hammer rate limits, Postgres and downstream APIs simultaneously — thundering herd from a single API key.

app/api/v1/batch/route.ts

Fix risk: Serializing ops changes latency profile; rate limits may trip differently for batch clients.

Fix effort: Add concurrency limit (e.g. p-limit 3) over batch operations — batch/route.ts.

311

v1 POST /filters accepts unbounded filter_prompt strings

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fixMay affect neighbors

app/api/v1/filters/route.ts requires filter_prompt as a string (~78–80) but enforces no max length before upserting into ai_report_filters (~95–107). API keys can store multi-megabyte prompts later injected into AI system instructions (including via v1 analyze).

app/api/v1/filters/route.ts

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Cap filter_prompt length (e.g. 8KB) — match ai_report_filters UI limits.

312

v1 POST /reports/analyze stores analyses under a nil system user UUID

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fix

app/api/v1/reports/analyze/route.ts inserts user_id: '00000000-0000-0000-0000-000000000000' (~229) into tool_ai_analyses. Activity ledger, analytics attribution and per-user analysis history cannot distinguish which API key or operator triggered the run.

app/api/v1/reports/analyze/route.ts

Fix effort: Store api_key.created_by or a dedicated api_service user per company — migration optional for history.

313

v1 POST /search returns full embedding content_text bodies

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenEasy fix

app/api/v1/search/route.ts maps every result field including content_text (~76). Semantic search can return large report/analysis text blobs to any API key with semantic_search + reports:read — bulk export surface beyond summaries, including PII from embedded report JSON.

app/api/v1/search/route.ts

Fix effort: Return content_summary only by default; opt-in include_full_text flag for API consumers who need it.

314

v1 schedules PUT never recalculates next_run_at after edits

Email, v1 API & Entrée Panel Polish

mediumOpenEasy fix

app/api/v1/schedules/[id]/route.ts (~103–117) updates cron/timezone/interval but leaves next_run_at stale. Operators editing a schedule's cron via API do not shift the next fire time — combined with write-only schedules table, edits look saved but timing semantics are broken.

app/api/v1/schedules/

Fix effort: Recompute next_run_at on PUT when cron/timezone/interval change — schedules/[id]/route.ts.

315

v1 tools API stores created_by as the API key UUID

Build, IDE & Custom Tools

mediumOpenEasy fix

app/api/v1/tools/route.ts (~104) and v1 tools/[id]/data write created_by:apiKey.id, not the key creator's user UUID. custom_tools.created_by references auth.users(id) per migration 075 — corrupts attribution and may violate FK unless keys happen to match user ids.

app/api/v1/tools/route.ts

Fix effort: Store apiKey.userId or key creator profile id instead of apiKey.id.

316

Webhook cron lock table does not exist — locking is fail-open

API & Operations

mediumOpenEasy fixHigh blast radius

app/api/cron/webhooks/route.ts acquireCronLock upserts into cron_locks, but no migration creates that table (grep cron_locks in supabase/migrations returns zero). On error it returns true ('skip locking and proceed anyway'). With webhooks scheduled every 5 minutes, overlapping retry runs are possible.

app/api/cron/webhooks/route.tssupabase/migrations

Fix risk: Fail-closed change denies access on transient errors — opposite of today's permissive behavior.

Fix effort: Migration creating cron_locks table + keep fail-closed on lock miss if desired.

317

x-api-user-id is spoofable on custom-tools/generate

Build, IDE & Custom Tools

mediumOpenEasy fix

app/api/custom-tools/generate/route.ts (~416–428): when a valid API key matches x-api-company-id, x-api-user-id is trusted for attribution and downstream inserts without verifying the UUID belongs to the key creator. API consumers can attribute AI usage to arbitrary user IDs within the tenant.

app/api/custom-tools/generate/route.ts

Fix effort: Bind user id to API key creator profile; ignore client x-api-user-id except internal secret path.

318

/quotes-admin is tenant-scoped despite admin chrome

Controls, Admin & Collaboration

mediumOpenModerate effort

loadSubmissions in app/quotes-admin/page.tsx selects all rows but RLS (migration 012 SELECT policy) limits to caller's company_id. Password gate + 'admin' UI imply platform-wide quote review; data is single-tenant unless the operator belongs to each company.

app/quotes-admin/page.tsx

Fix effort: Use service-role /api/admin/submissions for list or relabel UI as company admin.

319

/scan re-implements the tool detector

Code Quality & Hygiene

mediumOpenModerate effort

app/scan/page.tsx (~103–273) implements its own columnMatches(), required/unique/exclude scoring, and 0.7 auto-route threshold inline. lib/tools/detector.ts exports detectTool(), detectAllTools(), calculateMatchScore() with the same 0.7 threshold (~72) and TOOL_SIGNATURES from lib/tools/registry.ts. Scan page never imports detector.ts — two implementations that drift when column aliases change in processors.

app/scan/page.tsxlib/tools/detector.tslib/tools/registry.ts

Fix effort: Replace scan/page.tsx logic with lib/tools/detector.ts import — verify parity on test files.

320

/window and /windowadmin are English-only

Window, Sales & Kiosk Scheduling

mediumOpenModerate effort

app/window/WindowClient.tsx and app/windowadmin/WindowAdminClient.tsx have zero useLanguage imports. /sales uses bilingual t(en, es) throughout (SalesClient.tsx). Spanish-preference warehouse users get English scheduling and staff review UIs on the will-call booking path.

app/window/WindowClient.tsxapp/windowadmin/WindowAdminClient.tsx

Fix effort: Wrap WindowClient and WindowAdminClient copy with useLanguage like SalesClient.

321

/windowadmin Will Calls tab has no date picker — latest snapshot only

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenModerate effort

WindowAdminClient.tsx WillCallsPanel (~340–361) fetches only GET /api/public/shanetree-latest?modal=willCalls once on mount. Staff reviewing appointment requests cannot see will-call context for prior/future days without leaving admin for /sales or Shanetrée — limits operational review of booking backlog.

Fix effort: Add date picker + shanetree_snapshots or will_call_appointments query by date — WindowAdminClient.tsx.

322

18 Windows executables and launchers are tracked without an ops/ namespace

Repository Structure Audit (Pass 23)

mediumOpenModerate effort

git ls-files lists 18 paths ending in .exe, .bat, .vbs, or .ps1 — SumatraPDF.exe, start-shanetree-autoprint.bat, scripts/*.ps1, scripts/start-entree-bridge-hidden.vbs, heal-and-restore.ps1, etc. No CONTRIBUTING.md explains which are dev-only. Professional layout: ops/windows/ or bridge/ with a single SETUP-BRIDGE.md index linking install-bridge-autostart.ps1 and verify-architecture.ps1.

Fix effort: Consolidate under ops/windows/ with SETUP-BRIDGE.md index; gitignore or LFS for any remaining binaries.

323

79 flat route folders under app/ with no domain grouping

Repository Structure Audit (Pass 23)

mediumOpenModerate effort

ls app/ shows 79 top-level entries — forklift, picker, entree, timer, sales, controls, problems, phl, produce, etc. all siblings. No app/(warehouse)/, app/(kiosk)/, app/(admin)/ or app/(platform)/ route groups. Next.js supports parentheses groups without changing URLs; grouping would document IA, enable shared layouts per surface, and stop the app/ directory reading like an uncurated dump of every URL ever shipped.

Fix effort: Introduce app/(warehouse), (kiosk), (platform) route groups without URL changes — incremental moves.

324

AgentGenerativeUI eagerly pulls eight @nivo packages into /chat

Monolith Files & Bundle Weight

mediumOpenModerate effort

app/components/AgentGenerativeUI.tsx (2,047 lines) static-imports recharts plus ResponsiveSankey, Chord, Bump, Stream, SwarmPlot, CirclePacking, Network, Sunburst, ParallelCoordinates, and Marimekko from @nivo/* (~13–23). app/chat/page.tsx and daily-stats/GrecoAnalyticsChat.tsx import it at module top — opening Team Chat downloads the full nivo suite even when the conversation has no generative UI widgets. Lazy-load per chart type from spec.kind.

app/components/AgentGenerativeUI.tsxapp/chat/page.tsx

Fix effort: Switch on spec.kind with dynamic(() => import('@nivo/sankey')) per chart type — reduces /chat first load; test all generative UI widget kinds.

325

Aggressive polling loops

Scalability & Performance

mediumOpenModerate effortHigh blast radius

app/entree/EntreeClient.tsx will-call/auto-print setInterval(..., 3_000) at ~27220 and ~27363. app/live/greco/page.tsx (~128–129) reloads full session every 10_000 ms without document.hidden check. Some EntreeClient heartbeat loops (~10787, ~11296) skip hidden tabs; not universal. dashboard 30s refresh and inbox check visibility — kiosk paths do not. Dock screens open all shift → sustained load for rarely-changing data.

app/entree/EntreeClient.tsxapp/live/greco/page.tsx

Fix risk: Duplicate — EntreeClient and /live/greco polling is load-bearing for will-call print and session sync.

Fix effort: Same polling → Realtime/backoff pass.

326

Aggressive polling loops on kiosk screens

Performance & Scale

mediumOpenModerate effortHigh blast radius

app/entree/EntreeClient.tsx will-call claim and auto-print watchers use setInterval(..., 3_000) at ~27220 and ~27363 — some ticks skip when document.hidden, not all loops. /live/greco/page.tsx (~128–129) reloads full DockSheet session every 10_000 ms with no visibility gate. Shanetrée heartbeat loops use 5 min / 30 min intervals (~10787, ~11296) with hidden-tab skips on some paths only. Multiply by dock kiosks left open all shift — sustained Postgres reads for rarely-changing data.

app/entree/EntreeClient.tsx

Fix risk: Stretching intervals makes wall mounts show stale dock/session data — operators rely on 3–10s refresh during receiving.

Fix effort: Replace 3s/10s polls with Realtime or Page Visibility backoff — touches Shanetrée, live/greco, will-call loops.

327

ai_analysis job emailRecipients is an authenticated email relay via the queue

Security & API Gaps (Pass 24)

mediumOpenModerate effortHigh blast radius

app/api/jobs/workers/ai-analysis/route.ts (~179–196): when payload.sendEmailOnComplete and payload.emailRecipients are set, worker enqueues batch_email to arbitrary addresses under FileDisplay's Resend domain. app/api/jobs/enqueue/route.ts (~141–145) spreads user payload into enrichedPayload with no recipient allowlist. Any authenticated user can enqueue ai_analysis (ALLOWED_JOB_TYPES) with external emails — parallel open relay to tracked /api/email/send gap.

app/api/jobs/workers/ai-analysis/route.tsapp/api/jobs/enqueue/route.ts

Fix risk: Allowlist must include legitimate batch_email paths from picker/forklift notify chains, not only company members.

Fix effort: Validate emailRecipients against company member emails in enqueue worker; reject external domains.

328

ai-usage-rollup cron loads entire month of ai_usage_logs into Node memory

Security & API Gaps (Pass 24)

mediumOpenModerate effort

app/api/cron/ai-usage-rollup/route.ts (~26–29) selects all ai_usage_logs rows gte monthStart with no .limit() — entire current month across all tenants into monthLogs array. Second query (~92–96) pulls 8-day window for anomaly detection, also unbounded. Client-side for loops aggregate in Node. Busy month + many tenants → 1 GB Vercel memory exhaustion, stale rollups. Distinct from tracked upsert-failure gap — this is read-side scale.

app/api/cron/ai-usage-rollup/route.ts

Fix effort: Replace client-side loops with Postgres RPC GROUP BY company_id or chunked .range() pagination.

329

Anomaly baselines lose samples under concurrent processing

Data Integrity & Races

mediumOpenModerate effort

updateBaseline() in lib/anomaly/service.ts SELECTs the employee_baselines row, appends to recent_values and recomputes stats in JS, then upserts the whole JSONB back. Two reports processing in parallel for the same employee/metric silently drop one sample (last writer wins), quietly skewing the mean and std-dev the detector compares against. Needs a row lock, version column, or SQL-side aggregation.

lib/anomaly/service.ts

Fix effort: Same concurrent baseline fix.

330

Anomaly baselines lose samples under concurrent updates

Backend & Database

mediumOpenModerate effortMay affect neighbors

lib/anomaly/service.ts updateBaseline() (~162–191): SELECT employee_baselines row, push newValue onto recent_values in JS, recompute day_of_week_stats, upsert whole JSONB. Parallel report processing for same employee/metric/tool is last-writer-wins — one sample silently dropped, mean/std-dev skew. No row lock, version column, or SQL-side jsonb_set append.

lib/anomaly/service.ts

Fix risk: Row locks during forklift save burst may slow report processing — trade throughput for accuracy.

Fix effort: SQL-side jsonb append RPC with row lock, or version column — lib/anomaly/service.ts refactor.

331

Anomaly detectAnomalies does N+1 DB round-trips per employee metric

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenModerate effort

lib/anomaly/service.ts detectAnomalies() (~251–268) loops metrics sequentially, calling getBaseline() then updateBaseline() (each upserts employee_baselines) per employee/metric. A forklift report with 30 employees × 5 metrics can issue ~300 sequential queries on save — tracked baseline race is separate; this is per-save latency and connection churn.

lib/anomaly/service.ts

Fix effort: Batch baseline fetch/upsert in one RPC or Promise.all with concurrency cap — lib/anomaly/service.ts.

332

app/problems/page.tsx is a 4,800-line meta-file — split before pass 24

Repository Structure Audit (Pass 23)

mediumOpenModerate effort

The /problems audit itself lives in one page.tsx (NEW_CATEGORIES + CATEGORIES + tab UI). needs.tsx was extracted for Biggest Needs; ranked.ts, taxonomy.ts, fix-metadata.ts exist — but 22 audit passes still append to page.tsx. Next structural step: move NEW_CATEGORIES slices to app/problems/passes/*.ts or one file per pass. Prevents the documentation system from becoming the next EntreeClient-scale monolith.

app/problems/passes/

Fix effort: Extract NEW_CATEGORIES to app/problems/passes/*.ts like needs.tsx pattern.

333

Archived activity log is invisible to authenticated users

Backend & Database

mediumOpenModerate effort

supabase/migrations/089_team_activity_log_archive.sql enables RLS on team_activity_log_archive with zero SELECT policies (service-role only). After activity-log-cleanup cron moves rows, they vanish from /history and HistoryPanel with no UI to query the archive.

supabase/migrations/089_team_activity_log_archive.sql

Fix effort: Add SELECT RLS on archive matching company_id + optional /history?archive= UI.

334

Build file profiler scans every spreadsheet row client-side

Build, IDE & Custom Tools

mediumOpenModerate effortHigh blast radius

lib/custom-tools/file-profiler.ts (~124–145) iterates all data rows per sheet. Build file mode (app/build/page.tsx ~631) runs this in-browser before generate-from-file — large uploads can freeze the tab before the API is hit.

lib/custom-tools/file-profiler.tsapp/build/page.tsx

Fix risk: Spreadsheet parser changes affect every upload tool — cell types and dates may shift.

Fix effort: Sample rows cap in file-profiler.ts; stream profile on server instead.

335

Calendar has two parallel event-detail interaction models

UI, Flow & Density

mediumOpenModerate effort

Grid view supports (a) clicking a day → bottom/side day panel with inline EventDetail cards (~806–860) and (b) clicking an event chip → expandedEventId fixed overlay (~864–878). Two different UIs for the same action; day panel and chip overlay can both be open conceptually but overlay only shows when !selectedDay. Confusing which click path to use.

Fix effort: Pick one path — day panel OR chip overlay — and remove the other; or unify into a single EventDetail drawer.

336

Calendar is mouse-only

UI, Colors & Aesthetics

mediumOpenModerate effort

app/calendar/page.tsx grid day cells (~712–714) are <div onClick={() => setSelectedDay(day)}> with no role='button', tabIndex, or onKeyDown. EventFormModal (~188) is fixed inset-0 overlay closed by backdrop click only — no role='dialog', aria-modal, or Escape handler. Keyboard users cannot select days or dismiss the create/edit modal via standard patterns.

app/calendar/page.tsx

Fix effort: Add role=button, tabIndex, onKeyDown, focus trap + Escape on event modal — calendar/page.tsx.

337

canAccessTool() is dead code — legacy tool ACL never runs

Controls, Admin & Collaboration

mediumOpenModerate effort

lib/supabase/member-permissions.ts exports canAccessTool() (~304) with zero imports outside the module. Legacy tool_access rows are not consulted on /forklift, /picker or other routes — only dashboard card filtering via greco_card_permissions (and even that is cosmetic per pass 10). Member Permissions 'tool access' toggles may be decorative for direct navigation.

lib/supabase/member-permissions.ts

Fix effort: Call canAccessTool in GrecoOnlyGuard or delete member_permissions.tool_access if greco_card_permissions is canonical.

338

Controls has no email or notification settings section

Controls, Admin & Collaboration

mediumOpenModerate effort

app/controls/ControlsClient.tsx sections (~45–52): cards, tools, modals, team, appearance, tabs, permissions — no email/notification matrix. Per-tool email recipient admin lives only in the unwired dashboard modal and Reports-tab gear icons. Controls subtitle promises 'everything for your Client ID' but omits the email surface founders configure via member permission flags.

app/controls/ControlsClient.tsx

Fix effort: Add Email section to Controls wiring report_email_settings + AI filters, or wire dashboard modal.

339

Controls Window Access does not gate the public /window scheduler

Window, Sales & Kiosk Scheduling

mediumOpenModerate effortHigh blast radius

ModalAccessSection title 'Window Access' (app/controls/sections/ModalAccessSection.tsx) drives shanetree_modal_permissions for Entrée modal keys only. Public /window posts to /api/public/window-appointments with no modal-permission check. Admins think they restricted 'Window' in Controls; customers can still self-book at /window.

app/controls/sections/ModalAccessSection.tsx

Fix risk: Gating /window breaks public Will-Call kiosk URLs printed on QR collateral unless redirect target is preserved.

Fix effort: Either rename Controls section or add server flag to block window-appointments POST when disabled.

340

Counting eagerly bundles framer-motion and recharts on first paint

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenModerate effort

app/counting/CountingClient.tsx statically imports framer-motion (~39) and six recharts components (~45–54). Opening /counting downloads animation + chart libraries before any chart renders — not covered by tracked picker/shorts recharts notes or ExcelJS CDN list.

app/counting/CountingClient.tsx

Fix effort: dynamic() CountingCharts child with lazy recharts; reduce framer-motion to CSS transitions where possible.

341

DockSheet Live typography is too small for wall-mounted reading

UI, Flow & Density

mediumOpenModerate effort

app/live/greco/page.tsx relies heavily on text-[9px]–text-[11px] for status badges, PO numbers, building/layout chips and table cells (~161–643); compact mode goes down to text-[7px]–text-[8px]. Designed for dense data on a monitor, but wall-mounted kiosk TVs at 10–15 feet make PO numbers and status labels hard to read at a glance.

app/live/greco/page.tsx

Fix effort: Add kiosk/wall mode with scaled type (text-sm+), larger PO badges and optional TV layout preset.

342

Document notification route bypasses NotificationService and never writes email_logs

Email, v1 API & Entrée Panel Polish

mediumOpenModerate effort

app/api/notifications/document/route.ts POSTs directly to Resend (~154–166). Failed-send counting on document route may be tracked separately — this angle is the missing audit trail: no email_logs rows for team document-share notifications, unlike session email paths through NotificationService.

app/api/notifications/document/route.ts

Fix effort: Route document POST through NotificationService.send or insert email_logs rows after Resend — notifications/document/route.ts.

343

Documents broadcast content_diff silently drops remote edits during unsaved work

Data Flows & Wiring

mediumOpenModerate effortHigh blast radius

app/documents/page.tsx content_diff handler applies remote edits only when !hasUnsavedChanges — comment mentions 'or if the remote change is newer' but there is no timestamp comparison. Two concurrent editors get silent divergence with no conflict banner.

app/documents/page.tsx

Fix risk: Same collaboration conflict — merge strategy choice affects multi-editor docs sessions.

Fix effort: Add timestamp/version conflict UI or merge strategy when both editors have unsaved work.

344

Driver Chat staff UI (/inbox) is English-only

Shanetrée, Routing & Driver Comms

mediumOpenModerate effort

app/inbox/InboxClient.tsx has zero useLanguage imports — thread list, AI toggle copy, send errors and settings strings are hardcoded English. Sibling /routing wraps LanguageProvider and bilingual t(en, es) throughout RoutingClient.tsx. Spanish-preference operators can use Shanetrée in Spanish but respond to drivers in an English-only inbox.

app/inbox/InboxClient.tsx

Fix effort: Wrap InboxClient copy with useLanguage/t(en,es) like RoutingClient.

345

driver_chat_messages INSERT RLS does not constrain sender role

Security & Trust Boundaries

mediumOpenModerate effort

supabase/migrations/129_create_driver_chat.sql INSERT policy only checks company_id membership — any company member can insert rows with sender: 'driver'. /inbox correctly uses /api/driver-chat/send (forces sender: 'greco'), but a browser client could spoof driver messages directly.

supabase/migrations/129_create_driver_chat.sql

Fix effort: Migration: INSERT WITH CHECK sender IN ('greco','ai') for authenticated role, or force all inserts through send API only via revoked direct INSERT.

346

driver_scan_reports has no CREATE TABLE migration in repo

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenModerate effort

lib/supabase/tool-reports.ts (~1928–1974) inserts/selects driver_scan_reports. Migrations only ALTER it (113_add_excluded_to_tool_report_tables.sql). Same migration-gap class as tracked greco_daily_stats and firmas_entries — fresh Supabase environments cannot reproduce Driver Scan persistence from migrations alone.

lib/supabase/tool-reports.ts

Fix effort: Add migration CREATE TABLE driver_scan_reports with company_id + RLS — export from hosted schema.

347

Email HTML templates interpolate data without escaping

Email, v1 API & Entrée Panel Polish

mediumOpenModerate effort

lib/email/service.ts generateEmailHtml() embeds ${data.*} directly in HTML (anomaly_alert ~593–595, marketing ~461–462, new_chat_message preview ~416, agent_notification ~487–489). Crafted display names, previews or anomaly text in outbound data can inject HTML into company-branded email bodies.

lib/email/service.ts

Fix effort: HTML-escape all ${data.*} interpolations in generateEmailHtml — lib/email/service.ts.

348

EmailService.sendWelcome() and agent_digest template are dead code

Kiosk Surfaces, Greco Tools & Platform Polish

mediumOpenModerate effort

lib/email/service.ts defines sendWelcome() (~323–332) with welcome HTML template (~357+) — grep finds zero callers outside the definition. agent_digest is registered in EmailTemplate (~26), mapped to weekly_digest prefs (~71), with HTML (~498+) but no route, cron or job worker invokes template: 'agent_digest'. Signup onboarding and agent weekly digest UI toggles have no sender path.

lib/email/service.ts

Fix effort: Wire sendWelcome on signup confirm + cron/job for agent_digest, or remove templates and UI toggles.

349

firmas_entries has no CREATE TABLE migration

Greco Warehouse Operations

mediumOpenModerate effort

CindyVueClient saves driver signatures via saveFirmaEntry/getFirmasForDate/deleteFirmaEntry in lib/supabase/tool-reports.ts (~1867–1902) against firmas_entries. lib/agent/greco-tools.ts lists firmas_entries in allowed SQL tables. No migration file creates the table or RLS — same migration-gap class as greco_daily_stats but for DockSheet signature photos.

lib/supabase/tool-reports.tslib/agent/greco-tools.ts

Fix effort: Migration for firmas_entries + storage bucket refs; mirror greco_daily_stats RLS pattern.

350

Four nested app/**/lib/ trees duplicate the top-level lib/ convention

Repository Structure Audit (Pass 23)

mediumOpenModerate effortMay affect neighbors

Domain logic also lives in app/cyclecount/lib/, app/drivers/lib/, app/transfers/lib/ and app/tools/greco/lib/ (items-engine, schedule-engine, excel-exports). Rest of the codebase uses lib/ at repo root. Inconsistent import mental model: some tools import @/lib/tools, others reach into app/tools/greco/lib. Consolidate Greco engines under lib/greco/ (today only greco-tools.ts + greco-cards.ts) or lib/tools/greco/.

app/cyclecount/lib/app/drivers/lib/app/transfers/lib/app/tools/greco/lib/

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Move app/tools/greco/lib/* to lib/greco/; same for cyclecount, drivers, transfers engines.

351

Geronimo and Empty Slots deliberately dropped the Entrée demand pipeline

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenModerate effort

app/geronimo/page.tsx header (~18–22) says it was lifted out of Window Low Slots 'standalone of the entrée bridge — drop the Dakota xlsx and go, no ODBC dependency, no demand pipeline.' app/empty-slots/page.tsx (~16–17) has no Supabase reads/writes. Both consume BFC Dakota R08921s only — no fourWeekAvg short-dollar ranking, no arslot context, no tie to the ODBC demand that makes Window Low Slots actionable. Enterprise slot planning needs both planes on every surface.

app/geronimo/page.tsxapp/empty-slots/page.tsx

Fix effort: Optional ODBC demand overlay on Geronimo/Empty Slots (reuse /low-slots + dakota-merge) — mode flag for bridge-less vs full merge.

352

GodView does N+1 queries and full-table scans

Performance & Scale

mediumOpenModerate effort

app/api/admin/godview/route.ts getCompanies() (~307–314) runs Promise.all of four count queries per company (profiles, quote_submissions, team_chat_messages, shared_documents) — 4×N companies per GodView Companies tab load. Agent section in app/components/GodView.tsx (~3430+) fetches all companies then per-company stats. app/api/monitoring/metrics/route.ts getJobMetrics (~142–165) selects all background_jobs.status rows into JS and filters in memory — no GROUP BY, no .limit().

app/api/admin/godview/route.tsapp/components/GodView.tsxapp/api/monitoring/metrics/route.ts

Fix effort: Replace per-company counts with SQL aggregate RPC; godview + monitoring routes.

353

Greco dashboard stats fire 11 Supabase round-trips on every mount

Monolith Files & Bundle Weight

mediumOpenModerate effort

When isGreco is true, loadDashboardData (~1274–1293) runs Promise.all with seven head-count queries (forklift, picker, replen, returns, shorts, skips, truck_errors) plus four recent-activity selects — 11 round-trips before the Stats tab is ever opened. Non-Greco tenants skip this, but every Greco operator pays the cost on every dashboard load and refresh. A single RPC or materialized daily rollup would collapse this.

Fix effort: Single RPC or daily rollup view returning counts + recent activity — migration + dashboard loader swap; test non-Greco tenants still skip.

354

Greco dashboard tab bar is overcrowded on mobile

UI, Flow & Density

mediumOpenModerate effort

Greco users get 10+ tab entries (Overview, Account, Shanetrée, DockSheet, Stats, Tools, AI, Workspace, Docs, Reports, Support) in one horizontal scroller at text-[10px] with only a right-edge gradient hint. Discoverability suffers — operators scroll sideways hunting for the tab they used yesterday; no grouping or 'More' overflow menu.

Fix effort: Group external links under More menu, collapse Greco shortcuts, or promote Workspace folder over duplicate tab entries.

355

Greco kiosk/session APIs are world-readable by design

Security & Trust Boundaries

mediumOpenModerate effortCritical — test everything

GET /api/greco/session (full dock session for any date), /api/greco/analytics-log, /api/greco/custom-appointments, /api/port/ai-summary/memory and /api/entree/query serve operational data without authentication so kiosk pages (/inbound, /timer, /live/greco) work. Trade-off may be acceptable but is implicit — a shared kiosk token or documented allowlist would make the boundary deliberate.

Fix risk: Adding auth breaks /timer, /inbound, /live/greco, /noe without a kiosk token story — public GET is load-bearing for wall mounts.

Fix effort: Product decision first: shared kiosk JWT vs documented allowlist vs IP allowlist — then implement consistently across ~5 routes.

356

History → chat deep link drops half its payload

Wiring & Broken Flows

mediumOpenModerate effort

“Comment on activity” links from the history page append ?prefill=…&activity=<id> to /team-chat, but team-chat only reads prefill and dm. The activity id — presumably meant to link the message back to the activity — is silently discarded. Similarly, /history?highlight=<id> is fully implemented on the read side (loads all, scrolls, highlights) but no code anywhere generates a highlight link.

Fix effort: Same activity param wiring.

357

History → team-chat deep link drops the activity reference

Data Flows & Wiring

mediumOpenModerate effort

app/history/page.tsx 'Comment on activity' links append ?prefill=…&activity=<id> to /team-chat, but team-chat only reads prefill and dm params. The activity id is never consumed — messages can't be linked back to the ledger entry. /history?highlight=<id> is implemented on the read side but no code generates highlight links.

app/history/page.tsx

Fix effort: Read ?activity= in team-chat, attach to message metadata or thread context — needs schema or message prefix convention.

358

History Excel export always fetches the unbounded ledger

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenModerate effortHigh blast radius

app/history/page.tsx handleExportExcel (~175–177) calls getUnifiedHistory(0) whenever on-screen loadLimit isn't already 'all'. Tracked issues cover UI 'All' mode; this export button is a separate path that always pulls the complete merged ledger (team_activity + dock + print + pull) for Excel generation — multi-megabyte export even when UI shows 50 rows.

app/history/page.tsx

Fix risk: Export explicitly calls getUnifiedHistory(0) — cap breaks 'entire history' workbook promise on the button label.

Fix effort: Server-side export endpoint with date range + cursor, or cap export at visible filter window.

359

Hourly port-ai-summary cron can exceed Vercel maxDuration

API & Operations

mediumOpenModerate effort

Cron runs sequentially per company with maxDuration = 300 while each generatePortAISummaryForCompany() is multi-step Gemini. Two companies × ~3 minutes risks a platform kill mid-loop — partial companies updated, rest skipped until next hour with no resume cursor.

Fix effort: Per-company time budget, cursor/resume state, or split into one-company-per-invocation queue.

360

IDE sandbox executes arbitrary user JavaScript

Security & Trust Boundaries

mediumOpenModerate effortMay affect neighbors

app/ide/page.tsx preview iframe uses sandbox='allow-scripts allow-modals' and injects editor contents via srcDoc including a <script> block (createSandboxDoc). Any code the user or AI assistant writes runs in browser with script execution — self-XSS / prototype-pollution vector on shared machines, even if isolated from the parent origin.

app/ide/page.tsx

Fix risk: Sandboxing breaks user snippets that depend on full DOM/window — IDE feature regression.

Fix effort: Tighten sandbox (drop allow-scripts for preview?), or run in isolated worker; balance IDE usefulness vs XSS on shared PCs.

361

IDE semantic index stays empty — context search is non-functional

Dead Code & Orphan Features

mediumOpenModerate effort

ide_code_nodes is populated only by /api/ide/index, which has no UI caller. Dead routes /api/ide/context/search, /api/ide/patch and both assistant chat handlers read ide_code_nodes for symbol context — but with an empty table the 'semantic codebase awareness' feature can never return results. Appears functional in architecture docs; effectively useless on web IDE today.

Fix effort: Call /api/ide/index on workspace save from ide/page.tsx, or remove ide_code_nodes dependency from assistant routes.

362

IDE workspaces are company-wide, not per-user

Build, IDE & Custom Tools

mediumOpenModerate effort

GET /api/ide/workspaces/route.ts (~21–26) lists all ide_workspaces for company_id with no created_by filter. Every company member sees and can open/edit/delete files in coworkers' workspaces via ide/workspaces/[id]/files. Tracked IDE silent errors and per-keystroke saves compound the risk.

Fix effort: Filter workspaces by created_by; add shared-workspace flag if collaboration intended.

363

ide_user_settings table has no company_id column

Agent, Collaboration & Platform APIs

mediumOpenModerate effort

supabase/migrations/108_create_ide_user_settings.sql defines only user_id + settings blobs. Violates repo multi-tenancy rule (all tables need company_id REFERENCES companies). RLS is per-user only — no tenant anchor for audits, future multi-company users, or company-offboarding data cleanup.

supabase/migrations/108_create_ide_user_settings.sql

Fix effort: Migration adding company_id + backfill from profiles; update settings-sync route to set it.

364

Inbound kiosk is English-only

Greco Warehouse Operations

mediumOpenModerate effort

app/inbound/InboundClient.tsx has zero useLanguage imports — all copy ('Scan PO / Item', schedule upload history, POSTAT labels, error strings) is hardcoded English. Warehouse floor kiosks (/inbound, /timer, /live/greco) are Spanish-first in other tools (Controls language toggle + greco-lang-pref), but the highest-traffic public lookup screen never switches locale.

app/inbound/InboundClient.tsx

Fix effort: Wrap InboundClient copy with useLanguage/t(en,es) like other Greco kiosk pages.

365

Incentives has no server persistence or audit trail

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenModerate effort

app/incentives/IncentivesClient.tsx fetches routes from /api/public/shanetree-latest?modal=routes (~103) and computes case/stop payouts client-side. No upsertDailyStat, logReportGeneration, or Supabase writes under app/incentives/. Driver incentive numbers vanish on browser clear with no team_activity_log or Daily Stats entry.

app/incentives/IncentivesClient.tsxapp/incentives/

Fix effort: Add incentives_runs table + upsertDailyStat + logReportGeneration on weekly export — payroll audit requirement.

366

Incentives payroll state lives only in global localStorage keys

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenModerate effortMay affect neighbors

app/incentives/IncentivesClient.tsx — EXCLUDED_KEY = 'greco.incentives.v1.excluded' and DAYS_KEY = 'greco.incentives.v1.days' (~23–24) with no per-user suffix (~34–53, 89–96). Shared warehouse PCs leak route exclusions and computed week history between operators — payroll-adjacent state should be server-backed or namespaced.

app/incentives/IncentivesClient.tsx

Fix risk: Namespacing keys resets shared-PC preferences until users reconfigure.

Fix effort: Namespace keys with userId or persist exclusions/days to Supabase incentives_settings table.

367

Meat-count 'replace day' is delete-then-upsert without transaction

Backend & Database

mediumOpenModerate effortMay affect neighbors

lib/supabase/meat-counts.ts replaceMeatCountsForDate() (~289–297) awaits deleteMeatCountsForDate() then upsertMeatCountsForDate() as two separate Supabase round-trips — no RPC, no transaction. deleteMeatCountsForDate() (~304–311) DELETEs by company_id + count_date; overlapping uploads (double-click, two operators, retry) interleave so one run's delete can land mid-insert — partial or empty day snapshot.

lib/supabase/meat-counts.ts

Fix risk: RPC transaction changes failure mode — partial day visible during upload may become lock contention.

Fix effort: Single RPC doing DELETE+INSERT in one transaction in supabase migration.

368

Meat-count “replace day” is delete-then-upsert with no transaction

Data Integrity & Races

mediumOpenModerate effortMay affect neighbors

replaceMeatCountsForDate() in lib/supabase/meat-counts.ts issues a DELETE for the (company, date) rows and then a separate bulk upsert. Two overlapping uploads (double-click, two operators, a retry) can interleave so one run's delete lands mid-way through the other's insert — leaving a partial or empty day snapshot. A single RPC doing both in one transaction closes the window.

lib/supabase/meat-counts.ts

Fix risk: Duplicate title — same concurrent upload interleave risk vs fix behavior.

Fix effort: Same transactional RPC.

369

member_permissions.dashboard_tabs is stored but never applied

Permissions, Settings & Misleading UI

mediumOpenModerate effortHigh blast radius

getMyFullPermissions() returns dashboardTabs from member_permissions.dashboard_tabs (lib/supabase/member-permissions.ts ~513), but grep dashboardTabs in dashboard/page.tsx returns zero matches. Dashboard only consumes toolAccess from permissions (~1566). Per-member tab ACL is computed and persisted but no page reads it — useless schema field today.

lib/supabase/member-permissions.ts

Fix risk: Applying stored tabs suddenly hides Workspace cards admins thought they configured — latent settings activate.

Fix effort: Read dashboardTabs in dashboard/page.tsx and filter tabs/cards, or drop column from schema.

370

Members, Support and History pages are English-only

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenModerate effort

app/members/page.tsx, app/support/page.tsx and app/history/page.tsx have zero useLanguage / t() usage. Tracked i18n gaps cover team-chat and documents — Spanish-preference Greco operators hit fully English member management, support tickets and activity ledger after switching language elsewhere.

app/members/page.tsxapp/support/page.tsxapp/history/page.tsx

Fix effort: Add useLanguage + t() passes on members, support, history pages — same as team-chat i18n effort.

371

Monitoring metrics collectors fetch unbounded rows then count in Node

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenModerate effortMay affect neighbors

app/api/monitoring/metrics/route.ts — getJobMetrics() selects all background_jobs (~142–146); getApiMetrics() pulls every api_activity_log row in 24h (~184–193); getAnomalyMetrics() pulls all anomalies in 24h (~224–233). No SQL-side aggregation or .limit() — grows with tenant volume and slows founder monitoring polls.

app/api/monitoring/metrics/route.ts

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Replace full selects with SQL count/aggregate RPCs or head:true count queries per metric bucket.

372

Pass 22 dead-code hunt — ~2,400+ additional verified orphan lines

Dead Code Hunt (Pass 22)

mediumOpenModerate effort

Grep pass 22 beyond prior Dead Code categories: lib/agent/policy-verification.ts (~62 lines, zero imports), lib/routing/geocoder.ts (~109 lines, zero imports), lib/guide/printable-guide.ts (~1,200 lines, docs/whitepaper only), 13 unused Nivo render* exports in lib/agent/renderer.ts (~500+ lines), six ai-conversations CRUD/comment helpers (~250 lines), companyCache/statsCache write-only objects (~120 lines), npm packages ai + @ai-sdk/openai with zero source imports, plus ~25 orphan exports across member-permissions, chat-rooms, calendar, inbound alerts, Greco excel-exports, notifications, ai-filters, port-ai-summaries, shanetree-snapshots, greco-card-permissions, company-settings, individual-report-settings, documents, ai-chat-sessions, and duplicate type definitions. Total new maintainable surface that executes nothing in product flows: roughly 2,400–2,800 lines.

lib/agent/policy-verification.tslib/routing/geocoder.tslib/guide/printable-guide.tslib/agent/renderer.ts

Fix effort: Batch-delete verified orphans in policy-verification, geocoder, unused render* exports, ai-conversations CRUD, orphan supabase helpers, and npm ai/@ai-sdk/openai; run tsc after each batch.

373

Permission presets API exists with no UI

Controls, Admin & Collaboration

mediumOpenModerate effort

getPermissionPresets, savePermissionPreset, deletePermissionPreset and applyPresetToMember in lib/supabase/member-permissions.ts (~524–666) have zero callers outside that file. permission_presets table + RLS in migration 057 are maintained with no Controls or Account surface — dead schema beside the live matrix UIs.

lib/supabase/member-permissions.ts

Fix effort: Add presets picker to Controls MemberPermissionsSection or drop permission_presets table.

374

Profile avatars stored as inline base64 data URLs in profiles.avatar_url

Agent, Collaboration & Platform APIs

mediumOpenModerate effort

app/account/page.tsx (~749–753) reads avatar files with FileReader.readAsDataURL() and lib/supabase/account.ts (~148) persists the data URL to profiles.avatar_url. No file-size or MIME check; multi-megabyte images inflate profile rows and every presence/mention render that loads avatar_url. Should upload to Storage and store a URL (distinct from quote-attachment JSONB issue).

app/account/page.tsxlib/supabase/account.ts

Fix effort: Upload to Supabase Storage avatars bucket; store public URL; add 2MB cap — account page + migration for existing rows optional.

375

Public kiosk relay routes have no rate limiting

Security & Trust Boundaries

mediumOpenModerate effortCritical — test everything

app/api/public/inbound-relay, driver-chat and po-doc have zero withRateLimit calls (grep finds none under app/api/public/*). Each can enqueue bridge/ODBC relay jobs or trigger inline AI replies — abusable for cost and bridge load beyond the already-tracked item-alert-subscribe gap.

app/api/public/inbound-relayapp/api/public/

Fix risk: Rate limits on /api/greco/session, /api/entree/query, will-call relay can block wall mounts during receiving rush.

Fix effort: Add withRateLimit to inbound-relay, driver-chat and po-doc — per-IP for anon kiosk callers.

376

QStash-down marks ai_analysis jobs completed without running AI

API & Operations

mediumOpenModerate effort

lib/jobs/service.ts executeAIAnalysisJob() is a stub returning { success: true } with no gateway call. When QStash is unavailable, enqueueJob falls back to synchronous execution and marks those background_jobs completed while doing zero analysis.

lib/jobs/service.ts

Fix effort: Implement real executeAIAnalysisJob or mark jobs failed when QStash unavailable instead of stub success.

377

Rate limiting fails open, and the Redis path races

Rate Limits & Abuse Surface

mediumOpenModerate effortHigh blast radius

lib/rate-limit.ts returns allowed: true whenever the check_rate_limit RPC errors (unless RATE_LIMIT_FAIL_CLOSED=true, which isn't set), and requests carrying no bearer token and no companyId skip limiting entirely — so public routes that call withRateLimit without auth context are effectively unlimited. The Redis API-key fast path in lib/api/auth.ts is also check-then-increment (two separate ops), so concurrent bursts slip past the per-minute cap.

lib/rate-limit.tslib/api/auth.ts

Fix risk: Fail-closed change denies access on transient errors — opposite of today's permissive behavior.

Fix effort: Same rate-limit hardening.

378

Rate limiting fails open; several routes have none

API & Operations

mediumOpenModerate effortHigh blast radius

lib/rate-limit.ts returns allowed when no companyId/bearer token and on RPC errors unless RATE_LIMIT_FAIL_CLOSED=true. /api/public/item-alert-subscribe, /api/dockvoo/notify and /api/jobs/enqueue have no withRateLimit at all.

lib/rate-limit.ts

Fix risk: RATE_LIMIT_FAIL_CLOSED=true or adding limits to dockvoo/notify can drop legitimate burst traffic from forklift save loops.

Fix effort: Set RATE_LIMIT_FAIL_CLOSED in prod, add withRateLimit to public routes, fix Redis race with atomic INCR — several files.

379

Report getters with no row caps

Scalability & Performance

mediumOpenModerate effortMay affect neighbors

getAllPickerReports, getShortsHistory (when uncapped), getAllDriverScanReports, getUserFiles and getCompanyDocuments all select without .limit() — and several embed full JSONB report payloads per row. The picker page then flattens every row of every report into memory for client-side filters. Each works now; each degrades linearly with usage and none has a ceiling.

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Same row caps + pagination.

380

Report/document getters have no row caps

Performance & Scale

mediumOpenModerate effortMay affect neighbors

lib/supabase/tool-reports.ts getAllPickerReports() (~494–512) .select('*') on picker_reports with no .limit() — embeds full picker_data + short_runner_data JSONB per row. lib/supabase/documents.ts getCompanyDocuments() (~82) selects all shared_documents. app/picker/page.tsx (~234–241) flattens every row of every report into allReportsData for client-side crew/name/scan filters. getShortsHistory (uncapped) and getUserFiles follow same pattern — linear degradation, no ceiling.

lib/supabase/tool-reports.tslib/supabase/documents.tsapp/picker/page.tsx

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Add sensible .limit() defaults + pagination params to 4–5 supabase getters; update consuming pages.

381

Returns Intel trends by BFC Client ID with no Entrée arcusto bridge

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenModerate effort

app/docs/page.tsx (~1858–1859) documents Returns Intel expecting BFC Client ID per location. app/returns-intel/page.tsx aggregates week-over-week by that string with no lookup to Entrée CUSTNO/COMPANY (arcusto). Return Diffs normCust strips leading zeros (~20748) but Returns Intel does not — location-level return trends cannot roll up to salesman, credit terms, or Entrée customer master for enterprise reporting.

app/docs/page.tsxapp/returns-intel/page.tsx

Fix effort: Lookup table or ODBC /customer?cust= join enriching Returns Intel rows with Entrée CUSTNO/COMPANY/salesman.

382

Routing client calls OSRM directly from the browser

Shanetrée, Routing & Driver Comms

mediumOpenModerate effort

fetchOSRMRoute() in RoutingClient.tsx (~1232) hits https://router.project-osrm.org/route/v1/driving/… client-side with errors swallowed. No server proxy, auth or rate limit — distinct from tracked /api/routing/geocode (Nominatim). Road-distance mode exposes dispatcher client IPs to a third-party demo service and breaks if OSRM throttles or blocks the warehouse egress IP.

Fix effort: Proxy OSRM via /api/routing/osrm with rate limit; or bundle self-hosted routing.

383

Several Greco kiosk endpoints are world-readable by design — worth an explicit decision

Security & Trust Boundaries

mediumOpenModerate effortCritical — test everything

GET /api/greco/session (full dock session data for any date), /api/greco/analytics-log, /api/greco/custom-appointments, /api/port/ai-summary/memory and the /api/entree/query bridge proxy serve data without authentication so the public kiosk pages (/inbound, /timer, /live/greco) work. That trade-off may be acceptable, but it's currently implicit — a shared kiosk-token or at least a documented allowlist would make the boundary deliberate rather than accidental.

Fix risk: Locking down public kiosk APIs without token replacement breaks wall-mounted workflows.

Fix effort: Same kiosk boundary decision + implementation.

384

Shorts page (~4,327 lines) eagerly imports recharts on a data-entry route

Monolith Files & Bundle Weight

mediumOpenModerate effortHigh blast radius

app/shorts/page.tsx is the fourth-largest page file. It static-imports six recharts components (~25) and lib/supabase/tool-reports save/get helpers while also hosting upload parsing, row editing, history drawer, and AI summary UI in one component. Dock supervisors opening Shorts for a quick entry form download charting libraries before any chart renders. Split charts behind dynamic() or a ShortsCharts child.

app/shorts/page.tsxlib/supabase/tool-reports

Fix risk: Large single-file page — extracting or refactoring risks breaking embedded business logic.

Fix effort: Extract ShortsCharts child with dynamic() recharts import — 1–2 files; verify chart tab still renders after upload.

385

Signup page performs no work-email domain validation

Public API, Jobs, Onboarding & Greco Gaps

mediumOpenModerate effort

app/signup/page.tsx calls supabase.auth.signUp({ email, password }) (~23) with no domain allowlist/denylist before ensureUserProfile() (~26). Pairs with ensure_user_profile auto-company creation — personal emails create orphan tenants with full founder access.

app/signup/page.tsx

Fix effort: Client + server domain allowlist or block personal-email providers before ensureUserProfile.

386

Six IDE API routes have no web UI callers

Dead Code & Orphan Features

mediumOpenModerate effort

app/ide/page.tsx fetches only /api/ide/workspaces/* and /api/ide/assistant/chat (non-stream). These routes have zero fetch callers: /api/ide/index (~114 lines), /api/ide/patch (~462), /api/ide/context/search (~89), /api/ide/settings-sync (~89), /api/ide/assistant/complete (~99), /api/ide/assistant/chat/stream (~251). ~1,100 lines of semantic-index / patch / completion infrastructure maintained for a desktop extension that never calls them from the web IDE.

app/ide/page.tsx

Fix effort: Wire web IDE to index/patch/context routes or delete ~1,100 lines if desktop-only extension is abandoned.

387

Supabase browser client loaded from CDN with major-only pin

Code Quality & Hygiene

mediumOpenModerate effort

app/layout.tsx loads https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2 (major-only) while package.json pins ^2.91.1 for server usage. lib/supabase/client.ts createClient() reads window.supabase from the CDN script, not the npm bundle — supply-chain exposure and version skew between CDN and server paths.

app/layout.tsxsupabase/supabase-jslib/supabase/client.ts

Fix effort: Bundle @supabase/supabase-js via npm import in client.ts, remove CDN script from layout — test auth persistence on warehouse machines.

388

Support chat has no realtime and swallows failures

Data Flows & Wiring

mediumOpenModerate effortMay affect neighbors

app/support/page.tsx loads messages once on mount — no postgres_changes subscription or polling, so admin replies don't appear until refresh or the user sends again. getMySupportMessages() returns [] on error; handleSend never surfaces result.error. Failed loads look like 'No messages yet'; failed sends look like a no-op.

app/support/page.tsx

Fix risk: Realtime filter changes alter what live updates appear — easy to hide needed events.

Fix effort: Add postgres_changes subscription (like team-chat) + surface send/load errors in UI.

389

Tab visibility only hides two Overview shortcut cards

Permissions, Settings & Misleading UI

mediumOpenModerate effort

company_tab_settings / adminHiddenTabs is loaded on dashboard but consulted only twice — to hide Overview cards for tools and reports (~1838, ~1872). The tab strip, Workspace folder cards and <Link href='/team-chat'> never read adminHiddenTabs. Hiding 'Chat' or 'Members' in Account/Controls does nothing visible outside two card removals.

Fix effort: Apply adminHiddenTabs to tab strip, Workspace folder and external links — or rename feature 'Overview card visibility'.

390

Team-chat per-room notification bell is cosmetic

Dead Code & Orphan Features

mediumOpenModerate effortHigh blast radius

team-chat exposes a notifications on/off toggle per room (chat_notification_settings table, lib/supabase/chat-rooms.ts toggleNotification). UI updates notifications_enabled and persists to DB, but handleSendMessage never reads the flag — no email, push, badge or mute logic consults it on send. Users think they muted a room; nothing changes. Useless control (~80 lines UI + table writes).

lib/supabase/chat-rooms.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Implement mute check on send (email/badge) or remove toggle and chat_notification_settings table.

391

The /scan page re-implements the tool detector

Dead Code & Orphans

mediumOpenModerate effort

app/scan/page.tsx has its own copy of the column-matching/scoring logic that lib/tools/detector.ts already provides — the two can drift apart. Additionally, scan redirects to Reconciliation, Loader and Returns Intel with a sessionStorage scannerFile those three pages never read, so users arrive with nothing loaded (nine other tools do consume it).

app/scan/page.tsxlib/tools/detector.ts

Fix effort: Same detector consolidation.

392

Thirteen migration numbers are used twice

Backend & Database

mediumOpenModerate effort

supabase/migrations/ has colliding prefixes at 027, 028, 029, 030, 031, 032, 033, 055, 094, 095, 100, 130 and 132. Apply order depends on alphabetical tie-breaking; fresh environments may sequence conflicting changes differently than production.

supabase/migrations/

Fix effort: Rename conflicting files to unique sequential numbers — must not re-run on prod; document apply order for fresh envs only.

393

Transfers/overstock is BFC-only — no Entrée on-hand or slot validation

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenModerate effort

app/docs/page.tsx (~788–806): Transfers ingests BFC Total Overstock xlsx (Hecht/Brewster/Rana tabs) only. No join to Entrée ONHAND, itemwhselocs, or arslot to validate whether BFC overstock positions match ERP inventory. Subs modal already correlates replenishment risk with DockSheet QTYREC — standalone Transfers and Replen (two BFC uploads, app/replen/page.tsx ~237, ~379) lack that receiving-bridge context.

app/docs/page.tsxapp/replen/page.tsx

Fix effort: Enrich Transfers rows with Entrée ONHAND + arslot via /stockvoo or item lookup — moderate ODBC fan-out per upload.

394

v1 file download loads entire blob into memory with no streaming cap

Email, v1 API & Entrée Panel Polish

mediumOpenModerate effort

app/api/v1/files/[id]/route.ts (~56–64) uses Buffer.from(fileContent) with no size guard. Large user_files payloads can OOM serverless handlers — parallel to tracked JSONB bulk rows but specific to v1 files download path.

app/api/v1/files/

Fix effort: Stream from Storage with max size check, or reject files >N MB in files/[id]/route.ts.

395

v1 schedules POST sets next_run_at to now without cron math

Email, v1 API & Entrée Panel Polish

mediumOpenModerate effort

app/api/v1/schedules/route.ts (~158–174) comments 'Calculate next run time' but assigns nextRunAt = now.toISOString() regardless of cron_expression / interval_seconds / timezone. Schedules never executing is tracked — even with a runner, first-run timing is wrong on create.

app/api/v1/schedules/route.ts

Fix effort: Use cron-parser or interval math to compute next_run_at from cron_expression + timezone.

396

Verified dead-code inventory (~3,600 lines + 5 missing workers)

Dead Code & Orphan Features

mediumOpenModerate effortMay affect neighbors

Grep-verified orphans across this pass: ~720 lines of unused lib/supabase exports (legacy team-chat CRUD, team-activity helpers, anomalies client dupes, quote/chat/port/rate-limit helpers), ~74 lines unused lib/tools/index.ts barrel, ~800 lines duplicate /produce page, ~1,900 lines of API routes with zero fetch callers (six IDE routes, monitoring pair, desktop-download, cache HTTP), plus subscribeToTeamMessages (~50 lines, tracked separately in Hygiene). lib/jobs/service.ts maps five worker URLs (report, embedding, scheduled-report, export, cleanup) that have no route files — enqueue paths 404. Total maintainable surface that executes nothing useful: roughly 3,400–3,800 lines today, plus broken job types.

lib/supabaselib/tools/index.tslib/jobs/service.ts

Fix risk: Bulk delete may remove bridge-invoked routes (internal autopull, desktop-download) — grep callers per file, not batch.

Fix effort: Roll-up cleanup pass — delete verified orphans in batches (lib exports, unwired routes, /produce) and implement or remove the five missing job workers.

397

weekly_digest and marketing_emails toggles have no sender

Permissions, Settings & Misleading UI

mediumOpenModerate effort

Account shows weekly_digest and marketing_emails (~91–100). Grep under app/api/cron finds no digest or marketing cron. No UI path checks these prefs before sending — they only exist in lib/email/service.ts template mapping and the open /api/email/send relay allowlist. Users opt in/out of emails that are never sent.

app/api/cronlib/email/service.ts

Fix effort: Implement digest/marketing crons that respect prefs, or remove toggles from account UI.

398

Will-call kiosk APIs beyond relay trio have no rate limiting

Window, Sales & Kiosk Scheduling

mediumOpenModerate effort

Tracked public relay gap names inbound-relay, driver-chat and po-doc only. Sibling unauthenticated routes also lack withRateLimit: /api/public/window-appointments (POST spam + slot polling), /api/public/sales-relay (ODBC job enqueue), /api/public/will-call-print-request (print-queue flood), /api/public/invoice-status (live ODBC + relay), /api/public/shanetree-latest (full snapshot reads).

Fix effort: Add withRateLimit to window-appointments, sales-relay, will-call-print-request, invoice-status, shanetree-latest.

399

willcall-print-claim is a non-atomic SELECT-then-UPDATE race

Warehouse Tool Coverage, Monitoring & Collaboration

mediumOpenModerate effortMay affect neighbors

app/api/internal/willcall-print-claim/route.ts (~56–81) reads oldest pending will_call_print_requests row, then updates by id + status=pending. Two bridge processes can race; loser returns { row: null } with no retry signal. Unlike shanetree_relay_claim RPC, no FOR UPDATE SKIP LOCKED — duplicate print claims or missed jobs under concurrent bridge workers.

app/api/internal/willcall-print-claim/route.ts

Fix risk: Atomic claim RPC changes double-print vs missed-print tradeoff on /noe kiosk.

Fix effort: Add claim_willcall_print_request RPC with FOR UPDATE SKIP LOCKED — mirror shanetree_relay_claim pattern.

400

/port, /noe and /adrian mount the full EntreeClient chunk

Monolith Files & Bundle Weight

mediumOpenHard / riskyCritical — test everything

app/port/page.tsx, app/noe/page.tsx and app/adrian/page.tsx each static-import EntreeClient with a mode prop (portMode, noeMode, adrianMode). Relay tablets on /port and sales kiosks on /noe download the same ~42k-line Shanetrée bundle as full /entree even though most modal tiles are hidden per mode. Route-level dynamic import or a thin mode shell would let each entry point ship a smaller first chunk.

app/port/page.tsxapp/noe/page.tsxapp/adrian/page.tsx

Fix risk: EntreeClient monolith — kiosk and ERP panels share state; drive-by edits risk production dock workflows.

Fix effort: Route-level code splitting or thin mode shells that dynamic-import only the tiles each kiosk needs — must not break relay/print flows.

401

CindyVueClient statically chains ~5k lines of schedule + items engines

Monolith Files & Bundle Weight

mediumOpenHard / riskyCritical — test everything

app/tools/greco/CindyVueClient.tsx (6,003 lines) statically imports parseXlsxAndBuildSchedule and export helpers from schedule-engine.ts (2,448 lines) and items logic from items-engine.ts (2,543 lines). Opening /cindyvue pulls ~11k lines of scheduling/parsing code into the first JS chunk. schedule-engine uses exceljs types at module scope; CindyVue also imports lib/supabase/tool-reports for firma photos and daily stats.

app/tools/greco/CindyVueClient.tsxlib/supabase/tool-reports

Fix risk: DockSheet receiving schedule is business-critical; lazy-loading or splitting can break 30s autosave and grid sync paths.

Fix effort: Dynamic-import schedule-engine on file upload; consider server-side parse API — CindyVue is operationally critical, needs dock-floor QA.

402

Dakota merge math is triplicated — drift breaks WMS↔ERP number parity

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenHard / riskyCritical — test everything

lib/lowslots/dakota-merge.ts (~11–17) documents a faithful port of inline EntreeClient parseDakotaSlotXlsx + mergeWithDakota that 'MUST stay in lockstep.' lib/lowslots/pick-list-xlsx.ts carries the same warning. Three copies (EntreeClient + two lib files) cannot import each other without refactoring the 42k-line monolith. Any Dakota column tweak must be applied thrice or /port relay and in-modal Low Slots diverge — unacceptable for enterprise sign-off.

lib/lowslots/dakota-merge.tslib/lowslots/pick-list-xlsx.ts

Fix risk: Consolidating merge logic in lib/ must preserve EntreeClient modal behavior; wrong merge breaks slot counts operators trust for replen.

Fix effort: Extract parseDakotaSlotXlsx + mergeWithDakota into lib/lowslots/core.ts; EntreeClient imports it — pairs with Entree monolith split, high regression risk.

403

Dashboard dark mode ends at the dashboard

Aesthetics & Consistency

mediumOpenHard / risky

app/dashboard/page.tsx (~201–207, ~464–487) persists dashboard-theme in localStorage and toggles .dashboard-dark-mode on the dashboard root only (background #0f172a at ~1687). app/layout.tsx sets --page-bg on body but Account, Docs, Members and Greco tools use default light shells. Navigating from a dark dashboard to /account snaps to white — comment at ~466 explicitly says 'only this dashboard flips'. Site-wide theme would need layout-level class or next-themes.

app/dashboard/page.tsxapp/layout.tsx

Fix effort: Same site-wide theme vs WebKit trade-off.

404

dashboard/page.tsx is a ~3,500-line shell with almost no code splitting

Monolith Files & Bundle Weight

mediumOpenHard / riskyHigh blast radius

app/dashboard/page.tsx is 3,496 lines — auth, Greco tool grid, email settings modals, team activity, documents, AI conversations, custom cards/tools, and admin tabs all live in one component. Only GodView is dynamic() (~63); everything else (including 20+ lib/supabase imports) loads on first dashboard visit. Extracting tab panels (Files, Team, Settings, Greco cards) behind dynamic() would shrink the default route chunk.

app/dashboard/page.tsxlib/supabase

Fix risk: Tab lazy-load changes mount timing for 30s refresh, Greco grid, and settings modals — dashboard is the hub for every other route.

Fix effort: Extract tab panels behind dynamic() — touches auth bootstrap, Greco grid, and settings modals; multi-day refactor with dashboard regression testing.

405

Escape key handling in Shanetrée closes over stale modal state

Wiring & Broken Flows

mediumOpenHard / riskyHigh blast radius

EntreeClient's global Escape handler closes ~a dozen modals, but the effect's dependency array only lists three of those state values — the listener captures stale booleans, so Escape can no-op on a freshly-opened modal until an unrelated dep changes. The Drivers tool has a cousin bug: its requestAnimationFrame timer loop has no unmount cleanup.

Fix risk: Fixing deps in 41k-line EntreeClient — high regression risk; dedicated tested pass required.

Fix effort: Fix useEffect deps in 41k-line EntreeClient.tsx — high regression risk; needs dedicated tested pass.

406

Forklift productivity is BFC-only — no tie to Entrée labor, receiving or PO context

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenHard / risky

app/forklift/page.tsx + lib/tools/processors/forklift.ts parse Day/Night BFC Task Summary xlsx into forklift_reports. No bridge endpoint, no join to Entrée SECID/employee master, no correlation with DockSheet QTYREC or /po-doc receiving lines. For enterprise ops, forklift TPH should eventually connect to what was received and what was still short — today it is an isolated leaderboard metric.

app/forklift/page.tsxlib/tools/processors/forklift.ts

Fix effort: Join forklift_reports to DockSheet session_date + /po-doc QTYREC rollup — needs date/employee keys and optional ODBC pull on save.

407

GodView disable-user toggle is cosmetic

Controls, Admin & Collaboration

mediumOpenHard / riskyHigh blast radius

toggleUserStatus in app/api/admin/godview/route.ts (~941–950) only updates profiles.is_disabled — inline comment admits 'would require auth admin API to actually disable users.' GodView 'disable user' does not block Supabase Auth login; flag is informational only unless something else reads it.

app/api/admin/godview/route.ts

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Call Supabase Auth admin ban API or hide toggle until profiles.is_disabled is enforced on login.

408

lib/supabase/ is a flat 43-file directory with no domain subpackages

Repository Structure Audit (Pass 23)

mediumOpenHard / risky

All Supabase client modules live in lib/supabase/*.ts at one depth — team-chat.ts, chat-rooms.ts, shanetree-snapshots.ts, member-permissions.ts, etc. No lib/supabase/chat/, reports/, greco/, or settings/ folders. Workable at current size but already hard to navigate; professional repos group by bounded context and co-locate types/tests per domain.

lib/supabase/lib/supabase/chat/

Fix effort: Split into lib/supabase/chat, reports, greco, settings with barrel re-exports — wide import path churn.

409

lib/supabase/tool-reports.ts (~2,170 lines) is in every warehouse tool bundle

Monolith Files & Bundle Weight

mediumOpenHard / riskyHigh blast radius

lib/supabase/tool-reports.ts holds save/get helpers for forklift, picker, shorts, skips, replen, returns, truck-errors, loader, reconciliation, daily stats, firma uploads, and notification hooks — 2,170 lines. Grep shows 15+ client pages static-importing it (picker, shorts, forklift, CindyVue, EntreeClient, drivers, daily-stats, etc.). Tree-shaking helps but type exports and shared upsertDailyStat still pull a large shared module into most Greco routes.

lib/supabase/tool-reports.ts

Fix risk: Large single-file page — extracting or refactoring risks breaking embedded business logic.

Fix effort: Split into per-tool modules (picker-reports.ts, shorts-reports.ts, etc.) or server actions — wide import graph across 15+ pages.

410

No middleware — page auth is client-side only

Auth & Session

mediumOpenHard / riskyCritical — test everything

There is no middleware.ts. Every protected route checks supabase.auth.getUser() in a client useEffect and redirects after mount. RLS protects data, but unauthenticated visitors briefly render page shells and each page re-implements its own guard differently.

Fix risk: Central auth changes affect every protected route and public kiosk exception list.

Fix effort: Add middleware.ts with Supabase session cookie check + redirect — touches every protected route's guard logic; high regression surface.

411

Realtime broadcast channels have no membership authorization

Agent, Collaboration & Platform APIs

mediumOpenHard / riskyMay affect neighbors

lib/realtime/broadcast.ts (~49–54) builds broadcast:${scope}:${resourceId} from predictable inputs. Any logged-in Supabase client knowing a document UUID can subscribe and send cursor, typing, content_diff, selection and scene_action events. No server-side channel ACL — collaboration spoofing on shared docs is possible if IDs leak.

lib/realtime/broadcast.ts

Fix risk: Realtime filter changes alter what live updates appear — easy to hide needed events.

Fix effort: Supabase Realtime channel auth or server-minted channel tokens — requires RLS on realtime.messages or custom auth hook.

412

Returns+ (BFC) and Window Returns modal (ODBC) are never reconciled

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenHard / risky

app/returns/page.tsx categorizes BFC export columns (Item, Rtn Cd, Customer Name, Orig. Inv#). Window Returns modal pulls live artrans/arytran QTYSHP<0 via ODBC (shanetree-whitepaper.ts ~58, ~84). No tool compares Returns+ categorized output against Entrée ODBC returns the way Return Diffs does for credit memos — another BFC-vs-ERP gap where only one side got the three-way diff treatment.

app/returns/page.tsx

Fix effort: New diff modal or extend Return Diffs to compare Returns+ categories vs ODBC QTYSHP<0 — similar snapshot pattern to return_diffs_snapshots.

413

Root layout and globals.css hard-lock light color-scheme

UI, Colors & Aesthetics

mediumOpenHard / risky

app/layout.tsx sets html style colorScheme:'light' plus meta tags; app/globals.css lines 6–8 force color-scheme: light !important. This is intentional for WebKit gradient fixes but means dashboard's dark-mode toggle (localStorage dashboard-theme) can't propagate — leaving /dashboard snaps every other page back to forced light.

app/layout.tsxapp/globals.css

Fix effort: Site-wide theme requires reconciling WebKit gradient fix (globals.css !important) with dashboard dark mode — architectural CSS conflict.

414

Routing autosave is last-writer-wins on full stops JSONB

Shanetrée, Routing & Driver Comms

mediumOpenHard / risky

Debounced autosave in RoutingClient.tsx (~1130–1140) POSTs the entire stops array after every edit. POST /api/routing/routes upserts greco_routes by id with no version column or optimistic lock (routes/route.ts ~80–102). Two dispatcher tabs editing the same route overwrite each other's stop order — parallel to tracked DockSheet session autosave but greco_routes is not named there.

Fix effort: Add updated_at optimistic lock or merge strategy on greco_routes POST.

415

ScheduleOutlookClient duplicates schedule-engine logic verbatim

Monolith Files & Bundle Weight

mediumOpenHard / riskyHigh blast radius

app/schedule-outlook/ScheduleOutlookClient.tsx (2,278 lines) comments at ~72 and ~298 say logic was 'copied verbatim' from app/tools/greco/lib/schedule-engine.ts. Bug fixes and performance tweaks must be applied twice; bundle weight doubles for anyone who uses both DockSheet and the Outlook binder. Extracting a shared @/lib/greco/schedule-core module is the fix — moderate refactor with regression risk on colored exports.

app/schedule-outlook/ScheduleOutlookClient.tsxapp/tools/greco/lib/schedule-engine.tslib/greco/schedule-core

Fix risk: Deduping into shared module must update DockSheet colored exports and Outlook binder in lockstep — bugfix currently applied twice for a reason.

Fix effort: Extract shared schedule-core module used by both CindyVue and Outlook — must verify colored exports and combine-by-load parity on real xlsx files.

416

Seven copies of 'resolve the user's company'

Backend & Database

mediumOpenHard / riskyHigh blast radius

tool-reports, shanetree-snapshots, shanetree-pull-log, shanetree-print-log, shanetree-manual-subs, account-geocodes and shanetree-relay each re-implement profile lookup + fallback + profile repair, and they've drifted (relay skips profile repair; company-settings returns null with no fallback).

Fix risk: Fixing one resolver without the other six reintroduces drift; relay skips profile repair, company-settings returns null — behaviors differ intentionally today.

Fix effort: Extract shared resolveCompanyId() in lib/supabase/ and replace 7 implementations — wide diff, each tool path needs smoke test.

417

Seven copies of “resolve the user's company”

Database & Migrations

mediumOpenHard / riskyHigh blast radius

tool-reports, shanetree-snapshots, shanetree-pull-log, shanetree-print-log, shanetree-manual-subs, account-geocodes and shanetree-relay each re-implement profile lookup + fallback + profile repair, and they've already drifted (relay skips profile repair; company-settings returns null with no fallback). Beyond the known Greco auto-assign problem, the duplication itself means any fix has to land seven times. One shared resolveCompanyId() is overdue.

Fix risk: Duplicate audit title — same seven-way resolver drift risk.

Fix effort: Same shared helper extraction.

418

Subs modal proves DockSheet↔PO receiving join — that pattern isn't platform-wide

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

mediumOpenHard / riskyHigh blast radius

shanetree-whitepaper.ts §5.19: Subs export pulls prior-day DockSheet session POs and live /po-doc QTYREC per PURNO to flag whether receipts cover next-day OOS demand (EntreeClient.tsx ~12637, ~13189, ~13779). This is meaningful WMS(scheduler)+ERP(receiving) intimacy. Standalone Replen, Negative Slots, Loader and Thruput never consume greco_cindyvue_sessions or /po-doc — the join logic lives only inside one Shanetrée modal instead of a reusable lib/greco/receiving-bridge module.

lib/greco/receiving-bridge

Fix risk: Extracting receiving-bridge from EntreeClient modal risks breaking Subs export that flags OOS vs receipts.

Fix effort: Extract lib/greco/receiving-bridge.ts from Subs modal; wire Replen/Transfers/Loader to greco_cindyvue_sessions + /po-doc QTYREC.

419

Two spreadsheet stacks, two chart stacks, two screenshot stacks

Dependencies & Build Hygiene

mediumOpenHard / riskyHigh blast radius

exceljs and xlsx are both installed for the same job (see previous item); recharts powers ~14 pages while ten @nivo/* packages exist solely for AgentGenerativeUI; html2canvas and html-to-image each serve one DOM-to-image call site. Every pair is duplicated bundle weight and divergent behavior. Also, @types/leaflet sits in dependencies instead of devDependencies.

Fix risk: Spreadsheet parser changes affect every upload tool — cell types and dates may shift.

Fix effort: Dependency consolidation: drop xlsx OR exceljs, drop html2canvas OR html-to-image, audit nivo usage — wide bundle impact.

420

Monolith page files make any cross-cutting change risky

Code Quality & Hygiene

mediumOpenMajor refactorHigh blast radius

wc -l: app/entree/EntreeClient.tsx ~41,917; app/tools/greco/CindyVueClient.tsx ~6,076; app/shorts/page.tsx ~4,327; app/dashboard/page.tsx ~3,496. Loader unification and back-navigation had to tip-toe around them. Any cross-cutting UI change (shared modal, a11y, error banner) risks merge conflicts — gradual panel extraction lowers blast radius.

app/entree/EntreeClient.tsxapp/tools/greco/CindyVueClient.tsxapp/shorts/page.tsxapp/dashboard/page.tsx

Fix risk: Same monolith blast radius — cross-cutting UI in 41k-line EntreeClient is not a safe drive-by fix.

Fix effort: Gradual panel extraction from EntreeClient/CindyVue — weeks of careful work, not one PR.

421

Monolith page files make UI evolution risky

Maintainability (UI-relevant)

mediumOpenMajor refactorHigh blast radius

wc -l: app/entree/EntreeClient.tsx ~41,917; app/tools/greco/CindyVueClient.tsx ~6,076; app/shorts/page.tsx ~4,327; app/dashboard/page.tsx ~3,496. Loader unification and back-navigation pass tip-toed around these — any cross-cutting UI change (shared modal, a11y, error banner) risks merge conflicts across tens of thousands of lines. Gradual panel extraction lowers blast radius.

app/entree/EntreeClient.tsxapp/tools/greco/CindyVueClient.tsxapp/shorts/page.tsxapp/dashboard/page.tsx

Fix risk: Any extraction from EntreeClient/CindyVueClient/shorts/dashboard risks breaking dock-floor panels that share local state — loader pass deliberately avoided these.

Fix effort: Same long-term decomposition.

422

Multi-megabyte JSONB rows in Postgres

Performance & Scale

mediumOpenMajor refactor

greco_cindyvue_sessions stores list_data + grid_data + history_log JSONB per dock day; shanetree_snapshots holds full ODBC modal payloads; quote_submissions.file_attachments embeds base64 in JSONB (app/quote/page.tsx); picker_reports.raw_input_data and report JSONB columns grow unbounded. lib/supabase/user-files.ts correctly uses Storage — these older tables bypass object storage and will dominate backup size and query latency.

app/quote/page.tsxlib/supabase/user-files.ts

Fix effort: Migrate dock sessions, snapshots, quote attachments to Supabase Storage — large data migration, URL rewiring, backward compat.

423

One env var UUID gates the entire warehouse suite

Multi-Tenancy Hardcodes

mediumOpenMajor refactorCritical — test everything

NEXT_PUBLIC_GRECO_COMPANY_ID is load-bearing in ~15 places: GrecoOnlyGuard on every warehouse tool, the session/driver-chat/will-call/item-alert public APIs, unified history's dock-event merge, and dashboard card logic. Plus greco_-prefixed tables (greco_daily_stats, greco_routes, greco_card_permissions) and name-matching on 'greco'. Fine as long as FileDisplay serves one warehouse operator — but it's the single biggest refactor standing between this codebase and a second customer, and worth a deliberate decision rather than gradual accretion.

Fix risk: Removing GRECO_COMPANY_ID hardcode requires threading real tenant context through 29 tools, kiosks, and public APIs — partial migration breaks Greco-only routes for everyone.

Fix effort: Multi-warehouse onboarding refactor — company-scoped tables, remove greco_ prefix pattern, ~15+ env references.

424

The dock session table isn't multi-tenant

Backend & Database

mediumOpenMajor refactorCritical — test everything

greco_cindyvue_sessions has no company_id column. POST app/api/greco/session/route.ts (~176–186) upserts on onConflict: 'session_date' only — list_data/grid_data/history_log keyed globally per calendar day. Writer check (~155–160) verifies profile.company_id === GRECO_COMPANY_ID env UUID, not per-row tenant. DockSheet, /live/greco, /timer and /inbound share one global session row per date — second warehouse impossible without schema change.

app/api/greco/session/route.ts

Fix risk: Adding company_id + RLS touches DockSheet, /live/greco, /timer, /inbound, CindyVueClient autosave, and every onConflict: session_date upsert — second warehouse is a schema migration plus full stack retest.

Fix effort: Add company_id + RLS to greco_cindyvue_sessions and thread through entire DockSheet/live/timer stack — prerequisite for second warehouse customer.

L

Low severity

105
425

.env.example documents WebRTC TURN for a nonexistent calling feature

Permissions, Settings & Misleading UI

lowOpenTrivial fix

Lines 145–153 describe NEXT_PUBLIC_TURN_* for WebRTC relay. Pass 7 noted the calls table has zero app queries; complementary angle: env docs promise video-call infrastructure (calendar call_type column too) that was scaffolded in migrations and never integrated. Operators configuring TURN are wiring a feature that does not exist.

Fix effort: Remove TURN block from .env.example or add '# future — not implemented' banner.

426

/api/ai/models is unauthenticated and triggers Vercel AI Gateway fetch

Security & API Gaps (Pass 24)

lowOpenTrivial fix

app/api/ai/models/route.ts GET (~4–7) has no session, API key, or withRateLimit. discoverAllowedAIModels() in lib/ai/model-discovery.ts (~19) fetches https://ai-gateway.vercel.sh/v1/models on cache miss. Public internet can invoke model catalog discovery and trigger outbound gateway requests — exposes available models and may consume origin rate quota.

app/api/ai/models/route.tslib/ai/model-discovery.ts

Fix effort: Require session or withRateLimit; cache response server-side only.

427

/api/health/ready and /api/health/live have no in-repo callers

Dead Code Hunt (Pass 22)

lowOpenTrivial fix

app/api/health/ready/route.ts and app/api/health/live/route.ts implement standard k8s-style probes, but grep finds zero fetch, docs link, vercel.json reference, or script calling health/ready or health/live. Main /api/health is referenced in problems and ArchitectureDiagram; these two sub-routes may exist only for external infra that was never configured — ~40 lines of unmaintained probe surface.

app/api/health/ready/route.tsapp/api/health/live/route.ts

Fix effort: Wire into vercel.json/k8s probes or delete routes if infra never adopted.

428

/api/routing/routes has no rate limiting

Agent, Collaboration & Platform APIs

lowOpenTrivial fix

app/api/routing/routes/route.ts authenticates via Bearer token but grep finds zero withRateLimit calls. GET/POST can hammer greco_routes full stops JSONB reads/writes without throttling — unlike most app/api/* routes. Route-planning spam could stress Postgres and the Entrée bridge when AI-build is chained.

app/api/routing/routes/route.tsapp/api/

Fix effort: Add withRateLimit to routing/routes GET and POST — config-only pattern.

429

/phl page is an orphan marketing route (~405 lines)

Permissions, Settings & Misleading UI

lowOpenTrivial fix

app/phl/page.tsx is a Permawrite-license landing page with no dashboard card, greco-tools entry or nav link. Grep href='/phl' in app/ finds only docs/page.tsx and ArchitectureDiagram.tsx. ~405 lines maintained with no operational entry point.

app/phl/page.tsx

Fix effort: Delete page or add footer/docs-only nav; avoid maintaining unreachable route.

430

Account settings tabs scroll horizontally with no overflow hint

UI, Flow & Density

lowOpenTrivial fix

app/account/page.tsx settings tab strip uses overflow-x-auto with -mx-4 px-4 (~849) but no fade gradient or scroll affordance (contrast dashboard tab bar which has a sm:hidden right gradient at ~1765). Users on small phones may not realize Notification / Security / API tabs continue off-screen.

app/account/page.tsx

Fix effort: Copy dashboard tab-bar right-edge gradient or add scroll-snap + chevron affordance.

431

addPortSummaryMemory and cleanupRateLimitLogs are never invoked

Dead Code & Orphan Features

lowOpenTrivial fix

lib/supabase/port-summary-memory.ts addPortSummaryMemory() (~30 lines) has zero callers — production uses addPortSummaryMemoryBulk via lib/ai/port-summary-persist.ts. lib/rate-limit.ts cleanupRateLimitLogs() (~10 lines) is exported but no cron or route calls it despite cleanup_rate_limit_logs RPC existing in migrations.

lib/supabase/port-summary-memory.tslib/ai/port-summary-persist.tslib/rate-limit.ts

Fix effort: Delete singular addPortSummaryMemory; schedule cleanupRateLimitLogs in memory-cleanup cron.

432

AGENTS.md documents a vscode-fork/ directory that no longer exists

Dependencies & Build Hygiene

lowOpenTrivial fix

AGENTS.md line 43 warns about vscode-fork/ lint errors — git ls-files finds no such directory. README.md (~70–72) documents yarn dev and pnpm dev in an npm-only repo (package-lock.json). Stale guidance sends agents and contributors hunting for ghosts.

Fix effort: Edit AGENTS.md + README — docs only.

433

AGENTS.md references a vscode-fork/ directory that doesn't exist

Code Quality & Hygiene

lowOpenTrivial fix

AGENTS.md line 43 warns agents about lint errors from vscode-fork/ — glob finds zero such directory in checkout (git ls-files vscode-fork returns empty). README.md Getting Started (~70–72) still documents yarn dev and pnpm dev while package-lock.json and .cursorrules specify npm only. Stale onboarding sends every new contributor/agent on a ghost hunt.

Fix effort: Edit AGENTS.md + README — docs only.

434

BridgeRelayPanel polls relay queue every 1.5s with no visibility gate

Email, v1 API & Entrée Panel Polish

lowOpenTrivial fixMay affect neighbors

app/entree/BridgeRelayPanel.tsx (~414–437) adaptive poll loop has no document.hidden check (unlike PortAISummaryPanel.tsx ~292–297). Leaving the relay panel open on a dock PC sustains shanetree_relay_requests reads all shift — compounds tracked aggressive polling on kiosk screens.

app/entree/BridgeRelayPanel.tsx

Fix risk: Slowing poll delays print relay on Shanetrée — autoprint workflow is time-sensitive.

Fix effort: Skip poll tick when document.hidden — BridgeRelayPanel.tsx, match PortAISummaryPanel.

435

build-output.txt is a stale debug artifact in the repo root

Dependencies & Build Hygiene

lowOpenTrivial fix

git ls-files includes build-output.txt (~8.5 KB) at repo root — captured Windows PowerShell output from a failed next build on EntreeClient.tsx:9454 (duplicate customerOpen useState, Turbopack error). .gitignore covers /build.log but not build-output.txt. Stale failure log from a since-fixed compile error; should be deleted and pattern added to .gitignore.

Fix effort: Delete file + gitignore pattern.

436

Calendar month grid hides event times on mobile

UI, Flow & Density

lowOpenTrivial fix

Month grid event chips render start times with hidden sm:inline (~734) — below sm breakpoint users see colored title text only, no time-of-day. Scheduling at a glance on a phone requires opening the day panel or expanded overlay for every event.

Fix effort: Show abbreviated times on xs (e.g. 9a) or always show time in chip tooltip/title.

437

canJoinSession in ai-chat-sessions is never consulted

Dead Code Hunt (Pass 22)

lowOpenTrivial fix

lib/supabase/ai-chat-sessions.ts:366 exports canJoinSession() for capacity guarding. app/chat/page.tsx uses joinChatSession() directly with no prior canJoinSession check. ~25 lines of session-capacity logic that never gates joins — users can exceed intended session limits silently.

lib/supabase/ai-chat-sessions.tsapp/chat/page.tsx

Fix effort: Call canJoinSession before joinChatSession in chat/page.tsx or remove export.

438

Collaboration TypingIndicator lacks aria-live semantics

Kiosk Surfaces, Greco Tools & Platform Polish

lowOpenTrivial fix

app/components/collaboration/TypingIndicator.tsx (~30–41) renders animated 'X is typing…' with no role='status' or aria-live='polite'. Used in app/documents/page.tsx, app/chat/page.tsx and app/team-chat/page.tsx — screen readers won't announce remote typing during collaborative editing.

app/components/collaboration/TypingIndicator.tsxapp/documents/page.tsxapp/chat/page.tsxapp/team-chat/page.tsx

Fix effort: Add role='status' aria-live='polite' to TypingIndicator.tsx.

439

Controls section nav hides descriptions below large screens

UI, Flow & Density

lowOpenTrivial fix

app/controls/ControlsClient.tsx sidebar section buttons hide blurb text with hidden lg:block (~203). Tablet-width admins (md–lg) see icon + title only — nine sections (Users, Tools, Cards, AI, Email, Integrations…) look like a dense icon list with no context for what each controls.

app/controls/ControlsClient.tsx

Fix effort: Show blurbs from md breakpoint (hidden md:block) or tooltip on hover/focus for section buttons.

440

Developer ?tab=webhooks deep link ignored after mount

Controls, Admin & Collaboration

lowOpenTrivial fix

app/developer/page.tsx (~108–110) reads searchParams once into useState(initialTab) with no useEffect to sync URL changes. In-app navigation to /developer?tab=webhooks from an already-mounted page won't switch tabs — API Keys stays active.

app/developer/page.tsx

Fix effort: useEffect sync activeTab from searchParams when tab param changes.

441

Driver Chat and Thruput share identical card gradients

UI, Colors & Aesthetics

lowOpenTrivial fix

lib/greco/greco-tools.ts GRECO_TOOLS entries at lines 80–81: Driver Chat (/inbox) and Throughput Tracker (/thruput) both set color: 'from-emerald-500 to-teal-600' — same emerald-to-teal Tailwind gradient on adjacent tool-grid cards. Dashboard/minimal layouts use gradient as primary identifier; operators can't tell logistics chat from analytics at a glance without reading labels.

lib/greco/greco-tools.ts

Fix effort: Change color string on one card in greco-tools.ts.

442

Duplicate icons in the tools grid

Aesthetics & Consistency

lowOpenTrivial fixMay affect neighbors

lib/greco/greco-tools.ts GRECO_TOOLS reuses SVG path d strings: Forklift Productivity (line 62) and Throughput Tracker (line 81) both use lightning bolt M13 10V3L4 14h7v7l9-11h-7z; Weekly Returns (line 74) and Cycle Count (line 68) share the same bar-chart icon M9 19v-6a2 2 0 00-2-2H5…; Shorts (line 64) and Damages (line 75) share the warning-triangle path. In compact/minimal dashboard layouts icon is the primary discriminator — duplicates slow floor recognition.

lib/greco/greco-tools.ts

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Same icon path updates.

443

Empty Slots logs activity but never writes greco_daily_stats

Public API, Jobs, Onboarding & Greco Gaps

lowOpenTrivial fix

app/empty-slots/page.tsx calls logReportGeneration + notifyReportGenerated (~507–508) but never upsertDailyStat. app/daily-stats/DailyStatsClient.tsx TOOL_META has no empty_slots key (unlike driver_scan). Daily Stats grid and greco-digest cron stay blind to Empty Slots runs despite being a tracked Greco tool.

app/empty-slots/page.tsxapp/daily-stats/DailyStatsClient.tsx

Fix effort: Call upsertDailyStat on save + add empty_slots to DailyStatsClient TOOL_META.

444

exportTrackerWorkbook in Greco excel-exports is never called

Dead Code Hunt (Pass 22)

lowOpenTrivial fix

app/tools/greco/lib/excel-exports.ts:29 exports exportTrackerWorkbook(). CindyVueClient imports sibling exports (exportCombinedWorkbook, exportFenceReport, etc.) but grep finds zero references to exportTrackerWorkbook — ~80 lines of tracker-specific ExcelJS workbook builder that no UI path triggers.

app/tools/greco/lib/excel-exports.ts

Fix effort: Delete exportTrackerWorkbook from excel-exports.ts or add CindyVue export button.

445

FancyLoader has no screen-reader loading semantics

Email, v1 API & Entrée Panel Polish

lowOpenTrivial fix

app/components/FancyLoader.tsx has no role='status', aria-busy, or aria-live. Used site-wide for auth and data loading gates — assistive-tech users get no 'loading' announcement while sitting on a blank or spinning screen.

app/components/FancyLoader.tsx

Fix effort: Add role='status' aria-live='polite' and visually hidden 'Loading' text to FancyLoader.tsx.

446

FileInput interface is duplicated in processor.ts and processors/base.ts

Dead Code Hunt (Pass 22)

lowOpenTrivial fixMay affect neighbors

lib/tools/processor.ts:46 and lib/tools/processors/base.ts:8 each export interface FileInput with the same shape. Processors import from base.ts; processor.ts re-declares the type for its own ProcessRequest. Minor drift risk (~20 lines duplicate) atop the already-tracked dead lib/tools/index.ts barrel.

lib/tools/processor.tslib/tools/processors/base.tslib/tools/index.ts

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Import FileInput from processors/base.ts in processor.ts; delete local duplicate.

447

Footer on dark pages is nearly invisible

UI, Colors & Aesthetics

lowOpenTrivial fix

app/layout.tsx (~58–60) renders AdminFooterLink on every page. app/components/AdminFooterLink.tsx uses text-slate-400 text-[9px] on all routes. Greco warehouse tools (forklift, shorts, entree) and dashboard dark mode use #0f172a backgrounds — slate-400 on slate-900 fails WCAG contrast (~2.5:1). Footer reads as a stray artifact on dark tool pages rather than intentional subtle branding.

app/layout.tsxapp/components/AdminFooterLink.tsx

Fix effort: Dark-route-aware footer text class via PageBgSync or route-chrome helper.

448

getAllQuoteSubmissions and deleteChatRoom are unused exports

Dead Code & Orphan Features

lowOpenTrivial fix

lib/supabase/quote-submissions.ts getAllQuoteSubmissions() (~30 lines) has zero callers — admin dashboard loads submissions via /api/admin/submissions inline. lib/supabase/chat-rooms.ts deleteChatRoom() (~25 lines) is exported but team-chat never deletes rooms. Two small orphans that suggest unfinished admin/chat management flows.

lib/supabase/quote-submissions.tslib/supabase/chat-rooms.ts

Fix effort: Delete exports or add admin UI that uses them.

449

getMemberPermissions is imported in account but never invoked

Dead Code Hunt (Pass 22)

lowOpenTrivial fixHigh blast radius

lib/supabase/member-permissions.ts:133 exports getMemberPermissions(userId). app/account/page.tsx:15 imports it alongside getAllMemberPermissions / updateMemberPermissions, but grep getMemberPermissions( finds zero call sites — dead import plus dead export. Account admin panel loads all members via getAllMemberPermissions only.

lib/supabase/member-permissions.tsapp/account/page.tsx

Fix risk: Wiring latent toggles suddenly enables or blocks behavior users assumed was active.

Fix effort: Drop unused import from account/page.tsx; delete or keep export only if admin API needs it later.

450

getUnreadMentionCount is a dead export

Dead Code & Orphan Features

lowOpenTrivial fix

lib/supabase/team-chat.ts:334 exports async getUnreadMentionCount() querying chat_mentions with .eq('mentioned_user_id', user.id) — zero grep importers. Dashboard header and account page use getMyUnreadMentionCount (~line 310) with room-scoped logic. Dead ~25-line duplicate; deleting export won't break any caller.

lib/supabase/team-chat.ts

Fix effort: Delete export or redirect to getMyUnreadMentionCount.

451

getUpcomingEvents is a dead import and orphan export in calendar

Dead Code Hunt (Pass 22)

lowOpenTrivial fix

lib/supabase/calendar.ts:121 exports getUpcomingEvents(days, limit). app/calendar/page.tsx:13 imports it but grep getUpcomingEvents( finds zero call sites — page loads full events via getCalendarEvents only. Dead import on the page plus ~35 lines of unused 'next N days' query logic.

lib/supabase/calendar.tsapp/calendar/page.tsx

Fix effort: Remove import from calendar/page.tsx; delete export or use in dashboard sidebar widget.

452

hiddenDashboardCards localStorage is global per browser profile

Permissions, Settings & Misleading UI

lowOpenTrivial fixMay affect neighbors

dashboard/page.tsx (~454, ~487) and account/page.tsx (~199, ~213) read/write hiddenDashboardCards with no user suffix. On a shared warehouse PC one operator hiding Overview cards affects the next login on the same browser profile.

Fix risk: Per-user suffix resets card layout for shared dashboard login — operators may lose custom grid.

Fix effort: Namespace key with userId like homepage-bg-theme should be.

453

Homepage background theme localStorage is not namespaced per user

Code Quality & Hygiene

lowOpenTrivial fix

app/page.tsx (~359–370): useEffect reads/writes localStorage key 'homepage-bg-theme' globally — no auth.uid() or company_id suffix. Contrast lib/i18n/context.tsx greco-lang-pref and WindowClient window-theme keys namespaced per context. Shared dock PC: operator A picks 'midnight' gradient; operator B on / landing inherits A's choice before login.

app/page.tsxlib/i18n/context.tsx

Fix effort: Suffix key with userId once session known in app/page.tsx.

454

Item-alert confirmation email uses UTC sessionDate

Shanetrée, Routing & Driver Comms

lowOpenTrivial fixHigh blast radius

app/api/public/item-alert-subscribe/route.ts (~107) sets data.sessionDate to new Date().toISOString().slice(0, 10) in the Resend confirmation template. Subscribers near the Eastern midnight boundary see tomorrow's date in the 'you're subscribed' email while the warehouse floor still considers it today.

app/api/public/item-alert-subscribe/route.ts

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Use warehouse-local date in item-alert-subscribe confirmation template data.

455

Item-alert confirmation reuses the truck-scheduled email template

Window, Sales & Kiosk Scheduling

lowOpenTrivial fix

app/api/public/item-alert-subscribe/route.ts line 100 sends template:'item_scheduled' with actionLabel:'Subscription Confirmed' — a truck-scheduled template, not a dedicated subscribe confirmation. Misleading subject/body for new item-alert subscribers. Separate from tracked UTC sessionDate in the same route.

app/api/public/item-alert-subscribe/route.ts

Fix effort: Add dedicated item_alert_subscribed template in lib/email/service.ts.

456

Leaderboard still awards points for removed Smart Report actions

Dead Code & Orphan Features

lowOpenTrivial fix

Migration 094 removed Smart Reports, but lib/supabase/leaderboard.ts still assigns 8 points to smart_report_api_generated and smart_report_recipe_generated and includes them in the Reports bucket filter. No new events can arrive — the scoring rules are dead weight that mislead anyone tuning leaderboard weights. Related HistoryLedger styling is tracked separately in Hygiene; this is the useless gamification half.

lib/supabase/leaderboard.ts

Fix effort: Remove smart_report_* from POINT_VALUES and Reports bucket in leaderboard.ts.

457

lib/notifications/trigger logs full event payloads to the browser console

Public API, Jobs, Onboarding & Greco Gaps

lowOpenTrivial fix

lib/notifications/trigger.ts console.log('[TRIGGER] Data:', JSON.stringify(data)) (~31) on every notification. Report filenames, mention contexts and member metadata appear in DevTools on shared warehouse machines. Picker notifyReportGenerated console dump is tracked separately — this is the shared module every Greco tool calls.

lib/notifications/trigger.ts

Fix effort: Remove or gate console.log behind NODE_ENV — strip from production bundle.

458

lib/tools/index.ts barrel is never imported

Dead Code & Orphan Features

lowOpenTrivial fix

The ~74-line re-export barrel documents importing from '@/lib/tools', but grep finds no live imports of that path — callers import lib/tools/registry, processor and detector directly. Barrel also re-exports getProcessableTools(), validateToolMatch() and getAvailableProcessors(), which are themselves uncalled (~40 lines across registry/processor/detector).

lib/toolslib/tools/registry

Fix effort: Delete barrel file and unused getProcessableTools/validateToolMatch/getAvailableProcessors exports.

459

Login page never redirects already-authenticated users

Auth & Session

lowOpenTrivial fixMay affect neighbors

app/login/page.tsx mount useEffect (~20–35) only reads ?redirect= param and fd_remembered_email from localStorage — no supabase.auth.getSession() or getUser() check. Logged-in users visiting /login see the full sign-in form until they submit credentials again. Contrast signup/reset-password patterns; missing middleware.ts would also catch this centrally.

app/login/page.tsx

Fix risk: Auto-redirect to dashboard can loop with ?redirect= open-redirect fixes or break kiosk landing on /login.

Fix effort: getSession() on mount → router.replace(redirect || '/dashboard').

460

Micro-spinners still vary

Loading & Transitions

lowPartly fixedTrivial fix

Panel-scale loaders (analytics tabs, chat panels, settings modals…) were unified onto the fancy loader, but tiny in-button spinners (Save, Add, Generating…) keep their small border-spinner style — a 3-ring loader isn't legible at 12–16px. They're consistent within their contexts but not identical across pages. Acceptable, noted for completeness.

Fix effort: Acceptable as-is; optional unified micro-spinner component.

461

Monitoring alerts webhook-failure query has no row cap

Warehouse Tool Coverage, Monitoring & Collaboration

lowOpenTrivial fixMay affect neighbors

app/api/monitoring/alerts/route.ts (~172–177) selects all webhook_deliveries with success=false in the last 24h with no .limit(). High-volume tenants with flaky webhook endpoints can return thousands of failure IDs per founder poll — compounds unbounded metrics collectors above.

app/api/monitoring/alerts/route.ts

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Add .limit(100) on webhook_deliveries failure select in monitoring/alerts/route.ts.

462

Monitoring APIs ignore profiles.is_founder flag

Agent, Collaboration & Platform APIs

lowOpenTrivial fix

app/api/monitoring/metrics/route.ts (~87–88) and monitoring/alerts/route.ts gate on role === 'admin' || role === 'founder' only. Unlike app/api/admin/members/route.ts (~57–59), they omit is_founder. A user with is_founder: true and role: 'editor' is denied /monitoring despite being a founder — inconsistent with GodView and admin member routes.

app/api/monitoring/metrics/route.tsapp/api/admin/members/route.ts

Fix effort: Add is_founder check to monitoring/metrics and alerts routes — match admin/members pattern.

463

Most cron routes expose unauthenticated GET metadata

API & Operations

lowOpenTrivial fix

job-retries, stale-jobs, greco-digest, webhooks, agent-autonomy, memory-cleanup and activity-log-cleanup export public GET returning { status: 'ok', endpoint: '...' } without CRON_SECRET check. Confirms internal route names and schedules to unauthenticated scanners (greco-digest also leaks retention_days).

Fix effort: Return 401 on GET unless CRON_SECRET header present — ~7 cron route files.

464

npm packages ai and @ai-sdk/openai have zero TypeScript imports

Dead Code Hunt (Pass 22)

lowOpenTrivial fix

package.json lists ai ^6.0.86 and @ai-sdk/openai ^3.0.29. Grep from 'ai' and from '@ai-sdk' across *.ts/tsx finds zero imports — all live AI paths use @/lib/ai/gateway, geminiFetch, and direct fetch to Google APIs. Two unused SDK dependencies adding install weight and audit surface.

lib/ai/gateway

Fix effort: npm uninstall ai @ai-sdk/openai if no planned Vercel AI SDK migration.

465

Personal names in tool titles

Naming & Labels

lowOpenTrivial fix

lib/greco/greco-tools.ts:69 registers { name: "Juan's Counting Tool", shortName: "Juan's Tool", route: '/counting' } in GRECO_TOOLS. Appears on dashboard tool grid, Controls greco_card_permissions matrix (keyed by /counting), and lib/i18n strings. Production tool title tied to one person — ages poorly as teams change; functional name + subtitle credit is safer.

lib/greco/greco-tools.tslib/i18n

Fix effort: Rename Juan's Tool card in greco-tools.ts.

466

Picker tool dumps notification payloads to the console

Wiring & Broken Flows

lowOpenTrivial fix

app/picker/page.tsx (~1233–1246): after notifyReportGenerated(), console.log('[PICKER] Starting email notification...'), console.log('Notification result:', JSON.stringify(notifyResult, null, 2)) dumps member emails and send counts. Shared warehouse PCs with devtools open leak notification metadata — should use structured logger behind NODE_ENV or remove.

app/picker/page.tsx

Fix effort: Remove console.log in app/picker/page.tsx after notifyReportGenerated.

467

public/ still ships default create-next-app SVGs beside production brand assets

Repository Structure Audit (Pass 23)

lowOpenTrivial fix

git tracks public/next.svg and public/vercel.svg alongside logo.png, deltamorph-icon.png and warehouse photography. Boilerplate assets signal an unpolished deploy to anyone inspecting static files. Delete unused SVGs; add public/README.md listing intentional brand files only.

Fix effort: Delete next.svg and vercel.svg if unused; grep public/ references first.

468

PullLogPanel today stats bucket uses UTC midnight

Shanetrée, Routing & Driver Comms

lowOpenTrivial fixHigh blast radius

stats useMemo in app/entree/PullLogPanel.tsx (~323) compares created_at.slice(0,10) to new Date().toISOString().slice(0,10) for the 'today' HUD counter. Evening ODBC pulls count toward tomorrow's bucket — subset of Entree UTC date issues but the Pull Log operator HUD is not called out in pass 10.

app/entree/PullLogPanel.tsx

Fix risk: Date boundary changes reorder stats, leaderboards, and filters keyed on existing date columns.

Fix effort: Compare against warehouse-local today string in PullLogPanel stats useMemo.

469

Quote page lets viewers fill the form before the role warning

UI, Flow & Density

lowOpenTrivial fix

app/quote/page.tsx renders the full project-request form (title, description, file upload) for every role. canSubmit gates only the submit button and shows a 'Viewers cannot submit' banner at the bottom (~238–252). Viewers can spend time filling fields and attaching files before discovering submission is blocked — should disable inputs or show the restriction at the top.

app/quote/page.tsx

Fix effort: Disable inputs when !canSubmit or show view-only banner above the form on mount.

470

RATE_LIMIT_FAIL_CLOSED is undocumented in .env.example

Code Quality & Hygiene

lowOpenTrivial fix

lib/rate-limit.ts reads RATE_LIMIT_FAIL_CLOSED (fail-open on RPC errors unless 'true'). Referenced in problems copy but missing from .env.example — prod deploys won't know the knob exists. Earlier env-var audit marked fixed but this one was left out.

lib/rate-limit.ts

Fix effort: Add one line to .env.example documenting fail-closed rate limiting.

471

RELAY_ENDPOINT_OPTIONS registers /subs twice

Shanetrée, Routing & Driver Comms

lowOpenTrivial fix

lib/supabase/shanetree-relay.ts lines 68 and 75 both list { value: '/subs', … } with different labels ('Subs' vs 'Subs (OOS + Demand)'). ALL_RELAY_ENDPOINTS dedupes via Set so runtime is fine, but the ODBC relay allowlist UI can show duplicate /subs entries and signals copy-paste drift in the bridge relay registry.

lib/supabase/shanetree-relay.ts

Fix effort: Remove duplicate /subs entry; consolidate labels in shanetree-relay.ts.

472

Root viewport export lacks viewportFit cover for iOS kiosk safe areas

Kiosk Surfaces, Greco Tools & Platform Polish

lowOpenTrivial fix

app/layout.tsx viewport (~14–20) sets width, initialScale and userScalable but not viewportFit: 'cover'. lib/custom-tools/bridge.ts ToolAPI viewer uses env(safe-area-inset-top) (~290) but root layout doesn't enable notch-aware viewport — fixed headers on iPhone kiosks can clip under the status bar.

app/layout.tsxlib/custom-tools/bridge.ts

Fix effort: Add viewportFit: 'cover' to app/layout.tsx viewport export.

473

Shanetrée PrintPanel polls history without visibility gating

Shanetrée, Routing & Driver Comms

lowOpenTrivial fix

While the Print modal is open, PrintPanel.tsx (~147–151) setIntervals refreshHistory every 8s with no document.hidden check. Related to tracked aggressive 3s auto-print/will-call polling, but PrintPanel is a separate always-on loop whenever an operator leaves the print history modal open — sustained load on shanetree_print_log reads.

Fix effort: Skip refreshHistory tick when document.hidden or increase interval on hidden tab.

474

Smart Reports ghosts in history and leaderboard

Code Quality & Hygiene

lowOpenTrivial fix

Migration 094 removed Smart Reports, but grep finds live references: app/components/HistoryLedger.tsx (~80, ~136) badges smart_report_recipe_generated; lib/supabase/leaderboard.ts POINT_VALUES (~41–42) and Reports bucket (~80); lib/history/history-excel-export.ts (~119–120). No emitter remains — dead styling and 8-point leaderboard weights for action types that can never fire again.

app/components/HistoryLedger.tsxlib/supabase/leaderboard.tslib/history/history-excel-export.ts

Fix effort: Remove smart_report_* cases from HistoryLedger + history-excel-export.

475

Smart Reports ghosts in the ledger and leaderboard

Dead Code & Orphans

lowOpenTrivial fix

Migration 094 removed Smart Reports tables, but grep finds live references: app/components/HistoryLedger.tsx (~80, ~136, ~186) still styles smart_report_recipe_generated badges; lib/supabase/leaderboard.ts POINT_VALUES (~41–42) awards 8 points each for smart_report_api_generated and smart_report_recipe_generated; Reports bucket filter (~80) includes both; lib/history/history-excel-export.ts (~119–120, ~168) maps them to Excel labels. No code path can emit new smart_report_* events — dead styling and scoring confuse leaderboard tuning.

app/components/HistoryLedger.tsxlib/supabase/leaderboard.tslib/history/history-excel-export.ts

Fix effort: Remove smart_report_* cases from HistoryLedger + history-excel-export.

476

subscribeToTeamMessages is orphaned dead code

Code Quality & Hygiene

lowOpenTrivial fix

lib/supabase/team-chat.ts subscribeToTeamMessages() subscribes to channel 'team-chat' on legacy table team_messages, but team-chat migrated to chat_rooms / subscribeToChatRoomMessages. Never imported anywhere — dead realtime wiring that could confuse future refactors.

lib/supabase/team-chat.ts

Fix effort: Delete function from lib/supabase/team-chat.ts.

477

TeamMember interface is duplicated between calendar and team-chat

Dead Code Hunt (Pass 22)

lowOpenTrivial fixMay affect neighbors

lib/supabase/calendar.ts:316 exports TeamMember used by calendar flows. app/team-chat/page.tsx:24 defines a separate local interface TeamMember with overlapping fields — never imports the calendar export. Two shapes for the same roster concept; refactors to calendar TeamMember won't typecheck team-chat usages.

lib/supabase/calendar.tsapp/team-chat/page.tsx

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Import TeamMember from calendar.ts in team-chat/page.tsx or extract shared type.

478

The health endpoint maps the infrastructure for anyone

Security & Trust Boundaries

lowOpenTrivial fix

GET app/api/health/route.ts (~256–279) has no auth. ?detailed=true runs parallel checkDatabase, checkCache, checkJobQueue, checkEmailService, checkAIService, checkExternalAPIs (~275) and returns per-component status/latency/message. checkAIService (~219) probes GOOGLE_AI_API_KEY env (tracked mismatch vs GOOGLE_API_KEY). Zero grep callers enforce CRON_SECRET or session — ArchitectureDiagram.tsx references the route but no gate exists. Free infra reconnaissance for unauthenticated scanners.

app/api/health/route.ts

Fix effort: Require CRON_SECRET or session for ?detailed=true on /api/health.

479

Three chat-rooms read helpers are orphaned exports

Dead Code Hunt (Pass 22)

lowOpenTrivial fix

lib/supabase/chat-rooms.ts exports getChatRoom() (~168), getNotificationSetting() (~444) and getChatRoomMembers() (~507) with zero imports outside the file. Live /team-chat uses getChatRooms, sendChatRoomMessage, toggleNotifications and subscribeToChatRoomMessages — but never fetches a single room by ID, reads per-room notification prefs, or lists members via these helpers (~90 lines).

lib/supabase/chat-rooms.ts

Fix effort: Delete getChatRoom, getNotificationSetting, getChatRoomMembers or wire team-chat member list / mute UI.

480

Three pairs of duplicate tool icons

UI, Colors & Aesthetics

lowOpenTrivial fixMay affect neighbors

lib/greco/greco-tools.ts: Forklifts + Thruput share the lightning bolt; Cycle Count + Weekly Returns share the bar chart; Skips + Damages share the warning triangle. In compact/minimal dashboard layouts icon is the primary identifier.

lib/greco/greco-tools.ts

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Assign distinct SVG paths in greco-tools.ts for 6 cards.

481

Trigger functions in migrations 114 and 136 omit SET search_path

Security & API Gaps (Pass 24)

lowOpenTrivial fix

supabase/migrations/133_create_will_call_appointments.sql (~89–98) correctly sets SET search_path = '' on plpgsql triggers. Newer migrations omit it: 114_create_greco_routes.sql set_greco_routes_updated_at() (~61–67) and 136_create_account_geocodes.sql set_account_geocodes_updated_at() (~59–65) use LANGUAGE plpgsql without search_path pin. Supabase linter flags search_path-mutable triggers — privileged user could shadow pg_catalog in trigger context. Separate from account_geocodes missing FK issue.

supabase/migrations/133_create_will_call_appointments.sql

Fix effort: New migration: ALTER FUNCTION ... SET search_path = '' on both trigger functions.

482

UISpec type is duplicated in three UI files instead of importing renderer

Dead Code Hunt (Pass 22)

lowOpenTrivial fixMay affect neighbors

Canonical UISpec lives in lib/agent/renderer.ts:27. Local interface UISpec duplicates exist in app/components/AgentGenerativeUI.tsx:50, app/chat/page.tsx:33 and app/daily-stats/GrecoAnalyticsChat.tsx:8. Only app/api/analytics-chat/route.ts imports UISpec from renderer. Schema drift risk when agent chart payloads add fields — three copies can desync silently.

lib/agent/renderer.tsapp/components/AgentGenerativeUI.tsxapp/chat/page.tsxapp/daily-stats/GrecoAnalyticsChat.tsx

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Replace local interface UISpec with import type from lib/agent/renderer.ts in three files.

483

UserHistoryPanel roster sidebar swallows load failures

Email, v1 API & Entrée Panel Polish

lowOpenTrivial fix

app/entree/UserHistoryPanel.tsx loadRoster() (~884–885) empty catch with comment 'roster is optional UX sugar'. Bridge timeout or RLS failure looks identical to 'no recent users' — operators cannot distinguish empty history from a failed ODBC pull.

app/entree/UserHistoryPanel.tsx

Fix effort: Surface inline error in roster sidebar on loadRoster catch — UserHistoryPanel.tsx.

484

v1 POST /search accepts unvalidated similarity_threshold

Public API, Jobs, Onboarding & Greco Gaps

lowOpenTrivial fix

app/api/v1/search/route.ts passes client similarity_threshold straight through (~34, 52) to semanticSearch() in lib/embeddings/service.ts with no clamp to 0–1. Values like -1 or 999 reach the search_similar_reports RPC — unpredictable result sets or Postgres errors.

app/api/v1/search/route.tslib/embeddings/service.ts

Fix effort: Clamp similarity_threshold to [0, 1] before RPC call.

485

/api/account/username-check enables cross-tenant username probing

Agent, Collaboration & Platform APIs

lowOpenEasy fix

app/api/account/username-check/route.ts (~35–40) uses service role to select('id').eq('username', username) on profiles globally. Any authenticated user learns whether a username is taken anywhere in the platform (available: false) — enumeration across companies, not just within own tenant.

app/api/account/username-check/route.ts

Fix effort: Scope query to own company or return available without revealing global occupancy — privacy tradeoff.

486

/api/desktop-download, /api/cache and monitoring routes are unwired

Dead Code & Orphan Features

lowOpenEasy fix

Grep finds no app fetch to /api/desktop-download (~113 lines), /api/cache (~163 lines), /api/monitoring/metrics (~321 lines) or /api/monitoring/alerts (~217 lines). Only docs/ArchitectureDiagram.tsx references them. ~814 lines of HTTP surface for desktop distribution, cache admin and ops dashboards that nothing in the product invokes.

Fix effort: Link from Developer/docs UI or delete four unwired route files (~814 lines).

487

Account duplicates the full Member Permissions UI from Controls

Controls, Admin & Collaboration

lowOpenEasy fixMay affect neighbors

app/account/page.tsx (~1792+) has a founder-only Member Permissions block duplicating app/controls/sections/MemberPermissionsSection.tsx. Tab-visibility duplication is tracked; this ~260-line permissions duplicate can drift independently (same stale ALL_TOOLS list in both).

app/account/page.tsxapp/controls/sections/MemberPermissionsSection.tsx

Fix risk: Deduping parallel implementations must update every copy or behavior drifts again.

Fix effort: Extract shared MemberPermissionsPanel component used by Account and Controls only.

488

Almost every browser tab says 'FileDisplay.com'

UI, Colors & Aesthetics

lowOpenEasy fix

app/layout.tsx metadata (~9–10) exports title: 'FileDisplay.com' only — blank default description. generateMetadata or per-route titles exist only on routing, transfers, drivers, our-story (grep 'export const metadata' in app/). Dashboard, /chat, /calendar, /build, /forklift inherit generic tab label — indistinguishable bookmarks and OG previews.

app/layout.tsx

Fix effort: Add title template in root layout + per-route metadata exports — mechanical across ~40 routes.

489

Bridge proxy calls rely on maxDuration, not a fetch timeout

Scalability & Performance

lowOpenEasy fix

/api/entree/query and /api/public/will-call-items fetch the dock-PC bridge with no per-request AbortSignal. An earlier pass added AbortSignal.timeout(30s) here, but that was reverted: legitimate ODBC reports (a full-month Sales range, wide date windows) routinely run 1-2+ minutes, so a 30s abort would kill valid slow queries. These routes intentionally set maxDuration (300 / 60) as the real ceiling and let the platform enforce it. The short-budget callers that DO cut off fast (inbound-relay, po-relay at ~4.5s) can afford to because they fall back to the pull-based relay queue on timeout — the proxy routes have no such fallback, so aborting early just fails the request. Net: left as-is; the original “hung query holds the function open” worry is already bounded by maxDuration.

Fix effort: Documented intentional — only 'fix' if product wants sub-minute abort with relay fallback for entree/query.

490

Build-page preview storage isn't namespaced

Database & Migrations

lowOpenEasy fixMay affect neighbors

lib/custom-tools/bridge.ts persists preview tool data under global localStorage keys (fd_build_preview_tool_data_v1 and friends) keyed at most by toolId — never by user or company. On a shared warehouse workstation, one account's preview state bleeds into the next login's build session.

lib/custom-tools/bridge.ts

Fix risk: Namespacing clears preview state between accounts on shared PC — acceptable but confusing mid-build.

Fix effort: Same as namespaced-by-user entry — prefix localStorage keys with userId.

491

Build-page preview storage isn't namespaced by user

Code Quality & Hygiene

lowOpenEasy fixMay affect neighbors

lib/custom-tools/bridge.ts persists preview data under global localStorage keys keyed at most by toolId — never by user or company. Shared warehouse workstation: one account's preview bleeds into the next login's build session.

lib/custom-tools/bridge.ts

Fix risk: Duplicate title — same shared-workstation preview bleed.

Fix effort: Prefix localStorage keys with userId in lib/custom-tools/bridge.ts.

492

Counting metric and day labels bypass i18n

Warehouse Tool Coverage, Monitoring & Collaboration

lowOpenEasy fix

app/counting/CountingClient.tsx — METRIC_OPTIONS (~78–81) and DAY_LABELS / DAY_LABELS_FULL (~83–84) are hardcoded English while the rest of the page uses t(). Spanish-preference operators see bilingual chrome with English-only metric/day headers in the counting grid.

app/counting/CountingClient.tsx

Fix effort: Wrap METRIC_OPTIONS and DAY_LABELS in t() calls in CountingClient.tsx.

493

Damages omits embedded AnalyticsLogPanel that sibling tools include

Warehouse Tool Coverage, Monitoring & Collaboration

lowOpenEasy fix

app/damages/page.tsx gates AI via getToolPagePermissions('Damages') but never renders AnalyticsLogPanel. app/shrink-meeting/page.tsx (~706) and app/forklift/page.tsx embed it. Licenses omission is tracked separately — Damages operators cannot audit runs on the floor without visiting Daily Stats.

app/damages/page.tsxapp/shrink-meeting/page.tsxapp/forklift/page.tsx

Fix effort: Add AnalyticsLogPanel + canViewAnalytics to damages/page.tsx — copy shrink-meeting wiring.

494

DockSheet Live status filters lack aria-pressed semantics

UI, Colors & Aesthetics

lowOpenEasy fix

app/live/greco/page.tsx check-in/receiving tab toggle, building/layout chips and status filter pills are styled buttons with no aria-pressed, role='tablist' or aria-current. Third accessibility gap on a wall-mounted kiosk screen (after calendar and members modal).

app/live/greco/page.tsx

Fix effort: Add aria-pressed to filter chips and role=tablist/tab on check-in/receiving toggle in live/greco/page.tsx.

495

Duplicate tab-visibility UIs with identical stale tab lists

Permissions, Settings & Misleading UI

lowOpenEasy fixMay affect neighbors

Same ALL_DASHBOARD_TABS array and upsert-to-company_tab_settings logic exist in account/page.tsx (~422–478, ~1706+) and controls/sections/TabVisibilitySection.tsx (~34–78). ~260 lines of duplicated admin UI that can drift independently and both already target obsolete tab names.

Fix risk: Consolidating UIs changes where admins edit — both account and controls paths exist today.

Fix effort: Extract shared TabVisibilityEditor component; one tab list constant.

496

Every tab says “FileDisplay.com”

Wiring & Broken Flows

lowOpenEasy fix

The root layout exports one generic title with a blank description, and only a handful of routes (routing, transfers, drivers) set their own metadata. Dashboard, Chat, Calendar, Build and the warehouse tools are indistinguishable in browser tabs, bookmarks and link previews. A per-page title template would fix all of it.

Fix effort: Same metadata template pass.

497

IDE file_path accepts traversal-like paths with no normalization

Agent, Collaboration & Platform APIs

lowOpenEasy fix

app/api/ide/workspaces/[id]/files/route.ts (~78–80) and app/api/ide/patch/route.ts parsePlanJson accept file_path with trim/slice(0,300) only — no rejection of .., leading /, or absolute paths. Could create overlapping or confusing workspace file keys; enterprise IDE sync should canonicalize paths server-side.

app/api/ide/workspaces/app/api/ide/patch/route.ts

Fix effort: Reject .. and leading /; normalize to relative workspace paths in IDE file routes.

498

Licenses omits AnalyticsLogPanel that Negative Slots includes

Kiosk Surfaces, Greco Tools & Platform Polish

lowOpenEasy fix

app/negative-slots/page.tsx wires AnalyticsLogPanel + canViewAnalytics from getToolPagePermissions. app/licenses/page.tsx only gates AI via getToolPagePermissions('Licenses') — no embedded analytics log for operators auditing license export runs on the floor.

app/negative-slots/page.tsxapp/licenses/page.tsx

Fix effort: Add AnalyticsLogPanel + canViewAnalytics to licenses/page.tsx — copy negative-slots pattern.

499

Members and Support flash protected UI before redirect

Auth & Session

lowOpenEasy fix

app/members/page.tsx and app/support/page.tsx push to /login on missing session but have no loading gate — the full page shell (headings, layout, empty tables) renders immediately while the redirect races. Worse UX than a spinner and a brief leak of page structure to logged-out visitors.

app/members/page.tsxapp/support/page.tsx

Fix effort: Add loading gate (don't render shell until auth resolves) on members + support pages.

500

Nine orphan exports across live Supabase helper modules

Dead Code Hunt (Pass 22)

lowOpenEasy fix

Grep-verified zero external callers: individual-report-settings.getReportEmailAddresses() (~394), notifications.isNotificationEnabled() (~198) and updateNotificationSettings() (~242), greco-card-permissions.resetMemberCards() (~299), company-settings.getCompanySettingsById() (~128), ai-filters.getAllAIFilters() (~53) and getRecentAIFilterChanges() (~183), port-ai-summaries.listRecentPortAISummaries() (~95), shanetree-snapshots.updateShanetreeSnapshotMeta() (~317), documents.getDocument() (~106). Sibling exports in each file are live — these are unfinished admin/list/meta paths (~280 lines combined).

Fix effort: Delete nine verified orphan exports or wire admin UIs that were planned (filters list, snapshot meta edit, document by-id fetch).

501

No supabase/config.toml — migrations-only folder with no local stack story

Repository Structure Audit (Pass 23)

lowOpenEasy fix

supabase/ contains only migrations/ (no config.toml, seed.sql, or functions/). Standard Supabase CLI layout uses supabase/config.toml for local docker stack. Repo documents hosted Supabase only (AGENTS.md) but structure does not match CLI docs newcomers expect — minor professionalism gap for contributors used to supabase init repos.

supabase/config.toml

Fix effort: Run supabase init for config.toml or document hosted-only workflow in docs/DATABASE.md.

502

No web app manifest — public kiosks cannot install as standalone PWA

Kiosk Surfaces, Greco Tools & Platform Polish

lowOpenEasy fix

No manifest.json or manifest.webmanifest in repo; app/layout.tsx metadata has title/description only (~9–12). /timer, /sales, /window and /inbound are mobile-first kiosk surfaces but cannot be 'Add to Home Screen' as branded standalone apps with icon and display mode.

app/layout.tsx

Fix effort: Add public/manifest.webmanifest + link in layout metadata — icons for timer/sales kiosks.

503

POST /api/embeddings accepts arbitrary sourceTable values

Agent, Collaboration & Platform APIs

lowOpenEasy fix

app/api/embeddings/route.ts (~64–79) requires sourceTable but does not allowlist it before storeEmbedding(). Unlike lib/agent/tools.ts report-table guards, authenticated callers can tag embeddings with arbitrary table names — pollutes semantic search index and complicates embedding cleanup per tool.

app/api/embeddings/route.tslib/agent/tools.ts

Fix effort: Allowlist sourceTable against report tables + shared_documents — mirror agent tools guard.

504

POST /api/ide/settings-sync and client-event accept unbounded payload blobs

Agent, Collaboration & Platform APIs

lowOpenEasy fixMay affect neighbors

app/api/ide/settings-sync/route.ts (~60–77) upserts settings/keybindings/extensions text via service role with no length cap. app/api/telemetry/client-event/route.ts (~35–48) inserts event_data JSONB with no size limit. Authenticated users can bloat ide_user_settings rows and client_events with multi-megabyte payloads per request.

app/api/ide/settings-sync/route.tsapp/api/telemetry/client-event/route.ts

Fix risk: Adding caps breaks export or load-all flows that depend on fetching entire tables.

Fix effort: Cap settings/keybindings/extensions and event_data JSONB size (e.g. 1MB) — reject with 413.

505

printable-guide.ts (~1,200 lines) is docs/whitepaper-only runtime

Dead Code Hunt (Pass 22)

lowOpenEasy fixHigh blast radius

lib/guide/printable-guide.ts exports buildGuideHTML() consumed only by app/docs/page.tsx:10 (print guide button) and whitepaper embed scripts. Zero imports from dashboard, Greco tools, Entrée, or warehouse routes — large guide-rendering module that never runs in operator workflows; maintainable dead weight unless docs are considered product-critical.

lib/guide/printable-guide.tsapp/docs/page.tsx

Fix risk: Large single-file page — extracting or refactoring risks breaking embedded business logic.

Fix effort: Keep for docs or move to scripts/; delete only if /docs print guide is retired.

506

public/deltamorph-icon.png is ~2 MB without a compressed favicon set

Repository Structure Audit (Pass 23)

lowOpenEasy fix

du reports public/deltamorph-icon.png at ~2.0 MB — likely unoptimized PNG used as PWA/icon source. No public/favicon.ico or sized icon set in repo. Professional frontends ship 32/192/512 variants (often via next/metadata icons). Large single PNG slows every cold load that references it.

Fix effort: Compress PNG; add favicon.ico and 192/512 icons via app/icon.tsx metadata.

507

routing.lastActiveRouteId localStorage is global per browser profile

Shanetrée, Routing & Driver Comms

lowOpenEasy fixMay affect neighbors

LAST_ACTIVE_ROUTE_KEY = 'routing.lastActiveRouteId' in RoutingClient.tsx (~69) is read/written with no userId suffix (~873, 921, 1092). Shared warehouse dispatch PCs restore the previous operator's in-progress route on login — same class as tracked hiddenDashboardCards but routing persistence is not named.

Fix risk: Routing planner remembers route per browser not per user — namespacing changes default route on shared PC.

Fix effort: Prefix LAST_ACTIVE_ROUTE_KEY with userId on shared dispatch PCs.

508

Thirteen Nivo render* helpers in agent/renderer.ts have zero callers

Dead Code Hunt (Pass 22)

lowOpenEasy fix

lib/agent/renderer.ts exports renderBubble, renderTreemap, renderSparklineTable, renderNumberTrend, renderSankey, renderChord, renderBump, renderStream, renderSwarmplot, renderCirclePacking, renderNetwork, renderSunburst, renderParallelCoordinates and renderMarimekko — grep finds definitions only. lib/agent/greco-analytics-tools.ts imports other render* helpers; AgentGenerativeUI.tsx uses @nivo/* directly. ~500+ lines of advanced chart builders that agent tools can never invoke.

lib/agent/renderer.tslib/agent/greco-analytics-tools.ts

Fix effort: Delete unused render* exports from renderer.ts; keep only helpers greco-analytics-tools imports.

509

useCollaborators requires both presence and broadcast to show connected

Email, v1 API & Entrée Panel Polish

lowOpenEasy fix

lib/realtime/useCollaborators.ts (~229) sets isConnected: presConnected && bcConnected. If one Supabase realtime channel fails (network blip, quota), collaboration UI shows fully disconnected even when cursors or typing on the other channel still work — confusing half-connected state.

lib/realtime/useCollaborators.ts

Fix effort: Show partial-connected state or retry failed channel — useCollaborators.ts.

510

/api/jobs routes have no UI callers

Agent, API & Docs Integrity

lowOpenModerate effort

grep 'fetch.*(/api/jobs' across app returns zero matches. app/api/jobs/route.ts (~200 lines) and jobs/enqueue expose authenticated job queue management, but no dashboard/developer/GodView panel surfaces queue status despite lib/jobs/service.ts being load-bearing for webhooks, AI and agent work.

app/api/jobs/route.tslib/jobs/service.ts

Fix effort: Add job queue panel to Developer/GodView or delete authenticated jobs HTTP surface.

511

Access-denied screens differ per page

UI, Colors & Aesthetics

lowOpenModerate effort

app/components/GrecoOnlyGuard.tsx renders polished 'Tool Not Available' for status='denied'. app/controls/, app/quotes-admin/, app/ide/ each use inline permission gates with different copy and layout. No shared DeniedAccess component — consistency pass stopped at FancyLoader unification.

app/components/GrecoOnlyGuard.tsxapp/controls/app/quotes-admin/app/ide/

Fix effort: Extract shared AccessDeniedCard component and adopt on controls, IDE, quotes-admin, etc.

512

app/api/ has confusing sibling folders — public/ vs public-tools/, ai/ vs ai-analyze/

Repository Structure Audit (Pass 23)

lowOpenModerate effort

31 top-level API directories include both public/ (unauthenticated ODBC relays) and public-tools/ (separate namespace), plus ai/, ai-analyze/, ai-summary/, and embeddings/ as peers. Grep-newcomer cannot infer auth boundary from folder name alone. A README in app/api/ or renames to api/public-relay/, api/session-ai/, api/v1/ (already good) would clarify the map.

app/api/

Fix effort: Add app/api/README.md auth map; optional renames in a breaking-change pass.

513

Build page (~2,615 lines) keeps all builder modes in one client tree

Monolith Files & Bundle Weight

lowOpenModerate effortHigh blast radius

app/build/page.tsx hosts describe mode, file-upload mode, ToolAPI preview, bridge status, and chat side-panel in a single component. Monaco and DiffViewer are correctly dynamic() (~build imports), but the surrounding state machine, localStorage preview keys, and generate-from-file profiler call (~631) still live in one 2,615-line module. Mode-based dynamic() for file vs describe flows would trim unused paths per visit.

app/build/page.tsx

Fix risk: Large single-file page — extracting or refactoring risks breaking embedded business logic.

Fix effort: dynamic() for describe vs file-upload mode components — Monaco already split; ~half-day with build-flow smoke test.

514

Controls Appearance settings do not affect public kiosks

Window, Sales & Kiosk Scheduling

lowOpenModerate effort

AppearanceSection writes company_settings for dashboard branding (app/controls/sections/AppearanceSection.tsx). /window, /sales, /noe and /adrian use hardcoded themes plus localStorage keys (window-theme, sales-theme). Admins editing title/colors in Controls see no change on will-call kiosk pages.

app/controls/sections/AppearanceSection.tsx

Fix effort: Read company_settings theme on kiosk pages or document that Appearance is dashboard-only.

515

Dashboard Developer tab is a launcher, not embedded settings

UI, Flow & Density

lowOpenModerate effort

Non-Greco admins get a Developer tab that only shows two link cards (Projects, API Keys & Webhooks) pointing at /projects and /developer (~2132–2160). Clicking the tab never shows keys/webhooks inline — extra hop vs Build hub pattern and vs users who expect tab content to load in place.

Fix effort: Either embed developer settings inline (like Reports tab) or rename tab to 'Developer Hub' so launcher pattern is explicit.

516

Heavy libraries bundled statically on tool pages

Performance & Scale

lowOpenModerate effort

grep 'import.*exceljs' finds static imports in app/drivers/lib/, app/cyclecount/lib/, app/transfers/lib/ engine modules — full ExcelJS in initial JS. recharts imported at top of ~10 Greco tool pages (forklift, picker, shorts, etc.). app/components/AgentGenerativeUI.tsx (~13–23) static-imports 8 @nivo/* chart packages. Contrast: EntreeClient uses await import('exceljs') at call sites. Dock tablets pay full bundle before first upload.

app/drivers/lib/app/cyclecount/lib/app/transfers/lib/app/components/AgentGenerativeUI.tsx

Fix effort: Dynamic import() at export/render sites in Drivers, CycleCount, Transfers — per-file change, test each tool.

517

Homepage is English-only despite site-wide i18n

UI, Colors & Aesthetics

lowOpenModerate effort

Root layout wraps the app in LanguageProvider and many pages (Controls, Quote, Dashboard) use useLanguage() / t(en, es). app/page.tsx hardcodes all marketing copy in English — Spanish-preference users get no translation on the landing page.

app/page.tsx

Fix effort: Wrap app/page.tsx copy in t() calls — large string count but no logic change.

518

lib/supabase/types.ts schema types are never imported by app code

Dead Code Hunt (Pass 22)

lowOpenModerate effort

lib/supabase/types.ts (~231 lines) exports Company, Profile, and related interfaces. Referenced only in MULTI_TENANCY_SETUP.md — grep from '@/lib/supabase/types' in app/ and lib/ finds zero imports. Application code uses inline types or per-module interfaces instead; schema doc types drift from live tables without compile-time enforcement.

lib/supabase/types.tslib/supabase/types

Fix effort: Generate types from Supabase CLI and import in lib/* or delete stale hand-written types file.

519

Orphan pages with no inbound links

Dead Code & Orphans

lowOpenModerate effort

/scan (Excel auto-detector), /picture, /our-story, /produce and /problems itself are linked from nowhere. /produce is fully superseded by the Produce panel inside Shanetrée (its header even says so) — ~800 duplicated lines. /flyer, /quotes-admin and /confirmation are also unlinked but plausibly intentional (print QR collateral, admin-only direct URL, auth redirect target). Decide per page: link it, or delete it.

Fix effort: Product decision per page (link, delete, or keep intentional) — /scan, /picture, /produce, etc.

520

Picker page (~2,215 lines) is a single-file upload + filter + chart pipeline

Monolith Files & Bundle Weight

lowOpenModerate effortHigh blast radius

app/picker/page.tsx combines xlsx upload, column mapping, per-employee filters, recharts analytics, savePickerReport, getAllPickerReports (uncapped — tracked separately), and notification hooks in one 'use client' page with no sub-route splitting. Any picker UX tweak touches the whole file; initial load includes recharts despite charts appearing only after upload.

app/picker/page.tsx

Fix risk: Picker uploads feed leaderboard, anomalies, and notifications — refactor can break save → notify → stats chain.

Fix effort: Split upload parser, filter table, and PickerCharts into separate files with dynamic chart import — half-day refactor.

521

Shorts↔PICK comparison joins on item only — slot-level WMS↔ERP signal is lost

Enterprise WMS ↔ ERP Integration (BFC · Entrée · Greco)

lowOpenModerate effort

app/shorts/page.tsx (~594–597) documents matching on trimmed uppercase Item code; Slot is intentionally excluded so cross-location shorts surface as signal. That is useful analytically but means BFC Shorts Slot never joins Entrée arslot or Dakota pickSlots[] in the comparison workbook. Enterprise merge may want both views: item-level KPI (current) plus optional slot-level reconciliation when the same SKU moved bins overnight.

app/shorts/page.tsx

Fix effort: Add optional slot-level comparison sheet/tab in Shorts workbook — extend key to (item, slot) when operator toggles 'strict slot match'.

522

Team-chat and Documents pages skip site-wide i18n

UI, Flow & Density

lowOpenModerate effort

Root layout provides LanguageProvider and Controls/Quote/Dashboard use useLanguage() / t(en, es). app/team-chat/page.tsx and app/documents/page.tsx have zero useLanguage imports — all copy is hardcoded English (room names, create-doc modal, sharing labels). Spanish-preference Greco operators hit fully English collaboration surfaces after switching language elsewhere.

app/team-chat/page.tsxapp/documents/page.tsx

Fix effort: Wire useLanguage/t() through team-chat and documents — large string surface but mechanical.

523

The same anomaly API speaks two dialects

API Consistency

lowOpenModerate effortMay affect neighbors

Internal POST app/api/anomalies/route.ts (~121) expects body { anomalyId, action, notes }. v1 PATCH app/api/v1/anomalies/route.ts (~72) expects { anomaly_id, action, notes } — snake_case ID. GET shapes also differ: internal returns { success, anomalies, count, stats }; v1 returns { data, count, stats, filters }. Both delegate to lib/anomaly/service.ts. Shared clients must branch on route; converging field names before a third consumer would reduce integration bugs.

app/api/anomalies/route.tsapp/api/v1/anomalies/route.tslib/anomaly/service.ts

Fix risk: Converging internal vs v1 field names breaks existing dashboard fetches and API consumers expecting snake_case.

Fix effort: Converge internal + v1 response shapes — breaking change for any consumer of /api/anomalies.

524

Tool icons stored as raw SVG path strings

Maintainability (UI-relevant)

lowOpenModerate effort

lib/greco/greco-tools.ts GRECO_TOOL interface (line 35) stores icon as a raw SVG path d string per tool (~55–83). Dashboard renders path strings in the tool grid with no shared Icon component. Duplicate d strings propagate (Forklift/Thruput lightning bolt, Returns/CycleCount bar chart) because there is no named icon registry — editing one tool means hunting opaque path strings across 29 GRECO_TOOLS entries.

lib/greco/greco-tools.ts

Fix effort: Named icon registry component — nice-to-have refactor of greco-tools.ts.

525

Two tools, two XLSX header detectors

Database & Migrations

lowOpenModerate effortMay affect neighbors

lib/tools/processors/forklift.ts findHeaderRow() (~43) scans sheets for Day/Night Task Summary column aliases. lib/supabase/meat-counts.ts defines a separate findHeaderRow() (~179) with its own HeaderHit type and column-alias table for meat-count uploads. Zero shared import — header-tolerance fixes in forklift never reach meat-counts. Same drift class as lib/lowslots/pick-list-xlsx.ts 'MUST stay in lockstep' warnings.

lib/tools/processors/forklift.tslib/supabase/meat-counts.tslib/lowslots/pick-list-xlsx.ts

Fix risk: Shared findHeaderRow risks breaking meat-count column aliases that differ from forklift — lockstep warnings exist for a reason.

Fix effort: Extract shared xlsx header parser used by forklift.ts and meat-counts.ts.

526

Custom-tools API routes use untyped any Supabase singletons

Code Quality & Hygiene

lowOpenHard / risky

Eight+ routes (custom-tools/*, dashboard/custom-cards, admin/godview) use let _supabaseAdmin: any lazy-init. godview alone has 12+ any usages in handlers. Refactors to typed clients won't catch regressions in these paths.

Fix effort: Replace _supabaseAdmin: any with typed createClient across 8+ routes — wide but mechanical.

527

GodView.tsx (~3,566 lines) is lazy-loaded but still a monolith chunk

Monolith Files & Bundle Weight

lowOpenHard / riskyHigh blast radius

dashboard/page.tsx dynamic-imports GodView (~63) so founders-only admin tabs avoid the chunk on first paint — good. But GodView itself is 3,566 lines with Companies, Agents, Jobs, and Metrics sub-tabs all in one file (N+1 query patterns tracked separately). First GodView open still downloads a multi-thousand-line admin surface; further splitting per tab would improve time-to-interactive for controls work.

Fix risk: Large single-file page — extracting or refactoring risks breaking embedded business logic.

Fix effort: Split Companies/Agents/Jobs/Metrics into dynamic sub-tabs inside GodView — founders-only surface but N+1 fixes pair well with this.

528

Route-level loader can't know the destination theme

Loading & Transitions

lowOpenHard / risky

Next's route-level loading screen (app/loading.tsx) renders before the target page's code loads, so it can't adapt to dark destinations — there is still a brief light frame when hopping between dark tools on slow connections. Unavoidable without a route→theme map at the layout level; low impact since chunks are cached after first visit.

app/loading.tsx

Fix effort: Route→theme map at layout level for app/loading.tsx — limited Next.js flexibility.

529

Shanetrée's internal panel spinners left as-is

Loading & Transitions

lowOpenHard / risky

Shanetrée's entry loading screen uses the unified fancy loader (via the shared access guard), but the small spinners inside its modals/panels were deliberately not touched — its 41k-line client is business-critical and every edit there carries risk. If desired, those can be migrated one panel at a time in a dedicated, carefully-tested pass.

Fix effort: Panel-by-panel FancyLoader migration inside EntreeClient — business-critical, avoid big-bang.

Ranked backlog merges unique open/partial items from New + Audit tabs · sorted severity then fix effort · triage in fix-metadata.ts · fix-risk.ts flags blast-radius (Pass 25)