Skip to content

v1.11.0

Choose a tag to compare

@MorganKryze MorganKryze released this 28 Jul 15:45
86ce6ae

cairn has a new mark, a complete icon set, and a way to show your services' status without sending anyone to a monitoring dashboard.

🪨 A new mark

Four stones instead of five, all of the same gauge, the top one set down at nine degrees.

The old drawing failed where it mattered most. At 16 pixels its five stones merged into a teal cone with nothing countable in it, and its stagger was about one unit on a 32 grid: too small to read as a choice, big enough to read as a misalignment. Measured on the actual 16×16 raster, the new mark is 51% partial pixels against the old one's 52%, so the tilt costs nothing.

The accent stays #247b7b. It was checked rather than kept by default: at 4.36:1 on the light background and 3.57:1 on the dark one, it was the only candidate tried that clears 3:1 on both, which is what lets a single favicon file serve both themes.

📱 The icon set is complete

cairn shipped one 180-pixel icon. Chromium offers "install this site" only when the manifest carries both a 192 and a 512, so no cairn site has ever been installable on Android, despite serving a manifest and declaring minimal-ui. Nothing failed and nothing warned.

Added, all generated from the one drawing by the same script: favicon.ico at 16/32/48 for the feed readers and link previewers that fetch /favicon.ico and never read the html, and icon-192.png and icon-512.png for the install prompt. /favicon.ico is now served at the root, and yields to the operator: a site with its own favicon gets a redirect to theirs rather than cairn's stones.

The two app icons are declared any maskable rather than shipped twice. The usual reason for a separate padded maskable file is that a full-bleed icon loses its edges to Android's crop; this mark is a narrow stack whose farthest pixel sits 190 from centre against a 205 safe radius. Measured, not assumed, and two files avoided.

🖼️ Your own icons, stated truthfully

The manifest used to stamp 180x180 on whatever favicon an operator supplied, whatever its real size. It was a guess published as a fact, so it is gone. Every field is now either established or absent:

  • An svg favicon is declared sizes: any and serves the home screen the way cairn's own does. Previously an svg favicon left cairn's mark on your visitors' home screens: only a png was honoured.
  • A raster in /assets is measured when the config loads and declared at its real size.
  • A raster behind a URL carries no size. Measuring it would mean an outbound request, and cairn makes none.

For full control there is a new icons list in site.yaml:

icons:
  - { src: /assets/brand-192.png, sizes: 192x192 }
  - { src: /assets/brand-512.png, sizes: 512x512, purpose: any maskable }

It drives the manifest and the iOS home screen alike, since iOS ignores the manifest and reads the apple-touch-icon link alone. cairn does not resize images: that would mean shipping a scaler for one rare path, and a badly resampled logo is worse than the one you drew.

🟢 Status without a destination

A new key, status.linked. Set it to false and the pills state the state and nothing else.

status:
  gatus: http://gatus:8080     # internal, and staying that way
  linked: false

The pill has always been a link, and its target falls back to status.gatus when status.page is unset. That fallback is the problem: status.gatus is very often an internal address, precisely because the docs recommend polling over the internal network, so an operator who never set status.page has been sending visitors to a host their browser cannot resolve.

With linked: false the pill becomes a plain span rather than a disabled link: no target in the markup, no keyboard stop, no screen-reader label announcing a destination that is not there, and on a card the click falls through to the service like everywhere else. The state is untouched: Gatus is polled on the same cadence, and a service it does not monitor still gets no pill.

🐛 Fixes

  • The search said "no results" on the way back to the full list. Clearing the box, pressing Escape, or typing only spaces all left the message on screen, and #empty carries role="status", so it was announced too. An empty box is not a search that found nothing.
  • A protocol-relative logo or icon came out pointing nowhere. //cdn.example.org/logo.png starts with a slash but names another origin; treating it as a local path produced /cairn//cdn.example.org/logo.png. The two kinds of leading slash are now told apart in one place instead of four.

🧪 Internal

Coverage is 90.0% across 199 tests, against 89.6% and 128 in 1.10.1.

search.js was the one behaviour go test could not reach, which is exactly why the search bug shipped. just test-search and a new CI job now drive it in a real browser against the example config; it pulls nothing from a registry, so unlike the demo job it is safe to make a required check.

An unknown key is now answered with the keys of the entry that refused it, read off the struct instead of a hand-kept list. That list had already drifted: it never gained show_version, so a misspelt show_versions was answered with a set of suggestions that did not contain the right spelling. Service entries got no list at all, and nested entries were offered the top-level keys.

The mark was drawn twice, in favicon.svg and inline in layout.tmpl as the fallback icon for services without one. Redrawing one left the other on the old five stones. A test now compares the two and fails if they diverge.

📦 Image

docker pull ghcr.io/morgankryze/cairn:1.11.0

Still two files, /cairn and a CA bundle, under 10 MB: the whole icon set adds about 11 kB. This release also moves 1.11, 1, stable and latest.