EventFromNotification took the date from the notification's own day, on the
grounds that a meeting notification is about today or it would not be firing.
Calendar apps break that. A 21:00 reminder reading "Tomorrow at 09:00" became
an event at 09:00 today, twelve hours in the past, and FactKey filed that
wrong meeting under today's date. Storing a wrong meeting is the one outcome
this parse works to avoid.
An explicit day word now moves the date: завтра, tomorrow, послезавтра,
сегодня, today, tonight. Matched whole, so послезавтра is not read as завтра,
and stripped from the summary so the meeting is not named after the day.
Anything still landing more than two hours before the notification is refused,
which covers the cases with no day word at all. The grace keeps a repost for a
meeting already under way.
Also matches the bearer scheme with EqualFold. A phone sending "bearer <tok>"
fell through to the X-Maven-Token branch and got a 401 that looked like a
wrong token. A bare token with no scheme in Authorization is now rejected
rather than silently accepted. The route table in mavweb gains its /api/ambient
row, and the missing calendar_busy write is recorded as a known gap.
Found in review of #57.
Maven does not get a work credential. A corp mail or calendar session living on
the homelab ties the box's blast radius to the employer's data, which is the
thing this task exists to refuse. What she reads instead is the signal: an
Android notification-listener on the phone relays meeting notifications over
wg/LAN to POST /api/ambient, and the ones that clearly describe a meeting become
calendar events at source=ambient:notif, confidence 0.6.
The provenance is the point. A notification is evidence about a meeting, not a
reading of a calendar, so it is never indistinguishable from one: it is stored
below full confidence, store.CalendarEvents keeps the source and confidence on
every row it returns, and the query path hedges — "похоже, Планёрка @ 14:00" for
a relayed event, plain text for a CalDAV read.
The parse is deliberately conservative (internal/calendar/ambient.go). It needs
a real clock reading and a summary that is not just that clock reading;
otherwise it stores nothing at all. A bare hour is not a time, an unread count
is not a time, and "срок 2026.08.15" does not offer 08:15 as a meeting — loose
digits in a notification are far more often a badge or a date, and a mailbox of
noise rendered as invented meetings is worse than a gap.
The ingest is off unless configured: no -ambient-token, no route registered. The
token is a shared secret compared in constant time, because the poster is a
background Android service and WebAuthn has no answer for one. The endpoint is
write-only, accepts one shape of write, and cannot read anything back out.
Reposts of the same notification dedupe against the latest fact for that
key+source, the same append-only discipline cmd/mavcaldav follows.
Not shipped: the Android relay app itself, which is a separate artifact and a
device, not Go in this repo.
Radicale becomes a write-only render target, not a store. sqlite stays
canonical: every poll mavcaldav reads the pending reminders out of core and
publishes each one as a single-event iCal resource, withdrawing the ones that
have fired or been cancelled. Losing the collection costs nothing — the next
tick rebuilds it, and nothing is ever read back from it.
It structurally cannot write to a calendar maven only reads. The render URL and
credential are their own flags, and -render-url is refused at startup when it
names the collection -url reads; the only paths it addresses carry the
maven-reminder- prefix, so even aimed at the wrong collection it can only touch
resources it created. Rendering is off unless -render-url is given.
The calendar data model now lives in one place, internal/calendar: the Event,
the iCal parse it comes from and the render it goes to, the fact key/value
encoding, and the source constants that say which calendars may be written to.
It was a parse inlined in cmd/mavcaldav and a Sprintf in two files; #126 and
#128 both need to agree with it.
Fixes a latent day-boundary bug moved out of that inline parse: it took the day
number off a local clock reading but built the window boundaries in UTC, so on
a box east of Greenwich part of the evening fell outside "today" and the poller
saw an empty calendar after 20:00 UTC. Today is now the owner's day in the
owner's location, which is what the busy gate and the day plan mean.