From f5876f29ae5476a76ae15947420fab5d1b0bc50c Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Mon, 31 Aug 2026 19:14:51 +0200 Subject: [PATCH 1/4] chore: docs update --- docs/development/architecture.md | 1 + docs/guides/deployment.md | 4 ++++ docs/guides/server-architecture-for-it.md | 3 +++ docs/reference/formulus.md | 9 ++++++++- docs/reference/synkronus-server.md | 14 ++++++++++++++ docs/using/formulus-features.md | 8 +++++--- docs/using/synchronization.md | 19 +++++++++++++++++-- docs/using/troubleshooting.md | 2 ++ 8 files changed, 54 insertions(+), 6 deletions(-) diff --git a/docs/development/architecture.md b/docs/development/architecture.md index c68b7ed..5fdf2d0 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -223,6 +223,7 @@ See [Security reference](/docs/reference/security) for deployment checklist and - **Local Database**: Fast queries using WatermelonDB - **Incremental Sync**: Only sync changes since last sync +- **Adaptive pages**: Formulus starts at 32 pull / 4 push, grows toward 500 / 100, floor 1 - **Lazy Loading**: Load attachments on demand - **Caching**: Cache app bundles and form specifications diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 2b4cb32..7e1e96e 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -432,6 +432,10 @@ sudo ufw enable ## Performance Tuning +### Reverse proxy timeouts + +Field sync, photo upload, and app-bundle download can run for minutes on slow radio. The bundled [`nginx.conf`](https://github.com/OpenDataEnsemble/ode/blob/main/synkronus/nginx.conf) sets `proxy_send_timeout` and `proxy_read_timeout` to **600s**. If you use Caddy, Apache, or an institutional load balancer, set equivalent send/read (or idle) timeouts to at least 10 minutes. Leave login/refresh on the default short path — Synkronus already bounds `/api/auth/*` at 25s. + ### PostgreSQL Optimization Add to `docker-compose.yml` under postgres service: diff --git a/docs/guides/server-architecture-for-it.md b/docs/guides/server-architecture-for-it.md index 7f523ce..ea4b27d 100644 --- a/docs/guides/server-architecture-for-it.md +++ b/docs/guides/server-architecture-for-it.md @@ -112,6 +112,8 @@ Details: [Installing Formulus](/docs/getting-started/installation/installing-for Synkronus limits attachment uploads to **32 MB** per file. Configure your reverse proxy body size limit to at least 32 MB. +**Proxy timeouts:** field devices on slow radio can take minutes for a pull, push, photo, or app-bundle zip. Set reverse-proxy send/read timeouts to at least **600 seconds** (the bundled `nginx.conf` uses `proxy_send_timeout` / `proxy_read_timeout 600s`). Default nginx/Caddy idle values (~60s) will drop those transfers. Login and token refresh are bounded at **25 seconds** inside Synkronus; do not apply that short deadline to sync routes. + ## Reference deployment pattern Typical self-hosted pattern (e.g. research institutions running custom apps like AnthroCollect): @@ -129,6 +131,7 @@ Coordinate **Formulus and Synkronus versions** on upgrade—the mobile app check - [ ] Hardened reverse proxy with TLS (TLS 1.2+) - [ ] Pin Synkronus image tag (e.g. `v1.3.0`) rather than `:latest` in production - [ ] Proxy upload limit ≥ 32 MB per attachment +- [ ] Proxy send/read timeouts ≥ 600s (sync, attachments, bundle zip) - [ ] Automated Postgres backups + tested restore - [ ] Backup `appdata` volume (attachments + bundles) - [ ] Volume/disk encryption at platform level diff --git a/docs/reference/formulus.md b/docs/reference/formulus.md index 70cfcd1..83fb7d1 100644 --- a/docs/reference/formulus.md +++ b/docs/reference/formulus.md @@ -34,6 +34,7 @@ formulus/ │ ├── navigation/ # Navigation configuration │ ├── screens/ # Screen components │ ├── services/ # Business logic services +│ ├── sync/ # Adaptive pull/push sizes, retries │ ├── webview/ # WebView integration and bridge │ └── utils/ # Utility functions ├── android/ # Android native code @@ -68,6 +69,8 @@ Two-phase synchronization protocol: 1. **Observation Sync**: JSON metadata synchronization 2. **Attachment Sync**: Binary file synchronization +Pull and push unit sizes are **adaptive** (no enumerator preset). Fresh devices start at **32** observations per pull page and **4** per push batch, grow toward 500 / 100 on a fast link, and can shrink to **1** on very poor radio. Attachment downloads are serial. See [How Formulus sizes each request](/docs/using/synchronization#how-formulus-sizes-each-request). + ### Form Rendering Integration with Formplayer for form rendering: @@ -276,13 +279,17 @@ When opening forms programmatically, `openFormplayer` accepts: 3. **Push**: Send local changes to server 4. **Resolve Conflicts**: Handle conflicts if any +Page and batch sizes AIMD from conservative starts (pull **32**, push **4**) between floor **1** and ceiling **500** / **100**. Constants: `formulus/src/sync/networkProfile.ts`. Axios JSON timeout is 10 minutes. + #### Phase 2: Attachment Sync 1. **Download Manifest**: Get list of attachments to download -2. **Download Files**: Download missing attachments +2. **Download Files**: Download missing attachments (concurrency 1) 3. **Upload Files**: Upload pending attachments 4. **Update Status**: Mark attachments as synced +Observation JSON can complete while attachment files remain pending. + ### Sync State Management The app maintains sync state: diff --git a/docs/reference/synkronus-server.md b/docs/reference/synkronus-server.md index 9343c0e..be251ea 100644 --- a/docs/reference/synkronus-server.md +++ b/docs/reference/synkronus-server.md @@ -34,6 +34,19 @@ Images are published on [GitHub Container Registry](https://github.com/OpenDataE - **API**: RESTful HTTP API - **Documentation**: OpenAPI 3.0 specification +### HTTP timeouts + +Go `ReadTimeout` / `WriteTimeout` are **unset**. Those clocks start when the request begins, so a short cap (historically 15s) aborted legitimate sync, photo, and app-bundle transfers on slow radio. + +| Bound | Duration | Scope | +|-------|----------|--------| +| Request headers (`ReadHeaderTimeout`) | 25s | Slowloris protection only | +| Login / refresh | 25s | `http.TimeoutHandler` on `/api/auth/*` only | +| Keep-alive idle | 60s | Between requests | +| Reverse proxy send/read | 600s | Reference `nginx.conf` in the Synkronus tree | + +Do not wrap `/sync`, attachments, or bundle download in a short handler timeout. Pull `limit` default is 50, maximum 500 (OpenAPI). Formulus requests smaller pages first — see [Synchronization — How Formulus sizes each request](/docs/using/synchronization#how-formulus-sizes-each-request). + ### Project Structure ``` @@ -49,6 +62,7 @@ synkronus/ ├── auth/ # Authentication utilities ├── database/ # Database connection and migrations ├── logger/ # Structured logging + ├── httptimeout/ # ReadHeaderTimeout, auth TimeoutHandler ├── middleware/ # HTTP middleware └── openapi/ # OpenAPI generated code ``` diff --git a/docs/using/formulus-features.md b/docs/using/formulus-features.md index 5243f5b..ec621da 100644 --- a/docs/using/formulus-features.md +++ b/docs/using/formulus-features.md @@ -212,11 +212,13 @@ The app automatically syncs when: ### Sync Process -1. **Pull**: Download new forms and server data -2. **Push**: Upload pending observations -3. **Attachments**: Upload photos, audio, and other files +1. **Pull**: Download new forms and server data (starts at 32 observations per page) +2. **Push**: Upload pending observations (starts at 4 per batch) +3. **Attachments**: Upload photos, audio, and other files (one at a time) 4. **Confirmation**: Server acknowledges receipt +Observation records can finish syncing while photos are still transferring. Page sizes grow on a good link and shrink on a slow one; there is no Settings control for this. + ### Sync Indicators - **Sync Icon**: Appears in status bar when syncing diff --git a/docs/using/synchronization.md b/docs/using/synchronization.md index a6a08d3..5f82b67 100644 --- a/docs/using/synchronization.md +++ b/docs/using/synchronization.md @@ -46,7 +46,7 @@ Syncs observation records (forms) and their metadata: - Client updates its last seen `change_id` 2. **Push Phase** - Send local observations to server - - Client sends all unsync ed observations + - Client sends unsynced observations in small batches - Each observation includes a transmission ID for idempotency - Server validates and stores observations - Server returns success/failure for each record @@ -74,6 +74,21 @@ Syncs binary files attached to observations (photos, audio, documents): - Once successful, remove from local pending queue - Mark as synced +## How Formulus sizes each request + +Formulus does not expose a network-quality setting. Every device starts with **small** pages so a first attempt can finish on slow radio, then grows on a good link: + +| Direction | First attempt | Smallest (poor radio) | Largest (good link) | +|-----------|---------------|------------------------|---------------------| +| Pull (download observations) | 32 | 1 | 500 | +| Push (upload observations) | 4 | 1 | 100 | + +- Fast pages grow; slow or failed pages shrink (halve), down to one observation at a time. +- Photos and other attachments download **one at a time**. Observation JSON can finish while photos are still pending. +- There is nothing to configure in Settings for this. + +A reverse proxy in front of Synkronus must allow long transfers (about **10 minutes**). See [Server Architecture for IT](/docs/guides/server-architecture-for-it). + ## Understanding the Sync Algorithm ### Change Detection with `change_id` @@ -271,7 +286,7 @@ A conflict occurs when: ## Sync Settings -Users can configure synchronization behavior: +Users can configure when sync runs (interval, Wi-Fi vs cellular). **Page and batch sizes are automatic** — there is no network-quality picker. See [How Formulus sizes each request](#how-formulus-sizes-each-request). ### Auto-Sync Interval diff --git a/docs/using/troubleshooting.md b/docs/using/troubleshooting.md index 744996b..bd6aa66 100644 --- a/docs/using/troubleshooting.md +++ b/docs/using/troubleshooting.md @@ -483,6 +483,8 @@ If you encounter errors not covered here: - Check server performance and load - Verify attachment sizes are reasonable - Consider syncing during off-peak hours +- On slow radio, Formulus starts with small pages (32 pulled / 4 pushed) and can drop to one observation at a time. Observation data can finish while photos are still downloading. +- Confirm the reverse proxy allows ~10 minute transfers (see [Server Architecture for IT](/docs/guides/server-architecture-for-it)) ## Getting Additional Help From 4362537217f9a565a8a1b625037026f071791c67 Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Mon, 31 Aug 2026 19:19:33 +0200 Subject: [PATCH 2/4] chore: add container image tag info --- docs/development/building-testing.md | 12 +++++--- docs/guides/deployment.md | 41 ++++++++++++++++++++++------ docs/reference/synkronus-server.md | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/development/building-testing.md b/docs/development/building-testing.md index f1cf732..b1dc7be 100644 --- a/docs/development/building-testing.md +++ b/docs/development/building-testing.md @@ -234,10 +234,14 @@ The project uses GitHub Actions for continuous integration: ### Workflows **Synkronus Docker Build:** -- Triggers on push to `main` or PRs affecting `synkronus/` -- Builds Docker image -- Publishes to GitHub Container Registry -- Tags: `latest`, `v{version}`, `{branch-name}` +- Builds on relevant pushes to `main` or `dev`, pull requests, published GitHub Releases, and manual dispatches +- Publishes multi-platform images to GitHub Container Registry (pull requests build without publishing) +- Stable releases publish `v{version}`, major/minor pointers, and `latest` +- Pre-releases publish `v{version}-{pre}` and `latest-pre-release` +- Branch pushes publish `main` or `dev` plus an immutable `sha-{short}` tag +- Manual dispatches publish only `sha-{short}`; feature-branch images are not published automatically + +See the [Deployment guide](/docs/guides/deployment) for the image-tag channels and recommended uses. **Frontend Quality Checks:** - Runs on all PRs diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 7e1e96e..4e22ce9 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -160,20 +160,43 @@ services: Pre-built images are automatically published to GitHub Container Registry (GHCR) via CI/CD. -### Pull the Latest Image +### Choose an Image Tag + +Choose a deployment tag based on the update channel you want: + +| Tag | What it tracks | Recommended use | +|-----|----------------|-----------------| +| `latest` | Most recently published **stable** GitHub Release | Production deployments that intentionally auto-update between stable releases | +| `latest-pre-release` | Most recently published GitHub Release marked **pre-release** | Demo/staging deployments and Watchtower-managed pre-release testing | +| `dev` | Tip of the `dev` branch | Bleeding-edge integration testing; may contain unpublished work | +| `main` | Tip of the `main` branch | Testing current main between releases | +| `v1.2.3-alpha.4` | One specific pre-release | Reproducible pre-release deployment; does not auto-update | +| `v1.2.3` | One specific stable release | Reproducible production deployment; does not auto-update | +| `sha-abc1234` | One specific commit | Debugging or exact-build reproduction; does not auto-update | + +`dev` is a branch-head channel, **not** the published pre-release channel. To track published alpha or release-candidate images, use `latest-pre-release`: ```bash -docker pull ghcr.io/opendataensemble/synkronus:latest +docker pull ghcr.io/opendataensemble/synkronus:latest-pre-release +``` + +Versioned and moving release tags are produced only when a GitHub Release is **published**. Merely pushing a Git tag is not enough, and the release must be marked as a pre-release for `latest-pre-release` to move. Publishing a stable release updates `latest` but does not update `latest-pre-release`; there is no single tag that tracks the newest release regardless of whether it is stable or pre-release. + +Feature-branch images are not published automatically. A manually dispatched workflow run publishes only an immutable `sha-{short}` tag and does not move `latest`, `latest-pre-release`, `main`, or `dev`. + +### Automatic Updates with Watchtower + +Watchtower follows the tag configured on the running container. For a demo server that should receive each published pre-release, configure the Synkronus service with the moving pre-release tag: + +```yaml +services: + synkronus: + image: ghcr.io/opendataensemble/synkronus:latest-pre-release ``` -### Available Tags +Use `latest` instead to follow stable releases. Do not use a versioned tag such as `v1.2.3-alpha.4` if you expect automatic upgrades; versioned and `sha-*` tags identify fixed builds. -| Tag | Description | -|-----|-------------| -| `latest` | Latest stable release from main branch | -| `v1.0.0` | Specific version tags | -| `develop` | Development branch (pre-release) | -| `feature-xyz` | Feature branches (pre-release) | +For production, pin a tested version tag and perform controlled upgrades rather than relying on an automatically moving tag. ### Run Pre-built Image diff --git a/docs/reference/synkronus-server.md b/docs/reference/synkronus-server.md index be251ea..887fd93 100644 --- a/docs/reference/synkronus-server.md +++ b/docs/reference/synkronus-server.md @@ -22,7 +22,7 @@ Production deployments should pin a release tag rather than `:latest`: ghcr.io/opendataensemble/synkronus:v1.3.0 ``` -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). +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). See the [Deployment guide](/docs/guides/deployment) for stable, pre-release, and branch tracking channels. ## Architecture From 00991eb296c24fa63baf50cd4e11a0f73dca692e Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Tue, 1 Sep 2026 17:35:57 +0200 Subject: [PATCH 3/4] chore: release prep for v1.3.2 --- docs/collector/collector-getting-started.md | 32 +- docs/development/installing-formulus-dev.md | 12 +- docs/getting-started/architecture-overview.md | 2 +- docs/getting-started/faq.md | 2 +- docs/getting-started/installation.md | 7 +- .../installation/installing-formulus.md | 32 +- .../installation/installing-ode-desktop.md | 8 +- docs/guides/deployment.md | 2 +- docs/guides/server-architecture-for-it.md | 8 +- docs/reference/security.md | 6 +- docs/reference/synkronus-server.md | 2 +- docusaurus.config.ts | 9 + sidebars.ts | 5 + src/pages/downloads/index.tsx | 294 ++++++++++++++++++ src/pages/downloads/styles.module.css | 117 +++++++ 15 files changed, 482 insertions(+), 56 deletions(-) create mode 100644 src/pages/downloads/index.tsx create mode 100644 src/pages/downloads/styles.module.css diff --git a/docs/collector/collector-getting-started.md b/docs/collector/collector-getting-started.md index 8c280e4..841deb4 100644 --- a/docs/collector/collector-getting-started.md +++ b/docs/collector/collector-getting-started.md @@ -17,33 +17,25 @@ Before starting, make sure you have: ## Step 1: Install Formulus -### Option A: From Google Play Store (Android) +### Option A: F-Droid (Android) -1. Open **Google Play Store** on your Android phone -2. Search for **"Formulus"** -3. Tap **Install** -4. Wait for installation to complete (usually 1-2 minutes) +1. Open the [Formulus page on F-Droid](https://f-droid.org/en/packages/org.opendataensemble.formulus/) +2. Install the F-Droid client if prompted +3. Tap **Install** and wait for the download to complete -### Option B: From App Store (iOS) +### Option B: App Store (iPhone and iPad) -1. Open **App Store** on your iPhone -2. Search for **"Formulus"** -3. Tap **Get** -4. Authenticate with Face ID, Touch ID, or Apple ID -5. Wait for installation to complete +1. Open the [Formulus App Store page](https://apps.apple.com/dk/app/formulus/id6798318215) +2. Tap **Get** +3. Authenticate with Face ID, Touch ID, or Apple ID +4. Wait for installation to complete -### Option C: Direct Installation (Android) +### Option C: Obtainium or direct APK (Android) -If your project manager provided an APK file: - -1. Download the APK file to your phone -2. Open your file manager and locate the APK -3. Tap the file to install -4. If prompted, allow installation from "Unknown Sources" -5. Tap **Install** +Use [Obtainium](https://github.com/ImranR98/Obtainium) with `https://github.com/OpenDataEnsemble/ode` for updates from GitHub Releases, or download the APK directly from [Downloads](/downloads). :::note -If you don't have Google Play Store access or need a specific version, contact your project manager for a direct download link. +The [Downloads](/downloads) page has the current Android APK and all desktop/CLI downloads. ::: ## Step 2: Open Formulus & Connect to Your Project diff --git a/docs/development/installing-formulus-dev.md b/docs/development/installing-formulus-dev.md index 1be0fb4..8119c52 100644 --- a/docs/development/installing-formulus-dev.md +++ b/docs/development/installing-formulus-dev.md @@ -161,9 +161,9 @@ adb install app-debug.apk ```bash # Browse the release and download the arm64-v8a APK for most phones: -# https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.0 -# Asset names look like: formulus-v1.3.0-35-arm64-v8a-release-YYYYMMDD.apk -adb install /path/to/formulus-v1.3.0-*-arm64-v8a-release-*.apk +# https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.2 +# Asset names look like: formulus-v1.3.2-64-universal-release-YYYYMMDD.apk +adb install /path/to/formulus-v1.3.2-*-universal-release-*.apk ``` @@ -171,9 +171,9 @@ adb install /path/to/formulus-v1.3.0-*-arm64-v8a-release-*.apk ```powershell # Browse the release and download the arm64-v8a APK for most phones: -# https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.0 -# Asset names look like: formulus-v1.3.0-35-arm64-v8a-release-YYYYMMDD.apk -adb install "C:\path\to\formulus-v1.3.0-*-arm64-v8a-release-*.apk" +# https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.2 +# Asset names look like: formulus-v1.3.2-64-universal-release-YYYYMMDD.apk +adb install "C:\path\to\formulus-v1.3.2-*-universal-release-*.apk" ``` diff --git a/docs/getting-started/architecture-overview.md b/docs/getting-started/architecture-overview.md index 87c4bb5..944787e 100644 --- a/docs/getting-started/architecture-overview.md +++ b/docs/getting-started/architecture-overview.md @@ -6,7 +6,7 @@ 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.3.0](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.0) +> **Current ODE release:** [v1.3.2](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.2) · [Downloads](/downloads) ## Core Components diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index 8f9169c..0e7bba6 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -6,7 +6,7 @@ sidebar_position: 5 Common questions about ODE installation, usage, and development. -> **Current ODE release:** [v1.3.0](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.0) (Synkronus container, Formulus APK, Desktop, Portal) +> **Current ODE release:** [v1.3.2](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.2) (Synkronus container, Formulus, Desktop, CLI, Portal) · [Downloads](/downloads) ## General Questions diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index abff6ca..d9fd4ba 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -11,15 +11,16 @@ To run ODE you need two things: a **server** (Synkronus) that stores and syncs d | Component | What it is | Guide | |-----------|------------|--------| | **Server (Synkronus)** | Backend that hosts the API, portal, and database. Runs on a Linux server or VPS. | [Install Synkronus](installation/installing-synkronus) | -| **Client (Formulus)** | Mobile app for Android that field workers use to fill forms and sync data. | [Install Formulus](installation/installing-formulus) | +| **Client (Formulus)** | Mobile app for Android and iOS that field workers use to fill forms and sync data. | [Install Formulus](installation/installing-formulus) | 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.3.0**. +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.3.2**. ## Next steps - **[Install Synkronus](installation/installing-synkronus)** — Set up the server on a Linux machine or VPS. -- **[Install Formulus](installation/installing-formulus)** — Put the Formulus app on Android devices and connect it to your server. +- **[Downloads](/downloads)** — Get Formulus, ODE Desktop, or the Synkronus CLI for your platform. +- **[Install Formulus](installation/installing-formulus)** — Put the Formulus app on Android or iOS devices and connect it to your server. diff --git a/docs/getting-started/installation/installing-formulus.md b/docs/getting-started/installation/installing-formulus.md index 50af4a8..be08f45 100644 --- a/docs/getting-started/installation/installing-formulus.md +++ b/docs/getting-started/installation/installing-formulus.md @@ -4,15 +4,16 @@ sidebar_position: 2 # Installing Formulus App -Complete guide for installing the Formulus mobile application on Android devices. +Complete guide for installing the Formulus mobile application on Android and iOS devices. For the current links, see [Downloads](/downloads). ## Overview -Formulus is available for Android devices through multiple installation methods. Choose the method that best fits your needs: +Formulus is available for Android and iOS. Choose the method that best fits your device: -- **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.3.0**) +- **F-Droid** (recommended for Android) - Install Formulus directly from [F-Droid](https://f-droid.org/en/packages/org.opendataensemble.formulus/) +- **Obtainium** (Android) - Installs Formulus from GitHub releases with automatic updates. +- **Direct APK** (Android) - Download the current APK from [Downloads](/downloads) or [GitHub releases](https://github.com/OpenDataEnsemble/ode/releases). +- **App Store** (iPhone/iPad) - Install Formulus from the [Apple App Store](https://apps.apple.com/dk/app/formulus/id6798318215). - **Development Build** - For developers who want to build from source ## System Requirements @@ -22,6 +23,7 @@ Before installing, ensure your device meets these requirements: | Requirement | Minimum | |-------------|---------| | **Android Version** | Android 7.0 (API level 24) or higher | +| **iOS Version** | iOS 15.1 or higher | | **Storage Space** | 50 MB free space | | **Internet Connection** | Required for initial setup and synchronization | | **Permissions** | Camera, Storage, Location (for form features) | @@ -86,7 +88,7 @@ You have two options to install Obtainium: ![Obtainium Add App Screen](/img/installation/obtainium-add-app.png) -**Stable release:** Install **v1.3.0** (or the latest [GitHub release](https://github.com/OpenDataEnsemble/ode/releases)). Pre-release toggles are only needed for alpha/beta testing. +**Stable release:** Install **v1.3.2** (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 @@ -97,7 +99,7 @@ You have two options to install Obtainium: - App name: **ode** - Developer: **OpenDataEnsemble** - Package: `org.opendataensemble.formulus` - - Latest version: **v1.3.0** (or current [release](https://github.com/OpenDataEnsemble/ode/releases)) + - Latest version: **v1.3.2** (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: @@ -127,7 +129,7 @@ Obtainium will automatically check for updates: 5. **Confirm the update** when prompted 6. **App data is preserved** during update -### Method 2: F-Droid +### Method 2: F-Droid (recommended for Android) Install Formulus directly from F-Droid (no Obtainium required): @@ -136,13 +138,13 @@ Install Formulus directly from F-Droid (no Obtainium required): 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 +### Method 3: Direct APK Installation (Android) If Obtainium is not available or you prefer direct installation: #### Step 1: Download the APK -1. **Download the latest APK** from the [releases page](https://github.com/OpenDataEnsemble/ode/releases) +1. **Download the latest APK** from [Downloads](/downloads) or the [releases page](https://github.com/OpenDataEnsemble/ode/releases) 2. **Save the file** to your device's Downloads folder #### Step 2: Enable Unknown Sources @@ -165,7 +167,13 @@ If Obtainium is not available or you prefer direct installation: 6. **Wait for installation** to complete 7. **Tap "Open"** to launch the app -### Method 4: Development Build +### Method 4: App Store (iPhone and iPad) + +1. Open the [Formulus App Store page](https://apps.apple.com/dk/app/formulus/id6798318215) on your iPhone or iPad. +2. Tap **Get**, then authenticate with Face ID, Touch ID, or your Apple ID. +3. Wait for Formulus to install, then open it from your home screen. + +### Method 5: Development Build For developers who want to build and install from source, see the [Development Installation Guide](/docs/development/formulus-development). @@ -273,7 +281,7 @@ To verify that Formulus is installed correctly: ### Via Direct APK -1. **Download the latest APK** from the [releases page](https://github.com/OpenDataEnsemble/ode/releases) +1. **Download the latest APK** from [Downloads](/downloads) or the [releases page](https://github.com/OpenDataEnsemble/ode/releases) 2. **Install over existing installation** (no need to uninstall) 3. **App data is preserved** during update diff --git a/docs/getting-started/installation/installing-ode-desktop.md b/docs/getting-started/installation/installing-ode-desktop.md index 975c5c8..869eaef 100644 --- a/docs/getting-started/installation/installing-ode-desktop.md +++ b/docs/getting-started/installation/installing-ode-desktop.md @@ -6,9 +6,9 @@ sidebar_position: 3 Complete guide for installing **ODE Desktop** on Windows, macOS, and Linux. -:::info ODE v1.1.0 +:::info ODE v1.3.2 -ODE Desktop is part of the **ODE v1.1.0** release. Pre-built installers are published on [GitHub Releases](https://github.com/OpenDataEnsemble/ode/releases). +ODE Desktop is part of the **ODE v1.3.2** release. Use the [Downloads](/downloads) page for direct, platform-matched installers or browse [GitHub Releases](https://github.com/OpenDataEnsemble/ode/releases). ::: @@ -36,8 +36,8 @@ Choose the installation method that fits your role: ## Method 1: GitHub Releases (recommended) -1. Open [OpenDataEnsemble/ode releases](https://github.com/OpenDataEnsemble/ode/releases). -2. Select the **v1.1.0** release (or the latest stable tag). +1. Open [Downloads](/downloads) to download the installer matched to your platform, or open [OpenDataEnsemble/ode releases](https://github.com/OpenDataEnsemble/ode/releases). +2. Select the **v1.3.2** release (or the latest stable tag). 3. Download the artifact for your platform: | Platform | Typical artifact | diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 4e22ce9..3f3ad05 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -13,7 +13,7 @@ Complete guide to deploying ODE in production environments using containers (Doc 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.3.0`), not `:latest`. +Pin the image tag in production (e.g. `ghcr.io/opendataensemble/synkronus:v1.3.2`), not `:latest`. ## Recommended Production Setup diff --git a/docs/guides/server-architecture-for-it.md b/docs/guides/server-architecture-for-it.md index ea4b27d..db9ab19 100644 --- a/docs/guides/server-architecture-for-it.md +++ b/docs/guides/server-architecture-for-it.md @@ -7,7 +7,7 @@ title: Server Architecture for IT One-page overview for infrastructure teams evaluating or hosting ODE (Synkronus). -> **Current ODE release:** [v1.3.0](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.0) · **Reference stack:** [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart) +> **Current ODE release:** [v1.3.2](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.2) · [Downloads](/downloads) · **Reference stack:** [synkronus-quickstart](https://github.com/OpenDataEnsemble/synkronus-quickstart) ## Summary @@ -41,7 +41,7 @@ Reference layout from [synkronus-quickstart](https://github.com/OpenDataEnsemble | Container / role | Image | Purpose | |------------------|-------|---------| | Reverse proxy | Caddy 2 (quickstart) or IT-standard proxy | TLS termination, forward to Synkronus | -| `synkronus` | `ghcr.io/opendataensemble/synkronus:v1.3.0` | API, sync, auth, app-bundle hosting, Portal | +| `synkronus` | `ghcr.io/opendataensemble/synkronus:v1.3.2` | API, sync, auth, app-bundle hosting, Portal | | `db` | `postgres:17` (quickstart) | Observations, users, metadata | ### Common deployment variants @@ -122,14 +122,14 @@ Typical self-hosted pattern (e.g. research institutions running custom apps like 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.3.0** via Obtainium or F-Droid; configure server URL in app settings +5. Field tablets install Formulus **v1.3.2** via F-Droid, Obtainium, the App Store, or direct APK; 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.3.0`) rather than `:latest` in production +- [ ] Pin Synkronus image tag (e.g. `v1.3.2`) rather than `:latest` in production - [ ] Proxy upload limit ≥ 32 MB per attachment - [ ] Proxy send/read timeouts ≥ 600s (sync, attachments, bundle zip) - [ ] Automated Postgres backups + tested restore diff --git a/docs/reference/security.md b/docs/reference/security.md index a94d16d..2a99149 100644 --- a/docs/reference/security.md +++ b/docs/reference/security.md @@ -14,7 +14,7 @@ For a one-page infrastructure overview aimed at IT departments, see [Server Arch ## Supported versions -Security updates are provided for the latest release and the immediately preceding major version. **Current ODE release: [v1.3.0](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.0).** +Security updates are provided for the latest release and the immediately preceding major version. **Current ODE release: [v1.3.2](https://github.com/OpenDataEnsemble/ode/releases/tag/v1.3.2).** | Component | Supported | |-----------|-----------| @@ -77,7 +77,7 @@ Recommend **device passcode or biometric lock** and **MDM remote wipe** for lost ### Container images -- Production: pin `ghcr.io/opendataensemble/synkronus:v1.3.0` (not `:latest`). +- Production: pin `ghcr.io/opendataensemble/synkronus:v1.3.2` (not `:latest`). - Scan images for vulnerabilities as part of your supply-chain process. ### Network @@ -149,7 +149,7 @@ Before production: - [ ] OS and image dependencies patched - [ ] Reverse proxy rate limiting configured - [ ] Proxy upload limit ≥ 32 MB -- [ ] Synkronus image tag pinned (e.g. `v1.3.0`) +- [ ] Synkronus image tag pinned (e.g. `v1.3.2`) - [ ] Device passcode/MDM policy for field tablets ## Security updates diff --git a/docs/reference/synkronus-server.md b/docs/reference/synkronus-server.md index 887fd93..24fabc7 100644 --- a/docs/reference/synkronus-server.md +++ b/docs/reference/synkronus-server.md @@ -19,7 +19,7 @@ Synkronus is a robust synchronization API server built with Go. It provides REST Production deployments should pin a release tag rather than `:latest`: ``` -ghcr.io/opendataensemble/synkronus:v1.3.0 +ghcr.io/opendataensemble/synkronus:v1.3.2 ``` 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). See the [Deployment guide](/docs/guides/deployment) for stable, pre-release, and branch tracking channels. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index f7b1c46..d7512d4 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -73,6 +73,11 @@ const config: Config = { label: 'Documentation', position: 'right', }, + { + to: '/downloads', + label: 'Downloads', + position: 'right', + }, { label: 'Components', position: 'right', @@ -166,6 +171,10 @@ const config: Config = { label: 'Overview', to: '/docs', }, + { + label: 'Downloads', + to: '/downloads', + }, { label: 'Getting Started', to: '/docs/getting-started', diff --git a/sidebars.ts b/sidebars.ts index dd57e1c..a433de7 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -30,6 +30,11 @@ const sidebars: SidebarsConfig = { items: [ 'getting-started/why-ode', 'getting-started/key-concepts', + { + type: 'link', + label: 'Downloads', + href: '/downloads', + }, { type: 'category', label: 'Installation', diff --git a/src/pages/downloads/index.tsx b/src/pages/downloads/index.tsx new file mode 100644 index 0000000..73446aa --- /dev/null +++ b/src/pages/downloads/index.tsx @@ -0,0 +1,294 @@ +import React, {useEffect, useState} from 'react'; +import Layout from '@theme/Layout'; +import Link from '@docusaurus/Link'; + +import styles from './styles.module.css'; + +const RELEASE_API_URL = 'https://api.github.com/repos/OpenDataEnsemble/ode/releases/latest'; +const RELEASES_URL = 'https://github.com/OpenDataEnsemble/ode/releases'; +const F_DROID_URL = 'https://f-droid.org/en/packages/org.opendataensemble.formulus/'; +const OBTAINIUM_URL = 'https://github.com/ImranR98/Obtainium'; +const APP_STORE_URL = 'https://apps.apple.com/dk/app/formulus/id6798318215'; + +type Platform = 'macOS' | 'Windows' | 'Linux' | null; +type Architecture = 'arm64' | 'amd64'; + +type ReleaseAsset = { + name: string; + browser_download_url: string; +}; + +type Release = { + tag_name: string; + html_url: string; + assets: ReleaseAsset[]; +}; + +type PlatformDownload = { + label: string; + platform: Platform; + architecture: Architecture; + asset?: ReleaseAsset; + note: string; +}; + +function detectPlatform(): {platform: Platform; architecture: Architecture} { + if (typeof navigator === 'undefined') { + return {platform: null, architecture: 'amd64'}; + } + + const userAgent = navigator.userAgent.toLowerCase(); + const platform = navigator.platform.toLowerCase(); + const architecture: Architecture = + /arm64|aarch64|arm/.test(userAgent) || /arm64|aarch64|arm/.test(platform) + ? 'arm64' + : 'amd64'; + + if (/macintosh|mac os x/.test(userAgent)) { + return {platform: 'macOS', architecture}; + } + if (/windows/.test(userAgent)) { + return {platform: 'Windows', architecture}; + } + if (/linux/.test(userAgent)) { + return {platform: 'Linux', architecture}; + } + return {platform: null, architecture}; +} + +function findAsset( + assets: ReleaseAsset[], + required: string[], + extensions: string[], +): ReleaseAsset | undefined { + return assets.find(asset => { + const name = asset.name.toLowerCase(); + return ( + required.every(part => name.includes(part)) && + extensions.some(extension => name.endsWith(extension)) + ); + }); +} + +function DownloadLink({asset, children}: {asset?: ReleaseAsset; children: string}) { + return asset ? ( + + {children} + + ) : ( + + View release assets + + ); +} + +function DownloadTable({downloads}: {downloads: PlatformDownload[]}) { + return ( +
+ {downloads.map(download => ( +
+
+ {download.label} + {download.note} +
+ Download +
+ ))} +
+ ); +} + +export default function Downloads(): React.ReactElement { + const [release, setRelease] = useState(null); + const [loadFailed, setLoadFailed] = useState(false); + const [detected, setDetected] = useState<{platform: Platform; architecture: Architecture}>( + {platform: null, architecture: 'amd64'}, + ); + + useEffect(() => { + setDetected(detectPlatform()); + void fetch(RELEASE_API_URL) + .then(response => { + if (!response.ok) { + throw new Error(`GitHub responded with ${response.status}`); + } + return response.json() as Promise; + }) + .then(setRelease) + .catch(() => setLoadFailed(true)); + }, []); + + const assets = release?.assets ?? []; + const desktopDownloads: PlatformDownload[] = [ + { + label: 'macOS — Apple silicon', + platform: 'macOS', + architecture: 'arm64', + asset: findAsset(assets, ['ode-desktop-darwin-arm64'], ['.dmg', '.zip']), + note: 'M-series Mac', + }, + { + label: 'macOS — Intel', + platform: 'macOS', + architecture: 'amd64', + asset: findAsset(assets, ['ode-desktop-darwin-amd64'], ['.dmg', '.zip']), + note: 'Intel Mac', + }, + { + label: 'Windows — ARM64', + platform: 'Windows', + architecture: 'arm64', + asset: findAsset(assets, ['ode-desktop-windows-arm64'], ['.msi', '.exe']), + note: 'Windows on ARM', + }, + { + label: 'Windows — x64', + platform: 'Windows', + architecture: 'amd64', + asset: findAsset(assets, ['ode-desktop-windows-amd64'], ['.msi', '.exe']), + note: 'Most Windows PCs', + }, + { + label: 'Linux — ARM64', + platform: 'Linux', + architecture: 'arm64', + asset: findAsset(assets, ['ode-desktop-linux-arm64'], ['.appimage', '.deb', '.rpm']), + note: 'AppImage when available', + }, + { + label: 'Linux — x64', + platform: 'Linux', + architecture: 'amd64', + asset: findAsset(assets, ['ode-desktop-linux-amd64'], ['.appimage', '.deb', '.rpm']), + note: 'AppImage when available', + }, + ]; + const cliDownloads: PlatformDownload[] = [ + { + label: 'macOS — Apple silicon', + platform: 'macOS', + architecture: 'arm64', + asset: findAsset(assets, ['synkronus-cli-darwin-arm64'], ['']), + note: 'M-series Mac binary', + }, + { + label: 'macOS — Intel', + platform: 'macOS', + architecture: 'amd64', + asset: findAsset(assets, ['synkronus-cli-darwin-amd64'], ['']), + note: 'Intel Mac binary', + }, + { + label: 'Windows — ARM64', + platform: 'Windows', + architecture: 'arm64', + asset: findAsset(assets, ['synkronus-cli-windows-arm64'], ['.exe']), + note: 'Windows on ARM binary', + }, + { + label: 'Windows — x64', + platform: 'Windows', + architecture: 'amd64', + asset: findAsset(assets, ['synkronus-cli-windows-amd64'], ['.exe']), + note: 'Most Windows PCs', + }, + { + label: 'Linux — ARM64', + platform: 'Linux', + architecture: 'arm64', + asset: findAsset(assets, ['synkronus-cli-linux-arm64'], ['']), + note: 'ARM64 binary', + }, + { + label: 'Linux — x64', + platform: 'Linux', + architecture: 'amd64', + asset: findAsset(assets, ['synkronus-cli-linux-amd64'], ['']), + note: 'Most Linux PCs and servers', + }, + ]; + const recommendedDesktop = desktopDownloads.find( + download => + download.platform === detected.platform && + download.architecture === detected.architecture, + ); + const recommendedCli = cliDownloads.find( + download => + download.platform === detected.platform && + download.architecture === detected.architecture, + ); + const formulusApk = + findAsset(assets, ['formulus-', '-universal-'], ['.apk']) ?? + findAsset(assets, ['formulus-', 'arm64-v8a'], ['.apk']) ?? + findAsset(assets, ['formulus-'], ['.apk']); + + return ( + +
+
+
+

Open Data Ensemble

+

Downloads

+

+ Get the latest stable ODE applications. Download links below are loaded from the + latest GitHub release and are matched to your device when possible. +

+ {release ? ( +

Latest stable release: {release.tag_name}

+ ) : loadFailed ? ( +

Could not load the latest release automatically. Browse the GitHub releases.

+ ) : ( +

Loading the latest stable release…

+ )} +
+ +
+
+

Formulus

+

Mobile data collection for Android and iPhone/iPad.

+
+
+
+

Android

+

F-Droid is the recommended store for open-source Android apps.

+ Get Formulus on F-Droid +

Alternatively, use Obtainium with https://github.com/OpenDataEnsemble/ode for GitHub-release updates.

+ Download APK directly +
+ +
+
+ +
+
+

ODE Desktop

+

Manage observations, synchronize data, and work with forms and custom app bundles.

+ {recommendedDesktop ? ( +

Recommended for your device: {recommendedDesktop.label}

+ ) : null} +
+ {recommendedDesktop ? Download ODE Desktop : null} + +
+ +
+
+

Synkronus CLI

+

The synk command-line client for login, synchronization, bundles, and exports.

+ {recommendedCli ? ( +

Recommended for your device: {recommendedCli.label}

+ ) : null} +
+ {recommendedCli ? Download Synkronus CLI : null} + +

After downloading a macOS or Linux binary, make it executable with chmod +x <filename>. See the CLI reference for setup and usage.

+
+
+
+
+ ); +} diff --git a/src/pages/downloads/styles.module.css b/src/pages/downloads/styles.module.css new file mode 100644 index 0000000..4228e45 --- /dev/null +++ b/src/pages/downloads/styles.module.css @@ -0,0 +1,117 @@ +.page { + padding: 3rem 0 5rem; +} + +.header { + max-width: 48rem; + margin-bottom: 3rem; +} + +.header h1 { + margin-top: 0; +} + +.header > p { + font-size: 1.125rem; +} + +.eyebrow { + color: var(--ifm-color-primary); + font-size: 0.875rem !important; + font-weight: 700; + letter-spacing: 0.08em; + margin-bottom: 0.25rem; + text-transform: uppercase; +} + +.release { + color: var(--ifm-color-content-secondary); + font-size: 1rem !important; +} + +.product { + border-top: 1px solid var(--ifm-color-emphasis-300); + display: grid; + gap: 1.25rem; + padding: 2.25rem 0; +} + +.product h2, +.product h3 { + margin-top: 0; +} + +.product > div > p { + max-width: 46rem; +} + +.mobileOptions { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.mobileOptions article { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: var(--ifm-card-border-radius); + padding: 1.5rem; +} + +.mobileOptions article p { + min-height: 3rem; +} + +.downloadTable { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: var(--ifm-card-border-radius); + overflow: hidden; +} + +.downloadRow { + align-items: center; + border-bottom: 1px solid var(--ifm-color-emphasis-200); + display: flex; + gap: 1rem; + justify-content: space-between; + padding: 1rem 1.25rem; +} + +.downloadRow:last-child { + border-bottom: 0; +} + +.downloadRow span { + color: var(--ifm-color-content-secondary); + display: block; + font-size: 0.875rem; + margin-top: 0.125rem; +} + +.recommended { + color: var(--ifm-color-primary-dark); +} + +.hint { + color: var(--ifm-color-content-secondary); + margin: 0; +} + +@media screen and (max-width: 768px) { + .page { + padding-top: 2rem; + } + + .mobileOptions { + grid-template-columns: 1fr; + } + + .mobileOptions article p { + min-height: 0; + } + + .downloadRow { + align-items: flex-start; + flex-direction: column; + } +} From c286896e78ed958a5a972aff66a43aefa4c99ad3 Mon Sep 17 00:00:00 2001 From: Emil Rossing Date: Fri, 25 Sep 2026 13:43:57 +0200 Subject: [PATCH 4/4] updated sync instructions --- docs/guides/building-custom-apps-v2.md | 2 ++ docs/guides/custom-applications.md | 16 +++++++++++++ docs/guides/custom-extensions.md | 6 +++++ docs/reference/formulus.md | 31 +++++++++++++++++++++----- docs/using/custom-applications.md | 2 ++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/guides/building-custom-apps-v2.md b/docs/guides/building-custom-apps-v2.md index cb9740f..7502ae1 100644 --- a/docs/guides/building-custom-apps-v2.md +++ b/docs/guides/building-custom-apps-v2.md @@ -584,6 +584,8 @@ The Formulus API: ## API reference +The bridge also exposes synchronous `api.getProfileId()` and `api.getLocalStorageRef()` for profile-aware custom-app state. Use the storage reference instead of raw `localStorage` under the shared `file://` origin; see [profile-aware browser storage](./custom-applications.md#profile-aware-browser-storage) for examples and limitations. + ### getObservations(formName) ```javascript diff --git a/docs/guides/custom-applications.md b/docs/guides/custom-applications.md index 1b48fa2..35216b3 100644 --- a/docs/guides/custom-applications.md +++ b/docs/guides/custom-applications.md @@ -16,6 +16,22 @@ Custom applications are **web applications** (HTML, CSS, and JavaScript) that ru The **ODE repository** (Formulus, Formplayer, Synkronus Portal, design packages) uses **pnpm** — see [Development Setup](/docs/development/setup#package-manager-pnpm). **Your** custom app project can use **npm**, **pnpm**, or **yarn**; the examples below use common **npm** script names from the [custom_app](https://github.com/OpenDataEnsemble/custom_app) template. ::: You may author them with **any** stack—plain static files, **Vite**, **React**, **Vue**, **Svelte**, or another bundler—**as long as the build output** can be packaged as described in the [app bundle format](/docs/reference/app-bundle-format) (entry HTML, assets, and `forms/` layout). They provide specialized workflows, custom navigation, integration with the ODE form system, and interfaces tailored to your use case. +## Profile-aware browser storage + +Formulus hosts each custom app for the active **profile**. Users add and switch profiles in the in-app **Profiles** screen; the host remounts the WebView when switching. The host scopes observations, attachments, bundle files, and Formplayer drafts to the active profile. Custom apps should use the synchronous profile-aware browser storage reference after the bridge is ready: + +```javascript +const api = await getFormulus(); +const profileId = api.getProfileId(); // stable for this WebView; not the display name +const storage = api.getLocalStorageRef(); +storage.setItem('lastTab', 'home'); +const lastTab = storage.getItem('lastTab'); +storage.removeItem('lastTab'); +// storage.clear() removes only this profile's custom-app keys. +``` + +The reference uses physical keys `ode:{profileId}:app:{key}`. Both helpers are synchronous, and storage errors propagate. Do **not** use raw `localStorage` for profile-specific state: custom apps and dependencies loaded under a shared `file://` origin may read or write raw storage outside the namespace. This is organizational/storage namespacing, **not a sandbox or confidentiality boundary**; deleting a profile cannot guarantee removal of unrelated third-party raw keys. Depending on WebView file-access settings and device behavior, code in a custom app or form extension may also read other profiles' data or attachments via file access. A connected Synkronus server can distribute app bundles containing code; not every server necessarily does so. Connect only to trusted servers and install only trusted bundles. Do not store secrets in browser storage. See the [Formulus bridge reference](../reference/formulus.md#getprofileid-and-getlocalstorageref). + ## Scaffolding ODE does **not** require a special installer: start from a **standard** project scaffold (for example **`npm create vite@latest`** with React, Svelte, or Solid templates) and then align the **folder layout** with the app bundle spec. Copy-paste commands, a **Vite `outDir` example**, and a post-scaffold checklist are maintained in the **[custom_app](https://github.com/OpenDataEnsemble/custom_app)** repository README on GitHub (AI and author context for the Formulus API and forms live in that repo as well). diff --git a/docs/guides/custom-extensions.md b/docs/guides/custom-extensions.md index d01a52c..7e4f901 100644 --- a/docs/guides/custom-extensions.md +++ b/docs/guides/custom-extensions.md @@ -19,6 +19,12 @@ The extension system enables you to: - **Reusable Components** - Package extensions for distribution to other implementations - **Automatic Distribution** - Deploy via app bundles; users get updates automatically +## Profile-aware extensions + +Custom renderers and validators run in a Formplayer WebView for the **active host profile**. Formulus owns the profile's observation database, attachments, app bundle cache, and Formplayer draft storage. Switch profiles from the Formulus **Profiles** screen; switching remounts the WebView rather than changing its profile in place. + +For extension-owned browser preferences, use the synchronous bridge helpers `formulus.getProfileId()` and `formulus.getLocalStorageRef()` (after the bridge is ready). The latter supports `getItem`, `setItem`, `removeItem`, and `clear` for keys in `ode:{profileId}:app:{key}`; `clear` affects only this profile's app namespace. Avoid raw `localStorage`: a shared `file://` origin may expose raw keys to other profiles or third-party code. Profiles provide organizational/storage namespacing, **not a sandbox or confidentiality boundary**, nor a guarantee that third-party raw keys will be erased on profile deletion. Depending on WebView file-access settings and device behavior, code in a form extension or custom app may read other profiles' data or attachments via file access. A connected Synkronus server can supply bundles with such code, though connecting does not mean every server executes arbitrary code. Connect only to trusted servers and install only trusted app bundles; do not store secrets in browser storage. See [Formulus JavaScript interface](../reference/formulus.md#getprofileid-and-getlocalstorageref) and [custom-app storage guidance](./custom-applications.md#profile-aware-browser-storage). + ## Sub-observations (`format: sub-observation`) :::tip Built-in Formplayer control diff --git a/docs/reference/formulus.md b/docs/reference/formulus.md index 83fb7d1..2e00847 100644 --- a/docs/reference/formulus.md +++ b/docs/reference/formulus.md @@ -53,6 +53,12 @@ Formulus uses WatermelonDB for local data storage: - **App Bundles**: Custom application files cached locally - **Sync State**: Tracks synchronization status +### Profiles + +Formulus keeps local observations, attachments, app bundles, sync state, server connection, and credentials per profile. Open **Profiles** from the in-app menu (or tap the active profile in the drawer) to add, select, rename, or delete profiles. Switching profiles happens in the app and restarts/remounts the active app context; it does not change the profile of an already-open WebView. Configure the active profile's server URL and sign-in on **Profiles**, not in Settings. Deleting a profile removes its host-managed local observations, attachments, bundles, and namespaced browser keys when cleanup runs; it cannot guarantee removal of third-party raw browser keys. Sync anything you need to keep first. The last profile cannot be deleted. + +**Profiles provide organizational and storage namespacing, not a sandbox or confidentiality boundary.** Code in a custom app or form extension may be able to read data or attachments from other profiles through WebView file access (depending on platform and WebView configuration), and raw browser storage may expose keys across profiles. A connected Synkronus server can supply app bundles containing such code; this does not mean every server executes arbitrary code. Connect only to trusted servers and install only trusted app bundles. Do not store secrets in browser storage. + ### Custom Application Hosting Formulus hosts custom web applications in WebViews: @@ -61,6 +67,7 @@ Formulus hosts custom web applications in WebViews: - **JavaScript Bridge**: Communication between native and web - **Formulus API**: Injected JavaScript interface for custom apps - **Asset Loading**: Serves app bundle files from local storage +- **Profile storage**: Host-owned databases, files, bundles, and Formplayer browser keys are scoped to the active profile. Custom apps should use the profile-scoped storage bridge below for their own browser keys. ### Synchronization Engine @@ -98,6 +105,20 @@ const version = await api.getVersion(); ### Core Methods +#### getProfileId() and getLocalStorageRef() + +These methods are **synchronous** (do not `await` them). `getProfileId()` returns the immutable host profile ID captured when this WebView was created, not a profile label. `getLocalStorageRef()` returns a profile-scoped subset of browser storage: `getItem(key)`, `setItem(key, value)`, `removeItem(key)`, and `clear()`. It uses physical keys `ode:{profileId}:app:{key}`; `clear()` removes only the current profile's app keys, not Formplayer's keys or other storage. Storage errors (such as quota failures) propagate to the caller. + +```javascript +const api = await getFormulus(); +const profileId = api.getProfileId(); +const storage = api.getLocalStorageRef(); +storage.setItem('lastView', 'visits'); +const lastView = storage.getItem('lastView'); // 'visits' +``` + +Use this reference instead of raw `window.localStorage` for custom-app preferences. This is **namespacing, not a security sandbox**: custom apps and third-party code loaded from a shared `file://` origin may access raw browser storage across profiles. The host does not monkeypatch `localStorage` and cannot guarantee removal of unrelated third-party raw keys when a profile is deleted. Do not put secrets in WebView storage; audit dependencies that write to raw storage. See [Custom applications](../guides/custom-applications.md#profile-aware-browser-storage). + #### getVersion() Get the Formulus host version. @@ -302,12 +323,12 @@ The app maintains sync state: ### Server Configuration -Configured through Settings screen: +Configure the active profile on **Profiles**: + +- **Server URL**: Synkronus server address for this profile +- **Username and password**: Credentials for this profile; sign in from Profiles -- **Server URL**: Synkronus server address -- **Username**: User credentials -- **Password**: User password -- **Auto-login**: Enable automatic login +Use the in-app **Profiles** screen to switch servers/workspaces. **Settings** is for preferences such as language and theme, not server credentials. ### Sync Configuration diff --git a/docs/using/custom-applications.md b/docs/using/custom-applications.md index d0ba31c..99454fe 100644 --- a/docs/using/custom-applications.md +++ b/docs/using/custom-applications.md @@ -41,6 +41,8 @@ window.formulus.editObservation(formType, observationId); window.formulus.deleteObservation(formType, observationId); ``` +Once the bridge is ready (`const api = await getFormulus()`), `api.getProfileId()` identifies the active host profile for this WebView and `api.getLocalStorageRef()` provides synchronous profile-scoped `getItem`, `setItem`, `removeItem`, and `clear` methods. Prefer this to raw `localStorage` for custom-app state. The host remounts the WebView on an in-app **Profiles** switch; shared `file://` raw storage is not isolated from other profiles or third-party scripts. See [profile-aware browser storage](/docs/guides/custom-applications#profile-aware-browser-storage) for the API and security caveats. + ## Creating a Custom Application Custom applications are web-based interfaces that run within the Formulus mobile app. They integrate with ODE through the Formulus JavaScript interface.