Internal documentation · Wattr.ai

Wattr Operator App
Visual Specification

Screen-by-screen specification of the operator mobile app, generated from source. Screens are recreated as annotated HTML mockups from the widget code; live values shown are representative examples.

Flutter · Dart ^3.11 Riverpod 3 · Dio 5 · Material 3 Android · iOS English · हिन्दी · ਪੰਜਾਬੀ Repo: wattr-ai/mobile-app-operator · release/mao-4.0.0
Chapter 1

App overview

The fleet-control companion for scheme staff — many pumps, one operator.

Wattr Operator is used by scheme operators and Junior Engineers (JEs) to monitor and control the pumps in their scope: a filterable fleet dashboard, per-pump detail with live readings and start/stop, run-session history with export, per-pump schedules, automation settings, and a push-notification inbox. Where the farmer app serves one person's 1–15 pumps, this app is built for hundreds (staging tenant 41: 503 pumps).

Roles

RoleAPI role codeDifferences in-app
OperatorOPERATORBaseline experience
Junior Engineer (JE)CLIENT_ADMIN / JESees an "Operator: <name>" line on rows/detail and an operator filter group. Note: per-user scoping is not yet enforced server-side — an operator currently sees the whole tenant.

Architecture in brief

  • State: Riverpod 3 (providers per feature, autoDispose where re-entry should refetch). HTTP: single Dio client, base URL hard-coded to https://staging-api.wattr.ai/v1 (no build-time override — a known gap), Bearer token injected by interceptor, one automatic retry after POST /auth/refresh on 401.
  • Navigation: no router package. An AuthGate switches Splash / Login / Dashboard on auth state; everything else is imperative Navigator.push. Push notifications deep-link through a global navigator key.
  • Layout convention: feature-first (features/<name>/data·domain·presentation), shared code under core/.
  • i18n: same mechanism as the farmer app — the English string is the dictionary key, WText looks it up, hi.json/pa.json hold ~245 entries each, language persists under wattr.language. Server-rendered notification text is deliberately never re-translated.
  • Tokens are stored in SharedPreferences (wattr_access_token / wattr_refresh_token / wattr_user) — not secure storage.
Chapter 2

Design system

Material 3 seeded from brand teal, Inter throughout, full light + dark themes following the OS setting.

Source: lib/core/constants/app_colors.dart · app_text_styles.dart · app_dimensions.dart · lib/core/theme/app_theme.dart

Brand & surfaces (light)

primary#0F6E5C · buttons, focus, chart bars
primaryDark#0B5245 · login gradient start
secondary#3DA18E · login gradient end
background#F9FAFB · scaffold, input fill
surface#FFFFFF · cards, app bar
textPrimary#111827
textSecondary#6B7280 · muted
border#E5E7EB

Status colors (the 3-tier scheme)

success#2E7D32 · running normally (bg #E6F4EA)
warning#B8860B · needs attention — faults are amber (bg #FFF4DE)
error#C62828 · off, any reason (bg #FCE8E8)
brand orange#F26C21 · splash bolt only (local constant)

Resolved design decision (docs/prd-vs-design-discrepancies.md §1): faults render amber, not red — red means "not running, for any reason" (shutdown, bypass, remote stop, power loss). Color is never the only signal; every state also carries an icon and a text caption.

Dark theme

Full dark palette ships and follows the OS: background #0B0F19, surface #161B26, text #F9FAFB, muted #9CA3AF, border #2A3040, input fill #1F2937. The status tints have no dark variants — the light backgrounds (#E6F4EA / #FFF4DE / #FCE8E8) are reused as-is in dark mode.

Typography

One family — Inter (variable, bundled) with Noto Sans Devanagari / Gurmukhi fallbacks for Hindi and Punjabi. Scale: display 32/700 · headline 24/700 · titleLarge 20/600 (app bar) · titleMedium 16/600 · body 16 & 14/400 · labelLarge 14/600 (buttons) · bodySmall 12/400. No serif, no monospace — numerics use Inter.

Dimensions & shared widgets

4-pt spacing grid (4/8/16/24/32/48), radii 8/12/20/pill, buttons and inputs 52 px tall, inputs filled #F9FAFB with 12 px radius and a 1.5 px teal focus border.

WidgetSpec
WTextTranslating Text — English literal is the i18n key; raw Text is reserved for data (names, IDs, times)
SectionCardM3 Card, 16 px padding, title 16/700 + optional 12 px subtitle + optional trailing action
PrimaryButtonFull-width 52 px, teal fill, 12 px radius; loading swaps the label for a 24 px spinner
Sparklinefl_chart line, 48 px tall, curved, 2 px stroke, 12% area fill, no axes/touch
MaskedIdTextPrivacy control: device icon + ••••• + last 5 digits of the IMEI — the full IMEI is never rendered
AppScaffoldScaffold wrapper (SafeArea + optional 16 px padding) used by list screens
Chapter 3

Splash, login & shell

AuthGate switches Splash → Login → Dashboard; a drawer carries account-level actions.

Source: lib/features/splash/ · lib/features/auth/ · lib/features/shell/presentation/widgets/app_drawer.dart

9:41▲ ▂▄▆ ▮
wattr
Watts & Water. Forever.
Splash — flat teal, shown during session restore
9:41▲ ▂▄▆ ▮
🌐 English
Wattr logo tile
Operator Console
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
Login — teal gradient, navy logo tile
Ravinder Singh
Junior Engineer

🔔Notification History
🎚Notification Preferences
🌐LanguageEnglish ⌄

Logout
Drawer — attached to the dashboard only

Behavior notes

  • Splash is a plain teal #0F6E5C screen with the orange bolt + "wattr" wordmark (raw Text, never translated). Shown only while restoreSession() runs: stored token → GET /users/current; 401/403 clears tokens; network/5xx falls back optimistically to the cached user.
  • Login: fields disable while submitting; client validation "Please enter username and password" (no network call); server errors show in a fixed light-red banner (#FCE8E8 — does not adapt to dark mode). Network failure → "Connection failed. Check your network."; otherwise "Invalid credentials" or the server's message. Language chip (top-right) opens the same en/hi/pa sheet as the drawer.
  • Drawer: name (fallback email, then "—") + role label ("Junior Engineer" / "Operator"), Notification History, Notification Preferences, a Language dropdown row, and Logout (red icon) with the confirmation "Sign out? — You will need to sign in again to continue." (Cancel / Yes, sign out). Logout clears tokens but keeps the language preference.
  • Auth API: POST /auth/login (usernameOrEmail + password), GET /users/current, POST /auth/logout (best-effort), POST /auth/refresh.
Chapter 4

Dashboard (pump list)

The home screen: summary tiles that are also filters, search, a filter/sort sheet, and an infinite-scrolling fleet list.

Source: lib/features/dashboard/ · docs/pump-list-api-contract.md

Wattr.ai Operator 🔔3
🔍 Search by tower or pump name
🎚
503
Total Pumps
66
On
390
Off
47
Fault
⛛ Status: On
💧 A
Tw 5
Operator: Ravinder Singh
Running · Auto
Running for 2h 8m
Just now
▫ •••••35838
Tw 2
Fault · Reverse Phase
Faulted for 24m
5 min ago
▫ •••••41209
📶
Tw 3
Off · Bypass
Off for 3d 4h
Stale
▫ •••••77031
Dashboard — "On" tile active as a filter; running, faulted and stale rows

Anatomy

  1. App bar — "Wattr.ai Operator", hamburger (drawer), bell with an unread-count badge (GET /notifications/unread-count).
  2. Search + filter button — search matches tower/pump name, debounced 350 ms; the tonal filter button shows a dot badge when any filter is active and opens the filter sheet.
  3. Summary tiles are filters — Total / On / Off / Fault counts from GET /pumps/status-summary (always unfiltered, whole scope). Tapping On/Off/Fault toggles that status filter; the active tile gets a 1.5 px border and 10% tint. Values show "—" while loading. Mapping: On = running.total; Fault = running.with_faults + power_issues; Off = stopped.total.
  4. Active filter chips — plain rows (icon + label + ⓧ) for Status / Mode / Operator; search is deliberately not chipped. Sort alone doesn't count as a filter.
  5. Pump row — 56 px status disc (green/amber/red by severity; icon by cause: droplet running, ⚠ fault, power-off, cloud-off dormant, remote/bypass variants for idle); "A" badge = Auto Mode; dimmed disc + wifi-off badge + red "Stale" when last-seen >30 min. Caption examples: "Running · Auto", "Fault · Reverse Phase", "Off · Power Down", "No contact". Elapsed line ("Running for 2h 8m") and relative time tick every 30 s. IMEI always masked to last 5 digits. JE logins also see "Operator: <name>".
  6. Long-press — quick start/stop dialog ("Turn on pump? / Turn off — <name>"); blocked with a snackbar when Auto Mode is on. Confirmation polls GET /pumps/:id/status at 1/2/3/5/8 s.

Filter sheet

Bottom sheet with drag handle, four ChoiceChip groups: Filter by status (All/On/Off/Fault), Filter by mode (All/Manual/Schedule/Auto → triggered_by=), Filter by Operator (JE only; server endpoint not implemented yet, so empty in practice), Sort by (Most recent / Status). "Clear all filters" appears when anything is active.

States

LoadingSpinnerCentered spinner in the list area; tiles show "—". No skeletons.
ErrorCould not load pumps.48 px cloud-off icon + Retry button.
EmptyNo matches"No pumps match your search or filters."
OfflineBanner"Last known status — connection lost. Updated 45s ago" on the M3 error container; last data stays on screen.
PagingInfinite scroll25/page, prefetch at 600 px; footer spinner, or "Could not load more. Retry" on failure.
RefreshSilentPull-to-refresh + a 30 s auto-refresh that only fires while scrolled to the top.

Enums (complete)

Run states: running · idle · bt_power_down → "Off · Power Down" · dormant_7d_plus → "No contact". Faults: Dry Run, Overload, Overcurrent, Overvoltage, Undervoltage, Reverse Phase, Phase Down, Phase Imbalance, Overheat, Mechanical — all amber. Modes: Manual, Schedule, Auto, Remote, Bypass (precedence bypass > remote > schedule > auto/manual). Live-data note: 88% of rows have no trigger and read "· Manual" by fallback; Schedule/Auto triggers have not been observed on staging.

Chapter 5

Pump detail

One pump's full state: status hero, live readings, mode control, schedule preview, phase health, and a pinned action bar.

Source: lib/features/pump_detail/

Tw 5
⚠ Overvoltage detected 10 min ago
Running
The pump is running normally.
▫ •••••35838
Scheduled Run
💧 Running now
84/240 min delivered
Status
Last UpdateJust now
Electricity AvailabilityAvailable
Pump StatusRunning
ModeManual
OperatorRavinder Singh
Live Readings
4.21 kW
Power
💧
11.80 L/s
Discharge
50.1 Hz
Frequency

Runtime2h 8m
Voltage Health
Per-phase reading against configured safe range
Phase R238.4 V
Min 180Max 260
Phase Y↗ 252.1 V
Min 180Max 260
Phase B--
No range configured
⚙ Settings 🕘 Running Times
⏻ Turn Off Pump
Pump detail — running with a recent fault banner and an active scheduled run

Anatomy (top to bottom)

  1. Stale banner (conditional) — "Last known status — connection lost. Updated 3 hr ago" when last-seen is unknown or >30 min.
  2. Recent alert banner (conditional) — "<Fault> detected <time> ago", amber.
  3. Status hero — tinted 20 px-radius banner. Titles: "Running" / "Fault · Dry Run" / "Off · Power Down" / "No contact" / "Off · Remote" etc. Each has a one-line reason, e.g. "Protection tripped — the pump stopped itself.", "Mains power is unavailable.", "Stopped from the panel bypass switch.", "Reason not reported."
  4. Scheduled Run card (self-hiding) — live occurrence with progress bar and "84/240 min delivered"; states: Running now / Starting soon / Paused due to power cut — will resume automatically / Resuming after power cut.
  5. Status card — Last Update, Electricity Availability, Pump Status, Mode with an inline Auto/Manual switch (confirmation dialog: "Switch to Auto Mode? — Auto Mode runs the pump on electricity availability and overrides manual control and schedules."), Operator (JE only).
  6. Live Readings — Power / Discharge / Frequency tiles (forced to 0.00 when not running; "—" when null) + Runtime.
  7. Schedule card — next-session preview with inline toggle, or "No upcoming schedules." + Add Schedule, or the Auto-Mode override notice, or "All schedules are paused for this pump."
  8. Voltage/Current Trend (self-hiding) — 48 px sparklines of today's phase averages from GET /reports/telemetry-log.
  9. Voltage/Current Health — per-phase bars against device-configured min/max: green in range, amber ↗ within 10% of a limit, red ⚠ out of range, "--" + "No range configured" when unset. Values are never fabricated.
  10. Pinned action bar — Settings + Running Times outlined buttons, then a full-width Turn On (green) / Turn Off (red) button. Confirmation includes the location: "Turn on — Tw 5 · Punjab > Sangrur > …". Disabled with "Manual control is overridden while Auto Mode is on." in Auto Mode.

Command flow

POST /commands/pump/:id/start|stop with an idempotency key → 202 + expected_ack_seconds (clamped 15–120 s, default 30). Button shows "Turning on…/off…" with a spinner; polls GET /pumps/:id/status at 1/2/3/5/8/12/18/25 s. Timeout copy is deliberately neutral: "Still working — pull down to refresh." Mode switches go through POST /commands/pump/:id/command with a SetMode CONFIGURE payload and confirm by refetching the pump (90 s cap).

LoadingSpinnerNo action bar until data exists.
ErrorCould not load pump.+ Retry. 404 (not 403) when out of scope.
DegradedPartialThreshold-config failure → "No range configured"; trend/occurrence failures hide their cards; the screen keeps its last value during refetch so the action bar never unmounts mid-command.

No background polling on this screen — refresh is pull-to-refresh or command-driven. Data: GET /pumps/:id then best-effort GET /device-config/devices/:deviceId/config/reported.

Chapter 6

Running times

Run-session history with range presets, a daily-runtime bar chart, a short-run anomaly flag, and CSV/Excel export.

Source: lib/features/running_times/

Running Times — Tw 5
Today ✓ Last 7 days Last 30 days Custom
14h 32m
Total Runtime
12
Sessions
1h 13m
Avg Session
⚠ 3 short sessions — possible fault cycling.
Daily Runtime
26/7
27/7
28/7
29/7
30/7
31/7
1/8
Session Log
01/08/2026 · 05:30 – 07:45
2h 15m
⟳ Schedule
31/07/2026 · 14:02 – 14:04
2m · possibly abnormal
👆 Manual
Running times — 7-day window with a short-run warning

Behavior

  • Ranges: Today / Last 7 days (default) / Last 30 days / Custom (date-range picker, max 31 days — "Choose a range of up to 31 days." on violation).
  • Stats: Total Runtime / Sessions / Avg Session from GET /reports/start-stop/summary; "—" placeholders while loading.
  • Anomaly flag: completed sessions shorter than 3 minutes are marked "possibly abnormal" (red icon/subtitle) and counted in the amber banner "N short sessions — possible fault cycling."
  • Chart: teal bars, one per day (label d/M) or one per session for single-day windows (label HH:mm); bar width and label density adapt to entry count; no tooltips.
  • Sessions: one page of up to 1,000 rows from GET /reports/start-stop; if more exist, footer reads "Showing the most recent runs only." Trigger pills: Manual, Schedule (teal), Auto (amber), Remote, Bypass; sessions still in progress read "Running".
  • Export: app-bar share menu (CSV / Excel) → POST /reports/generate, poll /reports/jobs/:id (~30 s cap), download, hand to the native share sheet. Copy: "Preparing export…", "Nothing to export.", "Could not export the log."
EmptyNo sessions in this period.No chart, no banner.
ErrorCould not load running times.+ Retry; stats stay as "—".

Reached only from Pump Detail's action bar. No auto-refresh timer; pull-to-refresh reloads.

Chapter 7

Schedules

Per-pump schedule list with a pause-all switch, and a bottom-sheet create/edit form. Capped at 16 slots (firmware limit).

Source: lib/features/schedules/

Schedules — Tw 5
Pause all schedules
Temporarily stop all schedules for this pump without deleting them.
Untitled scheduleActive
Every day · 6:00 AM – 6:20 AM
🗑
Untitled scheduleInactive
Mon–Fri · 6:00 PM – 7:00 PM
🗑
🛡 Safety protections always stop a scheduled run early — they take priority over any schedule.
+ Add new schedule
Schedules list
New Schedule
Schedule Name
Repeat Days
✓ Mon ✓ Tue ✓ Wed ✓ Thu ✓ Fri ✓ Sat ✓ Sun
Start Time
6:00 AM
Duration
20 min
End Time (optional)
Set end time
The pump stops at this time, even if the run has not finished.
Add Schedule
Create sheet — defaults: all days, 6:00 AM, 20 min

Behavior

  • Names are always "Untitled schedule" in real data — the backend never populates name.
  • Status pills: Paused (amber, when pause-all is on — wins over everything) · Active (green) · Inactive (amber). Row toggles disable while pause-all is on.
  • Pause all: confirmation only when pausing ("Pause all schedules? — All schedules for this pump stop running until you resume them."); resuming is immediate. Auto-Mode pumps show an amber banner: "This pump is in Auto Mode, so schedules are overridden and will not run until Auto Mode is turned off in Settings."
  • Validation (in order): "Pick at least one day." (inline, red, clears on tap) · "Set a start time." · "Enter a duration between 1 and 1440 minutes." · "End time must be after the start time." · "Duration must fit before the end time." Setting an end time triggers the hard-end confirmation: "Set an end time? — The pump will stop at this time even if the run has not finished. A run cut short is marked incomplete."
  • Delete: "Delete schedule? — This removes the schedule permanently." with a red Delete button; snackbar "Schedule deleted".
  • Cap: adding beyond 16 slots is blocked client-side: "You can add up to 16 schedules per pump."
  • Server errors render verbatim (by design — they name the conflicting schedule): overlap 400, Auto-Mode 409, read-only tenant 403.
  • API: GET/POST /pumps/:id/schedules, PUT/DELETE …/:slotIndex, POST …/pause; day bitmask Sun=bit 0 (127 all, 62 Mon–Fri, 65 weekends); stop_time is server-derived; every mutation returns the full slot list.
Chapter 8

Settings

Deliberately automation-only: an Auto Mode switch and two timing fields. Language and sign-out live in the drawer, not here.

Source: lib/features/settings/ · docs/prd-vs-design-discrepancies.md §6.4

Settings — Tw 5
Automation
How the pump behaves automatically
🕐 Schedules
Auto Mode
Runs the pump automatically based on electricity availability. Highest priority — overrides both manual control and any active schedules while on.

Auto Delay
Wait time before the pump restarts automatically after stopping.
30 sec
Auto Max Duration
Maximum time the pump is allowed to run continuously.
240 min
Save Settings
Settings — automation card only

Behavior

  • Scope was intentionally cut for v5: no protection thresholds (Vmin/Vmax, Imin/Imax), no send intervals, no HP calculator — roughly 80% of the originally designed screen. This spec documents the shipped scope.
  • Auto Mode always confirms in both directions ("Switch to Auto Mode?" / "Switch to Manual Mode?"), flips optimistically, and snackbars "Switching to Auto…/Manual…". Failure snaps back with "Something went wrong. Please try again."
  • Timing fields seed once from reported config and never clobber typing; blank Max Duration means 0 (no cap); invalid input → "Enter a valid number."; success → "Settings saved."
  • Firmware quirk that shapes the code: the device honours only the first command in a batched CONFIGURE envelope, so each setting is sent as its own POST /commands/pump/:id/command spaced 1 s apart — saving both fields takes ≥1 s by design.
  • Read: GET /device-config/devices/:deviceId/config/reported. No device attached → "This pump has no device to configure."
Chapter 9

Notifications & push

An FCM-driven inbox with category filters, a six-toggle preferences screen, and deep links into pump detail.

Source: lib/core/push/push_service.dart · lib/features/notifications/

Push behavior

  • Channels (IDs shared with the farmer app backend): critical_standard "Critical alerts" (heads-up + sound) and warning_silent "Warnings" (silent). The payload's tag: critical picks the channel.
  • Routing: a tapped notification with pump_id opens that pump's detail; anything else opens Notification History. Those are the only payload keys the client acts on — title/body arrive server-rendered in the recipient's language and are never re-translated.
  • Permission: if denied, one dialog per launch — "Turn on notifications — Notifications are off, so you won't get pump alerts. Turn them on for Wattr Operator." (Not now / Open Settings).
  • Registration: PUT /user-devices/devices with {fcm_token, fcm_platform}; iOS waits for the APNS token (up to 10 × 500 ms).

Notification History

Cursor-paginated inbox (GET /notifications?limit=30) with category filter chips (All / Pump / Fault / Schedule / Weather), severity dots (red / warning / info), unread styling, optimistic mark-read and mark-all-read, and grouped rows ("…N times today" via group_count). The bell badge comes from GET /notifications/unread-count and is invalidated on push arrival, read actions, and pull-to-refresh.

Notification Preferences

Six per-type toggles, all default ON, saved optimistically via PUT /notification-preferences (silent rollback on failure): Power Down · Overvoltage · Undervoltage · Overload · Dry Run · Reverse Phase. Everything else (theft, no-signal, weather, schedule) is mandatory and ignores these toggles.

Chapter 10

API reference

Every endpoint the operator app calls. Base: https://staging-api.wattr.ai/v1 (hard-coded), Bearer JWT, one auto-retry via POST /auth/refresh.

EndpointUsed byNotes
POST /auth/login · /auth/logout · /auth/refresh · GET /users/currentAuthLogin body {usernameOrEmail, password}
GET /pumpsDashboardpage/page_size=25, search, status=running|idle, fault=any, triggered_by, sort=status; retries once without sort/mode on 400
GET /pumps/status-summarySummary tilesScope from JWT; refreshed with the list
GET /pumps/:id + GET /device-config/devices/:id/config/reportedPump detail, SettingsConfig call is best-effort
POST /commands/pump/:id/start|stop · GET /pumps/:id/statusControl202 + expected_ack_seconds; Idempotency-Key header; confirm polls 1/2/3/5/8(/12/18/25) s
POST /commands/pump/:id/commandMode + auto timingsCONFIGURE envelope: SetMode / SetAutoDelay / SetAutoDuration — one command per request, 1 s apart
GET/POST /pumps/:id/schedules · PUT/DELETE …/:slotIndex · POST …/pause · GET /pumps/:id/occurrences/liveSchedulesMutations return the full slot list
GET /reports/start-stop + /summaryRunning times≤31-day window, one 1,000-row page
GET /reports/telemetry-logTrend sparklinesToday, first page only, data_tier=auto
POST /reports/generate · GET /reports/jobs/:id + /downloadExportCSV/XLSX via job polling, then native share sheet
GET/POST /notifications… · GET/PUT /notification-preferencesInboxCursor pagination; optimistic reads
PUT /user-devices/devicesPush token{fcm_token, fcm_platform}
GET /pumps/operatorsJE operator filterNot implemented server-side — client returns []

Shared client-side error copy: "Check your connection and try again." (no response) · "Something went wrong. Please try again." (anything else) · server error.message rendered verbatim when present.

Chapter 11

Known gaps & notes

ItemDetail
Base URLHard-coded to staging; no build-time override or environment toggle yet
ScopingPer-user pump scoping is not enforced server-side — operators currently see the whole tenant
Operator filterUI exists (JE), backend endpoint missing; filter also applies client-side after pagination, which can shorten a page
Mode fallback88% of live rows have no event_triggered_by and read "· Manual" by fallback; Schedule/Auto triggers unobserved on staging
Schedule namesBackend never populates name — every tile reads "Untitled schedule"
Dark-mode tintsStatus backgrounds (#E6F4EA/#FFF4DE/#FCE8E8) and the login error banner have no dark variants
Search clear buttonThe ✕ suffix only appears after an incidental rebuild (onChanged doesn't setState)
Fixture dataFixturePumpRepository (109 seeded pumps, "Tw 1–6") is test-only — not wired into the running app
Token storageSharedPreferences, not platform secure storage
Undesigned v5 itemsOperator name on detail, role-scoped notification history, JE list shape (docs/prd-vs-design-discrepancies.md §3, §6.1–6.3)