Skip to content

Latest commit

 

History

History

flip-ui

README.md

FLIP Front-End

flip-ui

The flip-ui is the web-based user interface for the FLIP platform. It provides researchers and administrators with a portal to manage projects, monitor federated learning jobs, inspect cohort query results, and retrieve trained models.

Deployment

Local development

The flip-ui runs as a Docker container (Vite dev server) in the full local stack:

make ui            # starts the UI only

or as part of the full platform:

make up

The UI is accessible over plain HTTP at the port defined by UI_PORT in .env.development.example: http://localhost:<UI_PORT>. Only a port the local Keycloak realm registers as a browser origin works — take one from 44350–44359, since the shipped 443 is registered for https only and the dev server serves no TLS. See deploy/keycloak/README.md.

Staging and production (AWS)

The UI is served as static assets from S3 behind CloudFront at the canonical subdomain (stag.flip.aicentre.co.uk / app.flip.aicentre.co.uk). CloudFront relays /api/* to the internal NLB's web listener, so users and trusts keep hitting the same URLs. There is no flip-ui Docker container on stag/prod EC2 — only flip-api and the FL services. See deploy/providers/AWS/README.md for the full rollout.

Deploy from the working tree with:

cd deploy/providers/AWS
make deploy-ui HUB=stag     # or HUB=prod for prod

This runs npm ci && npm run build:deploy (whose postbuild:deploy writes dist/js/window.js from the target environment's values) and publishes dist/ to the UI bucket with scripts/publish-ui.sh. Nothing is invalidated: index.html and window.js are served no-cache. CI runs the same script after every successful apply (publish_ui.yml, FLIP#1186); make deploy-ui is the manual fallback and rollback.

Public Ark+ demo build (/ark_demo)

A read-only, fully-offline demo of the Ark+ chest X-ray experiment register, safe to host unauthenticated. It is the real SPA built with --mode demo (VITE_DEMO inlined to true): the auth guard is bypassed, no Amplify/Cognito call is ever made, and every API request is answered by an in-browser MirageJS server (mocks/demo-server.ts) with no passthrough — a published demo bundle cannot reach a real backend. The replayed payloads live under mocks/demo/data/; capture provenance and sanitisation rules are documented in mocks/demo/ark-plus-register.ts.

npm run demo          # dev server at http://localhost:4174/ (base "/")
npm run build:demo    # production bundle under dist/ with base "/ark_demo/"
npm run serve:demo    # preview the built bundle at /ark_demo/

The build regenerates dist/js/window.js from scripts/generate-demo-window-js.sh, which reads no environment and points at no real backend. Host the bundle as a CloudFront behaviour (or any static prefix) at /ark_demo/ next to the real app.

The demo is a point-in-time capture, not a live system, and says so twice: window.RELEASE_VERSION is stamped ark-demo/<capture-date>+<git-short-sha> (the date is grepped out of DEMO_CAPTURE_DATE in ark-plus-register.ts so it can't drift from the provenance note there), and every page renders a small, non-dismissible banner (src/demo/DemoBanner.vue, mounted in App.vue outside <router-view> so it survives every route) reading the same capture date in human form.

The large download bundles (results + model files, several hundred MB each) are not part of the bundle: they are served by the production CloudFront distribution at /ark_demo/assets/* from a private, OAC-locked S3 bucket with the WAF in the request path (Terraform: deploy/providers/AWS/cloudfront.tf, "Public Ark+ demo download assets"; bucket name via DEMO_ASSETS_BUCKET_NAME). Nothing in the demo is fetched from a public S3 URL.

Configuration

The flip-ui reads its runtime configuration from window.* globals set by public/js/window.js. That file is gitignored and generated from environment variables by scripts/generate-window-js.sh:

  • In local dev: generated automatically by the predev npm hook (before npm run dev), reading the VITE_* vars that docker-compose passes into the container.
  • In stag/prod: generated by make deploy-ui before the S3 sync, reading the values sourced from .env.stag / .env.prod.

No environment-specific Cognito IDs or API URLs live in the repo.

The UI reads its config at runtime from window.* (generated into dist/js/window.js by scripts/generate-window-js.sh from the env at dev-start or deploy time). The same env vars drive the rest of the stack, so there is no VITE_-prefixed duplication to keep in sync.

Variable Description
AUTH_BACKEND Identity provider the hub runs: cognito (default when unset — stag/prod) or keycloak (the dev stack). Must match flip-api's AUTH_BACKEND.
AWS_COGNITO_USER_POOL_ID Cognito User Pool ID (required when AUTH_BACKEND=cognito)
AWS_COGNITO_APP_CLIENT_ID Cognito App Client ID (required when AUTH_BACKEND=cognito)
AWS_REGION AWS region hosting the pool (default eu-west-2)
KEYCLOAK_PUBLIC_URL Base URL of Keycloak as the browser reaches it, e.g. http://localhost:8081 (required when AUTH_BACKEND=keycloak; emitted as window.KEYCLOAK_URL)
KEYCLOAK_REALM Keycloak realm (required when AUTH_BACKEND=keycloak)
KEYCLOAK_CLIENT_ID Keycloak public client the UI signs in with (required when AUTH_BACKEND=keycloak)
CENTRAL_HUB_API_URL Full base URL of the flip-api backend, including /api
BLACKLISTED_MODEL_FILES Comma-separated file names to reject in model uploads
VITE_LOCAL Set to true for local mock mode (bypasses Cognito). Local dev only — see warning below.
VITE_DEMO Selects the public Ark+ demo bundle (bypasses Cognito, offline Mirage API). Never set this in the environment — see warning below; it is inlined automatically by npm run build:demo / npm run demo (--mode demo).

Authentication sits behind a provider seam (src/auth/, FLIP#919): the store, router guard, axios interceptor and auth pages speak only the AuthProvider contract in src/auth/provider.ts, and getAuthProvider() picks the implementation once at boot from window.AUTH_BACKEND.

  • Cognito (src/auth/cognito-provider.ts, stag/prod) — AWS Cognito through aws-amplify, the only file allowed to import it (enforced by the flip-ui/no-amplify-outside-provider ESLint block). A valid Cognito User Pool and Client ID are required for a production deployment. Every capability is on: temporary- password change, TOTP challenge and in-app enrolment, code-by-email password reset, admin-triggered reset, global sign-out.
  • Keycloak (src/auth/keycloak-provider.ts, the dev stack) — the OIDC Resource Owner Password grant against the realm's token endpoint, no SDK and no redirect, so the same username/password form works. Tokens live in localStorage["flip.auth.keycloak.<clientId>"] and refresh 30 s ahead of expiry. Its capabilities are off by design, except the admin "Reset Password" (flip-api re-sends the user's invite): the password grant cannot run Keycloak's required actions (forced password update, TOTP enrolment are browser flows in Keycloak's own UI), and Keycloak answers a wrong password and a missing OTP with the same invalid_grant, so an in-app TOTP step cannot be offered honestly. The pages link out to the Keycloak console instead ("Forgot password?" on the login page), and an account with a pending required action gets a "Finish setting up your account in Keycloak" notice with a link.

VITE_LOCAL=true and VITE_DEMO=true must never reach a production build. Both flags are inlined by Vite at build time and short-circuit the Cognito session check in src/utils/auth.ts plus enable a MirageJS mock in src/main.ts — so a build carrying either ships an unauthenticated app. Every npm run build* therefore refuses to proceed if either flag is set to true in the environment (enforced by scripts/check-build-flags.mjs and a belt-and-braces check in vite.config.mts). Use VITE_LOCAL only with npm run dev against a mocked API, and never set VITE_DEMO at all — the demo build gets it from --mode demo. Keep both out of CI and deploy environments.

Dependency scoping

Production source may not import a package that is only declared in devDependencies, nor one that is declared nowhere and resolves through npm's hoisting of a parent's tree. "Production source" is everything the deployed bundles reach: src/, minus its test files, plus mocks/ — which build:demo pulls into the public /ark_demo bundle.

This is enforced, not just documented: the flip-ui/dependency-scoping block in eslint.config.mjs runs import-x/no-extraneous-dependencies over exactly that file set, so npm run lint fails in CI the moment an import lands in the wrong stanza.

The rule exists for Dependabot, not the build. vite build tree-shakes from the entry points and bundles whatever they reach whichever stanza a package sits in, and every install path in the repo (npm ci in CI, the Dockerfile, make deploy-ui) installs both stanzas — so a wrong stanza breaks nothing and is invisible until it matters. What it changes is the scope label on a security alert: a package that ships to users but sits in devDependencies produces an alert labelled "Development", which reads as not in the production bundle and invites a wrongly-dismissed alert on code CloudFront is serving. FLIP#1041 corrected 19 packages: 16 moved out of devDependencies, two that were declared nowhere at all (codemirror, tippy.js — they resolved only by hoisting), and husky, which had drifted the other way. Correct scoping is also the precondition for ever adopting npm ci --omit=dev here.

Note the rule keys off the import graph, not the bundle: a package imported by production source is a dependency even if tree-shaking currently drops it. What production code imports is stable, whereas what survives tree-shaking silently flips the correct answer whenever an unrelated component starts or stops being used. (@popperjs/core was the worked example of this until FLIP#1063 removed its only importer along with the dead AiSelect/AiChipSelect components, at which point it left the manifest entirely — the honest resolution, and the one a lint rule cannot reach, since nothing flags an import that is merely unreachable.)

Two traps when auditing this by hand:

  • A from "..." grep under-reports. highlight.js is loaded lazily via import("highlight.js/lib/core") and import("highlight.js/lib/languages/json") in src/utils/highlightJson.ts, and is correctly a dependency despite having no static import. Test files also live under src/ (most under __tests__/ directories, plus two flat *.spec.ts and the src/unit-tests/ tree), and their imports — vitest, @vue/test-utils, @pinia/testing — are genuinely dev-only.
  • A static import of a side-effecting module is never dropped, even from a branch that folds. Neither mock server may be imported statically from src/main.ts; both are loaded through dynamic import() inside their own folded branch. mocks/server was static until FLIP#1041 and shipped ~230 modules — Mirage, Pretender, route-recognizer, inflected and all of lodash — in the production entry chunk of every build, because miragejs patches Error.prototype at module scope. Making it dynamic cut the entry chunk from 331 KB to 182 KB. See the comment above bootstrap(). This is enforced twice, because miragejs is a legitimate dependency (the demo bundle ships it) and so invisible to the scoping rule above: the flip-ui/no-static-mirage block in eslint.config.mjs fails npm run lint on any static import of miragejs/pretender/the mock servers from src/ (dynamic import() stays legal), and scripts/assert-no-demo-artefacts.mjs carries Mirage/Pretender sentinels so the built artefact is checked too — the only guard that also catches a folded branch that stops folding.

Testing

Unit tests (Vitest)

cd flip-ui
npm install
npm run test:unit          # one-off run with coverage
npm run test:unit:watch    # watch mode while iterating

Unit tests live alongside source under src/**/*.spec.ts and run in jsdom with Pinia / Vue Router stubs.

End-to-end tests (Cypress)

The Cypress suite under test/cypress/integration/ drives the UI in Chrome against a stubbed API — every backend call is intercepted at the network layer in test/cypress/support/globalIntercepts.ts and Cognito auth is short-circuited in test/cypress/support/cognito.ts by writing a fixture token straight into localStorage. The suite needs no real backend, no AWS account and no Cognito user pool.

Specs are split into six groups (group-1 … group-6) so CI can run them in parallel. The split is by file, not by feature — it's purely a sharding device for the matrix job, balanced so the slowest single shard (rather than the sum of all specs) sets the wall-clock time. There's no semantic difference between adding a new spec to one group versus another; pick the group with the fewest existing specs (or the shortest total runtime).

Run the full suite locally

cd flip-ui
npm install
# One-shot env file. Values are dummies — the suite mocks Cognito + the API,
# so nothing here ever leaves the box. Match the values used in CI.
#
# It MUST be `.env.e2e`, never `.env.development`: VITE_E2E arms the Cypress
# auth seam (src/utils/auth.ts), which takes the signed-in user from a
# `cypress.auth.user` localStorage key instead of Cognito. Vite loads
# `.env.development` for `npm run dev` / `make ui` too, so putting the flag
# there makes the interactive dev server bounce every page refresh to
# /auth/login while your real Cognito tokens sit unused in localStorage.
# `vite.config.mts` now refuses to start in any mode but `e2e` with the flag
# set, so a misplaced file fails loudly instead of wasting an afternoon.
cat > .env.e2e <<'EOF'
VITE_LOCAL=false
VITE_E2E=true
CENTRAL_HUB_API_URL=http://localhost:8080
AWS_COGNITO_USER_POOL_ID=eu-west-2_DUMMY1234
AWS_COGNITO_APP_CLIENT_ID=dummyclientid1234567890ab
AWS_REGION=eu-west-2
BLACKLISTED_MODEL_FILES=flip.py,server_app.py
EOF
npm run test:ci            # boots the dev server on :4173 and runs cypress

test:ci uses start-server-and-test to start the Vite dev server (npm run test:start, plain HTTP on http://localhost:4173, --mode e2e so it reads .env.e2e) and then runs cypress run --browser chrome against it. The non-privileged port 4173 avoids the need to run with sudo on Linux/macOS and matches the port used in CI.

To open the Cypress GUI for interactive debugging:

npm run test:start &       # in one shell — leave running
npx cypress open           # in another — pick a spec

Typecheck the test tree

npm run test:types         # tsc --noEmit -p test/tsconfig.json

The Cypress tree has its own TS project (test/tsconfig.json) that npm run lint (eslint over src/ only) never sees, so CI runs this as a separate step to keep the custom-command declarations honest.

Run a single group

# with the dev server running (npm run test:start &), then in another shell:
npx cypress run --browser chrome --spec 'test/cypress/integration/group-3/**/*.spec.ts'

Run via Docker (no host Chrome / Xvfb required)

On Linux without xvfb installed, or anywhere you'd rather not install Chrome locally, run Cypress out of the official cypress/included image. The dev server stays on the host (so hot-reload still picks up your edits); the container reaches it via --network host.

cd flip-ui
make e2e_test               # full suite, end-to-end (boots Vite, runs cypress in docker)

# Or, if you already have npm run test:start running in another shell:
docker run --rm --network host \
    -v "$PWD":/e2e -w /e2e --entrypoint cypress \
    cypress/included:15.21.0 \
    run --browser electron \
    --spec 'test/cypress/integration/group-3/**/*.spec.ts'

The image (cypress/included:15.21.0) is ~3 GB on first pull and cached afterwards. Pin the tag to whatever cypress version is in package.json so the binary in the image matches the project config.

Demo-video recording (live stack)

The end-to-end demo video is not recorded with Cypress: make demo-video (repo root) drives the live dev stack with Playwright from flip-api/tests/demo_video.py. Only its final concatenation lives here, scripts/assemble-demo-video.sh. See flip-api/AGENTS.md ("Demo Video Recorder").

CI

The Cypress suite runs on every PR and on push to develop / main via the cypress-e2e job in .github/workflows/test_flip_ui.yml. The job:

  • Uses cypress-io/github-action@v7, which caches the Cypress binary and node_modules between runs.
  • Fans out across the six spec groups via a strategy.matrix.group so wall-clock time stays short.
  • Boots the Vite dev server with the same .env.e2e stub shown above.
  • On failure, uploads test/cypress/screenshots as an artefact (cypress-screenshots-<group>, retained 7 days).

Running the suite against the real backend stack is out of scope for the in-repo CI — that's the nightly E2E job tracked separately. Anything in this suite must be runnable purely from fixtures.

Further Reading

License

FLIP is Apache 2.0 licensed. See the LICENSE file for details.