Skip to content

Repository files navigation

Nightbell — the dashboard worst-first, an urgent alert, and a monitor detail

Uptime monitoring that lives on your phone and actually wakes you up.

Android Kotlin Jetpack Compose minSdk 26 targetSdk 36 Release 3.0.2 Tests Apache 2.0

Download APK   Docs   Report a bug

Why  ·  What it watches  ·  Alerts  ·  Widget  ·  How it works  ·  Install

Point it at anything that answers over HTTP, or at one element on a rendered web page, and it watches it, charts it, and gets loud when it breaks. No server, no account, no third party — the phone in your pocket does the checking.


Why

I kept missing outages. The monitoring services I tried either wanted a subscription to text me, or sent a notification that looked exactly like a marketing email and got swiped away with one. I wanted the thing that tells me production is down to be impossible to confuse with anything else.

So urgent alerts in Nightbell arrive as a red card that behaves like an incoming call, with a looping alarm that keeps going until you acknowledge it, and it wakes the screen if the phone is locked.

The urgent alert: a red heads-up with Acknowledge, Re-check and Mute

Note

Three buttons, because at 3am you want to decide one thing: Ack silences it, Re-check runs the check again there and then, and the crossed bell mutes that monitor for an hour.


What it watches

Kind What it does
Status check Hit a URL, assert on the status code. Exact, any 2xx, a range, or any response at all.
Request and response Pick the method, set headers and a body, then assert on what comes back. Contains, does not contain, equals, regex, or a JSON field by path.
Page element Loads the real page in an embedded WebView and watches DOM nodes you picked by tapping them. Existence, text, or an attribute.

Page-element monitors watch any number of nodes per page and resolve them all against one page load, so watching six costs about what watching one costs. The expensive part is booting the WebView, not the assertions.

Setting one up is a four-step wizard — pick a kind, point it somewhere, say what "healthy" means, and choose a cadence. Everything is changeable later.

The new-monitor wizard: What to watch, Target, Expectations, Cadence and alerts

Getting the alerts to actually arrive

This is the part every phone monitoring app gets wrong, so it is worth being specific about.

Warning

Android will happily let an app think it is alerting you while delivering nothing. Nightbell needs four grants, and there is a screen on first launch that walks you through them.

The first-launch setup screen: notifications, unrestricted battery, full-screen alerts, and Do Not Disturb access
Grant What breaks without it
Notifications Nothing is posted at all.
Unrestricted battery Android can refuse the service that owns the page and its repeat loop.
Full-screen alerts A page on a locked phone cannot wake the screen. It waits on the lockscreen.
Do Not Disturb access Bedtime mode silences urgent pages completely, which is exactly when you want one.

Two of those show a dialog in place. The other two are Special App Access toggles, and Android exposes no API to request them, so the screen deep-links straight to each toggle and re-checks when you come back. It cannot be one tap and it does not pretend to be. It is four taps instead of hunting three settings sections.

Once they are granted, the alert policy is yours to shape — what counts as down, how it sounds, the haptic pattern, how hard it escalates, and when to stay quiet.

Settings: the default alert policy, haptic styles, escalation and quiet hours

Tip

Urgent pages follow your ringer switch by default — vibrate mode gets haptics only. There is a setting to override that if you want a pager that answers to nothing.


The home-screen widget

Worst monitor first, tap a row to open it, tap the cog to reconfigure it. Every piece of the header switches off independently — the mark, the word Nightbell, the "1 of 6 is down" summary, the cog — because "make it clean" means different things to different people, and one flag for all four meant losing the summary to keep the branding.

Monitors flow into columns. A widget dragged flat has spare width and no height, so instead of pushing monitors below the fold and counting them in "+4 more", they move sideways:

The widget tall in one column, spilling into two columns, and flat in a single wide row

Columns are chosen from the size the launcher reports, capped by width — no number of monitors justifies a column too narrow to read a name in. Below about 150dp per column the trailing latency is dropped so the name keeps its room, and a widget too short for a footer and a monitor drops the footer rather than clipping both. Columns: Auto in the widget settings, or pin it to 1–3.


How it works

One CheckEngine runs a check, folds the result into persisted state, and decides whether to interrupt you. Four alert tracks come off that — deliberately separate, so a bug in Nightbell never gets reported as your website being down.

flowchart LR
    M["Monitors<br/>HTTP · request · page element"] --> E["CheckEngine"]
    E -->|folds each result| DB[("State<br/>one JSON doc")]
    E --> A1["Down and recovery"]
    E --> A2["Degraded on latency"]
    E --> A3["Urgent paging"]
    E --> A4["Checker health"]
    A3 --> PG["Full-screen red page<br/>looping alarm until you ack"]
    style A3 fill:#2F6BFF,stroke:#2F6BFF,color:#ffffff
    style PG fill:#FF4D57,stroke:#FF4D57,color:#ffffff
    style DB fill:#11151f,stroke:#2F6BFF,color:#ffffff
Loading

State is one JSON document in DataStore, which makes the whole store trivially exportable. Checks run on WorkManager by default, or on a foreground service if you turn on strict mode and want a cadence Android will not batch away.

The longer version

docs/reference.md has the full account — the four tracks and why the checker's health is one of them, count-based sample retention, the certificate track, the light-theme re-pick, and the reasoning behind the awkward parts. It is the file to read before changing anything load-bearing.


Install

Grab the APK from Releases, or from artifacts/ in this repo, and sideload it.

adb install -r artifacts/Nightbell-3.0.2-release.apk

Important

3.0.0 does not update a 2.x install. The app was called Pulse until 3.0.0 and its 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 the old one with an empty data directory. No signing key or manifest setting changes that.

To carry your monitors across: export the JSON from Settings in 2.4.3, install 3.0.0, import it, then uninstall Pulse. Placed widgets and your notification channel settings do not survive, because a launcher stores a widget provider as a fully-qualified ComponentName and channel grants belong to the old package. Full detail in docs/MIGRATION_3.0.0.md.

It is signed with my own key, so Play Protect will ask you to confirm — there is no Play listing. 3.0.1 onward updates in place as usual.

3.0.1 changed the signing key. The old certificate's subject read CN=Pulse Monitor, and a subject cannot be edited without issuing a new certificate. Android refuses an update signed by a different key, so if you installed 3.0.0 you have to uninstall it before installing 3.0.1. Export first.

Build

Needs JDK 17 and an Android SDK with API 36. local.properties wants sdk.dir=/path/to/Android/Sdk.

./gradlew :app:assembleDebug          # debuggable
./gradlew :app:assembleRelease        # minified, needs keystore/keystore.properties
./gradlew :app:testDebugUnitTest      # 359 JVM tests

Release builds are signed from keystore/keystore.properties, which is gitignored along with the key itself. Without it the release task still builds, just unsigned.

Tests — 359 JVM + 173 on-device

359 JVM tests cover the pure logic: the alert state machines, escalation, quiet-hours arithmetic, assertions, the latency baseline, backup round-trips, and the widget's column arithmetic — which decides whether a monitor is visible at all and can otherwise only be exercised by dragging a widget around a home screen. 173 instrumented tests cover the parts that need a real Android: notifications, channels, the foreground service, the WebView element checker, widgets, the launcher icon's transparency, and a suite that drives a genuine connection-refused outage all the way to a red page on screen.

# one class at a time, the emulator does not enjoy the whole suite at once
adb shell am instrument -w -e class me.river.nightbell.UrgentPageEndToEndTest \
  me.river.nightbell.debug.test/androidx.test.runner.AndroidJUnitRunner

The urgent path has a habit of breaking in ways only a device shows — the alarm taking half a minute to stop after acknowledging, a custom notification layout getting squashed vertically, colorisation being silently dropped. None of that is visible from reading the code, so those cases got tests that were checked against the broken version first.

Known limitations

Important

Quiet hours still suppress urgent pages unless you turn on the bypass. That default is wrong and is the next thing I want to fix properly — with a migration, not a flipped constant.

  • Element checks need a WebView per page load. They are the slow kind of check.
  • Strict mode costs battery and a permanent notification. That is Android's price for a real cadence, not a design choice.
  • No CI. The test commands above are run by hand.

Licence

Apache 2.0, see LICENSE. Same licence as everything it depends on (androidx, Compose, OkHttp, kotlinx), so there is nothing awkward to reason about if you want to fork it or lift a piece of it.

Do what you like with it. Keep the notice, and say what you changed.


Built to be impossible to confuse with a marketing email. If it saved you an outage, a ⭐ is appreciated.

About

Android uptime monitoring with no server and no account. Checks run on your phone; an outage arrives as a full-screen page with a looping alarm instead of a notification you swipe away.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages