Agri Field Monitor

Backend for monitoring Alternate Wetting and Drying (AWD) rice irrigation: fields, pipes, water-level readings, alerts and weekly reports behind a GraphQL API, with REST endpoints for token issuance and visit photo uploads.

This page is a static schema reference exported with strawberry export-schema on every push to main; there is no backend behind it. Open the Codespace for a running GraphiQL at /graphql/ with seeded demo data (login agent / agent1234), or deploy your own copy to Render.

REST endpoints

Everything else is GraphQL at POST /graphql/. Operations other than login need Authorization: Bearer <jwt>.

MethodPathAuthDescription
GET/health/noneLiveness probe: {status, database}
POST/api/v1/auth/token/none{username, password} to {token, token_type, expires_in}
POST/api/v1/visits/{id}/photos/BearerMultipart image (+ caption); jpg, jpeg, png, webp; 5 MB default
GET/api/v1/visits/{id}/photos/BearerPhotos of a visit with absolute URLs
GET/api/v1/reports/weekly.csv ยท .xlsxBearerWeekly report as a file; week_start, group_by, upazila, union_name
POST/graphql/BearerGraphQL endpoint; GraphiQL IDE on GET when GRAPHIQL=true
GET/admin/staff sessionDjango admin

GraphQL schema (SDL)

Download schema.graphql
type AlertPage {
  items: [AlertType!]!
  total: Int!
  offset: Int!
  limit: Int!
}

type AlertType {
  id: ID!
  field: FieldType!
  kind: String!
  severity: String!
  message: String!
  value: Decimal
  triggeredAt: DateTime!
  resolvedAt: DateTime
  resolvedBy: UserType
  resolutionNote: String!
  isOpen: Boolean!
}

type AreaType {
  m2: Float!
  hectare: Float!
  acre: Float!
  bigha: Float!
  decimal: Float!
}

type AuthPayload {
  token: String!
  tokenType: String!
  expiresInSeconds: Int!
  user: UserType!
}

type AwdCheckResultType {
  fieldsChecked: Int!
  opened: Int!
  resolved: Int!
  unchanged: Int!
}

"""Date (isoformat)"""
scalar Date

"""Date with time (isoformat)"""
scalar DateTime

"""Decimal (fixed-point)"""
scalar Decimal

input FarmerInput {
  name: String!
  phone: String!
  village: String!
  unionName: String!
  upazila: String!
  district: String!
  nid: String = null
}

type FarmerPage {
  items: [FarmerType!]!
  total: Int!
  offset: Int!
  limit: Int!
}

type FarmerType {
  id: ID!
  name: String!
  phone: String!
  nid: String!
  village: String!
  unionName: String!
  upazila: String!
  district: String!
  createdAt: DateTime!
  fields: [FieldType!]!
  totalAreaM2: Decimal!
}

input FieldInput {
  farmerId: ID!
  name: String!

  """List of [lat, lng] pairs"""
  polygon: JSON!
  crop: String! = "boro"
  awdThresholdCm: Int = null
}

type FieldPage {
  items: [FieldType!]!
  total: Int!
  offset: Int!
  limit: Int!
}

type FieldType {
  id: ID!
  farmer: FarmerType!
  name: String!
  crop: String!
  polygon: JSON!
  areaM2: Decimal!
  centroidLat: Float!
  centroidLng: Float!
  unionName: String!
  upazila: String!
  district: String!
  awdThresholdCm: Int!
  isActive: Boolean!
  createdAt: DateTime!
  pipes: [PipeType!]!
  seasons: [SeasonType!]!
  area: AreaType!
  perimeterM: Float!
  activeSeason: SeasonType
  latestReading: ReadingType
  openAlerts: [AlertType!]!
  lastIrrigation: IrrigationEventType
  readings(limit: Int! = 50): [ReadingType!]!
}

type IrrigationEventType {
  id: ID!
  field: FieldType!
  startedAt: DateTime!
  endedAt: DateTime
  method: String!
  waterMm: Decimal
  note: String!
  loggedBy: UserType
  durationMinutes: Int
}

input IrrigationInput {
  fieldId: ID!
  startedAt: DateTime = null
  endedAt: DateTime = null
  method: String! = "pump"
  waterMm: Decimal = null
  note: String! = ""
}

"""
The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf).
"""
scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf")

type LocationType {
  district: String!
  upazila: String!
  unionName: String!
  fields: Int!
}

type Mutation {
  """Exchange username/password for a bearer JWT"""
  login(username: String!, password: String!): AuthPayload!
  createFarmer(input: FarmerInput!): FarmerType!
  createField(input: FieldInput!): FieldType!
  addReading(input: ReadingInput!): ReadingType!
  logIrrigation(input: IrrigationInput!): IrrigationEventType!
  createVisit(input: VisitInput!): VisitType!
  resolveAlert(id: ID!, note: String! = ""): AlertType!

  """
  Evaluate AWD rules for every active field now (also run by the scheduler)
  """
  runAwdCheck: AwdCheckResultType!
}

type PipeType {
  id: ID!
  field: FieldType!
  code: String!
  depthCm: Int!
  installedOn: Date!
  deviceId: String!
  isActive: Boolean!
  hasDevice: Boolean!
  latestReading: ReadingType
}

type Query {
  me: UserType!
  farmers(search: String = null, district: String = null, upazila: String = null, unionName: String = null, offset: Int! = 0, limit: Int! = 20): FarmerPage!
  farmer(id: ID!): FarmerType
  fields(farmerId: ID = null, district: String = null, upazila: String = null, unionName: String = null, crop: String = null, withOpenAlerts: Boolean = null, includeInactive: Boolean! = false, offset: Int! = 0, limit: Int! = 20): FieldPage!
  field(id: ID!): FieldType
  readings(fieldId: ID = null, pipeId: ID = null, since: DateTime = null, until: DateTime = null, readingSource: String = null, offset: Int! = 0, limit: Int! = 50): ReadingPage!
  alerts(fieldId: ID = null, kind: String = null, severity: String = null, upazila: String = null, openOnly: Boolean! = true, offset: Int! = 0, limit: Int! = 50): AlertPage!
  alert(id: ID!): AlertType
  locations: [LocationType!]!

  """
  Weekly AWD progress. group_by: field | union | upazila. Defaults to last full week.
  """
  weeklyReport(weekStart: Date = null, groupBy: String! = "field", upazila: String = null, unionName: String = null): [ReportRowType!]!
}

input ReadingInput {
  levelCm: Decimal!
  pipeId: ID = null
  pipeCode: String = null
  recordedAt: DateTime = null
  source: String! = "manual"
  note: String! = ""
}

type ReadingPage {
  items: [ReadingType!]!
  total: Int!
  offset: Int!
  limit: Int!
}

type ReadingType {
  id: ID!
  pipe: PipeType!
  levelCm: Decimal!
  source: String!
  recordedAt: DateTime!
  recordedBy: UserType
  note: String!

  """True when the level is at or below the field's AWD threshold"""
  belowThreshold: Boolean!
}

type ReportRowType {
  groupBy: String!
  key: String!
  label: String!
  weekStart: Date!
  weekEnd: Date!
  fields: Int!
  areaHa: Float!
  readings: Int!
  avgLevelCm: Float
  minLevelCm: Float
  fieldDaysBelowThreshold: Int!
  irrigationEvents: Int!
  irrigationPerField: Float!
  estimatedWaterSavingPct: Float!
  visits: Int!
  alertsOpened: Int!
  alertsOpenNow: Int!
  fieldIds: [Int!]!
}

type SeasonType {
  id: ID!
  crop: String!
  variety: String!
  transplantedOn: Date!
  awdStartOn: Date!
  awdEndOn: Date
  expectedHarvestOn: Date
  isActive: Boolean!
}

type UserType {
  id: ID!
  username: String!
  firstName: String!
  lastName: String!
  isStaff: Boolean!
}

input VisitInput {
  fieldId: ID!
  visitedAt: DateTime = null
  cropStage: String! = ""
  notes: String! = ""
  lat: Float = null
  lng: Float = null
}

type VisitPhotoType {
  id: ID!
  caption: String!
  uploadedAt: DateTime!
  url: String!
}

type VisitType {
  id: ID!
  field: FieldType!
  visitor: UserType
  visitedAt: DateTime!
  cropStage: String!
  notes: String!
  lat: Float
  lng: Float
  photos: [VisitPhotoType!]!
}