Skip to content

feat: name the channel, the date and the local time in the protocol - #34

Merged
TheMeinerLP merged 1 commit into
mainfrom
feat/protocol-header
Aug 20, 2026
Merged

feat: name the channel, the date and the local time in the protocol#34
TheMeinerLP merged 1 commit into
mainfrom
feat/protocol-header

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Three things a reader of a meeting protocol looks for and could not find. Before and after:

+**Channel:** [Meeting-Raum](https://discord.com/channels/752…/1166…)
+**Date:** @[20.08.2026](mention://date/2026-08-20) · 15:08, 47 min · 2 participants
+
 ## Participants
 …
 ## Transcript

-**13:08:12** · @[Phillipp](mention://user/7a621c83-…) (…)
+**15:08:12** · @[Phillipp](mention://user/7a621c83-…) (…)

Times were written in UTC

The protocol is read by the people who were in the room, so the times in it are theirs. UTC was wrong in the way that is hardest to catch: 15:08 reads as a plausible meeting time whether or not it is the right one. Nothing about the document would have looked broken.

timezone is now per-guild configuration, defaulting to Europe/Berlin. An unusable value falls back to UTC with a warning naming the guild rather than failing the job — a wrong offset is recoverable, a missing protocol is not.

Checked before relying on it: the python:3.14-slim image does carry the zoneinfo database, so ZoneInfo("Europe/Berlin") resolves in the container and 13:08 UTC renders as 15:08 CEST. A slim image without tzdata would have failed only in production.

The channel was missing entirely

The worker writes the document and holds no Discord connection, so it knew only the id. The bot records the name when it opens the session — migration 0005, nullable.

Capturing it at open time rather than at render time is deliberate: a channel renamed afterwards should not rewrite the protocols of meetings held under the old name. The heading links the channel either way, falling back to the id when no name was captured, so sessions recorded before this column existed still get a working link.

The date is now an Outline date chip

@[20.08.2026](mention://date/2026-08-20)MentionType.Date exists in the deployed version (v1.9.1), so Outline renders it as a chip rather than text.

Date only, deliberately. v1.9.1's parseMentionUrl matches [a-z0-9-]+ for the value; the datetime form (…T15:08) only arrives with the regex on main, which carries the comment "The modelId is a UUID, a date (2024-02-03) or a datetime (2024-02-03T13:00)". An ISO datetime would silently degrade to a plain link here.

The heading also carries the start time, duration and participant count.

Design note

ChannelRef is a separate value object rather than a field on Transcript: it describes the meeting, not what was said, and the name is a snapshot rather than a live lookup. render_transcript's channel parameter is optional so a caller that cannot resolve it still gets a protocol — the heading just omits that line.

Verification

369 tests pass. New tests cover: times rendered in the configured zone (22:00 Berlin, not 20:00 UTC), an unusable zone still producing a document, the channel link with and without a name, the date mention's exact form, a protocol rendering without a channel at all, and the name reaching the session row when it opens.

Checked against the defect, not just the fix: replacing the timezone lookup with a hardcoded UTC turns one of the new tests red.

One cosmetic thing I left alone: the channel name renders as Meeting\-Raum — the Markdown escaper escapes the hyphen. Outline renders it back as -, and the escaping is there because a channel name is attacker-controllable text, so I did not weaken it for looks.

Three things a reader of a meeting protocol looks for and could not find.

**Times were written in UTC.** The protocol is read by the people who were
in the room, so the times in it are theirs. UTC was wrong in the way that is
hardest to catch: 15:08 reads as a plausible meeting time whether or not it
is the right one. `timezone` is now per-guild configuration defaulting to
Europe/Berlin, and an unusable value falls back to UTC with a warning naming
the guild -- a wrong offset is recoverable, a missing protocol is not.

**The channel was missing entirely.** The worker writes the document and has
no Discord connection, so it knew only the id. The bot records the name when
it opens the session (migration 0005, nullable): capturing it there also
means a channel renamed afterwards does not rewrite the protocols of
meetings held under the old name. The heading links the channel either way,
falling back to the id when no name was captured.

**The date is now an Outline date mention**, `mention://date/YYYY-MM-DD`,
which Outline renders as a chip. Date only, deliberately: the deployed
version's parser accepts no time component, so an ISO datetime would degrade
to a plain link. The heading also carries the start time, the duration and
the participant count.

`ChannelRef` is a separate value object rather than a field on `Transcript`:
it describes the meeting, not what was said, and the name is a snapshot
rather than a live lookup.
@TheMeinerLP
TheMeinerLP merged commit 0c6baa8 into main Aug 20, 2026
8 checks passed
TheMeinerLP added a commit that referenced this pull request Aug 20, 2026
Two restructurings of `SturnusClient` met here: main's reconfiguration
engine (#35) and this branch's capture pipeline. Main's structure is the
trunk; this branch's behaviour is re-expressed inside it.

The one decision worth naming: the rejoin guard moved out of
`on_voice_state_update` and into `_sync_participants`. On this branch the
event handler was the only way a session could start, so the guard was
correct there. Under main's engine it no longer is -- `_build`,
`_retarget` and `_apply_pending` reach `_sync_participants` too, so a
deferred channel change landing on the same tick as a capture failure
would have smuggled a session past a guard sitting in the handler. One
check at the funnel covers every path.

The cooldown is armed between `_return_to_idle` and `_apply_pending`, not
after, for the same reason. It lapses in `_tick_guild` after `_reconcile`,
so the recount uses the configuration the reconcile just settled on.

Also merged: `RecordingService.retarget()` now takes the channel name
alongside the id (#34 + #35), and the capture-pipeline fakes follow the
`open_session` signature that gained `channel_name`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant