tools.book: serve a busy-only iCalendar feed on cal.lassul.us
For when.lassul.us (timeful) to show lass's availability without event
details, at https://cal.lassul.us/busy/<token>.ics.
With --busy-token-file, GET /busy/<token>.ics returns the busy time book
itself uses for free slots (CalDAV.busy plus held requests), so the feed
and the booking page agree on what blocks time: recurrences expanded,
all-day events from local midnight, TRANSPARENT and CANCELLED events
left out, TENTATIVE events (own ones and pending booking requests) kept
as busy, since book holds those slots too.
Intervals are merged so overlapping events cannot be counted, and each
VEVENT carries only DTSTART/DTEND in UTC, DTSTAMP (required by RFC
5545), SUMMARY "Busy", TRANSP OPAQUE and a UID hashed from its times.
The window runs from 7 days back to 90 days ahead: book's own 28 day
horizon is too short for polls planned weeks ahead, and the past week
keeps polls that are still open showing sensible data.
The feed is generated per request, so it is as fresh as the calendar;
an unreadable calendar gives 503, never an empty feed. Any other path
under /busy/ gives 404; the token is compared in constant time.
configs/book.nix: the token comes from the prompt-less vars generator
busy-feed and reaches book as a systemd credential, so it never lands in
the Nix store; nginx proxies all of cal.lassul.us/busy/ to book.
book.lassul.us returns 404 for /busy/ so the feed has a single URL.
tools.hermes-voice: Android wrapper as digital assistant
packages.hermes-voice-android builds an unsigned APK (us.lassul.assist,
"Assist") that opens https://assist.lassul.us/ as a Trusted Web
Activity, so Chrome handles the mic permission and the stored access
token exactly as in the browser. Its single activity is the launcher
entry and handles android.intent.action.ASSIST. That is enough to be
offered under Default apps > Digital assistant app: the ASSISTANT
role's AssistantRoleBehavior (packages/modules/Permission) qualifies a
package either by an assist-capable VoiceInteractionService or by an
exported activity resolving ACTION_ASSIST, with no system-app check.
The activity binds the first browser offering the TWA category, opens
a session and launches the TWA intent; without such a browser it opens
a Custom Tab, which other browsers treat as a plain VIEW intent. It
stays below the page in the task and finishes when returned to.
The build is plain aapt2 + javac + d8 from androidenv (build-tools and
platform 36; the SDK is unfree, so androidenv comes from a nixpkgs
instance with allowUnfree and the license accepted). The only library
is androidx.browser 1.10.0's classes.jar (AAR from Google Maven, 289
KB): the client, session and TWA intent builder used here need none of
its dependencies (androidx.core is only referenced by launchUrl-style
helpers, which are not called). The APK is 72 KB and byte-identical
across rebuilds (fixed zip timestamps).
packages.hermes-voice-android-sign zipaligns and signs it with
apksigner (v1-v3, no .idsig) from a keystore and password file, by
default the packaged unsigned APK, and prints the certificate so it
can be compared with assetlinks.json.
tools.realwallpaper: real colour imagery on the day side
The day side now shows what the geostationary satellites see, like
zoom.earth, instead of the base map under white clouds synthesised from
infrared:
- Europe/Africa/Atlantic: MTG (Meteosat-12) rgb_truecolour from EUMETSAT.
rgb_geocolour looks the same by day but renders ~4x slower on the
server and timed out (502) for full 2048 px tiles.
- Americas/Pacific: GOES-East and GOES-West GeoColor from GIBS.
- Asia/Australia: GIBS has no Himawari colour product. Himawari band 3
(red, 10 min) is turned into white cloud over the base map, with the
cloud opacity being the reflectance beyond the ground's own albedo.
The daily VIIRS true colour mosaics were the alternative, but their
clouds are up to a day old, have swath seams of different times and
would not match the IR clouds next to them.
Each source is weighted by how well its satellite sees the point: full
trust up to 62° view zenith, none from 78° (the limb is smeared), and
the more nadir satellite wins in overlaps with cos^8 of the zenith
angle, so seams are soft. Trust also needs the sun not to be in front
of the satellite: looking towards a low sun both MTG (mornings over the
Indian Ocean) and Himawari (evenings) are washed out to a pale band
with straight edges, while a long path with the sun behind the
satellite is fine (Berlin at winter noon). A plain path-length limit
would have dropped the latter. Near the terminator (cosz < 0.2)
GeoColor fades into its purple night blend, so the live imagery fades
out there and the IR clouds over the base map take over as before.
Where it is shown, the synthetic clouds and so their relief shading and
shadows are scaled down, so the real image is not shaded twice.
All live layers (including the IR clouds) now request an explicit TIME,
the render time floored to 10 min: without TIME GIBS served tiles of
one layer from different frames (sun glint cut at a tile edge), and it
makes --time render the imagery of that time, not only the sun. The
TIME is capped at the newest frame the server lists (GIBS
DescribeDomains, EUMETSAT per-layer capabilities), since GIBS answers
an unpublished time with an empty image and EUMETSAT with an error;
MTG true colour stops producing frames while its disk is dark.
GIBS also now and then answers with an empty 1 kB image for a frame it
has (the same request succeeds when repeated), which left a quarter of
the Himawari disk missing, so empty tiles are asked for up to 3 times.