Commit graph

99 commits

Author SHA1 Message Date
Randall Stillwell
3864219f0a Backfill: prefer fieldData over top-level + auto-load .env
While running the backfill against production we found that many
cards had user-corrected values trapped in fieldData with stale
OCR originals in the top-level column (e.g. LaGay: top-level
email "lagayferters@yahoo.com" vs fieldData "lagayfenters@yahoo.com").

The previous "only promote when top-level is empty" rule skipped
these — so the list view, search, and CSV export still showed the
stale OCR data even though the detail view showed the correction.

New rule:
  - When fieldData[canonical] is non-empty, promote it to the
    top-level column (regardless of whether the top-level column
    already has a value). Reasoning: pre-fix, the UI saved
    non-core edits only to fieldData, so any non-empty
    fieldData[canonical] is the user's most recent value (or
    matches the OCR original — harmless either way). Verified
    against the prod DB that fieldData never contains
    firstName/lastName/name, so there is no risk of reverting
    user-edited names.

Also:
  - Adds `import "dotenv/config"` so `npx tsx` picks up .env.
  - Logs fill-empty vs overwrite counts and a sample of conflicts
    (top-level → fieldData) so the dry-run is easy to audit.

Applied against prod: 23 form fields flipped, ~3127 values
promoted across 901 cards, 282 names recomputed. Re-run dry-run
reports 0 remaining changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:09:08 -05:00
Randall Stillwell
9c1aaaa61f Bootstrap agent-context pipeline (L1 + L2 + L3)
Installs a 3-layer Cursor-aligned agent pipeline so future agent
sessions can orient quickly and stay inside guard rails:

L1 — context for any agent reading the repo:
  - AGENTS.md (top-level orientation, conventions, no-go zones)
  - .cursor/rules/ (no-go-zones, api-routes, prisma, prisma-schema-map)
  - .cursor/skills/ (add-api-route, add-prisma-model)
  - docs/SCHEMA_MAP.md generated from prisma/schema.prisma
  - scripts/generate-schema-map.ts (regenerate the map; wired up as
    `npm run schema:map`)

L2 — subagent roles for the 9-stage idea-to-feature pipeline:
  - .cursor/agents/role-*.md (conductor, architect, ia-architect,
    design-system-auditor, implementer, reviewer, ux-reviewer,
    a11y-auditor, doc-writer) with explicit multitask annotations.

L3 — pipeline scaffolding:
  - .github/CODEOWNERS, PR template, and CI workflows (ci.yml,
    preview-smoke.yml, visual-diff.yml, pr-health-rollup.yml).
    Test job is intentionally disabled until Playwright is wired up.
  - .convoys/ folder for per-feature run notes + scripts/log-convoy-event.sh.
  - scripts/wt.sh worktree helper.
  - src/lib/flags/index.ts simple env-driven feature flag wrapper.
  - tests/smoke/app.smoke.spec.ts (Playwright smoke; excluded from
    tsc until @playwright/test is installed — see tsconfig change).

Also writes .agent-context-manifest.yml so the sync-agent-context
skill can detect drift and offer selective updates from upstream.

Follow-ups (not in this commit):
  - Install @playwright/test and re-enable the test job in ci.yml.
  - Review .cursor/agents/role-*.md and trim any roles that don't
    apply to this codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:55:18 -05:00
Randall Stillwell
7aedae02fc Keep card name and contact fields in sync after edits
Two bugs caused the cards list and integrations to show stale data
after a user edited a card in the detail view:

1. ResponseCard.name is a denormalized display string set only at
   OCR / survey-submit time. Editing firstName or lastName never
   recomputed it, so the table header and Name column kept the old
   value. PUT /api/cards/[id] now recomputes name from first + last
   whenever either changes (unless the caller passed an explicit
   name). The detail page header reads from in-flight edits so the
   title updates live as the user types.

2. The default form template marked only firstName/lastName as
   isCore. Every other field (email, cellPhone, address, etc.) was
   non-core, so dynamic-field edits landed in ResponseCard.fieldData
   JSON and never touched the top-level columns the list view,
   search, CSV export, and integrations read from. The PUT route
   now promotes any fieldData keys that match canonical columns up
   to those columns; the default template marks all canonical
   fields as isCore so new orgs avoid the problem in the first
   place.

Adds scripts/backfill-core-fields.ts (dry-run by default; pass
--apply to commit) to flip existing FormField rows to isCore = true
where the key matches a canonical column and to promote any
existing fieldData values into empty top-level columns + recompute
stale name values.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 08:30:27 -05:00
Randall Stillwell
532a818995 Fix RSC build error: pass rendered icon element to DocArticle
DocArticle is a client component, so passing a Lucide icon *component
type* (a function) from the server-rendered doc pages tripped the RSC
"Functions cannot be passed directly to Client Components" check during
prerender of /docs/*. Switch the `icon` prop to React.ReactNode and pass
already-rendered <Icon /> elements from each doc page.

Made-with: Cursor
2026-04-24 15:17:55 -05:00
Randall Stillwell
3a75bfa7de Add in-app Contact Support dialog and Help Center docs
Route all support channels to support@stillwell.cloud for Libredesk
email ingest, add a global Contact Support dialog (category/subject/
message) that posts to POST /api/support with per-user rate limiting,
and build a /docs Help Center hub with Getting Started, Uploading
Cards, Forms & Templates, Reports & Exports, FAQ, and Troubleshooting
sections. Replaces stale echoocr.com/echoocr.app addresses across
marketing footer, terms, privacy, pricing, welcome, and features.

Made-with: Cursor
2026-04-24 15:10:56 -05:00
Randall Stillwell
be7e3dc502 Normalize role-based permissions across API and UI
Centralizes role/permission enforcement so each role (owner, admin, editor,
reviewer, viewer) behaves consistently in the API and UI.

- Extend src/lib/permissions.ts with an expanded action map (cards.reprocess,
  cards.assign, uploads.create, integrations.manage, etc.) plus helper
  predicates (isAdminRole, canEditContent).
- Add requireApiAuthWithPermission(action) to src/lib/api-auth.ts with a
  narrowed OrgSession return type and PermissionError -> 403 handling.
- Replace hand-rolled role checks in card, org, integration, form-template,
  settings, upload, and location routes with the shared helpers so 403s are
  uniform and derived from one permission map.
- Close the editor UI gap: the dashboard upload button, row-level mark
  reviewed/reprocess/delete, and card detail edit/reprocess/export/assign
  now flow from can(role, action) instead of ad-hoc isAdmin checks.
- Gate /settings/* at the middleware layer for non-admins and hide the
  Settings entry in the sidebar and top-bar menu when the role cannot
  access it.
- Use isAdminRole() in the team members settings page for consistency.

Made-with: Cursor
2026-04-23 12:17:35 -05:00
Randall Stillwell
11773a2ad2 Self-heal stale email verification banner
The banner was wired off session.user.isEmailVerified, which lives on
the JWT cookie. If a user verified their email in another tab or was
auto-verified by the invite flow after an existing session was issued,
the token kept saying unverified until sign-out, so the banner hung
around indefinitely.

- Add GET /api/auth/verify-email/status, a tiny authenticated endpoint
  that returns the authoritative emailVerified state straight from the
  DB.
- Banner now fetches that on mount and only renders if the DB agrees
  the user is unverified. If the DB says verified but the cookie is
  stale, we call useSession().update() to refresh the JWT so the rest
  of the app sees the correct state on the next render.

Made-with: Cursor
2026-04-19 10:53:36 -05:00
Randall Stillwell
f2c7d13a3e Unify invite landing with inline signup/login flow
Rewrite /invite/[token] so invited users can create an account and
join a workspace in a single page instead of bouncing to /signup.
The page branches on verify response + session state:

- New user: inline signup form (name + password, email locked), then
  auto signs in and redirects to the dashboard.
- Existing user, signed out: password-only login form that signs in
  and auto-calls /api/invitations/accept.
- Existing user, signed in with matching email: single Accept button.
- Existing user, signed in with mismatched email: amber notice plus
  one-click "Sign out and continue" that returns to the same invite
  URL logged out.

Also:
- Expired / already-accepted / not-found states now show the inviter's
  name and org context so users know who to contact for a new link.
- Extract the password + confirm-password + strength meter into a
  reusable <PasswordFields> component consumed by both the signup and
  invite pages so the UI stays in lockstep. The component supports
  hiding the confirm field and overriding autoComplete for login use.

Made-with: Cursor
2026-04-19 10:45:51 -05:00
Randall Stillwell
47da94fc95 Enrich invitation verify API and auto-verify invited signups
- /api/invitations/verify now returns inviterName and a structured
  failure reason ("not_found" | "expired" | "accepted") plus orgName,
  so the invite page can show contextual error messages and know who
  to blame for an expired link.
- /api/auth/register marks emailVerified immediately and skips the
  verification email when an inviteToken is present, since clicking
  the tokenized invite link already proves email ownership.

Made-with: Cursor
2026-04-19 10:45:41 -05:00
Randall Stillwell
4431778c0a Replace hardcoded echoocr.com fallbacks with resolved site URL
Introduces src/lib/site-url.ts with getSiteUrl(), getSiteUrlFromRequest(),
and getSiteHostname() helpers that cascade through NEXT_PUBLIC_SITE_URL,
AUTH_URL, VERCEL_URL, and localhost so no code path ever falls back to a
third-party domain we may not own.

- forgot-password route now derives the base URL from the incoming request
  origin so reset links always match the host the user hit
- email-sender, layout metadata, email preview, and QR code routes use the
  new helper; email footers display the derived hostname instead of a
  hardcoded brand string
- .env.example clarifies the real expected values for AUTH_URL and
  NEXT_PUBLIC_SITE_URL per environment

Made-with: Cursor
2026-04-19 10:12:44 -05:00
Randall Stillwell
8ae013879b Fix production build failure: lazy Prisma client initialization
The Prisma client was constructed eagerly at module load, which called
new URL(process.env.DATABASE_URL!) during Next.js 16 "Collect page data".
When DATABASE_URL wasn't in the build environment (production), this threw
TypeError: Invalid URL { input: 'undefined' } and failed the build for
routes like /api/auth/forgot-password.

Use a Proxy to defer client construction until the first property access,
so build-time module evaluation no longer touches DATABASE_URL.

Made-with: Cursor
2026-04-17 16:15:41 -05:00
Randall Stillwell
3e0a4458fe Add unified field mapping UI for integrations
Adds a reusable FieldMappingEditor component to the integration
detail page that works across all providers (Monday.com, Google
Sheets, Airtable, Planning Center). Users can visually map card
fields (core + dynamic FormTemplate fields) to external columns.

- New GET /api/integrations/source-fields returns curated core
  card fields plus the org's default FormTemplate dynamic fields
  (prefixed with fieldData.)
- flattenCardFieldData helper hoists fieldData entries to top-
  level keys before pushCard so dynamic fields are mappable
- Monday provider now upserts: re-syncing a card updates the
  existing item instead of creating duplicates, and the
  mondayItemId is persisted back to the ResponseCard

Made-with: Cursor
2026-04-17 15:53:49 -05:00
Randall Stillwell
a83c41694f Add change password to profile, persist all profile fields to DB
- Add jobTitle, company, bio columns to User model (previously localStorage only)
- Replace legacy Authentik GET /api/auth/me with session-based profile endpoint
- Update PUT /api/auth/me to persist all profile fields to DB
- Add PUT /api/auth/change-password endpoint with current password verification
- Add Security card to profile page with change password form
- Update user-profile provider to fetch from API instead of localStorage

Made-with: Cursor
2026-04-17 15:35:24 -05:00
Randall Stillwell
028840a887 Add SFTP support: replace FTP-only TLS toggle with protocol selector
Dreamhost (and most modern hosts) use SFTP (port 22), not FTP (port 21).
The previous implementation only supported FTP/FTPS via basic-ftp, causing
timeouts when connecting to SFTP servers.

Changes:
- Add ssh2-sftp-client for SFTP connections
- Replace ftpTls boolean with ftpProtocol ("sftp" | "ftp" | "ftps") in schema
- Rewrite ftp-watcher.ts to support both SFTP and FTP/FTPS protocols
- Update UI with protocol dropdown that auto-switches the default port
- Add ssh2/ssh2-sftp-client to serverExternalPackages in next.config
- Default to SFTP on port 22

Made-with: Cursor
2026-04-17 15:12:54 -05:00
Randall Stillwell
bc5ad6dbdb Fix FTP connection timeout: add 15s timeout and accept self-signed certs
The FTP client had no timeout, causing Vercel function timeouts on
connection issues. Added a 15-second timeout to both pollFtp and
testFtpConnection. Also added secureOptions.rejectUnauthorized=false
to prevent TLS failures with self-signed or non-standard certificates.

Made-with: Cursor
2026-04-17 14:46:16 -05:00
Randall Stillwell
58d8d99625 Fix masked passwords being sent to test endpoints and saved to DB
The settings GET endpoint masks emailImapPass and ftpPass for security.
The test endpoints and save handler were using these masked values, causing
IMAP/FTP test failures and potentially overwriting real passwords. Now the
test endpoints fall back to the DB-stored password when they receive the
masked value, and the save handler strips masked passwords from the payload.

Made-with: Cursor
2026-04-17 14:37:55 -05:00
Randall Stillwell
d0844ecadd Add team member management: role changes, member removal, invitation resend/revoke
- Create PATCH/DELETE /api/org/members/[id] for role updates and member removal
  with owner/admin guards, self-action prevention, and activeOrgId cleanup
- Create POST/DELETE /api/org/invitations/[id] for resending and revoking invites
  with automatic expiry extension on resend
- Filter accepted invitations from GET /api/org/invitations response
- Overhaul Team Members UI with inline role dropdowns, remove buttons,
  resend/revoke controls, expired invitation indicators, and admin-only guards

Made-with: Cursor
2026-04-17 12:48:59 -05:00
Randall Stillwell
3e5449cf4f Fix migration test issues: upload button, forgot password, IMAP, FTP UI, surveys nav, integration tests
- Mount UploadModal globally in AppShell so upload works from any page
- Add /forgot-password, /reset-password, /s, /api/survey/submit to public paths in middleware
- Fix IMAP test URL (/api/email/test -> /api/email-watch/test) and body shape
- Fix IMAP scan URL to use /api/email-watch with action body
- Add FTP server settings section to Upload Sources page with test connection
- Add "Surveys" nav item in sidebar with dedicated page showing links, QR codes
- Include org slug in form-templates API response for survey URL construction
- Add pre-creation "Test Connection" button on new integration page
- Create /api/integrations/test endpoint for pre-creation connection testing
- Replace overflow-hidden with overflow-clip on Card to fix click/z-index issues

Made-with: Cursor
2026-04-17 12:17:11 -05:00
Randall Stillwell
70134b4967 Polish login and signup UX: fix stale copy, add password toggles, strength meter, invite context, terms footer
Made-with: Cursor
2026-04-17 09:56:06 -05:00
Randall Stillwell
2664f78b00 Migrate Echo Life Church data, add invite acceptance for existing users
Data migration:
- Associate all 978 cards with Echo Life Church organization
- Split name field into firstName/lastName on all cards
- Seed default Connect Card form template with 25 fields
- Migrate legacy column data into fieldData JSON
- Create Echo Life Church location with 3 Sunday services (8:00, 9:30, 11:00)
- Add all users as members of Echo Life Church
- Populate 819 people from card data with dedup matching

Invitation flow fix:
- Add POST /api/invitations/accept for logged-in users to join orgs
- Update /api/invitations/verify to return org name and existing account flag
- Rewrite invite page to handle 3 scenarios:
  1. Logged in + email matches: one-click "Accept Invitation" button
  2. Logged in + email mismatch: prompt to switch accounts
  3. Not logged in + has account: "Sign In to Accept" button
  4. Not logged in + new user: "Accept & Create Account" (existing flow)

Made-with: Cursor
2026-04-17 08:15:31 -05:00
Randall Stillwell
f53b08f99f Add dynamic fields, people directory, analytics, security hardening, and UX polish
Phase 1 - Security & Bug Fixes:
- Add requireApiAuth helper and protect all 25 unprotected API routes
- Add org-tenant scoping to all card, job, stats, and notification queries
- Fix SSRF in ai-test, mask secrets in settings API, fix middleware bypass
- Fix cards pagination routing, stat filter sync, drag-drop file passing
- Add PUT /api/auth/me for profile persistence, stuck job recovery
- Fix email watcher MIME type detection

Phase 2 - Dynamic Fields & Digital Survey:
- Add FormTemplate, FormField, Person, PasswordResetToken models to schema
- Add fieldData, formTemplateId, firstName, lastName, personId to ResponseCard
- Build FormTemplate CRUD API with field management and org scoping
- Build Form Builder UI with field ordering, type config, and section management
- Refactor card detail page to render fields dynamically from templates
- Add dynamic OCR prompt/schema generation from template fields
- Build public survey page at /s/[orgSlug]/[formSlug] with branding
- Add QR code generation API and share section component

Phase 3 - People & Analytics:
- Build People CRUD API with merge and batch auto-link endpoints
- Build People list and detail pages with search, merge dialog
- Add auto-link logic in OCR completion to match/create Person records
- Add /api/stats/trends endpoint with time series and team activity
- Build Reports page with Recharts (area charts, bar charts, pipeline)
- Upgrade dashboard with sparklines and People stat card

Phase 4 - UX Polish:
- Replace silent error handling with toast notifications across all pages
- Add loading skeletons, differentiated empty states
- Add ARIA labels, skip-to-content link, accessible column toggle
- Add forgot password flow, Cmd+K command palette, Collection Days pages
- Unify Echo branding and theme toggle consistency

Made-with: Cursor
2026-04-16 23:29:26 -05:00
Randall Stillwell
1bceeb0418 Move onboarding to auth layout for consistent setup experience
The onboarding wizard was wrapped in the dashboard layout (sidebar +
topbar), which felt jarring when coming from the clean workspace-setup
page. Moved it to the (auth) layout so the entire setup flow stays in
the same centered, chrome-free experience. Widened the auth layout to
max-w-3xl and added max-w-md to each narrow auth page individually.

Made-with: Cursor
2026-04-16 21:43:15 -05:00
Randall Stillwell
150ec3524d Update governing law to Florida (Escambia County)
Made-with: Cursor
2026-04-16 20:54:42 -05:00
Randall Stillwell
2c765e05c4 Add privacy policy and terms of service pages
- Create comprehensive privacy policy covering all data categories:
  account info, org data, scanned card PII (20+ field types),
  AI processing (OpenAI via Vercel AI Gateway), subprocessors
  (Supabase, Vercel, Upstash, Brevo), configurable retention,
  data controller/processor roles, and individual rights
- Create terms of service covering eligibility, accounts, acceptable
  use, data responsibilities, subscriptions/billing, free tier,
  IP, third-party integrations, availability, liability limitation,
  indemnification, termination, and governing law
- Add /privacy and /terms to middleware public paths
- Update footer links from placeholder # hrefs to actual pages
- Add metadata layouts for both pages

Made-with: Cursor
2026-04-16 20:53:25 -05:00
Randall Stillwell
7ab2fe6894 Redesign transactional email templates to match Echo brand
- Replace bare inline HTML with a shared emailShell template system
- Use brand colors (warm neutrals), Quicksand font, and rounded card layout
- Add hidden preheader text for better inbox previews
- Proper table-based layout for email client compatibility (Outlook, Gmail, Apple Mail)
- Include Mso conditionals for Outlook button rendering
- Add dev-only email preview route at /api/dev/email-preview (gated behind NODE_ENV)
- Fix invitation email copy bug ("has invited you" / "You've been invited")

Made-with: Cursor
2026-04-16 20:46:05 -05:00
Randall Stillwell
c9f5b1e2ee Add OG/Twitter sharing metadata and screenshot for all marketing pages
- Update root layout with full Open Graph, Twitter Card, and SEO metadata
- Add OG image screenshot (1200x630) of the landing page hero
- Create layout.tsx with page-specific metadata for each marketing page
  (welcome, features index, 8 feature detail pages, pricing)
- Use Next.js title template ("%s | Echo") for consistent page titles
- Add NEXT_PUBLIC_SITE_URL to .env.example for metadataBase config

Made-with: Cursor
2026-04-16 20:34:26 -05:00
Randall Stillwell
7c4ec7bb9d Refresh JWT before hard navigation on onboarding completion
After completing onboarding, the JWT cookie still has stale org data.
Fetching /api/auth/session triggers the JWT callback to query the DB
and write the updated cookie before the hard navigate to /.

Made-with: Cursor
2026-04-16 19:12:21 -05:00
Randall Stillwell
4db78b7176 Fix workspace-setup redirect loop after org creation
The JWT callback was optimized to only query the DB on sign-in or
explicit session update, but the middleware reads the JWT cookie which
was stale after org changes. Reverted to always refreshing org data in
the JWT callback (matching original behavior). Also switched workspace-
setup and onboarding completion to hard navigation to guarantee the
middleware re-evaluates with the latest JWT cookie.

Made-with: Cursor
2026-04-16 19:10:41 -05:00
Randall Stillwell
bf92126cf5 Add feature pages, pricing page, and redirect unauthenticated users to landing page
- Middleware now redirects unauthenticated users to /welcome instead of /login
- Added /features and /pricing to public paths
- Created 8 dedicated feature pages: AI OCR, Scanner Integration, Integrations,
  Team Collaboration, Collection Days, Multi-Site, Analytics, Security
- Created features index page at /features with links to all detail pages
- Created standalone pricing page at /pricing with comparison table
- Extracted shared MarketingNav (with features dropdown) and MarketingFooter
- Created FeaturePageShell for consistent feature page layout
- Updated landing page to use shared components and link to feature pages

Made-with: Cursor
2026-04-16 19:09:52 -05:00
Randall Stillwell
52714c4e50 Add landing page at /welcome with pricing and feature overview
Marketing page targeted at churches/nonprofits with hero, features,
integrations showcase, 4-tier pricing table (Free/Starter/Growth/Enterprise),
testimonials, FAQ, and CTAs. Added /welcome to public middleware paths.

Made-with: Cursor
2026-04-16 18:59:33 -05:00
Randall Stillwell
ab873f7f08 Add multi-workspace support with org switcher and open registration
- Open registration to all users (remove invite-only gate)
- Add domain auto-join: new users matching org allowedDomains get auto-added
- Track active workspace via activeOrgId on User model
- Refactor JWT callback to resolve active org from all memberships
- Add org switch, list, and create-personal API endpoints
- Add workspace-setup page for users without an org
- Build OrgSwitcher dropdown in sidebar header
- Add allowed email domains management to org settings

Made-with: Cursor
2026-04-16 18:08:08 -05:00
Randall Stillwell
af91f3d5fc Allow unauthenticated access to invite and verification routes
Add /invite and /api/invitations/verify to public paths so invited
users can access the invite page and complete registration without
being redirected to login.

Made-with: Cursor
2026-04-16 17:22:29 -05:00
Randall Stillwell
807d197243 Trigger redeploy with corrected Brevo API key
Made-with: Cursor
2026-04-16 17:10:08 -05:00
Randall Stillwell
bf47fd262c Replace nodemailer with Brevo HTTP API for transactional email
- Swap nodemailer SMTP transport for @getbrevo/brevo SDK, which uses
  HTTP with built-in retries (better for Vercel serverless)
- Add sendInvitationEmail function and wire it into the org invitations
  POST route so new invites trigger an email automatically
- Update .env.example with BREVO_API_KEY, EMAIL_FROM_NAME,
  EMAIL_FROM_ADDRESS replacing the old SMTP_* vars

Made-with: Cursor
2026-04-16 12:51:17 -05:00
Randall Stillwell
42df7fc995 Fix admin role check to include owner role
The org membership role is "owner" but UI components only checked for
"admin", hiding action buttons (reprocess, push, export, etc.) for
org owners. Update isAdmin checks in card detail page and dashboard,
plus the delete API route, to include "owner".

Made-with: Cursor
2026-04-16 00:22:32 -05:00
Randall Stillwell
19270ef6ec Strip sslmode from connection string to fix TLS override
Newer pg versions treat sslmode=require as verify-full, overriding
the programmatic ssl.rejectUnauthorized=false config. Strip sslmode
from the URL and handle SSL entirely via the Pool ssl option.

Made-with: Cursor
2026-04-16 00:08:47 -05:00
Randall Stillwell
fd3e1ba150 Fix TLS certificate error for Supabase pooler connection
Supabase's PgBouncer pooler uses a certificate not in Node's default
CA store, causing "self-signed certificate in certificate chain" errors
on Vercel. Configure pg.Pool with ssl.rejectUnauthorized: false.

Made-with: Cursor
2026-04-16 00:05:00 -05:00
Randall Stillwell
21c863da01 Migrate to Vercel + Supabase + Upstash production stack
- Replace pdf2pic/GraphicsMagick with pdfjs-dist + @napi-rs/canvas for
  Vercel-compatible PDF rasterization
- Replace MinIO with Supabase Storage (S3-compatible); rename minio.ts
  to storage.ts and update all imports
- Replace in-memory job queue with Upstash QStash; upload route now
  persists files to storage before enqueuing, /api/jobs/process handles
  the QStash callback
- Convert email watcher from persistent IMAP connection to stateless
  scanInbox() polled by Vercel Cron every 2 minutes
- Add FTP watcher (basic-ftp) with cron polling for scanner integration
  via Dreamhost FTP drop directory
- Add FTP config fields to AppSettings schema
- Remove folder watcher (chokidar), standalone output, Docker-only code
- Update next.config.ts, middleware, instrumentation for serverless
- Add vercel.json with cron schedules for email and FTP polling
- Add migration scripts for database (pg_dump/restore) and storage
  (S3-to-S3 copy) with verification

Made-with: Cursor
2026-04-15 23:51:44 -05:00
Randall Stillwell
20bc4cbb03 Add missing org API routes and fix registration gate
- Create /api/org (GET details, PUT update)
- Create /api/org/members (GET list)
- Create /api/org/invitations (GET list, POST create)
- Create /api/org/locations (GET list with collection days, POST create)
- Fix registration: check org count instead of user count so
  a fresh domain setup works even if orphaned users exist
  from a prior AUTH_SECRET rotation

Made-with: Cursor
2026-04-15 14:47:15 -05:00
Randall Stillwell
1b669bb1b7 Fix onboarding redirect and integrations empty state
- Middleware now redirects to /onboarding when user has no org
  (previously required orgRole=owner which was undefined for new users)
- Refresh JWT cookie after onboarding completes so middleware sees
  updated orgId/onboardingComplete on next navigation
- Integrations page shows meaningful error instead of empty grid
  when API returns 401 due to missing org

Made-with: Cursor
2026-04-15 14:18:51 -05:00
Randall Stillwell
48959190a0 Fix middleware cookie detection behind HTTPS reverse proxy
getToken() defaults secureCookie to false, so it looks for
"authjs.session-token" cookie. Behind Traefik over HTTPS, Auth.js
sets "__Secure-authjs.session-token". Detect HTTPS via
x-forwarded-proto header and pass secureCookie accordingly.

Made-with: Cursor
2026-04-15 14:11:50 -05:00
Randall Stillwell
04e9811572 Fix Auth.js server error behind reverse proxy
Add trustHost: true to NextAuth config so Auth.js accepts requests
when running behind Traefik/reverse proxy. Without this, Auth.js
rejects the credential callback because the forwarded host doesn't
match its expectations.

Made-with: Cursor
2026-04-15 01:50:44 -05:00
Randall Stillwell
95a672a6cb Fix build: wrap useSearchParams in Suspense boundary
Next.js requires useSearchParams() to be inside a Suspense boundary
for static page generation. Wraps the new integration page component.

Made-with: Cursor
2026-04-15 01:35:54 -05:00
Randall Stillwell
a975043670 Add onboarding wizard, email verification, integration architecture, and settings restructure
- Auto-sign-in after registration instead of redirect to login
- Email verification system with token generation, send/confirm API routes, and persistent banner
- 7-step onboarding wizard (org, location, services, upload source, AI, integrations, complete)
- Middleware redirects owners with incomplete onboarding to /onboarding
- Integration provider plugin architecture with registry and 6 providers (Planning Center, Monday.com, Airtable, Google Sheets, Webhook, CSV Export)
- Full integration CRUD API with test, sync, fields, and OAuth authorize/callback routes
- Refactored fireIntegrationEvent to use Integration model with legacy AppSettings fallback
- Migration script for existing Monday.com/webhook config to Integration rows
- Settings page restructured from monolithic 1290-line file into focused sub-routes with section navigation
- Integration hub UI with provider tiles, connect flow, and individual config pages
- Post-onboarding contextual guidance cards on dashboard with dismissible hints
- Schema: Integration model, onboardingComplete/onboardingStep on Organization, dismissedHints on OrgMember

Made-with: Cursor
2026-04-15 01:29:13 -05:00
Randall Stillwell
d3e7374439 Add SaaS foundation: Auth.js, dashboard shell, org model, auto-assignment
Major architectural upgrade preparing Echo OCR for self-hosted SaaS deployment:

- Auth: Built-in Auth.js v5 with credentials + Authentik OIDC SSO, JWT sessions,
  middleware route protection, login/signup/setup pages, registration API
- UI: Dashboard layout with collapsible sidebar nav, AppShell wrapper, route
  groups for (dashboard) and (auth), new pages for events/people/reports
- Schema: Auth.js tables (Account, Session, VerificationToken), Organization,
  OrgMember, Location, CollectionDay, Invitation, SystemConfig, ApiKey models;
  proper User relations to ResponseCard/ActivityLog/Notification
- Permissions: Role hierarchy (owner/admin/editor/reviewer/viewer) with
  action-based permission map and requirePermission/requireAuth helpers
- Onboarding: Multi-step setup wizard for first-user bootstrap (account, org,
  location) with SystemConfig tracking
- Events: CollectionDay model with rrule support for recurring church services
- Auto-assign: Event-aware card assignment engine replacing getPreviousSunday()
- Migration: seed-migration.ts script for upgrading existing deployments

Made-with: Cursor
2026-04-14 23:59:38 -05:00
Randall Stillwell
0ab9932599 Add multi-user card review workflow with role-based access
- Add User model synced from Authentik headers with admin/reviewer/viewer roles
- Add assignment fields (assignedToId, assignedById, reviewedById, etc.) to ResponseCard
- Add userId tracking to ActivityLog and Notification models
- Create auth.ts with getOrCreateUser() and role mapping from Authentik groups
- Create /api/users endpoint and /api/cards/assign batch assignment endpoint
- Gate card mutations behind role checks (viewers read-only, reviewers edit assigned only)
- Gate Monday.com push behind reviewStatus=reviewed instead of ocr_complete
- Add "My Cards" stat card, Assigned To filter, and assignment columns to table
- Add Assign button with user picker to batch selection toolbar (admin only)
- Update card detail: assignment banner, Mark Complete button, prev/next nav, reassign
- Make all field components accept readOnly prop for role-based editing
- Gate settings page behind admin role
- Add userId to stats API for per-user card counts
- Expose dbUser and role through UserProfileProvider context

Made-with: Cursor
2026-04-11 00:00:11 -05:00
Randall Stillwell
aacf9c776b Add manual Scan Inbox button for email monitoring
- Add scanInbox function that opens a separate IMAP connection and
  processes all unseen messages in the configured folder
- Wire to email-watch API as action "scan"
- Add Scan Inbox button to settings page next to Start/Stop Monitoring
- Useful for processing emails that arrived before monitoring started,
  since IMAP IDLE only notifies about new messages

Made-with: Cursor
2026-04-10 13:04:45 -05:00
Randall Stillwell
b2c29120cd Add First Time Guest Date and Salvation Date fields
- Add firstTimeGuestDate and salvationDate (DateTime?) to Prisma schema
- Auto-compute dates during OCR: find previous Sunday from card createdAt
  when visitType indicates first/second time guest or nextStep includes
  Baptism
- Add editable date inputs on card detail page in Workflow section
- Add to Monday.com mappable fields in settings for column mapping
- Add table columns (hidden by default) for both date fields
- Handle full ISO datetime strings in Monday.com date parser
- Apply same logic during reprocessing

Made-with: Cursor
2026-04-09 17:20:24 -05:00
Randall Stillwell
d6d209cf9d Add Monday.com sync-all with push+update support and batch toolbar button
- Sync-all endpoint now supports mode param: "push" (new only), "update"
  (existing only), or "all" (both); also accepts cardIds for batch ops
- Response includes separate created/updated/failed counts
- Settings button renamed to "Sync All to Monday.com" and shows breakdown
- Add "Monday.com" button to the floating selection toolbar for syncing
  selected cards in batch

Made-with: Cursor
2026-04-09 15:14:48 -05:00
Randall Stillwell
05a40cca51 Export CSV based on currently visible columns instead of hardcoded list
- CSV export now respects column visibility settings, exporting only the
  columns the user has toggled on
- Boolean fields export as "Yes"/"No", arrays as comma-separated values
- Toast message shows row and column counts for confirmation

Made-with: Cursor
2026-04-09 10:35:58 -05:00