Tubig Project: A Grounded Water and Weather Risk Tracker for Metro Manila
Data collection, reservoir prediction and the limited role of AI in a public-information tool
Metro Manila's water supply depends on one reservoir system, and the information a household needs to plan around it is spread across agency tables, utility notices, climate bulletins and weather models. The Tubig Project is a small system that collects these public sources on a polite schedule, computes a deterministic picture of each city's situation, and presents it on a phone. The core is a reservoir model for Angat Dam that links daily water level changes to an antecedent rainfall index over the watershed. Its parameters are calibrated by replaying the season's own readings, and it runs dry, median and wet rain scenarios from ensemble forecasts over a 60-day horizon. The model reports its own confidence and withholds dated predictions when that confidence is low. Risk levels for water supply, flooding, typhoons and heat come from rules kept as data. The flood rating draws on measured river levels and rain gauges from PAGASA's flood forecasting system for the Pasig, Marikina and Tullahan rivers, on PAGASA's river basin status and on its public flood advisories, each checked before it may move a rating. The water rating also follows PAGASA's provincial drought outlook. A language model is used for one task only: writing a plain-language briefing from the computed situation, including a one-line answer to whether a reader should be concerned today. Every number in that briefing is checked against its input before it is shown, and the system falls back to a rules-written briefing when the check fails or the monthly budget runs out. The AI can also be switched off entirely, and the page then loses only the wording of the briefing. This paper describes the sources, the crawler, the prediction method, the AI layer and the safeguards around them, and reports measured results from the first deployment.
Keywords reservoir forecasting, antecedent precipitation index, Angat Dam, ENSO, flood monitoring, drought outlook, web crawling, grounded text generation, Philippines
1Introduction
1.1Background
Most of Metro Manila's raw water comes from Angat Dam in Bulacan. Water released from Angat passes Ipo Dam, which diverts it toward the La Mesa reservoir and the treatment plants of the two concessionaires, Maynilad in the west zone and Manila Water in the east zone. When Angat falls, the effects reach households in a predictable order. Allocations are cut first, then pressure drops at night, and eventually rotational interruptions are scheduled barangay by barangay.
The record shows how often this happens. Angat reached 157.56 m in July 2010 during a strong El Niño, and households queued at water tankers for about a month. In 2019 it fell below 160 m and Maynilad ran rotational interruptions that included parts of Caloocan. In July 2023 it dropped to 179.99 m, just under the 180 m minimum operating level, and about 591,000 Maynilad customers had interruptions of around nine hours. In 2026 the reservoir fell to 150.42 m on 25 July before heavy August rain refilled it past 208 m by the end of September [20].
1.2The problem
The facts needed to anticipate these events are public, but they are scattered. PAGASA publishes a daily dam table [1], NOAA publishes the ENSO indices [7], PAGASA issues El Niño advisories [3] and typhoon bulletins [2], and Maynilad posts interruption notices for specific streets [5]. Weather models provide rainfall forecasts for the watershed. None of these says what a particular barangay should expect over the next two months, and none explains how they relate. A level of 208 m means little without knowing how fast the dam usually falls once the rains stop, or how much rain the coming weeks are likely to bring.
1.3Objectives and scope
Tubig aims to give a household in Metro Manila a short, honest answer to three questions. Is there anything to do today? What do this week and the next one to two months look like? How sure is that, and where does it come from? The first release covers the 17 local government units of the National Capital Region with detailed profiles, and any other city or municipality in the Philippines with a generic profile that uses weather and rainfall only. The reservoir outlook covers 60 days, with a low-confidence extension to April 2027.
Tubig is not an official warning service. It repeats public information and computes from it, and it tells users to follow PAGASA, NDRRMC, their local government and their water utility.
1.4Design commitments
- Numbers come from code. Every level, date, percentage and risk rating is computed deterministically from stored data. The AI layer explains these results and cannot change them.
- Missing or old data is never shown as calm. Each block carries a data status, and a stale input raises a visible flag instead of quietly lowering a risk.
- Collection is polite. The crawler identifies itself, spaces its requests, retries with backoff and never tries to get around bot protection.
- Location stays private. A user's exact position is used once to find their barangay and is never stored or logged.
- Cost is capped. AI spending has a hard monthly limit, and the site keeps working without the AI.
2System overview
Tubig has three parts. A static web application runs on Cloudflare Pages. An API and a background worker run in Docker on a Raspberry Pi 4B and share an SQLite database on a USB SSD. The Pi is reached through a Cloudflare Tunnel. On the first deployment, Nginx Proxy Manager on the Pi forwards the API hostname to the API container. Figure 1 shows how they connect.
Work happens in a loop that the worker repeats through the day.
- ScheduleA cron-like scheduler in Manila time queues a crawl job for each source.
- CrawlThe job fetches the page or API, keeps a compressed snapshot, parses it and stores the result.
- AssessTwo minutes after any crawl that stored data, and every hour, each active city's situation is recomputed.
- NarrateFor cities someone opened in the last 24 hours, a briefing is written if the old one is missing, from an earlier day, or out of date.
- ServeThe API returns the stored situation, tailored to the user's barangay at request time.
3Data sources
Table 1 lists every source Tubig reads, what it contributes and how it is reached. All are public. Where an agency offers an API, Tubig uses it. Where it offers only a web page, Tubig parses the page and keeps a copy of what it saw.
| Source | What Tubig takes from it | Access | Used for |
|---|---|---|---|
| PAGASA dam information [1] | Water levels, rule curves and thresholds for Angat, Ipo, La Mesa and other dams, as read at 08:00 | HTML table | Reservoir levels, trends and the projection |
| PAGASA tropical cyclone bulletins [2] | Whether a cyclone is inside the Philippine Area of Responsibility, its name and latest bulletin | HTML page | Typhoon risk, flood risk |
| PAGASA El Niño and La Niña advisories [3] | Latest advisory, its date, title and stated probability | HTML page | ENSO context in the briefing |
| PAGASA river basin flood status [21] | Status of 18 major river basins and 4 dam sub-basins, and the daily bulletin for the NCR basin: observed and forecast rain, forecast water level | HTML pages | Flood risk, the briefing |
| PAGASA Pasig-Marikina-Tullahan flood forecasting system [22] | Water level at 17 river stations with PAGASA's alert, alarm and critical levels, and rain at 25 gauges, every 10 minutes | JSON lists | Flood risk, measured rain, river alerts |
| PAGASA public alerts [23] | General Flood Advisories: severity, expiry, the areas covered and the rivers named for each province. Tropical Cyclone Alerts: the storm's centre, winds, heading and land outlook | Atom feed of CAP 1.2 files (CC BY 4.0) | Flood risk, advisory alerts, typhoon rating and panel |
| PAGASA drought monitor [24] | Each province's monthly drought assessment and six-month outlook | JSON | Water risk, the briefing |
| Maynilad service advisories [5] | Emergency, scheduled and rotational interruptions: city, barangays, street, time window and reason | WordPress JSON API | Interruptions and "store water" actions |
| MWSS water elevation page [6] | Nothing automatically. The page sits behind a Cloudflare challenge. | Weekly probe only | Alerts if it becomes reachable; readings can be entered by hand |
| NOAA CPC Oceanic Niño Index [7] | Three-month running ONI values since 1950 | Plain text file | ENSO phase, strength and trend; analog years |
| NOAA CPC weekly SST indices [8] | Weekly Niño 3.4 sea surface temperature anomaly | Fixed-width text | The most recent ENSO signal |
| Open-Meteo forecast API [9] | Current conditions and hourly and daily forecasts at each city's centroid, and for the weather zone of each chosen Metro Manila barangay | JSON API | Heat index, rain intensity, the sky shown on screen |
| Open-Meteo ensemble API [10] | 16-day ensemble rainfall at four points over the Angat watershed | JSON API | Short-range rain scenarios |
| Open-Meteo seasonal API (ECMWF SEAS5) [11] | Seasonal ensemble rainfall for the same points, about six months ahead | JSON API | Long-range rain scenarios |
| NASA POWER daily point API [12] | Daily precipitation from 1991 to the present for each city and the watershed | JSON API | Observed rain, the 1991 to 2020 baseline, calibration |
| PSGC 2023 boundaries [13] [14] | Polygons for regions, provinces, cities, municipalities and NCR barangays | Bundled files | Turning a position into a city and barangay |
| Seed dataset [20] | Thresholds, 2026 readings and historical events from news reports and agency pages, compiled on 1 October 2026. Its drought outlook ran out in August and is now replaced by the drought monitor | Bundled file | Analog outcomes, early history, thresholds |
Two kinds of input are deliberately left out. Tubig reads no social media and no news feeds, so the briefing can only mention what the structured sources contain. And it does not get past the challenge on the MWSS site. When MWSS numbers matter, an operator enters them through an authenticated admin form, and they are stored with the source marked as manual. The same rule applies to PAGASA's own flood advisory map on its flood page. It loads the advisories through a signed API that hands out a one-time key, so Tubig reads the same advisories from PAGASA's open alert feed instead.
Each city has a profile kept as configuration. It records the water utility, which advisory feeds apply, which reservoirs supply it, and which flood watches apply. For example, Caloocan, Malabon, Navotas and Valenzuela are on the Tullahan River below La Mesa, so a La Mesa spill raises their flood risk. Profiles also name the river basin a city belongs to, and a separate, hand-checked station map lists which river gauges inform which cities.
4How the crawler works
4.1Scheduling and the job queue
The worker runs a scheduler on Manila time. The scheduler does not fetch anything itself. It puts a crawl job on a queue stored in the database. The worker takes jobs in two lanes, each one job at a time. The visitor lane takes the jobs a visitor is waiting on, a new city's first weather and assessment and a newly chosen barangay's weather, which take seconds. The background lane takes everything else, including crawls that take minutes, so a slow crawl never delays a visitor. Each job has a de-duplication key, so a source can never be queued twice. A job left half done by a crash or power cut is reclaimed after a 30-minute lease. When the Pi starts after being off, every source whose last attempt is older than its catch-up window is queued immediately. Table 2 gives the timetable.
| Source | When | Catch-up after | Why this timing |
|---|---|---|---|
| PAGASA dams | 09:00, 10:00, 12:00, 16:00 | 12 h | The 08:00 readings appear mid-morning; later slots skip once the day is stored |
| PAGASA river basins | Hourly at :25 | 2 h | A basin can go on flood watch at any hour |
| PAGASA river and rain gauges | Every 10 min; every 20 min while calm | 30 min | The stations report every 10 minutes |
| PAGASA public alerts | :08 and :38 | 1 h | Flood advisories go out around 05:00 and 17:00, more often in bad weather |
| PAGASA drought outlook | 11:50 daily | 1 day | Reissued once a month |
| Maynilad advisories | Every 3 h at :05 | 3 h | Repair work is often scheduled overnight |
| PAGASA typhoon status | Hourly at :20 | 3 h | Skips to every 3 h while no cyclone is active |
| PAGASA ENSO advisories | 11:10 daily | 1 day | Issued irregularly, a few times a season |
| NOAA ENSO indices | Tuesday 09:40 | 7 days | Weekly SST is updated on Mondays (US time) |
| Open-Meteo city and barangay weather | Hourly at :15 | 2 h | One batched request for all active cities, and one for the zones of barangays chosen in the last day |
| Open-Meteo watershed rain | 00:30, 06:30, 12:30, 18:30 | 6 h | Matches the ensemble update cycle |
| NASA POWER rainfall | 07:45 daily | 1 day | Daily data with a lag of a few days |
| MWSS probe | Monday 10:30 | 7 days | One polite check a week |
4.2Polite fetching
All HTTP traffic goes through one fetcher. It sends a User-Agent that names the project and gives a contact address. It keeps one client per host, each with its own minimum interval between requests (Table 3). It verifies TLS on every request, adding known intermediate certificates where a Philippine government site serves an incomplete chain, and never turns verification off. The river gauge server, for example, sends only its own certificate. Tubig ships GlobalSign's public intermediate for that host and still verifies the chain up to GlobalSign's root. Network errors, HTTP 429 and 5xx responses are retried with exponential backoff and jitter. A Retry-After header is honoured in both its seconds and date forms, up to five minutes. Other statuses are handed back to the source to interpret. A Cloudflare challenge on the MWSS site, for example, is recorded as blocked, and no attempt is made to solve it.
| Host | Timeout | Retries | Minimum gap |
|---|---|---|---|
| pagasa.dost.gov.ph | 45 s | 3 | 5 s |
| pasig-marikina-tullahanffws.pagasa.dost.gov.ph | 30 s | 2 | 2 s |
| publicalert.pagasa.dost.gov.ph | 30 s | 2 | 1 s |
| droughtcaster.pagasa.dost.gov.ph | 60 s | 2 | 3 s |
| www.mayniladwater.com.ph | 90 s | 2 | 3 s |
| mwss.gov.ph | 30 s | 0 | 5 s |
| www.cpc.ncep.noaa.gov | 60 s | 3 | 5 s |
| Open-Meteo (three hosts) | 30 to 60 s | 3 | 1 s |
| power.larc.nasa.gov | 180 s | 2 | 5 s |
4.3Snapshots and change detection
Every page or API response is saved as a gzip snapshot, named by its SHA-256 hash so that identical content is stored once. Before parsing, the source extracts the part of the page that matters, such as the dam table without the surrounding navigation, and hashes that region. If the hash matches the last snapshot that was parsed with the same parser version, the run is recorded as unchanged and nothing is parsed again. The raw snapshots also make it possible to re-parse old pages after a parser fix.
4.4Parsing and validation
PAGASA's dam table uses merged cells, two rows per dam and dates without a year, so the parser rebuilds the grid cell by cell and infers the year from the date the page was fetched. Each reading is checked before it is accepted. A level outside the range from 60 m below the dam's lowest threshold to 10 m above its highest, a change of more than 15 m in one day (the largest real daily change in the 2026 record is 8.66 m), or an observation time in the future marks the reading as quarantined. A quarantined reading is stored for review but never used in a calculation. If a known dam disappears from the table or the table's layout changes, the source raises a dam_missing or layout_changed alert instead of storing partial data.
The typhoon source needs positive evidence before it reports an active cyclone. A change in wording on PAGASA's page is not enough. The ENSO page is decoded explicitly as UTF-8, because the word "Niño" is otherwise easy to garble.
4.5Advisories and barangay matching
Maynilad publishes three feeds through its site's JSON API: emergency, scheduled and rotational. Each accepts a city filter, and an empty filter returns every city Maynilad serves, five notices a page, ordered by city. Tubig makes that one all-city query per feed every three hours and follows each feed's cursor to its last page, the same cursor the site's "Load more" button uses. The history therefore holds notices for every city Maynilad serves, including cities no one has opened in Tubig yet. Every call takes 15 to 23 seconds, while an empty feed answers in under one. On 2 October 2026 the emergency feed had eight pages and the scheduled feed four, about a dozen calls or four minutes in all. Asking per city would have needed at least three calls for each of the 19 cities. The number of pages is capped at 30 per feed, far above a busy day. Because the feed is ordered by city, a cut would drop whole cities, so reaching the cap raises an alert and a truncated result is never mistaken for a complete one. Each advisory is classified as supply_rotational (supply-limited or schedule-driven), maintenance_leak (repairs and leaks) or other. Its free-text barangay list, such as "Brgys. 49, 52, 53, 56 to 71", is expanded and matched to PSGC barangay codes. When the barangays cannot be identified, the advisory still applies to the whole city and is labelled as city-level.
4.6Failures and alerts
Every run, successful or not, is recorded with its status, HTTP code, row counts and duration. Repeated failures, quarantined values, layout changes and a source that comes back after being blocked all open an alert. There is one open alert per source and kind, so a recurring problem bumps a counter instead of creating noise. Alerts are shown on an admin page and sent to the operator by Telegram as a digest at most every five minutes, with a daily cap. Each step also writes a log line, for example a fetch line with the URL path, status, size and time, so the operator can watch the crawler work in real time.
4.7River gauges, basin status, flood advisories and drought
River basin status. PAGASA's flood page lists 18 major river basins and 4 dam sub-basins, each with a status. On 1 October every one read "Non-Flood Watch". The escalation PAGASA uses is Flood Watch or Outlook, Flood Advisory, Flood Warning and Critical Flood Warning. A status Tubig does not recognise is stored as printed, treated as a caution and reported to the operator, so a new term can never read as all clear. For the basins named in the city profiles, Tubig also reads the basin's own page. For Metro Manila that is the NCR, Pasig, Marikina and Laguna de Bay basin, whose daily bulletin gives the observed and forecast 24-hour rain and the forecast water level. If that page fails to load, the last good bulletin is kept and ages out by its issue time.
River levels and rain gauges. The flood forecasting system for the Pasig, Marikina and Tullahan rivers has a public map that loads two plain JSON lists: 17 river stations with the level now and 30 minutes, 1 hour and 2 hours ago, plus PAGASA's alert, alarm and critical levels, and 25 rain gauges with 10-minute, 1-hour, 3-hour and daily totals. The station codes group by basin (111 for the Marikina, 112 for the Pasig and San Juan, 113 for the Tullahan), which helped place each station on its river. A hand-checked map then lists, for each station, the cities it informs, either as their own river or as an upstream warning. Rain gauges are matched by distance instead: those within 8 km of a city's centre.
The raw readings need care. About a third carry an unexplained "(*)" mark, mostly at MMDA stations, and often repeat one value for hours. These are stored as unconfirmed and never set a rating. The Ugong station read 20.58 m against a 16.60 m critical level on a dry, steady night, so its levels and thresholds evidently do not match, and it is kept out of the rating. La Mesa's alert level is a dam operating level, not a flood signal, so La Mesa is left to its own spill rule. For every other station, a reading counts only if it is less than an hour old, unmarked, and, at alert level or above, from a live river: one that moved at least 5 cm in two hours or was below its alert level at some point in the past week. That rule stops a gauge parked above its thresholds from raising a false flood. While a river at its alert level is rising, or rain is heavy, the gauges are read every 10 minutes, and every 20 minutes otherwise. Every reading is kept, as a record for later flood models.
Flood advisories. PAGASA publishes its public alerts as an Atom feed of CAP 1.2 files, an international standard for machine-readable warnings, under a CC BY 4.0 licence. Tubig fetches each new file from the last three days and parses the XML with DTDs, entity expansion and network access all disabled. Each alert carries its sent and expiry times, an event such as "General Flood Advisory (Moderate)", a severity, a response type ("AllClear" on a final advisory), references to the alerts it updates, the rivers named for each province, and areas with polygons and province codes. The codes use the old 9-digit PSGC form, so 112500000 becomes 1102500000. An alert is in force until it expires, is an all-clear, or a later alert references it. It covers a city when one of its codes is the city's own, its province's or its region's, or, without codes, when its polygon contains the city's centre. Independent cities such as Baguio belong to no province in the PSGC, so a small table, derived from the boundary data, gives the province around each of them.
Cyclone alerts. The same feed carries PAGASA's Tropical Cyclone Alerts, but their area is the whole Philippine Area of Responsibility, so the area alone says nothing about a city. The alert's text does. It gives the centre as a latitude and longitude, the maximum winds and gusts, the forecast heading and speed, and a section on hazards affecting land areas that either names the places at risk or says the storm is unlikely to affect land. Tubig reads these with fixed patterns and computes the great-circle distance and compass direction from the city's centre to the storm, and whether its heading lies within 70 degrees of the line toward the city. The city is affected when the land section names it, its province or its region; Metro Manila's districts are matched as "Metro Manila" or "NCR". For Typhoon Queenie on 27 September 2026, the alert put the centre at 24.9° N, 127.6° E with winds of 150 km/h, moving north-northeast at 10 km/h, and said it was unlikely to affect the country. For Caloocan that is about 1,320 km to the north-northeast and moving away, so the typhoon rating is low and it no longer raises the flood level. Anything the text does not give is left out, never guessed.
Drought outlook. The map on PAGASA's El Niño advisories page is fed by a drought monitor with two JSON endpoints per island group: the monthly assessment and the six-month outlook, each listing provinces by class. PAGASA defines a dry condition as two months of below-normal rain (21 to 60% less than average), a dry spell as three such months or two months way below normal (more than 60% less), and a drought as three months way below normal or five below normal. On 1 October the outlook, issued on 21 September, put both Metro Manila and Bulacan, where the Angat watershed lies, in a dry condition in October, a dry spell in November and December, and drought from January to March 2027.
5Prediction method
This section describes the deterministic engine. All of it is ordinary code with unit tests. The AI layer described in Section 6 receives its output and nothing else.
5.1Daily level series and trailing rates
Readings from all sources are reduced to one level per Manila calendar day. PAGASA readings rank first, then manual and MWSS entries, then the seed data, and within a day the reading closest to 08:00 is kept. Trends are least-squares slopes over the last 7 days (at least 4 readings) and the last 30 days (at least 8 readings). For a falling reservoir, the days remaining until a threshold h* follow directly:
The days are counted from today, not from the date of the reading. A rising or flat trend gives no date at all, which the system reports as "not approaching" rather than a negative or infinite number.
5.2Reservoir response model
A trend says nothing about the rain to come, and a simple regression of level change on rainfall fits poorly. Light rain on a dry catchment barely reaches the reservoir, while after heavy rain the catchment keeps draining into it for days. Tubig captures both effects with an antecedent precipitation index (API) and a threshold:
Here Rt is the day's mean rainfall over four grid points on the upper Angat catchment in millimetres. k is the daily decay of the index, T is the index level below which rain has no effect, and b converts effective rain into metres of rise. The term adry(h) is the drawdown on a day with no effective inflow. It depends on the level, because the reservoir is narrower when it is low, so it is estimated separately for four level bands: below 170 m, 170 to 190 m, 190 to 200 m and 200 m and above. Rises are capped at the normal high water level of 210 m, since water above it is released or spilled. A level already above the cap is not pulled down to it.
5.3Measuring the dry-season drawdown
For each band, adry is the median daily change over intervals of up to three days in which the index stayed below T. Unlike the rain response in Section 5.4, which is fitted on recent months only, the drawdown is measured over the whole stored record, so a band measured in one dry season stays measured in the next. A band needs at least five such intervals to count as measured. Intervals within 0.5 m of the cap are excluded, because a flat line at full supply reflects operators releasing water, not a natural drawdown. A median slower than −0.05 m per day is also rejected as not credible. A band with no measurement borrows from the nearest measured band. If no band has been measured, the model uses a default of −0.40 m per day, taken from the 2026 dry spell. Angat fell from 180 m on 11 May to 162.94 m on 25 June (−0.38 m per day) and to 150.42 m on 25 July (−0.42 m per day). Each projection states whether its drawdown was measured, borrowed or defaulted.
5.4Calibration
The parameters k, T and b are found by grid search. For every combination of k ∈ {0.5, 0.6, 0.7, 0.8, 0.85, 0.9}, T ∈ {0, 10, 20, 30, 40, 60, 80} mm and b from 0.005 to 0.120 m per mm in steps of 0.005, the model is run forward from the first reading of the last 180 days using the observed rain, and the result is compared with the actual readings:
The combination with the lowest replay error wins, and the reported error is recomputed for the final banded model. Calibration needs at least 10 readings spanning at least 30 days. The index is warmed up over the 60 days before the window, so the first replayed day starts from a realistic index. On the readings available on 1 October 2026, the search settled on k = 0.8, T = 40 mm and b = 0.03 m per mm, with a replay error of 1.32 m over 24 readings. That is small next to the 58 m rise the season produced.
5.5Rain scenarios
The model is driven forward by three rain scenarios, taken from the spread of forecast ensemble members over the watershed. For the first 16 days, the Open-Meteo ensemble provides cumulative rainfall per member. Tubig takes the 10th, 50th and 90th percentiles of the cumulative totals, which keeps each scenario internally consistent, and differences them into daily amounts. After day 16 the scenarios continue with the same percentiles of the ECMWF SEAS5 seasonal members. If the forecasts were issued before today, the first day's increment is taken against the previous day's cumulative total, so old forecasts do not dump several days of rain onto day one. The days between the latest reading and today are filled with observed rain. The main horizon is 60 days, and the paths continue at low confidence to the end of the seasonal forecast.
5.6Confidence
Every projection carries a confidence level, decided by the rules in Table 4. The rest of the system treats low confidence seriously. Projected crossing dates are used by the risk rules only at medium or high confidence. At low confidence the interface draws the scenario fan faintly with dashed edges and labels the dates as scenarios, and the briefing is told to present them as possibilities, not expectations.
| Condition | Effect |
|---|---|
| Dry-season drawdown at the current level not yet measured (borrowed or default) | Low |
| Calibrated on fewer than 20 readings | Low |
| Replay error above 2 m | Low |
| Latest reading more than 2 days old | Low |
| Rain scenarios more than 24 hours old | Low |
| None of the above, at least 60 readings and error at most 1.5 m | High |
| Otherwise | Medium |
5.7A worked example: Angat on 1 October 2026
Figure 2 shows the situation the system computed on 1 October 2026. The reservoir had risen from its July low to 208.64 m. The calibration was good, but every dry interval in the record happened below 180 m, so the drawdown near full supply had never been measured and the model fell back to the −0.40 m per day default. The projection is therefore low confidence. The default is probably too steep at a high level, where the reservoir is wider, so these paths are more likely to overstate the fall than understate it.
| Scenario | Rain in 60 days | Level at day 30 | Level at day 60 | Below 180 m | Below 160 m |
|---|---|---|---|---|---|
| Dry (P10) | 216 mm | 197.6 m | 185.6 m | 15 Dec 2026 | 3 Feb 2027 |
| Median (P50) | 313 mm | 201.5 m | 189.5 m | 24 Dec 2026 | 12 Feb 2027 |
| Wet (P90) | 513 mm | 205.6 m | 197.4 m | 13 Jan 2027 | 4 Mar 2027 |
Because confidence was low, none of these dates fed the water risk rating. The rating that day was moderate, for a different reason: a strong El Niño was active with the dry season 61 days away.
5.8Climate context
Rain against normal. For windows of 30, 60 and 90 days, observed NASA POWER rain is compared with the mean of the same calendar window in each year from 1991 to 2020:
Both sides use the same dataset, so model bias cancels out. The result is reported in PAGASA's percent-of-normal classes: way below normal up to 40%, below normal from 41 to 80%, near normal from 81 to 120%, and above normal beyond that [4]. A window needs 90% of its days present and at least 20 baseline years, and a window whose latest data is more than 10 days old is reported as insufficient. On 1 October 2026 the Angat watershed had received 241% of its normal rain over 30 days and 171% over 90 days.
ENSO. The ENSO phase follows the NOAA thresholds on the latest ONI value. An ONI of 0.5 °C or more is El Niño and −0.5 °C or less is La Niña. Strength is weak below 1.0, moderate below 1.5, strong below 2.0 and very strong above that. The trend compares recent seasons, and the weekly Niño 3.4 value and the latest PAGASA advisory are shown beside it. The JJA 2026 ONI was +1.8, a strong El Niño, with the weekly index at +3.1.
Season. The refill season runs from June to November and the dry season from December to May. The engine reports how many days remain until the next change and lists PAGASA's expected number of tropical cyclones for the coming months.
Analog years. The four years that ended in a supply crisis or near-crisis (2010, 2019, 2023 and 2024) are compared with the present through the year before each. For each, the engine takes the six most recent ONI seasons and the same six seasons in the preceding year, and ranks the analogs by the root-mean-square difference. On 1 October 2026 the closest match was the run-up to 2024 (ONI difference 0.49 °C), when Angat stood at 198.81 m on 1 April during an El Niño-enhanced dry season.
5.9Weather signals
The heat index is computed for every hour of the 48-hour forecast with the Rothfusz regression used by the US National Weather Service, including its adjustments for very dry and very humid air [15] [16]. The peak is classed in PAGASA's categories: caution from 27 °C, extreme caution from 33 °C, danger from 42 °C and extreme danger from 52 °C [17]. Hourly rain is compared with PAGASA's rainfall warning colours: yellow from 7.5 mm, orange from 15 mm and red above 30 mm per hour [18]. Daily totals count only for the next three days, because a single model's rain total two weeks out is not something to act on.
Weather for the barangay. A city's weather comes from one point at its centre, and in a large or split city that point can stand far from the reader. On 2 October 2026 the point for Caloocan, in the north of the city, read 27.5 °C with thunderstorms while the south, where most of its barangays are, was at 29.6 °C and overcast. Open-Meteo's forecast is a grid of cells several kilometres across, and it answers a point with the cell it used. Tubig therefore asks once for each Metro Manila barangay's representative point, records its cell, and treats the barangays of one cell as a weather zone, since they get the same forecast. Caloocan's 188 barangays fall in six zones. A zone is fetched hourly while one of its barangays was chosen in the last 24 hours, without the 31 past days the city request carries, which keeps a zone at about 63 of the free tier's 10,000 daily calls. The barangay view then uses its zone's weather and runs the rules with it; when the zone has no recent forecast, the city's stands. The briefing is still written per city. Outside Metro Manila there are no barangay boundaries, so the city's weather is used.
5.10Risk rules and actions
Four hazards are rated low, moderate, high or severe. The thresholds live in a configuration file, each with a note on where it comes from, and Table 6 summarises them. Each rating lists its reasons as codes with an English message, and carries a data status of ok, stale or missing.
| Hazard | Condition | Level |
|---|---|---|
| Water supply | Supply rotational interruption scheduled for the area | Severe |
| Angat below 160 m (critical) | Severe | |
| Angat below 180 m (minimum operating) | High | |
| At the measured 7 or 30-day rate, below 180 m within 30 days (or 60 days) | High (moderate) | |
| Trusted projection: dry scenario below 180 m within 30 days, or median within 60 days | High, or moderate | |
| Within 10 m of 180 m with neither a trusted projection nor a measured trend | Moderate | |
| Strong or very strong El Niño with the dry season under way or within 90 days | Moderate | |
| PAGASA's outlook puts the reservoir's watershed in a dry spell or drought within six months | Moderate | |
| Flooding | Forecast or measured hourly rain in the red band (a gauge within 8 km) | High |
| Yellow or orange band, a day over 50 mm, or a cyclone rated moderate or higher for the city | Moderate | |
| A day over 100 mm, or La Mesa spilling (80.15 m) for cities on the Tullahan | High | |
| A station on the city's own river at its alert, alarm or critical level | Moderate, high, severe | |
| An upstream station at its alarm or critical level | Moderate, high | |
| PAGASA basin status: Flood Watch, Outlook or Advisory; Flood Warning; Critical Flood Warning | Moderate, high, severe | |
| A PAGASA General Flood Advisory in force over the province | By its severity, moderate or higher | |
| Red rain together with La Mesa spilling, a cyclone rated moderate or higher, a basin warning or a high river | Severe | |
| Typhoon | PAGASA's cyclone alert names the city, its province or its region in its land outlook | By the alert's severity, high or severe |
| Within 500 km of the city and not moving away | Moderate | |
| Farther, moving away, or PAGASA says it is unlikely to affect land | Low | |
| An active cyclone whose alert cannot be read | High | |
| Heat | Peak heat index in extreme caution, danger or extreme danger | Moderate, high, severe |
| Areas without a reservoir profile | 90-day rain at or below 40% of normal (60%), or PAGASA's outlook for the province: drought (dry spell) within three months | High (moderate) |
Actions follow from the ratings and from advisories. They are also data: each has a code, a priority, a horizon (now, the next two weeks, the next one to two months) and a message. A scheduled interruption for the user's own barangay produces a dated "store water before" action at the top of the list. Weather older than 3 hours, a cyclone check older than 6 hours, a basin status older than 6 hours, river gauges silent for 90 minutes and a reservoir reading older than 2 days all mark the affected ratings as stale. The stored situation is aged again whenever it is served, so a page opened hours after the last computation still says so.
6How the AI is used
6.1Its job, and its limits
A list of reasons and numbers is accurate but hard to read on a phone, so Tubig uses a language model for one narrow task: turning the computed situation into a short briefing. The briefing has a two-to-four sentence summary, a timeline for today, tomorrow, this week and the next two weeks, and an outlook with a one-line answer to "How worried should I be about water in the next one to two months?" The outlook also gives the main drivers, what would change the picture, the actions by horizon, and notes on any stale or missing data. Every timeline item, driver and condition cites the source ids it rests on. A last field, the glance, is one or two everyday sentences shown under the current conditions: should the reader be concerned today, and the one thing to do, if any. It must match the risk levels, so it stays calm when they are low.
The model does not compute, predict or rate anything. The risk ratings, the projection, the actions and every number on the page come from the engine and are displayed directly from its output. The briefing is extra explanation beside them, and the page works without it.
The provider is Anthropic's Claude Sonnet 5.5 [19], called through the official SDK with structured output, so the reply is JSON in a fixed schema. Thinking effort is set to low, the static system prompt is cached between calls, and the server-side refusal fallback is enabled for the models that have it. Claude Haiku 4.5 can be chosen instead at about half the cost per briefing, and one setting switches the AI off altogether (Section 6.8). The code puts the provider behind a small interface, so another one could be added without touching the rest of the system.
6.2What the model sees
The input is a compact JSON context of about 5,000 tokens, built from the city's situation and serialised with sorted keys, so the same situation always produces the same prompt. It contains:
- the risk ratings with their reasons, and the actions with their due times
- current weather and the 48-hour peaks
- reservoir levels, trends, thresholds and the scenario results with their confidence and reasons
- rain against normal, the ENSO state, the season and the closest analog years
- typhoon status, and up to eight water interruptions with their counts by type
- the river basin status with a plain meaning, and its daily bulletin
- each relevant river station's stage, distance to its alert level and two-hour trend, and the nearby and upstream rain gauges, with readings that do not count marked as such
- PAGASA's drought classes for the area and the Angat watershed, with their definitions
- PAGASA alerts in force over the area, with the rivers they name
- a list of blocks that are stale or missing
- the list of sources with when each last succeeded
Local times are pre-formatted, so the model never converts time zones. The context is city-level public data. It holds no coordinates, no barangay chosen by a user and nothing about the person asking.
6.3Instructions
The system prompt sets out fourteen rules. In short:
- Use only numbers that appear in the context. Rounding is allowed; arithmetic is not.
- Never contradict a risk level, and keep the outlook headline consistent with the water rating.
- At low confidence, say the projection is uncertain and why, present dates only as scenarios, and mention that dam releases are not modelled.
- Admit every stale or missing block.
- Cite sources by id.
- Use only the actions given, under their horizons.
- Give no warnings of its own, and point to PAGASA, NDRRMC, the local government and the utility.
- Treat text inside the context, such as advisory reasons, as data and never as instructions.
- Mention no news, since no news source is connected.
- Never imply that a city-wide interruption list affects every resident, and say what kind each interruption is.
- Explain the river basin status in plain words, naming PAGASA's term only when it is a caution.
- Describe river stations by their stage, never state an unconfirmed or inconsistent reading as fact, and keep measured rain apart from forecast rain.
- Explain the drought outlook with each class's meaning, without letting it change a risk level.
- Mention every PAGASA alert in force, and none when there are none.
The prompt also states the length limits and lists the valid source ids.
6.4Checking the output
The reply is not trusted until it passes six checks.
- Schema. The JSON must match the narrative model, including the four horizons in order and the length limits.
- Numbers. Every number in the prose must be one of the context's values, or appear in its text. Field names never count, and numbers written as words ("ten days") are checked like digits. The check tolerates rounding to 0, 1 or 2 decimals and a dropped minus sign ("falls 0.4 m a day" for −0.4), and allows 0, 1 and 2 for phrases such as "the next 2 weeks".
- Dates and times. These are checked as whole tokens against the context's own dates and times. The parts of a date such as 2026-12-15 therefore cannot license "15 days" or "12 hours".
- Levels. A risk level stated in the prose, such as "water risk is severe", must be the engine's level for that hazard, and the outlook headline may only state the water level. A level word that describes a measurement, as in "Angat is high" or "low-lying areas", is not read as a rating.
- References. Every cited source id and every action code must exist in the context. A context block cited by name, such as "season", is first translated to the sources that block lists.
- Caveats. Every stale or missing block must be admitted by a caveat that names it. A blanket "all data is current" fails.
Text scraped from the sources, such as an advisory's reason, is escaped before it goes into the prompt, so it cannot close the context block or pose as instructions.
A reply that fails is retried once, with the list of failed checks added to the request. If the retry also fails, or the model refuses, or the API errors, the system stores a briefing written by the rules instead. It has the same structure, assembled from the engine's own reason messages, and passes the same checks. The page then says which kind of briefing it is showing and why.
6.5When briefings are written
Briefings are written per city and language, never per user, so a thousand readers of one city cost the same as one. The API never calls the model while serving a page; it only reads the stored briefing. Only "hot" cities, opened by someone in the last 24 hours, get AI briefings. With nothing material changed, a hot city's briefing is rewritten once a day, when its day turns over at 05:00 Manila time, because that is when its "today" and "tomorrow" move on. A 24-hour cap guards against anything older. It is also rewritten when it is out of date, at most once an hour. Out of date means the situation's material digest has changed. The digest covers the side of each threshold that each reservoir is on, the four risk levels, supply-related interruptions (routine repairs only count as present or absent, so they do not trigger new text every day), the cyclone status and the ENSO state. It also covers the basin status, each river station's stage and whether it counts, any rain band at a nearby gauge, the drought classes and the alerts in force, but never the centimetres of a 10-minute reading. It includes the freshness of each source too, so data going stale triggers a rewrite that admits it. When a stored briefing no longer matches the current situation, or is from an earlier day, the API marks it as outdated. The page then hides its glance and says the numbers elsewhere are current.
Failures back off. After a failed attempt the next one waits twice as long as the last (one hour, then two, then four, up to the 24-hour cap), so a city whose briefing keeps failing costs a few calls a day instead of two an hour. If the attempt fails but an earlier AI briefing exists, that briefing is kept and flagged as outdated rather than replaced.
6.6Cost control
Every call is recorded in a ledger with its tokens, cost, latency and outcome. That includes invalid and failed calls, because those are billed too. Prices are kept as data with effective dates, and a model without a price is treated as an error, never as free.
Before each call, the system estimates the worst case. Every input token, including the schema the API adds, is priced at the cache-write rate. The full output allowance of 8,000 tokens is counted twice, because a declined request and its server-side fallback can both be billed. It makes the call only if the month's spend plus that estimate stays under the cap of US$10 per UTC month. The SDK's own retries are switched off, so every request passes through this check, and a request that times out is recorded at its worst case, since it may have been billed after the client gave up. Alerts go to the operator at 80% and 100%. When the cap is reached, the rules-written briefing takes over until the month ends. Table 7 shows measured costs from the first deployment.
| Run | Calls | Passed checks | Cost per call | Latency |
|---|---|---|---|---|
| First briefings (three cities) | 3 | 3 | US$0.021 to 0.029 | 8.6 to 12.6 s |
| Evaluation before prompt fixes | 4 | 2 | about US$0.024 | 8 to 10 s |
| Evaluation after prompt fixes | 3 | 3 | about US$0.023 | 11 to 12 s |
| Evaluation with river, basin and drought data (2 October) | 3 | 3 | US$0.026 to 0.034 | about 9 s |
A typical call reads about 4,900 uncached input tokens plus about 2,100 tokens from the prompt cache, and writes 1,100 to 1,500 tokens. Over the first two days the ledger recorded 56 calls costing US$1.27, an average of US$0.023 per call. Those days were unusually busy, because every change to the engine forced every briefing to be rewritten. In normal use, with the once-a-day rewrite above, one city costs about US$1.50 to 2.20 a month. The cost grows with the number of cities in use, never with the number of readers, and the cap still bounds it.
6.7What evaluation found
An evaluation command runs a stored situation through the model at chosen effort levels, applies the same checks, and reports validity, latency, tokens and cost. Its first runs found two failures. One driver ran past its 400-character limit, because a JSON schema cannot carry length limits to the model, so the limits are now written into the prompt. Another reply cited a context block name, "season", as a source id, so the valid ids are now listed in the request. After both fixes, every evaluated reply passed. An independent review of the AI layer then found five more gaps, all now closed. A wrong level word in the prose could pass. Numbers from field names and dates counted as grounded. Stale data did not trigger a rewrite. The SDK retried silently behind a single budget check. And a failing city was retried every hour. The checks in Section 6.4, the backoff in Section 6.5 and the cost rules above are the result.
On the second day the ledger showed that about one call in five was still rejected and paid for twice. Reading the rejections showed that most of the causes were in Tubig's own checks, not in the model. Five came from the headline check reading a word such as "high" as a rating when it described the reservoir's level. Three came from the model citing the context block "season" instead of its source. Four, all for Manila, came from the model counting interruptions by kind ("four emergency repairs") when the context held only the total. The fixes were to skip level words that describe a measurement, to translate block names into their sources, and to give the counts by type in the context.
6.8Without the AI
One setting, ENABLE_AI_INSIGHTS=false, stops every AI call. The worker queues no briefings, the evaluation command refuses to run, and every page serves the briefing written by the rules, including cities that had an AI briefing stored before. Everything else on the page was never written by the model: the ratings, actions, interruptions, alerts, weather, reservoirs and sources stay exactly as they are. Since the rules-written briefing then carries the whole explanation, it was reworked on 2 October. Today's entry opens with the weather now, lists the interruptions already under way with their end times as well as those starting later, and places the heat peak and the heaviest rain on the day they fall, only while they are still ahead. Long entries keep whole sentences, most important first.
Running a small language model on the Pi itself was considered and set aside. The briefing's input is about 5,000 tokens and its output about 1,100. A model of one or two billion parameters on the Pi 4's processor would take roughly six to ten minutes per briefing with all four cores busy, on a machine shared with other services, and models that small break rules such as "use only the numbers given" often enough that the checks would reject much of what they write.
7Delivery to the user
Finding the place. The web app offers "Use my location" or a search over all 1,642 cities and municipalities. No third-party geocoding or map service is involved in either. The search runs in the browser over a list of places that the app loads once from Tubig's own API, so nothing typed into it leaves the device. With location, the position comes from the browser's own location feature, which asks the user first and may rely on the phone's location services. It is sent once, in the body of a request to /v1/resolve. The API finds the city and barangay with a spatial index over the PSGC 2023 boundaries [13] [14], which are bundled with the API and queried offline. The user confirms the barangay from the nearest matches, and the app then remembers only the PSGC codes. Manila has no barangay polygons in the boundary set, so Manila resolves at city level.
Coverage. Any place in the country can be opened, but how much it gets depends on its profile (Table 8). Measured river levels reach every Metro Manila city except Parañaque, Las Piñas and Pasay, which have no stations; Muntinlupa gets the Laguna de Bay level, which has no alert levels.
| Places | What the page covers |
|---|---|
| Metro Manila, Maynilad west zone: Caloocan, Las Piñas, Malabon, Muntinlupa, Navotas, Parañaque, Pasay, Valenzuela | Angat outlook and projection, river basin status, drought outlook, flood advisories, weather, Maynilad interruptions, barangay views |
| Metro Manila, utility split by barangay: Makati, Manila, Marikina, Quezon City, San Juan | The same, with Maynilad's part of the interruptions; Manila at city level |
| Metro Manila, Manila Water east zone: Mandaluyong, Pasig, Taguig, Pateros | The same without interruption notices, which are not collected yet; the page says so |
| Rizal towns on the Marikina River: Rodriguez, San Mateo, Cainta, Taytay | The general picture below, plus measured river levels |
| Everywhere else, about 1,620 places | Weather, heat index, rain intensity, typhoon status, the province's drought outlook and flood advisories, at city level; no reservoir outlook or interruptions |
The situation. Opening a place registers it, which keeps it in the crawl rotation. The app then reads /v1/areas/{psgc}/situation, which returns the stored situation tailored at request time. The tailoring ages the data to the current time, keeps only the interruptions that name the user's barangay or name no barangay, re-runs the rules and merges identical interruption actions. The response also says whether Tubig reads the area's utility notices at all. Manila Water's notices are not collected yet, and for those areas the page says so instead of claiming there is no interruption.
The interface. The first screen asks "Should you be worried today?". The dashboard follows a glassmorphism weather design. Under the current conditions sits the glance line, then any alerts: a river at its alert, alarm or critical level, a PAGASA flood advisory over the province, or a basin under caution. Each alert names the river or area in plain words and links to PAGASA; the gauge numbers behind it stay off the page. Below the four risk badges, an active cyclone gets its own panel: whether PAGASA expects it to affect the area, a small dial with the storm's distance and direction from the city and where it is heading, its winds, and a link to the bulletin. Then come:
- the actions to take now
- the barangay's interruptions, each with a badge saying where it stands: ongoing, starts in 40 minutes, starts tomorrow, or ended
- the water outlook on a low-to-severe scale
- a chart of Angat's level with its scenario fan
- the briefing, the reservoirs, rain against normal, and the sources with their ages
The sky. The background is drawn live by a WebGL shader from the current weather code and the Manila time of day: dawn, day, dusk or night. It layers a sky gradient, two drifting cloud layers lit from the sun's or moon's side, stars that show between the clouds at night, rain falling at three depths, fog, a warm haze on hot days, and for thunderstorms lightning that lights the clouds from inside. Each strike flickers twice, seconds apart from the next, well under the three-flashes-a-second limit for safe content. The sky shows the weather now. A forecast of heavy rain has its own alert and no longer makes the background rain on a dry day. It renders at half resolution and 30 frames a second (24 on phones), pauses in background tabs, draws one still frame for readers who ask for reduced motion, renders fewer pixels and then stops moving on devices that cannot keep up, and falls back to a plain CSS sky without WebGL. It is sized to the full screen height, so a phone's address bar sliding in and out during scrolling no longer resizes and blanks it.
White text on a light sky is hard to read, so the shader caps the brightness of its lightest pixels with a soft knee, and a dark scrim sits over everything. Contrast was measured from screenshots: 13 combinations of time and weather, five frames each, and the four brightest day scenes again over 12 frames with lightning included. The worst case is 4.9:1 on a partly cloudy day, above the WCAG AA level of 4.5:1; nights reach 7.4 to 9.3:1. The trade-off is that daytime clouds are silver-blue rather than white. When the API cannot be reached, the app shows the last situation it saved, marks each block that has gone stale since, and stops showing it after 48 hours.
The public history. Everything Tubig collects is kept (Section 9), and the record is open to anyone at tubig-api.rjhon.net/history. It has a page each for interruptions, dams, typhoons, floods and climate, with filters by date, city, kind and state, and charts drawn on the server, so the pages run no scripts. They are the same pages the operator uses, without the operator's parts: the health cards, the crawl log, the system pages and the downloads. Interruptions cover every city Maynilad serves, including cities no one has opened in Tubig. Each page has a JSON twin under /v1/history, thirteen endpoints in all, and each endpoint has its own documentation page at tubig-api.rjhon.net/history/api with its parameters, every response field and a live example. Lists are paged with a next link, up to 500 items a call. Any website may read them, since every response, errors included, allows any origin. Each visitor may make five calls a minute, a limit kept apart from the site's own API, and Cloudflare stops bursts before they reach the Pi. Every response names its sources and their licences, such as CC BY 4.0 for PAGASA's alerts. The app's sources list links to the history, and the history links back to the app.
8Privacy and security
- Location. Precise coordinates appear only in the body of the resolve request. They are never put in a URL, stored, or logged. A chosen barangay is recorded by its code and the hour of the latest choice only, so its weather zone can be fetched; nothing about who chose it is kept, the record is deleted after a day without a choice, and barangay codes are not logged. Every log record passes through a filter that rounds any latitude or longitude field to two decimals, about 1.1 km. URLs are logged as host and path only, without query values. No third-party geocoder or map service ever receives a position, and the Content Security Policy would stop the page from sending one anywhere but the API.
- Exposure. By default the API is published on the Pi's loopback interface only. The first deployment publishes it on the home network as well, because Nginx Proxy Manager forwards the API hostname to the Pi's network address; it is never forwarded on the router. Rate limits key on the visitor address Cloudflare forwards in
CF-Connecting-IP, but only on requests that arrive from a trusted peer: loopback, Docker's bridge range or the container's own networks, which is where the tunnel and the proxy connect from. A device on the home network that sends the header itself is keyed by its own address, so it cannot fake a fresh limit per request. A header from an untrusted peer is logged once an hour with that peer's address, so a proxy missing from the list shows up instead of silently putting every visitor in one bucket. A client can register only a few new areas per hour, since each one adds work for the crawler. - Parsing untrusted input. Alert files are XML from outside, so they are parsed with DTDs, entity expansion and network access disabled; a test confirms a file that tries to read a local file gets nothing. Links taken from PAGASA pages are kept only if they are https on PAGASA's own domain.
- The public history. The public pages and API show what the sources published, never which places people open. Typhoon alerts are rated for Metro Manila only, the drought table shows Metro Manila and the Angat watershed's province only, and nothing lists the cities or barangays in use. The record holds no personal data to begin with.
- Operator access. The admin page sits behind Cloudflare Access and also needs a token. Anything that changes state requires the token as a Bearer header, which browsers never send on their own, so cross-site requests cannot trigger actions.
- Secrets. API keys and tokens live only in the backend's environment file, never in the frontend build, and are never logged. The AI ledger stores no prompt or output text.
- The web app. A Content Security Policy allows scripts, styles and fonts only from the site itself and network calls only to the API host. Fonts are self-hosted and there is no analytics.
9Operation
The API and the worker run in Docker on a Raspberry Pi 4B with Ubuntu 22.04, capped at 256 MB and 384 MB of memory. Measured on the same image, the API peaks at 184 MB and the worker at about 150 to 250 MB on its own (290 MB with a second process started inside it). The caps are kept, because they protect the other services that share the Pi's 4 GB. Both containers run with a read-only root filesystem, no Linux capabilities and health checks. The worker's health check watches a heartbeat file that its loop touches every few seconds. Database migrations run when the API starts, and the worker waits for them. The image is built on an arm64 Mac and copied to the Pi, so the Pi never compiles anything. The compose file needs Compose v2; the Pi's older Compose v1 could not read it, and Docker Engine 29 refuses its old API client, so the Ubuntu package for Compose v2 was installed. The API went live through the tunnel on 2 October 2026. Logs are human-readable by default, rotate automatically, and record each fetch, parse, store, job, assessment and AI call. Nothing collected is ever pruned: readings, every river gauge value, alerts, the daily projection log and the raw snapshots are all kept, at roughly 200 to 250 MB a year. The database can be backed up while running with SQLite's online backup. The web app is a static build on Cloudflare Pages, with long-lived caching for fingerprinted assets and none for the page shell. A build check refuses to produce a bundle whose API address is missing, insecure or absent from the security policy.
10Limitations and future work
- Releases are not modelled. The reservoir model treats operator releases, NWRB allocation changes and irrigation as part of the dry-day drawdown. A cut in allocation, as in 2019 and 2026, makes the real fall slower than projected, and a large release makes it faster.
- A single season of data. Calibration uses 2026 readings only, and the drawdown near full supply has not been measured yet. Confidence will rise once a dry spell at high levels has been observed, most likely in the coming dry season, or once a historical daily series for Angat is imported. Nothing collected is deleted, so each season's measurements keep counting. Each day's projection is also logged with its parameters and scenarios, so the forecasts can later be checked against the levels that actually followed.
- No stage-storage curve. The model works in metres, not cubic metres. Working in volume would handle the changing width of the reservoir directly.
- Coverage of utilities. Only Maynilad's advisories are collected. Manila Water areas get reservoir and weather information but no interruption notices, and the page says so. Maynilad also serves six Cavite cities (Bacoor, Imus, Kawit, Cavite City, Noveleta and Rosario), but they still use the general profile, so their Maynilad notices are not yet shown.
- Meaning, not just numbers. The checks confirm that every number in a briefing exists in the data, not that it is used for the right thing. A real "30" (a 30-day window) could still be misattached to a projection. The prompt rules and the low-confidence handling reduce this risk, but they do not remove it.
- City-level briefings. The AI briefing describes a whole city. The barangay's own interruptions and actions come from the rules.
- Typhoon detail. A cyclone is rated from the text of PAGASA's cyclone alert: its land outlook, and the storm's distance and heading from the city. Wind signals by province (TCWS 1 to 5) are not yet read, and a land outlook that names only a town inside a province is not matched to the city. When the alert's text cannot be read, any active cyclone is still rated high.
- River coverage. Measured river levels exist only for the Pasig, Marikina, San Juan and Tullahan rivers and Laguna de Bay. Parañaque, Las Piñas, the creeks of Muntinlupa and the Cavite towns have none. The station feed is undocumented, the meaning of its "(*)" mark is unknown, and Ugong stays out of the rating until its levels and thresholds agree.
- Basin bulletins as PDF. Four basins (Pampanga, Agno, Bicol and Cagayan) publish their bulletins only as PDF files, so Tubig reads their status but not their text. None of them covers Metro Manila.
- Independent cities. The table that gives each independent city its host province leaves out Zamboanga City, whose border splits between two provinces, so its provincial drought class reads as missing.
- Language. The interface is English only but built for translation, and a Tagalog briefing would double the AI cost per city.
- Storage. Raw snapshots are kept indefinitely, and a retention policy is still to come.
11Conclusion
Tubig shows that a useful local water outlook can be built from public data with modest means: a few well-behaved crawlers, a reservoir model simple enough to calibrate on one season, and rules that are easy to read and change. The model's main strength is that it knows when it does not know. It names the weakest part of its own estimate, and it keeps that uncertainty out of the risk ratings until the data supports it. The language model makes the result readable, but every number it writes must come from the engine, and when it cannot be checked, the system says less rather than more. The second release kept that discipline while adding the signals a household watches in the rainy months: measured river levels, PAGASA's own flood advisories and basin status, and the drought outlook for the months ahead. Each is checked before it may move a rating, and the whole picture still stands when the language model is switched off.
References
- [1]PAGASA. Dam Information, Hydrometeorology Division. pagasa.dost.gov.ph/flood
- [2]PAGASA. Tropical Cyclone Bulletins. pagasa.dost.gov.ph/tropical-cyclone/severe-weather-bulletin
- [3]PAGASA. El Niño and La Niña advisories. pagasa.dost.gov.ph/climate/el-nino-la-nina/advisories
- [4]PAGASA. Seasonal Climate Outlook, July to December 2026. pubfiles.pagasa.dost.gov.ph
- [5]Maynilad Water Services. Service advisories. mayniladwater.com.ph/service-advisories-2
- [6]Metropolitan Waterworks and Sewerage System. Water elevation. mwss.gov.ph/water-elevation
- [7]NOAA Climate Prediction Center. Oceanic Niño Index. cpc.ncep.noaa.gov/data/indices/oni.ascii.txt
- [8]NOAA Climate Prediction Center. Weekly OISST.v2 Niño indices. cpc.ncep.noaa.gov/data/indices/wksst9120.for
- [9]Open-Meteo. Weather Forecast API. open-meteo.com/en/docs
- [10]Open-Meteo. Ensemble API. open-meteo.com/en/docs/ensemble-api
- [11]Open-Meteo. Seasonal Forecast API (ECMWF SEAS5). open-meteo.com/en/docs/seasonal-forecast-api
- [12]NASA Langley Research Center. POWER Project, daily point API. power.larc.nasa.gov
- [13]Philippine Statistics Authority. Philippine Standard Geographic Code (PSGC). psa.gov.ph/classification/psgc
- [14]Faeldon, J. philippines-json-maps: PSGC 2023 boundaries in GeoJSON and TopoJSON. github.com/faeldon/philippines-json-maps
- [15]Rothfusz, L. P. (1990). The Heat Index "Equation". NWS Southern Region Technical Attachment SR 90-23.
- [16]NOAA Weather Prediction Center. The Heat Index Equation. wpc.ncep.noaa.gov/html/heatindex_equation.shtml
- [17]PAGASA. Heat Index monitoring and classification. pagasa.dost.gov.ph/weather/heat-index
- [18]PAGASA. Heavy Rainfall Warning (colour-coded rainfall advisories). pagasa.dost.gov.ph
- [19]Anthropic. Claude API documentation: models, structured outputs and prompt caching. platform.claude.com/docs
- [20]Tubig Project seed dataset, compiled 1 October 2026 from agency pages and news reports, including PAGASA readings reported by GMA News and Daily Tribune. Repository file
docs/el-nino-water-seed-data.json. - [21]PAGASA. Basin Hydrological Forecast and river basin bulletins, Hydrometeorology Division. pagasa.dost.gov.ph/flood
- [22]PAGASA. Pasig-Marikina-Tullahan Flood Forecasting and Warning System: water level and rainfall stations. pasig-marikina-tullahanffws.pagasa.dost.gov.ph
- [23]PAGASA. Public alerts in the Common Alerting Protocol (CAP 1.2), Atom feed, CC BY 4.0. publicalert.pagasa.dost.gov.ph/feeds
- [24]PAGASA. Philippine Meteorological Drought Monitor: monthly assessment and outlook by province. droughtcaster.pagasa.dost.gov.ph
The Tubig Project is not an official warning service. It summarises public data from PAGASA, NOAA, NASA, Open-Meteo and Maynilad and computes from it. PAGASA's public alerts are used under the CC BY 4.0 licence. For warnings and advisories, follow PAGASA, NDRRMC, your local government and your water utility.