Seantral: how it works inside, and the things that broke
· 8 min · marine, forecast, edge, testing
Marine forecasts turned into an honest verdict. The map was the easy part. The hard part was not lying when the data goes missing.
It started from a dull question I asked myself every time before going out: what’s it actually like today, at this exact spot? Weather sites give you numbers. Wind 14 knots, wave 0.8 metres. But turning those numbers into “worth it or not” I did in my head every time, and every time slightly differently. I figured: that translation step I can write once and keep honest.
Where the numbers come from
The data is Open-Meteo (Weather + Marine, CC-BY 4.0). The thing few people do, and the thing I cared about: I don’t take a single model. I blend four weather and three wave models per variable, and instead of a flat average I keep the distribution: mean, spread, percentiles. The spread between models isn’t thrown away: it becomes the confidence I show. When the models agree, the verdict is sharp. When they fight, I say so.
An hourly cron refreshes the cache for all the curated spots, in one batch. The client reads from the cache, it doesn’t call the providers from the browser. Sounds like a detail but it changes two things: the map is fast, and the providers don’t know who’s looking at what.
A couple of precautions that look like paranoia until you need them. When the cache expires and needs refreshing, a single requester does the fetch while the others wait on its result, instead of all firing at once and hammering the provider (anti-stampede). And if the fetch fails, I serve the last cache flagged as stale, with the notice: never a silent gap. The map downloads a compact per-spot summary; the detail loads only when you open the card.
The verdict, and why it leans pessimistic
The engine takes wave, wind, period, shelter and time-of-day and folds them into a 0–10 score with a word on top (calm, choppy, rough). Each activity weighs the same data differently: swimmers fear the wave, boaters watch the wind, anglers tolerate some chop. I didn’t invent a science: they’re “ideal/worst” thresholds per factor, hand-tuned and still under field validation, that push middling scores down rather than rounding them up. I’d rather say “choppy” and find flat water than the other way round.
The rule I never break: it is not a safety assessment. The score is a comfort read. For deciding whether to go out, the official notices are what count, full stop. I wrote it in three separate places in the app because it’s the sentence I don’t want any ambiguity on.
Make the data vanish, not the signal
Behind a single number there’s more stuff than whoever’s looking wants to see: seven models blended into distributions, the per-factor thresholds, the capped carried-forward wind, the missing-data handling. When you tap a spot, none of it reaches you. What reaches you is a word (calm, choppy, rough), a 0–10 score and the best window over the next hours. The hard part of the project wasn’t handling the data. It was hiding it without throwing the signal away.
The map isn’t a screen of the app, it’s the app. Colored badges per spot, readable at a glance, with no form to fill and no setting to pick. And the activity lens: swimming, boating, fishing re-fish the same data with one tap and change the reading, without adding a gram of complexity on screen. Underneath, all the work; on top, one switch.
The rest is there for whoever goes looking. The map shows a compact summary; the factor breakdown and the hourly trend load only when you open the spot, so the first view stays light and fast. And honesty doesn’t have to shout: the amber missing-data flag and the confidence coming from model spread sit there, discreet, for whoever wants them, and they don’t jump at you on the first look.
The AI features live by this same rule. The assistant answers “where should I swim” from the same engine as the map, so chat and map never contradict each other on a spot. The on-site report is left in three taps (as forecast, better, worse) and closes the loop toward the future scorecard. And the raw feedback gets classified by the AI into change requests, so I don’t drown in reports. The point is always the same: a lot of work underneath, one single thing to look at on top.
The evening I saw “4.7” on a flat sea
This one bit me. I look at the app and a spot shows a mediocre score while outside it’s a mirror. I lost an evening hunting a bug in the engine, and the engine was fine. The problem was upstream: Open-Meteo sometimes has nightly holes on the 3–6 hours, and the wind was missing for that window. The code carried the last known wind forward, sensible enough, but with no cap, and without saying so.
The fix wasn’t “hide the hole”. It was making it honest: carried wind has a six-hour cap from the data’s origin; beyond that it explicitly degrades to “wind missing”, the score is capped around 5, and an amber notice shows up. No factor without signal gets penalised silently: if a value is missing, you can see it’s missing. The number always travels with its word, even when the word is “don’t trust this too much”.
One engine, two runtimes, zero “it’s different on this device”
The verdict computation is needed in two places: in the browser (to answer instantly when you tap a spot) and on the edge (when the AI assistant answers “where should I swim”). The temptation is to write the same logic twice. That’s exactly how, three patches later, the map and the AI hand you two different numbers for the same spot, and nothing crashes: they just drift. A nightmare to find.
I kept a single pure module, shared between client and edge, and locked it with golden vectors: a set of cases with frozen input and expected output, run in CI on both runtimes: Vitest on the browser side, deno test on the edge. If a change makes the two diverge, the build fails before I notice by hand. It’s the net that let me touch the engine without fear. And in fact when I realigned the AI to the spot list, the bug was right there: the AI used the nowcast frozen at fetch time instead of projecting to the current hour like the map does. Same engine, different reference hour, different ranking.
The report from whoever’s actually there
A forecast is a forecast. What it lacks is someone in the water saying “yes, it’s like the app says” or “no, it’s worse”. I added a quick report: how it is versus the forecast: same, better, worse. With one condition: to leave it you have to actually be there. The client computes the distance from the spot (Haversine, threshold around 3 km) and with no location, or too far, the report doesn’t go through. I store the declared distance, not the coordinates. It’s a soft check, not a burglar alarm: it keeps reports anchored to reality, it doesn’t track anyone.
Where reports diverge sharply from the forecast, that’s a bridge to the part I care about most long-term: the per-spot scorecard. Forecast versus observed, and over time I tune the formulas on real data instead of by eye. For now the numbers are few and it’s all under validation. I say so plainly, because the worst thing would be passing an experiment off as science.
Where it’s heading
The direction is two steps. First collect real signal: the on-site reports, the divergences between models, the curated webcams. Then use it to recalibrate the formulas on data instead of by eye: a per-spot scorecard comparing forecast against observed over time. On top there are two experiments pulling at me: a multi-factor bathing index (water, air, UV, wind) and a “dirty sea” read from public water-quality sources, with a turbidity flag after it’s just rained.
Further out, the part I enjoy imagining: a measurement layer on site, not just forecast. Existing tide-gauge networks, in-situ data, and (as a demonstrator, not a product) a home-made buoy with a microcontroller, motion sensors, GPS and a small solar panel. The value isn’t the hardware: it’s having the comparison between what the model says and what the sea actually does. It’s an exploration, and I mark it as such.
What it taught me
That the hard part of a forecast app isn’t the forecast: it’s the behaviour when the data is missing. An engine that always returns a full number is simpler to write and easier to distrust. All the real work was building the honest ways of saying “I don’t know”: the carry-forward cap, the visible missing factor, the confidence from model spread. And a single engine, tested across two runtimes, because trust goes fast the day two screens of the same app disagree.