Appearance
Biodata: design-to-implementation audit (2026-09-28)
Asked by the owner on 2026-09-28, after finding the photo viewer blank, "Request more details" throwing an error, People and access not following its journey, and the WhatsApp share arriving as a plain link. The request was explicit: establish the complete gap list against the latest approved design first, and do not patch individual issues on assumptions. This page is that baseline. No product code was changed to produce it.
THE SHORT ANSWER
Three of the owner's four named flows are broken end to end, not cosmetically, and each break was reproduced:
- Photos: the viewer opens off-screen. It is
position: fixedinside a section whose entrance animation leaves atransformbehind, so it is sized to that section (2,400-4,000 px tall) and the photograph lands ~1,200 px below the screen while the page is locked from scrolling. - Request more details: the form is shown on the forwardable link, where the route refuses every submit (
no_token) before the backend is called. Where a request can be made, there is no WhatsApp code step, the family is never notified, and releasing it mints a link nobody receives. - People and access: after minting a person's private link, "Send on WhatsApp" and "Copy" hand the family the public forwardable address instead. No one ever holds a person link, so the rest of the journey (Release more, a request, an answer) cannot happen through the app.
And the fourth: the WhatsApp preview is served correctly (every tag, a 200 image); what differs from the design is that the app shares a bare URL where the design shares a message and the link.
1 · The source of truth used
| Project | Claude Design "QR setu prototype" (633dc069-…), pulled fresh on 2026-09-28 through DesignSync |
| Round | 57 is the latest (the registry SCREENS.md names nothing later) |
| Files | setu-card/BiodataPage.dc.html (Parichay), setu-card/BiodataChitra.dc.html, setu-card/BiodataPatrika.dc.html, consumer/BiodataView.dc.html (in-app reader), consumer/BiodataDesigns.dc.html (picker), consumer/Biodata.dc.html (hub, editor, People and access), consumer/MyQRSetu.dc.html, consumer/biodata-core.js, biodata-view.js, biodata-templates.js, biodata-publish.js, consumer-data.js, my-qrsetu/biodata.spec.md, SCREENS.md |
| Side by side | the artboards render on localhost:8090 (the mirror, refreshed today), the built web page on localhost:4175, the app PWA on localhost:8080 |
ONE SOURCE COULD NOT BE READ WHOLE (QRS-1399)
consumer/Biodata.dc.html is larger than DesignSync's 256 KiB read limit. The pull came back truncated: true and stops inside the People view's logic. The People markup is complete; the tail of its behaviour is not (the reopen card's copy, the withdraw / stop-link / removal confirmation copy, the end of the plan line). Those rows are marked not readable below, never inferred. An older complete pull (2026-09-10) is byte-identical on every overlapping People line, so it is a strong hint, not evidence. The fix is on the design side: split People and access into its own file.
2 · The intended journeys (from the design)
2a · A reader opens a shared biodata (phone browser, no app needed)
- The forwardable link (
qrsetu.com/<slug>/biodata) opens at Basic; a person's own link (…/biodata/<id>) opens at whatever the family released to that person. - Photographs (Parichay): Basic shows the cover alone, cropped 3:2. Released shows every shown photograph (up to 4) at 4:5 in a carousel that auto-advances every 4.2 s, pauses when touched and resumes 6 s after a swipe, with dots below the card (44 px targets, the active one 18 px wide). A tap opens a full-screen viewer: count, Close, previous and next, swipe; the crop at Basic, the whole photograph (
contain) at Released. No zoom is drawn in round 57. Chitra and Patrika draw no viewer. - Asking for more: any Basic reader, on either kind of link, sees "Ask the family for the fuller profile" (Parichay) or a "Held back for now" card listing what is held back (Chitra, Patrika). The reader gives their name, who they are, and a WhatsApp number; on Parichay a 6-digit code sent on WhatsApp confirms the number; the page then says the family was told on WhatsApp.
- The page closes with the growth card and "Report this profile".
- Sharing the page onwards: the page's own share card (WhatsApp, Copy link, More apps) sends a message ("Sharing
<first>'s profile. Please have a look and let us know.") with the link. The chat shows a card with the first name, one neutral line and a branded image with no face.
2b · The owner shares and answers (the app)
- People and access: the first share opens a sheet; "Send on WhatsApp" sends that person's own link. Each holder is a row with state, opens, expiry chips and actions (Extend, Withdraw, Message, Release more, Give access again, Copy link, Stop this link).
- A request arrives: a "Waiting for your answer" card with who asked, what they asked for, the number "confirmed by one code", how they used the link, what they see today and the honest line. The owner picks 30, 90 or 180 days and taps Release, "Ask them something first" or "Not now". The reader's page then fills in on its own.
- Removal by the subject is honoured within 72 hours and cannot be refused. Conclude withdraws every release and turns every link into a thank-you page; reopen starts with zero releases.
2c · The owner previews and changes the design (the app)
- Preview (
BiodataView, round 56): the real page framed at?embed=1for every design, with only the app's chrome: a header (back, title, address), a three-way reader switch (Anyone with the link, A family you released, Owner preview), and at Owner preview an owner band (Edit, People with the waiting count, and a design line "Your readers open it as<design>" with Change and open in a browser). The frame is also handed the app's light/dark mode (appearance) and any closed state. Inside the frame the page drops its own site header. - Change the design: the editor's "How it reads" card switches in place and offers "Preview as …" and "Compare all three", which opens the picker (both modes, live full-screen preview, a switched state).
3 · Photographs: design against build
| Area | Design (round 57) | Build (measured) | Gap |
|---|---|---|---|
| Basic hero | cover only, 3:2 crop | cover only, 3:2 | ✅ |
| Released hero | up to 4, 4:5, carousel | up to 4, 4:5, scroll-snap strip | ✅ (mechanism differs, behaviour matches) |
| Auto-advance | every 4.2 s; pause on touch, resume 6 s after a swipe | every 3 s (≈3.4 s measured); pause on touch, pointer, focus and while the viewer is open; resume ≈3 s after | ⚠ interval is an owner call (QRS-1371) |
| Dots | below the card, 44 px, active 18 px | below the card, 44 px, active 18 px | ✅ (added 2026-09-27) |
| Tap to open | full-screen viewer | opens off-screen: blank to the reader (live devv and local, whenever motion is allowed) | ❌ QRS-1373 |
| Viewer content | count, Close, prev/next, swipe; crop at Basic, contain at Released | same, plus zoom 1x/1.8x/2.5x at Released and a note | ⚠ zoom is not in round 57 (QRS-1395) |
| Viewer close | Close button only | Close, Escape, arrows; focus returns to body | ✅ plus extra keyboard support |
| Chitra, Patrika viewer | none drawn | none | ✅ matches; a design gap for the owner's parity request (QRS-1370) |
| Photo address | not specified | presigned for 120 s, no cache headers; a re-fetch after that returns 403 | ⚠ risk (QRS-1397) |
Why the viewer is blank (QRS-1373), measured. PhotoViewer is position: fixed. It renders inside [data-parity="biodata-page-profile"], which carries the class pc-rise (BiodataPage.tsx:288-289). That class runs pcRise .5s … both (sections/parichayCss.ts:29), and both keeps transform: translateY(0) after the animation ends. A transformed ancestor becomes the containing block for fixed children. Result on devv (414×896): the viewer box starts at y=102 and is 2,485 px tall, the photograph is centred at y≈1,215, and the page is locked from scrolling, so the reader sees the dark overlay, the count and Close, and nothing else. It works under reduced motion, where .pc-rise{animation:none} removes the transform, which is the run-web driver's default and is exactly why my check on 2026-09-27 missed it. The presigned URL is not the cause: the image is reused from the page (one R2 request, 0 at open), and it still decodes after 130 s.
4 · Request more details: design against build
| Step | Design (round 57) | Build (measured) | Gap |
|---|---|---|---|
| Who can ask | any Basic reader, on the forwardable link or a person link | the form shows on both; the route refuses the forwardable link (no_token, HTTP 400, before the backend) | ❌ QRS-1376 |
| Nav chip | "Ask for more" scrolls to the block | #ask anchor, scrolls | ✅ (this is the "CTA that acts as a link") |
| Fields | name, who you are, WhatsApp (+91, 10 digits) | same three; any E.164 number accepted (+44… passes) | ⚠ minor |
| Validation copy | three per-field messages | route-level messages; fields cleared on any error | ⚠ QRS-1396 |
| WhatsApp code step | 6-digit code, "Change the number", "Send the code again" | none; phone_verified has no writer anywhere | ❌ QRS-1377 |
| Pending | "You have asked the family for more", "They were told on WhatsApp" | cookie-scoped pending card; the family is not told (no WhatsApp, no push) | ❌ QRS-1378 |
| Owner sees it | waiting card in People and access | waiting card renders (from get_my_biodata_shares) | ✅ where reachable |
| Owner releases | the reader's page "fills in on its own" | a new share is minted with no address (profile_id not sent) and its token is discarded by the hook: nobody can open it | ❌ QRS-1379 |
| "Ask them something first" | opens Chats | generic chat list; an anonymous asker has no conversation | ⚠ blocked by the conversation model (QRS-1177) |
| Chitra, Patrika | a "Held back for now" card built from what is held back | no ask UI at all; the public read carries no requestable (QRS-1263) | ❌ QRS-1381 |
| Inside the Preview | not addressed by the design | submitting the form blanks the frame | ❌ QRS-1382 |
| Language | per-language copy | every askError.*, pendingTitle and pendingNote is English in hi and mr; the redirect drops ?lang | ❌ QRS-1396 |
Where it breaks, in order: (1) the reader on the forwardable link cannot ask at all; (2) a reader on a person link can, but nobody verifies the number and nobody tells the family; (3) the family's release goes nowhere. The reader waits for a page that never fills in.
5 · People and access: design against build
| # | Design (round 57) | Build (measured) | Gap |
|---|---|---|---|
| 1 | Share with the first person sends that person's link | mint works; Send and Copy share the forwardable address | ❌ QRS-1374 |
| 2 | share another person | the sheet keeps the first URL and offers no mint button until the screen remounts | ❌ QRS-1375 |
| 3 | Stop this link switches the forwardable link off | calls withdraw; the page ignores the withdrawn row and keeps serving | ❌ QRS-1385 |
| 4 | Give access again | always refused on a withdrawn row (the SQL requires withdrawn_at is null); works on a lapsed one | ❌ QRS-1386 |
| 5 | a row answered from a request is named | the row has no name and empty initials (the SQL insert never sets label) | ❌ QRS-1387 |
| 6 | Conclude withdraws every release | the SQL withdraws no share; released rows stay actionable | ❌ QRS-1388 |
| 7 | Reopen starts with zero releases | lands on a draft the screen describes as published | ❌ QRS-1389 |
| 8 | back to My QR setu | no back control; tabs push onto the stack; a mint refusal's reason is dropped; server and client count the link cap differently; a retry mints a new idempotency key; conclude can send version 0 | ❌ QRS-1390 |
| 9 | the waiting card, facts, honest line, release box, lapse chips | render as designed; the "confirmed by one code" label shows although nothing confirms | ⚠ QRS-1377 / QRS-1046 |
| 10 | removal card, 72 h clock | renders; rows stay actionable during removal (the design does not say otherwise) | ✅ as drawn |
| 11 | reopen / withdraw / removal confirmation copy | built | not readable in the round-57 pull (QRS-1399) |
6 · The in-app Preview and the design picker
| Area | Design (round 56/57) | Build (measured) | Gap |
|---|---|---|---|
| Frame | the real page at ?embed=1, every design | yes, WebView / iframe, projected draft | ✅ |
| Page chrome inside | site header hidden | site header shown (wordmark, language pills) | ❌ QRS-1383 |
| Reader switch | three options, incl. Owner preview | two (Anyone / Released) | ❌ QRS-1384 |
| Owner band | only at Owner preview; Edit, People (n waiting), design line with Change and open-in-browser | always shown; Edit and People; no design line | ❌ QRS-1384 |
| Theme to the page | `appearance=light | dark; closed state` | neither: a dark phone frames a light page; closed states only as a native card |
| Scan another family's biodata | their page | your own profile (the scan's slug and share are ignored) | ❌ QRS-1380 |
| Hub eye | opens the Preview | pushes ?viewer=[object Object] (the press event); falls back to Released | ⚠ QRS-1391 |
| Production | the page on qrsetu.com | the embed returns 404 on qrsetu.com (the web is not promoted) | ⚠ release item QRS-1392 |
| Picker ("How it reads", "Compare all three") | designed in full | not built; nothing writes template_key though the backend accepts it | ❌ QRS-1367 |
7 · WhatsApp sharing
| Design | Build (measured) | Gap | |
|---|---|---|---|
| Preview tags | first name title, one neutral line, branded image, no face | all present to WhatsApp's own crawler: og:title "Vedaa, marriage profile", description, 1200×628 PNG (200, 33 KB), twitter:card | ✅ |
| Preview image | wash, QR setu pill in the brand face, "Marriage profile" on a dark band | wash, QR setu pill (not the brand face), "Marriage profile" in grey, no band | ⚠ minor |
| What the app shares | a message and the link ("Sharing <first>'s profile. Please have a look…") | the bare URL | ❌ QRS-1394 |
| The page's own share card | WhatsApp · Copy link · More apps | not built | ❌ QRS-1120 |
| Why a plain link | — | not reproducible from here. The page answers in 1.5-2.7 s with no cache, and WhatsApp builds the card on the sender's phone while composing; a link shared before 11 Sep (when the tags shipped) may also be cached by WhatsApp as plain | ⚠ unverified: needs a device test (below) |
⚠ The design deliberately puts no photograph and no profile detail in the chat preview: "the preview carries a first name and nothing more. No photograph, no surname, no age and no city", because a chat caches it and a revocation can never reach that copy. A preview with the person's photo would reverse a disclosure rule, which is the owner's call, not a fix.
8 · The Biodata ID
| Question | Answer | Evidence |
|---|---|---|
| Is the format documented? | Yes, decision D6 (2026-08-31): QRS 482 011 735, nine digits grouped 3-3-3, stored as a number with QRS added at display, a keyed Feistel permutation over a hidden sequence (never sequential, no collisions), gender as a chip beside it and never encoded, a space never a hyphen, and lookup by number authenticated, rate-limited, Basic only and an owner switch | consumer/decisions.md D6 |
| Is it generated in the backend? | Yes: manage-biodata mints it at first publish with the BIODATA_REFERENCE_KEY secret | manage-biodata/index.ts:184-193, reference.ts |
| Is it stored per record? | Yes: biodata.profiles.reference, unique; every published or retired Dev profile has one, drafts none | Dev: 3G5FH0X, 3QF0KT8, 3N9EAVM, … |
| Does it match D6? | No: a 32-bit permutation encoded as 7 base-32 characters (3G5FH0X), stored as text, not D6's 9-digit number. No recorded decision changed it; it has been this way since the first commit (2026-09-05) | reference.ts |
| Exposed through APIs? | owner reads only (get_my_biodata); not in the public read | get_public_biodata projection |
| Shown in the UI? | nowhere, in the app or on the page | no reader of .reference |
| Where the design shows it | nowhere: no placement in any of the 7 biodata / My QR setu files | fresh pulls today |
So the scope is: (1) the owner decides the format (keep the 7-character code or move to D6; moving changes the codes on existing Dev profiles, harmless before launch); (2) a design request for where it appears and how it sits beside the gender chip; (3) exposure in the public read; (4) the lookup by number, if wanted for launch (QRS-1393).
9 · Consolidated fix list, by dependency and impact
Wave 1 · the three broken journeys (no design question, no backend decision).
| id | fix | layer |
|---|---|---|
| QRS-1373 | render the photo viewer outside the transformed section (a portal to body), and stop the entrance animation leaving a transform | web |
| QRS-1374 | Send and Copy share the minted person link | app |
| QRS-1375 | clear the minted link when the share sheet closes | app |
| QRS-1380 | a scanned biodata opens that family's page, not the owner's | app |
| QRS-1382 | the ask form inside the Preview never submits the frame away | web |
| QRS-1383 | hide the site header in embed mode, as the design does | web |
| QRS-1387 | name the row created from an answered request | backend (SQL) |
| QRS-1386 | "Give access again" on a withdrawn row mints a new link rather than calling a release the SQL refuses | app + backend |
| QRS-1391 | the hub eye passes a tier, not the press event | app |
Wave 2 · the request flow (needs one owner decision, then backend).
| id | fix | depends on |
|---|---|---|
| QRS-1376 | let a Basic reader on the forwardable link ask (the design's rule): a request tied to the profile and the asker's number rather than to a share token | owner decision on the model, backend report first |
| QRS-1377 | the WhatsApp code step for the asker, and phone_verified written; until then the "one code" copy is removed | reuses the WhatsApp OTP sender; backend |
| QRS-1378 | tell the family a request arrived (in-app notification now, WhatsApp template later) | backend; WhatsApp template approval |
| QRS-1379 | a release reaches the asker: send profile_id, keep the minted link, deliver it to the asker's verified number | QRS-1377 |
| QRS-1381 | Chitra and Patrika ask cards, fed by requestable in the public read | QRS-1263 backend |
| QRS-1385, QRS-1388, QRS-1389 | stop-link, conclude and reopen do what their copy says | backend |
| QRS-1390, QRS-1396 | the People screen's smaller defects; the ask copy translated, ?lang kept, fields kept on error | app, web, i18n |
Wave 3 · Preview, picker, sharing, ID.
| id | fix | depends on |
|---|---|---|
| QRS-1384 | the round-56 Preview chrome: three-way switch, band at Owner preview, design line, appearance, closed states | — |
| QRS-1367 | the "How it reads" card and the picker | fresh pull, screen by screen |
| QRS-1394, QRS-1120 | share a message with the link; the page's own share card; the preview image details | — |
| QRS-1393 | the Biodata ID | owner decision + design request |
| QRS-1392 | promote the web so the embed exists on qrsetu.com before the app ships | release |
Owner calls, not fixes: QRS-1371 (3 s or the design's 4.2 s), QRS-1395 (keep zoom, which round 57 removed), QRS-1393 (the ID format), whether the chat preview should ever carry more than a first name.
Design requests (Claude Design): QRS-1399 (split the hub artboard below 256 KiB), QRS-1370 (a viewer for Chitra and Patrika, if the owner wants one), QRS-1400 (the design-side ambiguities the four design passes recorded: e.g. Chitra and Patrika have no code step though their copy says one code confirms; Parichay's ask chips are hard-coded; "With the family since Sunday" is a literal; the celebration offers a PDF the core module says does not exist; the editor's "Anyone with the link" row resolves to the released tier; toast styling is not in the mirror).
A correction I owe (QRS-1401): on 2026-09-27 I recorded that BiodataView.dc.html "still draws its own reading" and marked 31 contract rows blocked on it. That was wrong: the round-56 artboard already frames the page, and it was on disk when I wrote it. Those rows must be re-assessed against round 56, and QRS-1368's open half closed. The design also absorbed QRS-1361 in round 57 (the growth card's Marathi and Hindi words), so that allowance will go stale on the next gate run.
10 · Re-validation checklist
Each line is answered by a command whose output is pasted, or by a named manual check on a device. "Motion allowed" means the check is run without reduced motion; the driver's default hides QRS-1373.
| # | Check | How | Pass |
|---|---|---|---|
| 1 | Photo viewer, Parichay, both tiers, motion allowed | run-web driver motion no-preference, open the viewer, read the <img> rect | the photograph is inside the viewport |
| 2 | Same on live devv, phone browser | owner, Android Chrome and iPhone Safari | the photograph shows on tap |
| 3 | Auto-advance and dots | driver, 10 s scroll sample, dot tap | advances at the agreed interval; a dot moves it and never opens the viewer |
| 4 | Ask on the forwardable link | driver POST to /<slug>/biodata | accepted once QRS-1376 lands; until then the form is not shown there |
| 5 | Code step | Dev, a real number | a code arrives on WhatsApp; a wrong code is refused with copy |
| 6 | Family told | Dev | the owner gets the in-app notification (and WhatsApp once approved) |
| 7 | Release reaches the asker | Dev, end to end | the asker's page opens at Released from the link they receive |
| 8 | People: first and second share | jest render + device | Send and Copy carry each person's own link |
| 9 | Stop this link, Give access again, conclude, reopen | pgTAP + Dev | each does what its copy says |
| 10 | Preview chrome | jest render against the round-56 spec | three options, band at Owner preview, design line, appearance, closed states |
| 11 | Scan another family's biodata | device | their page opens |
| 12 | Ask inside the Preview | driver embed | the frame never goes blank |
| 13 | WhatsApp share | device: share from the app to a WhatsApp chat, wait for the card before sending | the message text and the card (first name, line, image) |
| 14 | Biodata ID | per the owner's decision | the format, the public read and the placement match |
| 15 | Presentation gate | npm run check:biodata-presentation | 0 DIVERGED, stale allowances removed |
| 16 | Parity contracts | npm run check:design-parity | the People, Preview and page contracts re-assessed against round 57 |
| 17 | Suites | npm test, npm run test:db, npm run test:ef | green, counts pasted |
Method, so the next audit can repeat it
- The design was pulled fresh and read by four agents that saw only the design; four more measured only the implementation, from the served page in a real browser, jest renders of the real screens, and Dev's logs and rows (read-only). Neither side could colour the other.
- The owner's four reports were each reproduced before being explained.
- Three things this audit could not do: a native device run (the WebView, the phone's reduced-motion default, WhatsApp itself), WebKit and Firefox, and the unreadable tail of the hub artboard.