diff --git a/frontend/src/SurveyRunner.tsx b/frontend/src/SurveyRunner.tsx index 8614246..185b6c5 100644 --- a/frontend/src/SurveyRunner.tsx +++ b/frontend/src/SurveyRunner.tsx @@ -4,6 +4,7 @@ import { Survey } from 'survey-react-ui'; import { ApiError, fetchSurvey, submitResponse, type SurveyDetail } from './api'; import { RespondentLayout } from './RespondentLayout'; +import './surveyInit'; import { Card, CardBody, LoadingState } from './ui'; interface SurveyRunnerProps { diff --git a/frontend/src/admin/SurveyPreview.tsx b/frontend/src/admin/SurveyPreview.tsx index 062d389..d350429 100644 --- a/frontend/src/admin/SurveyPreview.tsx +++ b/frontend/src/admin/SurveyPreview.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Model } from 'survey-core'; import { Survey } from 'survey-react-ui'; +import '../surveyInit'; import { Alert, Button } from '../ui'; interface Snapshot { diff --git a/frontend/src/surveyInit.test.ts b/frontend/src/surveyInit.test.ts new file mode 100644 index 0000000..b230137 --- /dev/null +++ b/frontend/src/surveyInit.test.ts @@ -0,0 +1,15 @@ +import { Serializer } from 'survey-core'; +import { describe, expect, it } from 'vitest'; + +import './surveyInit'; + +describe('surveyInit', () => { + // Regression test for GH #49: on iOS Safari the default longTap=true makes + // the initial touch on a ranking item scroll the page instead of starting + // a drag. Disabling longTap is what restores tap-and-drag reorder on mobile, + // so a future bump of survey-core that resets the default would silently + // re-break iPhone reordering — this test catches that. + it('defaults ranking longTap to false so touch drag starts without a long-press', () => { + expect(Serializer.findProperty('ranking', 'longTap').defaultValue).toBe(false); + }); +}); diff --git a/frontend/src/surveyInit.ts b/frontend/src/surveyInit.ts new file mode 100644 index 0000000..fddf25a --- /dev/null +++ b/frontend/src/surveyInit.ts @@ -0,0 +1,19 @@ +import { Serializer } from 'survey-core'; + +// Make ranking reorder respond to a normal drag on touch devices instead of +// requiring a 500ms long-press first. With the default longTap=true, iOS +// Safari treats the initial touch as a scroll gesture and the page scrolls +// instead of starting the drag (see GH #49). SurveyJS's own docs recommend +// disabling longTap when users naturally swipe-to-drag rather than long-press. +// +// Idempotent: re-importing this module does not re-toggle the default. +// Explicit null-check so a future survey-core upgrade that renames the +// property fails loudly here rather than with a confusing "Cannot set property +// 'defaultValue' of null" at the assignment site. +const longTap = Serializer.findProperty('ranking', 'longTap'); +if (!longTap) { + throw new Error( + "survey-core ranking property 'longTap' not found — has the API changed? See GH #49.", + ); +} +longTap.defaultValue = false; diff --git a/infra/README.md b/infra/README.md index 6c4dc8b..1e9f68e 100644 --- a/infra/README.md +++ b/infra/README.md @@ -116,13 +116,67 @@ it can't reach the private-network Postgres or the in-image entrypoint: railway ssh --service web # drops into a shell in the web container ``` -## Analyst access, demo seed, secret rotation +## Connecting to Postgres directly (analysts, reviewers) -External analyst/reviewer database access (the opt-in `enable_postgres_proxy` TCP -proxy), seeding the initial admin (`seed` entrypoint verb), and rotating the -generated role passwords (`scripts/rotate_role_password.py` + the -`*_password_override` variables) are covered in -`docs/verification/m7.6-demo-to-prod.md`, along with the full demo→prod checklist. +Railway's Postgres sits on the project's **private network**, so there is no +public connection string until the opt-in TCP proxy is on. It is **off by +default** — a standing public 5432 is a deliberate exposure, so flip it on +while you need access and back off when you're done. Two paths in, by +audience: the operator shortcut below, or per-user logins for anyone else. + +### Operator shortcut (group-role login) + +As the deployer you already hold the role passwords (they're in tofu state), +so the fastest path is logging straight in as the group role. **Don't share +these** — they're operator-wide secrets with no per-person audit trail; for +anyone else, mint a per-user login (next subsection). + +```bash +# 1. Enable the proxy for this session — -var keeps it out of tfvars, +# so re-running stays idempotent. +tofu apply -var 'enable_postgres_proxy=true' +tofu output postgres_proxy # → : + +# 2. Grab the role password. +tofu output -raw stele_analyst_password # marts (the warehouse) +tofu output -raw stele_pii_reviewer_password # pii (free-text review) + +# 3. Connect. +psql "postgresql://stele_analyst:@:/stele" + +# 4. Close the public endpoint when you're done. +tofu apply -var 'enable_postgres_proxy=false' +``` + +Pick the role by what you need to read: `stele_analyst` reaches `marts` only, +`stele_pii_reviewer` reaches `pii` only — that one-schema-each ceiling is +what keeps a leaked credential low-stakes (CLAUDE.md *Schemas* table). Both +group roles are LOGIN and hold the schema grant directly, so no `SET ROLE` +step is needed. + +### Per-user logins (for colleagues, auditable, revocable) + +Mint a personal NOINHERIT login role with the M3.5 provisioning CLI, deliver +the one-time password, and they connect to the same proxy host:port. The +login is privilege-less until they `SET ROLE stele_analyst;` (or +`…_pii_reviewer`) after connecting. Full provision + revoke flow: +`docs/verification/m7.6-demo-to-prod.md`. + +### Gotchas + +- Use the plain `postgresql://` driver tag for psql — *not* the + `postgresql+psycopg://` tag the app's env vars use (that one's + SQLAlchemy-only). +- Default DB name is `stele` (`variables.tf` `database_name`); check your + `terraform.tfvars` if you overrode it. + +## Demo seed and secret rotation + +Seeding the initial admin (`seed` entrypoint verb), rotating the generated role +passwords (`scripts/rotate_role_password.py` + the `*_password_override` +variables), and per-user credential provisioning/revoke are covered in +`docs/verification/m7.6-demo-to-prod.md`, along with the full demo→prod +checklist. ## State holds secrets