Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/SurveyRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/admin/SurveyPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/surveyInit.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
19 changes: 19 additions & 0 deletions frontend/src/surveyInit.ts
Original file line number Diff line number Diff line change
@@ -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;
66 changes: 60 additions & 6 deletions infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 # → <host>:<port>

# 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:<password>@<host>:<port>/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

Expand Down
Loading