# PRD — mobile (LOGiiT)

Evidence date: 2026-10-09.

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

No prior PRD exists in the source repository or in `silicon_ark/mobile/`. Requirements below are recovered from the implementation and from `README.md`, `CHANGELOG.md`, and `TEST_RESULTS.md`. Provenance is labelled. Secrets in `credits.md` and `lib/config.dart` are not reproduced.

## Product purpose

**Implemented.** LOGiiT (`logiit_flutter`, version `1.3.0+82`) is a mobile client for dive and ROV logbooks. After login it keeps projects, dives, tours, profile data, and related records in an on-device Isar database and sends them to a LOGiiT HTTP API. People who approve work use a notifications screen. People who log work use projects, ROV tours, COMPiiT, and a sharing screen.

The API, the registration site, and the web profile are not in this repository. The app refuses login when user details are missing and tells the person to complete the profile on the web.

**Unknown / Requires Confirmation:** the commercial owner of LOGiiT, the regulatory purpose of the logbook, and whether this client is the only client of `https://app.logiit.co/`.

## Users and roles

Roles are the `UserType` enum in `lib/enum/user_type.dart`: `diver`, `supervisor`, `admin`, `rov_pilot`, `rov_supervisor`, `company`, `empty`.

| Actor | How the app treats them | Status |
|---|---|---|
| Diver | Drawer: Dashboard, Projects, COMPiiT, Manage Profile (diver profile), Share Logbook. Share Logbook is hidden when `isStudent` is true. Hours on the dashboard come from dives on completed projects owned by their user-details id | **Implemented** |
| Supervisor | Drawer: Dashboard, Notifications, Manage Profile (supervisor profile). Dashboard title for projects is "Approved Dives". Hours count dives where `approvedBy` is their user-details id and the project is completed | **Implemented** |
| ROV pilot | Drawer: Dashboard, ROV Tours, COMPiiT, Manage Profile (diver profile page), SHARiiT. `industryLogbookId` is `2` | **Implemented** |
| ROV supervisor | Drawer: Dashboard, Notifications, Manage Profile (supervisor profile). ROV hours count enabled logbook entries with status id `3`, `approverId` equal to the user id. `industryLogbookId` is `2` | **Implemented** |
| Student diver | Same as diver, plus a "Student" chip in the shell, and Share Logbook is hidden | **Implemented** (`User.isStudent`) |
| Admin | `User.isAdmin()` exists. No page checks it for navigation | **Implemented** as a flag only. Product behaviour **Unknown** |
| Company | Enum value only. No page checks `UserType.company` | **Unknown** whether this is an app user |
| Person who is not logged in | Splash sends them to `LoginPage` | **Implemented** |
| Deploy / build operator | README flavor commands. No in-app admin for releases | **Documented** in README |

`User` also groups roles:

- `isDive()`: diver or supervisor
- `isRov()`: ROV pilot or ROV supervisor
- `isContractor()`: diver or ROV pilot
- `isApprover()`: supervisor or ROV supervisor

COMPiiT approval UI offers the labels `ODM`, `LSS`, and `Supervisor` (`view_compiit_page.dart`). Those strings are not `UserType` values. **Implemented** as a dropdown. How they map to accounts is **Unknown / Requires Confirmation**.

## Systems the product talks to

1. The LOGiiT API at `AppEnvironment.baseApiUrl` + `api/...`, with header `Authorization: Bearer {jwt}`.
2. Browser pages `{baseApiUrl}register` and `{baseApiUrl}forgot-password`.
3. An offline-API host (`logiit.local` or a Balena device hostname) probed only from the network-status dialog.
4. On-device Isar (mobile), SharedPreferences (JWT, user id, periodic-sync flag), and FlutterSecureStorage (password hash keyed by email).
5. Workmanager, for a one-off sync task and an optional 15-minute periodic task. Not started on web.
6. Device camera, photos/storage, and battery-optimisation permission APIs (`PermissionService`). Location permission can be requested; which screen requires it was not traced to a user story.
7. Facebook `https://www.facebook.com/Logiit.stiint`, Instagram `https://www.instagram.com/logiit._/`, LinkedIn `https://www.linkedin.com/company/logiit` from the login screen.
8. `lib/config.dart` names `https://appstrax-services-lawm.codecapsules.co.za` and an API key. Nothing in `lib/` reads `Config.apiUrl` or `Config.apiKey`. **Implemented** as dead configuration. Do not treat it as a live integration.

There is no payment SDK, push-notification SDK, or analytics SDK in `pubspec.yaml`.

## Major workflows

### WF-1 — Cold start

`SplashPage` shows `assets/images/logiitLogo-noback.png`. On non-web it opens Isar and starts `SyncStatusService` observation, then `AuthService.isAuthenticated()`.

- No JWT and no offline user id, or `getUser()` fails: go to `LoginPage`.
- A user object is loaded: go to `LandingPage`.

Provenance: **Implemented**. `TEST_RESULTS.md` says this path ran on an Android 16 emulator for the dev flavor (17 November 2025).

### WF-2 — Online login

`LoginPage` requires a non-empty email and password (`isValid`). Email is passed through `UtilService.formatEmail`. The button shows a spinner while the call runs.

`AuthService.login` posts `api/login` with the email and password JSON. On success it stores `jwtToken` in SharedPreferences key `jwt`, `userId` in key `offlineUserId`, and a hash of the password in secure storage under the email.

Then `getUser()` GETs `api/user`.

Gates after that, in order:

1. `emailVerifiedAt == null` → logout and error "Please verify email before logging in".
2. If connected, sync and fetch user details. If user details are still null → logout and error "Please complete profile on web before app logging in."
3. If connected, `SyncService.startManualSync()` (does not block navigation).
4. Navigate to `LandingPage`.

Provenance: **Implemented**.

"Create an account" opens `{baseApiUrl}register` with `url_launcher`. A short press on "Forgot Password?" opens `{baseApiUrl}forgot-password`. A long press opens `NetworkStatusDialog`. The password field has a visibility toggle (`CHANGELOG` v1.3.0; **Implemented** in `login_page.dart`).

### WF-3 — Offline login

If `NetworkController` is not connected, login does not call the API. It loads a user by email from the local database and compares the secure-storage hash with `UtilService.getHashPassword` of the typed password.

Failures:

- No stored hash or no local user: "User Not locally found, please log in while online".
- Hash mismatch: "Password incorrect".

Success stores `offlineUserId` only. It does not refresh the JWT.

`isAuthenticated()` returns false when there is no JWT **and** the device is disconnected **and** there is no offline user id. A previously stored offline user id is enough to attempt `getUser()` from Isar while offline.

Provenance: **Implemented**.

### WF-4 — Logout

If a sync status row is `in_progress`, logout cancels the one-off and periodic Workmanager tasks and marks the sync failed with reason `User logged out`, category `user_logged_out`.

If connected and still authenticated, it POSTs `api/logout`. It always removes `jwt` and `offlineUserId` and clears the in-memory user and user details. The secure-storage password hash is not deleted in `logout()`.

Provenance: **Implemented**.

### WF-5 — Dashboard

`LandingPage` re-checks authentication, loads contractor logbook entries and ROV rows, then stamps, dives, projects, users, user details, KYC, breathing apparatus, dive types, and specialized tasks. Missing user details logs the person out with "User details not found, contact support."

Every 10 seconds it polls sync status. `completed` reloads the same data and shows "Sync completed". `failed` shows the error. If the session is gone, it opens `LoginPage`.

Project lists and hour totals depend on role (see Users and roles). Dive-role hours use `DiveService.getDiveTimes` on dives whose project is completed. ROV-role hours use tours whose logbook entry status id is `3` and whose project is completed (pilot) or whose approver is the current user (ROV supervisor).

Provenance: **Implemented**.

### WF-6 — Record a project and a dive (diver)

Drawer item Projects is visible only for `isDiver()`.

Saving a project (`project_widgets.dart`) requires name and other fields via `validateInput` (`*required` when empty). It sets `industryLogbookId` from the user (`1` unless the role is ROV pilot or ROV supervisor, in which case `2`), writes a SHA-256 field hash, saves the project, the contractor, and a batch of project supervisors.

Before that save it calls `verifySupervisorEmails()`. Each address must match a local user who is `adminApproved` and either `supervisor` or `rov_supervisor`. Otherwise the UI throws "One or more of the selected supervisors are not valid".

`CHANGELOG.md` v1.3.0 says "project supervisor no longer being verified before saving". The current save path still verifies. **Conflict — Requires Confirmation.**

Saving a dive (`dive_widgets.dart`) also verifies supervisor emails, using `isValidDiveSupervisorByEmail` (admin-approved **and** role `supervisor` only, not ROV supervisor). It then saves specialized tasks and calls `SaveDiveLogbookService.saveLogbook`.

Resend of dive notifications does nothing when the dive has no id or `AuthService.isVerified()` is false. No snackbar is shown on that silent return.

Provenance: **Implemented**, except the changelog conflict.

### WF-7 — ROV tours

Drawer item ROV Tours is visible only for `isRovPilot()`. `ROVPage` and `rov_projects_crud.dart` save projects with the same supervisor-email check as dive projects (`isValidSupervisorByEmail`).

Logbook contractor entries carry `logbookEntryStatusId`. `LogbookStatusUtils` maps:

| Id | Label |
|---|---|
| null or any value other than 2–5 | NOT APPROVED |
| 2 | APPROVED |
| 3 | VERIFIED |
| 4 | DECLINED BY APPROVER |
| 5 | DECLINED BY VERIFIER |

Id `1` has no label. **Unknown / Requires Confirmation.**

Notification text for a tour is `NOTIFICATION SENT` or `NOTIFICATION NOT SENT`, based on in-memory notifications of type `tour_logsheet` or `dive_logsheet`.

Provenance: **Implemented**.

### WF-8 — COMPiiT

Drawer item COMPiiT is visible for contractors (diver or ROV pilot). The page lists `CompanyCompetencies` whose `diver` equals the user id, newest date first. If user details are missing it sends the person to the diver or supervisor profile instead of the list.

Create and view pages collect competence, vessel, linked dives, linked ROV, documents, and approver. The view page can set approver position to ODM, LSS, or Supervisor and requires a non-empty approver comment before the action button is enabled.

A QR / barcode scanner widget (`mobile_scanner`) is used from `compiit_widgets.dart`. What scanned value must match is **Unknown / Requires Confirmation** beyond "a scanner is on that screen".

Provenance: **Implemented** as a competency UI. The business meaning of COMPiiT is **Inferred** from the name and the competence models. No glossary defines it.

### WF-9 — Notifications (approvers)

Drawer item Notifications is visible for approvers (supervisor or ROV supervisor) and shows `NotificationBadge`.

`NotificationType`: `dive_logsheet`, `rov_logsheet`, `profile_request`, `profile_notification`, `diver_certification_request`, `tour_logsheet`, `email_changed`.

`NotificationService` loads by `supervisor_id` when the user is a supervisor or ROV supervisor, otherwise by `from_id`. Separate widgets exist for diver, dive, and ROV notifications.

Provenance: **Implemented**. The full approve / decline payload for every type was not line-traced. Status ids 4 and 5 show the client can represent both outcomes.

### WF-10 — Profile

Manage Profile opens `DiverProfilePage` for contractors and `SupervisorProfilePage` otherwise (including admin, company, and empty, if such a user reached the drawer).

Both profiles can call `AuthService.resetPassword()`, which requires connectivity and a session, POSTs `api/reset-password` with the current user's email, and treats the message `We have emailed your password reset link!` as success.

Diver profile can check `isValidSupervisorByEmail` when a supervisor address is entered.

Provenance: **Implemented**.

### WF-11 — Share a logbook

Share Logbook (`LogbookPageOld`) is for a diver who is not a student. SHARiiT (`LogbookSharingPage`) is for ROV roles. Changelog v1.3.0 says SHARiiT uses the logbook entity. The route is still the sharing page.

`shareLogBook` refuses when `isVerified()` is false ("Account verification failed, please check web for more details"), when no dive is selected, or when the email is empty. Otherwise it sends a `LogSheetDto` through `LogSheetService.sendLogBook` with `userIsValid: true` hardcoded.

Provenance: **Implemented**.

### WF-12 — Offline work and sync

README (developer notes):

- Online save writes cloud and local database.
- Offline save writes an action log in the local database.
- When back online, sync walks action logs and performs them.

`SyncService.sync` loads all action logs and replays them through `serviceMap` (dives, projects, users, competencies, vessels, ROV rows, company competence, and the other registered types). It records in-progress, completed, and failed sync status. A second call while `_isSyncing` returns immediately.

`startManualSync` registers Workmanager task `com.logiit.manualSync` and polls status every 500 ms. It fails immediately with "Sync failed - connection failed" when `NetworkController` is disconnected.

Periodic sync is off unless the drawer toggle has been turned on (`periodic_sync_enabled` defaults to false). When on, Workmanager task `com.logiit.backgroundSync` is registered every 15 minutes and only when the OS reports a network connection.

The shell shows a red "Offline Mode" banner while `NetworkController.connected` is false.

When the API becomes reachable again, if `isAuthenticated()` is false the controller calls `logout()` and opens `LoginPage`.

Provenance: **Implemented**. `TEST_RESULTS.md` lists offline sync as a recommended test, not a completed one.

### WF-13 — Offline API status dialog

Long-press "Forgot Password?" opens a dialog titled OFFLINE API STATUS. The DNS field defaults to `AppEnvironment.offlineApiUrl`. The person can edit it (hint text `192.168.1.10 / logiit.local`) and start a poll. The dialog shows Connected or No connection. It does not change `baseApiUrl`.

Provenance: **Implemented**. Changelog v1.0.28 documents this behaviour.

## Verification flag

`AuthService.isVerified()` is true only when all three are true:

- KYC status string is `GREEN`
- `user.paidUp` is true
- `user.supervisorApproved` is true

It does not block login or the dashboard. It blocks sharing a logbook, and it blocks resend of dive / tour notifications (those resends return without a message). `buttons.dart` shows at least one control only when verified **and** connected. `DiveSupervisorService` and `LogbookContractorEntryVetterService` branch on the same flag.

Provenance: **Implemented**. Whether "paid up" is a subscription, a membership, or a manual flag is **Unknown / Requires Confirmation**. KYC provider is not named in the client.

## Functional requirements

### FR-001 — Session routing

Requirement: On launch, show the logo, open the local database on mobile, and open the dashboard if a session resolves to a user, otherwise the login screen.

Who: any person opening the app.

Trigger: process start (`SplashPage`).

Inputs: stored JWT and `offlineUserId`; local user row; `api/user` when connected.

Expected behaviour: `Get.off` to `LandingPage` or `LoginPage`. Errors use `SnackBarService`.

Validation: none beyond what `isAuthenticated` catches. On failure it clears `jwt` and `offlineUserId` and returns false.

Outputs: one of the two screens.

Error conditions: database open failure on mobile surfaces as the splash error snackbar. Web skips Isar and can still fail authentication.

Dependencies: Isar (mobile), `HttpService`, `AuthService`.

Evidence: `lib/pages/splash/splash_page.dart`, `lib/services/auth_service.dart`.

Implementation: **Implemented**.

Confidence: High.

### FR-002 — Online login

Requirement: A person with a verified email and an existing user-details profile can sign in against the flavor API and land on the dashboard. A manual sync starts when the network check passes.

Who: person on `LoginPage` while `NetworkController` reports connected.

Trigger: LOGIN pressed with non-empty email and password.

Inputs: email, password. Password is sent as JSON on `api/login` (not the local hash).

Expected behaviour: store JWT, user id, and password hash; load user; reject unverified email; reject missing user details; navigate to landing.

Validation: both fields non-empty. No format check in `isValid` beyond the later `formatEmail` call.

Outputs: session keys; navigation; sync task.

Error conditions: API `message` is thrown. Unverified email and missing profile use the strings in WF-2. Button returns to idle in `finally` via `setLoading(false)`.

Dependencies: flavor `baseApiUrl`, API login contract `{jwtToken, userId}`.

Evidence: `lib/pages/auth/login/login_page.dart`, `lib/services/auth_service.dart`.

Implementation: **Implemented**.

Confidence: High.

### FR-003 — Offline login

Requirement: A person who has previously signed in online on this device can sign in without the API if the email and password hash match the local user and secure storage.

Who: person on `LoginPage` while disconnected.

Trigger: LOGIN.

Inputs: email, password.

Expected behaviour: set `offlineUserId` and continue through the same post-login gates that can run locally.

Validation: local user exists; stored hash equals `getHashPassword(password)`.

Outputs: `offlineUserId`. No new JWT.

Error conditions: messages in WF-3.

Dependencies: prior online login on this install; Isar user row.

Evidence: `AuthService._offlineLogin`.

Implementation: **Implemented**.

Confidence: High.

### FR-004 — Registration and forgotten password in the browser

Requirement: Create account and Forgot Password open the flavor host's web pages. They are not forms inside the app.

Who: person on the login screen.

Trigger: tap.

Inputs: none from the form. URL is `{baseApiUrl}register` or `{baseApiUrl}forgot-password`.

Expected behaviour: external browser. Failure throws `Could not launch ...`.

Validation: `launchUrl` result.

Outputs: browser navigation.

Error conditions: launcher failure.

Dependencies: `url_launcher`, flavor base URL.

Evidence: `login_page.dart`.

Implementation: **Implemented**.

Confidence: High.

### FR-005 — In-app password reset email

Requirement: From the diver or supervisor profile, a signed-in person on a connection can request a reset email for their own address.

Who: signed-in user.

Trigger: profile reset action.

Inputs: `{email: current user email}` to `api/reset-password`.

Expected behaviour: success snackbar only when the body message equals `We have emailed your password reset link!`. Any other message is an error snackbar.

Validation: must be connected and authenticated. Offline shows "Please connect to the internet".

Outputs: snackbar.

Error conditions: API message other than the success sentence; not authenticated returns without a snackbar.

Dependencies: API copy must match that exact sentence.

Evidence: `AuthService.resetPassword`, `diver_profile.dart`, `supervisor_profile.dart`.

Implementation: **Implemented**.

Confidence: High.

### FR-006 — Role navigation

Requirement: The drawer shows only the destinations for the current role, as listed under Users and roles.

Who: signed-in user.

Trigger: opening the drawer.

Inputs: `User` role and `isStudent`.

Expected behaviour: hidden `NavbarOption` rows are not shown. Logout is always shown. Periodic sync toggle is under SETTINGS. The Action Logs row is visible only when the current title is already Action Logs, so it does not appear from other screens. Long-press on the version footer opens `ActionLogsPage`.

Validation: null user hides role items (`visible: user != null ? ...`).

Outputs: navigation via `Get.off`.

Error conditions: Manage Profile force-unwraps `user!`. A null user who still sees the row would throw. The row is visible even when user is null (`visible: true`).

Evidence: `lib/widgets/app_page.dart`.

Implementation: **Implemented**.

Confidence: High.

### FR-007 — Account verification gate

Requirement: Sharing a logbook and resending dive or tour notifications require KYC `GREEN`, `paidUp`, and `supervisorApproved`.

Who: signed-in user attempting those actions.

Trigger: share or resend.

Inputs: KYC row for the user details id; user flags.

Expected behaviour: share shows an error and does not send. Resend returns without sending and without a snackbar.

Validation: conjunction of the three conditions. Missing KYC or an exception yields false.

Outputs: blocked or allowed action.

Error conditions: `isVerified` catch returns false.

Dependencies: API fields `paidUp`, `supervisorApproved`, KYC `status`.

Evidence: `AuthService.isVerified`, `logbook_page_old.dart`, `dive_widgets.dart`, `logbook_entry_state.dart`.

Implementation: **Implemented**.

Confidence: High for the client rule. Medium for the business meaning of paid-up and KYC.

### FR-008 — Supervisor must already be an approved user

Requirement: A supervisor email typed onto a project, dive, or ROV project must match a locally loaded user who is admin-approved and has the expected role.

Who: diver or ROV pilot saving that form.

Trigger: save.

Inputs: email list; local users.

Expected behaviour: invalid set throws "One or more of the selected supervisors are not valid" and the save does not continue past that call.

Validation: dive path requires role `supervisor`. Project and ROV project paths accept `supervisor` or `rov_supervisor`. All require `adminApproved == true`.

Outputs: exception snackbar via `handleError`, or continued save.

Error conditions: unknown email, wrong role, or not admin-approved.

Dependencies: users must already be in the local database (`fetchByEmail` uses `find()` then memory). An online-only user who was never synced will fail this check. **Inferred** from `UserService.fetchByEmail`.

Evidence: `user_service.dart`, `project_widgets.dart`, `dive_widgets.dart`, `rov_projects_crud.dart`.

Implementation: **Implemented**. Conflicts with the changelog sentence noted in WF-6.

Confidence: High that the code checks. Low that the changelog and the code describe the same approved rule.

### FR-009 — Offline queue and sync

Requirement: While the flavor API is not reachable, mutations that go through the action log stay on device. A manual sync when the API is reachable replays them. An optional periodic task retries about every 15 minutes while the OS reports connectivity.

Who: signed-in user; the periodic toggle is in the drawer.

Trigger: offline save; login (manual sync); toggle; Workmanager alarm.

Inputs: `ActionLog` rows; environment name and documents directory passed into the task.

Expected behaviour: status `in_progress`, then `completed` or `failed`. Landing shows a snackbar when its 10-second poll sees a completed or failed status that still needs processing. Logout during `in_progress` marks the run failed.

Validation: `startManualSync` refuses when `NetworkController` is false. Periodic task constraint is `NetworkType.connected`.

Outputs: API writes, local status row, snackbars.

Error conditions: connection failed; per-log failures recorded on the status row (`errorMessage`). Empty log list completes without uploads (see `SyncService.sync` after the log load).

Dependencies: Workmanager (not web); Isar; flavor name so the background isolate can call `setupEnv`.

Evidence: `README.md`, `sync_service.dart`, `background_sync_service.dart`, `periodic_sync_service.dart`, `landing_page.dart`.

Implementation: **Implemented**.

Confidence: High for the control flow. Medium for every `DataType` branch inside the replay loop (the map is large; not every branch was executed).

### FR-010 — Flavor-specific API

Requirement: Dev, staging, demo, and production builds talk to the bases in `lib/env.dart` and show the matching title. Android staging is a distinct application id. The other three flavors share `co.logiit.app`.

Who: build operator and the person reading the drawer footer.

Trigger: `mainWithEnvironment`.

Inputs: `Environment` enum.

Expected behaviour: `AppEnvironment.title`, `baseApiUrl`, and `offlineApiUrl` match the table in `project-plan.md`. Footer shows `{title} - {buildVersion}`.

Validation: unit test in `test/widget_test.dart` asserts dev and production title and base URL only.

Outputs: configured singleton.

Error conditions: none in `setupEnv`. A wrong flavor binary is a build mistake, not a runtime check.

Dependencies: Android `productFlavors`, entrypoint file.

Evidence: `lib/env.dart`, `android/app/build.gradle`, `test/widget_test.dart`.

Implementation: **Implemented**.

Confidence: High.

### FR-011 — Files

Requirement: Uploads POST multipart to `api/uploadFile/{key}` with a `type` field. Downloads while connected POST `api/getSecureUrl`. While disconnected, the client builds `{baseApiUrl}secure-file-download/{type}/{filename}` and otherwise uses the cache.

Who: signed-in user attaching images or documents.

Trigger: file pickers on dives, tours, profile, COMPiiT.

Inputs: local path, `FileType`, auth header.

Expected behaviour: `HttpService.checkResult` accepts HTTP 200 and 201 and a JSON body. Body `"null"` throws "No data found". Other statuses throw `message` or `error`.

Validation: gallery permission depends on OS (iOS photos; Android storage through API 32, photos from API 33). Camera permission is requested separately.

Outputs: stored path or cached file.

Error conditions: permission denial; HTTP error string; cache miss while offline returns null from the fetch helper's catch paths.

Dependencies: API file routes; `permission_handler`.

Evidence: `http_service.dart`, `file_service.dart`, `permissions.service.dart`.

Implementation: **Implemented**.

Confidence: High for the routes. Medium for every screen that attaches a file.

### FR-012 — Logbook status labels

Requirement: A contractor logbook entry is shown as NOT APPROVED, APPROVED, VERIFIED, DECLINED BY APPROVER, or DECLINED BY VERIFIER according to `logbookEntryStatusId`.

Who: person viewing a tour or related notification.

Trigger: status render.

Inputs: integer id.

Expected behaviour: map in WF-7. Anything else, including null, is NOT APPROVED.

Validation: none.

Outputs: display string. Dashboard ROV hours additionally require id `3`.

Error conditions: unknown id `1` is shown as NOT APPROVED. That may hide a real server status.

Dependencies: server status catalogue. Not in this repo.

Evidence: `lib/utils/logbook_status_utils.dart`, `landing_page.dart`.

Implementation: **Implemented**.

Confidence: High for the labels. Low for whether id `1` should be distinct.

## Integrations

| System | Purpose | Trigger | Direction | Auth | Failure |
|---|---|---|---|---|---|
| LOGiiT API | CRUD, login, user, files | user action or sync | App → API | Bearer JWT after login. Login itself is unauthenticated JSON | `HttpService.checkResult` throws `message` or `error`. Sync marks failed |
| Web register / forgot-password | Account creation and reset outside the app | login screen taps | App opens browser | Unknown (web app) | `Could not launch` |
| Offline API host | Reachability poll on a local or Balena name | long-press Forgot Password | App → that host | Not shown in the dialog source that was read | Dialog shows No connection |
| Secure storage / preferences | Session and offline password | login, logout, sync toggle | Device only | OS storage | Missing keys mean logged out or no offline login |
| Social URLs | Marketing links | login screen icons | Browser | none | `Could not launch` |

The login response shape used by the client is JSON with `jwtToken` and `userId`. User payload is JSON `user` on `GET api/user`. CRUD bodies use `body` (`OnlineCrudService`). Reset-password success is the exact English sentence in FR-005.

## Important constraints

- Flutter 3.38.5 is the documented and FVM-pinned SDK. README says Dart 3.10.0+.
- iOS Podfile platform is 16.0 (changelog v1.0.28; was 12.0).
- Android `minSdkVersion` is `flutter.minSdkVersion`. The launcher-icon config sets `min_sdk_android: 24`. The numeric API level is whatever Flutter 3.38.5 injects. **Unknown** as a literal in-repo.
- Release signing uses `android/key.properties` when that file exists and has `storeFile`. Otherwise release builds are signed with the debug key. The properties file is not in git. `TEST_RESULTS.md` says the dev APK used debug signing.
- JWT in SharedPreferences is not encrypted by the app. Password hash uses secure storage. Logout does not remove the hash.
- Web Isar stub throws. Do not treat web as a supported logbook client on current evidence.
- `credits.md` holds live-looking passwords. That is a constraint on handling the repo, not a product feature.
- Generic CRUD is not REST. The conversion document is a proposal. Shipping the app against a REST-only API would break `OnlineCrudService` until both sides change.
- Dev flavor base URL `http://10.0.2.2/` is cleartext HTTP.

## Non-goals evidenced by absence

No in-app registration form, no in-app admin console for `UserType.admin`, no payment UI, no push-notification client, and no automated end-to-end suite. Absence is not a decision that those will never exist.
