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.
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
| Persona | Role code | What they see |
|---|---|---|
| Farmer — the primary user | INDIVIDUAL_USER | Their own pumps (typically 1–15), quota, weather, rewards |
| Scheme operator / admin | CLIENT_ADMIN & up | All pumps in their tenant — pump lists can reach 500+ |
| Wattr staff | SUPER_ADMIN | Everything, 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
| Environment | Base URL | Notes |
|---|---|---|
| Staging | https://staging-api.wattr.ai/v1 | Default for development; test accounts live here |
| Production | https://api.wattr.ai/v1 | Real 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
| Concern | Choice |
|---|---|
| Framework | Flutter 3.41 / Dart 3.11, Material 3 |
| State | setState for local state; Provider + ChangeNotifier only where state crosses widgets (translator, SOS controller) |
| HTTP | dart:io HttpClient wrapper with JWT auth (core/network/api_client.dart) |
| Charts | fl_chart |
| Fonts | google_fonts — Fraunces / DM Sans / Fira Mono (+ Noto Sans Gurmukhi fallback) |
| Push | Firebase Cloud Messaging + flutter_local_notifications (theft SOS) |
| Weather | Open-Meteo public API (no key) |
| Persistence | SharedPreferences (session, profile cache, language) |
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.
setStateunless state genuinely crosses widget boundaries — today onlyTranslatorProvider(language) andSosController(theft alarm) qualify for Provider. Don't introduce Riverpod/Bloc/GetX. - Services own HTTP. Screens never build requests; they call
*Serviceclasses which use the sharedApiClient(adds JWT, handles errors asServiceError). - 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 toassets/i18n/hi.jsonandpa.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 tabledocs/sos-theft-alert.md&docs/ios-sos-lockscreen.md— theft alarm designdocs/scheduling-status-report.md&docs/scheduling-v1-deferred.md— scheduling scope and deferred itemsAPI_REQUIREMENTS.md— backend endpoints the app expects
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
Accent families & semantic colors
Surfaces, borders & 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.
firaCode() so digits align.Shared widgets (core/widgets/)
| Widget | What it is |
|---|---|
WText | Drop-in Text replacement used app-wide; single place to control text behavior (overflow, scaling) |
AppCard | The standard white rounded card: C.card background, C.bd border, ~16 px radius, soft padding — nearly every content block sits in one |
PillBadge | Small rounded status pill (colored background + label) — pump ON/OFF, severities, tags |
SectionLabel | Uppercase letter-spaced section heading in C.t2 green |
ArcGauge | Semi-circular gauge (quota usage, soil moisture) with color thresholds |
SimpleAreaChart / SimpleBarChart | fl_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.
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.
Header anatomy
- 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.
- 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.
- Language button — globe icon in a frosted 40 px circle. Opens the Choose Language bottom sheet (English / हिन्दी / ਪੰਜਾਬੀ).
- 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
| # | Tab | Widget | Icons (inactive → active) | Notes |
|---|---|---|---|---|
| 0 | Home | HomeTab | home_rounded (same both) | |
| 1 | Weather | WeatherTab | wb_cloudy_outlined → _rounded | |
| 2 | Pump (center) | PumpTab | water_outlined → water | Default tab on launch. 60 px elevated gradient circle; shows a small white dot under the icon while the pump is running |
| 3 | Soil | SoilTab | eco_outlined → eco_rounded | |
| 4 | Market | MarketTab | storefront_outlined → _rounded | |
| 5 | (Rewards) | _ComingSoonPlaceholder | — | Unreachable — 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).
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)
| Mechanism | Behavior |
|---|---|
| Telemetry poll | GET /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 confirmation | After 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 ticker | 1 s timer while running so the runtime readout counts up smoothly between polls |
| OFF smoothing | pump_status=true flips ON immediately; OFF needs two consecutive false readings (a single data_type='of' packet flips OFF at once) |
| Theft latch | A 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
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:
- "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)
- "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.
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.
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
Anatomy
- 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.
- 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. - Sign-in card — white, 28 px radius. Fields have grey
#F9FAFBfills and#E5E7EBborders that turn green#22C55Eon focus. The eye icon toggles password visibility. "Next" on the keyboard moves username → password; "Done" submits. - Sign In button — full-width, 54 px,
#16A34A. While the request runs it turns pale green#86EFACwith a white spinner. - Footer — "Powered by WATTR.AI" and a version string. ⚠ The version is hard-coded "v1.0.0" while
pubspec.yamlis at 1.0.2+7 — a known stale label.
States
#FEF2F2, red border, ⓘ icon, server's message or "Login failed".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.
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
Anatomy (top to bottom)
- 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_idand refetches telemetry. - 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.
- Pump status card — hidden entirely when no pump. Title is
NAME · TYPEuppercased; anon_bypill (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#9CA3AFwhen 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". - 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).
- 24-hour temperature card — fl_chart area line of the next 24 h (2 m air temp), green
#16A34Awith dashed mint gridlines; tap → Weather tab. - 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
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.
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
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)
Anatomy (six cards)
- 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.
- Live scheduled-run card — appears only while
GET /pumps/:id/occurrences/livereports 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. - 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.
- 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).
- Last session preview — first of the last 30 on→off cycles (date, time range in mono, duration, trigger pill). Tap → Recent sessions screen.
- Energy/usage report — 14-day totals (runtime / water litres / cycles) from
/reports/start-stop/summaryplus 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.
7.2 · Wire-cut (tamper) takeover
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[](neverGET /pumps, which 403s for farmers) plus one/telemetry/latestcall 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.dartis a dead byte-identical legacy copy — the live one is underfeatures/pumps/.
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
#F3F4F6 background, single card of sectionsKey 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 state | Chip label | Colors (fg / bg) |
|---|---|---|
COMPLETED | Completed | #15803D / #DCFCE7 |
STOPPED_HARD_END | Stopped at hard end | #B45309 / #FFFBEB |
OVERRIDDEN | Manual override (+ "Schedule skipped — manual session ran instead") | #6B7280 / #F0FDF4 |
SKIPPED | Skipped | #6B7280 / #F0FDF4 |
HELD_FAULT | Held — motor protection | #DC2626 / #FEF2F2 |
INCOMPLETE (by off_reason) | Incomplete — hard end passed / power not restored / could not start | #DC2626 / #FEF2F2 |
PENDING · RUNNING · INTERRUPTED · RECOVERING | In 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.
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.
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
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)
| Metric | Band | Message | Color |
|---|---|---|---|
| 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) | < 20 | green #16A34A | |
| 20 – 40 | amber #F59E0B | ||
| ≥ 40 | red #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
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).
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
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.
Moisture bands (VWC %, at 3–9 cm)
| Band | Range | Color |
|---|---|---|
| Dry | < 25% | red #DC2626 |
| Low | 25 – 45% | amber #F59E0B |
| Moderate / Ideal | 45 – 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
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)
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).
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:
- 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).
- 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 →".
- 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.
- 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.
- 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.
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
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³).
Tier system (tier.dart)
| Tier | Icon | Points band | Color |
|---|---|---|---|
| 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.
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.
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
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 (commitaa70123). - 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: () {}andresolveAlerthas no callers. - Severity vocabulary mismatch to resolve before wiring it up: the API speaks
red / warning / info, the widget enum speaksred / sun / green, andFarmerAlerthas noiconfield though the card requires one. - The header bell badge is fetched once at launch and never refreshed (Chapter 4).
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 mode — nine cards in order
- 👤 Personal Information — Full Name, Father's Name, Mobile. Read-only everywhere (edit mode shows "Contact your administrator to update personal details.").
- 📍 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.
- 🚜 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."
- 💧 Water Source — Source (defaults "Groundwater Only") + Depth with unit.
- 🌊 Canal Irrigation — Yes/No; name, days and time period only when Yes.
- ⚖️ Laser Land Levelling — Year Done / Year Planned.
- 🌾 Kharif Crop — Crop, Variant, Sowing Type (rice only), Sowing Date (DD-MM-YYYY).
- 🗣️ Language Preference — English / हिन्दी / ਪੰਜਾਬੀ.
- 🔔 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.
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).
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
#DC2626, painted above the whole appHow it's raised
- Production path — backend sees a
tftelemetry packet → FCM push withdata.type = "theft"(+deviceId,pumpId,imei,eventTime,tenantId) →SosController.raise(). De-duped ondeviceId + eventTimeso double delivery never re-triggers the siren. - 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). - 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).MainActivitythen shows the app over the lock screen. Requires the two permissions prompted at first launch.
Alarm behavior
- Sound —
assets/sos_alarm.wavlooped 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.
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).
| Endpoint | Used by | Notes |
|---|---|---|
POST /auth/login | Login | {usernameOrEmail, password} → access + refresh tokens + user |
GET /users/current | Session restore | 401 → logout; network error → optimistic offline sign-in |
GET /farmer-profiles/me | Shell, Profile, Pump list | Profile + embedded pumps[] — the app's only pump list source (no GET /pumps for farmers) |
PUT /farmer-profiles/me | Profile save | snake_case body; pumps upserted keyed by device_imei; longitude travels as lng |
GET /telemetry/latest?pump_id=… | Home, Pump tab, Pump list/detail | Polled 10 s (off) / 30 s (on); 30+ fields incl. 8 fault flags, on_by, runtime_seconds, data_type (hb/of/tf) |
GET /pumps/:id/status | Command confirmation | Only while a start/stop is pending; running is physics-derived |
POST /commands/pump/:id/start|stop | All control surfaces | 202 = 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=30 | Sessions screens | 7-day window (server-capped) |
GET /reports/start-stop/summary · /daily | Pump tab energy card | 14-day window; daily kWh prorated from runtime |
GET/POST /pumps/:id/schedules · PUT/DELETE …/:slotIndex · POST …/pause | Schedules | IST HH:MM; day bitmask Sun=bit 0; mutations return the full slot list |
GET /pumps/:id/occurrences/live · /history?from&to | Pump tab live card, run history | Empty until SCHEDULE_CONTROLLER_ENABLED lands server-side |
GET /alerts/unread-count | Header bell badge | Fetched once at launch; failures silent |
GET /alerts · POST /alerts/:id/resolve | service only | Implemented but no UI calls them yet |
PUT /user-devices/devices | Push registration | {fcm_token, fcm_platform} |
Open-Meteo /v1/forecast + geocoder | Weather, Soil, Home | Third-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).
Feature status & test data
The single most useful table for a new joinee: what's real, what's mock, and what's parked.
| Feature | Status | Reality |
|---|---|---|
| Login / session | Live | Real Cognito-backed auth via /auth/login; offline-tolerant session restore |
| Pump control + telemetry | Live | The core product — real polling, real commands, honest "—" for missing data |
| Schedules | Live | CRUD works; global pause is non-actuating in Phase 1; run history empty until backend flag flips |
| Weather | Live | Open-Meteo (third-party, free) — real data, not the Wattr API |
| Soil health | Live | Open-Meteo modelled soil fields — clearly labeled "not sensors"; old fabricated pH/NPK UI removed |
| Profile | Live | Full read/edit against /farmer-profiles/me, incl. pump registration by IMEI |
| SOS theft alarm | Live | FCM + full-screen takeover on Android; banner-only on iOS pending critical-alert entitlement |
| Alerts | Empty shell | Screen shows only "No notifications available"; AlertCard + service ready but unwired |
| Market / Mandi | Coming Soon | Full design dormant behind a Coming Soon card; all data was hand-written mock |
| Rewards | Parked | Complete screen, unmounted; slot shows "Rewards coming soon"; no nav path at all |
| Water quota | Dead code | QuotaService + weeklyLimit = 120 m³ exist with zero call sites; no quota UI renders |
| Admin dashboard | Dead code | DashboardScreen 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
- Run the app against staging (
flutter run), sign in with a farmer account, and walk every tab with this doc open. - Read
farmer_home.darttop to bottom — it owns all shared state and every timer. - Trigger a pump start on a staging pump and watch the "confirming…" → confirmed flow (and the 30 s offline path with an unplugged device).
- Switch language to हिन्दी and note which strings fall back to English — untranslated strings are silent, not errors.
- Skim the four docs in
docs/— they explain why scheduling and SOS look the way they do.
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.