{
  "openapi": "3.1.0",
  "info": {
    "title": "Your Manipur API",
    "version": "1.0.0",
    "summary": "District-level public data for all 16 districts of Manipur, India.",
    "description": "Read-only JSON access to the same data the Your Manipur website renders: districts, live power-outage notices, crop prices, schemes, budgets, news, and per-source freshness.\n\n**No authentication.** There are no API keys, no OAuth flow, and no rate-limit headers. Requests are served from the same origin that serves the website.\n\n**No CORS restrictions.** Every endpoint answers `Access-Control-Allow-Origin: *` through the platform default, so browser-based agents can call them directly.\n\n**Provenance.** Every record carries the government portal or document it came from, or a link to the page that does. Where the site has not verified a figure, the corresponding collection is empty rather than estimated — an empty array means *not verified*, not *zero*.\n\nYour Manipur is an independent citizen project, NOT an official government website, and it is not affiliated with the Government of Manipur or the Government of India.",
    "contact": {
      "name": "Your Manipur",
      "email": "somgimanipur@gmail.com",
      "url": "https://www.yourmanipur.com/contact"
    },
    "license": {
      "name": "Public government data, republished under the National Data Sharing and Accessibility Policy (NDSAP)",
      "url": "https://www.yourmanipur.com/about"
    },
    "termsOfService": "https://www.yourmanipur.com/about"
  },
  "servers": [
    {
      "url": "https://www.yourmanipur.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Developer portal: quickstart, endpoint guide, and error reference",
    "url": "https://www.yourmanipur.com/developers"
  },
  "tags": [
    { "name": "Districts", "description": "The 16 districts of Manipur and the data held for each." },
    { "name": "Live data", "description": "Datasets that refresh on a schedule, most hourly." },
    { "name": "Status", "description": "How fresh each dataset is, and how much data the site holds." },
    { "name": "Feedback", "description": "Report a wrong figure, or vote on a feature request." }
  ],
  "paths": {
    "/api/districts": {
      "get": {
        "operationId": "listDistricts",
        "tags": ["Districts"],
        "summary": "List all 16 districts",
        "description": "Returns every district with a record count per dataset. Use this to discover the `slug` values that every other endpoint keys on.",
        "responses": {
          "200": {
            "description": "All districts, ordered by name.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/DistrictSummary" } },
                    "meta": {
                      "type": "object",
                      "required": ["total", "lastUpdated"],
                      "properties": {
                        "total": { "type": "integer" },
                        "lastUpdated": { "type": "string", "format": "date-time" }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/districts/{slug}": {
      "get": {
        "operationId": "getDistrict",
        "tags": ["Districts"],
        "summary": "Get one district with its data",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "District slug. The full list comes from `GET /api/districts`.",
            "schema": {
              "type": "string",
              "enum": [
                "bishnupur", "chandel", "churachandpur", "imphal-east", "imphal-west",
                "jiribam", "kakching", "kamjong", "kangpokpi", "noney",
                "pherzawl", "senapati", "tamenglong", "tengnoupal", "thoubal", "ukhrul"
              ]
            },
            "example": "imphal-west"
          }
        ],
        "responses": {
          "200": {
            "description": "One district, with the most recent rows of each dataset it holds.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/DistrictDetail" } }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Problem" },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/districts/{slug}/{dataset}": {
      "get": {
        "operationId": "getDistrictDataset",
        "tags": ["Districts"],
        "summary": "One dataset in full, paginated",
        "description": "Every row that district holds for one dataset, in pages. This is the endpoint for facts.\n\n**`GET /api/districts/{slug}` is not a substitute.** It carries a short preview of each dataset — 5 rows for schemes, 10 for crop prices — and its arrays are not the data. Reading that preview as the dataset is the easiest way to state a wrong number about this site.\n\n`pagination.total` is the true size of the dataset and `pagination.hasMore` says whether another page exists. Request `offset=pagination.nextOffset` until `hasMore` is false before describing a dataset as complete.\n\n**The `schemes` dataset is a mixed table.** It carries welfare schemes alongside power-outage notices, tenders, court pendency figures, helpline directories and data-source notes, discriminated by the `category` prefix. `?category=Central` filters to one prefix. `/llms.txt` says which prefixes are welfare schemes — the row total is not the number of schemes.",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "description": "District slug. The full list comes from `GET /api/districts`.", "schema": { "type": "string" }, "example": "imphal-west" },
          { "name": "dataset", "in": "path", "required": true, "description": "Which dataset to read.", "schema": { "type": "string", "enum": ["schemes", "crops", "news", "budgets", "leaders"] }, "example": "schemes" },
          { "name": "limit", "in": "query", "required": false, "description": "Rows per page, 1–500. Defaults to 100.", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } },
          { "name": "offset", "in": "query", "required": false, "description": "Rows to skip. Use `pagination.nextOffset` from the previous page.", "schema": { "type": "integer", "minimum": 0, "default": 0 } },
          { "name": "category", "in": "query", "required": false, "style": "form", "explode": true, "description": "Schemes only. Filters to rows whose `category` starts with any of the given prefixes. Repeat the parameter to pass more than one — the welfare schemes span six prefixes (`Central — `, `State — `, `Hill — `, `Valley — `, `Industry — Startup`, `Industry — Incubator`), and filtering to them in one request is what keeps a scheme count correct. Passing it with any other dataset is a 400.", "schema": { "type": "array", "items": { "type": "string" } }, "example": ["Central — ", "State — "] }
        ],
        "responses": {
          "200": {
            "description": "One page of one dataset, with the total behind it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": { "data": { "$ref": "#/components/schemas/DatasetPage" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem" },
          "404": { "$ref": "#/components/responses/Problem" },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/power-outages": {
      "get": {
        "operationId": "getPowerOutages",
        "tags": ["Live data"],
        "summary": "Current MSPDCL power outage notices",
        "description": "Notices published by the Manipur State Power Distribution Corporation, parsed from its official Facebook page and website and read as published — never rewritten. Times are IST. The site does not monitor the grid itself.",
        "parameters": [
          {
            "name": "includePast",
            "in": "query",
            "required": false,
            "description": "Set to `1` to include resolved notices as well as active ones. Omitted, only active notices are returned.",
            "schema": { "type": "string", "enum": ["1"] }
          }
        ],
        "responses": {
          "200": {
            "description": "Outage notices, newest first.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PowerOutagePayload" }
              }
            }
          },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/whats-changed": {
      "get": {
        "operationId": "getWhatsChanged",
        "tags": ["Live data"],
        "summary": "What changed most recently",
        "description": "A short feed of the most notable recent changes across datasets, already assembled into display cards with a severity.",
        "responses": {
          "200": {
            "description": "Change cards, most significant first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["cards", "generatedAt", "count"],
                  "properties": {
                    "cards": { "type": "array", "items": { "$ref": "#/components/schemas/ChangeCard" } },
                    "generatedAt": { "type": "string", "format": "date-time" },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/data-count": {
      "get": {
        "operationId": "getDataCount",
        "tags": ["Status"],
        "summary": "How many records the site holds",
        "responses": {
          "200": {
            "description": "Total records across all datasets, with the time of the most recent successful refresh. When the database cannot be read this endpoint answers 200 with `total: 0`, so treat a zero alongside a null `lastRefreshedAt` as “unavailable” rather than “empty”.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["total", "lastRefreshedAt"],
                  "properties": {
                    "total": { "type": "integer" },
                    "lastRefreshedAt": { "type": ["string", "null"], "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/status": {
      "get": {
        "operationId": "getStatus",
        "tags": ["Status"],
        "summary": "Freshness per data source",
        "description": "The latest refresh outcome for each data type. Use this to decide whether a dataset is fresh enough to rely on before quoting it.",
        "responses": {
          "200": {
            "description": "Per-dataset refresh status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["mostRecentRefresh", "refreshes", "totalRecords"],
                  "properties": {
                    "mostRecentRefresh": { "type": ["string", "null"], "format": "date-time" },
                    "refreshes": {
                      "type": "object",
                      "description": "Keyed by data type, e.g. `budget`, `schemes`, `power-outages`.",
                      "additionalProperties": {
                        "type": "object",
                        "required": ["status", "records", "at"],
                        "properties": {
                          "status": { "type": "string", "description": "Outcome of the last run, e.g. `success`." },
                          "records": { "type": "integer" },
                          "at": { "type": "string", "format": "date-time" }
                        }
                      }
                    },
                    "totalRecords": { "type": "integer" }
                  }
                }
              }
            }
          },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/feedback": {
      "get": {
        "operationId": "listFeatureRequests",
        "tags": ["Feedback"],
        "summary": "List feature requests with vote counts",
        "parameters": [
          {
            "name": "deviceId",
            "in": "query",
            "required": false,
            "description": "Optional anonymous device id. When supplied, the response also reports which of these requests that device has voted for.",
            "schema": { "type": "string", "maxLength": 64 }
          }
        ],
        "responses": {
          "200": {
            "description": "Feature requests, most voted first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["features", "myVotes"],
                  "properties": {
                    "features": { "type": "array", "items": { "$ref": "#/components/schemas/FeatureRequest" } },
                    "myVotes": { "type": "array", "items": { "type": "integer" } }
                  }
                }
              }
            }
          },
          "503": { "$ref": "#/components/responses/Problem" }
        }
      },
      "post": {
        "operationId": "submitFeedback",
        "tags": ["Feedback"],
        "summary": "Report a problem or suggest a feature",
        "description": "Writes one feedback row. Submissions are capped at 5 per device id per 24 hours. A hidden `website` field acts as a honeypot: filling it returns a success response and stores nothing, so clients must leave it empty. There is no API key, but please only submit on behalf of a person.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/FeedbackSubmission" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. The response also carries the new row id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "id": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem" },
          "429": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/api/feedback/vote": {
      "post": {
        "operationId": "voteFeatureRequest",
        "tags": ["Feedback"],
        "summary": "Toggle a vote on a feature request",
        "description": "Voting is a toggle: posting the same device id and feedback id again removes the vote. One vote per device, enforced by the pair.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["feedbackId", "deviceId"],
                "properties": {
                  "feedbackId": { "type": "integer", "description": "Id from `GET /api/feedback`." },
                  "deviceId": { "type": "string", "minLength": 8, "maxLength": 64, "pattern": "^[A-Za-z0-9-]+$" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The feature's new vote count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["votes", "mine"],
                  "properties": {
                    "votes": { "type": "integer" },
                    "mine": { "type": "boolean", "description": "Whether this device now counts as having voted." }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem" },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    }
  },
  "components": {
    "responses": {
      "Problem": {
        "description": "An error, as RFC 9457 problem details. Always `application/problem+json`, never HTML — including for paths that do not exist under `/api`, so a client can branch on `code` without parsing prose.",
        "content": {
          "application/problem+json": {
            "schema": { "$ref": "#/components/schemas/Problem" }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details, plus two extension members.",
        "required": ["type", "title", "status", "detail", "code", "resolution"],
        "properties": {
          "type": { "type": "string", "format": "uri", "description": "A URI identifying the problem type, ending in the `code`." },
          "title": { "type": "string", "description": "Short human-readable summary of the status." },
          "status": { "type": "integer", "description": "The HTTP status code." },
          "detail": { "type": "string", "description": "What went wrong, in one sentence." },
          "instance": { "type": "string", "description": "The request path the error applies to." },
          "code": { "type": "string", "description": "Stable machine-readable identifier. Branch on this, not on `title` or `detail`." },
          "resolution": { "type": "string", "description": "What the caller should do next." }
        }
      },
      "DistrictSummary": {
        "type": "object",
        "required": ["id", "slug", "name", "nameLocal", "headquarters", "division", "description", "createdAt", "updatedAt", "_count"],
        "properties": {
          "id": { "type": "integer" },
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "nameLocal": { "type": "string", "description": "District name in Meitei Mayek." },
          "headquarters": { "type": "string" },
          "division": { "type": "string" },
          "description": { "type": "string" },
          "population": { "type": ["integer", "null"] },
          "area": { "type": ["number", "null"], "description": "Square kilometres." },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "_count": { "$ref": "#/components/schemas/DistrictCounts" }
        }
      },
      "DistrictCounts": {
        "type": "object",
        "required": ["cropPrices", "schemes", "news", "budgets", "leaders"],
        "properties": {
          "cropPrices": { "type": "integer" },
          "schemes": { "type": "integer", "description": "Every row in the schemes table for this district — welfare schemes, outage notices, tenders, court figures and directories together. It is not the number of welfare schemes." },
          "news": { "type": "integer" },
          "budgets": { "type": "integer" },
          "leaders": { "type": "integer" }
        }
      },
      "DatasetSummary": {
        "type": "object",
        "description": "What a preview array on `DistrictDetail` actually contains.",
        "required": ["showing", "total", "complete", "fullDataset"],
        "properties": {
          "showing": { "type": "integer", "description": "Rows present in the array on this response." },
          "total": { "type": "integer", "description": "Rows the district actually holds for this dataset." },
          "complete": { "type": "boolean", "description": "True only when the array happens to contain every row. False means the array is a sample." },
          "fullDataset": { "type": "string", "description": "Path that returns the dataset in full, paginated." }
        }
      },
      "DistrictPreviews": {
        "type": "object",
        "description": "Present on every `DistrictDetail` response. Exists so the preview arrays cannot be mistaken for the datasets: it states the cap and the total beside each one.",
        "required": ["cropPrices", "schemes", "news", "budgets", "leaders"],
        "properties": {
          "cropPrices": { "$ref": "#/components/schemas/DatasetSummary" },
          "schemes": { "$ref": "#/components/schemas/DatasetSummary" },
          "news": { "$ref": "#/components/schemas/DatasetSummary" },
          "budgets": { "$ref": "#/components/schemas/DatasetSummary" },
          "leaders": { "$ref": "#/components/schemas/DatasetSummary" }
        }
      },
      "DistrictDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/DistrictSummary" },
          {
            "type": "object",
            "required": ["_count", "_previews", "cropPrices", "schemes", "news", "budgets", "leaders"],
            "properties": {
              "_count": { "$ref": "#/components/schemas/DistrictCounts" },
              "_previews": { "$ref": "#/components/schemas/DistrictPreviews" },
              "cropPrices": { "type": "array", "description": "Preview only — see `_previews.cropPrices`.", "items": { "$ref": "#/components/schemas/CropPrice" } },
              "schemes": { "type": "array", "description": "Preview only, and not a representative one: these are the most recently updated rows, which for schemes means outage notices and tenders. See `_previews.schemes` and use `/api/districts/{slug}/schemes`.", "items": { "$ref": "#/components/schemas/Scheme" } },
              "news": { "type": "array", "description": "Preview only — see `_previews.news`.", "items": { "$ref": "#/components/schemas/NewsItem" } },
              "budgets": { "type": "array", "description": "Preview only — see `_previews.budgets`.", "items": { "$ref": "#/components/schemas/BudgetItem" } },
              "leaders": { "type": "array", "description": "Preview only — see `_previews.leaders`.", "items": { "$ref": "#/components/schemas/Leader" } }
            }
          }
        ]
      },
      "Pagination": {
        "type": "object",
        "required": ["total", "limit", "offset", "returned", "hasMore", "nextOffset"],
        "properties": {
          "total": { "type": "integer", "description": "The true size of the dataset. This, not the array length, is the count to report." },
          "limit": { "type": "integer" },
          "offset": { "type": "integer" },
          "returned": { "type": "integer" },
          "hasMore": { "type": "boolean", "description": "False only once every row has been read." },
          "nextOffset": { "type": ["integer", "null"], "description": "Pass this as `offset` to read the next page, or null on the last page." }
        }
      },
      "DatasetPage": {
        "type": "object",
        "required": ["district", "dataset", "items", "pagination"],
        "properties": {
          "district": {
            "type": "object",
            "required": ["slug", "name"],
            "properties": {
              "slug": { "type": "string" },
              "name": { "type": "string" },
              "nameLocal": { "type": "string" },
              "division": { "type": "string" }
            }
          },
          "dataset": { "type": "string", "enum": ["schemes", "crops", "news", "budgets", "leaders"] },
          "category": { "type": ["array", "null"], "items": { "type": "string" }, "description": "The category prefixes applied to this read, or null when none were." },
          "items": {
            "type": "array",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/Scheme" },
                { "$ref": "#/components/schemas/CropPrice" },
                { "$ref": "#/components/schemas/NewsItem" },
                { "$ref": "#/components/schemas/BudgetItem" },
                { "$ref": "#/components/schemas/Leader" }
              ]
            }
          },
          "pagination": { "$ref": "#/components/schemas/Pagination" }
        }
      },
      "CropPrice": {
        "type": "object",
        "description": "One commodity at one market on one date, as published by Agmarknet.",
        "required": ["id", "districtId", "commodity", "market", "minPrice", "maxPrice", "modalPrice", "unit", "date", "fetchedAt"],
        "properties": {
          "id": { "type": "integer" },
          "districtId": { "type": "integer" },
          "commodity": { "type": "string" },
          "variety": { "type": ["string", "null"] },
          "market": { "type": "string" },
          "minPrice": { "type": "number", "description": "Rupees per quintal." },
          "maxPrice": { "type": "number" },
          "modalPrice": { "type": "number" },
          "unit": { "type": "string" },
          "date": { "type": "string", "format": "date-time" },
          "source": { "type": ["string", "null"] },
          "fetchedAt": { "type": "string", "format": "date-time" }
        }
      },
      "Scheme": {
        "type": "object",
        "description": "A welfare scheme available to residents of this district.",
        "required": ["id", "districtId", "name", "department", "category", "description", "status", "lastUpdated", "fetchedAt"],
        "properties": {
          "id": { "type": "integer" },
          "districtId": { "type": "integer" },
          "name": { "type": "string" },
          "department": { "type": "string" },
          "category": { "type": "string" },
          "description": { "type": "string" },
          "eligibility": { "type": ["string", "null"] },
          "benefits": { "type": ["string", "null"] },
          "status": { "type": "string" },
          "sourceUrl": { "type": ["string", "null"], "format": "uri", "description": "Where to apply, or the page the scheme was read from." },
          "lastUpdated": { "type": "string", "format": "date-time" },
          "fetchedAt": { "type": "string", "format": "date-time" }
        }
      },
      "NewsItem": {
        "type": "object",
        "required": ["id", "districtId", "title", "url", "source", "publishedAt", "fetchedAt"],
        "properties": {
          "id": { "type": "integer" },
          "districtId": { "type": "integer" },
          "title": { "type": "string" },
          "snippet": { "type": ["string", "null"] },
          "url": { "type": "string", "format": "uri" },
          "source": { "type": "string" },
          "category": { "type": ["string", "null"] },
          "imageUrl": { "type": ["string", "null"] },
          "publishedAt": { "type": "string", "format": "date-time" },
          "fetchedAt": { "type": "string", "format": "date-time" }
        }
      },
      "BudgetItem": {
        "type": "object",
        "description": "A state budget line. Manipur's budget is appropriated by department, not by district, so the same lines appear under every district — treat this as state-level data.",
        "required": ["id", "districtId", "department", "sector", "allocation", "year", "fetchedAt"],
        "properties": {
          "id": { "type": "integer" },
          "districtId": { "type": "integer" },
          "department": { "type": "string" },
          "sector": { "type": "string" },
          "allocation": { "type": "number", "description": "Crore rupees." },
          "year": { "type": "integer" },
          "description": { "type": ["string", "null"] },
          "sourceUrl": { "type": ["string", "null"], "format": "uri" },
          "fetchedAt": { "type": "string", "format": "date-time" }
        }
      },
      "Leader": {
        "type": "object",
        "description": "An elected representative or district official, from official government directories.",
        "required": ["id", "districtId", "name", "position", "fetchedAt"],
        "properties": {
          "id": { "type": "integer" },
          "districtId": { "type": "integer" },
          "name": { "type": "string" },
          "position": { "type": "string" },
          "constituency": { "type": ["string", "null"] },
          "party": { "type": ["string", "null"] },
          "contactOffice": { "type": ["string", "null"] },
          "contactPhone": { "type": ["string", "null"] },
          "contactEmail": { "type": ["string", "null"] },
          "sourceUrl": { "type": ["string", "null"], "format": "uri" },
          "fetchedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PowerOutagePayload": {
        "type": "object",
        "required": ["generatedAt", "lastScrapedAt", "notices"],
        "properties": {
          "generatedAt": { "type": "string", "format": "date-time", "description": "When this response was assembled." },
          "lastScrapedAt": { "type": ["string", "null"], "format": "date-time", "description": "Last successful scrape of the MSPDCL sources." },
          "notices": { "type": "array", "items": { "$ref": "#/components/schemas/OutageNotice" } }
        }
      },
      "OutageNotice": {
        "type": "object",
        "required": ["id", "source", "sourceLabel", "sourceUrl", "kind", "title", "areas", "districts", "lastUpdated"],
        "properties": {
          "id": { "type": "string" },
          "source": { "type": "string", "enum": ["mspdcl-website", "mspdcl-facebook"], "description": "Which MSPDCL channel the notice came from." },
          "sourceLabel": { "type": "string" },
          "sourceUrl": { "type": "string", "format": "uri" },
          "kind": { "type": "string", "enum": ["planned", "unplanned"] },
          "feeder": { "type": ["string", "null"] },
          "title": { "type": "string" },
          "dateISO": { "type": ["string", "null"] },
          "dateRaw": { "type": ["string", "null"], "description": "The date exactly as published." },
          "startMinutes": { "type": ["integer", "null"], "description": "Minutes from midnight, IST." },
          "endMinutes": { "type": ["integer", "null"] },
          "timeRaw": { "type": ["string", "null"] },
          "timeLabel": { "type": ["string", "null"] },
          "cause": { "type": ["string", "null"] },
          "duration": { "type": ["string", "null"] },
          "subDivision": { "type": ["string", "null"] },
          "areas": { "type": "array", "items": { "type": "string" } },
          "districts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["slug", "name"],
              "properties": { "slug": { "type": "string" }, "name": { "type": "string" } }
            }
          },
          "postedLabel": { "type": ["string", "null"] },
          "lastUpdated": { "type": "string", "format": "date-time" }
        }
      },
      "ChangeCard": {
        "type": "object",
        "required": ["id", "title", "description", "icon", "action", "href", "sourceType", "severity"],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "description": { "type": "string" },
          "icon": { "type": "string" },
          "action": { "type": "string" },
          "href": { "type": "string" },
          "sourceType": { "type": "string" },
          "severity": { "type": "string", "enum": ["critical", "warning", "info"] },
          "hazardGroups": {
            "type": "array",
            "description": "Present on hazard alerts: each hazard with the districts under it.",
            "items": {
              "type": "object",
              "required": ["hazard", "districts"],
              "properties": {
                "hazard": { "type": "string" },
                "districts": { "type": "array", "items": { "type": "string" } }
              }
            }
          }
        }
      },
      "FeatureRequest": {
        "type": "object",
        "required": ["id", "title", "body", "votes", "createdAt"],
        "properties": {
          "id": { "type": "integer" },
          "title": { "type": "string" },
          "body": { "type": "string" },
          "votes": { "type": "integer" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "FeedbackSubmission": {
        "type": "object",
        "required": ["kind", "body", "deviceId"],
        "properties": {
          "kind": { "type": "string", "enum": ["issue", "feature"], "description": "`issue` reports something wrong; `feature` suggests something new." },
          "title": { "type": "string", "minLength": 4, "maxLength": 140, "description": "Required when `kind` is `feature`." },
          "body": { "type": "string", "minLength": 10, "maxLength": 4000 },
          "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Optional. Only used to follow up on this one report; never published." },
          "pageUrl": { "type": "string", "maxLength": 300, "description": "Optional path, starting with `/`, of the page the report is about." },
          "deviceId": { "type": "string", "minLength": 8, "maxLength": 64, "pattern": "^[A-Za-z0-9-]+$", "description": "Anonymous per-device id, used only to cap submissions." },
          "website": { "type": "string", "description": "Honeypot. Must be left empty — submissions that fill it are discarded while still reporting success." }
        }
      }
    }
  }
}
