From 82e7ce1b026573106ec34040fbfdb9d75bd2696a Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Sat, 16 May 2026 14:41:47 +0200 Subject: [PATCH 1/2] documentation of indexes and ODE Desktop developer mode --- docs/getting-started/architecture-overview.md | 1 + docs/guides/custom-applications.md | 8 +- docs/guides/observation-queries.md | 100 ++++++++++++++++++ docs/guides/ode-desktop-developer-mode.md | 92 ++++++++++++++++ docs/implementer/implementer-index.md | 12 +++ docs/reference/formulus.md | 31 ++++-- docs/using/app-bundles.md | 4 + docs/using/custom-applications.md | 4 + sidebars.ts | 2 + 9 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 docs/guides/observation-queries.md create mode 100644 docs/guides/ode-desktop-developer-mode.md diff --git a/docs/getting-started/architecture-overview.md b/docs/getting-started/architecture-overview.md index b0137c8..dc90cf8 100644 --- a/docs/getting-started/architecture-overview.md +++ b/docs/getting-started/architecture-overview.md @@ -98,6 +98,7 @@ Custom App Structure: **Key Features:** - **Configuration-driven**: `app.config.json` controls app behavior - **Form Integration**: Seamless integration with Formulus form system +- **Local observation indexes**: Device-only index table for fast queries (never synced to Synkronus) - **Theme System**: Material Design 3 based theming - **Build Process**: Optimized bundle creation for deployment diff --git a/docs/guides/custom-applications.md b/docs/guides/custom-applications.md index 81a2246..f831bba 100644 --- a/docs/guides/custom-applications.md +++ b/docs/guides/custom-applications.md @@ -72,10 +72,16 @@ If your template uses **`app.config.json`** (common in React-based examples), it "onBackground": "#ffffff", "onSurface": "#e0e0e0" } - } + }, + "observationIndexes": [ + { "key": "patient_id", "path": "$.patient_id" }, + { "key": "site_code", "path": "$.site_code", "formTypes": ["visit_*"] } + ] } ``` +`observationIndexes` declares **local-only** SQLite indexes for fast `getObservationsByQuery` filters on `data.*` fields. They are maintained on the device and are **not synced**. See [Observation queries](./observation-queries.md). + ### Theme Integration Themes are automatically generated from your configuration: diff --git a/docs/guides/observation-queries.md b/docs/guides/observation-queries.md new file mode 100644 index 0000000..d41e4c9 --- /dev/null +++ b/docs/guides/observation-queries.md @@ -0,0 +1,100 @@ +# Observation queries and local indexes + +Custom apps query local observations through **`getObservationsByQuery`** using a **structured filter AST** (Abstract Syntax Tree). Formulus and ODE Desktop compile that AST to SQLite (`json_extract` plus a local **observation index** table). Indexes are declared in **`app.config.json`** and are **never synced** to Synkronus. + +## API + +```javascript +const people = await formulus.getObservationsByQuery({ + formType: 'register_person', + includeDeleted: false, + filter: { + op: 'and', + conditions: [ + { field: 'data.village', op: 'eq', value: 'London' }, + { field: 'data.sex', op: 'eq', value: 'female' }, + ], + }, +}); +``` + +### Filter shape + +| Node | Fields | Notes | +|------|--------|-------| +| Condition | `field`, `op`, `value` | `field` is `data.*` or a top-level column (`observation_id`, `deleted`, …) | +| Logical | `op: 'and' \| 'or'`, `conditions[]` | Parentheses via nesting | +| Quantifier | `op: 'any'`, `path`, `as`, `where` | Array members via `json_each` | + +Supported comparison ops: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`. + +**Age from date of birth** (`age_from_dob(...)`) is **not** compiled to SQL. Formplayer evaluates age in JavaScript after fetching (see [Dynamic choice lists](./dynamic-choice-lists.md)). + +Invalid filters **fail closed** (structured error; no unfiltered fallback). + +## Declaring indexes (`app.config.json`) + +```json +{ + "observationIndexes": [ + { "key": "p_id", "path": "$.p_id", "formTypes": ["hh_person", "p_*"] }, + { "key": "hh_id", "path": "$.hh_id", "formTypes": ["household", "hh_*"] }, + { "key": "age", "path": "$.age", "valueType": "number" } + ] +} +``` + +| Property | Purpose | +|----------|---------| +| `key` | Stable index name; matches `data.` in filters | +| `path` | JSON path under observation `data` (e.g. `$.p_id`) | +| `formTypes` | Optional patterns (`*` suffix = prefix match) | +| `valueType` | `"number"` stores `value_num` for numeric comparisons | + +Indexes rebuild automatically when the app bundle updates. On **ODE Desktop**, use **Sync → Re-create index** after changing `observationIndexes` or bulk imports. + +## Examples + +**Person visits (OR on several fields):** + +```javascript +filter: { + op: 'or', + conditions: [ + { field: 'data.p_id', op: 'eq', value: personId }, + { field: 'data.names', op: 'eq', value: personName }, + ], +} +``` + +**Household list:** + +```javascript +filter: { + op: 'or', + conditions: [ + { field: 'data.hh_id', op: 'eq', value: hhId }, + { field: 'data.hh_number', op: 'eq', value: hhNumber }, + ], +} +``` + +## Performance notes + +- One **`formType`** per call; use `Promise.all` for multiple types. +- Declared `data.*` paths use the **index table**; undeclared paths use **`json_extract`** with a dev warning. +- Expression indexes on `observations(data)` may be created for declared paths to speed fallback queries. + +## Platform mapping + +| Concept | Formulus | ODE Desktop | +|---------|----------|-------------| +| JSON payload | `observations.data` | `observations.payload` | +| Soft delete | `deleted` column | `observation_extras.deleted` | +| Observation id | `observation_id` | `id` | + +The AST always uses **`data.*`** for JSON fields; each platform compiler maps to the correct column. + +## ODE Desktop developer mode + +When [ODE Desktop developer mode](./ode-desktop-developer-mode) is on, `app.config.json` and observation indexes are read from the **mirrored** app under `bundles/dev-local/app/`. After you change index declarations in your local project, mirror again with **Refresh app** so the desktop workspace picks up the new config. diff --git a/docs/guides/ode-desktop-developer-mode.md b/docs/guides/ode-desktop-developer-mode.md new file mode 100644 index 0000000..3729146 --- /dev/null +++ b/docs/guides/ode-desktop-developer-mode.md @@ -0,0 +1,92 @@ +# ODE Desktop developer mode + +**Developer mode** in ODE Desktop lets you iterate on a **local custom app build** (for example `dist/` after `npm run build`) against a profile’s real observations and workspace, without replacing the bundle you downloaded from Synkronus. + +## What it is + +When developer mode is on, ODE Desktop **mirrors** your selected folder into the profile workspace under `bundles/dev-local/`. The workbench then loads: + +- **Custom app** from `bundles/dev-local/app/` +- **Form preview** from `bundles/dev-local/forms/` (when your folder includes a `forms/` directory) + +Observations, attachments, and sync still use the profile database. The Synk-downloaded bundle in `bundles/active/` is **not** overwritten. + +## Prerequisites + +- ODE Desktop installed (see project README for build-from-source). +- A **profile** with a configured workspace. +- A local folder whose root contains **`index.html`** (typical: your custom app `dist/` output). +- Optional: download an app bundle from Synkronus on the **Bundles** page so `bundles/active/` has forms and `app.config.json` when developer mode is off. + +## Enable developer mode + +1. Open **Workbench** → **Bundles**. +2. Turn **Developer mode** **On**. +3. Click **Browse…** and select the folder that contains `index.html`. +4. Wait for the first mirror to finish (or click **Refresh app**). + +While developer mode is on, an orange **banner** appears on all Workbench pages with the folder path and a **Refresh app** shortcut. + +## Folder layout + +Your selected folder is the **app root** (same idea as the `app/` directory inside a published bundle): + +``` +my-custom-app/dist/ + index.html ← required at root + app.js + assets/ + forms/ ← optional; mirrored for Form preview + my_form/ + schema.json + ui.json +``` + +- **Custom app entry:** `index.html` at the selected path (not a parent repo root unless that root is your built app). +- **Forms:** standard bundle layout under `forms/{formType}/schema.json` and `ui.json`, plus optional `forms/ext.json` and custom question types (same as Formulus bundles). + +## Refresh app + +After you rebuild the custom app or edit forms locally: + +1. Click **Refresh app** on the Bundles page or in the Workbench banner. + +This re-runs the mirror (`source` → `bundles/dev-local/app/` and `source/forms/` → `bundles/dev-local/forms/`). **Custom app** and **Form preview** pick up changes on the next load (the embed remounts automatically after a successful mirror). + +**Refresh from server** on the Bundles page is separate: it updates `bundles/active/` from Synkronus only. + +## What changes when developer mode is on + +| Area | Behavior | +|------|----------| +| Custom app (Workbench) | Loads mirrored app under `bundles/dev-local/app/` | +| Form preview | Lists and loads specs from `bundles/dev-local/forms/` when mirrored | +| Observations / sync | Unchanged — profile SQLite | +| Downloaded bundle | Stays in `bundles/active/` for server reload and non-dev use | +| `getCustomAppUri` / `getFormSpecsUri` (in preview bridge) | Point at dev-local roots when mode is on | + +## Workbench banner + +When developer mode is on, every Workbench route shows a compact status strip **above** sync/activity messages: active folder path and **Refresh app**. Configure the folder and toggle on the **Bundles** page only. + +## Limitations + +- **Device APIs** (camera, GPS, QR, etc.) are stubbed in ODE Desktop form preview, same as before developer mode. +- The mirror is a **copy** — edit files in your source folder, then **Refresh app**; ODE Desktop does not watch the filesystem. +- Invalid or missing folder (no `index.html`) shows a **blocking error**; the custom app does not silently fall back to `bundles/active/`. +- Observation query indexes use `app.config.json` from the **mirrored** app when developer mode is on; rebuild indexes if you change index declarations (see [Observation queries](./observation-queries)). + +## Platform comparison + +| | Formulus (device) | ODE Desktop (developer mode) | +|--|-------------------|----------------------------| +| Custom app source | Downloaded bundle | Local folder → `bundles/dev-local/app/` | +| Forms | Bundle on device | Mirrored `forms/` or `bundles/active/forms/` when off | +| Observations | On-device DB | Profile workspace SQLite | +| Typical use | Field collection | Local iteration before publish | + +## See also + +- [Understanding app bundles](../using/app-bundles) — Synk download to `bundles/active/` +- [Custom applications](../using/custom-applications) — custom app role in bundles +- [Observation queries](./observation-queries) — `getObservationsByQuery` and indexes diff --git a/docs/implementer/implementer-index.md b/docs/implementer/implementer-index.md index f02a409..5ebc841 100644 --- a/docs/implementer/implementer-index.md +++ b/docs/implementer/implementer-index.md @@ -61,6 +61,18 @@ Get your first form running in 30 minutes: +
+
+
+

ODE Desktop developer mode

+
+
+

Test a local custom app build in the Workbench against real profile data.

+ Learn More → +
+
+
+
diff --git a/docs/reference/formulus.md b/docs/reference/formulus.md index 91c84bd..6aac942 100644 --- a/docs/reference/formulus.md +++ b/docs/reference/formulus.md @@ -149,22 +149,37 @@ await api.deleteObservation('survey', 'obs-123'); **Returns:** Promise that resolves when deletion is complete -#### getObservations(formType, filters) +#### getObservations(formType, isDraft?, includeDeleted?) -Query observations from local database. +List observations for a form type (no structured filter). ```javascript -const observations = await api.getObservations('survey', { - status: 'synced', - limit: 10 +const observations = await api.getObservations('survey', false, false); +``` + +#### getObservationsByQuery(options) + +Query observations with a **structured filter AST** (preferred for custom apps). Declared `data.*` paths use a local **observation index**; other paths use `json_extract`. See [Observation queries](../guides/observation-queries.md). + +```javascript +const observations = await api.getObservationsByQuery({ + formType: 'hh_person', + includeDeleted: false, + filter: { + op: 'and', + conditions: [ + { field: 'data.village', op: 'eq', value: 'kopria' }, + ], + }, }); ``` **Parameters:** -- `formType` (string): The form type identifier -- `filters` (object): Optional query filters +- `formType` (string): Form type identifier +- `includeDeleted` (boolean, optional): Include soft-deleted rows +- `filter` (ObservationFilter, optional): Structured filter AST -**Returns:** Promise resolving to array of observations +**Returns:** Promise resolving to an array of observations #### sync() diff --git a/docs/using/app-bundles.md b/docs/using/app-bundles.md index ef90524..1563eda 100644 --- a/docs/using/app-bundles.md +++ b/docs/using/app-bundles.md @@ -167,6 +167,10 @@ For technical information about app bundle structure, format, and development, s - [Custom Applications Guide](/guides/custom-applications) - Building custom applications - [Form Design Guide](/guides/form-design) - Creating form specifications +## ODE Desktop Workbench + +On **ODE Desktop**, Synkronus bundles download into **`bundles/active/`** in the profile workspace. To iterate on a **local** custom app build without replacing that download, use [ODE Desktop developer mode](/docs/guides/ode-desktop-developer-mode) (mirror to `bundles/dev-local/`). + ## Related Documentation - [Your First Form](/using/your-first-form) - Get started with data collection diff --git a/docs/using/custom-applications.md b/docs/using/custom-applications.md index 13c208a..e0aa82f 100644 --- a/docs/using/custom-applications.md +++ b/docs/using/custom-applications.md @@ -107,6 +107,10 @@ Custom applications are suitable for: - Integration with external systems - Complex navigation requirements +## Testing locally with ODE Desktop + +Use [ODE Desktop developer mode](/docs/guides/ode-desktop-developer-mode) to load a local build (folder with `index.html`) in the Workbench **Custom app** page against a profile’s observations, then refresh after each build. + ## Next Steps - Read the [Custom Applications guide](/guides/custom-apps/overview) for detailed information diff --git a/sidebars.ts b/sidebars.ts index b038c5f..881b9e8 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -93,6 +93,8 @@ const sidebars: SidebarsConfig = { 'guides/building-custom-apps-v2', 'guides/form-design', 'guides/dynamic-choice-lists', + 'guides/observation-queries', + 'guides/ode-desktop-developer-mode', 'guides/custom-extensions', 'guides/deployment', 'guides/configuration', From daeb4b3c2846a9d3d57da7a38cdc3830a94f3ac3 Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Fri, 26 Jun 2026 10:06:36 +0200 Subject: [PATCH 2/2] docs(docs): updated diagrams and descriptions. Bump to v1.1.1 --- docs/developer/developer-getting-started.md | 11 +- docs/developer/developer-index.md | 6 +- docs/development/architecture.md | 39 +- docs/development/contributing.md | 7 + docs/development/installing-formulus-dev.md | 4 +- docs/getting-started/architecture-overview.md | 22 +- docs/getting-started/faq.md | 6 +- docs/getting-started/installation.md | 4 + .../installation/installing-formulus.md | 30 +- .../installation/installing-synkronus.md | 2 + docs/guides/deployment.md | 22 +- docs/guides/server-architecture-for-it.md | 146 ++ docs/implementer/implementer-overview.md | 4 +- docs/reference/configuration/server.md | 13 +- docs/reference/rest-api/authentication.md | 2 +- docs/reference/security.md | 170 ++ docs/reference/synkronus-server.md | 12 + docusaurus.config.ts | 4 +- package-lock.json | 1372 +++++++++++++++-- package.json | 1 + sidebars.ts | 2 + .../it-architecture-installation.drawio | 46 + .../diagrams/it-architecture-logical.drawio | 93 ++ static/img/it-architecture-installation.svg | 4 + static/img/it-architecture-logical.svg | 4 + 25 files changed, 1824 insertions(+), 202 deletions(-) create mode 100644 docs/guides/server-architecture-for-it.md create mode 100644 docs/reference/security.md create mode 100644 static/diagrams/it-architecture-installation.drawio create mode 100644 static/diagrams/it-architecture-logical.drawio create mode 100644 static/img/it-architecture-installation.svg create mode 100644 static/img/it-architecture-logical.svg diff --git a/docs/developer/developer-getting-started.md b/docs/developer/developer-getting-started.md index 310c243..57dce55 100644 --- a/docs/developer/developer-getting-started.md +++ b/docs/developer/developer-getting-started.md @@ -56,10 +56,11 @@ You want to deploy and manage ODE in your infrastructure. **Time to first deployment:** 1-2 hours **Next Steps:** -1. [Understand system architecture](/docs/development/architecture) -2. [Learn server configuration](/docs/reference/configuration/server) -3. [Follow deployment guide](/docs/guides/deployment) -4. [Set up monitoring](/docs/development/setup#monitoring) +1. [Server Architecture for IT](/docs/guides/server-architecture-for-it) +2. [Understand system architecture](/docs/development/architecture) +3. [Learn server configuration](/docs/reference/configuration/server) +4. [Follow deployment guide](/docs/guides/deployment) +5. [Security reference](/docs/reference/security) ### Path 4: Integration & APIs @@ -386,7 +387,7 @@ Choose your path and dive in: 1. **[Core Contributor Path](/docs/development/setup)** - Set up environment 2. **[Custom App Path](/docs/reference/rest-api/overview)** - Learn the APIs -3. **[Deployment Path](/docs/guides/deployment)** - Deploy ODE +3. **[Deployment Path](/docs/guides/server-architecture-for-it)** - IT overview, then [deployment guide](/docs/guides/deployment) 4. **[Integration Path](/docs/development/extending)** - Build integrations ### Read More diff --git a/docs/developer/developer-index.md b/docs/developer/developer-index.md index 56a50a8..fe8ed6a 100644 --- a/docs/developer/developer-index.md +++ b/docs/developer/developer-index.md @@ -7,6 +7,10 @@ title: ⚙️ For Developers Welcome to the **Developer** section! Whether you're contributing to ODE, building custom extensions, or deploying the platform, this guide covers development, architecture, and contribution workflows. +:::note Documentation paths +Persona pages live under `/docs/developer/`; technical guides under `/docs/development/`. Deploying for production? Start with [Server Architecture for IT](/docs/guides/server-architecture-for-it). +::: + ## Who This Guide Is For This section is designed for: @@ -122,7 +126,7 @@ Set up your development environment in 20 minutes: - [System Overview](/docs/development/architecture) - [Component Architecture](/docs/development/architecture#components) - [Data Flow & Sync Protocol](/docs/development/architecture#data-flow) -- [Authentication & Security](/docs/development/architecture#security) +- [Authentication & Security](/docs/reference/security) ### 🔧 Set Up Environment - [macOS Setup](/docs/development/setup#macos) diff --git a/docs/development/architecture.md b/docs/development/architecture.md index 4dc7cb2..a8ef0ba 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -4,25 +4,16 @@ sidebar_position: 2 # Architecture -Complete architecture documentation for ODE, including system overview, components, data flow, and technical details. +Complete architecture documentation for ODE, including sync protocol, database design, and extension points. + +:::tip Audience +**Hosting / IT?** See [Server Architecture for IT](/docs/guides/server-architecture-for-it). +**Product overview?** See [Architecture Overview](/docs/getting-started/architecture-overview). +::: ## System Overview -ODE follows a client-server architecture designed for offline-first data collection: - -``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ -│ Formulus │◄───────►│ Synkronus │◄───────►│ Formulus │ -│ (Mobile) │ Sync │ (Server) │ Sync │ (Mobile) │ -└─────────────┘ └──────────────┘ └─────────────┘ - │ │ │ - │ │ │ - ▼ ▼ ▼ -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ -│ Formplayer │ │ Database │ │ Formplayer │ -│ (WebView) │ │ (PostgreSQL) │ │ (WebView) │ -└─────────────┘ └──────────────┘ └─────────────┘ -``` +ODE follows a client-server architecture designed for offline-first data collection. High-level component relationships are documented in [Architecture Overview](/docs/getting-started/architecture-overview). Server installation layout is in [Server Architecture for IT](/docs/guides/server-architecture-for-it). ## Components @@ -155,7 +146,7 @@ Attachments are managed separately: - **Separate Sync**: Uploaded/downloaded in separate phase - **Manifest-based**: Server provides manifest of changes -See the [Synchronization guide](/using/synchronization) for more details. +See the [Synchronization guide](/docs/using/synchronization) for more details. ## Database Design @@ -203,11 +194,13 @@ See the [Synchronization guide](/using/synchronization) for more details. ### Data Security -- **Transport**: HTTPS enforced in production -- **Storage**: Database encryption via PostgreSQL +- **Transport**: HTTPS enforced in production (TLS terminated at your reverse proxy) +- **Storage at rest**: Encryption is a **host platform** responsibility (volume/disk encryption, managed DB TDE)—not a separate Synkronus feature - **Secrets**: Environment variables for sensitive data - **Validation**: Input validation on all endpoints +See [Security reference](/docs/reference/security) for deployment checklist and mobile storage details. + ## Performance Considerations ### Client-Side @@ -257,7 +250,7 @@ Current extension points: ## Related Documentation -- [Synchronization Details](/using/synchronization) -- [Sync Protocol Technical Details](/development/architecture) -- [Database Schema](/development/architecture) -- [API Reference](/reference/api) +- [Synchronization Details](/docs/using/synchronization) +- [Server Architecture for IT](/docs/guides/server-architecture-for-it) +- [Security reference](/docs/reference/security) +- [API Reference](/docs/reference/api) diff --git a/docs/development/contributing.md b/docs/development/contributing.md index a8582ca..523cac1 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -10,6 +10,13 @@ Guide to contributing to ODE, including contribution process, coding standards, ODE is an open-source project and welcomes contributions from the community. We believe that diverse perspectives and varied skill sets make our project stronger. +## Documentation URL convention + +- **`/docs/developer/*`** — Persona landing pages (overview, getting started paths) +- **`/docs/development/*`** — Substantive developer content (architecture, setup, component guides) + +When adding links in docs, use the full path prefix `/docs/...`. + ## Ways to Contribute You can contribute to ODE in many ways: diff --git a/docs/development/installing-formulus-dev.md b/docs/development/installing-formulus-dev.md index 2314ee6..dd3aa8e 100644 --- a/docs/development/installing-formulus-dev.md +++ b/docs/development/installing-formulus-dev.md @@ -161,7 +161,7 @@ adb install app-debug.apk ```bash # Download and install in one command -curl -L https://github.com/OpenDataEnsemble/ode/releases/download/v1.0.0/formulus.apk -o /tmp/formulus.apk +curl -L https://github.com/OpenDataEnsemble/ode/releases/download/v1.1.1/formulus.apk -o /tmp/formulus.apk adb install /tmp/formulus.apk ``` @@ -170,7 +170,7 @@ adb install /tmp/formulus.apk ```powershell # Download and install -Invoke-WebRequest -Uri "https://github.com/OpenDataEnsemble/ode/releases/download/v1.0.0/formulus.apk" -OutFile "$env:TEMP\formulus.apk" +Invoke-WebRequest -Uri "https://github.com/OpenDataEnsemble/ode/releases/download/v1.1.1/formulus.apk" -OutFile "$env:TEMP\formulus.apk" adb install "$env:TEMP\formulus.apk" ``` diff --git a/docs/getting-started/architecture-overview.md b/docs/getting-started/architecture-overview.md index dc90cf8..5067987 100644 --- a/docs/getting-started/architecture-overview.md +++ b/docs/getting-started/architecture-overview.md @@ -6,6 +6,8 @@ sidebar_position: 1 ODE (Open Data Ensemble) is a comprehensive platform for mobile data collection and synchronization. This guide explains the core architecture and components. +> **Current ODE release:** [v1.1.1](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.1.1) + ## Core Components ODE consists of four main components that work together to provide a complete data collection solution: @@ -102,6 +104,14 @@ Custom App Structure: - **Theme System**: Material Design 3 based theming - **Build Process**: Optimized bundle creation for deployment +Study-specific apps (for example AnthroCollect) are **app bundles** uploaded to Synkronus—not separate backend services. IT hosts only Synkronus, PostgreSQL, and a TLS reverse proxy. + +## Server deployment view + +![Installation view: reverse proxy, Synkronus container, PostgreSQL, and volumes](/img/it-architecture-installation.svg) + +For IT departments: see [Server Architecture for IT](/docs/guides/server-architecture-for-it) (full diagrams, security model, backups, sizing). + ## Data Flow ### Form Submission Flow @@ -137,9 +147,12 @@ Development -> Build -> ZIP Upload -> Server Storage -> Mobile Download ## Deployment Models ### Single Container Deployment -- **Synkronus**: Go server with embedded portal -- **Database**: PostgreSQL (separate container) -- **Mobile Apps**: Deployed via app stores or direct distribution +- **Synkronus**: Go server with embedded portal (`ghcr.io/opendataensemble/synkronus`) +- **Database**: PostgreSQL (container or managed service) +- **Reverse proxy**: TLS termination (Caddy in [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart); or your institutional proxy) +- **Mobile apps**: Formulus on field devices via Obtainium or F-Droid + +See [Server Architecture for IT](/docs/guides/server-architecture-for-it) and [Deployment guide](/docs/guides/deployment). ### Development Setup - **Local Development**: Docker Compose with hot reload @@ -149,7 +162,8 @@ Development -> Build -> ZIP Upload -> Server Storage -> Mobile Download ## Security Considerations - **Authentication**: JWT-based with configurable expiration -- **Data Encryption**: TLS for all communications +- **Data encryption in transit**: TLS at your reverse proxy (required in production) +- **Data encryption at rest**: Host platform responsibility (see [Security reference](/docs/reference/security)) - **Input Validation**: JSON Schema validation for all forms - **Access Control**: Role-based permissions for API endpoints diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index da3a365..2d8a4b9 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -6,6 +6,8 @@ sidebar_position: 5 Common questions about ODE installation, usage, and development. +> **Current ODE release:** [v1.1.1](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.1.1) (Synkronus container, Formulus APK, Desktop, Portal) + ## General Questions ### How do you pronounce ODE? @@ -36,7 +38,7 @@ See the [Installation](/docs/getting-started/installation) page for detailed sys ### Can I run ODE in the cloud? -Yes, ODE can be deployed to cloud platforms such as AWS, Google Cloud, or Azure. See the [Deployment guide](/guides/deployment/production) for details. +Yes, ODE can be deployed to cloud platforms such as AWS, Google Cloud, or Azure. See the [Deployment guide](/docs/guides/deployment) and [Server Architecture for IT](/docs/guides/server-architecture-for-it). ### Do I need a database? @@ -46,7 +48,7 @@ Yes, ODE requires PostgreSQL for data storage. The database schema is created au ### How do I create forms? -Forms are defined using JSON schema. See the [Form Design guide](/guides/forms/overview) for details. +Forms are defined using JSON schema. See the [Form Design guide](/docs/guides/form-design) for details. ### Can I customize the user interface? diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4c72ddf..c907e72 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -15,6 +15,10 @@ To run ODE you need two things: a **server** (Synkronus) that stores and syncs d Install the server first so that the client has something to connect to. Then install Formulus (or your client app) on each device and point it at your Synkronus server. +## For IT / infrastructure teams + +Hosting Synkronus for a study? See **[Server Architecture for IT](/docs/guides/server-architecture-for-it)** for a one-page overview: container layout, TLS, backups, and how custom apps (app bundles) relate to the server. Current platform release: **v1.1.1**. + ## Next steps - **[Install Synkronus](installation/installing-synkronus)** — Set up the server on a Linux machine or VPS. diff --git a/docs/getting-started/installation/installing-formulus.md b/docs/getting-started/installation/installing-formulus.md index c30e127..5253bb9 100644 --- a/docs/getting-started/installation/installing-formulus.md +++ b/docs/getting-started/installation/installing-formulus.md @@ -10,12 +10,11 @@ Complete guide for installing the Formulus mobile application on Android devices Formulus is available for Android devices through multiple installation methods. Choose the method that best fits your needs: -- **Obtainium** (Recommended) - App manager that installs Formulus directly from GitHub releases, including pre-release versions. Install Obtainium via F-Droid or direct download. -- **Direct APK** - Download and install the APK file directly from GitHub releases +- **Obtainium** (Recommended) - Installs Formulus from GitHub releases with automatic updates. Install Obtainium via F-Droid or direct download. +- **F-Droid** - Install Formulus directly from [F-Droid](https://f-droid.org/packages/org.opendataensemble.formulus/) +- **Direct APK** - Download and install the APK file directly from [GitHub releases](https://github.com/OpenDataEnsemble/ode/releases) (current: **v1.1.1**) - **Development Build** - For developers who want to build from source -**Note**: F-Droid is used to install Obtainium, which then installs Formulus. F-Droid does not directly install Formulus. - ## System Requirements Before installing, ensure your device meets these requirements: @@ -79,15 +78,15 @@ You have two options to install Obtainium: 3. **Enter the GitHub repository URL** in the "App source URL" field: - The field should contain: `https://github.com/OpenDataEnsemble/ode` - Ensure the full URL is entered correctly -4. **Configure GitHub options** (important for pre-release versions): - - **Enable "Include prereleases"** toggle - This is crucial for installing alpha/beta versions of Formulus - - **Enable "Fallback to older releases"** toggle - This allows Obtainium to use older releases if newer ones are unavailable +4. **Configure GitHub options** (for pre-release testing only): + - **Enable "Include prereleases"** if you need alpha/beta builds + - **Enable "Fallback to older releases"** if newer releases are unavailable 5. **Tap the "Add" button** to save the repository 6. **Wait for Obtainium to fetch** the repository information and available releases ![Obtainium Add App Screen](/img/installation/obtainium-add-app.png) -**Important**: Make sure "Include prereleases" is enabled (toggle switched to the right/purple) to see and install pre-release versions like `v1.0.0-alpha.12`. +**Stable release:** Install **v1.1.1** (or the latest [GitHub release](https://github.com/OpenDataEnsemble/ode/releases)). Pre-release toggles are only needed for alpha/beta testing. #### Step 3: Install Formulus @@ -98,7 +97,7 @@ You have two options to install Obtainium: - App name: **ode** - Developer: **OpenDataEnsemble** - Package: `org.opendataensemble.formulus` - - Latest version: `v1.0.0-alpha.12` (or current version) + - Latest version: **v1.1.1** (or current [release](https://github.com/OpenDataEnsemble/ode/releases)) - Status: **Not installed** 5. **Tap the "Install" button** at the bottom of the screen 6. **Confirm installation** when prompted: @@ -128,7 +127,16 @@ Obtainium will automatically check for updates: 5. **Confirm the update** when prompted 6. **App data is preserved** during update -### Method 2: Direct APK Installation +### Method 2: F-Droid + +Install Formulus directly from F-Droid (no Obtainium required): + +1. Install the [F-Droid](https://f-droid.org/) client if you do not already have it +2. Open F-Droid and search for **Formulus** (package `org.opendataensemble.formulus`) +3. Tap **Install** and wait for the download to complete +4. Updates are available through F-Droid when a new version is published + +### Method 3: Direct APK Installation If Obtainium is not available or you prefer direct installation: @@ -157,7 +165,7 @@ If Obtainium is not available or you prefer direct installation: 6. **Wait for installation** to complete 7. **Tap "Open"** to launch the app -### Method 3: Development Build +### Method 4: Development Build For developers who want to build and install from source, see the [Development Installation Guide](/docs/development/formulus-development). diff --git a/docs/getting-started/installation/installing-synkronus.md b/docs/getting-started/installation/installing-synkronus.md index 487ebd6..8949b1e 100644 --- a/docs/getting-started/installation/installing-synkronus.md +++ b/docs/getting-started/installation/installing-synkronus.md @@ -176,6 +176,8 @@ provisioning certificates, and your tunnel can handle HTTPS externally. Your Synkronus server is now running. +For production planning (TLS, backups, sizing, security), see **[Server Architecture for IT](/docs/guides/server-architecture-for-it)**. +

diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 6703fcd..995cc94 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -4,24 +4,30 @@ sidebar_position: 3 # Deployment -Complete guide to deploying ODE in production environments using Docker and Docker Compose. +Complete guide to deploying ODE in production environments using containers (Docker or Podman). -> **Quick start?** For a fast 5-minute setup with Docker or Podman, see the [Synkronus Quickstart](../getting-started/synkronus-quickstart.md) guide. It includes an automated installer and is perfect for testing or starting a production deployment. +> **IT overview?** See [Server Architecture for IT](./server-architecture-for-it) for a one-page infrastructure summary. +> **Quick start?** For a fast setup with automated TLS, see the [Synkronus Quickstart](../getting-started/synkronus-quickstart.md) (Podman/Docker + Caddy + PostgreSQL 17). ## Overview -ODE can be deployed using Docker containers, which simplifies deployment and ensures consistency across environments. This guide covers production deployment with Docker Compose, including PostgreSQL, Nginx reverse proxy, and optional Cloudflared tunnel for secure external access. +ODE production deployments center on the **Synkronus container image** (`ghcr.io/opendataensemble/synkronus`). The reference stack is [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart): Synkronus, PostgreSQL, and **Caddy** for TLS. Your IT team may use any hardened reverse proxy (Nginx, Apache, cloud load balancer) instead of Caddy—the requirement is **TLS termination** forwarding to Synkronus on port 8080. + +Pin the image tag in production (e.g. `ghcr.io/opendataensemble/synkronus:v1.1.1`), not `:latest`. ## Recommended Production Setup For production deployment, we recommend: - **Clean Linux server** (Ubuntu 22.04 LTS or Debian 12) -- **Docker & Docker Compose** installed -- **Cloudflared tunnel** for secure external access (no port forwarding needed) -- **PostgreSQL** database (dockerized via docker-compose) -- **Nginx** reverse proxy (included in docker-compose) -- **Persistent volumes** for data storage +- **Podman or Docker** with Compose +- **PostgreSQL** (container via quickstart, or managed service with `sslmode=require`) +- **TLS reverse proxy** (Caddy in quickstart; Nginx or institutional proxy equally valid) +- **Persistent volumes** for `pgdata` and Synkronus `appdata` +- **Backups** for database and `appdata` (see quickstart `utilities/`) +- **Security checklist** in [Security reference](/docs/reference/security) + +Optional: **Cloudflared tunnel** or similar if you prefer zero-trust ingress without opening inbound ports (not required when using a standard reverse proxy). ## Quick Start diff --git a/docs/guides/server-architecture-for-it.md b/docs/guides/server-architecture-for-it.md new file mode 100644 index 0000000..ea5e8e0 --- /dev/null +++ b/docs/guides/server-architecture-for-it.md @@ -0,0 +1,146 @@ +--- +sidebar_position: 2 +title: Server Architecture for IT +--- + +# Server Architecture for IT Departments + +One-page overview for infrastructure teams evaluating or hosting ODE (Synkronus). + +> **Current ODE release:** [v1.1.1](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.1.1) · **Reference stack:** [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart) + +## Summary + +ODE field collection runs on **mobile devices (Formulus)**. The **only server component ODE delivers** is **Synkronus** (an OCI container image). Study-specific applications (for example AnthroCollect) are **app bundles**—forms plus a small web UI—stored on and served by Synkronus. IT hosts Synkronus, PostgreSQL, and a TLS reverse proxy. + +Data classification and compliance (IRB, institutional policies, audit programs) are the **host organization's** responsibility. This page describes infrastructure only. + +## What ODE provides vs what the project provides + +| Deliverable | Provided by | Runs on | +|-------------|-------------|---------| +| Synkronus server image (`ghcr.io/opendataensemble/synkronus`) | ODE | Your server | +| Embedded admin Portal (`/portal`) | ODE (inside Synkronus) | Your server | +| Formulus mobile app | ODE | Field tablets/phones | +| PostgreSQL | Standard image (you operate) | Your server | +| TLS reverse proxy | **Your choice** (Caddy in quickstart; Nginx, Apache, cloud LB, etc.) | Your server | +| Custom app + forms (e.g. AnthroCollect) | Project team | Uploaded to Synkronus; synced to devices | + +## Logical architecture (end-to-end) + +![Logical architecture: field devices, Synkronus, and PostgreSQL](/img/it-architecture-logical.svg) + +**Traffic:** Field devices sync over **HTTPS** to the Synkronus REST API. Devices do not connect directly to PostgreSQL. The custom app UI runs inside a Formulus WebView and uses the same API through the Formulus bridge. + +## Installation view (server stack) + +![Installation view: reverse proxy, Synkronus container, PostgreSQL, and volumes](/img/it-architecture-installation.svg) + +Reference layout from [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart): + +| Container / role | Image | Purpose | +|------------------|-------|---------| +| Reverse proxy | Caddy 2 (quickstart) or IT-standard proxy | TLS termination, forward to Synkronus | +| `synkronus` | `ghcr.io/opendataensemble/synkronus:v1.1.1` | API, sync, auth, app-bundle hosting, Portal | +| `db` | `postgres:17` (quickstart) | Observations, users, metadata | + +### Common deployment variants + +1. **Colocated proxy** — Caddy or Nginx on the same VM as Synkronus (quickstart / typical self-hosted pattern). +2. **Institutional edge** — Your load balancer or reverse proxy terminates TLS; Synkronus runs on a private network only. + +### PostgreSQL: container vs managed + +| Pattern | When to use | Connection notes | +|---------|-------------|------------------| +| **A — Container** (quickstart default) | Pilot, single VM | `sslmode=disable` on the internal Docker/Podman network is normal | +| **B — Managed DB** (RDS, Azure, institutional service) | Enterprise production | Use `sslmode=require` in `DB_CONNECTION`; Synkronus still stores attachments on `appdata` | + +**Persistent data (operator responsibility):** + +| Volume | Contents | +|--------|----------| +| `pgdata` | PostgreSQL database | +| `appdata` | Synkronus `/app/data`: app bundles, attachment blobs | + +Backup scripts in the quickstart repo: `utilities/backup-db.sh`, `utilities/backup-attachments.sh`. + +## Security model + +| Concern | Responsibility | +|---------|----------------| +| TLS in transit | Reverse proxy (required in production); TLS 1.2+ | +| Encryption at rest | **Host platform** (disk/volume encryption, managed DB encryption)—ODE does not add a separate at-rest encryption layer inside containers | +| Authentication | JWT (Synkronus); roles `read-only`, `read-write`, `admin` | +| Secrets | `JWT_SECRET`, DB passwords, admin credentials — env vars or secret store | +| Network exposure | Public: proxy ports 443 (and 80 for ACME if used). Synkronus and Postgres not public | +| Rate limiting | **Not built into Synkronus** — configure at proxy/WAF | +| High availability | Single-node quickstart is typical; HA is an IT design choice | + +See [Security reference](/docs/reference/security) for the full checklist. + +## Field devices (brief) + +| Layer | Formulus behavior | Operator responsibility | +|-------|-------------------|-------------------------| +| Credentials | Stored in iOS Keychain / Android Keystore | Per-user Synkronus accounts; revoke on offboarding | +| Observation data | SQLite (WatermelonDB) in app private sandbox | Classify per study policy | +| Photos / attachments | App-private storage under the app sandbox | Same | +| Device encryption | Relies on OS full-disk encryption when device is locked | Recommend device passcode/biometric via policy or MDM | +| Android backup | `allowBackup="false"` | MDM remote wipe for lost devices | +| Network (iOS) | App Transport Security requires HTTPS | Serve Synkronus over HTTPS | +| Network (Android) | HTTP allowed with a user-visible warning | Enforce HTTPS in server URL policy | + +Synkronus does not provide certificate pinning. Institutional TLS inspection is possible if your CA is trusted on devices. + +## Distributing Formulus to field tablets + +| Method | Notes for IT | +|--------|----------------| +| **[Obtainium](https://github.com/ImranR98/Obtainium)** (recommended) | Installs/updates from GitHub Releases (`OpenDataEnsemble/ode`); may require allowing install from unknown sources for the installer | +| **[F-Droid](https://f-droid.org/packages/org.opendataensemble.formulus/)** | Direct install of Formulus | +| **MDM** | Institutions may sideload the APK via MDM — package `org.opendataensemble.formulus` | + +Details: [Installing Formulus](/docs/getting-started/installation/installing-formulus). + +## Sizing (starting point) + +| Scale | Suggested starting point | +|-------|--------------------------| +| Pilot / small study | 2 vCPU, 4 GB RAM, 40+ GB disk (attachments grow with photos) | +| Production | Tested backups; monitor `appdata` growth; pin image tags | + +Synkronus limits attachment uploads to **32 MB** per file. Configure your reverse proxy body size limit to at least 32 MB. + +## Reference deployment pattern + +Typical self-hosted pattern (e.g. research institutions running custom apps like AnthroCollect): + +1. Linux VM or cloud instance running Podman/Docker Compose +2. [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart) installer → Caddy + Synkronus + Postgres +3. DNS points to server; TLS via Let's Encrypt or institutional certificates on your proxy +4. Project team uploads the app bundle via Portal or `synk` CLI +5. Field tablets install Formulus **v1.1.1** via Obtainium or F-Droid; configure server URL in app settings + +Coordinate **Formulus and Synkronus versions** on upgrade—the mobile app checks server compatibility and may refuse sync on mismatch. + +## Operator checklist + +- [ ] Hardened reverse proxy with TLS (TLS 1.2+) +- [ ] Pin Synkronus image tag (e.g. `v1.1.1`) rather than `:latest` in production +- [ ] Proxy upload limit ≥ 32 MB per attachment +- [ ] Automated Postgres backups + tested restore +- [ ] Backup `appdata` volume (attachments + bundles) +- [ ] Volume/disk encryption at platform level +- [ ] Firewall: public access only to proxy; database not public +- [ ] Rate limiting on auth endpoints (proxy/WAF) +- [ ] Device passcode/biometric policy for field tablets +- [ ] Document who operates exports and admin Portal access + +## Further reading + +- [Install Synkronus](/docs/getting-started/installation/installing-synkronus) +- [Synkronus Quickstart](/docs/getting-started/synkronus-quickstart) +- [Deployment guide](/docs/guides/deployment) +- [Security reference](/docs/reference/security) +- [Synkronus Server Reference](/docs/reference/synkronus-server) diff --git a/docs/implementer/implementer-overview.md b/docs/implementer/implementer-overview.md index db2eb95..3076fa9 100644 --- a/docs/implementer/implementer-overview.md +++ b/docs/implementer/implementer-overview.md @@ -365,7 +365,7 @@ For large-scale projects (1000+ users): 1. **Understand your project needs** - What data do you need? 2. **Learn form design** → [Form Design Guide](/docs/guides/form-design) 3. **Design your first form** → [Form Controls Reference](/docs/reference/formplayer) -4. **Plan deployment** → [Deployment Guide](/docs/guides/deployment) +4. **Plan deployment** → [Server Architecture for IT](/docs/guides/server-architecture-for-it) and [Deployment Guide](/docs/guides/deployment) ### 📚 Resources @@ -378,7 +378,7 @@ For large-scale projects (1000+ users): - **Questions about form design?** → [Form Design Guide](/docs/guides/form-design) - **What controls can I use?** → [Form Controls Reference](/docs/reference/formplayer) -- **How do I deploy?** → [Deployment Guide](/docs/guides/deployment) +- **How do I deploy?** → [Server Architecture for IT](/docs/guides/server-architecture-for-it) and [Deployment Guide](/docs/guides/deployment) - **Help with specific issues?** → [Troubleshooting](/docs/using/troubleshooting) - **Need community support?** → [Get Help](/docs/community/getting-help) diff --git a/docs/reference/configuration/server.md b/docs/reference/configuration/server.md index f786dab..ec5d54b 100644 --- a/docs/reference/configuration/server.md +++ b/docs/reference/configuration/server.md @@ -8,18 +8,15 @@ Configuration options for Synkronus server. ## Overview -[Description placeholder] +Configuration options for Synkronus server are documented in the [Synkronus Server Reference](/docs/reference/synkronus-server). For production security and environment variables, see [Security reference](/docs/reference/security). ## Environment Variables -[Description placeholder] - -## Configuration Files - -[Description placeholder] +See [Synkronus Server Reference — Configuration](/docs/reference/synkronus-server#configuration). ## Related Content -- [Synkronus Configuration](/guides/configuration) -- [Production Deployment](/guides/deployment) +- [Server Architecture for IT](/docs/guides/server-architecture-for-it) +- [Security reference](/docs/reference/security) +- [Deployment guide](/docs/guides/deployment) diff --git a/docs/reference/rest-api/authentication.md b/docs/reference/rest-api/authentication.md index edd72ec..eae4836 100644 --- a/docs/reference/rest-api/authentication.md +++ b/docs/reference/rest-api/authentication.md @@ -437,5 +437,5 @@ console.log(userInfo); - [REST API Overview](/docs/reference/rest-api/overview) - [Deployment Guide](/docs/guides/deployment) - Set up server credentials -- [Security](/docs/development/security) - Security considerations +- [Security](/docs/reference/security) - Security considerations diff --git a/docs/reference/security.md b/docs/reference/security.md new file mode 100644 index 0000000..d3bf9b0 --- /dev/null +++ b/docs/reference/security.md @@ -0,0 +1,170 @@ +--- +sidebar_position: 9 +--- + +# Security + +Security practices for deploying and operating ODE (Synkronus, Formulus, and related components). + +:::info Compliance +ODE is **self-hosted research infrastructure**. Regulatory compliance (IRB, data classification, BAAs, audit programs) is the **host organization's** responsibility. This page describes technical controls ODE provides and what operators must add. ODE does **not** claim HIPAA, SOC 2, or similar certifications. +::: + +For a one-page infrastructure overview aimed at IT departments, see [Server Architecture for IT](/docs/guides/server-architecture-for-it). + +## Supported versions + +Security updates are provided for the latest release and the immediately preceding major version. **Current ODE release: [v1.1.1](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.1.1).** + +| Component | Supported | +|-----------|-----------| +| Synkronus | Latest release and previous major version | +| Formulus | Latest release and previous major version | +| Synkronus CLI | Latest release and previous major version | + +Keep server and mobile clients on compatible versions. See [Installing Formulus](/docs/getting-started/installation/installing-formulus). + +## Reporting a vulnerability + +**Do not report security vulnerabilities through public GitHub issues.** + +- **Email:** `security@opendataensemble.org` +- **GitHub:** [Private vulnerability reporting](https://github.com/opendataensemble/ode/security/advisories/new) + +Full disclosure policy and PGP key: [SECURITY.md on GitHub](https://github.com/OpenDataEnsemble/ode/blob/main/SECURITY.md). + +## Encryption and data protection + +### In transit + +- All production API traffic must use **HTTPS** (TLS 1.2+). +- Terminate TLS at a **reverse proxy or load balancer** you control (Caddy in [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart); Nginx, Apache, or cloud LB are equally valid). +- Formulus on **iOS** enforces App Transport Security (HTTPS). On **Android**, HTTP is allowed with a warning—enforce HTTPS via server URL policy. + +### At rest (server) + +- Database and attachment encryption at rest depends on the **underlying storage layer** (volume encryption, managed database TDE). Synkronus does not add a separate at-rest encryption layer inside the container. +- Encrypt database and volume **backups** before off-site storage. + +### On field devices + +| Data | Storage | +|------|---------| +| Login credentials | iOS Keychain / Android Keystore | +| Observations | SQLite (WatermelonDB) in app private sandbox | +| Photos / attachments | App-private directory in the sandbox | +| Android backup | Disabled (`allowBackup="false"`) | + +Recommend **device passcode or biometric lock** and **MDM remote wipe** for lost devices via institutional policy. Offline synced data remains on the device until wiped. + +### What ODE does not provide + +- Application-level database encryption on mobile devices +- Certificate pinning (institutional TLS inspection may work if your CA is on devices) +- Built-in rate limiting (configure at proxy/WAF) +- High availability or bundled monitoring (use `/health` and your log stack) +- Compliance certification + +## Authentication and access control + +- **JWT** access tokens (default **24 hours**); refresh tokens (**7 days**). +- Roles: `read-only`, `read-write`, `admin`. +- Passwords hashed with bcrypt on the server. +- Change default admin credentials immediately after deployment. +- Generate `JWT_SECRET` with `openssl rand -base64 32`. + +## Deployment security + +### Container images + +- Production: pin `ghcr.io/opendataensemble/synkronus:v1.1.1` (not `:latest`). +- Scan images for vulnerabilities as part of your supply-chain process. + +### Network + +```bash +# Example: allow only web ports on the host +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable +``` + +- Expose only the reverse proxy publicly. Keep Synkronus and PostgreSQL on internal networks. +- **Rate limiting:** not built into Synkronus—configure on the proxy, especially for login endpoints. + +### Database + +- **Container (quickstart):** `sslmode=disable` on internal Docker/Podman network is normal. +- **Managed PostgreSQL:** use `sslmode=require` in `DB_CONNECTION`. +- Limit database access to the Synkronus service only. + +### Secrets + +Never commit secrets to version control. Required secrets: + +| Variable | Purpose | +|----------|---------| +| `JWT_SECRET` | JWT signing | +| `DB_CONNECTION` | PostgreSQL (includes password) | +| `ADMIN_PASSWORD` | Initial admin (change after first login) | + +Use your platform's secret manager in production. + +### File uploads + +- Attachment limit: **32 MB** per file (configure proxy body size accordingly). +- Attachment IDs validated against path traversal. +- All attachment endpoints require authentication. + +### Portal + +The Synkronus Portal (admin UI) is embedded in the Synkronus binary at `/portal`. Protect it with TLS, strong passwords, and network ACLs where possible. + +## Mobile app (Formulus) + +- Signed release APKs; ProGuard/R8 on Android. +- Permissions: camera, storage, location as required by form features. +- Data syncs only to the server URL configured by the user—no third-party analytics pipeline in Formulus. + +Install paths: [Obtainium](https://github.com/ImranR98/Obtainium) (recommended) or [F-Droid](https://f-droid.org/packages/org.opendataensemble.formulus/). + +## Logging and monitoring + +- Synkronus logs to stdout (structured). Ship logs to your SIEM or log stack. +- Health check: `GET /health` on Synkronus (via proxy in production). +- Do not log passwords, tokens, or other secrets. + +## Deployment checklist + +Before production: + +- [ ] All default passwords changed +- [ ] Strong JWT secret generated +- [ ] HTTPS/TLS enabled (TLS 1.2+) +- [ ] Database connection uses SSL/TLS when not on a trusted internal network +- [ ] Firewall configured (proxy only public) +- [ ] Secrets stored securely (not in git) +- [ ] Postgres and `appdata` backups configured and restore tested +- [ ] Monitoring and log shipping configured +- [ ] OS and image dependencies patched +- [ ] Reverse proxy rate limiting configured +- [ ] Proxy upload limit ≥ 32 MB +- [ ] Synkronus image tag pinned (e.g. `v1.1.1`) +- [ ] Device passcode/MDM policy for field tablets + +## Security updates + +| Severity | Target response | +|----------|-----------------| +| Critical | Within 7 days when possible | +| High | Within 30 days | +| Medium | Within 90 days | + +Subscribe to [GitHub Security Advisories](https://github.com/opendataensemble/ode/security/advisories) for the ODE repository. + +## Related documentation + +- [Server Architecture for IT](/docs/guides/server-architecture-for-it) +- [Deployment guide](/docs/guides/deployment) +- [Synkronus Server Reference](/docs/reference/synkronus-server) +- [REST API Authentication](/docs/reference/rest-api/authentication) diff --git a/docs/reference/synkronus-server.md b/docs/reference/synkronus-server.md index 6590f44..ffe6987 100644 --- a/docs/reference/synkronus-server.md +++ b/docs/reference/synkronus-server.md @@ -8,10 +8,22 @@ Complete technical reference for the Synkronus server component. > **Want to get a server running quickly?** See the [Synkronus Quickstart](../getting-started/synkronus-quickstart.md) guide for a simple Docker/Podman setup with automated TLS provisioning. +> **IT / production hosting?** See [Server Architecture for IT](../guides/server-architecture-for-it) and [Security reference](./security). + ## Overview Synkronus is a robust synchronization API server built with Go. It provides RESTful endpoints for data synchronization, app bundle management, attachment handling, user management, and form specifications. The server uses PostgreSQL for data storage and JWT for authentication. +## Released container images + +Production deployments should pin a release tag rather than `:latest`: + +``` +ghcr.io/opendataensemble/synkronus:v1.1.1 +``` + +Images are published on [GitHub Container Registry](https://github.com/OpenDataEnsemble/ode/pkgs/container/synkronus) for each [ODE release](https://github.com/OpenDataEnsemble/ode/releases). + ## Architecture ### Technology Stack diff --git a/docusaurus.config.ts b/docusaurus.config.ts index cfa410d..10a16d0 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -16,12 +16,14 @@ const config: Config = { onBrokenLinks: 'throw', markdown: { - mermaid: false, + mermaid: true, hooks: { onBrokenMarkdownLinks: 'warn', }, }, + themes: ['@docusaurus/theme-mermaid'], + i18n: { defaultLocale: 'en', locales: ['en'], diff --git a/package-lock.json b/package-lock.json index ffc72ae..3cc946b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@docusaurus/core": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", + "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", @@ -331,6 +332,19 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2031,6 +2045,18 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3976,6 +4002,34 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", @@ -4577,6 +4631,23 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -4835,6 +4906,15 @@ "react": ">=16" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5289,6 +5369,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5357,6 +5690,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/gtag.js": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", @@ -5602,6 +5941,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5638,6 +5984,16 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vercel/oidc": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", @@ -7230,6 +7586,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -7547,172 +7912,698 @@ "engines": { "node": ">= 6" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.6.0.tgz", + "integrity": "sha512-7ZrRi/Z3cRL1d5I8RuXEWAkRFP3J4GeQRiyVknI4KC70RAU8hT4LysUZDe0y+fYNOktCbxE8sOPUOhyR12UqGQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/cssdb": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.6.0.tgz", - "integrity": "sha512-7ZrRi/Z3cRL1d5I8RuXEWAkRFP3J4GeQRiyVknI4KC70RAU8hT4LysUZDe0y+fYNOktCbxE8sOPUOhyR12UqGQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "MIT-0" - }, - "node_modules/cssesc": { + "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" + "d3-path": "^3.1.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" + "d3-array": "2 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" + "d3-time": "1 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "license": "MIT", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "css-tree": "~2.2.0" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debounce": { @@ -7876,6 +8767,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8014,6 +8914,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -8210,6 +9119,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -9310,6 +10229,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -9943,6 +10868,16 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9991,6 +10926,15 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10492,6 +11436,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10501,6 +11470,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10544,6 +11518,12 @@ "shell-quote": "^1.8.3" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10619,6 +11599,12 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -11191,6 +12177,48 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13563,6 +14591,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -13690,6 +14724,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -13771,6 +14811,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -15567,9 +16623,9 @@ } }, "node_modules/react-loadable-ssr-addon-v5-slorber": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", - "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz", + "integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.3" @@ -16190,6 +17246,24 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -16243,6 +17317,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -17063,6 +18143,12 @@ "postcss": "^8.4.31" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17285,6 +18371,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -17360,6 +18455,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index bdad1cb..cddb5c8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@docusaurus/core": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", + "@docusaurus/theme-mermaid": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", diff --git a/sidebars.ts b/sidebars.ts index 881b9e8..3562b80 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -96,6 +96,7 @@ const sidebars: SidebarsConfig = { 'guides/observation-queries', 'guides/ode-desktop-developer-mode', 'guides/custom-extensions', + 'guides/server-architecture-for-it', 'guides/deployment', 'guides/configuration', ], @@ -188,6 +189,7 @@ const sidebars: SidebarsConfig = { items: [ 'reference/configuration/client', 'reference/configuration/server', + 'reference/security', ], }, ], diff --git a/static/diagrams/it-architecture-installation.drawio b/static/diagrams/it-architecture-installation.drawio new file mode 100644 index 0000000..11830e0 --- /dev/null +++ b/static/diagrams/it-architecture-installation.drawio @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/diagrams/it-architecture-logical.drawio b/static/diagrams/it-architecture-logical.drawio new file mode 100644 index 0000000..bf7d6c9 --- /dev/null +++ b/static/diagrams/it-architecture-logical.drawio @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/it-architecture-installation.svg b/static/img/it-architecture-installation.svg new file mode 100644 index 0000000..4fad58c --- /dev/null +++ b/static/img/it-architecture-installation.svg @@ -0,0 +1,4 @@ + + + +
Internet / Field Networks
Host VM (Quickstart Stack)
Reverse Proxy (Caddy)
Ports: :443 (TLS)
Only Exposed Public Entrypoint
Synkronus Container
Port: :8080 (Internal Only)
ghcr.io/opendataensemble/synkronus:v1.1.1
appdata Volume
/app/data
(Bundles & Attachments)
[BACKUP TARGET]
PostgreSQL Container
Port: :5432 (Internal Only)
postgres:17
pgdata Volume
PostgreSQL Data

[BACKUP TARGET]
Mounts
Public Traffic (:443)
Proxy Pass (:8080)
Database Connection (:5432)
Mounts
\ No newline at end of file diff --git a/static/img/it-architecture-logical.svg b/static/img/it-architecture-logical.svg new file mode 100644 index 0000000..bfe3717 --- /dev/null +++ b/static/img/it-architecture-logical.svg @@ -0,0 +1,4 @@ + + + +
Field Tablet
Formulus
Local Offline DB
custom_app
Server Infrastructure
TLS Reverse Proxy
(e.g. nginx/caddy)
Synkronus Container
API + Embedded Portal
PostgreSQL
volume 
(attachments & app data)
HTTPS
App bundle download &
 sync of observations
\ No newline at end of file