Repository navigation
11 cht UI builder test hosting - #13
Merged
Merged
Conversation
Why: CHT's value is invisible to anyone who cannot get past setup — git, node, pnpm, python, cht-conf, a checkout, and somewhere to run CHT, before a single form is visible. This is a test platform so a flood researcher or a child-health team can build and validate a real config in a browser with nothing installed, then deploy it to their own CHT. Nothing is broken today; the app works completely for one person, on their own machine, editing folders on that machine. Each work item removes exactly one of those three assumptions. shared/ (25k lines, 818 tests) and the whole editor are untouched; state.ts (92 lines) is the only real rewrite and browse (~150 lines) is deleted in favour of a per-user project registry. Decisions locked by the PO 2026-08-20: rung 1 only (no upload, no hosted CHT runtime); user's data and liability; blank start first-class alongside templates; zip import in; desktop app stays. Key technical decision: delete projectPath from server state and address projects by id per request. state.ts:34 is the only single-slot state in the server; removing it drops 17 of 48 call sites, allows many configs at once per tab, and means no filesystem path ever crosses the wire — so /api/browse is removed rather than jailed. Verified while writing: blank, empty and malaria templates pass compile + convert + validate; cht-default does NOT (requires `moment`, no package.json); no per-template compile guard exists. Both recorded as pre-work in §9. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mplate compile as shipped
One image: node 22, python 3.11 + pyxform 4.5.0, git, cht-conf, the built
client. Proven inside the image: validate-generated-forms passes in both
directions and all four templates compile, convert and validate.
cht-default did not compile anywhere: its extras require('moment') and
nothing resolved it (cht-conf does not ship moment). The template now
declares it in a package.json, as cht-core's own config does, and the image
installs moment at /node_modules so webpack's upward node_modules walk finds
it from any project under /data with no npm install in the user's project.
scripts/validate-templates.mjs is the per-template compile guard the
standing directive asked for: copy (dotfiles included), npm install if a
package.json is present, compile-app-settings + convert/validate app and
contact forms. It would have caught the moment gap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…try, project ids on every request, import/export
Removes the three assumptions the desktop app made (one user, own machine,
own folders) without touching a parser or an editor:
- server/src/config.ts: CHT_UI_MODE=desktop|hosted, DATA_ROOT layout
DATA_ROOT/users/<user>/{registry.json,projects/<slug>}.
- server/src/auth.ts: email + password (scrypt), bearer tokens (sha256
stored), 30-day sessions; onRequest hook stamps req.userId. Desktop mode
has one implicit local user and no sign-in. The SSE stream route alone also
takes ?token= because EventSource cannot set headers.
- server/src/state.ts: the single-slot projectPath is gone. Every request
names its project with x-project-id and the server resolves it in the
requesting user's registry; resolveInsideProject(req, rel),
projectRootFor(req) and getDeployConfig(userId) thread through all 31
sites. Hosted mode refuses any entry outside the user's projects dir.
Desktop mode migrates the old state.json and falls back to the
last-opened project when no id is given, so a fresh tab lands where you
were and the existing Playwright suite keeps working.
- routes/project.ts: list / open-by-id / rename / forget (?files=1 deletes
only under the user's projects dir). Open-by-path stays, desktop only.
- routes/browse.ts: the filesystem browser, registered only in desktop
mode — absent from the hosted surface rather than jailed.
- routes/transfer.ts: import from a git URL (shallow clone, size budget,
project root found up to two levels down), push a branch back, import a
zip (raw body, zip-slip refusal, common-root stripping, exclusion list),
export a zip.
- routes/templates.ts: create by name under the user's dir when hosted;
'Start blank' is the empty template, first in the list.
- index.ts: auth hook, CORS headers for authorization/x-project-id,
SERVE_CLIENT=1 serves client/dist from the same origin.
- client: api.ts is the one chokepoint (VITE_API_BASE, bearer token in
localStorage, project id in sessionStorage so each TAB pins its own
project); SignIn; a project list on the picker with start blank /
template / import git / import zip (path input stays in desktop mode);
the wizard asks for a name instead of a folder when hosted;
ProjectTransfer on the overview (download zip, push branch).
Acceptance: scripts/hosted-acceptance.mjs (API level, 43 assertions, spawns
the server in both modes) and client/tests/hosted-authoring.spec.ts under
playwright.hosted.config.ts (sign up, start blank, two tabs two projects,
another user sees nothing, sign out). Both green; the acceptance script also
passes inside the Docker image.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rtures, CI runs template guard + both acceptances Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three view filters over the editor: collapse tabs off the sidebar rail (persisted per browser), toggle which label::xx columns render on each row card, and drop hidden/calculate rows from the form preview. View-only; no project data changes. A Playwright fixture seeds an empty hidden-tabs list so specs keep driving the full rail.
What a hosted deployment needs from its runtime: exactly one instance, a durable DATA_ROOT, response buffering off for the minutes-long build-log stream, resource sizing measured on a running instance, and the check that proves the container is really in hosted mode. Also corrects the hosted-authoring plan, which still presented the abandoned Vercel-plus-separate-API split as the architecture, and ignores future plan docs: they are design conversations that go stale the moment the work ships.
…on picker and the preview PO demo finding (2026-09-17). Hiding a language in the chip bar hid it on the row cards only; the add-question picker and the preview pane still offered it. The hidden set now lives in FormEditor and is threaded to SurveyTab, the picker (which asks for a label only in the languages on show — every active locale is still seeded '' on commit, so no column is dropped) and FormPreview (whose language tabs follow it, falling back to all when everything is hidden). Regression test in form-editing.spec.ts: a hidden `ne` is offered by neither the picker nor the preview, and comes back when shown again. The other two demo findings (Bikram Sambat retype, generate saves the hierarchy first) are not hosting work and live on `po-demo-feedback` off master. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the new picker and seeds every tab visible Three things the hosted-authoring branch had left red or wrong in desktop mode: - POST /api/project/close had become a no-op, and a request with no project id fell back to the most recently opened project — so "Change project" then reload landed straight back inside the project, and every spec that closes the project to reach the picker got the editor instead. The registry now records `lastOpenedId`; open/register set it, close and forget clear it, and the no-id fallback resolves it (null = closed → picker; absent on a registry written before this field → most recent, as before). Sidebar's close calls the route again. Hosted mode has no fallback, so there it stays a no-op. Five new acceptance assertions cover close → picker → re-open. - Seven build/deploy specs clicked the old "Create new project…" button; the picker now offers "From a template…". Template labels go back to "Empty project" / "Blank project" — the specs (and users) know them by those names; the "Start blank" button on the picker still preselects the empty template. - The collapsible-sidebar commit hides Tasks / Contact summary / Translations / Decisions / Standard codes for a fresh browser profile and seeded an empty hidden list only through setup.ts — specs importing `test` from '@playwright/test' (anc-build-deploy among them, in CI) clicked a nav item that was not rendered. playwright.config.ts now seeds the same key via storageState for every context, whichever way a spec imports `test`. CI allowlist on this branch: 40 of 41 desktop e2e green plus 4 of 4 hosted; condition-builder.spec.ts (9 of 11 red) fails identically on the branch tip before this change and is not addressed here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hosted Mode for CHT UI Builder
This pull request introduces a comprehensive hosted mode for the CHT UI Builder, enabling multi-user, browser-based CHT app authoring without requiring any local development setup.
Users can now sign up, create or import projects, build and edit CHT apps, and export or deploy their projects entirely through the browser. Hosted mode includes production-ready Docker deployment, authentication, per-user project isolation, session management, a focused first-run experience, and comprehensive end-to-end and acceptance test coverage.
Key Changes
1. Hosted Mode: Server, Client, and Deployment
Added a production-ready
Dockerfilethat builds and runs the complete application stack, including:pyxformgitmomentThe client is served from the same origin as the API, providing a self-contained deployment.
The Docker image also includes:
Additional deployment changes:
.dockerignoreto exclude development and build artifacts.momenttotemplates/cht-default/package.json.momentglobally at/node_modules, allowing projects under/datato resolve it without runningnpm install.cht-defaulttemplate.README.mdwith hosted-mode Docker instructions.docs/deployment-requirements.mddocumenting runtime and deployment requirements.2. Server: Modes, Authentication, and Per-User State
Application modes
config.tsintroducesCHT_UI_MODEto distinguish between:In hosted mode, filesystem paths are not exposed to the client.
Relevant files:
server/src/config.tsserver/src/routes/browse.tsAuthentication
Added
auth.tswith email/password authentication.The implementation includes:
scryptpassword hashingonRequesthookEvery authenticated request is associated with a user identity.
Relevant file:
server/src/auth.tsPer-user project state
state.tshas been redesigned around a per-user project registry.Every project-scoped request now carries:
The previous
getProjectPath()/setProjectPath()approach has been removed.This required rethreading project state through 31 call sites across 10 route files.
The result is:
Relevant files:
server/src/state.tsProject transfer
Added
routes/transfer.tssupporting:Relevant file:
server/src/routes/transfer.ts3. Client: Authentication, Sessions, and Project Isolation
The client API layer has been refactored to support hosted-mode sessions and project isolation.
client/src/api.tsnow handles:Storage is split by purpose:
localStorage— browser-level authentication/session tokensessionStorage— project ID for each browser tabAll API requests automatically include the appropriate:
x-project-idThis allows the same browser to work on different projects in different tabs without mixing project state.
Application state
App.tsxnow:Relevant file:
client/src/App.tsxHosted UI
Added:
SignIn — sign-up, login, and authentication flow.
ProjectPicker — project management interface with:
ProjectTransfer — ZIP download and branch push functionality.
4. First-Run Experience: Focus on Forms
Hosted users now get a more focused first-run experience.
Previously, every available CHT UI Builder tab was visible immediately, which exposed more of the product than a new author typically needs.
A new Tabs panel allows secondary tabs to be collapsed from the sidebar.
By default, a new user sees:
The following are hidden by default:
Nothing is removed from the project. Users can enable any hidden tab through the Tabs panel.
The visibility preference is persisted per browser.
Relevant files:
Sidebar.tsxstate/store.tsProjectOverview.tsxLanguage filter
Added a language chip bar to the form editor.
Users can toggle which
label::xxlanguage columns are displayed for each row.This is particularly useful for configurations such as
moh-nepal, which contains seven locales.The filter is view-only and does not modify the underlying form.
Form preview
Added a form-preview toggle that hides:
This is also view-only and does not modify the underlying XLSForm.
Relevant files:
FormEditor.tsxFormPreview.tsx5. Testing and CI
Hosted mode includes dedicated acceptance and end-to-end testing.
Playwright hosted tests
Added:
The hosted Playwright configuration:
The existing Playwright configuration now excludes hosted-mode tests.
A test fixture seeds an empty hidden-tab list so existing tests continue to exercise the complete sidebar.
Relevant files:
Hosted API acceptance tests
Added:
This runs 43 API-level assertions covering both desktop and hosted modes, including:
Template validation
Added:
Every starter template is now:
using the real CHT toolchain.
This ensures templates remain usable rather than only being structurally valid.
CI
CI has been extended to:
Relevant file:
Outcome
Together, these changes provide a secure, isolated, multi-user hosted authoring environment for CHT UI Builder.
A hosted deployment can now provide users with a browser-based workflow where they can:
pyxform,cht-conf, Git, or other local dependencies.The Docker image packages the required toolchain, while the authentication and project registry architecture keeps users and projects isolated from one another.
The result is a production-oriented hosted deployment model for CHT UI Builder rather than simply a browser wrapper around the existing local development workflow.