Commit graph

1 commit

Author SHA1 Message Date
ScreenTinker 8439f2bf18 fix(landing): replace broken Custom pricing card with enterprise contact form
The "Custom" tier on the public pricing page was misrendering as a
better-than-Free tier: headline "Custom", price "Free", "Unlimited
devices/storage", "Get Started" button. Root cause is in DB data,
not markup - the 'enterprise' plan row has price_monthly=0 and
max_devices/storage=-1, and the dynamic render in landing.html maps
those to "Free" + "Unlimited" with the wrong CTA.

Fix: filter the 'enterprise' plan out of the public landing render
(client-side, in landing.html only) and replace it with a hardcoded
Enterprise / Custom marketing card whose Contact Us button opens a
new lead-capture modal.

The DB row itself stays - it is actively used elsewhere:
- auth.js: first user in SELF_HOSTED=true mode is assigned to it
- settings.js: white-label feature is gated on enterprise plan
- 1 user (the dev account) is currently assigned to it
- /api/subscription/plans is also consumed by billing.js, settings.js,
  admin.js (logged-in surfaces); they keep getting the full plan list.
The filter is scoped to landing.html's render only.

The in-app billing page renders the same plan with the same cosmetic
bug; that's a logged-in admin surface, out of scope for this commit.

Other 4 cards (Free, Starter, Pro, Business) unchanged.

Frontend (landing.html):
- Filter 'enterprise' from public render
- Hardcoded Enterprise / Custom card. Uses .price class with "Let's
  talk" + empty .yearly spacer to match Free card's vertical baseline
  so the feature list aligns with the paid cards' baselines.
- Modal markup, CSS (mirrored from frontend/css/main.css conventions
  since landing.html doesn't import main.css), and inline JS for
  open/close/submit/escape/background-click.
- Honeypot field: hidden 'fax_number' input (off-screen + aria-hidden
  + tabindex=-1). Picked over the obvious 'website' name to catch
  mid-tier bots that explicitly skip the well-known honeypot names.

Backend (new server/routes/contact.js):
- POST /api/contact/enterprise, public (unauthenticated)
- Rate limited 5/min/IP+path via the existing rateLimit middleware
- Honeypot check: populated fax_number returns 200 silently, no email
- Server-side validation: required fields, email format, screens
  1-100000, multi_tenant in {single,multi}, hosting in {hosted,self,
  unsure}. Length caps prevent textarea-bomb abuse.
- Sends via existing services/email.js (Microsoft Graph) to
  dan@bytetinker.net from the support@screentinker.com Graph sender.
- Log lines: "[contact] enterprise inquiry from EMAIL (COMPANY)
  delivered" or "[contact] honeypot triggered from IP; dropping".

Wired in server.js alongside other public routes (before requireAuth).

Build-time tests passed locally:
- Module loads, server boots clean
- Validation: missing fields, bad email, bad multi_tenant, bad
  hosting, screens out of range - all return 400 with the right
  error message
- Honeypot: populated fax_number returns 200 success, no email sent,
  log line confirms drop
- Rate limit: kicks in at 6th request within a minute as expected
- Real end-to-end send: one test submission delivered to
  dan@bytetinker.net via Graph (subject "[ScreenTinker] Enterprise
  inquiry: ScreenTinker Build Verification", body formatted with all
  fields). GRAPH_DEV_RESTRICT_TO was temporarily widened to include
  the recipient for the test and restored to dw5304@gmail.com
  immediately after.
- Card render order verified against live API: Free (outline,
  Get Started) | Starter | Pro (featured, Most Popular badge) |
  Business | Enterprise / Custom (Contact Us -> modal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:52:24 -05:00