Skip to content

CLI ↔ Portal symmetry

GISPulse exposes two equivalent UIs over the same source of truth (triggers.yaml + the SQLite/PostGIS change-log): a CLI for terminal-first power users and a web portal for visual-first onboarding. This page is the invariance test: any public feature must appear in both columns — otherwise the UX debt is logged explicitly.

Product doctrine confirmed 2026-04-30. No GIS-client plugin required: QGIS save, ogr2ogr, ArcGIS Pro export, raw sqlite3, CLI or web portal — every DML statement fires the triggers via the change-log. See Architecture.

Status legend

StatusMeaning
Symmetric: feature available on both CLI and portal
⚠️Asymmetric: present on one side only, UX debt logged (see issue)
Deferred: not implemented on either side (see milestone)
🔧"Ops" surface intentionally CLI-only (no UI planned)

1. Rules — pipeline CRUD

Source of truth: JSON / YAML rules loaded by rules.loader. API: rules_router.py.

CapabilityCLIPortalStatus
Create a rulegispulse template use <preset> (scaffold) then manual JSON editRuleEditorModal (drag-and-drop registry → schema-driven form) — components/rules/RuleEditorModal.tsx, NodeEditor.tsx
List the rules of a pipelinegispulse capabilities (registry) + reading the JSONNodeEditor workspace — renders the pipeline DAG, registry-driven palette
Edit a ruleManual JSON edit + gispulse validateNodePropertyPanel (schema-driven form) + live validation — components/nodes/NodePropertyPanel.tsx
Delete a ruleManual JSON deleteDelete node from NodeEditor (Delete key / context menu)
Validate a pipelinegispulse validate <rules.json>Auto-validate on save in NodeEditor (POST /rules/{id}/validate)
Convert rule ↔ nodeN/A (the CLI manipulates raw JSON)GET /rules/{id}/to-node + POST /rules/from-node exposed to NodeEditor⚠️
Run a pipelinegispulse run <input> --rules <pipeline.json> -o <output>WorkflowsView → "Run" button (POST /pipelines/execute)
Export pipeline as triggers YAMLN/A — the CLI consumes YAML directlyN/A — the portal writes YAML for the runtime🔧

Logged asymmetries:

  • ⚠️ rule ↔ node converter: only exposed via REST API, no dedicated CLI command. → suggest issue feat(cli): gispulse rules to-node / from-node (v1.6+).

2. Triggers — configuration & runtime

Source of truth: YAML triggers + _gispulse_change_log table. API: triggers_router.py. CLI code: cli_triggers.py, cli_watch.py.

CapabilityCLIPortalStatus
Create a triggerManual YAML editTriggerBuilderInline / TriggerBuilderModalPredicateBuilder + ActionEditor + CronBuilder (POST /triggers)
List triggersgispulse triggers list --gpkg <path> (installed SQLite triggers)GET /triggersScenariosPanel / TriggerHistoryPanel
Edit a triggerManual YAML editTriggerBuilderModal (PUT /triggers/{id})
Delete a triggerManual YAML edit + gispulse triggers validateDELETE /triggers/{id} from ScenariosPanel
Enable / disableN/A (comment out in YAML)POST /triggers/{id}/toggle (UI switch in TriggerBuilderInline)⚠️
Validate a triggers YAMLgispulse triggers validate --config <yaml> --gpkg <path>Live validation on save in TriggerBuilderModal (reuses validate_against_gpkg)
Single tick (run-once)gispulse triggers run --config <yaml> --oncePOST /triggers/{id}/evaluate — "Test" button in TriggerBuilderInline
Long-running daemongispulse triggers run --config <yaml> --watch or gispulse watch <gpkg> -r <rules>N/A — the portal configures a trigger, the local runtime (CLI or daemon) executes it🔧
Stream live eventsgispulse triggers run --watch (JSON logs on stderr)GET /triggers/eval-stream (SSE) consumed by TriggerHistoryPanel + ActivityTimeline
Dryrun (preview actions)N/A--once mode actually executesPOST /examples/{id}/triggers/dryrun — preview actions without persisting (Mode 2 Try-it)⚠️
Inspect trigger operationsN/AGET /triggers/{id}/operations — execution history in TriggerHistoryPanel⚠️

Logged asymmetries:

  • ⚠️ CLI toggle: no gispulse triggers enable/disable <id>. → suggest issue feat(cli): gispulse triggers toggle <id> --enabled/--disabled (v1.6+).
  • ⚠️ CLI dryrun: no CLI equivalent of POST /examples/{id}/triggers/dryrun. → suggest issue feat(cli): gispulse triggers run --dry-run (v1.6+, deferrable).
  • ⚠️ CLI operations history: no gispulse triggers history <id>. → suggest issue feat(cli): gispulse triggers history <id> (v1.6+).

3. Tracking — SQLite change-log

Source of truth: _gispulse_change_log table inside the GPKG. CLI code: cli_track.py. API: datasets_router.py (enable_tracking / disable_tracking / tracking_status).

CapabilityCLIPortalStatus
Install tracking on a layergispulse track install <gpkg> --layer <name>POST /datasets/{id}/enable_tracking — "Enable tracking" button in DatasetCard
Install on every layergispulse track install <gpkg> --all-layersPOST /datasets/{id}/enable_tracking (no "all" toggle)⚠️
Uninstall trackinggispulse track uninstall <gpkg> --layer <name>POST /datasets/{id}/disable_trackingDatasetContextMenu action
List tracked layersgispulse track list <gpkg> (triggers + pending counts)GET /datasets/{id}/tracking_status — shown on DatasetCard
Tail pending changesgispulse track tail <gpkg> --limit 50N/AActivityTimeline consumes post-dispatch events, not the raw change-log⚠️
Diagnostic + auto-fixgispulse track doctor <gpkg> [--auto-fix]N/A⚠️
Global env diagnosticgispulse doctorN/A — intentional CLI-only "ops" surface🔧

Logged asymmetries:

  • ⚠️ all-layers UI: enable_tracking only handles one layer at a time. → suggest issue feat(portal): bulk enable tracking from DatasetCard (v1.6+).
  • ⚠️ change-log tail: useful for debug, no UI panel. → suggest issue feat(portal): raw change-log inspector panel (v1.6+, deferrable).
  • ⚠️ track doctor UI: trigger healthcheck + auto-fix should be exposed in DatasetCard. → suggest issue feat(portal): tracking health badge + repair action (v1.6+).

4. Datasets — upload, listing, deletion

API: datasets_router.py, portal_upload_router.py.

CapabilityCLIPortalStatus
Upload a dataset (local file)gispulse run consumes a local file directlyCatalogImportDialog + DragDropOverlay (POST /datasets/upload)⚠️
Upload from URLN/APOST /datasets/import-url — input in CatalogImportDialog⚠️
Import from OGC API FeaturesN/APOST /datasets/ogcCatalogPanel connector⚠️
List datasetsgispulse layers <file> (single-file); gispulse info <file>GET /datasetsDatasetsView + DatasetCard grid⚠️
Inspect metadata (CRS, layers, styles)gispulse info <file>GET /datasets/{id}InspectorPanel + DatasetSchemaGraph
Delete a datasetN/Arm <file> manuallyDELETE /datasets/{id}DatasetContextMenu⚠️
Rename a datasetN/APATCH /datasets/{id}RenameDialog⚠️
Export to GPKGgispulse run -o <output.gpkg> (pipeline output)POST /datasets/export-gpkg — "Export" button in DatasetCard
Export (other formats)gispulse run -o <output.{geojson,shp,parquet,fgb,...}>POST /datasets/export (16+ formats — see Formats I/O)

Logged asymmetries:

  • ⚠️ CLI dataset registry: datasets are implicit on the CLI side (a file on disk) vs explicit on the portal (persistent registry). → this design gap is intentional for Mode 1, but we could expose gispulse datasets list/add/rm pointing to an optional local registry. To debate v1.6+ — issue feat(cli): optional dataset registry.
  • ⚠️ import-url / OGC CLI: no gispulse import url <URL> or gispulse import ogc <endpoint>. → suggest issue feat(cli): gispulse import (v1.6+).

5. Examples — Mode 2 portal "Try it"

API: examples_router.py. Sprint v1.5.1, fixed read-only datasets registry.

CapabilityCLIPortalStatus
List available examplesN/A — intentional portal-only surface (Mode 2 Community demo)GET /examplesMarketplacePage + landing🔧
Example detailsN/AGET /examples/{id} → preview card🔧
Tile / MVT previewN/A — the viewer reads the local GPKG directlyGET /examples/{id}/preview + /examples/{id}/tiles/{z}/{x}/{y}.mvtMapView🔧
Dryrun triggers on examplegispulse triggers run --once --config <yaml> --gpkg <example.gpkg> (locally, after pipx install gispulse)POST /examples/{id}/triggers/dryrunTriggerBuilderModal "Test on this example"

"Try it" surface: by design the portal exposes examples as the on-ramp to pipx install gispulse. CLI users who clone the repo access the same datasets via examples/. No UX debt here — this is the funnel.


6. Styles — QML / SLD roundtrip

API: portal_datasets_router.py (styles import / export / breaks). Sprint v1.5.0.

CapabilityCLIPortalStatus
Import a QML styleN/A — QML already copied by gispulse run --all-layersPOST /datasets/{id}/styles/importLayerColorPicker / SchemaView action⚠️
Export a QML stylegispulse run automatically copies styles from the input GPKGGET /datasets/{id}/styles → "Download QML" button
Update styleN/APUT /datasets/{id}/stylesLayerColorPicker + MapLegend editing⚠️
Compute breaks (Jenks / quantile / equal interval)N/APOST /datasets/{id}/layers/{layer}/breaksLayerColorPicker classification picker⚠️
List distinct field valuesN/AGET /datasets/{id}/layers/{layer}/distinct/{field}⚠️
Descriptive stats (min/max/mean/quantiles)N/AGET /datasets/{id}/layers/{layer}/stats/{field}InspectorPanel⚠️

Logged asymmetries:

  • ⚠️ CLI styling: import / classify breaks / stats are inherently visual cartographic operations. CLI symmetry is low value here. → loggable as non-priority issue feat(cli): gispulse style classify --field <f> --method jenks --bins 5 for CI / batch. v1.7+.

7. Run — pipeline execution

Source of truth: core.pipeline + orchestration.session_manager. API: pipelines_router.py, jobs_router.py.

CapabilityCLIPortalStatus
Execute a pipeline (sync)gispulse run <input> --rules <pipeline.json> -o <output>POST /pipelines/executeWorkflowsView "Run"
Step-by-step executionN/A (no dedicated CLI — the engine runs the whole pipeline)POST /pipelines/execute-steps — debug mode in NodeEditor⚠️
Validate a pipelinegispulse validate <pipeline.json>POST /pipelines/validate — auto-validate on save
List jobsgispulse jobs list [--host HOST] [--api-key KEY]GET /jobsJobTrackerCorner (lazy panel)
Job statusgispulse jobs status <JOB_ID>GET /jobs/{id}JobTrackerCorner detail
Stream job eventsN/A (the CLI runs sync, no SSE)GET /jobs/{id}/events (SSE) → progress in JobTrackerCorner⚠️
Cancel a jobgispulse jobs cancel <JOB_ID>POST /jobs/{id}/cancelJobTrackerCorner action
Download job featuresN/A — output already written locally by gispulse runGET /jobs/{id}/features + /jobs/{id}/download⚠️
Submit an async jobN/A (gispulse run is synchronous)POST /jobs — submit async via WorkflowsView⚠️
Pipeline examples / presetsgispulse template list + gispulse template use <name>GET /pipelines/examples → palette or WorkflowList

Logged asymmetries:

  • ⚠️ execute-steps CLI: useful for step-by-step debugging. → suggest issue feat(cli): gispulse run --step <id> (v1.7+, deferrable).
  • ⚠️ jobs SSE / async CLI: gispulse run is synchronous by design (script-friendly). The async pattern is portal-only, justified for long-running workflows. Not urgent.

8. Schedules — cron jobs

API: schedules_router.py. Component: components/schedules/ScheduleForm.tsx.

CapabilityCLIPortalStatus
Create a scheduleN/A — use native OS cron / systemd timers to wrap gispulse runPOST /schedulesScheduleForm (CronBuilder reused from triggers)⚠️
List schedulesN/AGET /schedules⚠️
View / edit scheduleN/AGET / PATCH /schedules/{id}⚠️
Delete scheduleN/ADELETE /schedules/{id}⚠️
Manual run-nowgispulse run directlyPOST /schedules/{id}/run-now⚠️

Logged asymmetries:

  • ⚠️ schedules CLI absent: product decision pending — either we assume "use cron" for CLI users, or we expose gispulse schedules add/list/rm. → suggest issue decision: gispulse schedules CLI subcommand (v1.6+).

9. Marketplace — third-party plugins / capabilities

API: marketplace_router.py. Components: components/marketplace/, pages/MarketplacePage.tsx.

CapabilityCLIPortalStatus
List installed pluginsgispulse marketplace list [QUERY]GET /marketplace/pluginsMarketplacePage
Search the cataloguegispulse marketplace search QUERYGET /marketplace/search + /marketplace/catalog
Plugin detailsgispulse marketplace info NAMEGET /marketplace/plugins/{name}
Install a plugingispulse marketplace install NAMEPOST /marketplace/install
Uninstall a plugingispulse marketplace uninstall NAMEDELETE /marketplace/plugins/{name}

Full symmetry. Marketplace surface aligned by construction since v1.1.0.


10. Templates — project scaffolding

API: pipelines_router.py /examples. CLI: gispulse template.

CapabilityCLIPortalStatus
List templatesgispulse template listGET /pipelines/examples (preset library exposed in WorkflowList)
Scaffold a project from a templategispulse template use <NAME> [--output-dir DIR]OnboardingFlow (first launch) + SaveTemplateDialog
Create a workflow from a templategispulse template workflowWorkflowList → "From template"

Full symmetry.


11. Viewer / Portal / Engine — process lifecycle

"Ops" surface — how to launch GISPulse.

CapabilityCLIPortalStatus
Launch viewer (read-only)gispulse serve <file> [--port 8765]N/A — the viewer is embedded in the portal🔧
Launch portalgispulse portal [--port 8001]N/A — the portal is the portal (meta)🔧
Launch full enginegispulse engine [--port 8001] (Tauri sidecar JSON)N/A🔧
Connect "My engine" from public portalgispulse portal --backend=<URL> (Mode 2 — sprint v1.5.1)BackendStatusBanner + SettingsPanel (backend URL input, persisted in localStorage) — shipped gispulse-portal #30
Diagnose environmentgispulse doctorN/A🔧
Updategispulse update [--check] [--force]N/A — the web portal self-updates, the CLI manages its own version🔧
Initialize a projectgispulse init [DIR] [--name NAME]OnboardingFlow (visual equivalent for the first session)
Telemetry opt-ingispulse telemetry --enable / --disable / --statusN/A — CLI-only config (env var GISPULSE_TELEMETRY=1 for scripts)🔧

Intentional 🔧 surface: process lifecycle and telemetry are CLI-only by design — the portal is already running when the user clicks. No debt.


12. SQL Console — SQL editing / preview

API: portal_sql_router.py. Component: components/sql/SQLConsole.tsx.

CapabilityCLIPortalStatus
Execute a SQL queryN/Agispulse run accepts the pipeline + postgis_sql capabilityPOST /sql/executeSQLConsole⚠️
Preview SQL resultsN/ASQLPreviewTable (auth + blocklist on the backend, v1.1.0)⚠️
Export SQL resultsN/APOST /sql/export⚠️

Logged asymmetries:

  • ⚠️ CLI SQL: feature is mainly "interactive exploration" — already covered for batch via the postgis_sql capability inside a pipeline. Low priority. → deferrable issue feat(cli): gispulse sql --execute "SELECT ..." (v1.7+).

13. Auth — SSO and identity

API: auth_router.py. OSS: anonymous stub. Pro/Enterprise: OIDC (Google / Azure / Keycloak — see gispulse-enterprise).

CapabilityCLIPortalStatus
List SSO providersN/A (no CLI auth in OSS)GET /auth/providerspages/auth/🔧
User infoN/AGET /auth/meUserMenu + AuthGuard🔧

Intentional 🔧 surface: OSS Mode 1 = single-user CLI without auth. Mode 2 portal SaaS Pro v1.6+ will add visual auth. CLI auth ships with gispulse login (issue v1.7+).


Summary

Area✅ Symmetric⚠️ Asymmetric🔧 Intentional CLI/Portal-only❌ Deferred
Rules7110
Triggers6410
Tracking4310
Datasets3600
Examples1030
Styles1500
Run5400
Schedules0500
Marketplace5000
Templates3000
Lifecycle / Engine2060
SQL0300
Auth0020
Total3731140

Reading: of 82 public capabilities, 37 are already symmetric, 14 are CLI-only or portal-only by intentional design, and 31 UX debts are identified and listed above with their suggested issue. No capability is silently missing from either surface.


How this page stays up to date

This matrix is currently maintained manually. Any new feature (CLI or portal) must be added to the corresponding row with its status. A v1.6+ issue (feat(scripts): generate symmetry.md from CLI ↔ portal mapping) explores automatic generation from a declarative source-code mapping — for now manual content stays authoritative.

Process for any new PR adding a feature:

  1. Identify the row to add or update in this matrix
  2. If the PR introduces an asymmetry, log the corresponding debt issue in the same session
  3. Request review from Marco (gis-lead-dev) or Jordan (jordan-po) to validate the status

See also:

Published under AGPL-3.0 license.