Skip to content

Releases: productdevbook/nabiz

v3.12.1 — a summary may be pessimistic, not optimistic

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 10:01

Fixes a regression in v3.12.0 that could publish NaN %. Anyone on 3.12.0 with a group should take this.

A group of one member published NaN

  • v3.12.0's "never worse than the second-worst" indexed past the end of the ranking when a day had exactly one member row. Spreading undefined gave a row with no day, no total and no ok: the page printed NaN %, status.json said uptime_90d: null — which the docs define as "before the first day of data" — and history.json published 86 of 90 days with every field missing.
  • It reaches a group of one member, or a group of two on any day only one of them has a row for: a member onboarded later, a member removed, a round the rollup missed.
  • The clamp was wrong the other way too. Two members each down 30% of the time, never on the same day: every day had exactly one bad member, so every day published as good and the group read 100% where neither member was above 70%. A summary may be pessimistic; it may not claim things were fine when they were not.

An outage was reported as 72 hours

  • fail_at remembers when a run of failures began, and nothing bounded how old it could be. A process whose last round ended in one sub-threshold failure keeps that moment; three days later it returns, fails once more, crosses the threshold, and recovers a minute after that — ✅ recovered (after 72 h 1 m) for a sixty-second outage. Any restart, redeploy or missed cron in that state produces it.
  • Two failures cannot span three days at a minute a round, and the row knows how many there were. The start is never further back than the failures reach.

The phone fix stopped one breakpoint short

  • Below 481px v3.12.0 clipped nothing; from 481 to 600 it clipped up to 121 pixels of a 220-pixel timestamp. Both the event row and the day strip use 600 now.
  • The same rewrite left out the element the event row exists for: the monitor's name had no min-width, so the panel cut it — 59 pixels of a 42-character name at 320px.
  • The no-data bar's hover ignored prefers-reduced-motion: three classes beat the guard's one, so the one bar that grows threefold was the one that still moved.

Corrected

  • The upgrade notes gave one failure mode for every missing column — five surfaces returning 500. For this release's column that is false: every surface keeps answering and only the probe round throws, so the page freezes and updated_at stops moving, which is the only tell there is.
  • The migration blocks are in order; the newest sat between v3.6 and v3.9, where nobody reading downward from their own version reaches it.
  • wrangler.toml, compose.yaml and deploy/k8s/README.md — the three places an operator reads what LANG takes — still said five languages.

One word for one thing, in each of the six

  • A reader went through every string in all six languages against what the probe does. The meanings held: no language claims a cause the probe did not observe.
  • Turkish had two words for an answer and two for a notice, and three error messages in the formal second person where every button is informal. German and French each called a notice by a second name in the refusal and the aria label. Spanish said interrupción for a severity while saying caído for a state. Chinese wrote the sparkline title in English word order and used two roots for one outage.
  • The punctuation around a duration is the language's own now: 已恢复 (2小时5分钟后) rather than full-width text in half-width brackets.

The dialog fits every language

  • Measured at 360px: the four Spanish severities needed 247 pixels of the 218 there are, so the last one ran past the dialog and the dialog became horizontally scrollable. German had five pixels to spare. The row wraps now and a button is never narrower than its own word.
  • The language select was half the row by assumption, and todos los idiomas and toutes les langues were cut off inside it on every phone.

Ratchets, fourth pass

  • Seven more walk-arounds, each closed and verified by replaying it: a hardcoded "en" fallback satisfying the ?lang= rule, border-color: white invisible to the theme rule, a hex laundered through color-mix(), a two-label hostname, a top-level domain not in the list, a hostname in a CSS comment, and a Set used as a cross-request memo where the rule looked only for a Map.

Upgrading

  • A pull. The container adds v3.12's column on start; on Cloudflare the ALTER is in docs/UPGRADING.md.

v3.12.0 — a sixth language, and the numbers checked

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 09:11

A sixth language from outside, and an audit of every number the page publishes.

Simplified Chinese

  • The page, the feed, notices.json and the alerts speak zh-CN, contributed by @Whbbit1999 in #5.
  • Three things a translation cannot see from inside its own file: the code is written into <html lang="…">, where zh_CN with an underscore is not a valid BCP 47 tag; percent() gave a comma and a space to everything that is not English or Turkish; and held() put the word before the number, so 之后 — a postposition — landed in front. The duration reads 已恢复 (2小时5分钟后) now.
  • ?lang=xx, ?lang=EN, ?lang=all and ?lang=en-US all answered English on a deployment configured for Turkish. langOf takes the language to fall back to, and every caller that has one passes it.

The uptime figure was summed over ninety-one days

  • day >= today − 90 returns today and the ninety before it; the strip draws ninety marks ending today. The headline figure therefore counted a day no bar shows: it moved 0.10% for a two-hour outage falling out of the window, up to 1.10% for a whole-day one, with nothing on the page changing. history.json published 91 entries under a window_days of 90.
  • Every window was open at the top and the sweep only deletes what is old. A container that boots with a clock a day fast wrote rows nothing could expel: a day dated tomorrow summed into today's uptime (99.65% where the ninety drawn days were 100%), a check timestamped tomorrow became the latency chip at 4000 ms, and updated_at — the field a machine watches to see whether the page is frozen — read the future.

An outage was reported shorter than it was

  • since was written when the threshold was crossed, not when the failures began, so every recovery message was fail_threshold - 1 rounds short: 60 seconds at the shipped default, and held() floors to whole minutes, so a two-minute outage was announced as "1 min".

One member's bad day is not the group's

  • medianDays took the lower of two middles, so a group of two published the worse member's row — counts, timings and all — as the group's history, while the same row's state line said "partly up" because isDown refuses to call two-member trouble an outage. The history follows the state rule now: never worse than the second-worst. Two bad members make a bad day; one never does.

The waveform was a queue of readings, not a day

  • Points were placed by their index, and the store returned only the hours that had a successful probe — so a nine-hour hole, a probe loop dead all afternoon, drew as one step between adjacent pixels under a title reading "last day". There is a slot per hour now and the line breaks across the empty ones. It also returned 25 buckets for a 24-hour window.
  • percent() floored pct * 100, which in binary is 1839.9999999999998 for 18.4, so 23 of 125 probes published as 18.39%.

The page on a phone

  • v3.11.0's white-space: nowrap on an event's time was right for the two places a timestamp sits alone and wrong for the event row: the time ran past a panel that hides its overflow. At 320px in German, six of six rows clipped, 116 pixels of a 122-pixel timestamp — worse than the wrapping it replaced.
  • The same release wrapped .svc header to give the name room, which moved the uptime figure and nothing else; the pill and the latency sit inside .who and never left the name's line. Five of seven cards gained a row without gaining a character. Now the name takes the first line and as much of a second as it needs: "Documentation and developer portal (europe-west-1)" reads in full at 320px, where it showed 59 pixels.
  • A bar grows by a share of its height on hover, so the 3px no-data bar moved half a pixel; it grows by three times instead. The day gap steps 3px, 2px, 1px rather than falling off a cliff at 601px.

Ratchets, third pass

  • An agent walked around fourteen rules with bun run check at zero. The schema-order rule had already survived lower case and CREATE UNIQUE INDEX; this time an index rode the ); closing a table. The release gate fell to continue-on-error: true, then to deleting both steps and leaving their words in a comment. The theme rule could not see a colour that was not a token. The public-repository rule read hostnames only after http(s)://, missed a Telegram token stored without its bot prefix, and skipped .github/ — where a database id sat happily.
  • All closed, each verified by replaying the walk-around.

Upgrading

  • A container adds the column on start. On Cloudflare there is one ALTER in docs/UPGRADING.md.
  • A recovery message for an outage that began before this upgrade counts from what the old row knew, which is what today's code already assumed.

v3.11.0 — the history a phone could not show

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 08:19

The page was driven in a real browser for the first time. Everything below was measured there.

The ninety-day history was invisible on every phone

  • Ninety bars with a three-pixel gap need 267px of gap; the strip is 252px wide at a 360px viewport. Every bar computed to zero width, so thirteen rows of history rendered as a blank band.
  • Bars reach one pixel at a 465px viewport and two at 555px — every common phone is below both. The gap gives way below 600px now.

A day bar says which day it was in height as well as hue

  • Simulated over the palette: a reader with deuteranopia sees a good day and a bad one at 1.17:1 — two olives — and a protanope sees a good day and a degraded one at 1.00:1, the same colour. A bar is 4px wide, unlabelled, aria-hidden, so hue was the whole message.
  • The palette is unchanged. Good is the full column, degraded two thirds, bad under half.
  • A day with no data is a hairline rather than a full block: it was 1.21:1 against its own well, so a row with no history read as a solid band of something.

What a reader on a phone met

  • A service name truncated to a letter and a half — "Webhook delivery" as "We…", in German "W…" — because the pill and the latency take the width first. The name takes its own line below 480px. A status page that cannot say which service is down is not one.
  • The uptime figure broke across two lines in de, es and fr: the space before the sign was an ordinary one. It is non-breaking.
  • An event's timestamp broke into four lines at 360px.
  • The pen and the panel's icon links are 24 and 28 CSS pixels; on a coarse pointer they are 36.

Accessibility

  • .md a was colour-only until hover and 2.46:1 against the text around it in dark — a 1.4.1 failure. Underlined from the start.
  • The dialog's three fields set outline: none, leaving one pixel of border as the only focus cue. Two pixels, offset, like every other control.
  • Lighthouse: accessibility, best practices, SEO and agentic browsing all 100, desktop and mobile, 51 audits passed and none failed — none of which covered any of the above.

The design system

  • .md code was 6px inside a 20px surface where the concentric rule says 8.
  • The #lang chevron carried a hex matching neither theme's --mut; the dialog's shadow and scrim were light-theme values reused unchanged in dark. All three are tokens now, declared in the same three places every colour is — and that rule, held until now by attention alone, has a test.
  • The 2px corner on a day bar is written down as the one radius outside the two laws: it is a tick, not a surface.

Held

  • Every text pair passes AA in both themes (light floor 4.71:1, dark 5.97:1). Concentric 20/8 with squircles everywhere, capsules on every pill. Icon tiles are hue-on-tint, never white-on-solid. Under prefers-reduced-motion nothing moves at all — getAnimations() is empty and a view transition produces zero animations. No horizontal overflow at 360, 768 or 1440.

Upgrading

  • A pull. No schema change.

v3.10.1 — nothing a reader sees is a marker

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 07:43

Fixes a regression in v3.10.0 that corrupts notices. Anyone on 3.10.0 should take this.

A code span inside a link was published as the renderer's own marker

  • v3.10.0 held code spans and links out of the rules that follow them, and a replacement string is not rescanned — so a span held before a link was carried into that link's text and left there as &0&.
  • [/api/status.json](https://example.com/docs) came out as <a href="…">&0&</a> on the page, in notices.json and in the feed. The RSS title still read correctly, which made it look like corruption rather than a rule.
  • 6,210 of 30,000 realistic notice bodies differ this way from v3.9.0.
  • The tests added in v3.10.0 missed it by one case: "a link inside a code span" and "emphasis inside a link's text" were both written; a code span inside a link's text was not.

An empty ?lang= answered English on the last surface that still had the bug

  • v3.2.0 fixed this for the page and the feed. notices.json asked has("lang") — true for ?lang= with nothing after it — and fed "" to langOf, which answers English. A deployment configured for Turkish answered English notices.
  • The ratchet written for exactly this defect exempted ?? undefined, which is the shape this one had.

The sweep could take the whole database's retention with it

  • The monitor ids go into the reconciliation statement rather than into parameters, and batch() is a transaction. At 15,000 monitors D1 answers SQLITE_NOMEM, and the failure discards the retention deletes with it — measured, checks from 1970 surviving a sweep — so pruning stops permanently.
  • Five hundred ids to a statement. 20,000 monitors: 10 ms, every kind correct, retention applied.

The rule this repository would be most damaged by breaking now has a check

  • "No real hostnames, tokens, database ids, chat ids or webhook addresses anywhere" was enforced by attention alone. Sixteen distinct hosts appear in the tree, all placeholders or the project's own links; anything else fails, verified with a real-shaped hostname and a real-shaped database id.
  • An agent walked around eleven existing rules with their defects present and bun run check at zero. Closed: the schema-order rule was case-sensitive (create index in lower case put the defect back byte for byte), the release gate asked only that two strings appeared somewhere in the workflow, src/worker.ts was in no rule's file list at all, the memo inventory could not cross ??=, recentEvents(db, EVENT_ROWS * 3) passed, the endpoint table was checked one way only, and the CORS rule knew new Response but not Response.json.

latest cannot move backwards

  • Any tag published latest, {{major}} and {{major}}.{{minor}} in any order, so a v3.9.1 cut after v3.10.0 would move latest and 3 back a release under everybody who pulls them — which is what the upgrade notes tell a self-hoster to do.
  • The smoke test also now reads x-nabiz from the running container and compares it with the tag; it checked only that the container answered.

Corrected

  • "It applies schema.sql on start and then adds any missing column" was backwards, and had been since the commit that swapped the order.
  • "One statement failing takes the rest of the file with it" is true of bun:sqlite and not of wrangler d1 execute --file, which is all-or-nothing.
  • The probe's comment said both that the round's deadline gives "timed out" and that saying so is untrue. timeout means nothing answered in the time there was; a timeout longer than three quarters of the interval can never be reached, and docs/monitors.md says so where an operator sets it.

Upgrading

  • A pull. No schema change.

v3.10.0 — a copy is only true until somebody edits the original

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 07:06

Two agents went after v3.9.0 and the one path nothing had ever reviewed: what an operator writes, and everywhere it comes out.

One notice could spend a whole request's CPU

  • The link rule's [^\]]+ rescans to the end of the body from every [. At the 4,000 characters the write path allows that is 13.97 ms — more than the 10 ms a Worker request is given — and the feed pays it twice, for the title and the body, uncached.
  • 1,000 → 0.86 ms, 4,000 → 13.97 ms, 16,000 → 164.11 ms before; 0.37 ms, 1.74 ms, 9.27 ms after.
  • Nothing caps how many notices exist, and the page renders every open one.

A marker inside a URL wrote tags into the address

  • Emphasis and code ran before links, so [a](https://example.com/*x) produced href="https://example.com/<em>x" — not an escape, since quotes are escaped unconditionally, but a corrupted address and an unmatched tag.
  • A link inside a code span was a working link.
  • Code spans and links are held out of the rules that follow them, marked by a bare ampersand — the one character esc() never emits, so an operator cannot type one.
  • Control characters are dropped rather than escaped; the HTML page was the only document still carrying them.

One language's resolved notices erased every other language's

  • The resolved tail's cap was applied before the language filter rather than inside it. Thirteen resolved Turkish notices spent the whole window, and the one resolved English notice was unreachable from /?lang=en, notices.json?lang=en, the English feed, and notices.json with no language at all.

The kind on an event row went stale

  • v3.9.0 wrote events.grouped at insert and read it as the monitor's current flag. monitors.grouped is the one column nabiz never writes: an operator edits it to move a service into a group, and every event written before that edit stayed in the window the service left. Measured: a moved member's 400 events filled the named window and the named service's history disappeared — the defect the two windows exist to prevent.
  • The same gap made the migration fragile. A database that got the column but not the fill — a process killed between them, or an operator running the first of three documented statements — was stuck that way forever, because the guard is the column.
  • The hourly sweep sets the copy back to what monitors.grouped says, both directions. A half-applied migration repairs itself within an hour, and moving a monitor between groups is an edit and nothing more.

schema.sql could cost a database a table

  • v3.9.0 put an index over the new column above the notices table. Applying the file to a v1.0.0 database fails there and skips every statement below it, so the database lost the table rather than gained the index. Every index now comes after every table, and the upgrade notes say to run the D1 statements before re-applying the file.

Fixed

  • The round's deadline was published as the monitor's timeout: a host that answered 200 in 50 ms with a 60 s timeout it never came near was shown as "timed out". Only the monitor's own clock says that now.
  • oneLine stopped the C0 controls and let NEL, LINE SEPARATOR and PARAGRAPH SEPARATOR through — the forged alert line it exists to prevent, in three other characters.
  • A severity that was present but not a string was silently taken as info and answered 201; a client sending ["outage"] during an outage published an info notice and was told it worked.
  • postResolve accepted 1.5 and 1e308 as ids.
  • Three sentences the code does not keep, including one written in the same commit that falsified it.

Ratchets that can be walked around are not ratchets

  • An agent got round five of the three source-reading rules with their defects present. The memo rule guarded itself on a line the fix it was written for had changed, so it asserted nothing for a whole release — removing the warm entirely kept the suite green.
  • All five are closed, and two new rules cover the two defects above that no rule could have caught: every index after every table, and the release proving the tag, package.json and the version the code publishes agree before an image goes out. Until now the release gate was a two-line curl smoke test with no bun run check in it.

Upgrading

  • A container adds the index on start. On Cloudflare there is one statement in docs/UPGRADING.md, and it goes before any re-application of schema.sql.
  • If you ran only part of the v3.9 block, nothing to do: the first sweep after this upgrade repairs it.

v3.9.0 — a window that stops where it says it stops

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 06:26

Fixes six defects in v3.8.0's own code, found by measuring it. Anyone on 3.8.0 should take this.

The event read walked the whole table

  • Each of the two windows is LIMIT 400, and a LIMIT only short-circuits if its window can fill. The kind lived on monitors, so finding it needed a join, and a deployment with no grouped monitors — the default — could never fill the grouped side.
  • Every request to the page, status.json and the feed scanned every event ever kept: 0.97 ms at 1,000 events, 6.96 ms at 50,000, 24.81 ms at 200,000. On D1 that is rows_read — about a dozen page views a day against the free tier's 5M.
  • The kind is on the event row now, with an index over (grouped, at): 0.24 ms at every size, and the plan says SEARCH rather than SCAN.
  • A monitor written with grouped = 2 matched neither window and its history vanished from the page, the JSON and the feed. groupOf reads any non-zero as grouped; the windows now agree with it.

The body cap was set by the runtime, not by the constant

  • Each chunk was enqueued before the total was checked, so the read overshot by one chunk: 112,561 bytes on undici, 69,489 on workerd, against a 65,536 cap. The chunk is cut now.

Latency measured the body

  • An origin whose headers arrive in 3 ms and whose body takes a second reported 937 ms. That number is latency_ms, the 24h sparkline and days.ms_sum, so every deployment's chart took a permanent step at the v3.8.0 upgrade with nothing said about it.
  • It is time to the answer again. The body is still read; what reading it costs is not charged to the number you chart.

timeout had become unreachable

  • The reason asked whether a status had arrived before it asked whether our own signal had fired, so a monitor that blew its timeout while the body trickled reported "answered, then stopped". Our deadline is ours: blaming the host for a budget it never saw is not an answer.

Tests for the features 3.8.0 was named for

  • The cap, incomplete, the two windows and a grouped value that is not 1 had no test. They do now, each verified by making the fix fail.
  • Three ratchets read the source for the mechanisms behind five releases of defects: a claim in prose the code does not keep, a rule kept on one surface and not its sibling, a remembered value whose invalidation misses a writer.

Also

  • /health carries x-nabiz, the build answering. Nothing a deployment served said which version it was.
  • Telegram followed redirects; only the webhook had been fixed. A 3xx makes the POST a bodyless GET, answers 200, and the alert is gone.
  • The 401 and 429 from a write carry CORS like the 201 and 400 beside them.
  • The round warms the shaping, not only the database read — the first reader after each round was still paying a cost proportional to a group's hidden members.
  • llms.txt still called the JSON, the feed and the badge uncached, three releases after they began reading through a held page.
  • docs/api.md counted six endpoints above a table of seven, and /robots.txt was in neither.

Upgrading

  • A container adds the column, fills it and creates the index on start, and says so. On Cloudflare there are three statements in docs/UPGRADING.md — run in the order given.
  • Check expect_body before pulling if it matches something late in a large page. Since v3.8.0 only the first 64 KB is read, so a monitor watching a footer goes down at the first round after the upgrade, with an alert, while the host is fine. The one-liner to check is in the upgrade notes.

v3.8.0 — silence is not health

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 05:54

A host that answers and then stops is not up

  • With expect_body unset the body was never read, so a host that sent 200 OK and never wrote a byte was published as up, 3 ms — and that 3 ms went into the waveform and the daily average.
  • The body is always read now, first 64 KB only: 512 MB served inside a two-second timeout took the process from 78 MB to 603 MB before, 7 MB after. On Cloudflare that was one monitored site killing the isolate that serves the page and runs the cron.
  • A failure arriving after the status has its own word — incomplete, "answered, then stopped" — and keeps the code it was answered with.

A redirecting webhook lost every alert and reported success

  • fetch follows redirects, so an http:// webhook behind a redirect to https became a bodyless GET, got a 200, and counted it delivered. The probe has refused redirects since the first commit; the alert path did the opposite.
  • A lost alert now says what was lost: alert() returned how many channels refused and tick() threw it away.
  • A hanging channel skipped whole probe rounds — 10 seconds whatever the interval. Each channel gets a quarter of the round's budget.
  • The payload gained at, held_for and group, and is documented in configuration, including that it names a grouped member and why.
  • A newline in a monitor's name forged a line — an invented recovery inside an outage alert.

A group never publishes one customer's outage as its own

  • Half of a group of two is one customer. Down needs half and at least two; a group of one says troubled and never down.
  • 400 hidden members writing an event each filled the whole event window in one round, erasing every named service's history from the page, the JSON and the feed. Grouped and named events get a window each.

The timing channel, the other half

  • v3.7.0 made a cache hit flat and left the miss proportional to the hidden members: 5 ms at 2, 284 ms at 400, ranges not overlapping, samplable four times a minute.
  • A server holds until its next round, and the round rebuilds the page itself. After: 1.34 ms and 1.39 ms.

Also

  • status.json published whatever it found in the reason column while the HTML checked it against the words it documents.
  • German and Spanish called a monitor "not reachable" and "no response" for every kind of failure, including one that answered 500.
  • An outage under a minute reported "after 0m"; it says seconds, and every language but English gets a space before its unit.

Upgrading

  • A pull. No schema change.

v3.7.0 — which kind of red

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 04:18

The member count was readable from response timing

  • The 15s cache held the database read but not the shaping, and shaping a group costs a sort per day per hidden member.
  • /badge.svg — 504 bytes, no-store, no authentication — took 0.36 ms at 2 hidden members and 9.36 ms at 400. Four hundred published monitors cost 0.36 ms; the signal was specific to the hidden ones.
  • Rows are shaped once per read. After: 0.218 ms at 2, 0.217 ms at 400.

A red row says which kind of red it is

  • Nothing answered in time: no answer → timed out.
  • Refused, no such name, bad handshake: no answer → could not be reached.
  • Promised status, wrong words: HTTP 200 → answered, but not with what was promised. A red row whose stated reason was a success code.
  • In five languages, in status.json as reason beside last_status, and in an aria-label — it used to be a title and nothing else.
  • A group still says nothing of the sort.

Also fixed

  • A read that began before a probe round could be remembered after it — two of four rounds on Cloudflare.
  • The feed's <link> carried the Host header unescaped; a malformed host cost the whole feed.
  • RSS items for state changes had no description.
  • The documented compose upgrade disarmed the token: ADMIN_TOKEN read from the shell meant docker compose up -d set it empty — the page kept serving, every write refused, no other symptom. It belongs in a .env.
  • The documented backup is chmod 600; the live file is 0600 because it holds the URLs of everything you watch.
  • docs/api.md said a monitor could be degraded, never gave history.json a shape, and described a markdown subset without saying which.
  • The Turkish word for down said "does not answer" — a cause the probe has not observed when the answer was a 500.

Upgrading

  • A pull. The container adds the column it needs; on Cloudflare there is one ALTER in docs/UPGRADING.md.

v3.6.1 — the cache that forgot on the wrong map

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 03:19

Fixes the page cache shipped in v3.6.0. Anyone on 3.6.0 should take this.

The round and the render remembered different things

  • src/lib/store.ts is instantiated twice in a server process — once in the Astro bundle that renders, once from source in the process that probes. Invalidation deleted from one copy while the page answered from the other.
  • The page, status.json, history.json, the badge and the feed lagged the last round by up to 15 seconds always, not occasionally.
  • The events list is not cached and is read in the same request, so one document contradicted itself: a row reading "down · HTTP 500" above an event list saying it recovered 16 seconds earlier. 11,665 of 24,064 responses.
  • Fixed by pinning what the page remembers across both copies, as src/server/env.ts already does for the database handle. Verified: updated_at and the newest row agree to the millisecond.
  • Invalidation also ran before the round's writes rather than after, so a render landing mid-round could cache half a round for the whole window — two of four rounds poisoned on Cloudflare.

A group has no answer of its own to give

  • last_status: null means "nothing answered", and group rows published it about hosts that had answered 301 and 503. The field is absent for a group now.

Upgrading

  • A pull. No schema or configuration change.

v3.6.0 — it upgrades itself, and it holds

Choose a tag to compare

@productdevbook productdevbook released this 20 Aug 02:28

A database from an older version upgrades itself

  • CREATE TABLE IF NOT EXISTS can never add a column, so no column nabiz has ever added reached a self-hosted deployment — the upgrade notes said otherwise.
  • From a real v1.0.0 database: the process starts, prints success, /health answers 204 forever, every page returns 500 with no such column: last_status, each round writes checks then throws before the state — alerts stop, pruning never runs.
  • The server adds what schema.sql cannot reach and says which:
    [nabiz] added monitors.expect_body, monitors.fail_threshold, state.fails, state.last_status
    
  • Verified from v1.0.0, v1.1.0, v2.0.0, v3.0.0 and v3.4.0 schemas: each starts, keeps its history, serves the page.
  • Cloudflare has no start to do this at; re-running --file schema.sql there is not an upgrade, and the notes say what skipping the ALTERs looks like.

The page holds for the same 15 seconds the edge holds it

  • Measured on 40 monitors with 90 days of history: /health served 10,850 req/s, everything reading the database served 49 — forPage alone 21 of 25 ms.
  • / at 50 concurrent: 51 req/s, p50 1222 ms → 321 req/s, p50 158 ms.
  • /api/status.json at 50 concurrent: 49 req/s, p50 211 ms → 3,477 req/s, p50 14 ms.
  • The window is less than the time between two probes, and every write drops it: a notice is on the page the moment it reloads.
  • Probe cadence held to within 200 ms under load; memory oscillated 115–200 MB rather than climbing. The Kubernetes limit is 384Mi, with the measurement beside it.

Upgrading

  • A pull. The container adds anything your database is missing and says so.