# jet/noisebell

[View on GitCafe](https://git.cafe/jet/noisebell)

Repository: [jet/noisebell](https://git.cafe/jet/noisebell)

Visibility: public

Default branch: main

## Resources

- [Source](https://git.cafe/jet/noisebell/tree/refs%2Fheads%2Fmain?format=markdown)

- [Commit history](https://git.cafe/jet/noisebell/commits/refs%2Fheads%2Fmain?format=markdown)

- [Issues](https://git.cafe/jet/noisebell/issues?format=markdown)

- [Pull requests](https://git.cafe/jet/noisebell/pulls?format=markdown)

- [Stacks](https://git.cafe/jet/noisebell/stacks?format=markdown)

- [Branches](https://git.cafe/jet/noisebell/branches?format=markdown)

- [Tags](https://git.cafe/jet/noisebell/tags?format=markdown)

Snapshot commit: 8a79139f6bb771fc7a5f7ab8ba874ac5383e9e3d

## README

[README.md](https://git.cafe/jet/noisebell/blob/8a79139f6bb771fc7a5f7ab8ba874ac5383e9e3d/README.md?format=markdown)

---

# Noisebell

[![Space status](https://your-cache-domain.example.com/badge.svg)](https://your-cache-domain.example.com/status)

Monitors the door at [Noisebridge](https://www.noisebridge.net) and tells you whether it's open or closed.

A Raspberry Pi reads a magnetic sensor on the door and pushes state changes to a cache server. The cache keeps the latest state and fans updates out to chat integrations such as Discord and Zulip.

```
Pi (door sensor) ──webhook──> Cache ──webhook──> Discord / Zulip
                                |
                     polls Pi <-+
                                |
                                +──webhook──> Pi relay ──webhook──> Home Assistant
```

## Layout

| Directory | What it is |
|-----------|------------|
| [`pi/`](/jet/noisebell/tree/8a79139f6bb771fc7a5f7ab8ba874ac5383e9e3d/pi?format=markdown) | Raspberry Pi OS base with laptop-built Noisebell deploy |
| [`remote/`](/jet/noisebell/tree/8a79139f6bb771fc7a5f7ab8ba874ac5383e9e3d/remote?format=markdown) | Server-side services (cache, RSS, Discord, Zulip) |
| [`hosts/noisebell-do/`](/jet/noisebell/tree/8a79139f6bb771fc7a5f7ab8ba874ac5383e9e3d/hosts/noisebell-do?format=markdown) | Standalone DigitalOcean NixOS host for the remote services |
| [`secrets/`](/jet/noisebell/tree/8a79139f6bb771fc7a5f7ab8ba874ac5383e9e3d/secrets?format=markdown) | Shared agenix-encrypted secrets and recipient rules |

Each directory has its own README with setup and configuration details.

For hosted deployment, this repo exports `nixosConfigurations.noisebell-do`, a small DigitalOcean NixOS host that imports `noisebell.nixosModules.default`. The host provides deployment-specific values like domains and the Pi address, while the Noisebell module itself points `agenix` at the encrypted files in `secrets/` and consumes the decrypted runtime files on the target machine.

Useful commands:

- `./scripts/deploy-do [jet@noisebell-do]` redeploys the DigitalOcean remote host
- `./scripts/nhs` redeploys the old Hetzner host using the local checkout as the flake input
- `scripts/deploy-pios-pi.sh pi@100.66.45.36` redeploys the Raspberry Pi OS machine
- `scripts/share-grafana-public-dashboard jet@noisebell-do` repairs or prints the deterministic public-safe Grafana dashboard link

The full Home Assistant relay workflow is documented in `pi/README.md`. Public hosting, Cloudflare Tunnel, firewall, and Grafana sharing details are documented in `docs/hosting.md`.

## CI

Everything CI gates on is a Nix derivation, so a runner needs Nix and nothing else — no
Rust install, no cross toolchain setup, no `cargo` invocations of its own.

```sh
nix build -L --max-jobs auto --cores 0 .#ci
```

`.#ci` is a link farm over every entry in `checks.x86_64-linux`, which is what makes the
run parallel: Nix sees the checks as independent derivations and schedules them across all
available cores at once, rather than running fmt, then clippy, then tests, then builds.

| Check | What it covers |
|-------|----------------|
| `fmt` | `cargo fmt --check` |
| `clippy` | `--all-targets` with `--deny warnings` |
| `nextest` | the workspace test suite |
| `noisebell-{cache,rss,discord,zulip}` | x86_64 release binaries |
| `noisebell-pi{,-relay}` | aarch64 glibc, cross-compiled |
| `noisebell-pi{,-relay}-static` | aarch64 musl, statically linked |

Individual checks build on their own, which is the faster loop when iterating:

```sh
nix build -L --max-jobs auto --cores 0 \
  .#checks.x86_64-linux.clippy .#checks.x86_64-linux.nextest
```

`nix flake check` also works and additionally evaluates the NixOS configurations, but it
skips the `aarch64-linux` outputs, so it is the slower and less complete of the two.

### Toolchain

The flake pins a nightly toolchain via `selectLatestNightlyWith`, which walks back from the
newest nightly until it finds one that ships every component the build asks for — a
partially published nightly cannot break CI. Nightly buys two things:

- **`-Z threads`** parallelises the rustc frontend within a single crate. Cargo already
  builds independent crates in parallel; this fills in the tail of the dependency graph,
  where a workspace this narrow otherwise compiles one crate on one core.
- **Cranelift** (`rustc-codegen-cranelift-preview`) replaces LLVM as the codegen backend for
  the dev profile only. It emits object code much faster and the code it emits is slower,
  which is the right trade for clippy and tests. Release binaries — the ones that actually
  ship to the Pi and the DO host — stay on LLVM.

Cross-compilation for both Pi targets comes from the same toolchain carrying the aarch64
`std`; only the C toolchain and the cargo target environment differ between the glibc and
musl variants. Each target does one shared dependency build for both of its binaries, and
that build is scoped to `-p noisebell -p noisebell-relay` so host-only crates never enter
the cross graph.

### Runner sizing

The build closure is 1207 store paths totalling 4.1 GB, most of it the nightly toolchain,
the aarch64 cross toolchains, and vendored crates. Roughly 0.4 GB of that substitutes
straight from `cache.nixos.org` (including both cross C toolchains, so there is no gcc
bootstrap); another ~0.2 GB is Rust nightly tarballs from `static.rust-lang.org` that are
unpacked and patchelfed locally.

A disposable VM therefore wants ~16 GB of disk — closure plus cargo target directories
during the build — and 16 GB of RAM if you run with `--max-jobs auto --cores 0`, since that
can put several dependency builds in flight at once with a full complement of rustc
processes in each. Halve `--max-jobs` if memory is tighter than that.

Every run from a fresh VM redoes all of it. If this runs often, pointing the runner at a
binary cache (attic, cachix) or giving it a persistent `/nix` turns most of the work into a
download, and an unchanged tree into a no-op.

## Observability

The DigitalOcean host runs Prometheus, Loki, Grafana, Alloy, node_exporter, and blackbox_exporter via `hosts/noisebell-do/observability.nix`. Grafana provisions `Noisebell Full Debug` for authenticated operators and `Noisebell Public` for externally shared, Prometheus-only status.

- Grafana: `https://grafana-noisebell.extremist.software/` through Cloudflare Tunnel, login required
- Public-safe Grafana dashboard: `https://grafana-noisebell.extremist.software/public-dashboards/6e6f69736562656c6c7075626c696330`
- Prometheus: `http://noisebell-do:9090/` over Tailscale
- Loki: `http://noisebell-do:3100/` over Tailscale

The Pi deploy script enables persistent journald, installs `prometheus-node-exporter`, and installs `noisebell-loki-journal.service` to ship Pi journal logs to Loki on the DO host.

Prometheus is the source of truth for regular time-based data: scrape health, host CPU/memory/disk/uptime, DO-to-Pi poll counts and last results, GPIO state, Pi hardware readings, webhook counters, and retry counters. Loki/journald is reserved for sparse event logs that should be readable in chronological order: service start/stop, door state changes, cache state changes, Pi offline/online transitions, auth or rate-limit rejections, webhook retries/failures, stale events, and GPIO read errors. Routine successful polls, unchanged poll results, metrics scrapes, and badge/image/status reads are intentionally not logged at `INFO`.

