4 Commits
Author SHA1 Message Date
serfaandClaude Opus 5 cb148c3dc8 Panel: an architecture diagram, store costs, and Spanish that reads as Spanish
The right-hand panel now answers the question the stack list provoked — what
does a client actually install? Capacitor, iOS and Android lead "Built with",
and a new SVG diagram shows the two ways in: customers through the installed
app, staff through the admin dashboard behind Cloudflare Access.

Costs were half-quoted. The stack named Mapbox, Twilio, Resend and Sentry and
priced none of them, so each now states the shape of its bill rather than a
figure that would be wrong at every scale. App store accounts are listed too,
in the client's own name for the same reason the hosting is.

Currency was the serious one. Every figure is US dollars and the panel wrote
them as "$59", but this launches in Mexico, where "$" is the peso — the Spanish
column understated the running cost roughly seventeenfold to the only reader
who matters. money() now prints "$59" in English and "59 USD" in Spanish.

The rest of the Spanish was a literal translation of dense English: peninsular
vocabulary in a Mexican market, two stray vosotros forms among the tuteo, and
calques that were not grammatical — "aplicada en ninguna parte", "todo lo
discontinuo" for dashed lines. Reviewed all 126 strings. The architecture note
was 120 words in one block covering five topics; it is now two paragraphs, one
idea each, and the PostGIS sentence went because "What it does" already makes
that promise in language a client can act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:57:05 -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
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
serfowiandClaude Opus 5 582f13fa99 Entry screen is the app: phone frame with a live deck inside
The homepage was a marketing brochure -- a hero paragraph, a "three
steps" explainer and a tag list. It described the gesture in prose
while the actual Tinder deck sat behind /deck/[jobId], reachable only
after signing in AND posting a job. Nobody opening the app ever saw
the product.

Now / renders a phone illustration with the real app running inside
it: the same <Deck>, the same drag physics, real verified pros. On a
phone the bezel collapses and the deck simply fills the viewport --
drawing a picture of a phone on a phone is absurd, and it would eat
the width the cards need.

- Removed the fixed app bar and bottom tab bar. AppShell now renders
  the screen title as an in-flow h1; the bar owned the only h1 on
  every screen, so dropping it silently would have left every page
  headingless. The /jobs "post" action moved from bar chrome into the
  content, since the tab bar was its only other route there.

- getShowcaseDeck(): a deck with no job behind it. getDeck is
  job-scoped (joins jobs for category and location, anti-joins
  swipes), which an anonymous visitor has none of, so this centres on
  the launch city. Eligibility rules are copied verbatim -- nobody may
  appear in the shop window who could not appear on a real deck.

- deck.showcase: the only public procedure in the router. list and
  swipe stay behind clientProcedure. It reads nothing about the caller
  and writes nothing, so a right swipe on the entry screen is purely
  local. No real tradesperson is contacted until a job is posted.

- <Deck> filled a hardcoded 560px desktop box; it now fills its
  container. The card counter moved out from between the two action
  buttons so the thumb zone holds nothing but the two controls.

Verified against the seeded database: 22 eligible pros returned, and
all three seeded traps excluded for the right reason -- Pau Ribas
(22km out, 5km radius), Unverified Ulla (pending), Away Arnau (not
accepting). 7 new integration tests cover exactly that.

Also corrects a label I had written as "Plumbers near you" -- the
showcase deck is not category-filtered and shows every trade.

Includes concurrent edits to the mobile shell, ui/ primitives and
DESIGN.md made outside this session.

typecheck, lint, build clean; 141 tests pass.

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