Commit Graph
5 Commits
Author SHA1 Message Date
serfaandClaude Opus 5 35d99ce0e2 Containerise for Dokploy, and a demo login that survives production
Everything needed to build and run this on Dokploy at
linkdr.serfaty.site, plus the two things that turned out to be broken
the moment it left a laptop.

The build did not work in a container at all. `lib/auth.ts` throws when
AUTH_SECRET or NEXT_PUBLIC_APP_URL is missing — correct at boot, wrong
during `next build`, which imports every route module with
NODE_ENV=production and none of the runtime secrets. The only way past
it was baking a session key into an image layer, which is worse than
the problem the guard exists to prevent. Both checks now skip
NEXT_PHASE=phase-production-build and still fire on a real boot.

Corepack in node:22.12-alpine ships expired npm registry signing keys
and dies before it can download pnpm, so the image installs corepack
first and prepares the pinned version explicitly.

The image is the standalone trace, which needs outputFileTracingRoot at
the REPO root: pnpm hoists to a root .pnpm store and tracing from
apps/web silently omits every workspace package. 427MB, runs as
non-root, and its healthcheck talks to Postgres — a container that
cannot reach its database must never enter rotation, because a deploy
that goes green and then 500s does not roll back.

DEMO_LOGIN is a login bypass under NODE_ENV=production and there is no
honest way to describe it otherwise. It is a separate variable from
ALLOW_DEV_LOGIN so that copying a dev .env into a real environment
cannot enable it by accident, it still only affects the one seeded
number, and it prints a boot warning every single start so it cannot be
forgotten. That deployment holds nothing but fixtures. It comes out
before the platform sees a real signup.

Also: /api/health, and next/image hosts corrected to the Spaces bucket
rather than the R2 one this stopped using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:17:54 -04:00
serfaandClaude Opus 5 1808ad4cba Move the demo market to Mexico City, priced in US dollars
The showcase was a Barcelona market: Catalan names, +34 numbers, euro
rates and "Carrer Example 12" on every job. Presented to a Mexican
client, all of that reads as somebody else's product.

City comes from NEXT_PUBLIC_CITY_* as before, now Ciudad de México at
19.4326/-99.1332, with MAPBOX_COUNTRY=mx. The seed's fallbacks were
Barcelona literals, so an unset env quietly seeded a different city
than the app rendered — they now agree.

Two db tests pinned the Barcelona centre as a hardcoded constant, which
is why the deck returned zero cards on the first run here: every pro was
a continent outside the radius. They read the same env as the seed now,
so the trap cannot recur.

Money: formatCents defaults to USD/en-US, and the nine hardcoded euro
signs across the card, search rows, quote strip and forms are dollars.
The rate NUMBERS are unchanged and still read high for CDMX — that is a
pricing decision, not a currency one, and is left alone deliberately.

Seed people are Mexican, addressed on real Roma/Condesa streets rotated
by index rather than one placeholder repeated. Phones moved to +52 55,
which moves the demo login to +525500000000 / 000000.

Also in here, from the same session:
- Sending a job now confirms. The mutation always succeeded; the sheet
  just closed with no receipt, which from the customer's side is
  indistinguishable from a dead button. Dismissing that receipt resolves
  as 'sent', so the card does not return to the deck.
- Media moves to DigitalOcean Spaces, with the public origin derived
  from bucket and region instead of a second env var to keep in sync.
- Managed-Postgres TLS: DATABASE_CA_CERT takes a path or inline PEM.
- The client-facing project panel beside the running app.
- Two profiles removed and four renamed to match their photos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:56:31 -04:00
serfa 5086a238ea Revert "Demo page: the running product beside the case for it"
This reverts commit 7567aa4e75.
2026-08-21 07:32:41 -04:00
serfaandClaude Opus 5 7567aa4e75 Demo page: the running product beside the case for it
A page to present this to a client. Left column is the real app in the phone
frame — live, swipeable, the same deck and the same data, not a screenshot.
Right column is what somebody needs in order to judge it: how to get in, what it
does, what it is built on.

Credentials, both sides
- A one-sided demo of a two-sided marketplace shows half a product, so the panel
  carries a customer AND a tradesperson, each with a copy button. Reading a
  phone number off a screen into a form while a client watches is a small
  humiliation; mistyping one is a worse one.
- dev-login grows from one pinned number to a named DEMO_ACCOUNTS list. The
  three guards are unchanged — not production, explicitly enabled, and the
  number must be on the list. Both accounts map to seeded users that already own
  jobs, conversations and a completed booking, so the screens have something in
  them rather than five empty states.
- The page SAYS SO when sign-in is unavailable rather than letting somebody
  discover it mid-meeting. A fixed passcode is a login bypass and must never
  ship live; the honest fix for demoing against production is a real account and
  a real SMS, not a fourth flag.

Content lives in arrays at the top of demo-panel, so adding a feature or
swapping a dependency is one line and the layout is untouched. Four labelled
placeholder slots — roadmap, pricing, metrics, case study — reserve the space
and make it obvious what belongs where.

A deliberate exception to DESIGN.md §1.0 and §4, which ban desktop layouts and
marketing pages, and it is noted as one in the file. This is a frame around the
running app for a laptop, not a product screen; the app inside is untouched and
still mobile-only. Below `lg` the panel stacks under the phone, because a client
who opens the link on their own phone should still be able to read it.

Verified: both demo accounts complete the real OTP path end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 07:31:16 -04:00
serfowiandClaude Opus 5 176ba187c8 M1: phone app shell, settings, profile, dev login
Everything now renders inside a phone illustration on the entry screen,
with a five-tab bar. The frame lives in the root layout rather than one
page, so sign-in, onboarding and the job form are inside it too.

- Entry screen is the product running, not a marketing page: a live
  swipeable deck of real verified pros with a trade-filter strip above
  the card. deck.showcase is the only public procedure in that router
  and writes nothing, so an anonymous right swipe reaches no one.

- Settings: notification preferences (new table, defaults returned when
  no row exists), signed-in devices, GDPR export, deletion request.

  Closes the setEmail finding: an unverified address is no longer
  written to users.email, which is UNIQUE -- claiming a stranger's
  address used to block them from ever signing up with Google, and the
  uniqueness error leaked whether an address was registered. Now parked
  in email_change_requests until a token proves ownership.

- Profile: for a pro it leads with their REAL deck card, rendered by the
  same exported <Card> clients swipe, so the two cannot drift. Adds
  pro.previewCard (works at draft/pending, where publicProfile 404s) and
  pro.reorderMedia (photo position 0 is the deck card). Warns before an
  edit that would send a verified pro back for review, rather than after
  it silently drops them off the deck. Clients get a thin profile plus a
  route into pro onboarding -- supply is the launch blocker.

- Dev login: +34600000000 / 000000, behind THREE guards (NODE_ENV,
  an explicit ALLOW_DEV_LOGIN flag, and an exact number match). It
  overwrites the stored code rather than skipping verification, so the
  real expiry, attempt cap and single-use consumption still apply.

- Seed uses portrait photos. The cards previously showed picsum stock
  scenery -- a locksmith standing on a railway track.

Fixes found along the way: the card's name rendered ink-950 navy on a
dark photo because globals.css sets h1..h6 colour in @layer base, which
beat the inherited text-white; and the card referenced --color-go-500,
--border and --card, none of which exist, so the SEND JOB stamp had no
colour.

Also adds public/sw.js as a kill-switch: a service worker left
registered on localhost:3000 by a different project was intercepting
this app's chunks.

typecheck, lint clean; 186 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 03:16:27 -04:00