Skip to content

Releases: riveerxd/nightbell

Nightbell 3.0.2 — the cutout on every surface, and a cold start that rings

Choose a tag to compare

@riveerxd riveerxd released this 10 Aug 12:42

The icon is finally the same icon everywhere, and a cold start now rings.

Fixed: two surfaces had no heartbeat

The widget header and the status-bar glyph had shipped as solid bells since 3.0.0, while every other surface carried the trace knocked out of it. That is exactly why they looked wrong on a home screen: they disagreed with the launcher icon sitting next to them.

The reason I had given for it was wrong rather than merely cautious. The claim was that at 18dp the slot is "under two pixels" — that was the dp figure read as pixels:

1x 2x 3x 4x
widget header (18dp) 1.0 px 2.0 px 3.0 px 4.0 px
status bar (24dp) 1.3 px 2.7 px 4.0 px 5.4 px

All six drawables carry the trace now. The two 24dp canvases widen it by a third so the hole survives a 1x density and the antialiasing of a very small render.

docs/brand/verify-mark.png is the check, and it is the artefact that would have caught this a release earlier: thirteen surfaces in one image, where two solid bells among eleven carved ones are obvious at a glance.

Added: the cold-start animation

The bell arrives, rings, and the word wipes in beside it, over 2.8 s, on cold start only. It draws the same Compose mark the dashboard header uses, so it cannot drift from the icon it is introducing.

Three details worth knowing, because all three only showed up on a screen recording rather than in the code:

  • Android draws its own splash first from API 31, and the default puts the launcher icon on a white circle. The splash window is now themed with the app's void and its own mark, no plate, so the platform's screen is the first frame of ours instead of a competing one.
  • The bell used to jump left at the handoff, because the system centres the icon and the animation centres bell and word. It now starts where the platform left it and drifts into place as the word arrives.
  • The last 0.7 s is the finished lockup sitting still. That hold is the point, not padding: an opening that dissolves the instant it resolves reads as a glitch.

Skipped entirely when the system says animations are off, and it does not replay when you rotate the device.

Updating

Same signing key as 3.0.1, so 3.0.1 updates in place. Coming from 3.0.0 still needs an uninstall first, because 3.0.1 changed the key; coming from Pulse 2.x still needs export/import, because 3.0.0 moved the package. See docs/MIGRATION_3.0.0.md.

Verified

359 JVM tests and 103 on-device tests green. minSdk 26 (Android 8.0), targetSdk 36.

Nightbell-3.0.2-release.apk · 2,160,440 bytes
sha256 6a5a2a858ac3e245657b7c6a284596626a78f58eddf749f3af550f099be11115

SHA-256 20:d8:ab:da:a8:41:6a:9a:75:1e:3e:a1:44:ef:15:23:d7:dd:ba:ae:ee:9e:c6:be:01:d6:3a:65:57:4a:70:de

Nightbell 3.0.1 — new signing key, and the certificate says Nightbell

Choose a tag to compare

@riveerxd riveerxd released this 09 Aug 14:12

A signing-key change, and three things that were wrong in 3.0.0.

Read this if you installed 3.0.0

3.0.0 cannot update to 3.0.1. 3.0.0 was still signed by a key whose certificate subject read CN=Pulse Monitor, and a certificate subject cannot be edited without issuing a new certificate. So the key is new:

CN=Nightbell, OU=river, O=river, L=Prague, C=CZ
SHA-256  20:d8:ab:da:a8:41:6a:9a:75:1e:3e:a1:44:ef:15:23:d7:dd:ba:ae:ee:9e:c6:be:01:d6:3a:65:57:4a:70:de

Android refuses an update signed by a different key than the installed build, so a 3.0.0 install has to be uninstalled before 3.0.1 will install. Export your monitors from Settings first if you had already imported them.

This was done one day after 3.0.0 shipped, deliberately, because the cost of re-keying only ever grows. 3.0.1 onward updates in place as normal. The old key is archived and still verifies every release up to and including 3.0.0; it is never used again.

Coming from Pulse 2.x is unchanged and still needs export/import, since the application id moved in 3.0.0. See docs/MIGRATION_3.0.0.md.

Fixed

  • The promo video clipped the "g". The title card masks the wordmark on, and mask-image clips to the element's box; at line-height: 1 that box was exactly the font size, so the descender fell outside the mask and was shaved off flat for the whole scene. "Pulse" has no descender, which is why it only appeared after the rename.
  • The promo video advertised 2.5.0, a version that was never released. It now reads the shipped version.
  • The site's update FAQ was wrong. It claimed everything from 2.0.0 installs over the top, which had not been true since 3.0.0 moved the package.

Added

  • A Star on GitHub button in the hero, and the other two lost their articles so all three fit one row: "Download APK", "Read source", "Star on GitHub". It points at the repository root rather than /stargazers, which 404s for a signed-out visitor.
  • Every external link on the site opens in a new tab.

Verified

359 JVM tests and 105 on-device tests green. minSdk 26 (Android 8.0), targetSdk 36. No app behaviour changed in this release.

Nightbell-3.0.1-release.apk · 2,157,468 bytes
sha256 db1e7d79fcba532889fd27e89e13c0f9f9dc7a869009d44dacb96011f53ea21f

Nightbell 3.0.0 — new name, new icon, new package

Choose a tag to compare

@riveerxd riveerxd released this 08 Aug 21:49

Pulse is now Nightbell, and the icon finally matches the name: a bell with the heartbeat trace knocked out of it. The trace is the same six points the old mark was drawn from, scaled and centred inside the bell rather than redrawn, so the old identity is still in there.

Read this before you install

3.0.0 does not update a 2.x install. The application id moved with the name, from me.river.pulse to me.river.nightbell. Android identifies an app by that id, so 3.0.0 installs beside Pulse with an empty data directory. No signing key or manifest setting changes that.

To carry your monitors across:

  1. In Pulse 2.4.3, Settings → Export. It writes one JSON file.
  2. Install Nightbell 3.0.0.
  3. In Nightbell, Settings → Import, and pick that file.
  4. Check the monitors arrived, then uninstall Pulse.

Two things do not come across. Placed widgets need removing and re-placing, because a launcher stores a widget provider as a fully-qualified ComponentName. Notification channel settings and the four grants belong to the old package, so Nightbell asks for them again on first launch.

Backups written by any 2.x build import fine. The reader validates the JSON envelope, never the filename, so a file still called pulse-backup-….json is read exactly as it always was.

Full detail: docs/MIGRATION_3.0.0.md

Why the name changed

Pulse was unwinnable as a search term. Pulse Pager and Pulse UpTime both ship uptime monitoring under the same word, with two more products alongside them, so searching for this app returned four competitors first. Nightbell collides with nothing in software.

The mark

The cutout is a real hole rather than a dark line painted over the bell, which matters in the two places Android tints the icon flat from its alpha channel: the themed launcher icon and the status-bar glyph. A painted-on hole would vanish there and leave a solid bell. At 18dp the slot is under two pixels, so the widget header and the notification icon use the solid bell on purpose.

Also in this release

  • Persisted identifiers moved to nightbell.* alongside the package: DataStore names, notification channel ids, WorkManager unique names, the backup filename prefix, and the nightbell:// deep links. Safe to do here precisely because a new package has no live install to orphan.
  • The baseline profile was renaming-sensitive in a way that fails silently: 105 of its rules still named the old package, which does not break a build, it just stops matching and takes startup speed with it. Fixed.
  • MainActivity was still checking for the pulse URI scheme after the manifest and widget had moved to nightbell://, which would have broken every widget row tap. Fixed.
  • The thirty Pulse-era logo directions are archived unchanged under docs/brand/archive/pulse-directions/.

Verified

359 JVM tests and 105 on-device tests green on the new package. minSdk 26 (Android 8.0), targetSdk 36.

Nightbell-3.0.0-release.apk · 2,157,468 bytes
sha256 4b9005869c2f7a05c609aed12555c0b63c4766fd4270d89a0ceb20a2764bcadf

Signed with the same key as every release since 2.0.0:
SHA-256 c4:c1:19:2f:06:10:be:6b:cc:8f:22:33:f7:a9:4b:06:4a:fa:40:19:7c:3b:60:8c:ca:4d:25:dc:09:66:6a:96

The certificate subject still reads CN=Pulse Monitor. A DN cannot be edited without issuing a new certificate, and a new certificate would break updates, so it stays.

2.4.3 — blue pulse mark, cleaner urgent page, pager survives the power button

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:20

Pulse 2.4.3

The mark loses its ring. The heartbeat trace is the whole mark now, in the brand blue, everywhere it appears — launcher icon, dashboard header, widget mark, and the status-bar glyph — all regenerated from one geometry so the copies can't drift. Red still means failure in the data (charts, history strip, status orbs); it's just no longer the logo.

Status-bar icon is the trace alone — a ring at 24dp collapses to a blob under the status-bar mask, so the heartbeat reads on its own.

Strict-monitoring card gains the system chronometer counting down to the next check and a start-icon label for the window the line reaches back over. The "Stop strict mode" button is gone — strict mode is turned off in Settings, so a control on a permanent notice was clutter, not a shortcut.

Urgent page action pills are re-coloured and carry a glyph each: ✓ Acknowledge on green, ↻ Re-check on the brand blue, and a crossed bell on white for mute. They're hidden when the card is collapsed — the collapsed row is height-capped by the shade and used to clip the pills into an unreadable strip, so the collapsed card now carries only the verdict.

Fixed

  • The urgent alarm went silent when the screen turned off. The system cancels an ongoing vibration on screen-off (the vibrator history logs it as cancelled_by_screen_off), so pressing the power button silenced a page set to vibrate — the one thing a pager must never allow by accident. A vibration re-issued while the screen is already off runs normally, so a screen-off receiver restarts it and it keeps buzzing until the page is acknowledged.

359 JVM tests and 173 on-device. Sideload the signed APK below; it's signed with a personal key, so Play Protect will ask you to confirm. Anything from 2.0.0 onward updates in place and keeps your monitors.

Pulse 2.4.2

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:19

The wait until the next check, written on the line.

Updates in place from any 2.x and keeps your monitors.

What's new

The grey tail already showed how much of the wait was left, by its width. Now it says so
in words at the end of the line — 15m, 4m, now — driven by the same
value the tail's width comes from, so the number and the grey cannot disagree.

ProgressStyle carries no text: a segment takes a colour, a point takes a colour, and
that is the entire vocabulary. It does take an icon at each end, and an icon is a bitmap,
so the label is drawn into one.

Three things about that icon slot, found on a device

It centre-crops to a square. A bitmap sized to its own text gets trimmed from both
ends — 1h20m rendered as h20, now as how. The canvas is square now and the text
is fitted into it, so a short label renders larger than a long one.

It renders in full colour, not the alpha mask a status-bar icon gets. Established by
posting a bitmap of red/green/blue stripes into it and getting three stripes back.

Filled glyphs, not knocked-out ones. The first attempt punched the text out of a
pill, which put the card behind it — a colour the app neither chooses nor can measure —
on one side of the contrast ratio. It measured 2.5:1, below the floor for a graphic
and nowhere near the floor for text. It shipped, and was reported as unreadable, which it
was.

No container

Dropping the pill bought back its margin, so the glyphs draw larger — which is what
legibility actually turned on. With nothing between the text and the shade, the ink now
follows the system theme exactly as the line's colours do: dark slate on a light shade,
light grey on a dark one. Both checked at actual size in both themes, rather than at
zoom, which is how the unreadable version got through the first time.

Known limit

A colourised card is pinned to the app's near-black whatever the system theme is, so on
a light-themed phone whose card wins promotion the ink resolves the wrong way. The same
inversion already affects the segment colours. Fixing it means deciding colourisation
before the style is built rather than after — a larger change than this one.

Install

adb install -r Pulse-2.4.2-release.apk

Signed with my own key, so Play Protect will ask you to confirm. There is no Play
listing.

358 JVM tests and 173 on-device tests. One on-device test, theServiceNotificationBecomesThePage,
fails in a full-suite run and passes in isolation — a race on the shared notification id
between the urgent page and the strict notice. Confirmed pre-existing by running the same
suite against 2.4.1.

Pulse 2.4.1

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:19

A maintenance release for the strict-monitoring card on Android 16 — four bugs in
the live line, all reported from a device and none of them visible from reading the
code.

Updates in place from any 2.x and keeps your monitors.

The line no longer opens with grey

The window was measured from the oldest retained check of any age and then clamped
to a day — so a single check older than that stretched the line to a full day and was
then skipped for falling before the window's own start. Everything between the left
edge and the first drawable check stayed blank, under a label claiming "last 24h".

The label now describes the line actually drawn: last 16h 23m rather than a day that
was two thirds empty.

The tracker moves

It could not before. The tail returned the shortest interval in the fleet — a
constant that read neither the clock nor the last check — and because the elapsed part
of the bar is always exactly 48 buckets, a fixed tail pins the tracker with it.

Drawing the real countdown to scale does not help either: a bucket is the span over 48,
so on fifteen hours of history a bucket is nearly nineteen minutes and a fifteen-minute
countdown floors to the same value full and empty alike. The tail now spends a fixed
sixth of the bar and empties across it as the next check falls due.

It is also paced by one monitor — the fastest — rather than by whichever is due
soonest. With eight monitors staggered on a fifteen-minute interval something is always
nearly due, so the old value sat near its floor and reset every couple of minutes: it
was moving, in a sawtooth indistinguishable from stuck.

Measured on the reporter's phone across nine minutes: tail 5 buckets → 2, tracker
90.2% → 95.7%.

Compressing a busy line no longer invents outages

Capping the number of segments used to absorb the shortest non-outage band into a
neighbour — and the neighbours of an up-band are outages by construction, so the
absorbed uptime could only ever be handed to an outage, and the two red runs were then
fused into one. Every merge turned uptime into downtime.

Measured on a monitor that alternated pass/fail every half hour for a day: 24 buckets
genuinely failed, the line drew 40, and the longest drawn run was 33
— a claimed
sixteen and a half hours of continuous downtime that never happened.

The drawn red total now matches the real one exactly.

A live outage can no longer be painted over

Carry-forward ran on the fleet-merged tone, so a monitor checked every five minutes
filled every bucket after an hourly monitor's failure with green — the line drew green
all the way to the right edge, under a red tracker and a "1 DOWN" chip. Each monitor
now carries its own last verdict before the fleet merge.

Reading the line

What you see What it means
Green / red stretch how long that outcome lasted — to scale
Taller red block where an outage began — so a single failed check is findable
Grey tail past the dot the wait until the next check

Repository fix

2.4.0 shipped LiveCard, LiveTimeline, their tests and the dark-scheme colours in
its APK but left those six files untracked, so the v2.4.0 tag does not compile. They
are committed here. Nothing else was missing, and the published 2.4.0 APK itself was
fine.

Install

adb install -r Pulse-2.4.1-release.apk

Signed with my own key, so Play Protect will ask you to confirm. There is no Play
listing.

355 JVM tests and 173 on-device tests, all passing.

Pulse 2.4.0

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:19

A new identity, an app icon that is no longer a black square, and a widget that
uses the space you give it.

Updates in place from any 2.x build and keeps your monitors.

A new mark

A blue ring with a red trace cutting through it, replacing the gradient
cardiogram. It shows up in the launcher icon, the widget header, the dashboard
header, the widget's configuration preview, and the ongoing strict-mode
notification — which used to carry a refresh arrow and so read as a monitor being
re-checked when it never was.

The ring is blue because green means something specific now: blue is the app,
green is the thing it measures, red is the moment it breaks.

Colour means something

Charts no longer take their colour from the per-monitor accent. The sparkline, the
history strip under it and the detail screen's latency bars are all operational
green
, and still bleed to red at a failed check. Everything that is chrome — buttons,
chips, badges, headers — stays blue.

The app icon has no black plate

The mark now sits on transparency instead of a dark square.

Worth knowing why it took a rewrite rather than a colour change:
AdaptiveIconDrawable.draw() fills its layer bitmap with Color.BLACK before it
composites its layers, so an adaptive icon with a transparent <background> renders
solid black — in the launcher and in Settings' app-info screen. It is not a
launcher bug and not a stale icon cache. Android has no fully transparent app icon;
a legacy icon is the closest, and where a launcher insists on a plate you now get a
light one instead of a black one.

The ring is also two arcs with a real gap now, rather than a full circle with the gap
faked by a stroke painted in the plate colour — which is what made the plate
impossible to remove in the first place.

Widget: a header you can strip down

The mark, the app name, the fleet summary ("1 of 6 is down") and the settings cog each
switch off on their own. They used to be a single flag, so the only way to drop the
summary was to lose the branding with it.

Widgets already on your home screen keep their current look — old configurations are
migrated.

Widget: columns

A widget dragged flat has spare width and no height. It used to respond by pushing
monitors below the fold and counting them in "+3 more". Now they spill sideways.

Columns are chosen from the size your launcher reports, and three rules keep it
readable:

  • Width always caps the column count — no number of monitors justifies a column
    too narrow to read a name in, so a fixed count of 3 still collapses on a narrow
    widget.
  • Below about 150dp per column the trailing latency is dropped, so names stop
    truncating to "Market…". The dot already carries health and the number is one tap
    away.
  • A widget too short for both a footer and a monitor drops the footer, rather than
    clipping both.

Set Columns: Auto or pin it to 1–3 in the widget's settings.

Under the hood

  • All five copies of the mark are generated from the brand drawing by
    docs/brand/android_assets.py, after a hand-scaled set shipped a Compose mark whose
    ring gap was half the width of the vectors'.
  • Thirty explored logo directions are in docs/brand, with logos.html as a contact
    sheet.
  • 323 JVM tests and 157 on-device tests, up from 299 and 146. The new ones cover
    the widget's column arithmetic — which decides whether a monitor is visible at all —
    and the launcher icon's transparency, both of which are invisible to code review.

Install

adb install -r Pulse-2.4.0-release.apk

Signed with my own key, so Play Protect will ask you to confirm. There is no Play
listing. 1.x used a different application id — the only way across is the JSON export
in Settings.

Pulse 2.3.0

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:19

Certificate warnings, a light theme, and a dashboard you can search, filter and arrange.

Installs over 2.2.x and keeps your monitors.

New

TLS certificate expiry. Every HTTPS check already completes a handshake, and the handshake carries the expiry date — so watching it costs nothing and catches the one outage you can see coming. Warns at 14 days by default, escalates at 2, and says something once per threshold and then at most once a day. Never urgent, never during quiet hours: there is nothing to do at 3am about a certificate with nine days left. A renewal clears the notice on the next check.

A light theme. Not an inversion — the glass system draws depth as white at low alpha over black, which means nothing on a light surface. Every one of those overlays is now scheme-aware, and the status hues were re-picked rather than reused: mint #2FD98A has a contrast ratio of 1.9:1 on white, so light gets #07834B. Settings → Appearance offers System, Dark or Light.

Search, filter and sort. Behind two header icons rather than a permanent panel. Filter by problems, healthy, paused or unacknowledged; sort worst-first, by name, slowest, most or least recently checked. When a filter is hiding monitors a strip says so and offers a way out.

Drag to reorder. Pick "My order" and a grip appears on every card. It is its own sort mode because dragging under a worst-first ranking would arrange something the next completed check undoes. TalkBack users get Move up / Move down instead of a drag.

Bulk actions. Long-press a card to select several, then pause, resume, mute or delete them together.

Templates and in-app help. The empty state offers four starting points that pre-fill the wizard instead of dropping you into a blank four-step form. Settings has answers to the nine things people ask about most, including the deliberately surprising ones.

Tablet and landscape. The dashboard grows columns instead of stretching cards; everything else clamps to a readable measure.

Fixed

  • System Back in the setup wizard threw away the whole draft, including page elements that cost a load each to capture. It steps the wizard now, and asks before discarding.
  • Relative times and mute windows were frozen. A row could sit on "just now" for a full fifteen-minute interval, and an expired mute kept its amber rim until something unrelated redrew the card.
  • "93% UPTIME" was not uptime. It was passing-checks over retained-checks, over a span that varied with the check interval. It now reports a real 24 hours, or says how far back it can actually see.
  • Auto Backup was uploading your API tokens. The monitor store includes request headers, and Android's cloud backup was shipping it to Google Drive. Off now — Settings → Backup and transfer is the only way across, and it scrubs alert state on purpose.
  • Touch targets were 34dp throughout. The hit areas are 48dp; the visuals are unchanged.
  • The system "Remove animations" setting was ignored. Honoured now, and it genuinely stops: measured at 1.25M pixels of movement over five seconds with animations on, and exactly zero with the setting off.
  • Charts speak their values to a screen reader instead of announcing "Response time trend". The detail screen can show every stored check rather than 24 of them. Page-element monitors show the text they last saw.

Tests

299 JVM + 146 on-device, all green. Every screen was driven on an emulator in both schemes; six of the fixes above were found by looking at the result rather than by reading the code.

Sideloaded and signed with my own key, so Play Protect will ask you to confirm.

Pulse 2.2.2

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:19

Acknowledging is instant instead of taking up to a minute.

Reported from real use: tapping Ack on the page left the phone vibrating for about thirty seconds.

The ack itself was never slow. It cancelled the notification and persisted the state immediately. But the alarm is stopped by the service loop, and after posting a page that loop slept for between fifteen and sixty seconds. So the phone kept buzzing until the loop woke on its own schedule. The same sleep held the red card on screen.

The sleep is interruptible now, the alarm is a single shared instance so an ack can silence the player that is actually running (there were two), and both are driven from one place. Acknowledge, Mute 1h and recovery are all felt at once.

The regression test asserts the page stops within two seconds. I reverted the fix and re-ran it first to check it actually bites: it times out against the old code.

Installing

adb install -r Pulse-2.2.2-release.apk

Updates any other 2.x in place and keeps your monitors. Signed with a personal key, so Play Protect asks you to confirm.

Pulse 2.2.1

Choose a tag to compare

@riveerxd riveerxd released this 07 Aug 21:19

Fixes the setup screen button, which built its label by lowercasing the row title and so read "Set up get through do not disturb" on the Do Not Disturb step.

Each step has its own wording now, and it says whether the tap opens a dialog ("Allow ...") or leaves for Settings ("Open ..."), which is the difference between one tap and a round trip.

Installing

adb install -r Pulse-2.2.1-release.apk

Updates any other 2.x in place and keeps your monitors. Signed with a personal key, so Play Protect asks you to confirm.