Internal onboarding document · Wattr.ai

Wattr Farmer App
Visual Specification

The complete screen-by-screen guide to the farmer-facing mobile app — every screen, state, and interaction, with faithful UI mockups rebuilt from the source code. Written for new joinees: read it top to bottom once, then use the sidebar as a reference.

Flutter 3 · Dart 3 v1.0.2 (build 7) Android · iOS English · हिंदी · ਪੰਜਾਬੀ Repo: wattrai/mobile-app
How the mockups work Every phone frame in this document is an HTML re-creation of the real screen, built from the widget code — same colors, type, copy, and layout. They are illustrative of structure, not pixel screenshots: live data values are representative examples.
Chapter 1

App overview

What the app is, who uses it, and how it fits into the Wattr platform.

Wattr Farmer is the mobile companion to the Wattr.ai water-pump monitoring platform (PHMC). It lets farmers monitor and control their IoT-connected water pumps from their phone, schedule irrigation, and see farm-relevant weather and modelled soil conditions. It also carries a safety-critical feature: a full-screen theft alarm (SOS) that takes over the phone when a pump's controller detects tampering. Mandi prices, a conservation rewards program, and water-quota tracking are designed and partially built, but deliberately parked until real data feeds exist (Chapter 17 has the full status map).

The platform behind it monitors ~5,400 devices and ~4,400 pumps across 54 tenants in 8 Indian states. The app talks to the same backend API as the web dashboard.

Who uses it

PersonaRole codeWhat they see
Farmer — the primary userINDIVIDUAL_USERTheir own pumps (typically 1–15), quota, weather, rewards
Scheme operator / adminCLIENT_ADMIN & upAll pumps in their tenant — pump lists can reach 500+
Wattr staffSUPER_ADMINEverything, across all tenants (used for testing/support)

The UI is the same for every role — what changes is the data scope the API returns. There is no separate "admin mode" in the app.

Environments

EnvironmentBase URLNotes
Staginghttps://staging-api.wattr.ai/v1Default for development; test accounts live here
Productionhttps://api.wattr.ai/v1Real farmers — be careful with pump commands

You can switch environments from inside the app: long-press the logo on the login screen (dev toggle). Staging test credentials are listed in the repo README.md — never commit or share them outside the team.

Tech stack at a glance

ConcernChoice
FrameworkFlutter 3.41 / Dart 3.11, Material 3
StatesetState for local state; Provider + ChangeNotifier only where state crosses widgets (translator, SOS controller)
HTTPdart:io HttpClient wrapper with JWT auth (core/network/api_client.dart)
Chartsfl_chart
Fontsgoogle_fonts — Fraunces / DM Sans / Fira Mono (+ Noto Sans Gurmukhi fallback)
PushFirebase Cloud Messaging + flutter_local_notifications (theft SOS)
WeatherOpen-Meteo public API (no key)
PersistenceSharedPreferences (session, profile cache, language)
Chapter 2

Architecture & code map

Feature-first layered architecture: cross-cutting code in core/, everything else owned by a feature.

lib/
├── main.dart                  # Bootstrap only — binding init, translator init, runApp
├── app.dart                   # WattrApp: MaterialApp + auth gate (splash / login / home)
│                              #   + global SOS overlay + desktop max-width clamp
├── core/
│   ├── constants/             # app_prefs.dart (SharedPreferences keys), quota.dart
│   ├── i18n/                  # translator.dart — TranslatorProvider (en / hi / pa)
│   ├── network/               # api_client.dart, service_error.dart
│   ├── push/                  # FCM + local notifications + SOS controller/permissions
│   ├── theme/                 # colors.dart (class C), typography.dart
│   ├── utils/                 # format.dart
│   └── widgets/               # WText, AppCard, PillBadge, SectionLabel, ArcGauge, charts
└── features/<name>/
    ├── data/datasources/      # *_service.dart — API calls; models live alongside
    ├── domain/entities/       # pure-Dart entities (FarmerProfile, Tier)
    └── presentation/
        ├── screens/           # full-page widgets
        └── widgets/           # feature-private sub-widgets

Rules a new joinee must know

  • State stays local. setState unless state genuinely crosses widget boundaries — today only TranslatorProvider (language) and SosController (theft alarm) qualify for Provider. Don't introduce Riverpod/Bloc/GetX.
  • Services own HTTP. Screens never build requests; they call *Service classes which use the shared ApiClient (adds JWT, handles errors as ServiceError).
  • Models double as entities. No DTO/entity split — pragmatic classes returned straight from services.
  • All user-visible text is rendered with WText, which looks up the string in the active language dictionary — the English string itself is the translation key. Adding UI copy means adding the matching entries to assets/i18n/hi.json and pa.json.
  • Text scale is clamped app-wide to 0.85–1.1× so huge OS font settings can't break layouts; on windows wider than 1024 px content is centered in an 800 px column.

Docs worth reading next

  • README.md — build/run instructions, test accounts, API status table
  • docs/sos-theft-alert.md & docs/ios-sos-lockscreen.md — theft alarm design
  • docs/scheduling-status-report.md & docs/scheduling-v1-deferred.md — scheduling scope and deferred items
  • API_REQUIREMENTS.md — backend endpoints the app expects
Chapter 3

Design system

A calm, light, green-on-cream identity. Tokens live in core/theme/colors.dart (class C) and core/theme/typography.dart. This document itself is set in the app's palette and typefaces.

Color — the C palette

Green primary on a Tailwind-ish 50–900 scale, plus accent families (sky, sun, lime, earth, red), surface tones, and four text shades. Backgrounds are never pure white-on-white: the scaffold is a faint green-tinted off-white with white cards and mint borders.

Primary greens

C.p900#052E16 · headlines, "ink"
C.p800#14532D
C.p700#15803D · active tab
C.p600#16A34A · primary buttons
C.p500#22C55E · "ON" green
C.p400#4ADE80
C.p300#86EFAC
C.p200#BBF7D0
C.p100#DCFCE7 · pill bg
C.p50#F0FDF4 · tinted surface

Accent families & semantic colors

C.sky#0EA5E9 · water, info (bg #F0F9FF)
C.sun#F59E0B · warning (bg #FFFBEB, bd #FDE68A)
C.lime#84CC16 (bg #F7FEE7, bd #D9F99D)
C.earth#B45309 · soil
C.red#DC2626 · faults, OFF actions (bg #FEF2F2, bd #FECACA)

Surfaces, borders & text

C.bg#FAFFFE · scaffold
C.surf#F0FDF4
C.card#FFFFFF
C.bd#D1FAE5 · card border
C.bdHi#A7F3D0 · border, emphasized
C.t1#052E16 · primary text
C.t2#166534 · secondary text
C.tm#6B7280 · muted text
C.tl#9CA3AF · lightest text

Typography

Three top-level helper functions in typography.dart — call sites use them everywhere instead of raw TextStyles. All three attach a Noto Sans Gurmukhi fallback so Punjabi (ਪੰਜਾਬੀ) renders with an intentional typeface.

fraunces(size)Fraunces · serif
default w700 · C.t1
Headlines & screen titles
dmSans(size)DM Sans · sans
default w400 · C.t1
Body copy, labels, buttons — the workhorse for everything readable.
firaCode(size)Fira Mono · mono
default w500 · C.t1
238.4 V · 12.1 A · 4.2 kW — numerics & telemetry
Rule of thumbSerif = headings, Sans = words, Mono = numbers. If you're showing a measured value, use firaCode() so digits align.

Shared widgets (core/widgets/)

WidgetWhat it is
WTextDrop-in Text replacement used app-wide; single place to control text behavior (overflow, scaling)
AppCardThe standard white rounded card: C.card background, C.bd border, ~16 px radius, soft padding — nearly every content block sits in one
PillBadgeSmall rounded status pill (colored background + label) — pump ON/OFF, severities, tags
SectionLabelUppercase letter-spaced section heading in C.t2 green
ArcGaugeSemi-circular gauge (quota usage, soil moisture) with color thresholds
SimpleAreaChart / SimpleBarChartfl_chart wrappers for trends (water usage, prices) with the app's green styling

Internationalization

Three languages ship in assets/i18n/: English, Hindi (hi.json, ~457 entries), Punjabi (pa.json, ~460 entries). The mechanism is unusual and worth understanding: the English string is the translation key. WText('Good Morning') looks the literal string up in the active dictionary via TranslatorProvider; en.json is an identity map, so English needs no entries. The chosen language persists under the SharedPreferences key wattr.language, and pickers exist on both the login screen and the home header (globe icon). If a string is missing from hi.json/pa.json it silently renders in English — e.g. "Good Afternoon" and "Good Evening" are currently untranslated.

Chapter 4

Shell, splash & navigation

One authenticated mode, five tabs, everything wired through FarmerHome.

Source: lib/app.dart · lib/features/dashboard/presentation/screens/farmer_home.dart

The auth gate

WattrApp decides what to show with two booleans — there is no router or named routes:

_initialising ? SplashScreen
: _authenticated ? FarmerHome(onLogout)
                 : LoginScreen(onLoginSuccess)

On launch, AuthService.restoreSession() checks for a stored token and validates it against GET /users/current. An authoritative 401 clears tokens and shows Login; a network failure or 5xx optimistically keeps the user signed in so the app works offline. There is no role check anywhere in the client — every signed-in user gets the same FarmerHome.

9:41▲ ▂▄▆ ▮
Wattr app logo
WATTR.AI
Splash — shown only while the session restores (usually <1 s)
GS
Good Morning
Gurpreet Singh
🌐
🔔 3
— active tab renders here (IndexedStack) —
🏠Home
☁️Weather
💧
Pump
🌿Soil
🏪Market
FarmerHome shell — header + elevated center Pump tab (default on launch)

Header anatomy

  1. Avatar — 44 px circle, green gradient, 2 px translucent white ring. Shows the initials of the first two words of the profile name ("Gurpreet Singh" → GS; falls back to "F" for the placeholder name "Farmer"). Tap → pushes the Profile screen.
  2. Greeting + name — greeting picked by local hour: before 12 "Good Morning", before 17 "Good Afternoon", else "Good Evening" (DM Sans 11, white). Name below in Fraunces 16 bold white; literal "Farmer" until the profile loads.
  3. Language button — globe icon in a frosted 40 px circle. Opens the Choose Language bottom sheet (English / हिन्दी / ਪੰਜਾਬੀ).
  4. Notification bell — same circle, with a red badge (#DC2626, caps at "9+") when unread alerts exist. Tap → pushes the Alerts screen. Note: the count is fetched once at launch (GET /alerts/unread-count) and never refreshed afterwards.

The header background is solid C.p700 #15803D with a subtle dot-grid overlay (1 px white dots at 8% opacity on an 18 px lattice). There is no Material AppBar, no drawer, and no FAB anywhere in the app — the elevated center Pump button plays the FAB role.

Tab map

#TabWidgetIcons (inactive → active)Notes
0HomeHomeTabhome_rounded (same both)
1WeatherWeatherTabwb_cloudy_outlined → _rounded
2Pump (center)PumpTabwater_outlined → waterDefault tab on launch. 60 px elevated gradient circle; shows a small white dot under the icon while the pump is running
3SoilSoilTabeco_outlined → eco_rounded
4MarketMarketTabstorefront_outlined → _rounded
5(Rewards)_ComingSoonPlaceholderUnreachable — no nav item selects it; would show "🏆 Rewards coming soon"

The body is an IndexedStack, so all tabs stay alive — switching tabs never loses scroll position or state. Active tab is marked by a 28×3 px green pill that animates in above the icon (200 ms).

Key design decision

FarmerHome owns all shared state — profile, pump list, active pump, live telemetry, weather, alert count — and passes it into the tabs as props. The tabs themselves never fetch pump or weather data. If you're debugging "stale data in a tab", look at farmer_home.dart, not the tab.

Live-data engine (what FarmerHome runs in the background)

MechanismBehavior
Telemetry pollGET /telemetry/latest?pump_id=… every 10 s while the pump is OFF, 30 s while ON; paused when the app is backgrounded, refreshed immediately on resume
Command confirmationAfter start/stop, no optimistic flip — a tapered poll at 1, 2, 3, 5, 8, 12, 18, 25 s checks telemetry and GET /pumps/:id/status; a 30 s timeout marks the device offline with an error snackbar
Runtime ticker1 s timer while running so the runtime readout counts up smoothly between polls
OFF smoothingpump_status=true flips ON immediately; OFF needs two consecutive false readings (a single data_type='of' packet flips OFF at once)
Theft latchA telemetry packet with data_type='tf' latches the theft banner and raises the full-screen SOS; clears only when the pump reports running again or the user switches pumps

Shared sheets & toasts

Choose Language
English
हिन्दी
ਪੰਜਾਬੀ
Language sheet (header globe, also on Login)
Select pump3 pumps
Borewell North
SINGLE_PHASE
Canal Pump
DUAL_HP
Pump #4021
TLM
Pump picker — only offered when the farmer has >1 pump

Command snackbars float above the nav bar (rounded 12 px, green #16A34A for success with a ✓, red #DC2626 for errors with a ⚠). The full message set, verbatim:

  • "Start command sent — confirming…" / "Stop command sent — confirming…"
  • "Start failed: <reason>" / "Stop failed: <reason>"
  • "Could not start pump. Check your connection." / "Could not stop pump. Check your connection."
  • "Device is offline — no response from pump. Check connectivity and try again." (30 s timeout)
  • "No pump configured" · "Pump identifier missing — pull to refresh and try again"

First-run permission dialogs (Android)

Right after login, the app checks the two permissions the theft alarm needs to take over a locked screen, prompting one at a time and only when missing:

  1. "Allow full-screen theft alerts" — "So the loud theft alarm can take over your screen when your phone is locked, allow full-screen alerts for Wattr." (Android 14+ full-screen intent)
  2. "Allow display over lock screen" — "Some phones also need 'Display over other apps' so the theft alarm can appear over the lock screen." (OEMs like ColorOS)

Actions: Not now / Open Settings; returning from Settings re-checks the next permission automatically.

Dead code to be aware of

DashboardScreen (an admin-style fleet overview: pump-status stat cards, total power, fault badges, faulted-pump list, last-7-days table) is fully built but unreachable — its only entry point, a header dashboard icon, is commented out in farmer_home.dart. Same for the Rewards tab slot (index 5) and the header tier badge. Don't be surprised when you find these files; nothing in the running app navigates to them.

Chapter 5

Login

Username + password sign-in on a deep-green gradient. No registration, no forgot-password, no OTP.

Source: lib/features/authentication/presentation/screens/login_screen.dart · auth_service.dart

9:41▲ ▂▄▆ ▮
🌐 English
Wattr app logo
WATTR.AI
Farm Water Intelligence
Sign In
Enter your credentials to continue
Username
👤 Enter your username
Password
🔒 Enter your password👁
ⓘ Use your registered email and password
Sign In
Powered by WATTR.AI
v1.0.0
Login — idle state

Anatomy

  1. Language chip (top-right) — frosted pill with globe icon; opens the same language sheet as the home header. The whole screen re-renders in the picked language instantly.
  2. Logo block — 90 px rounded logo with a green glow shadow, "WATTR.AI" in Fraunces 32, tagline "Farm Water Intelligence" in mint #86EFAC. The column fades/slides in over 900 ms on mount.
  3. Sign-in card — white, 28 px radius. Fields have grey #F9FAFB fills and #E5E7EB borders that turn green #22C55E on focus. The eye icon toggles password visibility. "Next" on the keyboard moves username → password; "Done" submits.
  4. Sign In button — full-width, 54 px, #16A34A. While the request runs it turns pale green #86EFAC with a white spinner.
  5. Footer — "Powered by WATTR.AI" and a version string. ⚠ The version is hard-coded "v1.0.0" while pubspec.yaml is at 1.0.2+7 — a known stale label.

States

ValidationEmpty fieldsTapping Sign In with an empty username or password shows "Please enter username and password" — no network call is made.
LoadingSigning inButton disabled (pale green) with a 22 px white spinner; fields stay editable.
ErrorAuth failedRed error box between the hint and the button: light red fill #FEF2F2, red border, ⓘ icon, server's message or "Login failed".
OfflineNo networkSame error box with "Connection failed. Check your network."

Hidden dev toggle

Long-press "Powered by WATTR.AI" to open the Server URL dialog — a plain text field pre-filled with the current base URL (default https://staging-api.wattr.ai/v1). Saving persists to SharedPreferences and takes effect on the next request. There is no validation and no visual confirmation — this is how you point a build at staging vs production.

API

POST /auth/login with {usernameOrEmail, password} → expects data.accessToken, data.refreshToken, data.user; tokens persist in SharedPreferences. Refresh is automatic: on the first 401/403 the client calls POST /auth/refresh once and replays the request.

Chapter 6

Home tab

The at-a-glance farm summary: pick a pump, see the weather, control the pump, and peek at the two forecasts. Every card is a shortcut into a deeper tab.

Source: lib/features/home/presentation/screens/home_tab.dart · pump_picker_accordion.dart

GS
Good Morning
Gurpreet Singh
🌐
🔔3
💧
Borewell North Submersible · 7.5 HP
3
CHOLMUKH, SANGRUR, PUNJAB · LIVE
31.4°
Partly Cloudy
Feels like 34.2°C
💧 HUMIDITY
62%
💨 WIND
12 km/h NW
🌬 GUSTS
21 km/h
📊 PRESSURE
1004 hPa
Updated 2m ago · Tap for forecast →
BOREWELL NORTH · SUBMERSIBLE RemoteSwitch ⇕
3.75 kW
⚡ Power
12.40 L/s
🌊 Discharge
50.0 Hz
⏱ Freq
Runtime · 01:24:07
Stop Pump Session
24-HOUR TEMPERATURE
Surface (2 m) · tap for full forecast
09:0015:0021:0003:0008:00
NEXT 7 DAYS SOIL MOISTURE FORECAST
Root zone average (3–9 cm) · tap for full soil view
TodaySatSunMonTueWedThu
🏠Home
☁️Weather
💧
Pump
🌿Soil
🏪Market
Home — pump running, weather live

Anatomy (top to bottom)

  1. Pump picker accordion — three renders by pump count: 0 pumps → a mint banner "No associated pumps"; 1 pump → renders nothing; 2+ → this collapsible selector (border turns green 1.5 px when expanded, rows with radio icons). Selecting a pump persists to wattr_selected_pump_id and refetches telemetry.
  2. Weather hero — dark-green gradient card, tap → Weather tab. Place label uppercased with "· LIVE" when data is <10 min old; big Fraunces temperature; WMO condition label + emoji; four right-aligned stats. While loading it shows placeholders ("—°", "Loading…") instead of a spinner so the layout never jumps.
  3. Pump status card — hidden entirely when no pump. Title is NAME · TYPE uppercased; an on_by pill (Remote / Manual / Schedule) shows who started it; "Switch ⇕" opens the pump picker sheet (only with >1 pump). Three mono metric tiles — Power (kW), Discharge (L/s), Freq (Hz) — colored when running, grey #9CA3AF when off (Power/Discharge zero out; Freq keeps the last value). Runtime counts up live every second; when off it becomes "Last run · HH:MM:SS".
  4. Start/Stop button — green "Start Pump Session" / red "Stop Pump Session". Stop asks for confirmation ("⛔ Stop pump? — This will stop the current irrigation session for <pump>."); Start fires immediately. Disabled grey states, in priority order: "Not connected", "Device offline", "No electricity at site", "Connecting…". Electricity detection trusts the flag or any phase voltage >100 V (firmware-glitch override).
  5. 24-hour temperature card — fl_chart area line of the next 24 h (2 m air temp), green #16A34A with dashed mint gridlines; tap → Weather tab.
  6. Soil moisture forecast card — 7 daily samples of 3–9 cm volumetric soil moisture (%), dot on the last point; tap → Soil tab. Both chart cards are simply absent until weather data arrives.

States

LoadingProgressive shellNo skeletons or spinners — hero shows "—°" placeholders, charts absent, tiles show "-". The page grows as data lands.
EmptyNo pumps"No associated pumps" banner; pump card skipped — tab is just banner + hero (+ charts).
ErrorSilentThe Home tab has no error UI; weather errors only surface on the Weather tab, telemetry errors are swallowed. Command failures appear as red snackbars.
RefreshPull-to-refreshGreen indicator; refetches telemetry then weather. Background polls continue regardless (10 s / 30 s).
Field quirk to know

The "⚡ Power" tile reads telemetry.kwh — the cumulative energy field — not active_power. The snapshot model has both; keep this in mind when comparing against the web dashboard.

Data

Everything comes from the shell's shared state: GET /farmer-profiles/me (name, village, embedded pumps[] — there is no separate /pumps call for farmers), GET /telemetry/latest?pump_id=…, POST /commands/pump/:id/start|stop, and Open-Meteo for the hero + charts.

Chapter 7

Pumps

The heart of the app: live telemetry, remote start/stop, health diagnostics, sessions, and the wire-cut takeover. Four screens share this feature.

Source: lib/features/pumps/presentation/screens/ — pump_tab.dart · pump_list_screen.dart · pump_detail_screen.dart · recent_sessions_screen.dart

Two visual languages coexist here

PumpTab and RecentSessionsScreen use the modern design system (AppCard, Fraunces, emoji, #15803D app bar). PumpListScreen and PumpDetailScreen are older CRM-mirroring surfaces: darker #166534 app bar, plain Material cards, no emoji, hard-coded hexes. Expect the difference — it's intentional legacy, not a bug.

7.1 · Pump tab (the center tab, default on launch)

BOREWELL NORTH · SUBMERSIBLE ScheduleSwitch ⇕
3.75 kW
⚡ Power
12.40 L/s
🌊 Discharge
50.0 Hz
⏱ Freq
Runtime · 00:42:18
Stop Pump Session
💧
SCHEDULED RUN
Running now
42 / 60 min delivered
⏹ Stops by 06:30 PM
🕐Schedule

NEXT SESSION
Morning watering
Mon–Fri · 1 hr
05:30 AMActive
PUMP/DEVICE HEALTH DIAGNOSTICS
84%
BATTERY
Normal
67
SIGNAL
Normal
0.82
PF
Normal
Electricity Available: Available
🕘LAST SESSIONLast 30 sessions ›
DateTime rangeDurationMode
Today05:30 – 06:1242mSchedule
6h 24m
RUNTIME
128,400 L
WATER
14
CYCLES
DAILY ENERGY (ESTIMATED)
18/0722/0726/0731/07
Per-day kWh is prorated from runtime — the daily endpoint reports runtime, not direct kWh.
Pump tab — running, with an active scheduled session (header/nav cropped)

Anatomy (six cards)

  1. Control card — same structure as the Home pump card (metric triplet, live runtime, Start/Stop button) with a ripple animation on tap. Power and Discharge are forced to "0.0" when off; missing values render "—", never fabricated numbers.
  2. Live scheduled-run card — appears only while GET /pumps/:id/occurrences/live reports PENDING / RUNNING / INTERRUPTED / RECOVERING. Status lines: "Running now" (green), "Starting soon" (grey), "Paused due to power cut — will resume automatically" (brown), "Resuming after power cut…" (sky). Shows delivered/target minutes, a progress bar, and "Stops by h:mm a" when a hard end exists. Polled every 30 s.
  3. Schedule entry card — tap anywhere → Schedule list (Chapter 8). Shows the next session (name, days, duration, big start time) with an inline enable toggle, or "Automate start times & duration" when no schedules exist. The next session is computed client-side from the day bitmask + start time.
  4. Health diagnostics — Battery (normal ≥20%), Signal (normal ≥40), Power Factor (normal 0.4–1.0 while running) with Normal/Abnormal badges; the electricity row; and, if any of the 8 fault flags is set, a single red "Fault detected" banner (individual fault names are computed but not rendered).
  5. Last session preview — first of the last 30 on→off cycles (date, time range in mono, duration, trigger pill). Tap → Recent sessions screen.
  6. Energy/usage report — 14-day totals (runtime / water litres / cycles) from /reports/start-stop/summary plus a daily-energy line chart from /reports/start-stop/daily, with an honest footnote that kWh is prorated from runtime.

Start/stop, precisely

Stop asks "⛔ Stop pump?"; Start never confirms. The command POSTs, then the UI parks on a grey "Connecting…" button — never an optimistic flip — while polls at 1/2/3/5/8/12/18/25 s check telemetry and /pumps/:id/status. Confirmation flips the state; 30 s of silence marks the device offline with a red toast.

Empty🚰 No pump configured"Your account isn't linked to a pump yet. Pull down to refresh, or contact your field officer."
BlockedGrey button"Not connected" (never reported) · "Device offline" (30 s no-ack) · "No electricity at site" · "Connecting…"
TheftTamper takeoverSee mockup below — replaces the entire tab while the theft latch is active.
ErrorsSilent cardsSide-fetches (usage/sessions/schedules/live) swallow errors and show their empty variants.

7.2 · Wire-cut (tamper) takeover

⚠ Wire Cut Detected 2:47 PM
Device Tampered
Borewell North · Submersible · 7 HP
Pump wire has been cut. Device is currently offline and cannot be controlled.
2:47 PM
Detected at
18 min ago
Status duration
Tamper card — persists until the pump reports ON again
All Pumps (501)
🔍 Search by name, village, IMEI...
✓ ALL (501) ON (112) OFF (389)
Borewell NorthON
📍 Cholmukh
POWER
3.7 kW
HP
7
IMEI
...45102938
ID
#4021
Canal PumpOFF
📍 Bhawanigarh
POWER
HP
5
IMEI
...77209311
ID
#4022
Pump list — search + ALL/ON/OFF filter chips (legacy visual language)

7.3 · Pump list & detail (legacy surfaces)

  • PumpListScreen — reached from Profile ("View all N pumps") or the dead Dashboard. Data comes from the profile's embedded pumps[] (never GET /pumps, which 403s for farmers) plus one /telemetry/latest call per pump in parallel. Status is derived client-side: ON / OFF / UNKNOWN. No polling — refresh is manual. From Profile, tapping a pump selects it and jumps back to the Pump tab; otherwise it opens the swipeable detail pager.
  • PumpDetailScreen / pager — full telemetry: status dot + Start/Stop side-by-side buttons (no stop confirmation here), POWER & ENERGY grid (Power / PF / Freq / kWh / Discharge / Temp), 3-phase voltage & current tiles (R red, Y amber, B blue), FAULT STATUS chips (only Dry Run / Overload / Overheat surfaced), and a DEVICE INFO key-value card. Polls every 15 s off / 30 s on; the pager gives each page its own timers and shows "N of M · swipe to browse" with page dots (≤10 pumps).
  • RecentSessionsScreen — "Last 30 sessions / Past 7 days · <pump>": a clean table of Date · Time range (mono, en-dash) · Duration rows from GET /reports/start-stop?pump_id=…&last_n_sessions=30. Empty state: "No sessions in the past 7 days". Note: lib/screens/recent_sessions_screen.dart is a dead byte-identical legacy copy — the live one is under features/pumps/.
Chapter 8

Irrigation schedules

Automated pump sessions: a schedule list with a global pause, a create/edit form with a hard-end cutoff, and a 7-day run history.

Source: lib/features/schedules/ — schedule_list_screen.dart · schedule_edit_screen.dart · occurrence_history_screen.dart · days_of_week_picker.dart

Schedules
Borewell North
🕘
All schedules
Turn off to pause all scheduled sessions
All schedules+ New

💧
Morning wateringActive🗑
Mon–Fri · 05:30 AM · 1 hr
⏹ Hard end 07:00 AM

💧
Evening top-upPaused🗑
Weekends · 06:00 PM · 45 min
+ Add new schedule
Schedule list — global pause card + per-schedule toggles
New schedule
Borewell North · Submersible · 7 HP
Save
SCHEDULE NAME
e.g. Morning watering

REPEAT DAYS
M T W T F S S

START TIME
05:30 AM

DURATION
How long should the pump run?
1 hour

END TIME (OPTIONAL)
07:00 AM
Create/edit form — grey #F3F4F6 background, single card of sections
Scheduled runs
Past 7 days · Borewell North
TodayIn progress
🕐05:30 – 42/60 min
YesterdayCompleted
🕐05:30 – 06:3060/60 min
Tue, 29 JulStopped at hard end
🕐05:41 – 07:0049/60 min
Mon, 28 JulManual override
🕐0/60 min
Schedule skipped — manual session ran instead
Sun, 27 JulIncomplete — power not restored
🕐05:30 – 05:5222/60 min
Run history — every status chip variant

Key behaviors

  • Global pause — the "All schedules" switch asks for confirmation ("Pause all schedules? All scheduled sessions will be paused… Individual schedule settings will be saved."). While paused, every row dims to 60% opacity, row toggles disable, and an amber banner reads "Scheduling is off — no sessions will run automatically until you turn it back on."
  • Day picker — seven 40 px circles, Monday-first M T W T F S S; selected = filled #15803D. Under the hood the bitmask is firmware order with Sunday = bit 0 (127 = every day, 62 = Mon–Fri, 65 = weekends).
  • Duration — bottom sheet with preset pills (15/30/45 min, 1 hour, 1h 30m, 2 hours) plus a free "min" field (1–1440).
  • Hard end — an optional cutoff time. Saving with one set triggers the confirmation "Set end time as cutoff? … The pump will stop at this time even if the full N min duration hasn't been completed. The session will be marked Incomplete if cut short." Cancel strips the hard end and saves anyway; dismissing aborts.
  • Validation (snackbars, in order): "Pick at least one day." · "Set a start time." · "Set a duration." · "End time must be after the start time." · "Duration must fit before the end time." Overlap conflicts come back from the API as 400s and surface verbatim.
  • Per-row toggle is optimistic — flips instantly, PUTs the slot, reverts with an error snackbar on failure.
  • Delete — trash icon → "Delete schedule? This removes "<name>" from this pump." with a red Delete button.

Run-history status chips (complete map)

API stateChip labelColors (fg / bg)
COMPLETEDCompleted#15803D / #DCFCE7
STOPPED_HARD_ENDStopped at hard end#B45309 / #FFFBEB
OVERRIDDENManual override (+ "Schedule skipped — manual session ran instead")#6B7280 / #F0FDF4
SKIPPEDSkipped#6B7280 / #F0FDF4
HELD_FAULTHeld — motor protection#DC2626 / #FEF2F2
INCOMPLETE (by off_reason)Incomplete — hard end passed / power not restored / could not start#DC2626 / #FEF2F2
PENDING · RUNNING · INTERRUPTED · RECOVERINGIn progress#0EA5E9 / #F0F9FF

API

GET/POST /pumps/:id/schedules · PUT/DELETE /pumps/:id/schedules/:slotIndex · POST /pumps/:id/schedules/pause {paused, confirm:true} · GET /pumps/:id/occurrences/history?from&to (7-day window) · GET /pumps/:id/occurrences/live. Times travel as IST HH:MM; every mutation returns the full slot list as source of truth.

Phase-1 caveats (from docs/scheduling-status-report.md)

Global pause is non-actuating in Phase 1 — the API stores the flag but firmware keeps running its slots, so the banner copy over-promises. Per-schedule is_active on PUT is speculative (may be ignored or 400). Run history stays empty until the backend enables SCHEDULE_CONTROLLER_ENABLED. Times are correct only on IST devices. Not built in V1: skip/postpone one occurrence, calendar preview, power-cut recovery of owed minutes, push notifications.

Chapter 9

Weather tab

Agricultural weather from the free Open-Meteo API — current conditions, irrigation-relevant advisories (ET₀ and VPD), a 24-hour temperature curve, precipitation, and a wind profile.

Source: lib/features/weather/presentation/screens/weather_tab.dart · weather_service.dart

CHOLMUKH, SANGRUR, PUNJAB · LIVE
31.4°
Partly Cloudy
Feels like 34.2°C
62%
💧 Humidity
11.5 km/h NW
💨 Wind
24.8 km/h
🌬 Gusts
996.4 hPa
📊 Pressure
Updated just now  ↻ Refresh
🌾
Agricultural Advisory
ET₀ · VPD · Precipitation outlook
💧
Reference ET₀ (FAO-56)
Low ET₀ — minimal irrigation needed today
0.4 mm/hr
🌡
Vapour Pressure Deficit
High VPD — heat stress risk, monitor closely
1.8 kPa
DEWPOINT
18.4°C
PRECIP PROB
12.0%
RAIN (1HR)
0.0 mm
VISIBILITY
24140 m
🌡
24-Hour Temperature
Surface (2 m)
09:0015:0021:0003:0008:00
🌧
Precipitation Forecast
Rain · Showers · Probability
Precip Probability
Rain 0.0 mm Showers 0.0 mm Snow 0.0 cm
💨
Wind Profile
Speed at 4 heights · km/h
180 m
36.6 km/h NW
120 m
28.9 km/h NW
80 m
22.1 km/h NW
10 m
11.5 km/h NW
💨 Gusts (10m): 24.8 km/h
Weather tab (header + nav bar cropped)

How it works

All data comes from one call to api.open-meteo.com/v1/forecast (7 forecast days, 41 hourly variables, timezone pinned to Asia/Kolkata). The tab owns no fetching — FarmerHome passes down weather, loading, error, and onRefresh. "Current" tile values read the hourly array at the index matching the current hour; charts slice the next 24 hours from now, not the calendar day.

Location resolution: profile lat/lon if set (place label built from the farmer's own village/district/state so it always matches what they entered) → otherwise geocode the village name via Open-Meteo's geocoder → otherwise no data at all (the tab sits on the loading state forever — a known gap, with no error message).

Advisory thresholds (the only "advice logic" in the app)

MetricBandMessageColor
ET₀ (mm/hr)< 2"Low ET₀ — minimal irrigation needed today"sky #0EA5E9
2 – 5"Moderate ET₀ — irrigate as planned"green #16A34A
≥ 5"High ET₀ — consider extra irrigation"amber #F59E0B
VPD (kPa)< 0.4"Low VPD — disease risk elevated"sky
0.4 – 1.6"Optimal VPD — ideal crop conditions"green
≥ 1.6"High VPD — heat stress risk, monitor closely"red #DC2626
Wind bars (km/h)< 20green #16A34A
20 – 40amber #F59E0B
≥ 40red #DC2626 — bars scale against an 80 km/h max

There is deliberately no "good day to irrigate" verdict — the ET₀/VPD banners are the closest the app gets.

States

LoadingSpinnerGreen spinner + "Fetching weather data…" + "Open-Meteo · <village>".
Error⚠️ card"Could not load weather", raw exception text below, green Retry button. Only when there's no cached data — otherwise stale data stays on screen.
RefreshPull or ↻Pull-to-refresh, or the "↻ Refresh" text in the hero footer. Freshness shown as "Updated just now / N min ago / N hr ago".
Gotchas

The tab redefines the whole palette in a private class W instead of importing C (values currently identical — will drift silently). assets/geography_latlong.json and its GeographyLookup parser are fully built but never called. Flat 0% precipitation renders as a flat line at the top of the sparkline box (min/max collapse).

Chapter 10

Soil health tab

Modelled soil moisture and temperature at depth — honest about being model data, and deliberately silent on irrigation advice.

Source: lib/features/soil/presentation/screens/soil_tab.dart

Not mock, not sensors

Despite the README calling this feature "mock", the live Soil tab is driven by the same Open-Meteo response as Weather (its soil_moisture_* / soil_temperature_* hourly fields). Every card subtitle says "modelled by Open-Meteo · not sensors". The old fabricated content — pH gauge, NPK bars, organic carbon, "Start Recommended Irrigation" — is all commented out and must not be shown.

SOIL MOISTURE PROFILE
Volumetric (VWC) · modelled by Open-Meteo · not sensors
Low
38% VWC at 3–9 cm · modelled, not measured
0–1 cmSurfaceLow32%
3–9 cmActive RootLow38%
27–81 cmDeep RootIdeal52%
Dry <25% Low 25–45% Ideal 45–70% Wet >70%
SOIL TEMPERATURE PROFILE
Modelled by Open-Meteo · not sensor readings
0 cmSurface
34.2°C
18 cmMid Root
28.9°C
54 cmDeep Root
26.1°C
NEXT 7 DAYS MOISTURE FORECAST
Root zone average (3–9 cm)
TodaySatSunMonTueWedThu
Soil tab (header + nav cropped)

Moisture bands (VWC %, at 3–9 cm)

BandRangeColor
Dry< 25%red #DC2626
Low25 – 45%amber #F59E0B
Moderate / Ideal45 – 70%green #16A34A
Wet> 70%sky #0EA5E9

Thresholds live in one place (_SoilThresholds: dry 25 / MAD 45 / saturated 70) and are hard-coded for the sugarcane–wheat range — a TODO wants them derived per-crop from the profile. Note the naming inconsistency: the big headline calls 45–70% "Moderate" while the row pills and legend call the same band "Ideal".

Temperature rows

Bars scale 5–50 °C. Surface (0 cm) turns red above 33 °C; mid-root (18 cm) turns amber above 30 °C; deep (54 cm) is always green. Out-of-range values (including a missing reading, which coerces to 0.0 °C) render a red bar with a ⚠ warning triangle.

Why no "irrigate now" advice?

A source comment spells it out: a real irrigate/hold call needs field capacity, wilting point, crop Kc and an ET₀ water balance the app doesn't compute — so the tab reports descriptive moisture bands only. Resist re-adding the old advice UI without that model.

States

Empty📡 Soil data unavailable"Check connection and try again" — shown whenever weather data is null (including first load; the tab is stateless and has no spinner of its own).
RefreshNoneNo pull-to-refresh here — data updates when Home/Weather refresh the shared weather object.
Chapter 11

Market (Mandi) tab

Today: a single honest "Coming Soon" card. Behind it sits a fully-designed mandi-price experience, deliberately disabled until a real data feed exists.

Source: lib/features/market/presentation/screens/market_tab.dart (1,342 lines, mostly dormant)

Mock data — deliberately disabled

All price data in this file is a hand-written const (kMandiData); there is no network call for mandi prices anywhere in the app. A source-comment warning block forbids re-enabling the price widgets before a real MandiService exists — the "Coming Soon" card is the only honest UI until then. Planned integrations: e-NAM, Maharashtra APMC, Open-Meteo, IMD (Phase 4 of the roadmap).

COMING SOON
Mandi Marketplace
Live crop prices and AI harvest advisory are on the way. Here's what you can expect:
📡
Live status bar
Real-time mandi connectivity & last-sync timestamp
🔌
API source panel
e-NAM · Maharashtra APMC · IMD district feeds
🌾
Crop price cards
Today's rates, trend sparkline, alternative markets
💧
Water–harvest AI insights
Tie water savings to optimal sell timing
📅
Upcoming market days
Nearby mandi schedule with travel distance
Market tab as shipped — the only rendered element

The dormant design (for context)

If you're asked to "turn on the market tab", this is what's already built and commented out, in order:

  1. Live status bar — mint strip with a pulsing green dot, "Live Mandi Prices", and an underlined "View API Sources" toggle (260 ms cross-fade into an integration list: e-NAM · Maharashtra APMC · Open-Meteo · IMD, each with refresh cadence and a pulse dot).
  2. Three crop price cards — Sugarcane 🎋 ₹3,150/quintal (▲ 3.3%, "✦ Harvest Now"), Onion 🧅 ₹1,240 (▼ 10.1%, "⏳ Hold Stock"), Wheat 🌾 ₹2,275 (▲ 0.0%, "👁 Monitor") — each with a 7-day sparkline (green when up, red when down), a source tag, mock "Today 06:30 AM" timestamps, and "Tap for details →".
  3. Detail bottom sheet — price hero (₹ in 40 px Fira Mono), Min/Max/MSP triplet, 7-day trend chart (axis "7d…Today"), a "Wattr AI: Harvest Now" advisory panel, "Compare Markets" rows with "+₹30 better" deltas, and a data-source footer.
  4. Wattr insight banner — dark-green gradient card linking water to harvest: "Sugarcane price at 3-week high. Stopping irrigation 4 days before cut improves sugar content AND saves ≈18 m³ of groundwater." with 💧 Water Saved / 📈 Price Today stat tiles.
  5. Upcoming market days — "UPCOMING MARKET DAYS — PUNE BLOCK": Mon 11 Mar Pune APMC, Wed 13 Mar Ahmednagar, Fri 15 Mar Lasalgaon.

A compact MandiPreviewCard for the Home tab exists too (also disabled). Note the file defines its own palette clone (class M) plus three indigo tokens (#4F46E5/#EEF2FF/#C7D2FE) for the APMC tag. Rupee amounts use Western grouping (₹3,150), not lakh grouping.

Chapter 12

Rewards tab

A complete gamification screen — tier rank, weekly water challenge, points, leaderboard — kept unmounted because every number in it is fabricated.

Source: lib/features/rewards/presentation/screens/rewards_tab.dart · domain/entities/tier.dart

Unreachable by design

RewardsTab is not mounted anywhere. IndexedStack slot 5 renders a placeholder instead — 🏆 "Rewards coming soon / Points and the village leaderboard will appear here once they're ready." — and no nav item even points at slot 5. The team explicitly chose not to ship fabricated points and neighbour rankings; the screen stays parked until real /rewards + /leaderboard endpoints exist. The mockup below shows the intended design (with its historical demo values 340 pts, 68/120 m³).

YOUR CONSERVATION RANK
🌿
Sprout
340 pts
Next: 🌾 Harvest160 pts away
THIS WEEK'S CHALLENGE
Stay under 120 m³
4 days remaining · Resets Sunday
🌿 On Track
USAGE THIS WEEK
68 / 120 m³
Remaining
52 m³
% Used
57%
Bonus
+35 pts
BLOCK LEADERBOARDYour rank: #4
🥇🌾
Sunita Jadhav
Saved 48%
156 PTS
🥈🌾
Manoj Deshmukh
Saved 44%
132 PTS
#4🌿
Raju Patil (you)
Saved 38%
68 PTS
🎁 Redeem Rewards
Soil kits, inputs & subsidies
Explore →
Rewards — intended design (points card & savings tiles omitted for length)

Tier system (tier.dart)

TierIconPoints bandColor
Seedling🌱0 – 199#4ADE80
Sprout🌿200 – 499#22C55E
Harvest🌾500 – 999#16A34A
Guardian🏆1000+#F59E0B

Resolution: highest tier whose threshold is met. At Guardian the "Next:" row and progress bar disappear. No benefits/perks are modelled — the only redemption surface is the generic "Redeem Rewards" CTA, whose Explore button is a no-op.

Weekly challenge logic

  • 12-segment usage bar; segments fill by round(pct×12), colored sky → green → amber as usage climbs.
  • Badge flips from "🌿 On Track" to "⚠ Near Limit" (amber) at 80% of the weekly limit; the big number turns amber too.
  • Bonus points = round((1−pct)×80) — up to +80 for saving the whole quota. Explainer strip: "💡 How points work: Stay under 120 m³ and earn up to +80 bonus points…"
  • Points breakdown card: 💧 Water saved vs. baseline +40 · 🎯 Under daily AI target × 4 +20 · 📱 Daily app check-in (3-day) +6 · ⚡ Pump efficiency bonus +2. Water savings card: 💧 4,820 litres this month · 📉 34% vs last month.
Known inconsistencies in the mock

The leaderboard shows "you" with 68 pts while the hero shows 340; row tier icons are hard-coded literals, not derived from points (rank 1's 156 pts would actually be Seedling). Keep these in mind if the screen is ever revived — they're data bugs waiting to happen.

Chapter 13

Alerts

Reached from the header bell. Today the screen is an intentional empty state — the dummy list was removed and the real feed hasn't landed yet.

Source: lib/features/alerts/ — alerts_screen.dart · alert_card.dart (currently unused) · alerts_service.dart

Alerts & Notifications
Stay updated with the latest farming insights
🔕
No notifications available
Alerts screen as shipped — empty state only
ACTIVE ALERTS · 3
⚠️Soil moisture below threshold09:14 AM
Field 1 · 15 cm sensor · 42% (threshold 45%)
Start Irrigation
🔴Pump bearing vibration elevatedYesterday
Borewell 1 · Exceeds normal range by 18%
Call Officer
RESOLVED
✓ Irrigation session completed
42 min · 29.4 m³ · 4.1 kWh · Yesterday 5:08 PM
Design intent — the AlertCard system (recovered from git history; not currently rendered)

What exists vs. what renders

  • AlertsService is real and used for the header badge: GET /alerts (optionally ?status=active|resolved), GET /alerts/unread-count, POST /alerts/:id/resolve. The screen itself makes no network call — it's a hardcoded empty state since the dummy data was removed (commit aa70123).
  • AlertCard is a finished three-severity component (red / amber "sun" / green — colored background + 4 px left accent bar + optional action button) with zero call sites. Its action button is onPressed: () {} and resolveAlert has no callers.
  • Severity vocabulary mismatch to resolve before wiring it up: the API speaks red / warning / info, the widget enum speaks red / sun / green, and FarmerAlert has no icon field though the card requires one.
  • The header bell badge is fetched once at launch and never refreshed (Chapter 4).
Chapter 14

Profile & settings

One pushed screen with two modes — a read-only card list, and an accordion edit form. Also home to the language switcher, the theft-alarm permission tile, and Sign Out.

Source: lib/features/profile/presentation/screens/profile_screen.dart (1,905 lines) · profile_service.dart · farmer_profile.dart

Reached by tapping the avatar in the home header (it's a pushed route, not a tab). The largest screen in the app: nine view-mode cards, an eight-section edit form, and the full pump-registration flow.

View & Edit Profile
View your details · Tap Edit Profile to update
👤  Personal Information
Full NameGurpreet Singh
Father's NameHarbans Singh
Mobile Number98761 43210
📍  Location & Feeder
StatePunjab
VillageCholmukh
Feeder NameBhawanigarh-2
Farm Area5.5 acres
🚜  Pump Details
3
PUMPS
18
TOTAL HP
12.0
TOTAL ACRES
2 Submersible 1 Centrifugal
⚡ View all 3 pumps · live status
Search, filter by status, and swipe between pumps.
🔔
Theft Alarm on Lock Screen
The loud theft alarm can take over your lock screen.
⎋ Sign Out
✎ Edit Profile
Profile — view mode (Water Source, Canal, Laser Levelling, Kharif Crop & Language cards continue below the fold)

View mode — nine cards in order

  1. 👤 Personal Information — Full Name, Father's Name, Mobile. Read-only everywhere (edit mode shows "Contact your administrator to update personal details.").
  2. 📍 Location & Feeder — State, District, Block/Taluka, Village, Feeder Name, Lat/Lon (4 dp), Land Ownership, Farm Area. Location fields are admin-managed too; only Land Ownership + Farm Area are editable.
  3. 🚜 Pump Details — stat tiles (count / total HP / total acres), type-breakdown chips, and a green CTA into PumpListScreen ("View live status & controls" for one pump, "View all N pumps · live status" otherwise). Empty state: "No pumps yet. Tap Edit Profile to add one."
  4. 💧 Water Source — Source (defaults "Groundwater Only") + Depth with unit.
  5. 🌊 Canal Irrigation — Yes/No; name, days and time period only when Yes.
  6. ⚖️ Laser Land Levelling — Year Done / Year Planned.
  7. 🌾 Kharif Crop — Crop, Variant, Sowing Type (rice only), Sowing Date (DD-MM-YYYY).
  8. 🗣️ Language Preference — English / हिन्दी / ਪੰਜਾਬੀ.
  9. 🔔 Theft Alarm on Lock Screen (Android only) — green check when both permissions are granted; red border + "Allow full-screen alerts" / "Allow display over other apps" buttons when not. Then the outlined red Sign Out button (confirmation dialog: "Are you sure you want to sign out?").

Empty values render as an em-dash "—" in grey — the screen never shows a loading or error state because it receives a fully-resolved profile from the shell.

Edit mode

The FAB flips to Cancel + Save Profile and the cards become an 8-section accordion (one open at a time, Personal Information open first). Highlights:

  • Pump Details is the heart of it: a gradient counter bar ("Total Pumps: N" + Add Pump), then a card per pump with Name, HP dropdown (1–30), Coverage Area, Type (Submersible / Centrifugal / Turbine / Jet Pump), Manufacturer, Year (2000–2029), Device IMEI (15 digits, required — the backend upserts pumps keyed by IMEI), and a Capacitor switch. Delete is instant, no confirmation, and hidden when only one pump remains.
  • Kharif Crop — 17 crop options with dependent variant lists; "Sowing Type" appears only for Rice / Paddy (DSR vs PTR); Material date picker for sowing date (2020–2030).
  • Language — switching applies instantly (whole UI re-renders, persisted locally), before any Save; Cancel does not revert it.
  • Info banners: sky-blue "Source: Groundwater Only (as per Wattr platform scope)" and amber "⚖️ Laser land levelling reduces irrigation water use by 20–30%…".

Save flow

Client-side pump validation runs first — failures open the Pump section and show a red snackbar with the first error ("Pump 2: Device IMEI must be exactly 15 digits", duplicate-IMEI check included). Then PUT /farmer-profiles/me. Success pops the screen with a green snackbar ("Profile saved. Weather updated for <village>."); failure keeps edit mode with "Couldn't sync to server — saved locally. Will retry next load." — the profile is always written to the local cache before the network call, so local wins offline.

Gotchas (documented in source)

The red * markers on crop/variant/language dropdowns are decorative — only pump name + IMEI are actually enforced. onSave updates the shell's profile even when the server rejected the write. Pump-card text fields can lose focus while typing (controllers recreated on rebuild — known bug). Longitude travels as lng on the wire but lon in the model. name/mobile are never sent — they belong to the auth user record.

Data

GET/PUT /farmer-profiles/me (snake_case wire format, camelCase local cache under SharedPreferences key wattr_farmer_profile). ProfileService.load() merges cache → server → Cognito user fields, repairs phone-number-as-name legacy data, and backfills village/lat/lon from the first pump when missing (state inferred from lat/lon bounding boxes).

Chapter 15

SOS theft alarm & push

The safety-critical feature: when a pump controller reports tampering, the phone becomes a siren — full-screen red takeover, looping alarm on the ALARM audio stream, forced max volume, and (on Android) a lock-screen takeover.

Source: lib/features/sos/presentation/sos_screen.dart · lib/core/push/ · docs/sos-theft-alert.md · docs/ios-sos-lockscreen.md

2:47▲ ▂▄▆ ▮
⚠️
THEFT ALERT
Possible tampering detected
💧 Borewell North
🕐 2:47 PM · 1 Aug
📍 30.20512, 75.91503
🗺 View on Map
I'm aware — dismiss
SOS overlay — flat #DC2626, painted above the whole app

How it's raised

  1. Production path — backend sees a tf telemetry packet → FCM push with data.type = "theft" (+ deviceId, pumpId, imei, eventTime, tenantId) → SosController.raise(). De-duped on deviceId + eventTime so double delivery never re-triggers the siren.
  2. Interim path — the foreground telemetry poll itself sees data_type == 'tf' and raises locally (marked DEMO ONLY in source; it's the only path that supplies pump name and coordinates).
  3. Background/locked (Android) — the FCM background isolate stashes the payload and posts a full-screen-intent notification (channel sos_theft, "Theft / tamper alert", max importance, DND bypass, alarm sound). MainActivity then shows the app over the lock screen. Requires the two permissions prompted at first launch.

Alarm behavior

  • Soundassets/sos_alarm.wav looped at volume 1.0, routed to the Android ALARM stream (audible in silent mode / Doze); iOS uses the playback category so it plays through the silent switch.
  • Volume override — current alarm-stream volume is saved, forced to max, and restored on dismiss.
  • Vibration — Android: 300 ms on / 100 ms off, looping. iOS: heavy-impact double-thumps every 450 ms (can't loop a haptic pattern).
  • Auto-dismiss — self-acknowledges after 30 minutes. Dismissing clears only the overlay; the tamper card on the Pump tab stays until the pump reports running again.
  • No pulsing animation — the screen is deliberately static; all the urgency is audio + haptic.

iOS reality check

iOS never allows a custom full-screen takeover from a push — the lock screen shows a banner only. The app ships the time-sensitive entitlement now and has critical alerts staged pending Apple approval; the full SOS screen appears once the user opens the app.

Info-card content rules

Subject line precedence: pump name → "Pump #id" → "Device <imei>" → "Device #id" → "Your pump". Timestamp "h:mm a · d MMM" or "Just now". The IMEI row and coordinates only appear on the interim path (pushes don't carry them), and "View on Map" is currently not tappable — the maps URL is computed but never opened (known gap).

Push plumbing

Token registration: PUT /user-devices/devices with {fcm_token, fcm_platform} after login and on every token refresh. Acknowledge only calls SosController.clear() — it does not resolve a backend alert (the payload carries no alert_id), a documented doc/code drift.

Chapter 16

API reference

Every endpoint the app actually calls, in one table. Base: https://staging-api.wattr.ai/v1 (Bearer JWT; auto-refresh on first 401/403 via POST /auth/refresh).

EndpointUsed byNotes
POST /auth/loginLogin{usernameOrEmail, password} → access + refresh tokens + user
GET /users/currentSession restore401 → logout; network error → optimistic offline sign-in
GET /farmer-profiles/meShell, Profile, Pump listProfile + embedded pumps[] — the app's only pump list source (no GET /pumps for farmers)
PUT /farmer-profiles/meProfile savesnake_case body; pumps upserted keyed by device_imei; longitude travels as lng
GET /telemetry/latest?pump_id=…Home, Pump tab, Pump list/detailPolled 10 s (off) / 30 s (on); 30+ fields incl. 8 fault flags, on_by, runtime_seconds, data_type (hb/of/tf)
GET /pumps/:id/statusCommand confirmationOnly while a start/stop is pending; running is physics-derived
POST /commands/pump/:id/start|stopAll control surfaces202 = queued, never optimistic; 1/2/3/5/8/12/18/25 s confirm polls; 30 s no-ack → offline
GET /reports/start-stop?pump_id&last_n_sessions=30Sessions screens7-day window (server-capped)
GET /reports/start-stop/summary · /dailyPump tab energy card14-day window; daily kWh prorated from runtime
GET/POST /pumps/:id/schedules · PUT/DELETE …/:slotIndex · POST …/pauseSchedulesIST HH:MM; day bitmask Sun=bit 0; mutations return the full slot list
GET /pumps/:id/occurrences/live · /history?from&toPump tab live card, run historyEmpty until SCHEDULE_CONTROLLER_ENABLED lands server-side
GET /alerts/unread-countHeader bell badgeFetched once at launch; failures silent
GET /alerts · POST /alerts/:id/resolveservice onlyImplemented but no UI calls them yet
PUT /user-devices/devicesPush registration{fcm_token, fcm_platform}
Open-Meteo /v1/forecast + geocoderWeather, Soil, HomeThird-party, no key; 41 hourly vars, 7 days, Asia/Kolkata

Error copy is centralized in ServiceError.userMessage: "No network — check your connection and try again." · "Session expired — please sign in again." · "Not found." · "Server error — please try again in a moment." · else the server's own message or "Request failed."

Dead services (implemented, zero call sites): QuotaService (/water-quota/current, /savings) and DashboardService (/dashboard/stats, /fault-summary, /power-hourly, /daily-aggregates — only reachable from the unmounted DashboardScreen).

Chapter 17

Feature status & test data

The single most useful table for a new joinee: what's real, what's mock, and what's parked.

FeatureStatusReality
Login / sessionLiveReal Cognito-backed auth via /auth/login; offline-tolerant session restore
Pump control + telemetryLiveThe core product — real polling, real commands, honest "—" for missing data
SchedulesLiveCRUD works; global pause is non-actuating in Phase 1; run history empty until backend flag flips
WeatherLiveOpen-Meteo (third-party, free) — real data, not the Wattr API
Soil healthLiveOpen-Meteo modelled soil fields — clearly labeled "not sensors"; old fabricated pH/NPK UI removed
ProfileLiveFull read/edit against /farmer-profiles/me, incl. pump registration by IMEI
SOS theft alarmLiveFCM + full-screen takeover on Android; banner-only on iOS pending critical-alert entitlement
AlertsEmpty shellScreen shows only "No notifications available"; AlertCard + service ready but unwired
Market / MandiComing SoonFull design dormant behind a Coming Soon card; all data was hand-written mock
RewardsParkedComplete screen, unmounted; slot shows "Rewards coming soon"; no nav path at all
Water quotaDead codeQuotaService + weeklyLimit = 120 m³ exist with zero call sites; no quota UI renders
Admin dashboardDead codeDashboardScreen fully built, entry point commented out

Testing on staging

Long-press the login footer to point a build at staging vs production. Staging test accounts (Super Admin with all pumps, plus two farmer accounts with 14 and 501 pumps respectively — useful for small-list vs big-list testing) are listed in the repo README.md. Treat them as secrets: don't paste them into docs, tickets, or screenshots.

First-week checklist for a new joinee

  1. Run the app against staging (flutter run), sign in with a farmer account, and walk every tab with this doc open.
  2. Read farmer_home.dart top to bottom — it owns all shared state and every timer.
  3. Trigger a pump start on a staging pump and watch the "confirming…" → confirmed flow (and the 30 s offline path with an unplugged device).
  4. Switch language to हिन्दी and note which strings fall back to English — untranslated strings are silent, not errors.
  5. Skim the four docs in docs/ — they explain why scheduling and SOS look the way they do.
House style, in one paragraph

Green on cream, cards with mint borders, Fraunces for headlines, DM Sans for words, Fira Mono for every number. Missing data is an em-dash "—", never a fabricated value. Commands are never optimistic. English strings are the i18n keys. And if a widget looks finished but you can't find it in the app — check whether it's one of the deliberately parked features above before assuming a bug.