# Technical documentation — mobile (LOGiiT)

Evidence date: 2026-10-09.

Source: `git@bitbucket.org:overdrive/mobile.git`, `main` at `8ef251f` (2026-01-08).

This describes the client repository. It is not an API specification for `app.logiit.co`.

## Stack

| Item | Value | Evidence |
|---|---|---|
| Language | Dart | `pubspec.yaml` |
| UI | Flutter, Material 3, seed colour deep purple in `MyApp` (screens largely use their own blacks and whites) | `lib/main.dart` |
| Package name | `logiit_flutter` | `pubspec.yaml` |
| Version | `1.3.0+82` (name + Android versionCode) | `pubspec.yaml`. `TEST_RESULTS.md` cites the same |
| In-app label | `v1.3.0 [27/10/2025 9:30]` | `lib/env.dart` `buildVersion` |
| Pinned SDK | Flutter 3.38.5 via FVM | `.fvm/fvm_config.json`, `README.md` |
| SDK constraint | `>=3.3.1 <4.0.0` | `pubspec.yaml` |
| State and routing | GetX 4 | `get` dependency, `GetMaterialApp` |
| Local database | Isar Community `3.3.0-dev.2` (`isar_community`, flutter libs, generator) | `pubspec.yaml` |
| HTTP | `package:http` inside `HttpService`. `dio` is in `pubspec.yaml` | `lib/services/http_service.dart`. Whether Dio is imported anywhere else was not required for the CRUD path |
| Background work | `workmanager` 0.9.0 | `pubspec.yaml`, `background_sync_service.dart` |
| Codegen | `build_runner`, `json_serializable`, Isar generator | `build.yaml` exists. Command in README: `flutter pub run build_runner build --delete-conflicting-outputs` |
| Lint | `flutter_lints` 6, `analysis_options.yaml` | repo root |
| Font | Montserrat variable, plus italic | `pubspec.yaml`, `assets/fonts/` |
| Icon | `assets/icons/LOGiiT_icon.jpg`, adaptive icon background `#ffffff` | `flutter_launcher_icons` block |

`publish_to: "none"`. The package is not published to pub.dev from this file.

## Repository layout

```text
android/          product flavors, application id, signing
ios/              Podfile platform 16.0
lib/              application
  main.dart       shared startup; default environment is dev
  main_*.dart     one environment each
  env.dart        titles and URLs
  config.dart     unused CodeCapsules URL and key
  controller/     network, app state, sync state
  enum/           user type, notification type, file types
  models/         JSON models (cloud)
  models_isar/    Isar models (device)
  pages/          screens
  services/       auth, CRUD, sync, files
  styles/         colours, spacing, radius, text
  utils/          logbook status labels
  widgets/        shell, inputs, dialogs
test/             widget_test.dart only
web/ linux/ macos/ windows/   Flutter platform runners
assets/           images and fonts
```

Generated `*.g.dart` files are committed next to their models.

Root notes that are not runtime: `README.md`, `CHANGELOG.md`, `Notes.md` (two Android run snippets), `TEST_RESULTS.md`, `REST_API_CONVERSION_ANALYSIS.md`, `credits.md`, `summary.html`, `summary_api.html`, `sum_of_summary.html`, `window_dump.xml`, `window_dump2.xml`, `window_dump3.xml`, `fix_web_build.ps1`, `screenshots/`. The HTML and XML dumps were not treated as requirements. `credits.md` contains plaintext passwords and is excluded from this document's examples.

## Startup

`mainWithEnvironment`:

1. `WidgetsFlutterBinding.ensureInitialized`.
2. `Workmanager().initialize(callbackDispatcher)` unless `kIsWeb`.
3. `AppLifecycleManager.instance.initialize` (defined with the services layer).
4. `AppEnvironment.setupEnv`.
5. Permanent GetX registrations: `LocalDatabaseProvider`, `NetworkController`, `StateController`, `SyncStateController`.
6. `runApp(MyApp)` inside `runZonedGuarded`. Uncaught zone errors are printed.

`MyApp` is `GetMaterialApp` with `home: SplashPage` and `onGenerateRoute` for login, landing, old logbook, projects, diver profile, notifications, supervisor profile, and ROV. Several other pages (`CompiitPage`, `LogbookSharingPage`, `ActionLogsPage`) are opened with `Get.off(() => ...)` and are not in that map.

`onGenerateRoute` uses `builder!(ctx)` with no null check. An unknown route name throws. **Implemented.**

## Environments

See the table in `project-plan.md`. Source of truth is `lib/env.dart`, not `lib/config.dart`.

Android flavor dimension `default` in `android/app/build.gradle`:

- `dev` → app name `LOGiiT - DEV`, id `co.logiit.app`
- `staging` → `LOGiiT - Staging`, id `co.logiit.app.staging`
- `demo` → `LOGiiT - Demo`, id `co.logiit.app`
- `production` → `LOGiiT`, id `co.logiit.app`

Kotlin JVM target 17. `minSdkVersion` and `targetSdkVersion` come from the Flutter Gradle plugin. Launcher icon config asks for Android min SDK 24. Exact `minSdk` integer is **Unknown** without the Flutter SDK on the machine that wrote this note.

iOS: `platform :ios, '16.0'` in `ios/Podfile`. The post-install hook still raises any pod below 13.0 up to 13.0, which does not lower the app platform.

README run examples:

```text
flutter run --flavor dev --target lib/main_dev.dart
flutter run --flavor staging --target lib/main_staging.dart
flutter run --flavor demo --target lib/main_demo.dart
flutter run --flavor production --target lib/main_production.dart
```

The same flavor and target pairs are documented for `flutter build apk`, `appbundle`, `ios`, and `ipa`. iOS commands in the README pass `--target` and do not repeat `--flavor`. Whether Xcode schemes are named to match is **Unknown / Requires Confirmation**.

`Notes.md` shows `flutter run -d emulator-5554 --flavor dev`.

Release signing: if `android/key.properties` exists and `storeFile` is set, use that keystore. Otherwise the release build type uses the debug signing config. README and changelog both say this.

## Local database

`lib/services/local_database_provider.dart` exports:

- `local_database_provider_mobile.dart` when not compiling for HTML
- `local_database_provider_web.dart` when `dart.library.html` is available

The web provider's `getLocalDatabase` throws `UnsupportedError` ("Isar database is not supported on web platform"). `SplashPage` skips `getLocalDatabase` and `SyncStatusService.startObserving` when `kIsWeb` is true. Later screens still call services that expect Isar. Web is a compile stub plus a Chrome-oriented commit subject, not a second database implementation.

`TEST_RESULTS.md` records IsarCore on libmdbx v0.13.8 during the November 2025 emulator run.

Schemas are registered from the mobile provider (the README says to add a new schema there). Each cloud model typically has a twin under `lib/models_isar/local_*`.

## HTTP API used by the client

Base: `{AppEnvironment.baseApiUrl}api/{route}`.

Headers from `HttpService.getHeaders`: `Authorization: Bearer {jwt}`, `Content-Type: application/json`, `Accept: application/json`. The JWT may be null; the header is still sent as `Bearer null` if `getJwtToken` returns null. **Implemented.**

`checkResult`:

- body text `null` → exception "No data found"
- status 200 or 201 → decoded JSON map
- else throw `message`, else `error`, else the decoded object

| Client method | HTTP | Path |
|---|---|---|
| `OnlineCrudService.find` | GET | `find/{collection}` |
| `findById` | GET | `find-by-id/{collection}/{id}` |
| `findByValue` (scalar) | GET | `find-by-value/{collection}/{name}/{value}` |
| `findByValue` (list) | POST | `find-by-value-set/{collection}` |
| `save` when `id != null` | PUT | `put/{collection}` |
| `save` when `id == null` | POST | `post/{collection}` |
| `delete` | DELETE | `delete/{collection}/{id}` |
| `saveToLinkTable` | POST | `post-link-table/{collection}` |
| `deleteFromLinkTable` | POST | `delete-link-table/{collection}` |
| login | POST | `login` |
| current user | GET | `user` |
| logout | POST | `logout` |
| profile update while online | PUT | `update-user` |
| reset email | POST | `reset-password` |
| upload | POST multipart | `uploadFile/{key}` plus form field `type` |
| secure URL | POST | `getSecureUrl` with `type` and `path` |

Offline file URL, not under `api/`: `{baseApiUrl}secure-file-download/{type}/{filename}`.

Browser, not `HttpService`: `{baseApiUrl}register`, `{baseApiUrl}forgot-password`.

CRUD responses are read as `response['body']`. Login reads `jwtToken` and `userId`. User reads `result['user']`. A login `message` field is thrown before the token is stored.

`REST_API_CONVERSION_ANALYSIS.md` proposes replacing these paths with resource URLs and estimates 31–40 hours, including a PHP/CodeIgniter backend that is **not** in this repository. The proposal is not implemented in `OnlineCrudService`.

`lib/config.dart` is unused by the HTTP layer. It contains a key literal. Rotate it if it was ever a live credential. The value is intentionally omitted here.

## Sync

`SyncService.serviceMap` binds `DataType` to a `LocalCrudService`. Registered types include dive, diver, stamp, profile (user), allergy, project, passport, dive types, competency, contractor, dive images, user details, certificates, notification, dive supervisor, specialized tasks, project contractor, project supervisor, breathing apparatus, medical certificates, company, vessel, competence, and the logbook contractor / company entry family. The map continues in `lib/services/sync_service.dart` from line 46.

`sync()`:

- returns immediately if a sync is already running
- waits up to the network controller's init window for connectivity
- loads all action logs
- replays create, update, delete, link, file upload, user update, stamp, share log sheet, and save-logbook actions
- writes sync status through `SyncStatusService`

`startManualSync` does not run `sync()` in the UI isolate. It registers Workmanager task `manualSync` with input `environment` and documents `dir`, then polls `SyncStatusService` every 500 ms.

Periodic task `backgroundSync` uses the same input plus `skipIfBackground: false`, frequency 15 minutes, constraint network connected. Preference default is off.

Callback isolate: `lib/services/workmanager_callback.dart` requires both `dir` and `environment` in input data.

Landing and COMPiiT also poll every 10 seconds on the UI isolate so a completed background sync refreshes the screen.

## Auth storage

| Secret | Store | Key | Cleared on logout |
|---|---|---|---|
| JWT | SharedPreferences | `jwt` | yes |
| User id | SharedPreferences | `offlineUserId` | yes |
| Password hash | FlutterSecureStorage | the email address | no |

Hash algorithm for the password is `UtilService.getHashPassword` (not `HashService`). `HashService` is the SHA-256 field hash for models.

## Domain model (client)

Cloud models under `lib/models/` (each folder is one type; `*.g.dart` is generated):

People and access: `user`, `user_details`, `kyc_status`, `passport`, `external_users`, `stamp`.

Dive log: `project`, `project_contractor`, `project_supervisor`, `contractor`, `dive`, `dive_times`, `dive_types`, `dive_supervisor`, `dive_images`, `dives_dive_images`, `dive_specialized_task`, `specialized_tasks`, `breathing_apparatus`, `allergy`, `allergy_user_details`, `certificates`, `medical_certificates`, `medical_certification_user_details`, `supervisor_certification`, `competency`, `competency_user_details`, `diver`, `diver_competency_userdetails`, rejection comment types, `save_dive_logbook`, `log_sheet`.

ROV / contractor logbook: `logbook_contractor_entry` and child rows for ROV, sensor, manipulator, file, notification, specialized tasks, vetter, vetter vetting; `logbook_company_entry_vetter` and vetting; `logbook_entry_statuses`; `rov_manufacturer`, model, and unit; `industry`, `industry_logbooks`, sensors, manipulators; `vessel`.

Company competence (COMPiiT): `company`, `company_project`, `company_project_user`, `company_logbooks`, `company_competencies`, `company_competence_assessment`, `company_competence_dive`, `competence`, `competence_category`, `competence_sub_sections`, `competence_assessment_criteria`, `custom_competence`, `competency_documents`.

Other: `user_notification`, `vetting_status`, `vetter_type`.

`UserType` ordinal `id` is `index + 1` (`diver` = 1 … `empty` = 7). **Inferred** that the API may store the same integers. Confirm before relying on it. The JSON role used by `getUserType` is the lower-case name (`rov_pilot`, not the title case).

`DepthUnitPreference`: `meters`, `feet`.

## UI map

| Route constant | Widget | Who reaches it |
|---|---|---|
| splash is `home`, no named route | `SplashPage` | launch |
| `LoginPage.route` | login | unauthenticated |
| `/landing` | `LandingPage` | authenticated dashboard |
| projects | `ProjectsPage` | diver drawer |
| ROV | `ROVPage` | ROV pilot drawer |
| `/compiit` | `CompiitPage` | contractor drawer. Not in `onGenerateRoute` |
| notifications | `NotificationPage` | approver drawer |
| diver profile | `DiverProfilePage` | contractor Manage Profile |
| supervisor profile | `SupervisorProfilePage` | everyone else Manage Profile |
| old logbook route | `LogbookPageOld` | diver Share Logbook |
| sharing page | `LogbookSharingPage` | ROV SHARiiT |
| action logs | `ActionLogsPage` | long-press on the version footer |

Shell chrome (`PageState` / `app_page.dart`): black drawer, LOGiiT logo header, offline banner, student chip, periodic sync toggle.

## Permissions, scanner, files

`permission_handler` requests camera, photos or storage, battery-optimisation ignore, and location. Gallery rules are in business rule BR-027.

`image_picker`, `file_picker`, `open_filex`, `flutter_cache_manager`, and `pdfrx` support pick, open, cache, and PDF view. `mobile_scanner` is used on a COMPiiT widget.

`country_picker_pro` and `intl_phone_field` support country and phone inputs. `card_swiper` is a dependency; which screen swipes cards was not required for the main workflows.

## Security notes (no secret values)

- JWT is in SharedPreferences.
- Offline password hash remains after logout.
- `credits.md` at the repo root stores two emails and passwords in plaintext.
- `lib/config.dart` stores an API key that the app does not read.
- Dev and the default `main()` use `http://10.0.2.2/` (cleartext). Staging, demo, and production bases are HTTPS.
- Login POST sends the password in JSON, not the hash. The hash is only for the later offline check.
- File download while offline uses a constructed URL on the API host. Whether that route is authenticated is **Unknown** (the client does not show an extra header on that string; the online path uses `getSecureUrl`).

## Testing and CI

| Check | What exists |
|---|---|
| Automated | `test/widget_test.dart`: `AppEnvironment.setupEnv` for dev and production title and base URL. Comments say NetworkController widget tests are skipped because of timers. `tearDown` waits 12 seconds |
| Manual | `TEST_RESULTS.md`, 17 November 2025, Android 16 emulator, dev flavor, debug-signed `app-dev-release.apk` 81.4MB, package `co.logiit.app`, Impeller, Isar init, splash and auth check. Recommends real-API, login, and offline tests that are marked as next steps, not results |
| Pipeline | No `bitbucket-pipelines.yml` or GitHub workflow in the tree |
| Analysis | `analysis_options.yaml`, `devtools_options.yaml` |

This documentation pass did not run `flutter test` or a build.

## Platforms

| Target | Evidence of product support |
|---|---|
| Android | Flavors, README, `TEST_RESULTS.md`. Strongest evidence |
| iOS | Podfile 16.0, README `flutter build ios` / `ipa`. No test report in-repo |
| Web | Platform folder, `kIsWeb` branches, web Isar stub that throws, commit "chrome working", `fix_web_build.ps1`. Not a working offline database |
| Linux, macOS, Windows | Flutter platform directories only |

## Operations

No Dockerfile, Terraform, or deploy script for the app binary is in the repo. Distribution evidence is local `flutter build` plus an emulator `adb install` in the README. Store accounts and the production keystore are not in git.

The API hosts are operational dependencies:

- `https://app.logiit.co/`
- `https://staging-app.logiit.co/`
- `https://demo-app.logiit.co/`
- developer machine via `http://10.0.2.2/` for the dev flavor
- offline probe hosts `logiit.local` and `4c26c0265b2cad392ae77c1e1de0bd10.balena-devices.com`

Who operates those hosts is **Unknown / Requires Confirmation**. The Balena hostname identifies a device id. Confirm it is still current before documenting it as production infrastructure; the code uses it for staging and demo only.

## Developer extension point

README procedure to add a model: cloud model, Isar model, service cloned from an existing one, schema list, `DataType` and embedded payload on the action log, `SyncService.serviceMap`, then `build_runner`. That procedure matches the code structure. It is **Documented** and **Implemented** as the pattern, not as a generator CLI.
