Why this exists
I keep a 1987 Bayliner 3288 in a slip at the Port of Everett. Deciding whether a given day is worth going out on meant opening four or five tabs: tide predictions in one, currents in another, the coastal zone forecast in a third, a model in a fourth, and then doing the arithmetic in my head about whether the wind would be against the ebb through the middle of the afternoon.
None of those sources is wrong. They are just written for different readers, and none of them answers the question I actually have, which is: is this a good day to take my boat out, on the water I take it out on?
So this site does the tab-opening and the arithmetic once, every few hours, and puts the answer on a page. It is opinionated on purpose. The limits it scores against are mine — a comfort line for a 36-year-old flybridge cruiser and the people aboard her, not a safety limit for anybody else.
The checklists came later, and for the same reason. They were written for my own boat log, and they turned out to be the part other people ask for, so they are published too — tickable on a phone, and made to be printed and kept in a sleeve by the helm. The log book they were written for stays private; the lists are not worth keeping to myself.
What it actually does
Every run is the same four steps, per marine area:
- FetchNOAA tide and current predictions for the area's station, the NWS coastal zone forecast text, the MET Norway model for the area's lat/lon, and a WDFW rules feed.
- ScoreEach day gets wind, sea state, current and daylight scored against that area's configured bands. The worst factor rules the day — a day is not "good" on average, it is good only if nothing in it is bad.
- RateThe day gets one of five ratings, a plain-language label, and a note naming the limiting factor, so the rating always says why.
- PublishThe page is written to a temporary file and renamed over the live one, so a reader never sees a half-written page. The previous version is kept beside it.
It is deliberately dull. There is no database, no login, no queue, no JavaScript framework, and nothing to keep running between builds. Each page is a single file of HTML with its styles inlined, written to disk and served by nginx. If every moving part of this stopped at once, the last pages it wrote would go on being served, correctly, indefinitely.
What it refuses to do
A forecast site that is confidently wrong is worse than one that is plainly unavailable, so the generator would rather publish nothing than publish something it cannot stand behind:
- An area with no tides and no model does not publish. Two dead sources is not a forecast.
- A rating the index has no label for is a fault in this code, not a weather condition. It is reported as a fault rather than dressed up as "no forecast".
- If any area raises an exception, the index is not rewritten at all — an index full of "Unavailable" over six working pages turns a bug into an outage.
- A page that comes out suspiciously short, or without a closing tag, is never installed.
The last good page stays up in every one of those cases. Stale and labeled with its build time beats fresh and wrong.
Where the numbers come from
Four sources, each doing the one thing it is best at. How often each of them changes is a property of the source, not of this site — the times below are approximate, and are what the site is built around rather than a promise made by anyone publishing the data.
| Source | What it gives | How often it changes |
|---|---|---|
| NOAA CO-OPS | Tide heights and current slack/max times for the area's reference station | Published in advance; effectively static |
| NWS coastal zone forecast | The human forecast text for PZZ134 (Admiralty Inlet) and PZZ135 (Puget Sound), including wave language | About four times a day, and on amendment |
| MET Norway locationforecast | Hourly wind speed and gusts at the area's coordinates | Roughly hourly |
| WDFW rules feed | Season and emergency-rule notices | Irregular — whenever the department posts one |
Tide and current times are predictions: astronomy, computed years ahead, not measurements. They do not know about a storm surge. Wind and waves come from a mix of a human-written zone forecast and a numerical model, and where the two disagree the site takes the more pessimistic reading.
How often this updates
The build runs on a small virtual machine in Amazon's Oregon region and nowhere else. It is not triggered by anyone visiting, and no laptop needs to be awake for it — the box fetches, scores, renders and publishes entirely on its own.
A missed run is caught up rather than skipped: if the machine is asleep or rebooting when the timer should have fired, it runs at the next start instead of waiting four hours. Each run is spread by up to two minutes so six areas do not hit the same source at the same instant.
| Rebuild | every 4 hours — 00:00, 04:00, 08:00, 12:00, 16:00, 20:00 Pacific |
|---|---|
| Browser cache | 5 minutes — a rebuild is visible within that |
What the build number means
Every page on this site carries a build number in its top corner — the one you clicked to get here. It names the exact version of the generator that wrote the page you are looking at, and the day it wrote it.
The v is a serial, not a semantic version. It counts releases: v49 is simply the release after v48. It does not encode “breaking” or “minor”, because there is nothing downstream of it to break — no API, no library, nobody pinning a dependency on it. One human runs one site, and what the number is really for is answering “is the page I am looking at the one that has the fix?”
It increments once per release, not once per commit and not once per rebuild. The site rebuilds itself every four hours and those rebuilds do not change it: same code, new weather. What changes it is new code being installed, at which point the number moves by one and the changelog gets an entry naming what moved.
Point revisions — a v50.1 for a typo fix against the same release — are coming, but not yet. Until then a one-word fix and a new section both cost a whole integer, which is cheap enough.
A release does not reach the site by being uploaded. The build is staged in full on the server first, the new code's own test suite is run there against that staged copy, and only a suite that passes gets swapped into place. A build whose tests fail cannot go live at all — the live site is left untouched and the previous install stays on disk for a one-command rollback. That is why the number and the test count are worth printing together: the number tells you which code wrote the page, and the count tells you what it had to survive to get here.
For nerds
Constraints, chosen up front and kept:
- Standard library only. No pip install, no virtualenv, no dependency that can rot. The generator runs on whatever Python the OS shipped.
- No build step and no bundler. The CSS is a file that gets inlined; there is nothing to compile.
- No JavaScript for anything that matters. The public pages carry two small scripts of their own: the theme toggle, and the ticks on the checklists. Turn JavaScript off and every page still reads, and still prints — you lose the ticking, not the list.
- No third-party anything at runtime. No analytics script, no CDN, no fonts fetched per visitor, no trackers. Nothing on the page phones anyone.
- Themes in CSS custom properties — light, dark, and a red-on-black night mode for reading on the boat after dark without wrecking your night vision.
- Atomic writes. Every page is written to a temp file and renamed into place; the previous version is kept beside it as
index.prev.html.
Deploys go as a versioned tarball. The installer stages the whole thing, runs the new build's own test suite on the server, and only swaps it into place if the suite passes — a build that fails its tests cannot reach the live site, and the previous install stays on disk for a one-command rollback.
The numbers in the table below are read off this build while it is being written. Nothing there is typed into the page by hand.
| Last build took | 12.0 seconds |
|---|---|
| Marine areas | 6 of 6 built on the last run |
| Scored ahead | 14 days per area |
| Generator | 20 Python files, 11,619 lines, standard library only |
| Test suite | 832+ assertions, run on the server before an install is allowed to proceed |
| Page weight | 57 KB the front page, 215 KB an area page |
| Version | v53 2026-09-15 |
What this is not
It is not an official forecast, and it is not a substitute for one. There is no Coast Guard wind limit for recreational boats; the one official threshold is the National Weather Service Small Craft Advisory at 20–33 kt sustained, and the limits used here sit at roughly half that. A day this site calls rough is a day I would not enjoy, not a day that is necessarily dangerous — and a day it calls good can still be the wrong day for your boat, your crew, or your plan.
Season and shellfish information is verified by hand for Marine Area 8-2 only. Everywhere else it is a convenience copy of a feed. Confirm with WDFW and the Department of Health before you put a pot in the water or a fish in the box.
Check the real forecast. Then check the sky.
Colophon
The name and the artwork come from a flyer Jess made for her DJ nights at the Bluemoon in Seattle. The boat is being renamed TANGLEFOOT in her honor — why, and how we plan to do it — and the site got the name first.
Set in Archivo Narrow, Source Sans 3 and IBM Plex Mono. Written with the help of Claude, which also wrote most of this page — the opinions about boats are mine.