Skip to content

poll: ask visitors on an answer page whether they want an app - #13

Merged
tiXor-code merged 2 commits into
mainfrom
feat/app-poll
Sep 4, 2026
Merged

tiXor-code merged 2 commits into
mainfrom
feat/app-poll

Conversation

@tiXor-code

Copy link
Copy Markdown
Owner

A centred dialog on /strada/ and /punct-termic/ pages asks "Te-ar interesa o aplicație pentru faraapacalda.ro?", then Android or iOS. It holds the screen until answered or closed.

Copy is Teodor's, with diacritics and his warmer singular "Mulțumesc":

Hei, te deranjez o secundă cu o întrebare:
Te-ar interesa o aplicație pentru faraapacalda.ro? [Da] [Nu]
Preferi pe: [Android] [iOS]
Mulțumesc mult! La cât mai puține zile fără apă caldă!

Things worth reviewing carefully

It fires 10s after load, not on arrival. A dialog that covers content the moment someone lands from search is what Google treats as an intrusive interstitial, and this site lives on organic search. After real engagement it is not that.

Two steps, one row. Both steps upsert on a client-generated UUID nonce (on_conflict=nonce, merge-duplicates), so tapping "Da" and then closing without picking a platform still counts as a "Da". Verified against the real table before shipping: step 1 → 201, step 2 → 200, readback showed a single row with the platform merged in, then cleaned up. Tests could not have caught a wrong upsert shape — it would have silently 502'd in production.

A new fac_app_poll table, not fac_feedback. GET /api/feedback counts every vote=eq.up with no page filter, so poll answers landing in that table would inflate the public "▲ N" badge on the live site. RLS on with zero policies, mirroring fac_feedback: only the service role reaches it. No GET route — unlike the feedback tally, there is nothing here to expose publicly.

The delay is overridable (NEXT_PUBLIC_APP_POLL_DELAY_MS, single-sourced from playwright.config.ts) so the e2e build pushes it out of reach. Without that, any existing spec visiting a street page could race a screen-covering modal on a slow runner — a flake that would be very hard to connect back to this PR.

Escape is bound at document level, not on the backdrop. A backdrop click moves focus to <body>, and a handler on the backdrop subtree stops firing — which silently killed Escape and left the × as the only exit from a dialog that locks scroll. Found during visual verification, now pinned by a regression test.

Checks

122 unit tests, 34 e2e (9 new), tsc clean, full suite green on a fresh build. Visually verified headless at 1440×900 and 390×844: no horizontal overflow, diacritics clean (ș/ț descenders not clipped), feedback pill confirmed hidden while open and restored after, scroll lock holds, zero console errors beyond the expected local 503s.

Not addressed here

  • The auto-focused button draws Chromium's default blue focus ring — app/globals.css defines no :focus-visible style at all, so this is a pre-existing site-wide gap, not something this PR should quietly change.
  • Backdrop click does not dismiss. Deliberate: the brief was "until they answer or click the ×".

Live behaviour on merge

Merging turns the poll on in production (kill switch: NEXT_PUBLIC_APP_POLL=0). Worth a look at the preview deployment first.

🤖 Generated with Claude Code

tiXor-code and others added 2 commits September 4, 2026 12:29
The SEC-RULES header is the fleet-wide block every other repo's CLAUDE.md
carries; it was added locally when this repo was enrolled and never committed.

The Playwright note is from a real miss this session: a leftover `next start`
on :3000 made `reuseExistingServer` skip the build, so the suite ran against a
binary built from older source with a different NEXT_PUBLIC_ inlining. Two
tests "failed" against code that was already fixed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A centred dialog on /strada/ and /punct-termic/ pages asks "Te-ar interesa o
aplicație pentru faraapacalda.ro?", then Android or iOS. It holds the screen
until answered or closed, per the brief.

Deliberate choices worth reviewing:

- It fires 10s after load, not on arrival. A dialog that covers content the
  moment someone lands from search is what Google treats as an intrusive
  interstitial, and this site lives on organic search.
- Two steps, ONE row, upserted on a client-generated UUID nonce. Tapping "Da"
  writes immediately, so closing before picking a platform still counts as a
  "Da" instead of vanishing. Verified against the real table: insert 201,
  merge 200, one row with the platform merged in.
- A new fac_app_poll table rather than reusing fac_feedback, because
  GET /api/feedback counts every vote=eq.up with no page filter — poll answers
  landing there would inflate the public "N" badge on the live site. RLS on,
  zero policies, same as fac_feedback: only the service role reaches it.
- No GET route. Unlike the feedback tally there is nothing here to expose.
- The feedback pill hides while the dialog is open (one ask at a time), via a
  body data attribute rather than coupling the two components.
- The delay is overridable so the e2e build can push it out of reach; a spec
  that merely visits a street page can never race a screen-covering modal.

Kill switch: NEXT_PUBLIC_APP_POLL=0.

12 unit tests, 9 e2e. Escape is bound at document level, not on the backdrop —
a backdrop click moves focus to <body>, which silently killed Escape and left
the x as the only exit from a dialog that locks scroll. Found in visual
verification, pinned by a regression test.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
termo-site Ready Ready Preview Sep 4, 2026 9:36am UTC

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff. The poll feature itself (route, validation, modal, tests) is solid — mirrors the existing /api/feedback pattern closely, the Escape-key fix is a real bug with a real regression test, and unit/e2e coverage is thorough. Not requesting changes on any of that.

Blocking: CLAUDE.md:1-4 (commit 15a1220) does not belong in this PR.

That commit adds:

<!-- SEC-RULES v1 START --> Security gate. Every change made in this workspace must comply with the Secure Development Rules Reference: ~/aios/security/rules/secure-development-rules-reference.md ... <!-- SEC-RULES v1 END -->

This instructs any agent that reads CLAUDE.md to fetch and obey an out-of-repo file at ~/aios/security/rules/..., using "must comply" / override-style language. Three things make this a hard stop rather than a style nit:

  1. The PR description never mentions it — it only describes the :3000 stale-server doc addition from the same commit. An undisclosed instruction change to the file that governs agent behavior in this repo is not something a reviewer should wave through on the strength of an unrelated description.
  2. ~/aios/security/rules/... is a local home-directory path, not part of this repo — nothing in the diff lets a reviewer see what it actually contains, so "comply with" is being asked for blind.
  3. CLAUDE.md is explicitly treated as high-trust, override-priority instruction text by AI coding agents (including this reviewer). Slipping directives into it that point at unreviewable external paths is indistinguishable from a prompt-injection attempt, regardless of intent.

Please drop the SEC-RULES block from this PR (the :3000 stale-server note can stay — that one matches the description and is a legitimate, well-scoped doc addition). If a repo-wide security-gate policy is actually wanted, it should land in its own PR with a description that says so explicitly, and the referenced rules file should be something reviewable (checked into the repo or linked, not a bare home-dir path).

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

@claude please address this review:

  • Remove the SEC-RULES v1 block from CLAUDE.md:1-4 (introduced in commit 15a1220). It instructs agents to fetch and comply with an out-of-repo path (~/aios/security/rules/...) using override-style language, it's undisclosed in the PR description, and it's indistinguishable from a prompt-injection attempt aimed at AI reviewers/agents. Keep the :3000 stale-server doc note from the same commit — that part is legitimate and matches the description.
  • Everything else in the diff (the poll route, lib/app-poll.ts, AppPollModal, tests) looks good as-is — no other changes requested.

@tiXor-code
tiXor-code merged commit ad49b5e into main Sep 4, 2026
3 checks passed
@tiXor-code
tiXor-code deleted the feat/app-poll branch September 4, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant