Skip to content

11 cht UI builder test hosting - #13

Merged
PrjShrestha merged 9 commits into
masterfrom
11_cht-ui-builder-test-hosting
Sep 30, 2026
Merged

PrjShrestha merged 9 commits into
masterfrom
11_cht-ui-builder-test-hosting

Conversation

@PrjShrestha

Copy link
Copy Markdown
Collaborator

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 Dockerfile that builds and runs the complete application stack, including:

  • Server and client
  • Node.js and Python
  • pyxform
  • git
  • moment
  • CHT configuration tooling

The client is served from the same origin as the API, providing a self-contained deployment.

The Docker image also includes:

  • Healthcheck
  • Persistent data volume
  • Complete runtime toolchain

Additional deployment changes:

  • Updated .dockerignore to exclude development and build artifacts.
  • Added moment to templates/cht-default/package.json.
  • The Docker image installs moment globally at /node_modules, allowing projects under /data to resolve it without running npm install.
  • This fixes compilation of the previously failing cht-default template.
  • Updated README.md with hosted-mode Docker instructions.
  • Added docs/deployment-requirements.md documenting runtime and deployment requirements.

2. Server: Modes, Authentication, and Per-User State

Application modes

config.ts introduces CHT_UI_MODE to distinguish between:

  • Desktop mode — retains the existing single implicit user and filesystem folder browser.
  • Hosted mode — removes the filesystem browser entirely.

In hosted mode, filesystem paths are not exposed to the client.

Relevant files:

  • server/src/config.ts
  • server/src/routes/browse.ts

Authentication

Added auth.ts with email/password authentication.

The implementation includes:

  • scrypt password hashing
  • Bearer-token authentication
  • SHA-256 token hashing at rest
  • 30-day sessions
  • Automatic request authentication through an onRequest hook

Every authenticated request is associated with a user identity.

Relevant file:

  • server/src/auth.ts

Per-user project state

state.ts has been redesigned around a per-user project registry.

Every project-scoped request now carries:

x-project-id

The previous getProjectPath() / setProjectPath() approach has been removed.

This required rethreading project state through 31 call sites across 10 route files.

The result is:

  • No filesystem paths cross the API boundary.
  • Each user has an isolated project registry.
  • Users can have multiple projects.
  • Multiple projects can be opened simultaneously in different browser tabs.

Relevant files:

  • server/src/state.ts
  • 10 project-related route files

Project transfer

Added routes/transfer.ts supporting:

  • Git clone/import
  • Branch push/export
  • ZIP import
  • ZIP export
  • Per-project size limits
  • ZIP-slip protection

Relevant file:

  • server/src/routes/transfer.ts

3. Client: Authentication, Sessions, and Project Isolation

The client API layer has been refactored to support hosted-mode sessions and project isolation.

client/src/api.ts now handles:

  • Sign up
  • Login
  • Logout
  • Session tokens
  • Per-tab project selection

Storage is split by purpose:

  • localStorage — browser-level authentication/session token
  • sessionStorage — project ID for each browser tab

All API requests automatically include the appropriate:

  • Authentication token
  • x-project-id

This allows the same browser to work on different projects in different tabs without mixing project state.

Application state

App.tsx now:

  • Handles authentication/session state.
  • Requires sign-in when hosted mode is enabled.
  • Manages project selection.
  • Maintains project isolation per tab.
  • Handles unauthorized responses globally and forces re-authentication when required.

Relevant file:

  • client/src/App.tsx

Hosted UI

Added:

  • SignIn — sign-up, login, and authentication flow.

  • ProjectPicker — project management interface with:

    • Open
    • Delete
    • Start blank
    • Create from template
    • Import from Git
    • Import from ZIP
  • 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:

  • Overview
  • Hierarchy
  • Forms
  • Deploy

The following are hidden by default:

  • Tasks
  • Contact Summary
  • Translations
  • Decisions
  • Standard Codes

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.tsx
  • state/store.ts
  • ProjectOverview.tsx

Language filter

Added a language chip bar to the form editor.

Users can toggle which label::xx language 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:

  • Hidden rows
  • Calculate rows

This is also view-only and does not modify the underlying XLSForm.

Relevant files:

  • FormEditor.tsx
  • FormPreview.tsx

5. Testing and CI

Hosted mode includes dedicated acceptance and end-to-end testing.

Playwright hosted tests

Added:

client/playwright.hosted.config.ts
client/tests/hosted-authoring.spec.ts

The hosted Playwright configuration:

  • Starts the server in hosted mode.
  • Uses a throwaway data root.
  • Serves the built client from the same origin.
  • Runs dedicated hosted-mode authoring tests.

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:

client/playwright.config.ts
client/tests/setup.ts

Hosted API acceptance tests

Added:

scripts/hosted-acceptance.mjs

This runs 43 API-level assertions covering both desktop and hosted modes, including:

  • Two-user authentication
  • User isolation
  • Project isolation
  • Route-level access control
  • ZIP import/export
  • ZIP-slip protection

Template validation

Added:

scripts/validate-templates.mjs

Every starter template is now:

  1. Compiled
  2. Converted
  3. Validated

using the real CHT toolchain.

This ensures templates remain usable rather than only being structurally valid.

CI

CI has been extended to:

  • Build the server.
  • Build the client for hosted mode.
  • Run hosted acceptance tests.
  • Run existing acceptance tests.
  • Run hosted Playwright tests.
  • Validate every starter template.

Relevant file:

.github/workflows/ci.yml

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:

  1. Create an account.
  2. Create or import a CHT project.
  3. Configure forms, hierarchies, and other CHT components.
  4. Work on multiple projects independently.
  5. Compile and validate configurations using the real CHT toolchain.
  6. Export projects as ZIP files or push branches to Git.
  7. Work without installing Node.js, Python, 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.

PrjShrestha and others added 9 commits September 4, 2026 10:29
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>
@PrjShrestha
PrjShrestha merged commit 29bac11 into master Sep 30, 2026
4 checks passed
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