Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

117 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SNT Pipeline Webapps

This repository contains everything needed to build and deploy OpenHEXA static webapps that let users trigger SNT pipeline runs from a browser β€” no code required.

The active product is the SNT Pipelines Orchestrator β€” one webapp per workspace that presents all official SNT pipelines (~18, from the snt_development repo) as a single guided surface with a configuration/run sidebar. It ships in two UI variants: flowchart (an interactive 2D node/edge map) and cockpit (a one-step-at-a-time guided walkthrough, and the variant where new functionality lands first). The earlier single-pipeline webapps were the stepping stones toward it and now live in archive/ for reference only.

Webapps call the OpenHEXA GraphQL API via a same-origin proxy to start pipeline runs and poll their status. A simple webapp can be a single index.html; the orchestrator is a multi-file bundle (index.html + styles.css + app.js + JSON data files) served as-is from the same origin.

The deployed bundle is identical for every workspace. What each workspace contributes β€” the list of pipelines installed there, with their UUIDs and parameters β€” lives in that workspace's own file storage as a pipeline_cards.json, generated by the companion create_pipeline_cards pipeline and read by the app at page load. So changing the configuration means running a pipeline, not redeploying the webapp.

Agent-driven repo. This project is built with Claude Code. The authoritative, detailed instructions live in CLAUDE.md β€” the agent reads it at session start. This README is the human-facing overview; when the two disagree, CLAUDE.md wins.


Repository structure

/
β”œβ”€β”€ CLAUDE.md                        # Agent instructions (read by Claude Code at session start)
β”œβ”€β”€ README.md
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .claude/                         # Claude Code config (shared: settings + guardrail hook)
β”‚   β”œβ”€β”€ settings.json
β”‚   └── hooks/block-destructive-git.js   #   Hard-blocks destructive git commands
β”‚
β”œβ”€β”€ app/                             # One generic bundle per UI variant β€” shared by ALL workspaces
β”‚   β”œβ”€β”€ flowchart/                   #   2D node/edge flow-diagram map. English only
β”‚   β”‚   β”œβ”€β”€ index.html               #     Minimal shell
β”‚   β”‚   β”œβ”€β”€ styles.css               #     All styling
β”‚   β”‚   β”œβ”€β”€ app.js                   #     All logic (renders map, merges cards, runs/polls pipelines)
β”‚   β”‚   └── pipeline_map.json        #     Hand-authored map (layout + dependency edges), this variant only
β”‚   β”œβ”€β”€ cockpit/                     #   Guided one-step-at-a-time walkthrough. Bilingual EN|FR; embeds HTML reports
β”‚   β”‚   β”œβ”€β”€ index.html               #     Same 4-file shape as flowchart β€” variant owns its own copies
β”‚   β”‚   β”œβ”€β”€ styles.css
β”‚   β”‚   β”œβ”€β”€ app.js                   #     Also holds the I18N string table (app chrome, EN + FR)
β”‚   β”‚   └── pipeline_map.json        #     May drift from flowchart's map; `label`s are {en, fr}
β”‚   └── pipeline_descriptions.json   #   Shared node descriptions β€” ONE copy, every variant AND workspace; values are {en, fr}
β”‚
β”œβ”€β”€ utils_pipelines/                 # OpenHEXA pipelines that support the webapp itself
β”‚   └── create_pipeline_cards/       #   Generates each workspace's pipeline_cards.json in its bucket
β”‚       β”œβ”€β”€ pipeline.py              #     The pipeline
β”‚       β”œβ”€β”€ config.py                #     GraphQL queries, type map, output paths
β”‚       └── README.md                #     What it does, parameters, output location, caveats
β”‚
β”œβ”€β”€ schemas/                         # Machine-readable contracts / references
β”‚   β”œβ”€β”€ pipeline_map.schema.json          #   Schema for app/<variant>/pipeline_map.json
β”‚   β”œβ”€β”€ pipeline_cards.schema.json        #   Output contract for the generated pipeline_cards.json
β”‚   β”œβ”€β”€ pipeline_descriptions.schema.json #   Schema + Markdown-lite rules for pipeline_descriptions.json
β”‚   └── schema.generated.graphql          #   OpenHEXA GraphQL schema β€” query reference for agents
β”‚
β”œβ”€β”€ docs/                            # Consolidated knowledge (stable)
β”‚   β”œβ”€β”€ PRODUCT_SPEC.md              #   Product spec: functionality, UI variants, v0/v1/v2 roadmap
β”‚   └── personas/                    #   UX persona / discovery questionnaires
β”‚
β”œβ”€β”€ design/                          # WIP / design explorations (not contracts)
β”‚   β”œβ”€β”€ wireframes/                  #   UX/visual targets for the orchestrator:
β”‚   β”‚   β”œβ”€β”€ orchestrator_wireframe.html               #     flowchart variant
β”‚   β”‚   β”œβ”€β”€ orchestrator_wireframe_cockpit.html       #     cockpit variant (the live target)
β”‚   β”‚   β”œβ”€β”€ orchestrator_wireframe_cockpit_narrative.html  #  narrative layer β€” PARKED (v2)
β”‚   β”‚   └── orchestrator_frosted.html                 #     visual-style exploration
β”‚   β”œβ”€β”€ grid_editor.html             #   Interactive map-layout editor
β”‚   β”œβ”€β”€ pipeline_map_preview.html    #   Standalone visual render of pipeline_map.json (for review)
β”‚   └── pipeline_map_20260625.png    #   Map sketch (Whimsical)
β”‚
└── archive/                         # Retired spikes & pre-orchestrator single-file apps (reference only)
    β”œβ”€β”€ snt-app-dev/report-embed/    #   Report-iframe feasibility spike (since built into cockpit)
    β”œβ”€β”€ snt-testing/                 #   dhis2_reporting_rate, population_transformation(_split), status_spike
    └── snt-drc-workshop-demo/       #   Older flat single-file webapp

(A local-only ignore/ folder is git-ignored personal scratch β€” not part of the project.)

Generic artifacts live in app/<variant>/ (one bundle per UI variant) plus schemas/ / docs/ / design/. Cross-variant shared text lives in app/pipeline_descriptions.json β€” one hand-authored copy of every node's description, deployed unchanged into both variants' bundles. See "UI variants" below for why there's more than one app/ subfolder.

Nothing workspace-specific is committed here. There used to be a workspaces/ folder holding one pipeline_cards.json per workspace per variant; it was removed on 2026-08-04 when both variants started reading that catalog straight from the workspace's own file storage. Adding a new workspace now means deploying the same bundle and running create_pipeline_cards there β€” no new repo file at all.

No stored webapp config either. Webapp identity and scopes (id, slug, URL, allowed operations) are resolved live from the OpenHEXA API (list_static_webapps / get_static_webapp) at deploy time β€” the repo no longer keeps a workspace_config.json.


UI variants

The orchestrator can exist as more than one independently-deployable UI variant β€” same pipeline data, different layout/UX. Each variant is a fully self-contained bundle under app/<variant>/ (including its own pipeline_map.json β€” variants do not share files, by design, so each UI can evolve independently).

Variant Status What it is
flowchart Production β€” 3 workspaces Interactive 2D node/edge map + config/run sidebar. English only; links out to HTML reports
cockpit Production β€” 2 workspaces; v1 lead Focused, one-step-at-a-time guided walkthrough. Bilingual EN|FR, and embeds HTML reports in-app. Target UX: design/wireframes/orchestrator_wireframe_cockpit.html

New functionality generally lands in cockpit first (it's the v1 lead variant), so the two variants are deliberately not feature-equal β€” see the feature notes above.

Deploying a given workspace + variant combination is 5 files β€” 4 generic (app/<variant>/*) + 1 cross-variant shared (app/pipeline_descriptions.json, same content in both variants' bundles). There is no workspace-specific file: the bundle is byte-identical in every workspace, and the per-workspace pipeline catalog is read at runtime from the workspace's own file storage.

Since webapp identity isn't stored in the repo, live webapps are told apart by slug β€” snt-pipelines-orchestrator (flowchart) vs snt-pipelines-orchestrator-cockpit (cockpit). Slugs are consistent everywhere; names are not β€” snt-app-dev and snt-testing use the SNT Pipelines Orchestrator - Flowchart / - Cockpit convention, but cmr-snt-process is still named the bare SNT Pipelines Orchestrator.


The data architecture (orchestrator)

The orchestrator separates concerns across four kinds of file. The stable join key everywhere is the node id == the pipeline's Python function name (e.g. snt_dhis2_extract).

File Scope Holds
app/<variant>/pipeline_map.json per-variant, workspace-independent all nodes, labels, grid position, type, mutex group, directed edges (deps)
app/pipeline_descriptions.json shared across every variant AND workspace hand-authored, Markdown-lite description per node, keyed by id, as {en, fr}
pipeline_cards.json β€” in the workspace's file storage, not this repo per-workspace, shared by both variants which pipelines exist + uuid + parameters (drives active vs greyed)
app/<variant>/index.html + app/<variant>/app.js + app/<variant>/styles.css per-variant app shell (multi-file) renders the UI, merges map + descriptions with cards, runs/polls pipelines

The map is identical across all workspaces for a given variant β€” every orchestrator shows the same full diagram. What differs per workspace is only which nodes are active: a node is available iff its id appears in that workspace's pipeline_cards.json. Pipelines not present render greyed-out and unclickable. The map is hand-authored (validated against schemas/pipeline_map.schema.json), not generated from the API.

Where the pipeline catalog comes from

pipeline_cards.json is the one piece of per-workspace data, and it is not in this repo. Each workspace holds its own copy in its file storage at

utils_pipelines/create_pipeline_cards/pipeline_cards/pipeline_cards.json

written there by the create_pipeline_cards pipeline, which lists the workspace's live pipelines, matches them against the deployed map, and reads each one's current parameters. The webapp fetches it at page load through a signed URL (using the FILES_READ scope it already has for report downloads).

Why it's set up this way:

  • Config changes need no redeploy. Install a pipeline, or change a parameter, then re-run create_pipeline_cards β€” the app picks it up on the next refresh.
  • Parameters match what's installed. They're read from each pipeline's deployed version in that workspace, not scraped from GitHub main, so they can't drift from the version actually there.
  • One catalog per workspace, shared by both variants β€” the two maps declare the same nodes, so there's no reason to generate it twice.
  • It fails loudly if absent. A workspace where the generator has never run shows a clear "run create_pipeline_cards" message instead of a half-working app. ⚠️ That means every workspace needs the generator installed and run once before its orchestrator works. Done in snt-app-dev and snt-testing; not yet in cmr-snt-process, whose already-deployed flowchart app keeps running but would need it before the next redeploy.
  • Each run archives the previous catalog under …/pipeline_cards/historical/ with a timestamp, so you can see what changed.

Generic vs workspace-specific (what to reuse)

For a given variant, the deployed bundle is 5 files: 4 generic + 1 cross-variant shared β€” and nothing workspace-specific. This is what makes the orchestrator portable: a new workspace reuses the whole bundle byte-for-byte and supplies its own catalog out-of-band, by running a pipeline.

File Generic / Shared Notes
index.html Generic (per variant) Empty page shell β€” identical across workspaces for this variant.
styles.css Generic (per variant) All styling β€” no workspace details.
app.js Generic (per variant) All logic β€” zero hardcoded workspace specifics (see caveat below).
pipeline_map.json Generic (per variant) The SNT process map for this variant β€” same in every workspace, but NOT shared with other variants.
pipeline_descriptions.json πŸ” Shared Node description text (bilingual {en, fr}) β€” one repo copy under app/, deployed identically into both variants' bundles (same content, deployed twice).
(not deployed) pipeline_cards.json ⚠️ Per-workspace, in the workspace's file storage Which pipelines exist here + their uuid + parameters. Generated by create_pipeline_cards, fetched by the app at load. Never put it in the bundle.

Webapp identity and scopes (id, slug, URL, allowed operations) are not stored in the repo β€” they're resolved live from the OpenHEXA API (list_static_webapps / get_static_webapp) at deploy time, and are never fetched by the running app.

The app self-adapts at runtime: OpenHEXA injects window.OPENHEXA.workspaceSlug at page load, so the same app.js queries this workspace, fetches this workspace's catalog, and greys out any map node whose id isn't in it. β†’ New workspace (same variant) = deploy the same 5 files, then run create_pipeline_cards in that workspace.

One caveat to the "fully generic" claim: app.js hardcodes the SaaS front-end base https://app.openhexa.org (for run / dataset links). That's the same for every SaaS workspace, but a self-hosted OpenHEXA install would need it changed β€” it's the only non-per-workspace assumption baked into the code.


Languages (EN | FR)

French is the main interface language for v1 β€” nearly all users are French-speaking. It's built as one webapp with an EN/FR toggle in the header, not as separate per-language builds.

Shipped in the cockpit variant only. flowchart is still English-only. Both variants read the same shared app/pipeline_descriptions.json, so its bilingual shape has to keep working for both β€” flowchart simply reads the en side.

What is and isn't translated:

Text Where it lives Translated?
App chrome (buttons, statuses, headings) I18N table in app/cockpit/app.js βœ… EN + FR
Pipeline / step titles app/cockpit/pipeline_map.json β†’ label βœ… {en, fr}
Pipeline descriptions app/pipeline_descriptions.json βœ… {en, fr}
Parameter labels, help text, dropdowns the workspace's pipeline_cards.json ❌ English (comes from pipeline source)

If you edit a pipeline description, write both languages. Values are { "en": "…", "fr": "…" } objects holding the same Markdown-lite prose. (A plain string still works and is treated as English, so older entries don't break.)

The toggle remembers your choice (localStorage), and you can deep-link a language with ?lang=fr. Switching language re-renders the whole panel.

⚠️ The French strings are drafts pending review β€” they haven't been signed off as final copy.


How it works

Two independent tracks feed the running app β€” the bundle you deploy, and the catalog the workspace generates for itself:

IN THIS REPO β€” deployed, identical for every workspace
    app/<variant>/pipeline_map.json           ← hand-authored map for this variant: layout + dependency edges
    app/pipeline_descriptions.json            ← hand-authored node descriptions, shared across variants + workspaces
    app/<variant>/index.html + styles.css + app.js   ← this variant's app shell
            ↓
    deploy set = app/<variant>/*  +  app/pipeline_descriptions.json
            ↓  (flattened β€” all 5 files land in the webapp root, side by side)
    OpenHEXA static webapp   (webapp id/slug resolved live via list_static_webapps; variant told
                              apart live by webapp SLUG β€” see "UI variants")
            ↑
            β”‚  fetched at page load via a signed URL (FILES_READ)
            β”‚
IN THE WORKSPACE β€” generated per workspace, never deployed
    utils_pipelines/create_pipeline_cards/  (this repo)  ← deployed to the workspace as a pipeline
            ↓  run it (schemas/pipeline_cards.schema.json is the output contract)
    <workspace files>/utils_pipelines/create_pipeline_cards/pipeline_cards/pipeline_cards.json
                                              ← this workspace's catalog: names, UUIDs, parameters

Using with an AI agent (Claude Code)

Open this directory in Claude Code. The agent reads CLAUDE.md for full instructions automatically. For orchestrator work it also reads docs/PRODUCT_SPEC.md (product scope + v0/v1/v2 roadmap) at session start. Then just describe what you want:

  • Change the app itself: "Make the Run button in the cockpit do X" β€” the agent edits app/<variant>/ and redeploys the bundle
  • Set up a new workspace: "Create the flowchart webapp for workspace Z" β€” the agent creates the webapp with the right scopes and deploys the standard 5-file bundle, then asks you to run create_pipeline_cards there (that's what makes the pipelines appear)
  • A newly installed pipeline isn't showing up: run create_pipeline_cards in that workspace and refresh the app β€” no agent or redeploy needed at all

The agent needs access to the OpenHEXA MCP server (configured in Claude Code settings) to look up workspace/pipeline IDs and deploy webapps.

Guardrails are enforced, not just documented. CLAUDE.md opens with agent guardrails (ask before any git/GitHub write; never do anything destructive or history-rewriting; hand faster-by-hand steps back to you), and .claude/hooks/block-destructive-git.js is a PreToolUse hook that hard-denies the destructive commands (git reset --hard, push --force, rebase, clean, branch/tag deletion, stash drop, restore, filter-branch, gh pr close, gh repo delete, gh api -X DELETE, …) regardless of what the agent is asked to do. If you want one of those, run it by hand.


Adding a new workspace

  1. Open Claude Code in this directory
  2. Say: "Create the <variant> webapp for workspace <workspace name>" (specify which UI variant β€” flowchart or cockpit)
  3. The agent will:
    • Find the workspace slug via list_workspaces
    • Create the webapp with the four required scopes (PIPELINES_READ, PIPELINES_RUN, FILES_READ, USER_READ) β€” FILES_READ is what lets the app read its own catalog
    • Deploy the generic app/<variant>/ bundle + app/pipeline_descriptions.json (5 files)
    • Check the catalog exists in the workspace's file storage
  4. Install create_pipeline_cards in the workspace (from utils_pipelines/create_pipeline_cards/) and run it once. It takes no parameters β€” just Run.
  5. Nothing to commit β€” the bundle is unchanged and the catalog isn't a repo file.

The webapp comes first. create_pipeline_cards curates the catalog against the deployed pipeline_map.json of the webapp named by its config.WEBAPP_SLUG β€” currently the Cockpit app, snt-pipelines-orchestrator-cockpit. Until that webapp exists in the workspace, the generator run fails. So if you are only adding the flowchart variant to a workspace, either deploy Cockpit there too, or point WEBAPP_SLUG at snt-pipelines-orchestrator and redeploy the generator. In the other direction, the app itself shows a "run create_pipeline_cards" error until step 4 is done β€” so both halves are needed before the orchestrator works.


Adding a new pipeline

There is nothing to add to this repo, and nothing to redeploy:

  1. Install the pipeline in the workspace as usual (from its template)
  2. Run create_pipeline_cards in that workspace
  3. Refresh the orchestrator β€” the pipeline's node is now active, with its parameter form generated from the version you just installed

The only case that still needs a repo change is a pipeline that isn't on the map yet: the map (app/<variant>/pipeline_map.json) is hand-authored, so a genuinely new step needs a node adding there β€” and that does mean a redeploy. Pipelines already on the map but not installed simply render greyed-out until you install them.

The catalog carries a generated_at date, but its parameters come from each pipeline's deployed version in that workspace β€” not from GitHub main β€” so they can't drift from what's actually installed. What can be out of date is the catalog vs the workspace: if pipelines have been installed or upgraded since that date, re-run the generator.

Each entry also records the deployed version it was built from (version_name / version_number), so the app can eventually notice that drift by itself rather than relying on someone spotting it.


Deploying the bundle (and a known friction)

Deploys go through the OpenHEXA MCP tools (the webapp id/slug is resolved live via list_static_webapps). There are two paths, and the agent picks by the size of the change:

  • Small, targeted edits (a few lines of app.js, a CSS tweak, one description) go through edit_static_webapp_file β€” a find/replace applied server-side, so the agent never loads the whole file. This is the normal path.
  • New files, wholesale rewrites, or a first deploy go through update_static_webapp, which takes file contents inline.

Partial deploys work β€” only the changed files need to be sent β€” and after every deploy the agent re-reads the changed file(s) live (get_static_webapp_file for one file, get_static_webapp for the whole bundle) and diffs them against the repo (app/<variant>/ + app/pipeline_descriptions.json) to confirm what's live matches the source.

Known friction (large files). The update_static_webapp path only accepts file contents, not a file path, and the agent can only load a file into its context up to a size limit. Each app.js is now ~90 KB β€” well past that β€” so a wholesale rewrite of one means reading it back in slices and reassembling it. Targeted edits avoid this entirely (see above), so this only bites on a full rewrite. It's verified each time (the live file is diffed against the local copy), so it's a speed bump, not a risk.

Manual fallback β€” drag-drop from the repo. Because the bottleneck is only about getting the bytes into the agent, uploading a file yourself through the browser avoids it entirely. If the OpenHEXA UI lets you replace files on an existing webapp (Web Apps β†’ the webapp β†’ edit/settings), you can drag the changed file(s) straight from app/<variant>/ (generic to that variant) or app/pipeline_descriptions.json into the UI β€” the repo copy is always the up-to-date source, so this is safe. (The OpenHEXA CLI can deploy pipelines from local files but not static webapps today, so there's no command-line shortcut yet β€” a request to add one has been raised with the OpenHEXA team.)


Refreshing the OpenHEXA schema

If schemas/schema.generated.graphql becomes stale, regenerate it with:

Invoke-WebRequest -Uri "https://raw.githubusercontent.com/BLSQ/openhexa-app/main/frontend/schema.generated.graphql" -OutFile "schemas/schema.generated.graphql"

Deployed webapps

The active product is the SNT Pipelines Orchestrator, deployed per variant as that variant's generic app/<variant>/ bundle (plus the shared descriptions) β€” the same 5 files everywhere, with each workspace's catalog supplied from its own file storage:

Each (workspace, variant) pair is its own webapp with its own URL β€” five in total (verified live 2026-07-31):

Workspace Slug Variant URL
SNT App Dev snt-app-dev flowchart https://snt-pipelines-orchestrator.openhexa.io/
SNT App Dev snt-app-dev cockpit https://snt-pipelines-orchestrator-cockpit.openhexa.io/
SNT Testing snt-testing flowchart https://snt-testing-snt-pipelines-orchestrator.openhexa.io/
SNT Testing snt-testing cockpit https://snt-testing-snt-pipelines-orchestrator-cockpit.openhexa.io/
CMR SNT Process cmr-snt-process flowchart https://cmr-snt-process-snt-pipelines-orchestrator.openhexa.io/

cockpit is not deployed to cmr-snt-process. Note cmr-snt-process's webapp is still named the bare SNT Pipelines Orchestrator, while the other two workspaces use the … - Flowchart / … - Cockpit naming β€” match on slug, not name (see "UI variants").

URLs and IDs are resolved live from the OpenHEXA API (list_static_webapps) β€” that, not this table, is the source of truth.

Legacy single-pipeline webapps and spikes (A.2 DHIS2 Formatting in the DRC workshop demo; DHIS2 Reporting Rate, Population Transformation, and the status spike in SNT Testing; the report-embed feasibility probe in SNT App Dev) were the stepping stones toward the orchestrator. Their local copies now live under archive/ and are no longer maintained. A few of the spike webapps are still present on the platform (e.g. t0-9-status-proxy-spike, report-embed-probe) β€” they're leftovers, not part of the product.

The SNT Pipelines Orchestrator is built in the dedicated snt-app-dev workspace (all ~18 pipelines installed β€” the primary build target) and also deployed to snt-testing (a subset installed, so it demos the greyed-out state). The flowchart variant additionally reaches cmr-snt-process; cockpit is live in snt-app-dev and snt-testing only.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages