Skip to main content

Cassini - Releases

← App details

Nextcloud 35

Cassini 0.2.0
Release Details
UpdatedOct. 13, 2026, 4:42 p.m.
Changelog

Added

  • Administrators can select Transcription only (.json) in Settings → Publish pipeline to publish new meetings as transcription-only files in Nextcloud Files, retaining speaker timing, summaries, metadata, tags and annotations without audio. Existing meetings keep their format on reruns. Audio and transcription-only meetings can coexist, are labelled in the meeting list, and follow the same retention policies.
  • Administrators can publish transcription-only meetings and choose whether to retain source media for reruns or delete it after processing succeeds or fails. Each recording keeps the policy selected when it starts.
  • Operator jobs show media deletion progress and errors, offer deletion retries, and explain why processing cannot be rerun when deletion was selected.
  • Proof of concept: separate the voices of people who shared one participant's device. In the app, a meeting's People panel offers Separate voices for a participant, then lets readers listen to each voice, name it, say two voices are the same person, or undo the split; the operator republishes the recording with the new speakers and rewrites its summary. Diarization runs locally on that participant's own track and stores only turn timestamps; nothing is re-transcribed and the audio is not re-encoded, so marks stay attached. After a split the original transcript stays in the recording as raw-asr ("Original"); renaming a speaker without a split only changes the name in the one transcript. Each person can save a limited number of speaker changes in a while, across all meetings; past it the panel says when to try again and keeps what was typed. See docs/speaker-separation.md.
  • Voice separation (optional) in Settings: an administrator downloads the Nemotron 3 Diarization int8 model (about 62 MiB, CPU only) like any optional model; cassini models install|pack|import handle it too.
  • cassini speakers diarize|apply|show, and the operator route annotations/meetings/<id>/speakers (GET/POST, for anyone who can read the meeting). CASSINI_DIARIZATION_THREADS sets the diarizer's CPU threads (default 2).
  • Insights have names. Create an insight fills one in from the template and the meetings it reads (the question is quoted inside the insight instead), and you can change it before generating or later from the pencil beside the insight's title, with Save and Cancel as when renaming a tag. The list, the insight and the meeting's Insights card show the name, search matches it, and the document saved to your Nextcloud files is named after it.
  • An answered insight can now leave Cassini. Export in the insight drawer copies or downloads a markdown copy with a readable header (the insight's name, the question when that differs, when it was written, the template and model, and the meetings it drew from that you can see, each linking back to its meeting), and Open in Files opens the document in Nextcloud Files with its details open, where it can be shared.
  • Administrators can schedule whole-meeting expiry from Nextcloud Files independently of container storage. Expiry removes published audio, transcripts and notes together, with conditional deletion, durable recovery and protection against reruns or indexes recreating expired meetings. Nextcloud versions, trash and backups remain separate.
  • ADMIN-only APIs provide on-demand sweeps and eviction previews with the meeting name, creation and recording timestamps, age in calendar days and an evict/keep decision.
  • ADMIN-only POST /operator/storage/retention/sweep to run the scheduled expiry logic on demand, with overlap protection and synchronous results.
  • Configurable daily retention sweep time and timezone in Operator → Storage, defaulting to 02:00 UTC. Saved schedule changes take effect without restarting.
  • Operator → Storage retention configuration for container recordings, attempt history, current output archives and stage logs. Policies default to keep forever, with 7, 30, 60, 90 or custom days evaluated on UTC dates.
  • Startup/daily cleanup with restart recovery, whole-recording expiry (audio, video and supporting files together), and unavailable-source rerun protection. Only successful publication replaces the current local archive.
  • Storage insights now includes published Nextcloud files in the category comparison and temporal charts, with the same time range and exact-value controls as local assets.
  • Files without a matching job use recordedAtLocal or createdAtUtc metadata for their date, including inspection of older unindexed files. Undated files and partial measurement failures remain visible.
  • Operator → Storage shows local usage by retention category alongside retention settings, plus published Nextcloud usage including legacy archives.
  • Each category has a date bar chart with independent time range and precision controls, exact values and a data table. Attempt history can be combined or split into its fine-grained policy categories. Unknown dates and unclassified local files remain visible.
  • Storage usage is served from an in-memory index with its last-calculated time. Administrators can rebuild it on demand, and Cassini refreshes it on fixed five-minute boundaries.
  • After a Talk recording is first published, Cassini makes a best-effort attempt to post its meeting link as the moderator who started recording. Notification failures are logged without affecting publication or recording access; messages are not retried.

Changed

  • The fp32 Parakeet v3 model now downloads from dist.gocassini.com, like every other model.
  • The native speech runtime now includes Nemotron 3 Diarization support (sherpa-onnx v1.13.7-cassini.6). The runtime accepts the version-2 Nemotron models on dist.gocassini.com and fixes final-chunk padding. Parakeet and Silero model files and transcription behavior are unchanged.
  • The privacy guide describes on-demand voice separation, the summary rewritten after a speaker edit (which sends the transcript and the typed names to the configured summary endpoint), and where names and editors are kept.
  • A participant whose voices were separated stays in the recording's speaker list, marked with the voices that replace it, so every transcript's speakers are listed. Readers older than this release show it as one more speaker beside its voices.
  • Refreshed the App Store listing: a new description with a live demo link, an early-release note inviting feedback, rough hardware guide and Ethical AI rating; six new captioned screenshots; the Social & communication category; gocassini.com as the homepage; and the Codemyriad author link.
  • The documentation and landing page describe recording access as it now works: each recording is shared with the people, groups and Teams in its Talk room, with no audience to choose and no Team folders or Everyone Group app needed.
  • New recordings capture audio only by default, including upgrades without a saved video choice. Administrators can opt into retaining source camera video using Capture video in Operator → Publish pipeline. Retention review or reminder dismissal does not change video consent. Saving affects subsequent recordings; published meeting files remain audio only, and existing source video follows its retention policy.
  • Retention descriptions explain storage growth and the consequences of deleting each category. Existing keep-forever defaults and published Nextcloud recordings are unchanged.
  • Nextcloud Files retention uses the meeting’s recorded local date when present, otherwise its creation timestamp. Existing managed meetings adopt these dates without relying on job completion history; unknown provenance or invalid timestamps keep files unchanged.
  • Preview, operation-status inspection and on-demand sweeps are API-only tools; the retention UI provides policy settings. The durable operation journal and backend logging are preserved.
  • The old artifact-retention flag/environment variable is deprecated and ignored. Successful capture/intermediate duplicate cleanup remains unconditional; historical payloads now default to keep forever.
  • Search backfill against the local work root requires stopping its operator process to avoid racing archive promotion or expiry. Existing AppAPI installations must receive the new ADMIN retention route through a versioned manifest update.
  • Wide storage charts can be scrolled with the scrollbar, keyboard, or Earlier/Later buttons. Days per bar adapt automatically to the selected range.
  • A meeting opens on Summary and Transcript tabs, with insights at the end of the summary. The playback bar shows only under Transcript, and find-in-transcript sits beside the tabs, pinned while you scroll.
  • The meeting header is compact: the date, room, duration and participants sit in a details pill that opens them in full, technical details are folded into "About this recording", and the Raw JSON view is gone. Copy and download actions are one Export menu.
  • Tagging is a deliberate step. Dragging the timeline rail scrolls the transcript instead of selecting text, and a text selection offers Tag selection, Copy text and Cancel. An existing tagged section opens read-only, with Edit to adjust or remove it. A tag on the whole meeting is removed from its menu rather than an always-visible ✕.
  • Selections and hovered tagged sections are highlighted continuously across words, like a normal text selection.
  • On phones, the meeting and Prepare sheets fill the screen, and tapping the covered tab bar closes them.
  • Doctor opens like the other Operator pages: a fixed title and description, with Run all checks beside the title, and the overall verdict in a callout beneath, with when the checks last ran. The verdict also says when a test recording you started is unfinished, and links to it.
  • Each check leads with the next thing to do, under the explanation it acts on: Nextcloud's setup guide and Talk's settings when there is no High Performance Backend, Open test room while a test waits for Talk, Play the recording and an "I could hear the audio" checkbox once it is published, and See why it stopped, which opens the run on the Recordings page, when it fails. Re-check buttons are quieter, and a check's own time appears only when it differs from the rest of the run.
  • When something stops Talk from being checked, such as Talk refusing Cassini, Nextcloud being unreachable or Cassini having no recording secret, that is the one row to act on, and the rows it prevents from being checked wait on it.
  • Cassini says why it could not reach Nextcloud (an address that does not resolve, a refused connection, a timeout, an untrusted certificate, a server error, or a proxy answering instead), with steps for each.
  • The few commands Doctor gives, such as where to read a secret or how to see a log, are labelled with where they apply and come in a block with a copy button.
  • Archive search coverage reads in plain words. A running re-index shows progress and updates the row when it finishes. A failed one says what stopped it, and offers to try again only where that can help.
  • When the panel cannot refresh, it says why, whether a session that needs signing in again, Cassini not responding or the connection, and offers Try again while keeping the last results on screen.
  • Retention setup is now a nonblocking administrator reminder. Browse, playback and recording remain available before review; administrators can save unchanged defaults in Operator → Storage to acknowledge the reminder across sessions.
  • Administrators can choose “Don’t remind again” to keep the current settings and permanently dismiss the reminder for the installation. Unsaved drafts and concurrent policy changes are protected.
  • Meeting audio is encoded to match what its recording carries instead of at a fixed 64 kb/s, and with Opus tuned to reproduce the voices as they were rather than reshape them for a phone line. A 16 kHz phone recording is published at about half its previous size. A Talk call is coded at a third over what its highest-rate participant sent per channel, at most 64 kb/s; a participant whose client sends stereo counts at half their rate, since the meeting mix is mono. On our own meetings this makes the published audio about 30% smaller. Rebuilding a meeting published before this change keeps its original encode, so tags and marks on it carry over.
  • The hardware details under "Change" are one box: GPU, CPU cores, memory and the device choice as label and value, then one tip. The device choice warns only when it names a GPU this server doesn't have.
  • The line above Quality reads as one sentence ("Transcribing with Parakeet TDT 0.6B v3 — fp32 on the CPU (no GPU found).") with a Hardware button that opens the details.
  • Browsers revalidate the app's script and stylesheet on every load, so an update that keeps the version number no longer leaves an old stylesheet in place.
  • A model download says what happens after it finishes: "Downloaded · checks once no meeting is being processed", then "Checking it runs on this server", instead of sitting at "installed · 100%". The progress bar shows only while downloading.
  • With transcription off and the picked model downloaded but not yet checked, the hint says "Check the model below to turn it on." instead of asking to download it.
  • Model actions (Download, Check, Retry, Cancel, Use this model) and Retry media deletion are styled as buttons. They had no style at all inside Nextcloud.
  • The voice separation card leads with what the model is for, matches the transcript model row, and has more room at the bottom.
  • The Publishing card is now "After each meeting", and a strip at its top shows what new recordings get with the saved settings: transcription, summary, what is published and what is kept. A step that depends on one that is off is marked, and each step jumps to its setting.
  • The card asks four numbered questions in the order the steps run. Until transcription is saved on, the questions after it are dimmed and cannot be changed, and saving with transcription off publishes the audio.
  • The keep-on-server options say how long kept audio and video stay, from the Recordings retention policy in Storage.
  • Hardware is now a line in the transcript question; the detected GPU, cores and the device override open under Change.
  • Summarisation is now part of the card: its switch, provider and template sit in the summary question, and the separate Summarisation card is gone.
  • Vocabulary and Search spellings are now one Transcript hints card.
  • The documentation states the supported range as Nextcloud 33.0.9 to 35 everywhere, matching the App Store manifest.
  • The deprecated working-file retention setting no longer says published Nextcloud files are never evicted; their expiry is a separate setting in Operator → Storage.
  • Each Storage date chart says which files the next cleanup removes under its saved retention policy, and a dashed line separates the faded bars it removes from the ones it keeps.
  • Publishing now includes transcription: what is published, then whether and how a transcript is made, then what stays on the server.
  • Retention settings use plainer names on every screen: Recordings, Published meetings (on this server), Previous attempts, Processing logs and Published meetings in Nextcloud. The reminder banner is one sentence with Review and Dismiss.
  • The Storage report shows two totals, On this server and In Nextcloud Files, then space by type and by date. Previous attempts opens onto its four kinds, and each date chart picks its own bar width.
  • The retention editor shows each category as a heading, one line and its control. Previous attempts splits into its four kinds with a link, the daily cleanup is one row with a timezone list, and Discard changes appears only when there are unsaved edits.
  • New installations run the daily cleanup at 03:42 UTC instead of 02:00 UTC. A saved cleanup time is kept.
  • Publishing asks what people receive first, then what stays on the server. Keeping nothing is unavailable while audio is published.
  • Transcription has its on/off switch in the heading, and the model shows as one row with its size, state and one action.
  • Opening a meeting from a search that matched what was said fills the meeting's transcript search with the same words, as opening one of its moments already did.
  • The app asks for its stylesheet by build, not only by app version. An update that keeps the version number (a rebuilt image, a hot-deploy) could leave a browser pairing the new script with an older stylesheet, which unstyled every screen built from components.
  • On wide lists the matched transcript lines take more room and start further left, while the meeting's title, date and tags keep about 34rem.
  • "Who can see recordings" in Settings and the "Shared with participants" note in the rooms rail lead with "Nextcloud Files controls who can read each recording." and then say the same three points, as a list. The rail's note is wider than the rail, so it no longer wraps into a narrow column.
  • A meeting opened with words carried in from a search opens on its Transcript tab, where the matches are highlighted.
  • "Who can see recordings" is two short paragraphs. Its status and a Check again button appear only when sharing needs fixing.
  • Voice separation has its own card in Settings, with the same model row as transcription, instead of sitting inside the transcript step.
  • The transcript step opens with one line naming the model in use and where it runs (CPU or GPU), with Change for the hardware details. The status strip's Transcribe step names that model's tier. A model that the picked quality still needs shows under a small heading, without repeating the one in use.
  • The status strip shows two chips per row when the settings card is narrow, all the same height. Step headings are larger than the text under them.
  • Publish pipeline no longer has a reload button in its header. It discarded unsaved changes without asking; the browser's reload asks first.

Fixed

  • The Cassini app no longer requests the administrators-only operator status on every page load for users who are not administrators (each request answered 404).
  • Guests who join the call at the moment the recording starts are now listed under their own name instead of a participant-<id> placeholder.
  • Preserve the same audio timing when source video is omitted, including single-track recordings remuxed with FFmpeg 6.
  • Doctor's test recording no longer offers a test conversation you cannot record in. Talk lets only a conversation's moderators start a recording, and the connection check creates this one under Cassini's own account — so Open test room could drop you into a call with no record button, directly under steps telling you to press it. The link now appears only for a conversation that is yours, and Prepare test makes you one.
  • Doctor now notices when its test conversation has been deleted, instead of only while Talk has a High Performance Backend. Without one, a deleted conversation stayed on the panel as a dead Open test room link, and preparing a test re-armed it against a conversation that was gone. Cassini asks Talk directly now, and forgets the conversation only when Talk says it no longer exists.
  • Storage chart bars now fill the available width and keep date labels below the plot despite global timeline styles.
  • The browse page and meeting view load without pop-in: the sidebar holds its height while rooms and tags load, the meeting list fades in once ready, and header tags and the timeline rail appear with the meeting.
  • Copying a selection after moving its handles copies the adjusted text, not the original selection.
  • Insight titles no longer truncate to a letter on phones.
  • Status colours meet WCAG AA contrast in light themes, including Nextcloud's.
  • "Connect Talk" is no longer offered when Cassini has no recording secret to give Talk, and a re-check button no longer says "Check again" for a check that never ran.
  • New recordings retain their original recording time through processing, publishing, and transcription-only export, even when filenames change or transcription is disabled or fails.
  • A failed recordings setup check no longer hides the meeting list. It shows as a strip above it, and you can still browse recordings; only saving new recordings stops.
  • When transcription is on but its saved model's files are missing, Settings says so: the transcript step shows a warning instead of "In use", and the status strip's Transcribe step turns amber with "model not installed". Recordings published in that state have no transcript.
  • Downloading the voice separation model no longer shows "until the model is ready" on the Publish step, and that note no longer appears while transcription is on.
  • Voice separation's "(optional)" is lighter and smaller than its heading.
  • A model installed but not yet checked against the current build reads "Installed · checked before the next recording" instead of "runtime check needed".
  • Selecting transcript text by dragging from outside a word (the end of a line, the space between turns) no longer loses the selection and its Tag selection bar the moment the mouse is released.
  • The "Who can see recordings" card and, in high-contrast themes, the "Create an insight" card now have the same inner padding as the cards around them.
  • The dot or icon in a tag chip sits on the centre of its label in every browser, including the tag filter in the rooms rail and the tag picker.
  • Search results keep the meeting's title and details readable at any width: the matched transcript lines sit under them, and beside them only when the list is wide. The meeting's length no longer runs into the first timestamp.
  • Clicking a matched transcript line opens that moment again; the row's click area had been covering it.

Security

  • Doctor's test conversation is now private. It was created as a public conversation, which Nextcloud never lists or shows to ordinary users, but which any signed-in user holding its token could read and join. It is now a group conversation whose only participant is the administrator it was made for, and Talk answers everyone else that it does not exist. Recording is unaffected: Cassini joins through signaling, not as a participant.

Added

  • The recording checks now show their state in colour: green for a check that passed, red for one that is blocking recording, and grey for one nothing has verified yet. The heading takes the colour of the worst news, so the panel can be read at a glance.
  • Configure optional transcription in Settings with visible model download progress, cancellation, retry, and explicit activation.
  • Model downloads resume on their own after a dropped connection or a stalled transfer.
  • Prepare transferable models with cassini models pack and install manually copied packages or files on air-gapped systems with cassini models import.
  • A Doctor section in the operator answers "is recording going to work?" in one place. It reports the recording volume and temporary space, Nextcloud storage, Talk's High Performance Backend and its credential, the Talk connection, a test recording, and how much of your archive search can read. Checks run once when Cassini starts and then only when you ask — with Run all checks, or a single row's own button, which re-runs only the probe behind that row.
  • Cassini says when Talk has no High Performance Backend, which stops recording entirely. It reads Talk's own signaling mode, so this appears even before a recording credential exists; it used to be discoverable only through a connection check that never got that far. The row is on the checklist whatever the outcome, including before anything has been established, so the one check that decides whether recording can work is never simply absent.
  • Archive search coverage reports how many meetings search can actually read, so a partial answer is visible instead of looking like no matches. Where meetings were never indexed, a Re-index now button does it for you. A shortfall nobody can act on — a recording where nobody spoke, or an older meeting search cannot index at all — is reported without being counted against coverage.
  • cassini doctor --json emits its checks as structured data, each with a stable id. Running cassini doctor by hand prints exactly what it printed before.
  • Copy or download a meeting transcript as Markdown (.md) and download its audio from the meeting view or Prepare. Multiple selected meeting files download together in one ZIP.
  • The <cassini-meeting> embed takes layout="inline" for pages that already show the recording's name and details, and --cassini-color-* / --cassini-font-sans custom properties to match the page's colours and type.
  • The <cassini-meeting> embed dispatches a playbackerror event when the browser cannot play the recording, and follows its theme attribute when the page changes it.
  • Scheduled upstream compatibility checks and real-browser coverage of the embedded Cassini transcript and recording playback.

Changed

  • Share each recording with its Talk room audience through Nextcloud Files, without requiring Team Folders or Everyone Group apps.
  • Show only recordings in the caller's current Nextcloud shares and check each file read as that caller.
  • Replace the two recording access choices with one participant-based model; public meeting participants may reshare when Nextcloud permits it.
  • Rewrote the App Store summary and description to match the README and docs site, and pointed the user documentation link at gocassini.com/docs.
  • Brought the App Store description up to date: Nextcloud 33.0.9 to 35, transcription off until a model is installed (and the model download from dist.gocassini.com), the Doctor checklist, and transcript and audio downloads.
  • Rewrote the landing page copy: a new hero, a feature list that leads with the App Store install and Talk's Record button, an AI providers section in place of the summaries one, and reworked CPU and GPU cards.
  • Corrected the supported Nextcloud versions to 32 to 35, said that insights as well as summaries send transcript text to a configured endpoint, and linked the meeting file specification.
  • Renamed GoCassini to Cassini in page titles, link previews and support links.
  • Transcription defaults to off. Recordings remain playable with participant metadata when transcription is disabled, unavailable, or fails.
  • A recording waits, as it does for memory, when its model cannot be checked yet (GPU not visible, not enough free RAM or VRAM). It is published as audio only when the model is missing or the wait runs out.
  • CPU and CUDA images no longer bundle Parakeet or VAD. Models download from dist.gocassini.com into persistent revision directories and survive application upgrades without downloading unchanged weights.
  • A check that depends on another one says what it is waiting for rather than asking for attention itself, and offers no buttons of its own. With no High Performance Backend, the credential and the connection checks no longer each report a problem: the backend is the one row to act on, and fixing it is what makes the others checkable.
  • Testing a recording no longer asks you to supply a Talk room. Cassini makes the conversation for you, in your name so that you can moderate it — Talk only lets a conversation's moderators start a recording — and you join the call and press Talk's own record button, while the row reports what happened until you confirm you could play the audio back. The check appears only once everything it depends on has been checked and passed, so it is never offered as a test that cannot succeed.
  • Checks explain in words, with no shell commands to copy and no questions about how Nextcloud was installed. Where Cassini can fix something it offers a button; where it cannot, it points at Nextcloud's own settings and documentation. An instruction assembled from a guess about your deployment is worse than a reference.
  • Checks no longer re-run themselves every five seconds. A reading is taken when Cassini starts and whenever you ask for one, so the panel stops disabling its own buttons twelve times a minute, and a check costs a probe only when somebody wants it. A test recording still being worked on is followed until it publishes, because that is the one thing that moves on its own.
  • The signaling credential is reported by the High Performance Backend check rather than by a row of its own: it is that server's credential, and that check is what accepts or rejects it. One row now says whether a backend exists, whether Cassini has its secret, and whether the secret works. It says plainly that the value is not a Nextcloud setting and where on the signaling server to read it, it does not ask for the secret where no backend exists, and the form stays reachable once the check passes so the secret can be rotated.
  • The Nextcloud storage check says what it established: Cassini's account exists, its recordings folder is writable, and the sharing API answers. It used to report that a "preflight passed", which named an internal routine and no fact.
  • The operator's readiness endpoint is now GET /operator/health, because it reports on more than recording readiness. An installed app picks the new route up when it updates to a release carrying it.
  • Require Nextcloud 33.0.9–35 for new installations; retire Nextcloud 32 support in this release.
  • Require successful installed-app compatibility evidence for every advertised Nextcloud major before publishing a release, with exact tested versions and artifacts attached to the release.
  • Cache compatibility Go tools, report scenario phase timings, share GPU smoke transcription across assertions, and separate registry maintenance from release qualification.

Removed

  • The "Cassini is ready to record" first-run dialog and its per-install acknowledgement, left over from the two permission models. A fresh install records as soon as its checks pass; creating the cassini account, when Nextcloud refuses it to the app, stays in Operator › Publish pipeline.

Fixed

  • A failed refresh no longer erases the checks. It used to rewrite every row to "could not refresh", so one failed request hid whatever was actually wrong; the last findings stay on screen and the failure is reported once, on its own.
  • A check that was passing no longer turns into "not verified" five minutes later. It keeps what it found and says how long ago, so an idle panel stops looking like a problem — and a check that has never run says so, rather than being reported like one whose result merely aged.
  • A connection check that ran and could not reach Nextcloud or the signaling server now reports a problem instead of "not verified". It tried and failed, which is a finding about the deployment.
  • When Cassini's Talk backend URL override points at Cassini's own app proxy, the check says so and names the setting to clear, instead of blaming connectivity and TLS.
  • Buttons that read "Configure" and opened a panel about an unrelated fault are gone. Every action a check offers is named for what it does, and one the app cannot name offers no button at all.
  • The operator panels can be scrolled to the bottom again. Whenever a check needed attention, the banner saying so pushed the panels down without shortening them, so their last few centimetres sat below the window with no way to reach them.
  • Meeting tags now stay in click order while changes are confirmed or retried: selection and meeting counts update immediately without bouncing, and tag choices remain alphabetically ordered.
  • An embedded recording no longer takes the Space key on the whole page. Space plays and pauses only when pressed inside the viewer.
  • Following playback in an embedded recording scrolls only the transcript, never the page around it.
  • A narrow embedded recording no longer shows a back arrow that led nowhere.
  • An embedded recording no longer reads or rewrites the host page's URL fragment, so switching transcripts no longer replaces the page's #anchor.
  • Meeting tags and transcript annotations now appear immediately while saves run in the background. Editing stays available, pending changes survive closing and reopening the meeting, and failed saves can be retried without duplicating marks.

Added

  • Access control is now a choice rather than a requirement. Cassini used to have exactly one way to store recordings — a Nextcloud Team folder with per-recording permissions — which needs two Nextcloud apps an external app cannot install for itself, so an instance without them recorded meetings happily and then could not publish any of them. Recordings can now be visible to anyone with a Nextcloud account, in the cassini service account's own CassiniNoACL/Recordings, needing no extra apps at all; or to meeting participants only, which is exactly today's model in its Cassini Team folder. Each has its own folder, so neither can shadow the other's path.
  • Cassini can set itself up. It offers to make the changes an administrator used to make by hand: the cassini service account and its group, the Team folder, its group mappings, advanced permissions, and the permission-manager delegation. It lists every change before it makes any of them, and performs them as you — Nextcloud asks you to confirm your password with its own dialog, and Cassini never sees, stores or sends it. The occ recipe is still there for an administrator who would rather run it, and is generated from the same plan Cassini executes, so the two cannot describe different setups.
  • Every missing prerequisite is named, together with the occ command that fixes it. Cassini and GET /operator/status list them all — the service account, each Nextcloud app, the Team folder, its group mappings, its ACL setting and its ACL manager.
  • A development or CI deployment can still declare the mode, with the CASSINI_STORAGE_MODE deploy option (default or access_controlled). It is not for production installs, and it is believed only where it fits: a declared mode whose prerequisites are missing, or that meets recordings in both folders, or the same recording in both, is refused at enable time and not recorded. It seeds the initial value only, and changing the variable never moves an archive that already exists.
  • Switching says what it would do before it does it, and can be re-run. The confirmation reports how many recordings would move and between which folders, whether the destination already holds any, and whether an earlier switch left a copy behind. Working that out is read-only, and nothing happens until you confirm. The switch itself copies the archive, checks every recording arrived, records the new audience, and only then empties the old folder — so whichever audience is in force, that folder holds a complete archive, and a switch interrupted by a restart, a timeout or a closed browser tab strands nothing and can simply be asked for again. Cassini names the folder that still holds a leftover copy, offers one action to clear it, and says when recordings are sitting in the audience that is not in force.
  • cassini meetings summarize <meeting.opus> ... adds a meeting summary to already-published portable files without re-running transcription: the transcript sealed inside each file feeds one LLM call — configured by the same LLM_BASE_URL / OPENROUTER_API_KEY / SUMMARY_* variables as cassini build — and the file is rewritten in place with its audio bytes untouched.
  • Files that already carry a summary are skipped; --force regenerates and replaces one, and --out writes a single file's result to a new path instead of in place. A backfilled summary is marked provenance.meetingSummary.source = "backfill" in the manifest.
  • You can find words inside the meeting you are reading. A search box above the transcript narrows it to the turns that match, shows how many of them there are, and clearing it brings the whole transcript back. It searches what was actually said, so it still finds a turn whose wording was tidied for display.
  • Administrators can tell Cassini the spellings transcription produces for names it gets wrong, in the app's settings beside the existing vocabulary: one name per line, its spellings separated by commas. A search for the name then also finds the recordings where it was written down differently, and each result says whether it matched what you typed or a known mis-hearing. Cassini ships a starting list; anything configured here is added to it, and replaces it for a name that appears in both. It changes no recording — the vocabulary setting is the one that biases new transcriptions.
  • You can now search across your meetings. Cassini indexes each transcript as it is published and answers published/search with the moments that match — the meeting, the speaker, and where in the recording they said it. You only ever see meetings you can already open.
  • Search knows the names transcription gets wrong. Looking for "cassini" also finds the recordings where it was written down as "casino", and each result says whether it matched your words or a known mis-hearing of them.
  • Every answer says how much it covered: how many of the meetings you can read were actually searched. A meeting Cassini could not index is reported as outside coverage rather than passing for one with no matches, and an unreachable archive is an error rather than an empty result.
  • Cassini's search index lives in its own search.sqlite3 file beside the job database and holds nothing that cannot be rebuilt from the published recordings. Deleting it is safe: it costs a rebuild and never touches recordings or job history.
  • cassini-operator backfill-search indexes meetings published before the index existed. It is safe to re-run and safe to interrupt, and it indexes a meeting only when the stored transcript matches the recording that was delivered.
  • Searches are rate limited per account. Each one asks Nextcloud what that person may read, so a client looping on search would put that load on Nextcloud rather than on Cassini; a burst is fine and a loop is asked to slow down, with a reply that says when to retry. The limit is per account, so one runaway client cannot lock anyone else out.
  • Pick several meetings in the Cassini app — across rooms, and the picks survive changing the room or the search — and Prepare hands you all of them as one document to copy or download. It is the same document cassini meetings context prints for the same meetings in the same order, byte for byte, because the app asks the same producer for it rather than assembling its own.
  • Before it hands anything over, Prepare says what the bundle will not contain: how many of the picked meetings have no summary, how many were published before the index recorded whether they have one — a different statement, and never counted as the first — how many predate the single-file format, and whether the word count it shows is a total or a floor. All of it is read from the published meeting index, so nothing is downloaded to answer it.
  • A meeting that leaves the archive while it is picked is removed from the selection and said so, rather than quietly shrinking the document you were about to hand over.
  • A portable export offers no Prepare at all. There is no operator behind an exported site to assemble a bundle, and producing a lookalike in the browser would be a second implementation of a published format.
  • GET /jobs returns room_name, the conversation's display name as it was when the job ran. The operator now stores the room as real columns on the job rather than only inside its Talk binding, and recovers them for every recording it has already made, so a room-scoped question is an ordinary query rather than a JSON parse. The conversation's token gets a column but is deliberately left out of the response, for the same reason the Talk binding has always been withheld from it.
  • The meeting list is organised by room. A rooms nav down the left lists every room the archive knows about with its meeting count, alongside "All meetings" and a "No room" group that keeps recordings without a room — an imported file, a simulated run, an old recording — visible rather than hidden.
  • Meetings are grouped under month headings, and each row now shows its room, its date, its speaker count and how long it ran.
  • cassini insight run asks one question of several meetings and writes the answer. It reads the context bundles cassini meetings context --json --out produces — one --context per bundle, in the order you name them — runs the summarise workflow over all of them at once, and writes the result to --out or to standard output. --model overrides the model for that run alone, and --timestamps shows the model where in each meeting something was said so it can cite it.
  • The document it writes carries its own provenance as frontmatter: which meetings it was produced from, with their rooms; the workflow's id, version and the content hash of the exact prompt bytes that produced it; the endpoint and model that answered; and when. A summary has always been able to say only that some model wrote it at some point, which is not enough to tell two documents from different prompt versions apart. --record writes the same record as JSON beside the document, so a grading harness reads one pair rather than parsing prose.
  • cassini insight run exits with a different code per outcome — no endpoint configured, the endpoint refused, the model failed or timed out, a bad request — instead of warning and carrying on. Skipping quietly is reasonable for a summary nobody asked for and never for a document somebody did. The codes are listed in cassini insight run --help. A failed run still writes its --record file, saying what was attempted and why it did not happen.
  • Transcripts now carry cross-track speaker attribution evidence. Every word records how far the loudest other participant's microphone sat above its own noise floor compared with the speaker's own, so crosstalk the decoder transcribed as speech becomes visible instead of shipping unmarked. Where a meeting's own level distribution shows an unambiguous crosstalk population, affected words are flagged lowConfidenceSpeaker; the canonical transcript keeps every word, and the evidence travels with them as attributionGapDb.
  • Generated summaries now exclude words flagged as probable crosstalk. A reader can see and overrule a marked word in the transcript; a summary has no such reader, and a fabricated interjection there becomes a decision somebody supposedly made.
  • Speech recognition now runs behind a backend interface, so a second engine can be added by registering a factory rather than by changing the pipeline. Select one with CASSINI_STT_BACKEND; the bundled sherpa-onnx decoder remains the default, and an unknown selection fails loudly rather than silently transcribing with a different engine.
  • The viewer now surfaces the crosstalk evidence the transcript carries. A turn built entirely from words the acoustic evidence attributes to somebody else is badged "probably crosstalk", and in the exact-word view individual flagged words are de-emphasised with a dashed underline and a tooltip saying how many dB louder the other microphone was. The words are still shown and still seekable — the reader can listen and decide.
  • Added a standalone waveform challenge miner that finds tricky meeting sections, including overlapping speakers, without rerunning speech recognition.
  • A published meeting now records how its word ends were decided, as provenance.wordTimings.endsBoundedByAudio in the portable-meeting manifest. When the key is absent, a consumer may repair an over-long word by clipping it towards the meeting median; when present, the end was measured against audio and must be preserved.
  • A Settings surface in the app, beside Browse and Operator (admins only). It hosts transcription quality — moved off the operator page — and the new LLM settings: endpoints, whether summaries run and on which endpoint and model (with a fetched model list). Each endpoint carries its own request bounds — timeout and response-token limit — with empty fields meaning the defaults (900s / 4096).
  • The app keeps its own LLM settings (llm-settings.json beside the job database; GET/PUT /settings/llm): named endpoints, and the summary policy. Switching between a hosted provider and a self-hosted model server is one settings change, with no ExApp redeploy. GET /settings/llm/providers/{id}/models lists what an endpoint serves. API keys are stored server-side and never returned.
  • Generate an insight from inside the app. Pick meetings in the browse list, open Prepare context, and the panel now offers a Generate card under Copy and Download: choose one of the templates this deployment ships and press Generate. The transcripts go to the AI endpoint you picked and the answer is written into your own Nextcloud files.
  • Insight runs report themselves honestly rather than as a spinner. A run appears in the card the moment you press Generate and carries queued, then running, then a result or a failure — a local model over five meetings takes minutes, and the card says so instead of implying seconds. A failed run names the cause in words you can act on ("No AI endpoint is configured", "The endpoint rejected the request"), carries Retry, and — for an administrator — links straight to the AI providers panel that fixes it. Retry replays the request as you made it, to the endpoint you picked; it falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — which is what makes adding an endpoint a fix rather than a suggestion.
  • Ask your own question. One shipped template, "Ask your own question", takes a question you type and answers it from the meetings you picked: the answer in at most three sentences, the evidence with who said what, and what these meetings do not answer. The question box appears for that template and for no other — every other template asks its own question, and a question sent to one of those would be dropped without being asked, so it is refused rather than silently discarded. cassini insight run gained the matching --question flag.
  • Insights are reachable from the browser: the manifest declares three new USER routes — insights (create a run, list your own), insights/<id> (read one with its document) and insights/<id>/retry. They are the first routes in Cassini a logged-in non-administrator may POST to, and they are USER rather than ADMIN because a run acts only within what that person could already do by hand: it reads the meetings they picked, as them, and writes the answer into their own Nextcloud Files. On upgrade an existing install picks the new routes up from the release itself — no re-registration needed, because every release bumps <version> (see below).
  • An insight you ask for in the app is answered by the deployment on your behalf and delivered into your own Nextcloud Files, under a Cassini Insights folder in your home. You own the file outright, so Nextcloud's own sharing works on it and nothing new decides who may read it. It is deliberately not filed under Cassini/ — that name is the shared recordings Team folder, which is read-only for everybody but Cassini's service account — and an insight is never written next to the recordings.
  • A document is never overwritten. Each file is named for the day, the template and the run it came from, and if something is already at that name the run writes the next free one and records which it used.
  • Every meeting an insight covers is fetched as you, over WebDAV, at the moment the run happens — so Nextcloud's own per-file permissions decide what is in the bundle, and a run can only ever be assembled out of meetings you could already open yourself. A retry re-checks that: a meeting you can no longer read stops the run rather than quietly dropping out of the answer.
  • A failed run says which of the five things went wrong in words that name the fix — no endpoint configured, the endpoint refused the request, the model did not answer, the answer could not be written, or the request itself — rather than reporting one general failure.
  • Cassini now answers published/meetings-list with the meetings the calling account may read, narrowed by an optional date range or room. The viewer and the cassini meetings CLI previously fetched the whole catalogue and filtered it themselves; the same list is now available already filtered, in the catalogue format both already read.
  • A date-filtered list reports how many meetings it dropped because their published date could not be read, so a short list is visibly partial rather than quietly incomplete.
  • Unlike the catalogue it is built from, this list never answers an unreachable Nextcloud with an empty result: a substrate failure is reported as an error, and an empty list means only that the account may read no matching meeting.
  • Cassini Admin's Transcription quality panel reports the device and model the next build will actually use, so a CPU run is stated up front rather than inferred from a failed build. The device override adds a CPU choice alongside Auto and CUDA.
  • cassini meetings context takes several meeting ids and prints one document holding all of them, so a question can be asked of a set of meetings rather than one at a time. The meetings appear in the order you named them, separated by a --- rule, and the --json form carries them as a meetings array whose every entry is exactly the single-meeting document the command has always produced. An id you cannot read — or one that does not exist, which is answered the same way on purpose — fails the whole run: a bundle that quietly dropped a meeting would answer a question asked of all of them using only some of them, and look right doing it.
  • cassini meetings context --timestamps cites each passage's start time, as MM:SS or H:MM:SS past an hour, beside the speaker. Off by default. The timings were already in the --json output and had no way of reaching the text an agent actually reads, so nothing asked to say where something was said could point at it.
  • The context bundle's format has a published schema, spec/cassini-meetings-context-v1.schema.json, and its output is pinned by fixtures. cassini.meetings.context.v1 was a version string with nothing written down behind it; now a change to what the command emits shows up as a diff in a checked-in file.
  • The published meeting index (catalog.json, and so cassini meetings list --json) now records whether each meeting carries a summary and how many words its transcript holds. Both come out of the file the publish step already reads, so anything listing meetings can say which of them have a summary, and size a selection of them, without downloading a single recording. Meetings published before this update carry neither field, which reads as "not known" — never as "no summary" or "no words"; republishing one fills them in.
  • The Cassini app can ask for a set of meetings as one document: GET published/meetings-context?ids=…,…, optionally &format=json and &timestamps=true. It answers with exactly what cassini meetings context prints for the same meetings — the operator resolves what you may read the same way it resolves the meeting list, fetches each recording from Nextcloud Files as you, and runs the CLI over them, so there is one implementation of the document rather than one per reader. An id you may not read is answered 404, identically to one that does not exist; a request that cannot be answered is 502, never an empty document. At most 20 meetings per request, because each one costs a whole recording download.
  • cassini meetings context --local ./Meeting.opus [...] reads portable .opus files off disk instead of fetching meeting ids from Nextcloud, and needs none of the connection settings. Each meeting's id is its file's basename without the .opus, which is how a published archive names one. --catalog ./catalog.json supplies the room id and room name, which live in the catalog rather than in the file — with it, a run over an archive's files produces the same bytes as a run over the same meeting ids; without it a meeting renders with the room id its file was tagged with and no room name. Ids travel as one comma-separated ids, not as a repeated id: AppAPI's proxy collapses repeated query parameters to the last value, which would have answered a three-meeting request with a one-meeting document and a 200. A caller reaching the operator directly may still repeat id.
  • Insight templates is a real panel in the Operator: it lists every template this build ships, and each row opens onto the exact instruction that template sends to the model — the prompt itself, not a description of it — beside the id, version and content hash an insight document records. So a document you found a month later can be traced to the prompt that produced it, rather than to a name that may since have meant something else.
  • A second template, "Commitments and owners": one section per person who spoke, with what they took on and when they said so, then what was assigned to somebody who never answered, then what nobody claimed. cassini insight run --workflow todos runs it, and every other workflow the panel lists can be named the same way.
  • cassini insight workflows prints the same list on the command line, and --json adds each workflow's instruction verbatim. It is how you check what a given image will actually send, without reading its source. The Operator's templates panel is this command's output.
  • LLM settings carry a second step, Insights, beside the meeting summary, so an insight can run on a different provider and model from the one that writes each meeting's summary — a small local model for every published meeting, a larger one for a question you ask by hand. An insight step with no endpoint of its own runs on the summary endpoint, so an existing install gains insights without configuring anything, and the settings panel says which of the two is answering. Each step also names the workflow it runs.
  • Per-step request bounds (SUMMARY_TIMEOUT_SEC / INSIGHT_TIMEOUT_SEC and the matching _MAX_TOKENS), because two steps on two endpoints cannot share one timeout: a CPU-bound local model needs a far longer leash than a hosted API.
  • Each step's workflow field is checked against the templates the image actually ships, and names them when the id is not one of them. The operator cannot know the registry — it lives in the recorder — so a typo used to save cleanly and only fail at the run.
  • An insight now keeps a record of itself from the moment it is asked for, moving through queued, running and then succeeded or failed. A run that takes minutes against a local model shows what it is doing rather than nothing, and a failed one says why it failed instead of stopping silently.
  • Retrying a failed insight is another attempt on the same insight, not a second one: it keeps its id, workflow, meetings, question and the endpoint you picked, and replays that request. It falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — so fixing a missing key on your endpoint, or registering one where there was none, is enough to make the retry work. A retry pressed twice at once starts one attempt and refuses the other. A run the operator was restarted out from under is failed with a message asking you to retry it, instead of staying stuck on "running".
  • An insight now appears in the browse list beside the meetings it was drawn from, and under every room those meetings came from: a question asked of three rooms is found in all three, not filed under one of them. A Meetings/Insights switch above the list narrows to one kind or the other, and the last kind showing cannot be switched off. Insights that are queued or still running are listed too, saying so — an answer that takes minutes is visible while it is being written rather than appearing from nowhere afterwards — and a listing that fails says it failed instead of reading as "there are none".
  • Opening an insight shows the question it was asked, the meetings it read — each of which opens as itself, with the way back to the insight — the answer, and who ran it with which workflow and version. Every source meeting lists the insights that read it, so the link works in both directions. A meeting or an insight you may not read is simply absent from both, with nothing anywhere saying that it existed. The standalone viewer export, which has no operator behind it to have run anything, shows none of this and offers no filter for it.
  • GET operator/setup, which any logged-in Nextcloud user may read, now also reports what this deployment's AI configuration allows: features.summaries (a recording will be summarised) and features.insights (a question asked of a set of meetings will reach an endpoint). One bit each — never the endpoint, the model or the key, which stay administrator-only. It makes the promise in docs/privacy.md — that nothing leaves your infrastructure unless an endpoint is configured — checkable by the people whose meetings are being recorded.
  • The Cassini app now explains an unconfigured deployment instead of staying quiet about it: picking meetings on an instance with no AI endpoint says so in the Prepare panel, and an administrator gets a link straight to the AI providers panel that keeps the meeting they had open. Anyone who is not an administrator is told the fact and that only an administrator can change it, rather than being shown a control that would be refused. The standalone viewer export, which has no operator to ask, says nothing at all.
  • Pick the AI endpoint and model when you create an insight. Prepare now offers a Provider and a Model beside the template, defaulting to the first configured endpoint and its own default model. It is offered to everyone, not only administrators: which of the configured endpoints sees your transcripts is the asker's decision, so GET operator/ai/providers is a USER route carrying ids and display names — the base URL, the key and the request bounds stay administrator-only. A retry re-runs what you asked for, falling back to the deployment's endpoint only if the one you chose has since been removed. This adds a route, so the release that ships it must bump <version> in appinfo/info.xml — AppAPI only rewrites an app's route table on a version change.
  • "Ask your own question", the freeform insight template. Pick it in Prepare, type a question, and the answer comes back in three parts: the answer itself in at most three sentences, the evidence with who said what and when, and — never omitted — what these meetings do not answer. The machinery for a caller-supplied question already existed ({{QUESTION}}, Workflow.TakesQuestion); what was missing was a prompt carrying the placeholder. A template that takes a question is offered wherever one can be typed and withheld where one cannot: it is not offered as the publish pipeline's summary step, which runs unattended with nobody to ask.
  • The Participant and project vocabulary setting now guides the transcriber itself. Terms are applied as sherpa-onnx contextual biasing, so the recogniser can prefer "Librocco" or "Aire Spaces" where it used to invent a spelling. The vocabulary is a decoder prior rather than a text rewrite; its score therefore stays conservative so it does not overpower weak acoustic evidence.
  • provenance.speechToText.hints in the build manifest records how many terms were used, the boost, and the decoding method. When a vocabulary is configured but cannot be applied, it records that and why, so a build that ignored the setting never looks like one that honoured it.
  • CASSINI_STT_HINTS_DISABLED turns biasing off and restores greedy search, so an operator can get back to the transcription they had before; CASSINI_STT_HINTS_SCORE overrides the boost. Both are passed through the deployment compose file.
  • The meeting list's search box now searches what was said in your meetings, not just their names and dates. A matching meeting shows the moments that matched, and clicking one opens the meeting at that point with the transcript already filtered to those lines.
  • When search cannot answer — too many searches, an index still building, or an unreachable archive — the list says so instead of showing an empty result that would read as "nothing matched".
  • Cross-meeting search results now carry a short quote of what matched, so a result says why it matched rather than only where.
  • cassini annotate reads and writes the tags and marks a recording carries inside its own .opus file. show prints them. apply takes a batch of edits — tag the whole meeting or a stretch of it, remove a mark, remove a tag, undo everything one run added, rename a tag — and rewrites the file once, without touching the audio, the transcripts or the summary. Sending the same batch twice changes nothing the second time, and a batch that changes nothing does not rewrite the file.
  • cassini annotate carry moves the marks from a delivered recording onto a newly sealed one, so re-running a meeting does not lose them. When the new recording's audio differs, the marks are kept and flagged as made against different audio rather than redrawn on it.
  • A meeting's tags and marks can be read and changed at annotations/meetings/<id>, by people in the app, the CLI and agents alike. Anyone who can read a meeting can mark it, and every mark is attributed to the Nextcloud account that made it, never to a name the request supplies. The marks are written into the recording itself, so they travel with it when it is downloaded or shared.
  • Two people marking the same meeting at once no longer overwrite each other: a write that finds the recording changed underneath it re-reads the newer copy and applies its marks to that. A meeting you cannot read answers exactly as one that does not exist.
  • GET annotations/tags lists the tags on the meetings you can open, each with how many meetings and marks carry it, and says how many of your meetings it could read marks for. A tag that is only on meetings you cannot open never appears and never changes a count.
  • published/search and published/meetings-list take tag= (a label in any case, or a tag id) and answer only from meetings carrying a mark of that tag. Each search result also lists the tags whose marked stretches it falls inside.
  • Cassini keeps its tag index in its own annotations.sqlite3 beside the job database. It holds nothing that cannot be rebuilt from the marks inside the recordings, and cassini-operator backfill-annotations rebuilds it: recordings that have not changed are skipped without a download, and a fresh index keeps the tag namespace the archive already uses.
  • A recording can carry tags. A tag such as hiring can go on the whole meeting or on a stretch of it, and the marks live inside the recording's .opus file, so a meeting keeps its tags when it is downloaded or shared. The portable format and its schema describe them, and a reader that does not understand them still opens the recording normally.
  • cassini meetings tags lists the tags on the meetings you can read. cassini meetings annotations <id> shows one meeting's marks, and cassini meetings annotate <id> --ops <file> adds or removes them in one batch. Every mark records the Nextcloud account that made it, and whether a person or an agent made it, so an agent's whole run can be undone in one step.
  • cassini meetings search and cassini meetings list take --tag to look only at meetings with that tag. Against a Cassini app that does not offer tags, they fail instead of silently returning every meeting.
  • cassini inspect reports a recording's tags: how many there are, and whether they were made against this recording's audio.
  • You can tag meetings from the app. Each row in the meeting list shows its tags; tag one meeting from its row, or select several and tag them together. A filter beside the rooms narrows the list to one tag or several.
  • Inside a meeting, you can tag a section of the transcript: select its words (or, on a wide screen, drag down the rail beside it), choose Tag selection, and set exactly where it begins and ends with the handles in the text or the arrow keys. Tagged sections are underlined in their tag's colour and, on a wide screen, bracketed beside the text with the tag's name; arrows step from one to the next. The last tag you used is offered again beside Tag selection, so tagging several sections alike is one click each. On a phone, a section's tag sits in the text in front of it, and its details and the arrows sit above the player.
  • A tag has a colour and, if you want, an icon. You choose them where you create the tag, and change them in Manage tags, where tags can also be renamed, merged and deleted. A change reaches the meetings you can read; meetings in rooms you cannot open keep the tag as it was. Colours are stored for the installation rather than inside the recordings, so recolouring a tag rewrites nothing.
  • Each AI provider now carries a default model, set in AI providers. Summaries and insights use it unless the step names its own, and the Prepare panel's model field opens pre-filled with it, so a run can use another model without picking one every time.
  • A failed insight can be retried from its card in the browse list and from its document, not only from the panel that started it. A failed run now records only a reason (reason on the run, one of fourteen tokens); the app decides the wording, so the words can improve without touching stored runs.
  • One dialog, once per install. The first administrator to open a fresh Cassini is told that recordings, and the names of the rooms they came from, will be visible to anyone with an account on this Nextcloud, and given one button that creates the cassini Nextcloud account they are kept in. Nextcloud asks them to confirm their password, and Cassini never sees it; the account's own password is generated, set and discarded. The dialog is recorded as seen by the install rather than by the browser, and only once the account exists: an administrator who leaves it to change the audience first is asked again next time, and so is one whose install cannot create the account at all — where Cassini cannot create it, the dialog says so and points at Operator › Settings instead of claiming to be ready to record. On Nextcloud releases that let an app write to user administration, Cassini creates the account when it is enabled and nobody is asked anything.
  • Operator › Settings › "Who can see recordings", with two audiences and one sentence each: Everyone with a Nextcloud account and Meeting participants. The one in force is marked, and the line beneath says how many recordings already exist and who can see them, including after a switch, when the recordings from before it stay visible to everyone. Where recordings are stored, the service account and the occ recipe are behind "Details for administrators". Where the cassini account does not exist yet, the section says so and offers to create it, whichever audience is in force.
  • The meeting list says who can see the recordings in it, beside the meeting count: "Visible to anyone with a Nextcloud account" or "Visible to meeting participants". Administrators and everybody else see the same chip.
  • A switch shows its four steps while it runs, with a count of the recordings copied, and says the page can be closed; one that was interrupted offers Resume. Choosing Meeting participants without Team folders or Everyone Group offers to install them, and says that Everyone Group adds a group called Everyone to the whole of Nextcloud first. The confirmation says what the switch means for new and for existing recordings, in a number, and the direction that widens the audience carries that number in its own button.
  • Added linux/arm64 multi-arch support to the portable Cassini ExApp container image. Releases, pushes to main, and manual workflow runs now publish multi-platform images (linux/amd64 and linux/arm64) with bundled ARM64 sherpa-onnx runtime libraries and Parakeet ASR models.
  • Recordings a switch left visible to everyone can now be limited to the people who were in the call. Choosing "Meeting participants" on an instance that already has recordings copies them into the Team folder and leaves them readable by every account — Cassini had no way of knowing who was in a past meeting, and said so once, in the page that performed the switch. It now keeps that record while each meeting runs: the accounts, groups and teams with access to the room are captured when a recording starts and again when it stops, and frozen, so a room that gains or loses members later cannot change who may open a recording made before the change. Operator › Settings › "Who can see recordings" lists everything still visible to everyone, shows exactly who each one would be limited to, and limits the ones you tick. Groups and teams are granted as themselves rather than expanded, so reorganising a team is how access moves with it.
  • Recordings it cannot limit say why, instead of sitting there unexplained — recorded before Cassini kept the record, everyone in the call was a guest with no account to grant, or no job of this instance owns the file. Recordings of public conversations never appear at all: they are readable by everyone because the conversation was public. Anything needing a bespoke audience is still Files → Advanced permissions, and a recording you want left open can be dismissed from the list and brought back again.
  • The meeting list's search box now finds meetings by the names of their tags. Typing part of a tag's name brings up the meetings carrying it, alongside matches on the meeting's own name, date and what was said in it.
  • A recording published on its own — in a static export, or embedded in any page — now shows the tags and marked stretches that are inside it, read from the file rather than from a server.
  • <cassini-meeting src="…"> embeds one recording in any page from a single script tag, served from gocassini.com/embed/v1/viewer.js. The attribute contract is cassini-viewer/ATTRIBUTES.md.
  • An embed carries a "Recorded with Cassini" footer by default, linking to gocassini.com. Remove it with the hide-badge attribute.
  • Tag colours and icons are now saved in each recording's cassini.annotations.v1 document. Downloaded recordings and the public viewer retain their tag appearance without an installation-local lookup.
  • Cassini doctor and the operator /status diagnostic endpoint now verify the native speech engine runtime and report whether the Parakeet v3 reference frontend optimization (+cassini-parakeet-v3-reference-v1) is active. If an unpatched library is detected, a non-blocking warning is reported with guidance on how to enable the optimized runtime.
  • cassini dev meetings pull --out <dir> mirrors the meetings a Nextcloud account may read into a local seed pack — catalog.json plus meetings/<id>.opus, the shape a published site has always had. It pulls everything by default, --limit keeps the newest N, and --room/--from/--to narrow it the way cassini meetings list does. --dry-run reports the total size before committing to the transfer, and re-running skips what is already on disk at the size the server reports, so an interrupted pull is resumed by repeating the command. Every request is a GET or a HEAD; nothing is written back.
  • cassini dev stack up --seed <dir> loads a seed pack into the harness, and harness/bin/seed-nc-files.sh --pack <dir> does the same against a stack that is already running. A local development stack can now be brought up holding a real archive instead of a synthetic fixture, which is what the meeting list, cross-meeting search and insights actually need to be worked on. Seeding is a filesystem copy plus occ groupfolders:scan rather than a WebDAV upload, so 2 GB of recordings are copied and scanned in about four seconds, followed by a permissions pass of roughly half a second per meeting. The catalog is merged rather than replaced, so meetings a stack recorded itself survive a seed.
  • Seeded meetings are given an explicit read grant, and are therefore readable by every account on the stack, which the seeder says on every run: production's per-meeting permissions name production accounts and do not travel with the data. They also carry no operator job history. Neither affects the published surfaces, and both are stated so that no access-control change is tested against a seeded archive.
  • Recordings now carry a wall-clock anchor per audio track. Each track gains first_packet_wall_ms and first_timeline_ns MKV tags: two readings of the instant the track's first packet arrived, one in recorder wall time and one in meeting time. Because both come from the same monotonic clock, a later stage holding a wall-clock instant can convert it to a position on the meeting timeline exactly, without reopening the session artifact. The tags are emitted as a pair or not at all, and a recording that predates them reads as unknown rather than as the epoch. A third tag, first_rtp_timestamp, is recorded for triage only — Janus rewrites the timestamps it relays to each subscriber, so it is not the sender's clock and cannot place a sender-side time.
  • The selection bar's count opens a list of every meeting you have picked, including the ones the current room or search is hiding. Open a meeting from there, or unpick it.
  • The scripted transcript role, for authored text a recording is a performance of: a read script, a song's words. It is not a transcription, so it never names a source transcript.
  • Recording checks in Operator → Publish pipeline guide administrators through storage, processing, Talk authentication and handoff, followed by a real Talk recording and playback confirmation. Existing recordings remain accessible.
  • Persist the Talk internal secret securely on Cassini's volume, with deployment configuration taking precedence. Diagnostics expire and remain advisory; missing local credentials are reported before recording starts.
  • Provide installation-specific host commands and copyable provider requests, plus first-ExApp, ARM64/amd64 and AIO persistence guidance.

Changed

  • Skip standalone recorder integration jobs on PRs confined to the operator, frontend, microsite, or PR-conflict automation. Shared and unknown inputs still run the scenarios, and main pushes retain full coverage.
  • Storage that does not match the recorded audience is reported as a mismatch that refuses to publish and names both ways out. A Team-folder list Cassini cannot read counts as an unanswered question rather than as "there is no Cassini Team folder" — the folder list arrives with every folder on the instance in one response, so a single unrelated folder in an unexpected shape used to make the whole answer undecodable and indistinguishable from an empty one.
  • Cassini no longer builds Team folders or their ACL topology on its own. On every enable it checks what is there and reports what is not; it makes those changes only when you ask for them. A half-built setup can no longer end up looking healthy.
  • Talk refuses to start a recording when the storage it would be published to is missing a named prerequisite, instead of capturing the whole call and failing afterwards. The moderator sees Talk's own "The recording failed" message; the reason, and the command that fixes it, are in Cassini.
  • Installing the two Nextcloud apps is the one step Cassini still cannot take for you, and it now says so instead of implying otherwise. Nextcloud requires an administrator's password on that particular request, which Cassini declines to handle; it attempts the install through its own backend — which works on Nextcloud 32.x, 33.0.0–33.0.5, 34.0.0, and on any release where allowed_no_password_confirmation_ranges is set — and otherwise links you to Nextcloud's own Apps page. When the App Store refuses, the message says plainly that Nextcloud reports the same error whether the app is unknown, incompatible or the store is unreachable, and that retrying inside five minutes will say the same thing.
  • backfill-nc-files refuses to run where recordings are visible to anyone with a Nextcloud account, instead of writing access rules into a folder where they mean nothing. Everything it writes is protected by Team-folder permissions, and those recordings live in the cassini account's own folder where every signed-in account can already read them — so the migration would be at best a no-op with side effects, and at worst would re-derive an audience for recordings an administrator had deliberately left open. The refusal names the audience, writes nothing, and points at the setting that actually moves an archive between the two.
  • The recordings-permissions, install and privacy documents now describe both audiences. The upgrade section in particular had the Team-folder path collision backwards — a Team folder takes the Cassini path and Nextcloud renames the colliding home directory, rather than the mount being remapped — so its instruction to move the old folder out of the way first was unnecessary.
  • A keyless OpenAI-compatible endpoint now switches meeting summaries on: LLM_BASE_URL alone is enough, and OPENROUTER_API_KEY is needed only when the endpoint requires one. Previously both had to be set, so a self-hosted model server silently produced no summary. On upgrade, an install that had LLM_BASE_URL set without a key starts producing summaries through that endpoint from its next build.
  • With no key configured the recorder omits the Authorization header entirely rather than sending an empty bearer token, which some self-hosted servers reject. New request bounds for slow local models: CASSINI_LLM_TIMEOUT_SEC (default 900) and CASSINI_LLM_MAX_TOKENS (default 4096).
  • cassini meetings search says where a speaker id comes from — the speaker= value a previous result printed — so --speaker can actually be used. The ids are opaque and are not meant to be typed from memory.
  • The control panel names each recording after its conversation. A recording was labelled by its room token — Call 3qbs6123vxx — which identifies the call but not which conversation it was. Rows and the detail heading now show the Talk conversation's display name wherever the operator has it, keeping the token label as the fallback for a non-Talk recording, one whose room-name lookup never completed, and anything recorded before this release.
  • Opening a meeting slides it in over the list from the right, instead of replacing the list with a second column; on a phone it arrives as a bottom sheet. Close it with the × or Escape, and the list is still there, in the same place, with the same room selected.
  • The search box moved to the top of the list and filters within the selected room. It still matches a meeting's name or date, and the room and search narrowings each show as a chip you can clear.
  • A word can now be flagged lowConfidenceSpeaker only when its own track sits well below that speaker's own speech level (the quiet-owner signature). Genuine double-talk between microphones with different signal-to-noise ratios measures large gaps but is never flagged, and the crosstalk threshold is estimated on the quiet-owner population alone. The per-word attributionGapDb measurement itself is unchanged and still annotates every word.
  • The meeting manifest records what the attribution stage did: whether it ran, the mode (annotate/drop/disabled), words measured, flagged and dropped, and the estimated threshold when one was found. A build that deletes words in drop mode is no longer indistinguishable from one that never saw them.
  • CASSINI_STT_BACKEND, CASSINI_ATTRIBUTION_DISABLED and CASSINI_ATTRIBUTION_DROP are documented and passed through the deployment bundle; an unknown backend id now fails the build before any decoding or model download instead of after the mixdown.
  • CASSINI_ATTRIBUTION_DROP=1 removes flagged crosstalk words from the transcript itself instead of only marking them, and CASSINI_ATTRIBUTION_DISABLED=1 skips the measurement. Marking is the default: real crosstalk overlaps the level range genuine simultaneous speech occupies, so deleting canonical words on a threshold is opt-in.
  • The word being spoken now lights up instantly instead of fading in. The fade lasted about as long as a short word, so the highlight was always part-way through changing and never quite agreed with what you were hearing; it now tracks the audio exactly. Hovering a word to seek also highlights it immediately.
  • Overlapping speech reads as speech again. A turn is now one paragraph, however many pieces the producer cut it into: when two people talk over each other their sentences no longer alternate in one-to-three-word fragments. A short reaction someone made while another person was talking appears as a quiet chip where it happened, inside the sentence it landed in, and a turn that genuinely ran over another says so by naming the other speaker.
  • Durations are gone from the transcript. A badge reading "0.2 s during Ivan" on every fragment was precision nobody acts on, and it made the page harder to read than no marking at all. The measurements are still there for anything that wants them; they are no longer shown.
  • captions.vtt is now always produced, from the transcript itself. It used to exist only when an LLM endpoint was configured, as a by-product of cleanup.
  • LLM_BASE_URL, OPENROUTER_API_KEY and LLM_MODEL in the deploy environment now only pre-fill the LLM settings on the app's first start; after that the stored settings are what every build receives.
  • A non-administrator sees no template picker: the template registry is admin-only, so their insight runs whichever template the administrator configured, and the card says so rather than offering a control that would be refused. On a deployment with no AI endpoint the card does not appear at all — the existing "No AI endpoint is available" notice takes its place — and in a standalone static export, which has no operator to ask, neither appears.
  • docs/exapp-update-constraints.md answers the question it used to flag as unverified: an in-place app_api:app:update does rewrite AppAPI's route allowlist, but only when the manifest it is handed carries a new <version> — with the same version it short-circuits on "already updated" and the allowlist is untouched. Routes therefore behave unlike deploy environment variables, which stay creation-time. The rule for release review is that a release changing <routes> must bump <version>, which scripts/bump-exapp-version.sh already does. The measurement is written up with its evidence and is re-runnable as harness/bin/check-route-refresh.sh.
  • The privacy note now covers insights: a question sends the picked meetings' transcript and summary text to the configured LLM endpoint, the call uses the instance's API key — so the request reaches your provider attributable to the deployment, not to the person who asked — and the answer is written into that person's own Nextcloud Files, never beside the recordings.
  • The subprocesses an insight runs no longer inherit the operator's own credentials. The AppAPI shared secret, the Talk recording and signaling secrets and the operator API token are removed from their environment: none of them is read by any cassini command, and the AppAPI secret in particular is what lets a process act as any account on the instance. The step that only assembles the meetings loses the LLM credentials as well, since it never calls a model.
  • An insight is bounded in the three ways that can run away: how many meetings one may cover, how long one attempt may take before it is stopped, and how large the answer may be. Each limit is a refusal that says so, never a silently shortened document, and only two runs are performed at a time — the rest wait, and say they are queued.
  • The in-Nextcloud viewer now reads its meeting list from the app's list endpoint rather than the published catalogue file, so a Nextcloud outage no longer empties the list: it keeps showing the meetings it last loaded and says what went wrong, instead of silently rendering as though the account had no recordings. Standalone exports and the static viewer are unaffected, and the viewer falls back to the catalogue file against an app that does not serve the endpoint.
  • Transcription runs on the CPU when the host has no GPU. Operator builds resolve their device per run — CUDA when the image carries the CUDA runtime and an NVIDIA device is visible, CPU otherwise — instead of blocking every build on a GPU-less install. Recordings already stuck in build/blocked recover with Rerun in Cassini Admin; no image or deploy-daemon change is needed.
  • Each image carries the model of the tier it runs by default, and any other tier downloads once into a model cache on the persistent volume. The portable image bakes the 0.6B int8 model, the GPU image bakes fp32, and both stay small. A download survives a container recreate, and a host with no network egress gets a clear error that names the missing model. Cassini Admin shows the size of that download before the first build of the tier.
  • cassini meetings list now asks the app to filter by date and room, instead of downloading the whole catalogue and narrowing it locally. Against an app that does not offer that yet, it falls back to the previous behaviour, so an older CLI and a newer one both keep working.
  • cassini meetings list, rooms, fetch and context now report an unreachable recordings archive as a failure rather than as an empty result. Previously an outage was indistinguishable from "this account has no recordings" — or, when fetching by id, from "no recording you can read at that id".
  • Stop reasons and recording durations appear in run metadata. The operator view formats raw reason codes into plain labels and displays the total recorded duration in sidebar cards and detail panes.
  • cassini meetings context fetches the recording catalog once per run instead of once per meeting, so every meeting in a bundle is resolved against a single view of what you are allowed to read.
  • --keep-opus is refused when more than one meeting is asked for, and so is the same meeting id given twice. Both used to be silently meaningless; one file cannot hold several recordings, and the same transcript twice spends the context budget on one meeting while reading as coverage of two.
  • The publish pipeline's summary step and the summarise insight now run the same prompt file, not two copies of it. The summary is an insight like any other, so it names the template it runs; the bytes it sends are unchanged.
  • A prompt whose bytes are not compiled into the image is not listed and cannot be named — a template you can select is a template that will run. Two of the templates drawn in the design have no prompt authored yet and are therefore absent rather than shown as unavailable.
  • Editing a shipped prompt now fails the lint check until its version is bumped and its hash re-pinned. Prompt files are markdown, and a markdown-only change used to run no tests at all while changing what the product sends to a model.
  • CASSINI_SUMMARY_DISABLED no longer blocks cassini insight run. It turns off the summary written when a meeting publishes; an insight is a document you asked for by name, and turning insights off is removing the endpoint.
  • Configuration lives inside the Operator surface again, behind a left nav, instead of in a Settings tab of its own. Two tabs remain — Browse and Operator — and Operator now opens on a nav with two groups: Console (Recordings) and Settings (AI providers, Publish pipeline, Insight templates). The panels themselves are unchanged; only where you reach them moved. Below 720px the nav is a dropdown, and each panel opens at its own top rather than wherever the previous one was scrolled to.
  • A panel can be linked to directly: #surface=operator&panel=endpoints opens AI providers.
  • The run console splits into a list and a detail pane based on how wide the console itself is, not how wide the browser window is. In Nextcloud the app sits beside Nextcloud's own navigation, so a window wide enough by the old measure could still squeeze the run detail into a couple of hundred pixels.
  • Registering your first AI endpoint now switches meeting summaries on, pointed at it. An install that has just configured an endpoint and still publishes meetings without summaries has done the work and not got the feature. It fires once — on the save that takes a deployment from having no endpoint to having one — so an administrator who turns summarising off keeps it off, whatever they save afterwards. The AI providers page says so before you save, since it is a decision about what leaves your deployment.
  • AI providers configures endpoints and nothing else. Adding one is a form rather than a hidden row of fields, a saved endpoint reports whether its model list actually came back (and says so plainly when it did not, since an endpoint with no /models route still answers completions), and the page says which endpoint an insight will actually reach.
  • Publish pipeline is now the whole pipeline in the order it runs: detected hardware, transcription quality, summarisation, and the device override last. Summarisation moved here from AI providers — it is a step between the call ending and the meeting being published, and it was only living under endpoints because its endpoint is.
  • The summary step names an insight template from the registry instead of asking for a workflow id as free text, and its model field is a combobox that loads the endpoint's models when you open it rather than behind a "Load models" button. Typing a model the list does not have is still accepted: the list is the endpoint's, not a limit on what you may send.
  • The "this needs an AI provider" state is now the same compact card wherever it appears — the summarisation step, Insight templates, and the Prepare panel — with the button that fixes it. Anyone who is not an administrator still gets the explanation instead, because that panel would refuse them.
  • Create an insight in the Prepare panel is the template you pick and what it will ask, in one box: choosing a template shows the question it puts to the meetings and the shape of the document that comes back, or a question box where the template takes one. It opens on a real template rather than on a row reading "this deployment's default", which named nothing and read as a setting instead of a choice.
  • The Prepare panel no longer lists previous insight runs. It was a job console in a panel whose subject is the meetings you just picked, and a second place insights were listed — free to disagree with the browse catalogue behind it. The card now shows only the run it starts, which is feedback for a button press; every insight, that one included, is a card in the browse list as soon as it exists.
  • The publish pipeline's summary step opens on Meeting summary — the template it actually runs — instead of a "Default (the one Cassini ships)" row standing in front of it.
  • Meetings / Insights moved out of the list header into the rooms rail, under "Show", beside the other narrowing of the same archive, with a clearer gap between the two lists.
  • A meeting's header now carries its name, room, date, length and speakers, and stays put while the transcript scrolls under it. The insights a meeting was used in read directly under its summary rather than in a strip below the whole transcript, and an insight's own header carries its room, date and the model that answered.
  • Insight cards in the browse list are filled rather than outlined, so they read as a different kind of thing at a glance rather than only by the rule down their edge.
  • Upgraded sherpa-onnx Go bindings and native runtime libraries to v1.13.7 across the recorder and container base images.
  • Transcription output changes for every meeting, not only for the listed terms. Hotwords are only read under modified beam search, so the balanced and best quality tiers now decode with beam search instead of greedy search. It is a different decoder and it decides differently on marginal audio: measured on real meeting audio it lost a sentence on one clip and gained words on another. Decode cost is 1.3x to 1.5x, still far faster than realtime (worst measured realtime factor 0.091, so an hour of meeting decodes in about five and a half minutes on CPU). Memory is unchanged.
  • The fast quality tier is unaffected. It runs a CTC model, which sherpa-onnx cannot bias, so it keeps greedy search and records the vocabulary as not applied rather than ignoring it silently.
  • The vocabulary previously steered the LLM cleanup prompt and explicitly did not affect the transcript. It now affects the transcript and nothing else.
  • Decoder hints need a model bundle that ships bpe.vocab. Bundles without one record the vocabulary as not applied, and say so, rather than decoding unbiased in silence.
  • Re-running a meeting no longer erases the tags and marks added to its recording after it was published: the rerun carries them into the recording it delivers. Marks made against audio the rerun changed are kept and flagged as no longer matching, never dropped. If marks are being written at the moment of the rerun, the publish fails and asks to be re-run rather than overwrite them.
  • cassini-operator backfill-search recognises an archived recording that has gained marks since it was delivered as the same recording as the local copy — it compares the audio, not the whole file — and keeps indexing it from the local transcript's own segments.
  • The UI-only branch preview job is temporarily disabled (D-739). Every meeting in the published archive uses the exact-pcm audio integrity policy retired on 2026-09-02, so the export can read none of them and the preview failed for every branch on every push, over data no branch can fix. docs/runbooks/repair-published-archive.md is the repair procedure, and re-enabling the job is one line and part of closing D-739. Opt-in processing previews are unaffected: they build their meetings from raw recordings.
  • The endpoint's address stays with administrators. The insight document's provenance records the model and endpoint kind but no longer its base URL; an unnamed endpoint is shown as "Endpoint 1" rather than by its host; and a model list that cannot be fetched is reported to non-administrators without the URL.
  • Registering your first endpoint switches summarising on exactly once in a deployment's life. Removing every endpoint and adding one back no longer switches it on again for an administrator who had turned it off.
  • Removing an AI endpoint asks for confirmation first, naming the endpoint, that its stored key is destroyed, and which steps stop running on it.
  • Finding words inside a meeting now counts the matches, steps between them, and marks where they are on the rail beside the transcript. Hiding the turns that do not match is now a switch, off by default, instead of what always happened.
  • Generating an insight closes the Prepare panel and puts the new run at the top of the browse list, where it updates like every other insight.
  • The selection is bounded to the operator's 20-meeting cap before a request is made, with a sentence saying how many to unpick.
  • Cassini works out who can see recordings when the app is enabled, instead of refusing to record until it is told. A fresh install records as "everyone with a Nextcloud account", from the moment the cassini account exists. An install whose Cassini Team folder already holds recordings keeps "meeting participants", without asking and without moving anything, and recordings stranded in the other root are counted and offered a move. Cassini never widens an existing archive on its own, and "nobody has chosen yet" has stopped being a state an install can be in. A mode resolved this way reports mode_source: "resolved_on_enable"; a mode an older version recorded on its own, or an interrupted switch left behind, is kept and no longer needs confirming. Where Cassini cannot read the recordings it already has, it records no mode, says so on GET /operator/status, and looks again the next time the app is enabled; recording still goes ahead. The CASSINI_STORAGE_MODE deploy option is unchanged and still takes precedence.
  • A problem is one sentence now. Where storage is genuinely broken, Cassini says it cannot save recordings right now, names the cause in words, and offers Try again and Show details; the step, the paths and the commands live inside that disclosure. Somebody who is not an administrator gets the same sentence and no buttons, and nobody is told that an administrator has to decide anything. The cause is written by the operator, one sentence per check, so /status and the app say the same thing about the same failure. A check that has not run yet, as after a container restart, is a neutral band rather than a warning, and the meeting list stays where it is.
  • The storage API reports the install as well as the archive. GET /operator/storage carries first_run, answered once per install, and migration with the phase and counts while a switch runs. GET /operator/setup carries the mode, so the audience can be named for people who are not administrators. recordings_access on /status gains a cause sentence, and /setup carries the form of it that names no account, path or app.
  • The App Store description, the microsite and the documentation say who can see a recording before they say how it works, with the default first and the opt-in after it, in the same words the settings section uses.
  • The warning about widening no longer overstates how permanent it is. Switching to "Everyone with a Nextcloud account" said the recordings it opens will not be re-limited by switching back, which is still true and no longer the whole story: they can be limited again from the same settings section.
  • A public viewer is read-only by construction: the marks in a recording are drawn, and no control that would add or remove one is shown. Previously the viewer hid tags entirely when there was no operator to write them.
  • Tags that no vocabulary knows — every tag in a recording read on its own — are dealt distinct colours from the palette instead of each hashing its own id, which collided often enough to show several tags in one colour.
  • Rewrote the root README around the Nextcloud app: what you get, what leaves your server, the meeting file, requirements, install steps, and the CLI and agent skill.
  • Replaced the documentation site's pages with a set about installing and running the app, and added docs/cli.md for the CLI from a checkout.
  • Renaming or restyling a tag uses the existing visibility-scoped background rewrite flow. Only recordings the caller can read are updated; inaccessible recordings keep their saved appearance.
  • Graceful degradation on unpatched sherpa-onnx runtime. Parakeet v3 speech transcription no longer halts when running against an upstream or unpatched libsherpa-onnx-c-api runtime. Instead of failing recognizer initialization, Cassini logs an informative warning, gracefully falls back to the standard decode profile (defaultVADDecodePolicy()), and records referenceFrontend: false in provenance metadata. This keeps transcription available when the reference frontend is absent.
  • Cassini images now build the newest stable, officially signed FFmpeg source release selected at image-build time. CPU and CUDA image smoke tests gate the media capabilities Cassini requires instead of pinning a release number.
  • Release images now build with the FFmpeg version the current main image was already built and gated against, instead of resolving the newest upstream release again at tag time. Main keeps tracking the newest stable release, so an upstream regression still surfaces there first — but a release is never the first place a given FFmpeg appears, rebuilding a tag reproduces the same binary, and rolling back a release no longer silently rolls back FFmpeg with it.
  • Playback is shown in neutral rather than the accent colour: the word being played is inverted, and the play button, the scrubber and the Follow switch match it. The switch is called Follow at every width, turning it back on takes you straight to the audio, and the "Auto-scroll paused" badge is gone.
  • A meeting reads with less around it: its details sit on two lines under the title, Summary, Insights and Transcript share one heading, and the transcript's search counts its matches and steps between them inside the field. The meeting always leaves a strip of the list beside it to click back to.
  • The meeting list, the rooms rail and the Browse and Operator tabs follow the Cassini design. Dates name the day of the week, and who can open recordings is stated beside Rooms rather than among the filters.
  • The Operator's sidebar is a drawer on a phone, and its settings pages sit on the page rather than in nested cards. First-run setup asks who can see recordings as a choice, and its problems read as warnings with a way forward.
  • Prepare warns only about what stops it: a selection over the limit, or a meeting published in the older format.
  • Portable meetings identify their recording from canonical compressed Opus packets, making integrity checks independent of decoder implementation details.
  • Portable packing now converges on FFmpeg's stable Ogg end-trim granule before embedding that compressed identity, including multi-speaker mixes whose first metadata remux normalizes a few trailing samples.
  • Portable word transcripts no longer carry origin roles or source links. The packer, inspector, viewer, and static exporter accept entries with no role and ignore legacy origin metadata. Display transcripts still pair with their source words; withdrawn cleanup entries are skipped. The reference schema and format documentation also drop unused chapters and duplicate meeting hints.
  • Packed meetings use the published portable meeting format: CASSINI_FORMAT=org.cassini.portable-meeting/1, manifest "version": 1, and a resolvable CASSINI_PAYLOAD_SCHEMA served at https://format.gocassini.com/. Readers accept this contract directly and reject any other format.
  • Portable meeting producers and readers now implement only the published version 1 contract. Transcript bodies contain word items, the media digest comes from integrity.opusAudioSha256, and unsupported shapes are not repaired or upgraded.
  • Add opt-in recording-first scheduling: defer builds/sealing/publication while recorders are active, and yield running builds without spending resource retry attempts or losing captured media.
  • Serialize recorder finalization across subprocesses under recording priority.
  • Credit conservatively reclaimable cgroup file cache during memory admission, fixing waits caused by clean cache rather than memory pressure.
  • Add explicit concurrent/recording-first/auto policies. Auto permits recording overlap using live host/cgroup CPU and memory headroom, with a GPU-aware host CPU budget. Existing deployments retain concurrent behavior by default.
  • Simplified Go helpers and workflow context handling; shortened repetitive implementation comments.
  • Moved the removed Python transcriber's architecture notes into docs/history/.
  • Linux amd64/arm64 CPU builds now use prebuilt Parakeet v3 frontend libraries from codemyriad/sherpa-onnx-go-linux v1.13.7-cassini.4, with matching upstream v1.13.7 Go bindings. Normal developer, Docker and CI builds no longer compile sherpa-onnx. CUDA source builds consume a checksum-verified fork commit instead of applying a local patch. Other platforms retain stock libraries and the graceful fallback. Operator runtime diagnostics coalesce concurrent probes and refresh cached results, including unavailable runtimes, after 30 seconds.
  • Tag edits appear immediately after saving, while recordings update in the background. Delayed recording updates remain visible and can be retried.
  • Pending tag edits now survive operator restarts. Keep the annotation database on durable storage; rebuilding it from recordings cannot recover edits that have not yet reached those recordings.
  • Repairing a recording with the same audio restores saved tags. Replacing its audio clears the old annotations.
  • Recordings skipped during initial annotation import retry automatically and when listed or opened.
  • Large valid annotation documents can update their recordings; permanent recorder rejections show a blocked status instead of retrying indefinitely.
  • An older background status error no longer clears tags saved by a newer edit.
  • Rebuilding tag/search rows restores pending edits from the durable annotation database, even while the archive is unavailable.
  • Browsing the app prepares newly seeded recordings for tagging, imports wait for download capacity instead of being skipped, and bulk tagging automatically retries temporary preparation responses.

Removed

  • Word-level playback highlighting is gone from the meeting view. The transcript lit one word at a time and invited you to click a word to seek, which claimed a precision our transcripts do not carry. A line is the unit now: click any line to play from there. The "Exact words" toggle and the word/mixed/passage timing badge go with it.
  • The LLM readable-transcript cleanup step, which was shelved. Transcripts publish as transcribed, with speaker attribution and paragraph breaks but no model rewriting the words. Gone with it: CASSINI_READABLE_STRICT_BATCHES, the --strict-readable-cleanup build flag, the cassini-readable component, and the transcript.readable.v1.json producer. Meeting summaries are unaffected and remain the only LLM step in the pipeline. Files that already carry a readable transcript still open: the portable format keeps reading them.
  • The Model override field. It accepted only the three models the quality tiers already select, so it duplicated the tier control with no discoverability — and pinning an int8 model alongside CUDA produced a run that fragmented back onto the host CPU while reporting a GPU. Quality and device are now the whole of the policy. A model_override left in an existing settings.json is ignored.
  • The Insights step in AI providers. It configured "run insights somewhere other than the summary endpoint" while looking like the switch that turns insights on, which it never was. The field is still persisted and served, so PUT operator/settings/llm can still point insights at a larger model.
  • The Prepare panel no longer says that a meeting "does not record whether it has a summary". It is true of every meeting published before the catalog carried the field, it changes nothing about what the bundle contains, and it buried the one gap that does stop a bundle being assembled.
  • Removed the top-level ops/ directory; moved AppAPI registration helpers and batch processing scripts to scripts/, relocated production deploy tooling to codemyriad-systems, and purged stray out-of-scope host configuration.
  • The Setup tab and its wizard are gone. Cassini no longer opens on a question that blocks every recording until somebody answers it, and a link to #surface=setup now lands on Browse.
  • Removed an accidentally tracked harness executable; harness scripts continue to run its Go source.
  • Removed obsolete diagnostic reproduction tests; the production setup paths retain regression coverage.

Fixed

  • Manifest validation now rejects environment overrides whose names appear only outside environment declarations or inside XML comments, preventing AppAPI from silently dropping those values.
  • Tagging a selection of meetings now uses one atomic database batch and displays all updated rows together. Safe retries reuse the batch request ID; recordings continue syncing individually across the archive workers.
  • CI no longer fails when a third-party apt repository preinstalled on GitHub's runner image serves a bad package index. Every workflow install step now runs scripts/ci-apt-update.sh, which drops apt sources that are not the Ubuntu archive before updating, so a vendor we never install from cannot take a build down. On 2026-09-09 a corrupt Google Chrome index turned nine jobs across four workflows red on main with apt-get update exit 100; the previous remedy named packages.microsoft.com at three of the nine call sites, and this replaces it at all of them.
  • Restarting the Cassini container no longer stops it publishing until an administrator disables and re-enables the app. Cassini checked how Nextcloud is set up on the app's enabled edge, and that check was the only thing that could mark the storage usable — so a plain restart left it reporting an unknown state and refusing to publish, with nothing obviously wrong. It now re-checks at startup as well, for installations that have already recorded which storage model they use. A first install is unaffected: Nextcloud rejects the calls until registration finishes, so that case still waits for the enabled edge, which fires moments later.
  • cassini-operator backfill-search now indexes recordings this Cassini has no local copy of, by reading the published recording itself. Before, a meeting whose job record or working copy had been lost — which is every meeting published by an earlier installation, since the recordings outlive the app's own volume — was skipped entirely and silently absent from search. On the demo archive that was 25 of 30 meetings. Those are indexed at coarser granularity, and each result says which kind it is.
  • cassini-operator backfill-search now verifies a local transcript against the checksum the archive itself records for the delivered recording, instead of a digest from the job database that could never disagree with the local copy. A meeting whose local transcript belongs to a re-run that never published is detected and indexed from the published recording instead, so search only ever cites words that are in a recording you can play.
  • A read failure during backfill-search — a timeout, a busy Nextcloud — no longer removes a meeting's existing search entries. It is reported as failed and re-running is the fix; entries only change on evidence about the recording itself.
  • backfill-search no longer reads the job database at all, so it can run while the operator is publishing; the old database-is-locked failure is gone. Where the delivery recorded a checksum, a re-run now confirms an unchanged meeting with one metadata query instead of re-downloading the recording.
  • Speaker attribution now calibrates a participant's noise floor across all of their packet streams. A rotation or rejoin produces a fresh stream, and a short one carrying only ongoing speech had that speech as its own floor — so the real speaker measured as barely audible and a quiet track carrying only their bleed won the word by 20 dB.
  • Words keep their attribution provenance through LLM readable cleanup, so summaries actually exclude probable crosstalk. Readable cleanup and summarisation normally share one configured model, so losing the flag there made the summary protection a no-op in exactly the ordinary path.
  • Filtering no longer discards older segments that carry text but no word list.
  • A participant's noise floor is now the quietest floor any of their streams established, rather than a percentile over their pooled frames. Pooling was duration-weighted, so a long speech-only rejoin swamped the short stream that had actually seen the microphone idle, and crosstalk beat the real speaker again.
  • Readable cleanup maps each rewritten word to the single best-overlapping source word, so one contradicted word can no longer flag a legitimate neighbour and delete it from the summary.
  • Portable meetings now carry the per-word attribution evidence: cassini pack copies attributionGapDb and lowConfidenceSpeaker from transcript.words.v1.json into the portable transcript items (and cassini extract writes them back out) instead of stripping them from the published .opus.
  • The synthetic meeting generator now slides a turn that starts before the same speaker's previous one has finished, instead of aborting the whole run. Aborting happened partway through, leaving earlier participants generated and later ones missing — which is how the committed showcase fixture ended up with real speech for only two of its six speakers.
  • Transcription tests now build meetings with production's shape — late-joining tracks, DTX-sparse tracks, and rejoined streams sharing one participant id — instead of tidy tracks that all start together and are fully dense. The previous fixtures could not express the conditions that two thirds of production streams actually have.
  • The meeting list and header now show the date the meeting was recorded, not the date its file was last processed. Reprocessing an archive no longer restamps every meeting with the rebuild day.
  • Meeting list cards now show the meeting's start time alongside its date.
  • The viewer now shows when two people spoke at once. Overlapping speech used to render as ordinary consecutive paragraphs with no signal at all, and because a turn is cut at every speaker change, a listener's "right" in the middle of someone's sentence split that sentence across two paragraphs with an unrelated turn wedged between them — so a continuous utterance read as two turns and an interruption. An interrupted turn is now drawn as one turn containing the interjection, each turn carries a badge naming who it overlapped and for how long, and playback highlights every turn that is sounding rather than only the latest one to start.
  • Overlap is judged on the words that actually sound rather than on paragraph extents, and a word whose end ran past its own audio is pulled back before anything is drawn. Without that, meetings recorded before the timing fix would light up with overlap that never happened: across nine archived meetings their paragraphs intersect for 94.2 s while their words only sound together for 15.7 s. A meeting whose recording already measured where each word ends says so in its own file, and the viewer leaves its timings exactly as recorded — so a word somebody genuinely held for a second and a half stays that long.
  • A word now ends where its speaker's own audio ends. Parakeet stamps a trailing punctuation mark at the next acoustic onset, and the token-to-word conversion took that mark's timestamp as the word's end — so a sentence-final word could run for seconds while its speaker was silent, and across whoever spoke during that pause, which the viewer then drew as an overlap that never happened. The mark still travels in the word's text; it no longer decides the word's extent. Instead the energy gate that already decides whether a word is real follows the speaker's own track forward from the last spoken token and stops at the last active 10 ms of it, bridging a silence of up to 200 ms as a stop closure inside the word. A word built entirely from punctuation carries no audio of its own and is never extended, and no word may pass the end the old rule gave it. Measured against unmodified main on a 36-minute three-participant meeting (5549 words, the production fp32 model), the longest word falls from 4.07 s to 1.44 s and cross-speaker overlap stamped over a silent track from 4.18 s to 1.69 s, against 1.37 s of audible speech given up across the whole meeting, two words of it losing 100 ms or more. Those are whole-release totals; the final refinement alone — dropping an 80 ms grace after the last active frame — accounts for 0.38 s of that speech and for no word losing 100 ms.
  • Playing a long meeting no longer pins a processor core. The viewer rebuilt the entire transcript's highlight sixty times a second and re-checked every word on screen in order to move one of them, so on the longest published meetings playback ran at about twenty frames a second, the page stuttered while scrolling, and the highlight visibly trailed the audio it was meant to track. The highlight is now worked out once per transcript and touched only when the playhead actually crosses a word, which took the worst meeting in the archive from using most of a core to using under a tenth of one, at a steady sixty frames a second. Clicking a word to seek costs about half what it did on the same meetings. The longer you listened the worse it got, too: playback accumulated duplicate timers until it was running forty-four clocks where one was needed, so a five-minute listen was heavier per second than a one-minute listen. Memory and timer count now stay flat — measured over a continuous five-minute listen on the largest meeting.
  • A long answer is paragraphed again. Rebuilding a turn from the pieces the producer cut also undid the breaks it makes when somebody talks for a while, so one standup update rendered as a single 209-second block of text (the longest rows on that meeting were 209 s, 101 s and 97 s against a median of 8.7 s). The reading unit now follows those breaks where nothing came between two of the speaker's blocks, and overrides them only where somebody else's speech cut a sentence in half, which is the case this rebuilding exists for. Same meeting: longest row 58 s, median 10.4 s.
  • A quick back-and-forth is no longer folded into one turn. Rebuilding a speaker's turn now needs the person who spoke in between to have actually overlapped or touched it: if they said their piece in clean air and stopped, both people kept their own turn, in the order they spoke. Without that, five real turns of a fast exchange collapsed into one row with an answer rendered before the question it answered.
  • Playback follows the paragraph it is reading. It used to follow whichever producer fragment was sounding, so during two people talking over each other the page smooth-scrolled between the pieces of the same rebuilt paragraph every few hundred milliseconds.
  • The "probably crosstalk" warning is judged once over everything a turn actually said, instead of once per fragment. One flagged fragment no longer condemns a whole genuine turn, and a short reaction that is entirely flagged keeps its warning now that it renders as a chip.
  • The viewer now shows the meeting summary the recorder seals into the portable .opus. The summary was already inside the file (the summary.md attachment that cassini meetings reads), but the web viewer and the in-Nextcloud app never displayed it, so a meeting with a summary opened as transcript-only. It renders as Markdown in the existing Summary panel; a file without one, or with a damaged attachment, still opens without a panel rather than failing.
  • Selecting Fast now works on a CPU host. The pre-build environment check demanded transducer model files of every non-fp32 model, so the 110M CTC model — which ships a single model.int8.onnx — failed validation although it was bundled and correct.
  • An int8 model can no longer be pinned alongside CUDA. Those graphs fragment back onto the host CPU under the CUDA execution provider, so the combination reported a GPU run while doing something else; it is now refused when saved and when a build is admitted.
  • A stored device_override=cpu is preserved instead of being erased on every operator restart. Pinning cuda on a host without a usable GPU still blocks the build with an actionable message rather than silently using the CPU.
  • Filter values no longer appear in cassini meetings error output. The request URL is printed when a call fails, and its query string was not being masked.
  • Routine recording stops no longer trigger an error banner in the operator view. Successful runs with exit code 0 and normal stop reasons do not display the red error alert with internal runtime cancellation messages. The view displays an error alert only when a run or attempt actually fails.
  • Recordings from Nextcloud Talk display the call room token. The operator view reads the Talk base URL and room token from the trigger request. Sidebar cards and run details now show the call token and a direct link to the room instead of generic placeholder text.
  • A meeting link's timestamp survives a trip through the Operator surface. The shell writes its own hash params — the selected run, and now the selected settings panel — after the viewer's t=, and the viewer only reads t= when it is the last thing in the URL. So opening a recording at a moment, stepping into Operator, and coming back left a link that still named the moment but no longer jumped to it. The shell now keeps t= last, the way the viewing layer already does when it writes a link of its own.
  • Registering an AI provider is now the whole of the setup an insight needs. Before this, an administrator could save an endpoint and still be told "No AI endpoint is available" everywhere in the app, because an insight only resolved an endpoint through a pipeline step somebody had also switched on. An insight now resolves, in order: the endpoint its own step names, then the summary step's, then any registered provider — and the operator hands that resolved endpoint to the run, so what the settings page says and what the run receives cannot disagree. The rule that follows is stated in the AI providers panel and in docs/privacy.md: every endpoint you register is one an insight may reach, and removing them all is how insights are switched off. Summarising stays a separate opt-in that runs on every recording.
  • An insight document no longer opens with a paragraph of YAML. The recorder writes its provenance — which meetings, which prompt version and hash, which model — as front matter at the top of the file, and markdown renders --- as a horizontal rule, so twenty quoted keys arrived on screen above the answer somebody asked for. The provenance is still in the file, and the panel shows the same facts in its header and its "This run" list.
  • Hardened AppAPI registration lifecycle handling in scripts/lib-exapp-register.sh: stop immediately if unregister fails prior to re-registration, verify disabled status before re-enabling during enable cycling, reject arbitrary moving or non-release image tags by default, and require jq with valid manifest index parsing in preflight.
  • Restored word-level playback highlighting, hover and click-to-seek in the meeting viewer, including simultaneous speakers and inline interjections. Keyboard activation seeks to a focused word without also toggling playback; cleaned and untimed transcript text remains intact.
  • A search across meetings no longer lets one talkative meeting fill the whole page of results and hide every other meeting that matched.
  • A summary or insight the model stopped writing at its output limit is no longer recorded as a success. The run fails and says the answer was cut off at the model's output limit, so a half-written document is never published as a whole one.
  • The AI endpoint picker now works on a plain compose deployment too: the /ai/providers routes were mounted only under the ExApp base path, so the Prepare panel's provider list 404ed there.
  • Two insight runs can no longer run at once on one request: a retry whose row had been failed by the stale-run sweep and retried again used to start beside the newer attempt.
  • An insight stopped by an operator restart now says so and asks you to retry it, instead of claiming it took longer than an hour.
  • A finished insight whose outcome could not be written down is retried a few times before giving up, and if it still fails the log names the delivered document so it can be found.
  • A succeeded insight whose document you have since moved or deleted now says where it was, rather than "its document is empty".
  • Meeting-context refusals are never cached by the app proxy, and a meeting list that could not be read answers "unavailable" rather than "not one of yours".
  • The operator no longer logs a false "publish sink unavailable; using local" at startup. Recordings were always delivered to the configured destination; the message was wrong, and it pointed anyone reading the log at a storage problem that did not exist.
  • An insight run always asks the chosen provider for that provider's model; it can no longer inherit the summary step's model on a different endpoint.
  • When the AI endpoints or a model list cannot be read, the app now shows the operator's own error instead of a generic HTTP status. A provider base URL ending in /chat/completions or /models is refused with a hint to give the API root.
  • A non-admin's "No AI endpoint" card in the Prepare panel refreshes each time the panel opens, once an administrator has configured one.
  • The AI providers form warns when an OpenRouter key is pasted without its sk-or-v1- prefix, which used to save cleanly and then fail every run with a 401.
  • Finding more than one word inside a meeting now works. Typing a phrase used to return nothing at all.
  • Ctrl+F (⌘F) now opens the transcript find instead of Nextcloud's own search popup.
  • Narrowing the meeting list by room or tag now applies to search results too. Searching and then picking a tag used to leave meetings from outside the filter in the list, which looked as though the filter had stopped working.
  • Changing the room or tag while a search is active now re-runs the search instead of leaving the previous answer on screen.
  • Fixed selected tags being silently replaced by an existing case-insensitive label match. Meetings can retain multiple selected tag IDs, including legacy case variants, and the tag vocabulary now combines the same ID across recording namespaces. [D-773]
  • A meeting's header no longer writes down facts it does not have: a recording with no room said "No room", and one with no date showed its own filename under a calendar icon.
  • Applying an existing tag to another recording now copies its visible colour and icon into the new recording, including when tagging a selection. Existing local styles remain unchanged. Standalone viewers preserve saved tag colours and explicitly cleared icons. Annotation database upgrades preserve pending edits when moving from either the main or portable-style schema. The portable-file reader retains saved colour and icon fields, so embedded recordings show their archived appearance instead of fallback grey tags.
  • Guest participants in Nextcloud Talk meetings are now attributed under the display names they entered in the Talk UI rather than synthetic placeholder identifiers (such as participant-sXHFMabV). The recorder resolves decoupled Nextcloud room session IDs and actor IDs against active WebRTC signaling sessions, retains attendee display names delivered in presence updates, and upgrades early synthetic placeholder names when the participant's chosen name arrives.
  • Prevented FFmpeg 4.4 crashes when a participant audio track starts several minutes into a meeting.
  • Prevented repeated native decoder failures from filling the ExApp container with multi-gigabyte core dumps.
  • Kept readable truncated recordings buildable when FFprobe reports a non-fatal container warning alongside a valid packet timestamp.
  • Keyboard focus moves into a meeting, an insight or Prepare when it opens and stays there: the page underneath can no longer be reached with Tab or read by a screen reader. Their close buttons are larger.
  • Controls, borders, and hover and selected states stay visible in Nextcloud's high-contrast themes.
  • The meeting and insight drawers stay readable in Nextcloud's high-contrast themes.
  • Nothing in the list hides under the selection bar when the bar grows a second line, and an insights listing that failed reads as an error.
  • Reduce missing speech in Parakeet v3 transcriptions by matching the model’s reference audio preprocessing. Preserve detected utterances with surrounding recorded audio instead of splitting them every ten seconds and appending synthetic silence. Retain beam decoding and vocabulary hints.
  • Keep participant audio on the meeting clock after long mute gaps and codec pre-skip, including recordings with a nonzero container start time.
  • Fix duplicated overlap words when adjacent decodes disagree only on sentence-final punctuation.
  • Include the corrected native runtime in CPU/CUDA builds and the developer CLI.
  • Add recorded-audio comparisons and resumable full-meeting GPU validation tools.
  • Automatic participant-name hints no longer overwhelm uncertain audio. They use a capped decoder score of 0.5 while operator-entered vocabulary keeps its configured score, and each participant's own name is omitted from their track. This prevents short names such as “Silvio” from repeating hundreds of times while retaining useful spelling hints for other speakers. Published provenance records the participant-name policy and its effective score.
  • A portable .opus whose transcript body cannot be read is no longer called a broken file. That transcript is unavailable, named in a warning, and the meeting, the speakers and the other transcripts still read. cassini inspect still exits non-zero.
  • A repeated load-bearing tag now stops cassini inspect from printing any part of the manifest. It used to declare the metadata invalid and then print the title, the meeting id, the speakers and every descriptor anyway.
  • Transcript bodies are verified against the manifest's payloadRef rather than the CASSINI_TX_* tags that copy it. A corrupted body whose tag happened to agree with it was accepted, and a good body whose copied tag had drifted was rejected; a disagreement is now a warning.
  • The transcript a reader opens is the one cassini inspect marks default=yes, including in a file where no entry carries the flag and array order decides.
  • words= is the number of items read out of the chunk set, not the count the body declares.
  • Model ids containing _ no longer produce a transcript id the packer rejects, which let a bundle build and then fail at packing.
  • The exported transcript JSON keeps one segment per speaker turn. It used to collapse a multi-speaker transcript into a single segment with no speaker at all.
  • cassini inspect exits non-zero on unusable manifest metadata instead of printing a summary and reporting success.
  • Portable .opus files whose chunk sets carry base64url = padding now read. Producers write the encoding unpadded, but most base64url encoders pad by default, so a file written by another implementation was rejected outright as invalid metadata.
  • A transcript's chunk count and the default transcript now come from the manifest rather than from the CASSINI_TX_*_CHUNK_COUNT and CASSINI_TRANSCRIPT_DEFAULT tags that mirror it. A file whose two layers had drifted lost a transcript that was entirely present, returned a silently truncated one, or served a different transcript than the manifest names; the disagreement is now reported and the file still reads.
  • cassini inspect reports the word count it actually decoded out of a portable .opus, names any transcript whose body it could not read, and exits non-zero. It used to print the manifest's declared words=900 and cassini=ok for a file whose transcript was unreachable.
  • A meeting's words= is the default transcript's count rather than the sum across every transcript in the file. Alternative transcripts are further passes over the same speech, so a three-word meeting published with two passes was reported as six words.
  • Keep recording-setup edits responsive during background status refreshes and prevent older responses from replacing newly saved configuration.
  • Corrected installation and development guides for CPU transcription, the unified Cassini app, recording access, and optional summary/insight data sharing.
  • Fixed the app’s development proxy so the Operator section, settings and storage controls can reach a root-mounted operator API.
  • Fixed readiness requests and proxy path boundaries, rejected fallback/login responses in Operator detection, and stopped storage setup when its refreshed plan no longer includes the selected mode.
  • Replaced the homepage’s unsupported recording command with the documented CLI syntax.
  • Fixed concurrent atomic file writes by giving each writer its own temporary file.
  • Aligned architecture and API docs with the unified app and two-service Compose bundle, and corrected standalone publish examples.
  • The operator now hands its Talk recording secret to the recorder it starts. Since D-447 the operator generates that secret when the administrator sets none, and the recorder never received it, so on such installs Talk showed a working recording backend and every recording failed with "talk auth mode hpb-internal requires CASSINI_TALK_RECORDING_SECRET to be set".
  • Fix the embedded viewer showing no transcript for newly published portable version 1 meetings whose transcript bodies live in separate OpusTags chunks.
  • Build: the ExApp image installs the web workspace from the root package-lock.json with npm ci instead of resolving it afresh from the registry on every build. On 2026-09-03 the fresh resolution tripped an npm 10.9 bug (Cannot read properties of null (reading 'edgesOut')) on trees that had built minutes earlier.

Security

  • Search results never carry transcript text, and the search index cannot return the words it indexed. A search query is kept out of the operator's request log, where it would otherwise sit indefinitely.
  • A tag filter on the meeting list is kept out of the operator's request log, as search queries are.
  • A search term or tag no longer appears in the CLI's error output when the connection to Nextcloud fails before it answers.
  • Tags travel with a recording. Anyone you share a tagged recording with can see who tagged what, by Nextcloud user id.

Fixed

  • Fixed transcript reruns failing validation when overlapping decoder-window words end out of start-time order.

Fixed

  • Long speech turns admitted by the interjection-sensitive VAD are decoded in bounded overlapping windows, reducing Parakeet's silent omissions from 20–25-second utterances without reverting the short-interjection VAD thresholds. Dense merged-fallback windows also retain deterministic seam ownership when adjacent decodes disagree completely.
  • Production ExApp deploys now use collision-safe temporary manifest paths and clean them after settled runs, so a stale file from another operator cannot block an upgrade.
  • ExApp startup logs now report the effective publication sink instead of contradicting a resolved Nextcloud Files destination with a later local line.

Changed

  • GPU transcription upgrade: existing portable/plain-image ExApp installations continue to capture and preserve Talk audio, but operator-managed transcription now enters build/blocked instead of using CPU ASR. Install the matching -cuda image on a GPU deploy daemon, then choose Rerun for each preserved blocked job in Cassini Admin.

Fixed

  • Short interjections now retain their real timeline position and speaker attribution. Transcription drains final VAD audio, recovers more subsecond turns, and rejects near-silent decoder hallucinations without discarding speech whose decoded timestamp leads its waveform.
  • Transcript construction no longer applies duplicate timeline offsets or retains both sides of a mixed-track fallback, and genuine final words emitted in decoder or VAD padding are preserved at the real audio boundary.
  • Meeting duration now comes from playable audio without discarding source-container provenance or preallocating PCM from corrupt duration metadata.
  • GPU readiness failures are reported explicitly, and generated artifacts record the effective ASR device.

Added

  • cassini retag rewrites an already-packed .opus file's room and job fields without re-encoding its audio, verifying the result against the manifest's own PCM digest before writing it. It is what lets a recovered room survive a republish, and what the maintenance scripts below use.
  • A meeting's portable .opus now records which operator job and attempt produced it (meeting.jobId, meeting.attemptNumber, mirrored as CASSINI_JOB_ID and CASSINI_ATTEMPT_NUMBER). Both are optional; a file packed outside the operator has neither.
  • cassini inspect prints an origin line with a portable file's room, job and attempt — previously it decoded the room and printed nothing, so checking the producer chain by hand meant a raw ffprobe.
  • cassini meetings context includes the room the meeting came from, in both the markdown and the --json form.
  • scripts/backfill-catalog-rooms.sh gains --jobs-db PATH, --no-jobs-db and --no-retag; scripts/reattribute-catalog-room.sh gains --force, --jobs-db PATH, --no-jobs-db and --no-retag.

Changed

  • A recording's room is now recovered from the operator's own job history rather than guessed from its file name. The catalog entry's id is the job id, and jobs.talk_binding still holds the Talk room token for every job the operator ran — so scripts/backfill-catalog-rooms.sh derives the real room id for anything this installation produced, and falls back to a name-derived id only where no job row survives. Re-running it merges rooms an earlier pass split in two.
  • Both maintenance scripts now re-tag the published .opus, not just catalog.json. The exporter re-derives a catalog entry's room from the file on every republish, so a catalog-only change was silently undone by the next re-seal. Pass --no-retag to keep the old behaviour and accept that the change is temporary.
  • scripts/reattribute-catalog-room.sh refuses to move a meeting the operator has a recorded room binding for (new exit code 5, --force to override), including in a dry run. For those meetings the real room id is recoverable and the backfill is the correct tool; asserting a different one would leave a recording whose lineage and published room permanently disagree.
  • A room that got split in two is repaired by re-running the backfill, not merged by hand. 0.2.0-beta.2 said a split — from a pepper rotation, or from a recording made before that release — stayed until you merged it yourself. Re-running scripts/backfill-catalog-rooms.sh now re-derives every meeting whose job row survives and closes the split; only recordings imported from elsewhere still need scripts/reattribute-catalog-room.sh.
  • A meeting's room name now lives in catalog.json, not in the recording. 0.2.0-beta.2 announced the display name as something the portable .opus carries; it no longer does — the file carries the room id, and the operator writes the name into the catalog on every publish, preserving it across a republish. A display name is editable and a sealed recording is not, so honouring a rename used to mean rewriting every file that room ever produced; it is now one edit in a file that is rewritten anyway. The stamped name is the one that job recorded, not the room's current name — a republish restamps the same value — so what this buys is the ability to correct a name, not a name that follows Talk. The name at record time is still in the file, as the meeting title.

Fixed

  • A publish interrupted partway no longer leaves an unprotected recording behind: the next publish denies, removes and re-creates it.
  • A re-publish no longer resets a recording's access rules, so permissions changed by hand in Nextcloud Files now survive it.
  • An interrupted upload is no longer published as a silently truncated recording; the delivery fails and can be re-run.
  • A publish now fails instead of rewriting the archive when the meeting index cannot be read. A server error answered with a JSON body used to read as "this archive is empty", which replaced every meeting in the index with the one being published.
  • A room recovered by the backfill no longer reverts the next time its meeting is re-sealed or republished.

Security

  • Recordings are now given their access rules before they hold any audio. A recording used to be uploaded first and made private in a second request, so it was readable by every account on the instance in between — and permanently readable if that second request failed.
  • The meeting index gets the same treatment, in both places that write it: the one-shot Nextcloud Files migration used to upload the index of every migrated meeting — titles, room names, dates — before locking it to the owner.

Added

  • Data-processing & privacy note. A new docs/privacy.md documents what Cassini stores, where it lives, the single optional step (LLM cleanup and summaries) that sends data off your infrastructure, and what deletion and uninstall remove.
  • The App Store listing now shows what Cassini is: screenshots of the meeting viewer and the admin control panel, an ai category alongside integration, and user/admin documentation links. A screenshot-less entry reads as abandoned; the listing now presents the product.
  • Meetings now know which conversation they came from, and you can list and filter by it. Until now a recording's room name was flattened into its title and the room's identity was discarded, so there was no way to ask for "the meetings from this room". Every recording made from here on carries a room id and display name in its portable .opus and in the published catalog, and the CLI gains two ways to use that: cassini meetings rooms lists the conversations your account has readable recordings from, and cassini meetings list accepts --room, --from and --to (all optional, and they combine — --from 2026-08-01 --to 2026-08-31 means all of August). The agent skill for reading meetings now narrows by room and date before listing, instead of fetching the whole archive and guessing.
  • A new setting, CASSINI_ROOM_ID_PEPPER. The room id published on a meeting is a one-way derivation of the Talk conversation's identity, never the conversation's own token — for a public conversation that token is also the link that joins it, and every signed-in account can read those recordings. Setting this to a stable secret is what makes the derivation resistant to being reversed: a Talk token is short enough that an unpeppered one can be recovered by working through the possibilities offline. Choose it once — changing it changes every room id, and meetings already published keep the ids they were written with, so a room splits in two until you merge it again.

Changed

  • The Nextcloud App Store description now discloses that recording and transcription stay on your infrastructure and that only the optional external-LLM step sends data to a third party.
  • Recordings published before this update can be given their room back, from the files themselves. Run ./scripts/backfill-catalog-rooms.sh once, by hand, on the host where the app container runs; it reports what it would change and writes nothing until you add --apply. It recovers each meeting's room name from its published recording and derives a room id from that name, because those files never carried the conversation's token and it cannot be recovered from them.
  • Because of that, one conversation can appear as two rooms — one identified from its token, for recordings made since the update, and one identified from its name, for everything older. Cassini cannot prove they are the same conversation: two conversations can share a display name, and a room can be renamed between recordings. When you know they are the same, ./scripts/reattribute-catalog-room.sh --from <id> --to <id> merges them, and like the backfill it shows you every meeting it would move before it writes anything.

Fixed

  • Published recordings can be deleted again. Cassini created its Cassini Team folder with a Group Folders flag that, on recent versions, made every file in it permanently undeletable and un-moveable — by everyone, including the Cassini account and Nextcloud administrators. New installations no longer set it, and nothing else changes: who can read a recording is decided by the same per-recording permissions as before. An existing folder cannot be repaired automatically, because Group Folders offers no way to clear the flag after creation; the app's status page now reports the condition and what it costs instead of leaving it invisible.
  • Setting up recordings no longer fails on an installation that is already correct. On a Nextcloud that requires password confirmation for administration, an administrator has to create the recordings folder by hand — after which Cassini re-applied the same group and permission settings anyway, was refused, and reported the setup as broken even though nothing was wrong. It now checks what is already in place and only writes what is missing.
  • The app's status page no longer reports a completed setup as healthy when a step failed along the way.
  • Setting up the recordings folder now reports why it failed. The Group Folders API answers a refused request with HTTP 200 and the failure recorded only inside the response body, so Cassini read a refusal as a success and then failed while decoding it — reporting a JSON error that named neither the problem nor the cause. On Nextcloud installations that require password confirmation for administration, every one of these requests is refused, and the app's status page said only that setup was incomplete. Cassini now reports the actual reason and, when it cannot proceed, names the occ commands that create the Team folder by hand — which it then adopts on the next enable.
  • Setting up recordings no longer fails on Nextcloud installations that require password confirmation for user administration. Cassini creates a cassini service account and its group when the app is enabled; on Nextcloud 34 (and any instance enforcing password confirmation) those calls are refused, because an app has no browser session in which to confirm a password. An administrator can create the account and group by hand — but Cassini did not check whether they already existed before trying to create them, so a correctly prepared installation looked identical to an empty one and setup failed permanently. Cassini now looks first, adopts what is already there, and when it genuinely cannot proceed it names the occ commands that fix it.

Added

  • Cassini now makes the Talk signaling internal secret — the one input required for invisible HPB-internal recording that cannot be auto-provisioned — easy to know and set. When CASSINI_TALK_SIGNALING_INTERNAL_SECRET is missing, the operator logs an actionable WARNING at startup and GET /operator/status returns a signaling_internal_secret_hint, instead of only failing when a recording is attempted. The External Apps deploy-option description and docs/exapp-install.md now explain where to find it (e.g. docker exec nextcloud-aio-talk printenv INTERNAL_SECRET on Nextcloud All-in-One, or the signaling server's [clients] internalsecret on a standalone HPB) and how to set it.
  • Zero-config Talk recording secret: CASSINI_TALK_RECORDING_SECRET is now optional. When it is not supplied, the operator generates a secret on first start and persists it on the AppAPI volume (surviving restart and app update), so installing Cassini from the App Store no longer requires inventing or copying a shared secret. An explicitly supplied secret still wins and is treated as externally managed.
  • New ADMIN-only provisioning endpoint operator/talk/provisioning returns the ready-to-apply spreed recording_servers value (backend URL + secret), so an admin can register Cassini as Talk's recording backend in one step without ever handling the secret by hand.
  • The operator now delivers published recordings into Nextcloud Files. Each published meeting's portable .opus is uploaded before its entry is added to the catalog.json index under the canonical Cassini/Recordings/ folder. The in-Nextcloud viewer then serves its catalog and per-meeting audio authoritatively from Files (with its CSP and routes unchanged), while standalone development keeps its local source. Mutable catalog responses bypass browser caches, and the embedded viewer refreshes its meeting list on focus and every 15 seconds. Delivery failures are visible rather than silently masked by local fallback. Recordings are delivered as .opus only; Talk's recording /store endpoint (which rejects .opus) is not used. This first pass is public and writes as the Nextcloud admin; per-user/per-group access and a dedicated service account follow.
  • Recordings now belong to a dedicated cassini account, created automatically when the app is enabled. Previously they were owned by the Nextcloud administrator, which tied every recording to a human account that may be renamed, handed over or removed, and gave the identity holding all the recordings full administrative rights over the instance. Setup work — creating the recordings folder, its groups and the service account itself — is still performed as an administrator; storing and reading recordings is not. The account owns and serves the recordings; there is no other mode.
  • Per-participant access control for recordings. The operator freezes each recording's audience to the Talk room's participants at publish time — it grants read-only Nextcloud advanced-ACL (nc:acl-list) to each local participant (users, groups, circles) on the meeting's .opus; guests, email and federated participants are skipped. The in-Nextcloud viewer is then served per caller: the meeting list is built directly from catalog.json, filtered to the .opus files visible in the caller's own Nextcloud scan, and each recording is fetched as the caller so Nextcloud enforces access — a recording a user is not granted never appears and cannot be played. The read path fails closed (an empty list, never the full archive) on any error. Access is editable afterwards through Nextcloud's own sharing/ACL tools. Recordings are owned by a dedicated cassini service account, which also manages their ACLs; privileged setup is performed separately as a Nextcloud administrator. Two native prerequisites must be enabled first — the Team folders (groupfolders) and Everyone Group (group_everyone) apps — after which Cassini provisions the recordings folder and ACL topology itself. See docs/exapp-nextcloud-recordings-permissions.md.
  • Automatic setup of the recordings directory and its permissions. Enabling the Cassini ExApp now provisions everything the model needs by itself, over Nextcloud's HTTP APIs — no manual occ Team-folder setup. On the enabled edge the operator creates the Cassini Team folder with its default-deny advanced-ACL floor, gives Nextcloud's virtual everyone group read and the narrow cassini owner group full permissions, delegates cassini as ACL manager, migrates any per-recording permissions left by earlier versions, applies the root container ACL (so the owner can write and every account can traverse), and creates the canonical Cassini/Recordings/meetings collections. Because the audience group is virtual, every account has the mount from the moment it is created — there is no membership sweep and no convergence delay. Two native prerequisites remain, neither of which an external app can install for itself: the Group folders ("Team folders") app and the Everyone Group (group_everyone) app. The local harness enables both automatically.
  • The recordings setup now reports its own health. Setting up the recordings folder used to fail into a log line, leaving an app that looked healthy while showing nobody their recordings. GET /operator/status now reports whether the Team folder, its permissions and the universal everyone mount are actually in place, names the step that failed and the likely cause, and returns 503 when the app cannot serve recordings — so a missing Group folders or Everyone Group app is visible instead of silent.
  • The production deploy is now a committed, parameterised script (ops/deploy/deploy-exapp.sh) driven by an inventory file, instead of a one-off on a maintainer's laptop. It is dry-run by default, reads the live rollback target before touching anything, and refuses to deploy a moving tag (latest, latest-cuda, …) or an image whose child manifests are missing from the registry. CPU and GPU deployments differ by one inventory value, the daemon's compute device — there is no separate GPU path.
  • docs/exapp-update-constraints.md states what the Nextcloud admin UI's Install and Update buttons can and cannot deliver — most importantly that deploy environment is applied at container creation only, so a release adding a required environment variable is a breaking change the Update button cannot deliver.
  • A retention policy for per-run working files, --artifact-retention / CASSINI_ARTIFACT_RETENTION. Disk use grew with every recording and every rerun and nothing ever reclaimed it. The default (sealed) removes a completed run's working copies — all of which are duplicated in the operator's canonical library or were temporary staging — and keeps its sealed meeting file and every log. superseded only removes runs that a rerun has replaced; all keeps everything, as before — except a delivered run's staging copy, which is always removed once the recording reaches its destination so that no full copy of it is left outside the access model. Nothing removes the last copy of anything, and published recordings are never touched.
  • Installing Cassini now tells you whether recordings will actually work. Setting up the recordings folder used to fail into a log line, so an app whose setup had not completed still enabled, answered every request, and showed nobody their recordings. GET /operator/status now reports that setup under recordings_access as provisioned, unavailable (a named thing is missing — it says which app to install, or which setting to fill in), degraded (a setup call failed), or unknown (the app was restarted without being re-enabled), and returns 503 for anything that is not working. The two Nextcloud apps Cassini cannot install for itself — Team folders and Everyone Group — are now checked by name before setup starts, and reported individually.
  • GET /operator/setup — a USER-level route reporting whether recordings can be served (ok and state) and nothing else. It is what lets a non-admin be told that an install was never finished; the diagnosis (which step, which administrator, which paths) stays on the admin-only /operator/status. It answers 200 in every state, including the broken ones, so a browser can tell "Cassini is not set up" apart from "the app is down" — /operator/status remains the endpoint that answers 503 for monitoring. Existing installs pick the route up when the app is next registered or updated.
  • A one-shot migration for recordings published by an older version. Recordings made before Cassini stored them in Nextcloud Files live only on the app's own storage, and nothing publishes them again. ./scripts/backfill-nc-files.sh moves them across, run once by hand after updating; --dry-run reports what it would move without changing anything. Migrated recordings are readable only by the Cassini service account, because the audience a recording had when it was published cannot be recovered afterwards — grant access from the Files app, or use --public to make them readable by every signed-in account as they were before Cassini had access control. The migration changes nothing when Nextcloud Files already holds recordings, or when there is no older archive to move, so it is safe to run when you are unsure whether it is needed — and skipping it entirely is a supported choice: the old recordings simply stay where they are and everything published from now on works normally.
  • Read your meetings from outside Nextcloud: cassini meetings. An agent or script on a laptop or container can now list the recordings a Nextcloud account is allowed to read (cassini meetings list), download one as its single portable .opus (cassini meetings fetch <id>), or print one as readable context — transcript plus generated summary, as markdown or JSON (cassini meetings context <id>). It authenticates as a Nextcloud user with a Nextcloud app password, so no interactive browser login is needed and no new server route was added: the commands drive the app's existing per-recording read surface, which fetches from Nextcloud Files as the calling user. Nextcloud therefore enforces exactly the same permissions the in-Nextcloud viewer obeys — a recording you may not read is reported as not found, never as forbidden, so it never reveals that it exists. The surface is read-only; starting, stopping and deleting recordings remain administrator actions. context labels its transcript derived-from-words: a published meeting carries word-level timings, so paragraph breaks are inferred from pauses and speaker changes rather than editorially cleaned up. Requires ffprobe for context. Downloaded recordings are written readable by their owner only, since they hold a private meeting's audio and transcript. The CLI refuses to send your credentials anywhere other than the Nextcloud you named — it does not follow redirects, and it rejects a catalog entry pointing at another host. See docs/agent-meeting-access.md.
  • Working-file retention is now configurable on an installed app. CASSINI_ARTIFACT_RETENTION is declared as a deploy option, so administrators can choose to keep every per-run working file (all) when diagnosing a problem. The default is unchanged.
  • cassini meetings warns when --insecure is used with an app password, since disabling certificate verification exposes that password to anything able to intercept the connection.
  • Added explicit LAN media, signaling, and Talk callback configuration for running the full installed-ExApp harness on Docker Desktop for Mac, with fail-fast validation and a setup runbook.

Changed

  • Image publishing now verifies that every keep-forever tag — :latest, :latest-cuda, :latest-rocm, :cuda, and each :X.Y.Z[-cuda|-rocm] release pin — is actually pullable, failing the run instead of leaving a tag that resolves but points at a deleted manifest.
  • The operator/status talk block now reports secret_source (env | generated | unset) and the derived recording_backend_url (never the secret value), so admins can see at a glance how the recording secret was provisioned and what to register in Talk.
  • The operator now selects where published meetings are delivered with --sink / CASSINI_PUBLISH_SINK. The only sink today is local — the operator's own site root, which is what publishing has always done — and it is the default, so existing deployments need no change. An unrecognised name is rejected at startup with the valid names listed, rather than quietly publishing somewhere unintended.
  • The faithful installed-ExApp CI gate refuses its negative-test env hooks (D453_EXPECT_CONFIG_FAILURE, D453_FAIL_AT) unless D453_ALLOW_TEST_HOOKS=1 is also set, so a stray hook variable in a CI job fails loudly instead of quietly weakening the gate.
  • The demo sandbox now runs on Nextcloud All-in-One (AIO) instead of a hand-rolled docker-compose stack that reused the CI/dev harness. AIO provides Nextcloud, Postgres, the Talk HPB, and the reverse proxy — mirroring a real deployment. The sandbox no longer references anything under harness/. Cassini is installed/updated on top of AIO with a single idempotent sandbox/wire-cassini.sh (App Store release by default); one-time AIO + reverse-proxy + firewall setup is documented in sandbox/README.md.
  • Cassini's in-Nextcloud control-panel and viewer now render entirely through AppAPI's native embedded-UI mechanism, under Nextcloud's normal Content-Security-Policy. A stock Nextcloud + AppAPI install shows the admin control-panel and viewer with no modifications to AppAPI and no app_api integrity warning — the development-only CSP patch that edited AppAPI's signed proxy controller is gone.
  • Publishing a recording is now lightweight: cassini publish updates the meeting catalog and recordings only and no longer rebuilds or embeds the viewer on every publish. The viewer is served from the app image, so publishing is faster and no longer depends on the viewer build. Pass --rebuild-viewer (or set CASSINI_PUBLISH_REBUILD_VIEWER=1) to produce a self-contained static site with the viewer embedded, as before.
  • Upgrading needs no data migration and no manual step: recordings are stored in a group folder, which is shared storage rather than a personal one, so existing recordings stay where they are and remain accessible to the same people. Cassini installs the dedicated owner group's write mapping and then removes the legacy administrator-group mount and ACL-manager delegation after the service account has successfully managed the migrated root.
  • A recording that does not reach Nextcloud Files no longer counts as published. Delivery used to run after the publish was already marked successful, and its failures were only logged — so a job could report success with the recording nowhere in Nextcloud and the viewer showing nothing. Delivery is now part of publishing: if the upload, the catalog update or the per-meeting access grant fails, the publish fails, says which step failed, and a rerun re-attempts it.
  • An installed app now stores recordings in Nextcloud Files by default. CASSINI_PUBLISH_SINK selects the destination explicitly (nextcloud-files or local), and an installed app whose value is missing defaults to Nextcloud Files rather than quietly keeping recordings on the app's own volume. Selecting nextcloud-files without a usable Nextcloud connection now stops the app at startup with an actionable message instead of failing later, one recording at a time.
  • An installed app no longer keeps its own copy of published recordings. Recordings are stored in Nextcloud Files, but the app also retained a full copy of every meeting it had ever published on its own volume, outside the permissions that govern the originals. That copy is no longer produced: the working directory a publish builds is removed once Nextcloud has accepted the recording, and the app serves meetings only from Nextcloud. A recording the app cannot fetch is reported as missing rather than quietly answered from local disk.
  • Talk still receives recording status (started, stopped, failed) exactly as before, so the Record button, the room's recording indicator and failure reporting are unchanged. CASSINI_TALK_BACKEND_URL now covers status callbacks and OCS calls only.
  • Recording access now uses Nextcloud's virtual everyone group, giving new accounts the recordings Team-folder mount immediately without periodic membership reconciliation. Installers must enable both the Team folders (groupfolders) and Everyone Group (group_everyone) apps — they are the only two manual prerequisites; existing recording-viewers ACLs are migrated without changing whether a recording is private or public.
  • Whether a conversation is public is recorded at the time of the recording and stays with it. Making a conversation public later does not retrospectively widen recordings made while it was private, and making it private does not withdraw recordings people were already told they could see. If Cassini cannot determine the conversation's type, the recording stays restricted to its participants.
  • The recording-permissions runbook now states the access rule accurately: a recording is visible to everyone who had access to the conversation, including people invited who never joined, not only those present during the call.
  • Recordings are now access-controlled for everyone, with no way to turn it off. Per-participant access used to be an opt-in deploy option that defaulted to off, so a stock install shipped an archive where any logged-in Nextcloud user could browse and play every recording. That is gone: an installed Cassini app serves each person only the recordings Nextcloud says they may read, and there is no public-archive mode to fall back to. Nothing to configure — the app sets up the recordings folder and its permissions itself when you enable it. The two things an administrator still does are enable Nextcloud's Group folders ("Team folders") app and the Everyone Group app, neither of which an external app can install on its own.
  • The admin deploy option CASSINI_NC_ACCESS_CONTROL and the local harness's --nc-access-control switch were removed along with it. If you set either, drop it; nothing replaces it.
  • The AppAPI register logic is now one implementation (ops/deploy/lib/exapp-register.sh) shared by the sandbox, the harness install phase and the production deploy, so the rules that protect the recording archive hold everywhere: unregister before re-register, never pass --rm-data, and force PUT /enabled so the navigation entries come back.
  • The ExApp install guide now documents the /exapps/* reverse-proxy route to HaRP. It is required for every HaRP daemon and is what actually blocks an install when missing — the symptom is an app that reports [enabled] with no navigation icon, which points nowhere near the reverse proxy. It must use Caddy's handle, not handle_path, so the prefix survives.
  • The operator's job pipeline has a fourth stage, seal, between build and publish, with its own timestamps, log and progress step in the control panel. Publishing is now reached only through a completed seal.
  • A recording is no longer published when the recordings folder is not set up. Without it, recordings were written into a private folder belonging to Cassini's own service account: the upload succeeded, the job reported success, and nobody could ever open the result. Publishing now fails with the reason and points at GET /operator/status; a rerun works once the cause is fixed. This also applies after a container restart until the app is re-enabled, because setup runs when the app is enabled and nothing has verified where recordings would land until it does. To keep recordings on the app's own volume deliberately, set CASSINI_PUBLISH_SINK=local.
  • The install guide now lists both required Nextcloud apps under Prerequisites, documents every deploy option, and explains how to read the setup report.
  • An unfinished install now explains itself in Cassini, instead of failing with HTTP 502. When the recordings folder is not set up, opening Cassini no longer shows a failed meeting list — it shows what is wrong, and it says something different depending on who is looking. An administrator gets the step that stopped: the missing Nextcloud app named by name and id with the occ app:install line for it, or CASSINI_NC_ADMIN_USER when no administrator could be resolved, or the container log to read when a setup call simply failed and there is nothing to install — plus the disable/enable pair that re-runs setup, and the same sentence the container log carries. Everyone else is told that Cassini is not set up, that it is not their account, and is given a link to this Cassini page to send to an administrator — who, opening it, sees the instructions. Which message you get is decided by the same boundary that decides whether you see the Operator tab at all.
  • Where the message goes depends on whether recordings can still be read, which is not the same question as whether setup finished. When nothing is mounted, it replaces the meeting list, because the list would otherwise be an error or an empty lie. After a plain container restart — where setup has not re-run, so publishing is refused, but every recording already published still opens — it is a strip above the list and the list stays.
  • Enabling or updating the app no longer re-uploads your whole recording archive. It used to copy every recording it had ever published into Nextcloud Files again on each enable, install and update, under a fixed time budget for the entire batch. Past a certain archive size that ran out of time before it could update the recording list — and because the list is written last, it stayed frozen at its previous contents permanently, with new recordings delivered but invisible and nothing said about it. Recordings have gone straight into Nextcloud Files as part of publishing for some time now, so nothing depended on this any more.
  • All three Go modules now declare the same minimum Go version (1.24), matching the toolchain the release images have always built them with. Previously two modules declared 1.22 and one declared 1.24, so the version a module was checked and formatted under depended on which module it was.

Removed

  • sandbox/compose.sandbox.yml, sandbox/deploy.sh, and sandbox/destroy.sh — the harness-coupled compose sandbox they implemented is superseded by the AIO-based sandbox.
  • Cassini no longer uploads recordings to Nextcloud Talk. Every Talk-button recording used to file a raw .mkv into the room owner's /Talk/Recording/<token>/ through Talk's recording-store endpoint, in addition to publishing the meeting as a portable .opus. That second copy sat outside Cassini's canonical recordings root and outside its per-file access controls, so a meeting could be readable there by people it was never shared with. The upload is gone — including the rerun path that re-attempted it — and a meeting now reaches Nextcloud exactly once, as the published .opus.

Fixed

  • CUDA image tags (:X.Y.Z-cuda, :latest-cuda, :cuda) can be pulled again. Image pruning had deleted the manifests those tags pointed at, so docker pull failed and a CUDA deploy daemon silently fell back to the CPU image — running transcription without GPU acceleration and reporting no error. Tags published before this fix stay unpullable until republished; :latest-cuda and :cuda repair themselves on the next push to main.
  • cassini dev stack up --resume now recreates containers and mounts the existing Compose and ExApp data volumes after a normal stack down, instead of requiring the removed containers to still exist.
  • Publishing a recording no longer re-exports the whole library. Every publish used to rebuild and rewrite every meeting in the archive, so the time it took grew with the archive — on a production instance with 67 meetings that was roughly seven and a half minutes per recording, during which the job sits at publish / running and looks stuck. A publish now exports only the meeting being published and merges it into the live site, leaving every other recording's files untouched. Re-publishing a meeting (for example via a rerun) updates its entry in place rather than adding a second one.
  • The three-songs harness sync check now fails loudly when the recorder report is missing or contains fewer than 2 composed sessions, instead of silently skipping; the recorder run now writes the report the check needs.
  • The recorder now rejects --request-offer-interval 0 at startup instead of silently disabling every capture-recovery path, which could leave an empty recording for the whole call after a single dropped requestoffer.
  • Setup no longer assumes the administrator is called admin. Cassini probes for one — the configured account, then admin, then the instance's own account list — so instances that name theirs differently can complete setup. If none of them is an administrator it says so and stops, rather than continuing as an account that may not exist; CASSINI_NC_ADMIN_USER names one directly.
  • Publishing a recording no longer rewrites the whole Nextcloud Files archive. Each publish uploads its own recording and adds it to the shared catalog, so a delivery that fails can no longer remove other meetings from the list.
  • A recording you are not allowed to see now reads as "not found" rather than "forbidden". Reporting a denial told you the recording existed, which is exactly what the access model is meant to prevent. Genuine outages are still reported as errors, and denials are still distinguishable in the operator's logs.
  • The internal site manifest (cassini.json), which lists meeting counts and job ids, is no longer readable by any signed-in user of an installed app.
  • The viewer no longer treats an unreachable meeting list as an empty one. A Nextcloud outage now surfaces as an error instead of an empty archive.
  • Recordings of public conversations are now readable by everyone, as you would expect. A conversation anyone with the link can join was treated exactly like a private one, so its recording was restricted to whoever happened to be on the attendee list. That was worst for the most public meetings: participants who joined by link rather than by invitation cannot be granted access individually, so a public meeting full of link guests produced a recording only its owner could open. Recordings of public conversations are now visible to any account on the Nextcloud.
  • A newly created user can now see public recordings straight away. The recordings Team folder now uses Nextcloud's virtual everyone group, so every account has its mount from creation instead of waiting for a periodic membership sweep. A new account sees everything it is entitled to on its first Cassini visit.
  • A public recording no longer loses its broad access when the app restarts. A periodic integrity sweep re-applies the restrictive permissions to any recording that is missing them, and it mistook a public recording's deliberately broad grant for a missing restriction — quietly turning every public recording back into a participant-only one. The sweep now leaves a recording's own permissions alone, whichever way they were set, and only repairs recordings that carry none at all.
  • A momentary Nextcloud Talk hiccup no longer costs a recording. Working out who a meeting belongs to happens once, at publish time, and now gates publishing — so a single failed request used to fail the recording outright, recoverable only by re-running the job by hand. The lookup now retries, and then falls back to asking as the recordings owner, which is the case retrying alone cannot fix: the person who started the recording may have left the conversation, been removed or been disabled between the meeting and its publication. A meeting whose participants are all guests or federated users still publishes successfully and stays visible to its owner only.
  • The recordings list no longer gets stuck on "Meeting not found". Opening the viewer before its first recording exists — or during any brief Nextcloud hiccup — could leave the right-hand pane showing an error that stayed there even after the list loaded, until you clicked a meeting or reloaded. A link to a specific meeting opened in that window was also dropped instead of opening once the list arrived.
  • bin/cassini no longer leaks a ~17 MB temp directory on every invocation (about 3.7 GB/day on a dev VM running the harness). The wrapper now builds the CLI to a stable path under ${XDG_CACHE_HOME:-$HOME/.cache}/cassini/bin and reuses it, so there is nothing to clean up; concurrent invocations stay safe via an atomic rename, on macOS too.
  • A recording is no longer published before its downloadable meeting file exists. The portable .opus — the single-file meeting you download, share and archive — used to be produced in the background after the recording had already been handed off for publishing. If that step failed, was interrupted by a restart, or was overtaken by a second run of the same recording, the recording could still be reported as finished while its meeting file was missing, stale, or belonged to a different run. Packing that file is now a stage the recording has to pass: it is written once, checked, and fingerprinted before publishing starts, and the file that reaches the viewer is verified to be that exact file. A failure now fails the recording, with the reason recorded and a rerun available, instead of disappearing into the log.
  • Re-running a recording can no longer overwrite the previous run's meeting file. Each run now writes its own, so the two cannot race.
  • A failed publish can no longer leave the recordings page without its viewer. Refreshing the site's viewer files used to delete them before copying the new ones in; an interrupted copy left the page listing recordings it could no longer play. The new files are now staged beside the old ones and swapped in, so a failure leaves the previous, working page exactly as it was.
  • An interrupted replacement can no longer destroy a meeting file. cassini pack removed the previous .opus before moving the new one into place; it now replaces it in a single atomic step.
  • Setup no longer assumes the administrator is called admin. Cassini used to look up the administrator with a request made as the administrator it had not found yet, so on an instance whose administrator is named anything else the lookup failed, it carried on as admin regardless, and every subsequent step was rejected with nothing explaining why. It now asks Nextcloud who the accounts are and finds an administrator among them; if it cannot, it stops and says so, naming CASSINI_NC_ADMIN_USER as the way to point it at one.
  • A recording whose permissions could not be applied is no longer reported as published. Nextcloud accepts a permission change on a file outside a Team folder with a success code that hides a per-property rejection, so the recording could be stored with no restrictions at all while everything looked fine.
  • An administrator whose install was broken no longer loses the Operator surface. Cassini decides whether to show the operator by asking the admin-only status endpoint, and that endpoint answers 503 when recordings cannot be served — which was being read as "not an administrator". The one person who could fix the install saw the same browse-only app, and the same unexplained error, as everyone else.
  • Catalog meeting dates now come from the pack's recorded createdAtUtc (rendered in UTC), so meetings whose file name carries no timestamp — e.g. backfilled dailies — show a real date and sort correctly instead of echoing the raw id. Already-published archives pick this up on their next export.
  • Portable-meeting documentation was brought back in line with the format emitted by that release.
  • A recording delivered to Nextcloud Files is now verified to be the file the recording actually produced. Cassini fingerprints each meeting when it is packed and re-checks that fingerprint before publishing; the check was applied when recordings were stored on the app's own volume, but not on the Nextcloud Files path an installed app uses. A recording whose file does not match what was packed is now refused before anything is uploaded, so a half-delivered recording can no longer appear in the archive.
  • The standalone operator container no longer risks clearing more than its own site directory when started with an unusual CASSINI_OPERATOR_SITE_ROOT.
  • The recordings migration script (backfill-nc-files.sh) now runs on macOS's system bash, where invoking it with no options previously aborted.
  • The local test harness now stops with a clear message when Nextcloud Talk fails to install, instead of continuing and failing much later with an unexplained error.
  • Fixed harness startup under the macOS system Bash when optional Compose or Nextcloud command arguments are empty.

Added

  • Added a unified cassini dev stack up|down|status|plan workflow with explicit --services topology modes, including support for remote harness runs and a visible stack plan.
  • Restored the operator (recording-control) UI inside the single "Cassini" Nextcloud app as an admin-gated surface (D-420 V3): admins now get a Browse | Operator top nav, while non-admins keep the browse-only experience unchanged. The active surface is deep-linkable and back/forward-navigable via the URL fragment (#surface=operator), with no pathname router. Whether the operator surface is shown is decided by probing the operator API (403 → hidden); the operator JSON API stays admin-only in info.xml (the real security boundary), so a non-admin can never drive recordings even if they force the surface.
  • Added negative controls (harness/bin/test-session-artifact-packet-guard.sh, harness/bin/test-av-drift-pair-requirement.sh) that feed the real verifiers synthetic artifacts and assert they go red on captures with no media and green on healthy ones. They run on every pull request in the fast lint job — no Docker, no stack, no media tooling — so the guards' behavior is demonstrated rather than assumed.
  • cassini dev stack plan, up, and destructive down commands now surface non-fatal warnings for ignored settings, surprising recording paths, unreachable private media addresses, and resource-removing lifecycle options.

Changed

  • Standardized dev and CI harness execution around the same stack commands so e2e entrypoints and manual flows now use explicit, deterministic startup/topology flags.
  • Reworked the viewer to load meeting data through a single DataProvider seam instead of hardcoded static fetches, decoupling the viewer UI from its data source (groundwork for unifying the viewer and operator into one in-Nextcloud app). No user-visible behaviour change.
  • Collapsed the two Cassini Nextcloud top-menu entries ("Cassini" viewer + "Cassini Admin" control-panel) into a single "Cassini" entry backed by one in-Nextcloud app (cassini-app, which consumes the viewer as a layer). Every logged-in user gets the meeting-browsing app; the operator/recording-control surface returns as an admin-gated surface inside the one app in a follow-up (D-420 V3), and the operator JSON API stays admin-only throughout (unchanged security boundary). No change for viewer users.
  • Split the in-Nextcloud viewer into a shell (App) plus reusable MeetingList and MeetingView components — the app-shell layer for unifying the viewer and operator into one in-Nextcloud app (MeetingView is also the future standalone single-meeting share surface). No user-visible behaviour change.
  • Release CI now verifies that the exact CPU ExApp image can be installed through AppAPI/HaRP, record a Talk call, and publish a non-empty viewer transcript before image or harness changes merge.
  • Reworked the Cassini operator panel for the in-Nextcloud audience it is now shown to: the recordings list and run detail work as a master/detail pair, each run's attempts collapse into accordions so a long retry history opens at a readable summary, starting a recording is an on-demand composer rather than a permanently parked URL field, and the API-implementation captions are replaced with plain descriptions.
  • Rebuilt the connection indicator to state the connection first: a one-word status (Live / Connecting / Reconnecting / Offline) with the refresh cadence beside it, replacing a label that never said whether the panel was connected.
  • Restyled the transcription settings as three matching sections — detected hardware, quality, and device and model overrides — shown side by side on wide screens, with the overrides no longer hidden behind a collapse.
  • You can now link directly to a recording: the selected run is kept in the URL, and the back button returns to the list.

Fixed

  • Fixed harness teardown and bring-up edge cases so repeated runs no longer leave stale containers/networks or duplicate signaling-public-proxy aliases.
  • Cassini shell polish (D-420 review follow-ups): switching between the Browse and Operator surfaces now keeps the open meeting in the URL (deep-link no longer dropped); the operator surface uses the same light/dark theme as browse (stored preference, then OS) instead of only the OS setting; and the in-Nextcloud mount targets the innermost #content more robustly.
  • Added the missing operator/settings proxy route so the operator surface's STT-quality Settings panel works through the Nextcloud AppAPI proxy (it previously had no route and would have 403'd/404'd), and dropped the now-dead control-panel proxy routes left over from the collapsed second entry.
  • Talk recorder: stabilized subscriber negotiation behind intermittent empty/blank recordings ("no remuxable streams found in session artifact", silent MKVs, and 0-word transcripts). A fresh in-call participant previously caused two empty-SID requestoffer messages; the second could make the MCU tear down and recreate its Janus subscriber, rotating the SID while the first offer was still being answered. Initial and exhausted-peer recovery now issue exactly one request, repeated in-call snapshots keep the valid response throttle, and inbound offers cannot trigger recovery in front of themselves. The recorder also sends its answer without waiting for full ICE gathering, queues trickled candidates behind the matching answer, and emits end-of-candidates only after gathering completes (D-454).
  • Stopping a recording no longer fails with "job is not stoppable" when the operator has just reported the job as running. The stop registration now brackets the whole time the job is advertised as recording, so a stop is accepted from the moment the job goes running until it leaves the record stage, and one arriving before the recorder has finished starting up is delivered as soon as it does.
  • A stop requested after the recording has already ended on its own (while the operator is still uploading to Talk and queueing the build) is now accepted and recorded as a stop request instead of being rejected; the job's stop reason continues to report why the recording actually ended.
  • The release pipeline's required "Faithful installed ExApp" gate now decides whether to run from a deny-list (run unless a change is purely docs/notes/assets) instead of an allow-list of enumerated product paths, so a pull request touching a build input nobody thought to enumerate can no longer report the required check green without actually building and recording.
  • Talk recorder: closed two gaps that could still lose media on a call the recorder appeared to be recording normally. A participant whose handshake completed but whose media never arrived was left untouched for up to ~45 seconds, so short calls (a ~30s 1:1) could still finalize as empty ("no remuxable streams"); such a peer is now rebuilt about 12 seconds after its answer, but only while its connection is not actually established, so a slow-but-healthy participant is never torn down. A participant who turned their camera on mid-call could also be wedged permanently by a dropped signaling answer — their audio kept recording while the newly added video was silently lost for the rest of the call; the recorder now retransmits that answer as the signaling connection recovers, within a bounded budget, instead of skipping the participant forever (D-509).
  • Fixed the CI session-artifact guard passing recordings that captured no media. It verified headers and index framing but never counted packets, so a capture with a valid header and a 0-byte index reported "session artifact verification passed" — the exact empty-capture class (D-454) the per-PR e2e leg is relied on to catch. The guard now walks each stream log and requires real RTP media packets per captured stream, counted kind-aware so a stream carrying only RTCP no longer passes for having non-empty bookkeeping.
  • Fixed the A/V drift check reporting success without measuring anything. It exited 0 when no audio/video pair was found, and also when pairs were found but every one was too short to compare — printing "av drift check passed" after zero comparisons. Drift runs now require at least one pair to be actually compared, and the rejoin leg, which legitimately records video-less, opts down explicitly.
  • Fixed the e2e leg never producing a drift pair the check could measure. The publisher bots play the sample fixture once and stop at its end, and the default fixture is 15s — capping the measurable pair at ~11s against the 15s minimum regardless of the configured publish window. The leg now prepares a fixture sized to the publish window. The fixture is also rendered with a keyframe at least every 0.5s (libvpx's default 128-frame interval delayed decodable video up to ~4.3s behind audio, which the elapsed-difference metric misread as drift), and the leg's tolerance carries a documented budget for the remaining structural skew.
  • Fixed panels, cards and form fields losing their borders and shadows inside Nextcloud, which left content areas with nothing to separate them.
  • Fixed high-contrast themes leaving those same areas undefined, and the selected recording unmarked.
  • Fixed Nextcloud's app chrome applying twice, which added a second header-height gap, doubled the side margins, and clipped the bottom of the app.
  • Fixed the shell tab bar, the operator scrollbar gutter, and the detected-hardware values rendering with the wrong colours under some themes.
  • Fixed the recordings list not appearing on narrow screens.
  • Fixed the settings Save button offering to save when nothing had changed.
  • Fixed the meeting filter drawing a second focus ring inside its own.
  • Fixed the meeting load-in fade stalling and then snapping into place.

Added

  • Cassini marketing and docs microsite (cassini-microsite/): homepage, changelog page, and docs section with sidebar navigation.
  • The Cassini microsite is now published at https://gocassini.com.
  • Release tooling for the Nextcloud App Store: a version-ladder CLI (scripts/release-version.sh), changelog folder (scripts/fold-changelog.sh), local release-prep orchestrator (scripts/prepare-release.sh), App Store package builder/validator, and a manual Release workflow that signs and publishes gocassini.tar.gz. See docs/release.md.

Changed

  • Nextcloud toolbar icon updated to the Cassini brand mark with white fills and a tighter viewBox for correct rendering in NC ExApp containers.
  • Viewer restyled with the Saturn theme (ice blue and Saturn gold) and now shows a favicon. A previously saved light/dark choice resets once and follows your system setting until you set it again.

Fixed

  • Documentation pages on the microsite now show the site footer.
  • The microsite changelog timeline no longer draws a trailing line below the last entry.
  • Viewer meeting dates no longer append a timezone (e.g. "GMT+1") they cannot actually justify. Meeting date labels carry no timezone, so a UTC-derived time could be shown an hour off with a wrong-but-confident zone suffix. The viewer now renders the label's own wall-clock time and makes no timezone claim.
  • The operator control panel no longer flashes to loading spinners every couple of seconds while watching an active recording. Background refreshes (the 2s polling fallback and event-stream reconnects) now update the jobs list and run detail in place instead of re-showing the first-load placeholder each tick.

Added

  • The viewer's meeting list has a filter box for narrowing meetings by name or date.

Fixed

  • Talk recordings now show their conversation name and actual recording date in the viewer, instead of the raw job id as both name and date. When the name cannot be resolved, the meeting is shown as "Untitled meeting" with the recording date.
  • Meetings whose date could not be read no longer scatter through the list; the list stays newest-first and undated entries sort last.
  • Recordings publish to the viewer again. The static-site export crashed on every meeting — a regression that shipped with the D-462 meeting-name/date change (merged in #112) — so no recording reached the viewer. Publishing now completes normally.

First public alpha.

Added

  • Added public project metadata: AGPLv3 license, changelog, security policy, and contribution notes.

Security

  • Marked deterministic harness credentials as development-only values and expanded ignore rules for local dependency and signing artifacts.
Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureqHWu4BvUTmEz8vLKHwiQISBBfpIghrmDS8F2bm92mXtaqmKZ68QLuDaNdJxEIzEve9KtcadKOVv7FvybODZY1NG0wtyty2WAqtSQDgGyg3D6Rcf5UTI6SPH9Fqt9jrj6wrPugbAUZKKrf05SInXCwGineaoBCybE2uSE9lB0dkBi4VEEafNM7Cva/wESlp2/bFyU2f/C6lA1ivxAzwowijN6PB/uzNMfpstH9cW+xDO1pqoGeSw7hjJ1rFQvAIMREwIWcWZnTAiPIag1SzgYoJv0hK/AFa7KsWp2TnQDV0S/QWRafutYmGR2ZnuBowsjfXyKDEmjEWQn7ok0HGiTO0SswqMk1nzTn3B5kPLFSVBj0/+MJ6oAXvgqjqP3Au7Jmp+771rEK94xlcWlUmDi3mW6fNvaD5LPpeRoB1u6CmCEtZSh+cMOYFE7OlhWuXqZC3p5rBeUUYWLUsQDXnG3SeHDaGwcyZ+uvijc1FQ2DhJMMcnY9mhtztR/9vZUs5DttIRa2q1nuP7Nvcer+GRlkD1HgntP9HwAk2RBXHMuuewRfZMVOWGJXSxobZkAip80sg02rrHVgyWmE2jElCiZrWUKcTMd9BkF1d+yWy0msiY7dIrTEEBbOLx1hFI3wWsRnOdIS6WgtbX1xhmB4akWQxMuh6oI+6BAmuRRK+oH9Uw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=33.0.9,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.8
Release Details
UpdatedOct. 6, 2026, 10:06 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureoarCm0opumircwdOtbKX8fyvj4gEcHXlR8t9ga001qpk4LZvkY6SbU3hDM73gQtQYvlI8S3sHsoINzDxxWc+hb5oEJW0rNAOIjUGG4SeE7CGIk4zcVcUFI5R8nN3nirsm8yXhFTMNPVoFhb00Jmh05qbTkhe/A6qpPWfG1dUxMQkhGDo2EB6p7+1v8eOOhChrupJ7NWKxkdvDkQM56ITKti29dP09ajgYIYetM27swILRUFExqHSrZQzhADnSU7VWytj/inWFOYk1hZR6d5m2XYyihk6V8WeK2Jv+orEOlEDdzT1xSHhZAVyrwFEbFkDiO+VpMR1ZTxhh7XbF+htvF9itTBeeaZxZirzlpJ/KOFBvpUrc4bDEv6szcLknHowaKpjqa15/4u0Ssaaf8aJaJSbRi5khMYBGW3fyQ82DWx2nsFWsxVI/lBuG4T6HG+MX8x1YhLx7KledlNJnZlrHp89mv7TxQ2E0pLysWYeFauceBDBv/pdDAdYom3K+AcgD0HGSFBNhQ2BITlmk0DNQ5mGFhAy6Cqr+63vCrRP1Y7QTR5itIKw5BBGMjuI3XE+yfJ4ArjSkY2IciqMdEvk0ULjyV1kqvdlRMyFWuE2sYv87dFmPPvHMqXZNzIY/lAWx0hE56ZxjtrJDVVLfAWD67DxOTqR2Qsyliui96WoVOU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=33.0.9,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.7
Release Details
UpdatedSept. 19, 2026, 10:57 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
Signatureq9Yl5MApgS2v7JvdVeoXX8XY+e9xoIcNMy8HSwsU5UdFir++uQNW1/1oIKL66VVie9CuPCCQLIicFJ3pOjjf1CqjShJ1gaDVyUVHm5EVeHno5ah/lFqycCwQsW8qNlohtBz2wU7MVn0ooIULjGp6rFmSzxA3A9FQpSA/SSDjCnHjLgOymV+qWZjYzQluKES+NOj58Pyx8O19Z0U+5Gmubmc58nlKszodozXnU0K8s0gj58J699Lh9sMm1GNufZs4eE8zAo904wA4rqNGD9v3Bl27I8TVpGNwVLxrM90mOjpzaoYpzAaBWWYbneLBquZUflKpx2pJvinUDOEuD9rTbeCWfGXZ4cqCW5bwEuvx2QsOfCk0B2wOQpwyAfCgI2i3tsVxIvTs3hfb3qVhMJ45uZerqqYFyNW8oj7sqw1XJSZrTaRrk3ZW6RcuOTRMCMlT9y5/PjlSXQglZe2/OvndTi2o5DpttKChtd9EiN4NMbMTAW6ih++H3p11tyf1F8xLFc3dEm6U+aLMQFnMlR1hg8yclowh8klRxJf/r0PTcQoYIYwIBXsoDlowyohaLkJzXK4dsedTwf9gJdzjHOq+0h+gFiYGkPWgjejlL7n0v4jSD7BmA7oxp43YPRc9H1nB57oJKLKPQ4PudZS4gz6eJa/CxyQX7Y5A+oRM+PMJv/4=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.2
Release Details
UpdatedAug. 20, 2026, 7:15 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureCsJE9kLQCmBdKL0g4xSp/yu8dv0UbaGzLk6qCbrH2NOhjN4hr66SS+R66wvY1Q40l9Z7VR08jDLhwNnEeMVEY3C4Pk9YPEBPJAM/3MyRudg6EQ01amV7qgFPPaa96eZXiFD9lozVDbchBcihooe+k5m4HNJjey76F0LHIPNUqtGQjPeDBMJtMCXQRulQORbzDyx2ets+JvPOITr+pu65sE8T16yuoMgvm8qivWEtF8+12bsBOt6qXRNQokDowXzrNeWO/v8BlJg1pEBvmjK8G65EG03xYiRw0F6Vwm38Lx1OY7zWMFco9QLrSlHA7XeYahZZ8CMOWybxYrnPa1GFBbyMaPYoWRQRmRMoPSY7D7g/jAYzjSmh0jmLyfPAOPLV681mi644v1R5rVzbWPjYol+fIb3kUVU2d1z4oyRwM7sfd39NGeWmgI8SvG0LOZPArFpJod3AVrh9fvAD5PkASyzHMRH7cOHI3BI1g/4/lsMjIc/cTJddAZPsOsZ/PlTDD5GfJm87m0eoUWj0cetODqUmUUsJJvDH8siKNTXZIhQmbn/4wDFaqXPnufYgogY2XjyFqEDbdFzyL1hgsWlSO3NX8+t2HQjhTOdqnCTU8QeREpGc5RFOm3XcCTVFaQIEI/4hi+wwA98pkB4UXCGiV9+s0qCwhJdm1yOnjfiD9uU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.1
Release Details
UpdatedAug. 14, 2026, 5:49 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureR3TWjXw2RXfjhTk3UwiSEsLsfneLrrUFnqWhYZoE/rNoYyP0ETh7kkAkHrkIrPuhcZWo5U5L+wxeihkfMlMHwraMhMtmlUDGenmzPoxAuZhbjTuhHMn06+nsLY5mHZqIA7i5s68YT/xrduPvBcEDWqzJktJDXEsN+ZBZdpAoI4pqS8j8aZP+7io78c4wtYIHuA58V+e5yvpDvxnQr11W2CQWH/Q+E52clqSHsmcFRq25eQsKN7mPsvJb2eINY+vyUK0Mz88ZPg1vNFyhOBqDCTnp8xoyMrLHe37rNLriBNuuOuDP6Umuv5d1cW1GGc8fzaZxxoBXtMqQzkKyyxazKMOT4v23eFRgWlOx38y9DrUn9+qbMCxY8fLJ/rbG/XeKpZuEvoZGW74ol0Y62+EXIT1vueJOdlKFrRwIv8DvOxbYeqI7NFk17Zs49+iuUyRoNBAHdTrI2yvneLHL5ePeyMS8J5DuNdOtFNEtgRcXIfGoMHVzN6F33q2UnEeRN6q5iEJZwh7OgxAKQo9RL5kzLzky5A+6v1JP6/40fSrse5JxjzNA98+osNs87JEWA675GtPbWnOM757VjBC5ZwNA1hz97l5UA8WQYdgLoVcLhuWlW302KYnqg2PMQYM5zZkyZRYlyhJ8RDdyJXTMhOdJHQTHLtoNrQxISzeRicyX3bs=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.4
Release Details
UpdatedJuly 27, 2026, 12:25 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureDFZcmcqbeTiTh9ZtdIKLh6MOP647ayzVl9rR20Qxp74ysDBxcTsLs3LQRxmjh6WqZOjse/1t1x1oBdFcHkqbtjvNHsZJUOPb4RMh1b1sh0q5lUnHkCHp5qYoa1rJnrZt/WiqsYysgwwwtk9dMluvBLd/DHA8MY5ICMxII7LQtucl/ZRaH8kGYwsdx/Zn2YY9J0B+jjFeKM8gsZdy8TYwfKgb1k+b6jENXVgDb8WcMi9Vovw6VZ4SRTGPmsbOPOJsBVHIBQY+D+snIXjJ3VkWkU1laTJnYss9Fv7Su4nkUNLI8wpVwCSbrANTDY0HAx00zpbt1tH7rqWvS0rYqtYgfgJqfrrwTdpaCgcOoc6BcG1i4SMCyK9zRpxdXrpwG7kU6C2ubBqO0Zy7UL2c4cJ9npWU20UeWDSWfINngoRtdR0sbxoN5arkD1mvd7B1Doza3WMkT6XBgyk/PVcFnuduhWQYoyQIAIzajLEzTkSFrA5QM38cpWVeIDIRVYYKizya5xweM/mIvEuIArXAd4b+AyDL/glmuPC9L7ftmCMp7EJPTMWctRLheYNM4aMc44l+A5SpoU/nMqo69+/Q2wc23VWbT53hhVH9rbUMxMQ/SgX+EXj0uKDumldDQlpZV3HjCPW81uwCSB9gpLrCi0zREENxn9zSXB24WF+y9cFx7Dw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.3
Release Details
UpdatedJuly 11, 2026, 11:16 a.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureeRHRR6dJjmuxKf+SembwkG/mU3joMZwIrZ1bMXGB49Uc4l2t6HvD3Urbw5AdoofHgERt7RGkTf0YLF1hdSRSxqwCVw6XpvGLfjfAZEUCTM9i2mLTey/66QfctnFcE35wW0mzEQbkYbmQp5YKogsgZ+6X/cxlrio3I4EEOaR3ZBRQp7RxeSqUGhjTpnHomrVCOEwNpvGK2UIzVLqHA7gfLY5W7o/DBfmTiXt1BJ8pGTjbGV9Gi81jcxLp9LaTfjbkMVZf7stRdRtMvIhEuUgSgLyxAZftvoo4oGqB+lnx87rAGeQ90nynKx/6yVvYN9AO6Gzxg1Y5ElIS7qdBqSZxnZ8AJtoGgbCc1GT29aKbrLEAUBKcsuEXgSR21qe9DM/GNQgy8i59viKBsNOeOI+B+K2fZciKxOcv4B7iO94axm4po59voAETJyUej+R8iPfLj6kTSWr/mLhfFiF0vu89Gktb8T1OMYBaDrthHXfXl0D+5FAt6NQNzJOW2S2IDwerOMAKlAff0tkDOUZOGlffvJZC8W6qeh67EMd2UL83nkxO2h6dQpRaXD0wycZxlB2/HCoGDqrIEWvlz7rf25PV3iyaL0yu0Vh3rpFTd7tGOJyYq6y4qmJDEVL/Jd1ImEOloY3jWWeQ2dzNSOW2g6+/89KbkrV78UnOhWAPp6fmc2E=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32

Nextcloud 34

Cassini 0.2.0
Release Details
UpdatedOct. 13, 2026, 4:42 p.m.
Changelog

Added

  • Administrators can select Transcription only (.json) in Settings → Publish pipeline to publish new meetings as transcription-only files in Nextcloud Files, retaining speaker timing, summaries, metadata, tags and annotations without audio. Existing meetings keep their format on reruns. Audio and transcription-only meetings can coexist, are labelled in the meeting list, and follow the same retention policies.
  • Administrators can publish transcription-only meetings and choose whether to retain source media for reruns or delete it after processing succeeds or fails. Each recording keeps the policy selected when it starts.
  • Operator jobs show media deletion progress and errors, offer deletion retries, and explain why processing cannot be rerun when deletion was selected.
  • Proof of concept: separate the voices of people who shared one participant's device. In the app, a meeting's People panel offers Separate voices for a participant, then lets readers listen to each voice, name it, say two voices are the same person, or undo the split; the operator republishes the recording with the new speakers and rewrites its summary. Diarization runs locally on that participant's own track and stores only turn timestamps; nothing is re-transcribed and the audio is not re-encoded, so marks stay attached. After a split the original transcript stays in the recording as raw-asr ("Original"); renaming a speaker without a split only changes the name in the one transcript. Each person can save a limited number of speaker changes in a while, across all meetings; past it the panel says when to try again and keeps what was typed. See docs/speaker-separation.md.
  • Voice separation (optional) in Settings: an administrator downloads the Nemotron 3 Diarization int8 model (about 62 MiB, CPU only) like any optional model; cassini models install|pack|import handle it too.
  • cassini speakers diarize|apply|show, and the operator route annotations/meetings/<id>/speakers (GET/POST, for anyone who can read the meeting). CASSINI_DIARIZATION_THREADS sets the diarizer's CPU threads (default 2).
  • Insights have names. Create an insight fills one in from the template and the meetings it reads (the question is quoted inside the insight instead), and you can change it before generating or later from the pencil beside the insight's title, with Save and Cancel as when renaming a tag. The list, the insight and the meeting's Insights card show the name, search matches it, and the document saved to your Nextcloud files is named after it.
  • An answered insight can now leave Cassini. Export in the insight drawer copies or downloads a markdown copy with a readable header (the insight's name, the question when that differs, when it was written, the template and model, and the meetings it drew from that you can see, each linking back to its meeting), and Open in Files opens the document in Nextcloud Files with its details open, where it can be shared.
  • Administrators can schedule whole-meeting expiry from Nextcloud Files independently of container storage. Expiry removes published audio, transcripts and notes together, with conditional deletion, durable recovery and protection against reruns or indexes recreating expired meetings. Nextcloud versions, trash and backups remain separate.
  • ADMIN-only APIs provide on-demand sweeps and eviction previews with the meeting name, creation and recording timestamps, age in calendar days and an evict/keep decision.
  • ADMIN-only POST /operator/storage/retention/sweep to run the scheduled expiry logic on demand, with overlap protection and synchronous results.
  • Configurable daily retention sweep time and timezone in Operator → Storage, defaulting to 02:00 UTC. Saved schedule changes take effect without restarting.
  • Operator → Storage retention configuration for container recordings, attempt history, current output archives and stage logs. Policies default to keep forever, with 7, 30, 60, 90 or custom days evaluated on UTC dates.
  • Startup/daily cleanup with restart recovery, whole-recording expiry (audio, video and supporting files together), and unavailable-source rerun protection. Only successful publication replaces the current local archive.
  • Storage insights now includes published Nextcloud files in the category comparison and temporal charts, with the same time range and exact-value controls as local assets.
  • Files without a matching job use recordedAtLocal or createdAtUtc metadata for their date, including inspection of older unindexed files. Undated files and partial measurement failures remain visible.
  • Operator → Storage shows local usage by retention category alongside retention settings, plus published Nextcloud usage including legacy archives.
  • Each category has a date bar chart with independent time range and precision controls, exact values and a data table. Attempt history can be combined or split into its fine-grained policy categories. Unknown dates and unclassified local files remain visible.
  • Storage usage is served from an in-memory index with its last-calculated time. Administrators can rebuild it on demand, and Cassini refreshes it on fixed five-minute boundaries.
  • After a Talk recording is first published, Cassini makes a best-effort attempt to post its meeting link as the moderator who started recording. Notification failures are logged without affecting publication or recording access; messages are not retried.

Changed

  • The fp32 Parakeet v3 model now downloads from dist.gocassini.com, like every other model.
  • The native speech runtime now includes Nemotron 3 Diarization support (sherpa-onnx v1.13.7-cassini.6). The runtime accepts the version-2 Nemotron models on dist.gocassini.com and fixes final-chunk padding. Parakeet and Silero model files and transcription behavior are unchanged.
  • The privacy guide describes on-demand voice separation, the summary rewritten after a speaker edit (which sends the transcript and the typed names to the configured summary endpoint), and where names and editors are kept.
  • A participant whose voices were separated stays in the recording's speaker list, marked with the voices that replace it, so every transcript's speakers are listed. Readers older than this release show it as one more speaker beside its voices.
  • Refreshed the App Store listing: a new description with a live demo link, an early-release note inviting feedback, rough hardware guide and Ethical AI rating; six new captioned screenshots; the Social & communication category; gocassini.com as the homepage; and the Codemyriad author link.
  • The documentation and landing page describe recording access as it now works: each recording is shared with the people, groups and Teams in its Talk room, with no audience to choose and no Team folders or Everyone Group app needed.
  • New recordings capture audio only by default, including upgrades without a saved video choice. Administrators can opt into retaining source camera video using Capture video in Operator → Publish pipeline. Retention review or reminder dismissal does not change video consent. Saving affects subsequent recordings; published meeting files remain audio only, and existing source video follows its retention policy.
  • Retention descriptions explain storage growth and the consequences of deleting each category. Existing keep-forever defaults and published Nextcloud recordings are unchanged.
  • Nextcloud Files retention uses the meeting’s recorded local date when present, otherwise its creation timestamp. Existing managed meetings adopt these dates without relying on job completion history; unknown provenance or invalid timestamps keep files unchanged.
  • Preview, operation-status inspection and on-demand sweeps are API-only tools; the retention UI provides policy settings. The durable operation journal and backend logging are preserved.
  • The old artifact-retention flag/environment variable is deprecated and ignored. Successful capture/intermediate duplicate cleanup remains unconditional; historical payloads now default to keep forever.
  • Search backfill against the local work root requires stopping its operator process to avoid racing archive promotion or expiry. Existing AppAPI installations must receive the new ADMIN retention route through a versioned manifest update.
  • Wide storage charts can be scrolled with the scrollbar, keyboard, or Earlier/Later buttons. Days per bar adapt automatically to the selected range.
  • A meeting opens on Summary and Transcript tabs, with insights at the end of the summary. The playback bar shows only under Transcript, and find-in-transcript sits beside the tabs, pinned while you scroll.
  • The meeting header is compact: the date, room, duration and participants sit in a details pill that opens them in full, technical details are folded into "About this recording", and the Raw JSON view is gone. Copy and download actions are one Export menu.
  • Tagging is a deliberate step. Dragging the timeline rail scrolls the transcript instead of selecting text, and a text selection offers Tag selection, Copy text and Cancel. An existing tagged section opens read-only, with Edit to adjust or remove it. A tag on the whole meeting is removed from its menu rather than an always-visible ✕.
  • Selections and hovered tagged sections are highlighted continuously across words, like a normal text selection.
  • On phones, the meeting and Prepare sheets fill the screen, and tapping the covered tab bar closes them.
  • Doctor opens like the other Operator pages: a fixed title and description, with Run all checks beside the title, and the overall verdict in a callout beneath, with when the checks last ran. The verdict also says when a test recording you started is unfinished, and links to it.
  • Each check leads with the next thing to do, under the explanation it acts on: Nextcloud's setup guide and Talk's settings when there is no High Performance Backend, Open test room while a test waits for Talk, Play the recording and an "I could hear the audio" checkbox once it is published, and See why it stopped, which opens the run on the Recordings page, when it fails. Re-check buttons are quieter, and a check's own time appears only when it differs from the rest of the run.
  • When something stops Talk from being checked, such as Talk refusing Cassini, Nextcloud being unreachable or Cassini having no recording secret, that is the one row to act on, and the rows it prevents from being checked wait on it.
  • Cassini says why it could not reach Nextcloud (an address that does not resolve, a refused connection, a timeout, an untrusted certificate, a server error, or a proxy answering instead), with steps for each.
  • The few commands Doctor gives, such as where to read a secret or how to see a log, are labelled with where they apply and come in a block with a copy button.
  • Archive search coverage reads in plain words. A running re-index shows progress and updates the row when it finishes. A failed one says what stopped it, and offers to try again only where that can help.
  • When the panel cannot refresh, it says why, whether a session that needs signing in again, Cassini not responding or the connection, and offers Try again while keeping the last results on screen.
  • Retention setup is now a nonblocking administrator reminder. Browse, playback and recording remain available before review; administrators can save unchanged defaults in Operator → Storage to acknowledge the reminder across sessions.
  • Administrators can choose “Don’t remind again” to keep the current settings and permanently dismiss the reminder for the installation. Unsaved drafts and concurrent policy changes are protected.
  • Meeting audio is encoded to match what its recording carries instead of at a fixed 64 kb/s, and with Opus tuned to reproduce the voices as they were rather than reshape them for a phone line. A 16 kHz phone recording is published at about half its previous size. A Talk call is coded at a third over what its highest-rate participant sent per channel, at most 64 kb/s; a participant whose client sends stereo counts at half their rate, since the meeting mix is mono. On our own meetings this makes the published audio about 30% smaller. Rebuilding a meeting published before this change keeps its original encode, so tags and marks on it carry over.
  • The hardware details under "Change" are one box: GPU, CPU cores, memory and the device choice as label and value, then one tip. The device choice warns only when it names a GPU this server doesn't have.
  • The line above Quality reads as one sentence ("Transcribing with Parakeet TDT 0.6B v3 — fp32 on the CPU (no GPU found).") with a Hardware button that opens the details.
  • Browsers revalidate the app's script and stylesheet on every load, so an update that keeps the version number no longer leaves an old stylesheet in place.
  • A model download says what happens after it finishes: "Downloaded · checks once no meeting is being processed", then "Checking it runs on this server", instead of sitting at "installed · 100%". The progress bar shows only while downloading.
  • With transcription off and the picked model downloaded but not yet checked, the hint says "Check the model below to turn it on." instead of asking to download it.
  • Model actions (Download, Check, Retry, Cancel, Use this model) and Retry media deletion are styled as buttons. They had no style at all inside Nextcloud.
  • The voice separation card leads with what the model is for, matches the transcript model row, and has more room at the bottom.
  • The Publishing card is now "After each meeting", and a strip at its top shows what new recordings get with the saved settings: transcription, summary, what is published and what is kept. A step that depends on one that is off is marked, and each step jumps to its setting.
  • The card asks four numbered questions in the order the steps run. Until transcription is saved on, the questions after it are dimmed and cannot be changed, and saving with transcription off publishes the audio.
  • The keep-on-server options say how long kept audio and video stay, from the Recordings retention policy in Storage.
  • Hardware is now a line in the transcript question; the detected GPU, cores and the device override open under Change.
  • Summarisation is now part of the card: its switch, provider and template sit in the summary question, and the separate Summarisation card is gone.
  • Vocabulary and Search spellings are now one Transcript hints card.
  • The documentation states the supported range as Nextcloud 33.0.9 to 35 everywhere, matching the App Store manifest.
  • The deprecated working-file retention setting no longer says published Nextcloud files are never evicted; their expiry is a separate setting in Operator → Storage.
  • Each Storage date chart says which files the next cleanup removes under its saved retention policy, and a dashed line separates the faded bars it removes from the ones it keeps.
  • Publishing now includes transcription: what is published, then whether and how a transcript is made, then what stays on the server.
  • Retention settings use plainer names on every screen: Recordings, Published meetings (on this server), Previous attempts, Processing logs and Published meetings in Nextcloud. The reminder banner is one sentence with Review and Dismiss.
  • The Storage report shows two totals, On this server and In Nextcloud Files, then space by type and by date. Previous attempts opens onto its four kinds, and each date chart picks its own bar width.
  • The retention editor shows each category as a heading, one line and its control. Previous attempts splits into its four kinds with a link, the daily cleanup is one row with a timezone list, and Discard changes appears only when there are unsaved edits.
  • New installations run the daily cleanup at 03:42 UTC instead of 02:00 UTC. A saved cleanup time is kept.
  • Publishing asks what people receive first, then what stays on the server. Keeping nothing is unavailable while audio is published.
  • Transcription has its on/off switch in the heading, and the model shows as one row with its size, state and one action.
  • Opening a meeting from a search that matched what was said fills the meeting's transcript search with the same words, as opening one of its moments already did.
  • The app asks for its stylesheet by build, not only by app version. An update that keeps the version number (a rebuilt image, a hot-deploy) could leave a browser pairing the new script with an older stylesheet, which unstyled every screen built from components.
  • On wide lists the matched transcript lines take more room and start further left, while the meeting's title, date and tags keep about 34rem.
  • "Who can see recordings" in Settings and the "Shared with participants" note in the rooms rail lead with "Nextcloud Files controls who can read each recording." and then say the same three points, as a list. The rail's note is wider than the rail, so it no longer wraps into a narrow column.
  • A meeting opened with words carried in from a search opens on its Transcript tab, where the matches are highlighted.
  • "Who can see recordings" is two short paragraphs. Its status and a Check again button appear only when sharing needs fixing.
  • Voice separation has its own card in Settings, with the same model row as transcription, instead of sitting inside the transcript step.
  • The transcript step opens with one line naming the model in use and where it runs (CPU or GPU), with Change for the hardware details. The status strip's Transcribe step names that model's tier. A model that the picked quality still needs shows under a small heading, without repeating the one in use.
  • The status strip shows two chips per row when the settings card is narrow, all the same height. Step headings are larger than the text under them.
  • Publish pipeline no longer has a reload button in its header. It discarded unsaved changes without asking; the browser's reload asks first.

Fixed

  • The Cassini app no longer requests the administrators-only operator status on every page load for users who are not administrators (each request answered 404).
  • Guests who join the call at the moment the recording starts are now listed under their own name instead of a participant-<id> placeholder.
  • Preserve the same audio timing when source video is omitted, including single-track recordings remuxed with FFmpeg 6.
  • Doctor's test recording no longer offers a test conversation you cannot record in. Talk lets only a conversation's moderators start a recording, and the connection check creates this one under Cassini's own account — so Open test room could drop you into a call with no record button, directly under steps telling you to press it. The link now appears only for a conversation that is yours, and Prepare test makes you one.
  • Doctor now notices when its test conversation has been deleted, instead of only while Talk has a High Performance Backend. Without one, a deleted conversation stayed on the panel as a dead Open test room link, and preparing a test re-armed it against a conversation that was gone. Cassini asks Talk directly now, and forgets the conversation only when Talk says it no longer exists.
  • Storage chart bars now fill the available width and keep date labels below the plot despite global timeline styles.
  • The browse page and meeting view load without pop-in: the sidebar holds its height while rooms and tags load, the meeting list fades in once ready, and header tags and the timeline rail appear with the meeting.
  • Copying a selection after moving its handles copies the adjusted text, not the original selection.
  • Insight titles no longer truncate to a letter on phones.
  • Status colours meet WCAG AA contrast in light themes, including Nextcloud's.
  • "Connect Talk" is no longer offered when Cassini has no recording secret to give Talk, and a re-check button no longer says "Check again" for a check that never ran.
  • New recordings retain their original recording time through processing, publishing, and transcription-only export, even when filenames change or transcription is disabled or fails.
  • A failed recordings setup check no longer hides the meeting list. It shows as a strip above it, and you can still browse recordings; only saving new recordings stops.
  • When transcription is on but its saved model's files are missing, Settings says so: the transcript step shows a warning instead of "In use", and the status strip's Transcribe step turns amber with "model not installed". Recordings published in that state have no transcript.
  • Downloading the voice separation model no longer shows "until the model is ready" on the Publish step, and that note no longer appears while transcription is on.
  • Voice separation's "(optional)" is lighter and smaller than its heading.
  • A model installed but not yet checked against the current build reads "Installed · checked before the next recording" instead of "runtime check needed".
  • Selecting transcript text by dragging from outside a word (the end of a line, the space between turns) no longer loses the selection and its Tag selection bar the moment the mouse is released.
  • The "Who can see recordings" card and, in high-contrast themes, the "Create an insight" card now have the same inner padding as the cards around them.
  • The dot or icon in a tag chip sits on the centre of its label in every browser, including the tag filter in the rooms rail and the tag picker.
  • Search results keep the meeting's title and details readable at any width: the matched transcript lines sit under them, and beside them only when the list is wide. The meeting's length no longer runs into the first timestamp.
  • Clicking a matched transcript line opens that moment again; the row's click area had been covering it.

Security

  • Doctor's test conversation is now private. It was created as a public conversation, which Nextcloud never lists or shows to ordinary users, but which any signed-in user holding its token could read and join. It is now a group conversation whose only participant is the administrator it was made for, and Talk answers everyone else that it does not exist. Recording is unaffected: Cassini joins through signaling, not as a participant.

Added

  • The recording checks now show their state in colour: green for a check that passed, red for one that is blocking recording, and grey for one nothing has verified yet. The heading takes the colour of the worst news, so the panel can be read at a glance.
  • Configure optional transcription in Settings with visible model download progress, cancellation, retry, and explicit activation.
  • Model downloads resume on their own after a dropped connection or a stalled transfer.
  • Prepare transferable models with cassini models pack and install manually copied packages or files on air-gapped systems with cassini models import.
  • A Doctor section in the operator answers "is recording going to work?" in one place. It reports the recording volume and temporary space, Nextcloud storage, Talk's High Performance Backend and its credential, the Talk connection, a test recording, and how much of your archive search can read. Checks run once when Cassini starts and then only when you ask — with Run all checks, or a single row's own button, which re-runs only the probe behind that row.
  • Cassini says when Talk has no High Performance Backend, which stops recording entirely. It reads Talk's own signaling mode, so this appears even before a recording credential exists; it used to be discoverable only through a connection check that never got that far. The row is on the checklist whatever the outcome, including before anything has been established, so the one check that decides whether recording can work is never simply absent.
  • Archive search coverage reports how many meetings search can actually read, so a partial answer is visible instead of looking like no matches. Where meetings were never indexed, a Re-index now button does it for you. A shortfall nobody can act on — a recording where nobody spoke, or an older meeting search cannot index at all — is reported without being counted against coverage.
  • cassini doctor --json emits its checks as structured data, each with a stable id. Running cassini doctor by hand prints exactly what it printed before.
  • Copy or download a meeting transcript as Markdown (.md) and download its audio from the meeting view or Prepare. Multiple selected meeting files download together in one ZIP.
  • The <cassini-meeting> embed takes layout="inline" for pages that already show the recording's name and details, and --cassini-color-* / --cassini-font-sans custom properties to match the page's colours and type.
  • The <cassini-meeting> embed dispatches a playbackerror event when the browser cannot play the recording, and follows its theme attribute when the page changes it.
  • Scheduled upstream compatibility checks and real-browser coverage of the embedded Cassini transcript and recording playback.

Changed

  • Share each recording with its Talk room audience through Nextcloud Files, without requiring Team Folders or Everyone Group apps.
  • Show only recordings in the caller's current Nextcloud shares and check each file read as that caller.
  • Replace the two recording access choices with one participant-based model; public meeting participants may reshare when Nextcloud permits it.
  • Rewrote the App Store summary and description to match the README and docs site, and pointed the user documentation link at gocassini.com/docs.
  • Brought the App Store description up to date: Nextcloud 33.0.9 to 35, transcription off until a model is installed (and the model download from dist.gocassini.com), the Doctor checklist, and transcript and audio downloads.
  • Rewrote the landing page copy: a new hero, a feature list that leads with the App Store install and Talk's Record button, an AI providers section in place of the summaries one, and reworked CPU and GPU cards.
  • Corrected the supported Nextcloud versions to 32 to 35, said that insights as well as summaries send transcript text to a configured endpoint, and linked the meeting file specification.
  • Renamed GoCassini to Cassini in page titles, link previews and support links.
  • Transcription defaults to off. Recordings remain playable with participant metadata when transcription is disabled, unavailable, or fails.
  • A recording waits, as it does for memory, when its model cannot be checked yet (GPU not visible, not enough free RAM or VRAM). It is published as audio only when the model is missing or the wait runs out.
  • CPU and CUDA images no longer bundle Parakeet or VAD. Models download from dist.gocassini.com into persistent revision directories and survive application upgrades without downloading unchanged weights.
  • A check that depends on another one says what it is waiting for rather than asking for attention itself, and offers no buttons of its own. With no High Performance Backend, the credential and the connection checks no longer each report a problem: the backend is the one row to act on, and fixing it is what makes the others checkable.
  • Testing a recording no longer asks you to supply a Talk room. Cassini makes the conversation for you, in your name so that you can moderate it — Talk only lets a conversation's moderators start a recording — and you join the call and press Talk's own record button, while the row reports what happened until you confirm you could play the audio back. The check appears only once everything it depends on has been checked and passed, so it is never offered as a test that cannot succeed.
  • Checks explain in words, with no shell commands to copy and no questions about how Nextcloud was installed. Where Cassini can fix something it offers a button; where it cannot, it points at Nextcloud's own settings and documentation. An instruction assembled from a guess about your deployment is worse than a reference.
  • Checks no longer re-run themselves every five seconds. A reading is taken when Cassini starts and whenever you ask for one, so the panel stops disabling its own buttons twelve times a minute, and a check costs a probe only when somebody wants it. A test recording still being worked on is followed until it publishes, because that is the one thing that moves on its own.
  • The signaling credential is reported by the High Performance Backend check rather than by a row of its own: it is that server's credential, and that check is what accepts or rejects it. One row now says whether a backend exists, whether Cassini has its secret, and whether the secret works. It says plainly that the value is not a Nextcloud setting and where on the signaling server to read it, it does not ask for the secret where no backend exists, and the form stays reachable once the check passes so the secret can be rotated.
  • The Nextcloud storage check says what it established: Cassini's account exists, its recordings folder is writable, and the sharing API answers. It used to report that a "preflight passed", which named an internal routine and no fact.
  • The operator's readiness endpoint is now GET /operator/health, because it reports on more than recording readiness. An installed app picks the new route up when it updates to a release carrying it.
  • Require Nextcloud 33.0.9–35 for new installations; retire Nextcloud 32 support in this release.
  • Require successful installed-app compatibility evidence for every advertised Nextcloud major before publishing a release, with exact tested versions and artifacts attached to the release.
  • Cache compatibility Go tools, report scenario phase timings, share GPU smoke transcription across assertions, and separate registry maintenance from release qualification.

Removed

  • The "Cassini is ready to record" first-run dialog and its per-install acknowledgement, left over from the two permission models. A fresh install records as soon as its checks pass; creating the cassini account, when Nextcloud refuses it to the app, stays in Operator › Publish pipeline.

Fixed

  • A failed refresh no longer erases the checks. It used to rewrite every row to "could not refresh", so one failed request hid whatever was actually wrong; the last findings stay on screen and the failure is reported once, on its own.
  • A check that was passing no longer turns into "not verified" five minutes later. It keeps what it found and says how long ago, so an idle panel stops looking like a problem — and a check that has never run says so, rather than being reported like one whose result merely aged.
  • A connection check that ran and could not reach Nextcloud or the signaling server now reports a problem instead of "not verified". It tried and failed, which is a finding about the deployment.
  • When Cassini's Talk backend URL override points at Cassini's own app proxy, the check says so and names the setting to clear, instead of blaming connectivity and TLS.
  • Buttons that read "Configure" and opened a panel about an unrelated fault are gone. Every action a check offers is named for what it does, and one the app cannot name offers no button at all.
  • The operator panels can be scrolled to the bottom again. Whenever a check needed attention, the banner saying so pushed the panels down without shortening them, so their last few centimetres sat below the window with no way to reach them.
  • Meeting tags now stay in click order while changes are confirmed or retried: selection and meeting counts update immediately without bouncing, and tag choices remain alphabetically ordered.
  • An embedded recording no longer takes the Space key on the whole page. Space plays and pauses only when pressed inside the viewer.
  • Following playback in an embedded recording scrolls only the transcript, never the page around it.
  • A narrow embedded recording no longer shows a back arrow that led nowhere.
  • An embedded recording no longer reads or rewrites the host page's URL fragment, so switching transcripts no longer replaces the page's #anchor.
  • Meeting tags and transcript annotations now appear immediately while saves run in the background. Editing stays available, pending changes survive closing and reopening the meeting, and failed saves can be retried without duplicating marks.

Added

  • Access control is now a choice rather than a requirement. Cassini used to have exactly one way to store recordings — a Nextcloud Team folder with per-recording permissions — which needs two Nextcloud apps an external app cannot install for itself, so an instance without them recorded meetings happily and then could not publish any of them. Recordings can now be visible to anyone with a Nextcloud account, in the cassini service account's own CassiniNoACL/Recordings, needing no extra apps at all; or to meeting participants only, which is exactly today's model in its Cassini Team folder. Each has its own folder, so neither can shadow the other's path.
  • Cassini can set itself up. It offers to make the changes an administrator used to make by hand: the cassini service account and its group, the Team folder, its group mappings, advanced permissions, and the permission-manager delegation. It lists every change before it makes any of them, and performs them as you — Nextcloud asks you to confirm your password with its own dialog, and Cassini never sees, stores or sends it. The occ recipe is still there for an administrator who would rather run it, and is generated from the same plan Cassini executes, so the two cannot describe different setups.
  • Every missing prerequisite is named, together with the occ command that fixes it. Cassini and GET /operator/status list them all — the service account, each Nextcloud app, the Team folder, its group mappings, its ACL setting and its ACL manager.
  • A development or CI deployment can still declare the mode, with the CASSINI_STORAGE_MODE deploy option (default or access_controlled). It is not for production installs, and it is believed only where it fits: a declared mode whose prerequisites are missing, or that meets recordings in both folders, or the same recording in both, is refused at enable time and not recorded. It seeds the initial value only, and changing the variable never moves an archive that already exists.
  • Switching says what it would do before it does it, and can be re-run. The confirmation reports how many recordings would move and between which folders, whether the destination already holds any, and whether an earlier switch left a copy behind. Working that out is read-only, and nothing happens until you confirm. The switch itself copies the archive, checks every recording arrived, records the new audience, and only then empties the old folder — so whichever audience is in force, that folder holds a complete archive, and a switch interrupted by a restart, a timeout or a closed browser tab strands nothing and can simply be asked for again. Cassini names the folder that still holds a leftover copy, offers one action to clear it, and says when recordings are sitting in the audience that is not in force.
  • cassini meetings summarize <meeting.opus> ... adds a meeting summary to already-published portable files without re-running transcription: the transcript sealed inside each file feeds one LLM call — configured by the same LLM_BASE_URL / OPENROUTER_API_KEY / SUMMARY_* variables as cassini build — and the file is rewritten in place with its audio bytes untouched.
  • Files that already carry a summary are skipped; --force regenerates and replaces one, and --out writes a single file's result to a new path instead of in place. A backfilled summary is marked provenance.meetingSummary.source = "backfill" in the manifest.
  • You can find words inside the meeting you are reading. A search box above the transcript narrows it to the turns that match, shows how many of them there are, and clearing it brings the whole transcript back. It searches what was actually said, so it still finds a turn whose wording was tidied for display.
  • Administrators can tell Cassini the spellings transcription produces for names it gets wrong, in the app's settings beside the existing vocabulary: one name per line, its spellings separated by commas. A search for the name then also finds the recordings where it was written down differently, and each result says whether it matched what you typed or a known mis-hearing. Cassini ships a starting list; anything configured here is added to it, and replaces it for a name that appears in both. It changes no recording — the vocabulary setting is the one that biases new transcriptions.
  • You can now search across your meetings. Cassini indexes each transcript as it is published and answers published/search with the moments that match — the meeting, the speaker, and where in the recording they said it. You only ever see meetings you can already open.
  • Search knows the names transcription gets wrong. Looking for "cassini" also finds the recordings where it was written down as "casino", and each result says whether it matched your words or a known mis-hearing of them.
  • Every answer says how much it covered: how many of the meetings you can read were actually searched. A meeting Cassini could not index is reported as outside coverage rather than passing for one with no matches, and an unreachable archive is an error rather than an empty result.
  • Cassini's search index lives in its own search.sqlite3 file beside the job database and holds nothing that cannot be rebuilt from the published recordings. Deleting it is safe: it costs a rebuild and never touches recordings or job history.
  • cassini-operator backfill-search indexes meetings published before the index existed. It is safe to re-run and safe to interrupt, and it indexes a meeting only when the stored transcript matches the recording that was delivered.
  • Searches are rate limited per account. Each one asks Nextcloud what that person may read, so a client looping on search would put that load on Nextcloud rather than on Cassini; a burst is fine and a loop is asked to slow down, with a reply that says when to retry. The limit is per account, so one runaway client cannot lock anyone else out.
  • Pick several meetings in the Cassini app — across rooms, and the picks survive changing the room or the search — and Prepare hands you all of them as one document to copy or download. It is the same document cassini meetings context prints for the same meetings in the same order, byte for byte, because the app asks the same producer for it rather than assembling its own.
  • Before it hands anything over, Prepare says what the bundle will not contain: how many of the picked meetings have no summary, how many were published before the index recorded whether they have one — a different statement, and never counted as the first — how many predate the single-file format, and whether the word count it shows is a total or a floor. All of it is read from the published meeting index, so nothing is downloaded to answer it.
  • A meeting that leaves the archive while it is picked is removed from the selection and said so, rather than quietly shrinking the document you were about to hand over.
  • A portable export offers no Prepare at all. There is no operator behind an exported site to assemble a bundle, and producing a lookalike in the browser would be a second implementation of a published format.
  • GET /jobs returns room_name, the conversation's display name as it was when the job ran. The operator now stores the room as real columns on the job rather than only inside its Talk binding, and recovers them for every recording it has already made, so a room-scoped question is an ordinary query rather than a JSON parse. The conversation's token gets a column but is deliberately left out of the response, for the same reason the Talk binding has always been withheld from it.
  • The meeting list is organised by room. A rooms nav down the left lists every room the archive knows about with its meeting count, alongside "All meetings" and a "No room" group that keeps recordings without a room — an imported file, a simulated run, an old recording — visible rather than hidden.
  • Meetings are grouped under month headings, and each row now shows its room, its date, its speaker count and how long it ran.
  • cassini insight run asks one question of several meetings and writes the answer. It reads the context bundles cassini meetings context --json --out produces — one --context per bundle, in the order you name them — runs the summarise workflow over all of them at once, and writes the result to --out or to standard output. --model overrides the model for that run alone, and --timestamps shows the model where in each meeting something was said so it can cite it.
  • The document it writes carries its own provenance as frontmatter: which meetings it was produced from, with their rooms; the workflow's id, version and the content hash of the exact prompt bytes that produced it; the endpoint and model that answered; and when. A summary has always been able to say only that some model wrote it at some point, which is not enough to tell two documents from different prompt versions apart. --record writes the same record as JSON beside the document, so a grading harness reads one pair rather than parsing prose.
  • cassini insight run exits with a different code per outcome — no endpoint configured, the endpoint refused, the model failed or timed out, a bad request — instead of warning and carrying on. Skipping quietly is reasonable for a summary nobody asked for and never for a document somebody did. The codes are listed in cassini insight run --help. A failed run still writes its --record file, saying what was attempted and why it did not happen.
  • Transcripts now carry cross-track speaker attribution evidence. Every word records how far the loudest other participant's microphone sat above its own noise floor compared with the speaker's own, so crosstalk the decoder transcribed as speech becomes visible instead of shipping unmarked. Where a meeting's own level distribution shows an unambiguous crosstalk population, affected words are flagged lowConfidenceSpeaker; the canonical transcript keeps every word, and the evidence travels with them as attributionGapDb.
  • Generated summaries now exclude words flagged as probable crosstalk. A reader can see and overrule a marked word in the transcript; a summary has no such reader, and a fabricated interjection there becomes a decision somebody supposedly made.
  • Speech recognition now runs behind a backend interface, so a second engine can be added by registering a factory rather than by changing the pipeline. Select one with CASSINI_STT_BACKEND; the bundled sherpa-onnx decoder remains the default, and an unknown selection fails loudly rather than silently transcribing with a different engine.
  • The viewer now surfaces the crosstalk evidence the transcript carries. A turn built entirely from words the acoustic evidence attributes to somebody else is badged "probably crosstalk", and in the exact-word view individual flagged words are de-emphasised with a dashed underline and a tooltip saying how many dB louder the other microphone was. The words are still shown and still seekable — the reader can listen and decide.
  • Added a standalone waveform challenge miner that finds tricky meeting sections, including overlapping speakers, without rerunning speech recognition.
  • A published meeting now records how its word ends were decided, as provenance.wordTimings.endsBoundedByAudio in the portable-meeting manifest. When the key is absent, a consumer may repair an over-long word by clipping it towards the meeting median; when present, the end was measured against audio and must be preserved.
  • A Settings surface in the app, beside Browse and Operator (admins only). It hosts transcription quality — moved off the operator page — and the new LLM settings: endpoints, whether summaries run and on which endpoint and model (with a fetched model list). Each endpoint carries its own request bounds — timeout and response-token limit — with empty fields meaning the defaults (900s / 4096).
  • The app keeps its own LLM settings (llm-settings.json beside the job database; GET/PUT /settings/llm): named endpoints, and the summary policy. Switching between a hosted provider and a self-hosted model server is one settings change, with no ExApp redeploy. GET /settings/llm/providers/{id}/models lists what an endpoint serves. API keys are stored server-side and never returned.
  • Generate an insight from inside the app. Pick meetings in the browse list, open Prepare context, and the panel now offers a Generate card under Copy and Download: choose one of the templates this deployment ships and press Generate. The transcripts go to the AI endpoint you picked and the answer is written into your own Nextcloud files.
  • Insight runs report themselves honestly rather than as a spinner. A run appears in the card the moment you press Generate and carries queued, then running, then a result or a failure — a local model over five meetings takes minutes, and the card says so instead of implying seconds. A failed run names the cause in words you can act on ("No AI endpoint is configured", "The endpoint rejected the request"), carries Retry, and — for an administrator — links straight to the AI providers panel that fixes it. Retry replays the request as you made it, to the endpoint you picked; it falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — which is what makes adding an endpoint a fix rather than a suggestion.
  • Ask your own question. One shipped template, "Ask your own question", takes a question you type and answers it from the meetings you picked: the answer in at most three sentences, the evidence with who said what, and what these meetings do not answer. The question box appears for that template and for no other — every other template asks its own question, and a question sent to one of those would be dropped without being asked, so it is refused rather than silently discarded. cassini insight run gained the matching --question flag.
  • Insights are reachable from the browser: the manifest declares three new USER routes — insights (create a run, list your own), insights/<id> (read one with its document) and insights/<id>/retry. They are the first routes in Cassini a logged-in non-administrator may POST to, and they are USER rather than ADMIN because a run acts only within what that person could already do by hand: it reads the meetings they picked, as them, and writes the answer into their own Nextcloud Files. On upgrade an existing install picks the new routes up from the release itself — no re-registration needed, because every release bumps <version> (see below).
  • An insight you ask for in the app is answered by the deployment on your behalf and delivered into your own Nextcloud Files, under a Cassini Insights folder in your home. You own the file outright, so Nextcloud's own sharing works on it and nothing new decides who may read it. It is deliberately not filed under Cassini/ — that name is the shared recordings Team folder, which is read-only for everybody but Cassini's service account — and an insight is never written next to the recordings.
  • A document is never overwritten. Each file is named for the day, the template and the run it came from, and if something is already at that name the run writes the next free one and records which it used.
  • Every meeting an insight covers is fetched as you, over WebDAV, at the moment the run happens — so Nextcloud's own per-file permissions decide what is in the bundle, and a run can only ever be assembled out of meetings you could already open yourself. A retry re-checks that: a meeting you can no longer read stops the run rather than quietly dropping out of the answer.
  • A failed run says which of the five things went wrong in words that name the fix — no endpoint configured, the endpoint refused the request, the model did not answer, the answer could not be written, or the request itself — rather than reporting one general failure.
  • Cassini now answers published/meetings-list with the meetings the calling account may read, narrowed by an optional date range or room. The viewer and the cassini meetings CLI previously fetched the whole catalogue and filtered it themselves; the same list is now available already filtered, in the catalogue format both already read.
  • A date-filtered list reports how many meetings it dropped because their published date could not be read, so a short list is visibly partial rather than quietly incomplete.
  • Unlike the catalogue it is built from, this list never answers an unreachable Nextcloud with an empty result: a substrate failure is reported as an error, and an empty list means only that the account may read no matching meeting.
  • Cassini Admin's Transcription quality panel reports the device and model the next build will actually use, so a CPU run is stated up front rather than inferred from a failed build. The device override adds a CPU choice alongside Auto and CUDA.
  • cassini meetings context takes several meeting ids and prints one document holding all of them, so a question can be asked of a set of meetings rather than one at a time. The meetings appear in the order you named them, separated by a --- rule, and the --json form carries them as a meetings array whose every entry is exactly the single-meeting document the command has always produced. An id you cannot read — or one that does not exist, which is answered the same way on purpose — fails the whole run: a bundle that quietly dropped a meeting would answer a question asked of all of them using only some of them, and look right doing it.
  • cassini meetings context --timestamps cites each passage's start time, as MM:SS or H:MM:SS past an hour, beside the speaker. Off by default. The timings were already in the --json output and had no way of reaching the text an agent actually reads, so nothing asked to say where something was said could point at it.
  • The context bundle's format has a published schema, spec/cassini-meetings-context-v1.schema.json, and its output is pinned by fixtures. cassini.meetings.context.v1 was a version string with nothing written down behind it; now a change to what the command emits shows up as a diff in a checked-in file.
  • The published meeting index (catalog.json, and so cassini meetings list --json) now records whether each meeting carries a summary and how many words its transcript holds. Both come out of the file the publish step already reads, so anything listing meetings can say which of them have a summary, and size a selection of them, without downloading a single recording. Meetings published before this update carry neither field, which reads as "not known" — never as "no summary" or "no words"; republishing one fills them in.
  • The Cassini app can ask for a set of meetings as one document: GET published/meetings-context?ids=…,…, optionally &format=json and &timestamps=true. It answers with exactly what cassini meetings context prints for the same meetings — the operator resolves what you may read the same way it resolves the meeting list, fetches each recording from Nextcloud Files as you, and runs the CLI over them, so there is one implementation of the document rather than one per reader. An id you may not read is answered 404, identically to one that does not exist; a request that cannot be answered is 502, never an empty document. At most 20 meetings per request, because each one costs a whole recording download.
  • cassini meetings context --local ./Meeting.opus [...] reads portable .opus files off disk instead of fetching meeting ids from Nextcloud, and needs none of the connection settings. Each meeting's id is its file's basename without the .opus, which is how a published archive names one. --catalog ./catalog.json supplies the room id and room name, which live in the catalog rather than in the file — with it, a run over an archive's files produces the same bytes as a run over the same meeting ids; without it a meeting renders with the room id its file was tagged with and no room name. Ids travel as one comma-separated ids, not as a repeated id: AppAPI's proxy collapses repeated query parameters to the last value, which would have answered a three-meeting request with a one-meeting document and a 200. A caller reaching the operator directly may still repeat id.
  • Insight templates is a real panel in the Operator: it lists every template this build ships, and each row opens onto the exact instruction that template sends to the model — the prompt itself, not a description of it — beside the id, version and content hash an insight document records. So a document you found a month later can be traced to the prompt that produced it, rather than to a name that may since have meant something else.
  • A second template, "Commitments and owners": one section per person who spoke, with what they took on and when they said so, then what was assigned to somebody who never answered, then what nobody claimed. cassini insight run --workflow todos runs it, and every other workflow the panel lists can be named the same way.
  • cassini insight workflows prints the same list on the command line, and --json adds each workflow's instruction verbatim. It is how you check what a given image will actually send, without reading its source. The Operator's templates panel is this command's output.
  • LLM settings carry a second step, Insights, beside the meeting summary, so an insight can run on a different provider and model from the one that writes each meeting's summary — a small local model for every published meeting, a larger one for a question you ask by hand. An insight step with no endpoint of its own runs on the summary endpoint, so an existing install gains insights without configuring anything, and the settings panel says which of the two is answering. Each step also names the workflow it runs.
  • Per-step request bounds (SUMMARY_TIMEOUT_SEC / INSIGHT_TIMEOUT_SEC and the matching _MAX_TOKENS), because two steps on two endpoints cannot share one timeout: a CPU-bound local model needs a far longer leash than a hosted API.
  • Each step's workflow field is checked against the templates the image actually ships, and names them when the id is not one of them. The operator cannot know the registry — it lives in the recorder — so a typo used to save cleanly and only fail at the run.
  • An insight now keeps a record of itself from the moment it is asked for, moving through queued, running and then succeeded or failed. A run that takes minutes against a local model shows what it is doing rather than nothing, and a failed one says why it failed instead of stopping silently.
  • Retrying a failed insight is another attempt on the same insight, not a second one: it keeps its id, workflow, meetings, question and the endpoint you picked, and replays that request. It falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — so fixing a missing key on your endpoint, or registering one where there was none, is enough to make the retry work. A retry pressed twice at once starts one attempt and refuses the other. A run the operator was restarted out from under is failed with a message asking you to retry it, instead of staying stuck on "running".
  • An insight now appears in the browse list beside the meetings it was drawn from, and under every room those meetings came from: a question asked of three rooms is found in all three, not filed under one of them. A Meetings/Insights switch above the list narrows to one kind or the other, and the last kind showing cannot be switched off. Insights that are queued or still running are listed too, saying so — an answer that takes minutes is visible while it is being written rather than appearing from nowhere afterwards — and a listing that fails says it failed instead of reading as "there are none".
  • Opening an insight shows the question it was asked, the meetings it read — each of which opens as itself, with the way back to the insight — the answer, and who ran it with which workflow and version. Every source meeting lists the insights that read it, so the link works in both directions. A meeting or an insight you may not read is simply absent from both, with nothing anywhere saying that it existed. The standalone viewer export, which has no operator behind it to have run anything, shows none of this and offers no filter for it.
  • GET operator/setup, which any logged-in Nextcloud user may read, now also reports what this deployment's AI configuration allows: features.summaries (a recording will be summarised) and features.insights (a question asked of a set of meetings will reach an endpoint). One bit each — never the endpoint, the model or the key, which stay administrator-only. It makes the promise in docs/privacy.md — that nothing leaves your infrastructure unless an endpoint is configured — checkable by the people whose meetings are being recorded.
  • The Cassini app now explains an unconfigured deployment instead of staying quiet about it: picking meetings on an instance with no AI endpoint says so in the Prepare panel, and an administrator gets a link straight to the AI providers panel that keeps the meeting they had open. Anyone who is not an administrator is told the fact and that only an administrator can change it, rather than being shown a control that would be refused. The standalone viewer export, which has no operator to ask, says nothing at all.
  • Pick the AI endpoint and model when you create an insight. Prepare now offers a Provider and a Model beside the template, defaulting to the first configured endpoint and its own default model. It is offered to everyone, not only administrators: which of the configured endpoints sees your transcripts is the asker's decision, so GET operator/ai/providers is a USER route carrying ids and display names — the base URL, the key and the request bounds stay administrator-only. A retry re-runs what you asked for, falling back to the deployment's endpoint only if the one you chose has since been removed. This adds a route, so the release that ships it must bump <version> in appinfo/info.xml — AppAPI only rewrites an app's route table on a version change.
  • "Ask your own question", the freeform insight template. Pick it in Prepare, type a question, and the answer comes back in three parts: the answer itself in at most three sentences, the evidence with who said what and when, and — never omitted — what these meetings do not answer. The machinery for a caller-supplied question already existed ({{QUESTION}}, Workflow.TakesQuestion); what was missing was a prompt carrying the placeholder. A template that takes a question is offered wherever one can be typed and withheld where one cannot: it is not offered as the publish pipeline's summary step, which runs unattended with nobody to ask.
  • The Participant and project vocabulary setting now guides the transcriber itself. Terms are applied as sherpa-onnx contextual biasing, so the recogniser can prefer "Librocco" or "Aire Spaces" where it used to invent a spelling. The vocabulary is a decoder prior rather than a text rewrite; its score therefore stays conservative so it does not overpower weak acoustic evidence.
  • provenance.speechToText.hints in the build manifest records how many terms were used, the boost, and the decoding method. When a vocabulary is configured but cannot be applied, it records that and why, so a build that ignored the setting never looks like one that honoured it.
  • CASSINI_STT_HINTS_DISABLED turns biasing off and restores greedy search, so an operator can get back to the transcription they had before; CASSINI_STT_HINTS_SCORE overrides the boost. Both are passed through the deployment compose file.
  • The meeting list's search box now searches what was said in your meetings, not just their names and dates. A matching meeting shows the moments that matched, and clicking one opens the meeting at that point with the transcript already filtered to those lines.
  • When search cannot answer — too many searches, an index still building, or an unreachable archive — the list says so instead of showing an empty result that would read as "nothing matched".
  • Cross-meeting search results now carry a short quote of what matched, so a result says why it matched rather than only where.
  • cassini annotate reads and writes the tags and marks a recording carries inside its own .opus file. show prints them. apply takes a batch of edits — tag the whole meeting or a stretch of it, remove a mark, remove a tag, undo everything one run added, rename a tag — and rewrites the file once, without touching the audio, the transcripts or the summary. Sending the same batch twice changes nothing the second time, and a batch that changes nothing does not rewrite the file.
  • cassini annotate carry moves the marks from a delivered recording onto a newly sealed one, so re-running a meeting does not lose them. When the new recording's audio differs, the marks are kept and flagged as made against different audio rather than redrawn on it.
  • A meeting's tags and marks can be read and changed at annotations/meetings/<id>, by people in the app, the CLI and agents alike. Anyone who can read a meeting can mark it, and every mark is attributed to the Nextcloud account that made it, never to a name the request supplies. The marks are written into the recording itself, so they travel with it when it is downloaded or shared.
  • Two people marking the same meeting at once no longer overwrite each other: a write that finds the recording changed underneath it re-reads the newer copy and applies its marks to that. A meeting you cannot read answers exactly as one that does not exist.
  • GET annotations/tags lists the tags on the meetings you can open, each with how many meetings and marks carry it, and says how many of your meetings it could read marks for. A tag that is only on meetings you cannot open never appears and never changes a count.
  • published/search and published/meetings-list take tag= (a label in any case, or a tag id) and answer only from meetings carrying a mark of that tag. Each search result also lists the tags whose marked stretches it falls inside.
  • Cassini keeps its tag index in its own annotations.sqlite3 beside the job database. It holds nothing that cannot be rebuilt from the marks inside the recordings, and cassini-operator backfill-annotations rebuilds it: recordings that have not changed are skipped without a download, and a fresh index keeps the tag namespace the archive already uses.
  • A recording can carry tags. A tag such as hiring can go on the whole meeting or on a stretch of it, and the marks live inside the recording's .opus file, so a meeting keeps its tags when it is downloaded or shared. The portable format and its schema describe them, and a reader that does not understand them still opens the recording normally.
  • cassini meetings tags lists the tags on the meetings you can read. cassini meetings annotations <id> shows one meeting's marks, and cassini meetings annotate <id> --ops <file> adds or removes them in one batch. Every mark records the Nextcloud account that made it, and whether a person or an agent made it, so an agent's whole run can be undone in one step.
  • cassini meetings search and cassini meetings list take --tag to look only at meetings with that tag. Against a Cassini app that does not offer tags, they fail instead of silently returning every meeting.
  • cassini inspect reports a recording's tags: how many there are, and whether they were made against this recording's audio.
  • You can tag meetings from the app. Each row in the meeting list shows its tags; tag one meeting from its row, or select several and tag them together. A filter beside the rooms narrows the list to one tag or several.
  • Inside a meeting, you can tag a section of the transcript: select its words (or, on a wide screen, drag down the rail beside it), choose Tag selection, and set exactly where it begins and ends with the handles in the text or the arrow keys. Tagged sections are underlined in their tag's colour and, on a wide screen, bracketed beside the text with the tag's name; arrows step from one to the next. The last tag you used is offered again beside Tag selection, so tagging several sections alike is one click each. On a phone, a section's tag sits in the text in front of it, and its details and the arrows sit above the player.
  • A tag has a colour and, if you want, an icon. You choose them where you create the tag, and change them in Manage tags, where tags can also be renamed, merged and deleted. A change reaches the meetings you can read; meetings in rooms you cannot open keep the tag as it was. Colours are stored for the installation rather than inside the recordings, so recolouring a tag rewrites nothing.
  • Each AI provider now carries a default model, set in AI providers. Summaries and insights use it unless the step names its own, and the Prepare panel's model field opens pre-filled with it, so a run can use another model without picking one every time.
  • A failed insight can be retried from its card in the browse list and from its document, not only from the panel that started it. A failed run now records only a reason (reason on the run, one of fourteen tokens); the app decides the wording, so the words can improve without touching stored runs.
  • One dialog, once per install. The first administrator to open a fresh Cassini is told that recordings, and the names of the rooms they came from, will be visible to anyone with an account on this Nextcloud, and given one button that creates the cassini Nextcloud account they are kept in. Nextcloud asks them to confirm their password, and Cassini never sees it; the account's own password is generated, set and discarded. The dialog is recorded as seen by the install rather than by the browser, and only once the account exists: an administrator who leaves it to change the audience first is asked again next time, and so is one whose install cannot create the account at all — where Cassini cannot create it, the dialog says so and points at Operator › Settings instead of claiming to be ready to record. On Nextcloud releases that let an app write to user administration, Cassini creates the account when it is enabled and nobody is asked anything.
  • Operator › Settings › "Who can see recordings", with two audiences and one sentence each: Everyone with a Nextcloud account and Meeting participants. The one in force is marked, and the line beneath says how many recordings already exist and who can see them, including after a switch, when the recordings from before it stay visible to everyone. Where recordings are stored, the service account and the occ recipe are behind "Details for administrators". Where the cassini account does not exist yet, the section says so and offers to create it, whichever audience is in force.
  • The meeting list says who can see the recordings in it, beside the meeting count: "Visible to anyone with a Nextcloud account" or "Visible to meeting participants". Administrators and everybody else see the same chip.
  • A switch shows its four steps while it runs, with a count of the recordings copied, and says the page can be closed; one that was interrupted offers Resume. Choosing Meeting participants without Team folders or Everyone Group offers to install them, and says that Everyone Group adds a group called Everyone to the whole of Nextcloud first. The confirmation says what the switch means for new and for existing recordings, in a number, and the direction that widens the audience carries that number in its own button.
  • Added linux/arm64 multi-arch support to the portable Cassini ExApp container image. Releases, pushes to main, and manual workflow runs now publish multi-platform images (linux/amd64 and linux/arm64) with bundled ARM64 sherpa-onnx runtime libraries and Parakeet ASR models.
  • Recordings a switch left visible to everyone can now be limited to the people who were in the call. Choosing "Meeting participants" on an instance that already has recordings copies them into the Team folder and leaves them readable by every account — Cassini had no way of knowing who was in a past meeting, and said so once, in the page that performed the switch. It now keeps that record while each meeting runs: the accounts, groups and teams with access to the room are captured when a recording starts and again when it stops, and frozen, so a room that gains or loses members later cannot change who may open a recording made before the change. Operator › Settings › "Who can see recordings" lists everything still visible to everyone, shows exactly who each one would be limited to, and limits the ones you tick. Groups and teams are granted as themselves rather than expanded, so reorganising a team is how access moves with it.
  • Recordings it cannot limit say why, instead of sitting there unexplained — recorded before Cassini kept the record, everyone in the call was a guest with no account to grant, or no job of this instance owns the file. Recordings of public conversations never appear at all: they are readable by everyone because the conversation was public. Anything needing a bespoke audience is still Files → Advanced permissions, and a recording you want left open can be dismissed from the list and brought back again.
  • The meeting list's search box now finds meetings by the names of their tags. Typing part of a tag's name brings up the meetings carrying it, alongside matches on the meeting's own name, date and what was said in it.
  • A recording published on its own — in a static export, or embedded in any page — now shows the tags and marked stretches that are inside it, read from the file rather than from a server.
  • <cassini-meeting src="…"> embeds one recording in any page from a single script tag, served from gocassini.com/embed/v1/viewer.js. The attribute contract is cassini-viewer/ATTRIBUTES.md.
  • An embed carries a "Recorded with Cassini" footer by default, linking to gocassini.com. Remove it with the hide-badge attribute.
  • Tag colours and icons are now saved in each recording's cassini.annotations.v1 document. Downloaded recordings and the public viewer retain their tag appearance without an installation-local lookup.
  • Cassini doctor and the operator /status diagnostic endpoint now verify the native speech engine runtime and report whether the Parakeet v3 reference frontend optimization (+cassini-parakeet-v3-reference-v1) is active. If an unpatched library is detected, a non-blocking warning is reported with guidance on how to enable the optimized runtime.
  • cassini dev meetings pull --out <dir> mirrors the meetings a Nextcloud account may read into a local seed pack — catalog.json plus meetings/<id>.opus, the shape a published site has always had. It pulls everything by default, --limit keeps the newest N, and --room/--from/--to narrow it the way cassini meetings list does. --dry-run reports the total size before committing to the transfer, and re-running skips what is already on disk at the size the server reports, so an interrupted pull is resumed by repeating the command. Every request is a GET or a HEAD; nothing is written back.
  • cassini dev stack up --seed <dir> loads a seed pack into the harness, and harness/bin/seed-nc-files.sh --pack <dir> does the same against a stack that is already running. A local development stack can now be brought up holding a real archive instead of a synthetic fixture, which is what the meeting list, cross-meeting search and insights actually need to be worked on. Seeding is a filesystem copy plus occ groupfolders:scan rather than a WebDAV upload, so 2 GB of recordings are copied and scanned in about four seconds, followed by a permissions pass of roughly half a second per meeting. The catalog is merged rather than replaced, so meetings a stack recorded itself survive a seed.
  • Seeded meetings are given an explicit read grant, and are therefore readable by every account on the stack, which the seeder says on every run: production's per-meeting permissions name production accounts and do not travel with the data. They also carry no operator job history. Neither affects the published surfaces, and both are stated so that no access-control change is tested against a seeded archive.
  • Recordings now carry a wall-clock anchor per audio track. Each track gains first_packet_wall_ms and first_timeline_ns MKV tags: two readings of the instant the track's first packet arrived, one in recorder wall time and one in meeting time. Because both come from the same monotonic clock, a later stage holding a wall-clock instant can convert it to a position on the meeting timeline exactly, without reopening the session artifact. The tags are emitted as a pair or not at all, and a recording that predates them reads as unknown rather than as the epoch. A third tag, first_rtp_timestamp, is recorded for triage only — Janus rewrites the timestamps it relays to each subscriber, so it is not the sender's clock and cannot place a sender-side time.
  • The selection bar's count opens a list of every meeting you have picked, including the ones the current room or search is hiding. Open a meeting from there, or unpick it.
  • The scripted transcript role, for authored text a recording is a performance of: a read script, a song's words. It is not a transcription, so it never names a source transcript.
  • Recording checks in Operator → Publish pipeline guide administrators through storage, processing, Talk authentication and handoff, followed by a real Talk recording and playback confirmation. Existing recordings remain accessible.
  • Persist the Talk internal secret securely on Cassini's volume, with deployment configuration taking precedence. Diagnostics expire and remain advisory; missing local credentials are reported before recording starts.
  • Provide installation-specific host commands and copyable provider requests, plus first-ExApp, ARM64/amd64 and AIO persistence guidance.

Changed

  • Skip standalone recorder integration jobs on PRs confined to the operator, frontend, microsite, or PR-conflict automation. Shared and unknown inputs still run the scenarios, and main pushes retain full coverage.
  • Storage that does not match the recorded audience is reported as a mismatch that refuses to publish and names both ways out. A Team-folder list Cassini cannot read counts as an unanswered question rather than as "there is no Cassini Team folder" — the folder list arrives with every folder on the instance in one response, so a single unrelated folder in an unexpected shape used to make the whole answer undecodable and indistinguishable from an empty one.
  • Cassini no longer builds Team folders or their ACL topology on its own. On every enable it checks what is there and reports what is not; it makes those changes only when you ask for them. A half-built setup can no longer end up looking healthy.
  • Talk refuses to start a recording when the storage it would be published to is missing a named prerequisite, instead of capturing the whole call and failing afterwards. The moderator sees Talk's own "The recording failed" message; the reason, and the command that fixes it, are in Cassini.
  • Installing the two Nextcloud apps is the one step Cassini still cannot take for you, and it now says so instead of implying otherwise. Nextcloud requires an administrator's password on that particular request, which Cassini declines to handle; it attempts the install through its own backend — which works on Nextcloud 32.x, 33.0.0–33.0.5, 34.0.0, and on any release where allowed_no_password_confirmation_ranges is set — and otherwise links you to Nextcloud's own Apps page. When the App Store refuses, the message says plainly that Nextcloud reports the same error whether the app is unknown, incompatible or the store is unreachable, and that retrying inside five minutes will say the same thing.
  • backfill-nc-files refuses to run where recordings are visible to anyone with a Nextcloud account, instead of writing access rules into a folder where they mean nothing. Everything it writes is protected by Team-folder permissions, and those recordings live in the cassini account's own folder where every signed-in account can already read them — so the migration would be at best a no-op with side effects, and at worst would re-derive an audience for recordings an administrator had deliberately left open. The refusal names the audience, writes nothing, and points at the setting that actually moves an archive between the two.
  • The recordings-permissions, install and privacy documents now describe both audiences. The upgrade section in particular had the Team-folder path collision backwards — a Team folder takes the Cassini path and Nextcloud renames the colliding home directory, rather than the mount being remapped — so its instruction to move the old folder out of the way first was unnecessary.
  • A keyless OpenAI-compatible endpoint now switches meeting summaries on: LLM_BASE_URL alone is enough, and OPENROUTER_API_KEY is needed only when the endpoint requires one. Previously both had to be set, so a self-hosted model server silently produced no summary. On upgrade, an install that had LLM_BASE_URL set without a key starts producing summaries through that endpoint from its next build.
  • With no key configured the recorder omits the Authorization header entirely rather than sending an empty bearer token, which some self-hosted servers reject. New request bounds for slow local models: CASSINI_LLM_TIMEOUT_SEC (default 900) and CASSINI_LLM_MAX_TOKENS (default 4096).
  • cassini meetings search says where a speaker id comes from — the speaker= value a previous result printed — so --speaker can actually be used. The ids are opaque and are not meant to be typed from memory.
  • The control panel names each recording after its conversation. A recording was labelled by its room token — Call 3qbs6123vxx — which identifies the call but not which conversation it was. Rows and the detail heading now show the Talk conversation's display name wherever the operator has it, keeping the token label as the fallback for a non-Talk recording, one whose room-name lookup never completed, and anything recorded before this release.
  • Opening a meeting slides it in over the list from the right, instead of replacing the list with a second column; on a phone it arrives as a bottom sheet. Close it with the × or Escape, and the list is still there, in the same place, with the same room selected.
  • The search box moved to the top of the list and filters within the selected room. It still matches a meeting's name or date, and the room and search narrowings each show as a chip you can clear.
  • A word can now be flagged lowConfidenceSpeaker only when its own track sits well below that speaker's own speech level (the quiet-owner signature). Genuine double-talk between microphones with different signal-to-noise ratios measures large gaps but is never flagged, and the crosstalk threshold is estimated on the quiet-owner population alone. The per-word attributionGapDb measurement itself is unchanged and still annotates every word.
  • The meeting manifest records what the attribution stage did: whether it ran, the mode (annotate/drop/disabled), words measured, flagged and dropped, and the estimated threshold when one was found. A build that deletes words in drop mode is no longer indistinguishable from one that never saw them.
  • CASSINI_STT_BACKEND, CASSINI_ATTRIBUTION_DISABLED and CASSINI_ATTRIBUTION_DROP are documented and passed through the deployment bundle; an unknown backend id now fails the build before any decoding or model download instead of after the mixdown.
  • CASSINI_ATTRIBUTION_DROP=1 removes flagged crosstalk words from the transcript itself instead of only marking them, and CASSINI_ATTRIBUTION_DISABLED=1 skips the measurement. Marking is the default: real crosstalk overlaps the level range genuine simultaneous speech occupies, so deleting canonical words on a threshold is opt-in.
  • The word being spoken now lights up instantly instead of fading in. The fade lasted about as long as a short word, so the highlight was always part-way through changing and never quite agreed with what you were hearing; it now tracks the audio exactly. Hovering a word to seek also highlights it immediately.
  • Overlapping speech reads as speech again. A turn is now one paragraph, however many pieces the producer cut it into: when two people talk over each other their sentences no longer alternate in one-to-three-word fragments. A short reaction someone made while another person was talking appears as a quiet chip where it happened, inside the sentence it landed in, and a turn that genuinely ran over another says so by naming the other speaker.
  • Durations are gone from the transcript. A badge reading "0.2 s during Ivan" on every fragment was precision nobody acts on, and it made the page harder to read than no marking at all. The measurements are still there for anything that wants them; they are no longer shown.
  • captions.vtt is now always produced, from the transcript itself. It used to exist only when an LLM endpoint was configured, as a by-product of cleanup.
  • LLM_BASE_URL, OPENROUTER_API_KEY and LLM_MODEL in the deploy environment now only pre-fill the LLM settings on the app's first start; after that the stored settings are what every build receives.
  • A non-administrator sees no template picker: the template registry is admin-only, so their insight runs whichever template the administrator configured, and the card says so rather than offering a control that would be refused. On a deployment with no AI endpoint the card does not appear at all — the existing "No AI endpoint is available" notice takes its place — and in a standalone static export, which has no operator to ask, neither appears.
  • docs/exapp-update-constraints.md answers the question it used to flag as unverified: an in-place app_api:app:update does rewrite AppAPI's route allowlist, but only when the manifest it is handed carries a new <version> — with the same version it short-circuits on "already updated" and the allowlist is untouched. Routes therefore behave unlike deploy environment variables, which stay creation-time. The rule for release review is that a release changing <routes> must bump <version>, which scripts/bump-exapp-version.sh already does. The measurement is written up with its evidence and is re-runnable as harness/bin/check-route-refresh.sh.
  • The privacy note now covers insights: a question sends the picked meetings' transcript and summary text to the configured LLM endpoint, the call uses the instance's API key — so the request reaches your provider attributable to the deployment, not to the person who asked — and the answer is written into that person's own Nextcloud Files, never beside the recordings.
  • The subprocesses an insight runs no longer inherit the operator's own credentials. The AppAPI shared secret, the Talk recording and signaling secrets and the operator API token are removed from their environment: none of them is read by any cassini command, and the AppAPI secret in particular is what lets a process act as any account on the instance. The step that only assembles the meetings loses the LLM credentials as well, since it never calls a model.
  • An insight is bounded in the three ways that can run away: how many meetings one may cover, how long one attempt may take before it is stopped, and how large the answer may be. Each limit is a refusal that says so, never a silently shortened document, and only two runs are performed at a time — the rest wait, and say they are queued.
  • The in-Nextcloud viewer now reads its meeting list from the app's list endpoint rather than the published catalogue file, so a Nextcloud outage no longer empties the list: it keeps showing the meetings it last loaded and says what went wrong, instead of silently rendering as though the account had no recordings. Standalone exports and the static viewer are unaffected, and the viewer falls back to the catalogue file against an app that does not serve the endpoint.
  • Transcription runs on the CPU when the host has no GPU. Operator builds resolve their device per run — CUDA when the image carries the CUDA runtime and an NVIDIA device is visible, CPU otherwise — instead of blocking every build on a GPU-less install. Recordings already stuck in build/blocked recover with Rerun in Cassini Admin; no image or deploy-daemon change is needed.
  • Each image carries the model of the tier it runs by default, and any other tier downloads once into a model cache on the persistent volume. The portable image bakes the 0.6B int8 model, the GPU image bakes fp32, and both stay small. A download survives a container recreate, and a host with no network egress gets a clear error that names the missing model. Cassini Admin shows the size of that download before the first build of the tier.
  • cassini meetings list now asks the app to filter by date and room, instead of downloading the whole catalogue and narrowing it locally. Against an app that does not offer that yet, it falls back to the previous behaviour, so an older CLI and a newer one both keep working.
  • cassini meetings list, rooms, fetch and context now report an unreachable recordings archive as a failure rather than as an empty result. Previously an outage was indistinguishable from "this account has no recordings" — or, when fetching by id, from "no recording you can read at that id".
  • Stop reasons and recording durations appear in run metadata. The operator view formats raw reason codes into plain labels and displays the total recorded duration in sidebar cards and detail panes.
  • cassini meetings context fetches the recording catalog once per run instead of once per meeting, so every meeting in a bundle is resolved against a single view of what you are allowed to read.
  • --keep-opus is refused when more than one meeting is asked for, and so is the same meeting id given twice. Both used to be silently meaningless; one file cannot hold several recordings, and the same transcript twice spends the context budget on one meeting while reading as coverage of two.
  • The publish pipeline's summary step and the summarise insight now run the same prompt file, not two copies of it. The summary is an insight like any other, so it names the template it runs; the bytes it sends are unchanged.
  • A prompt whose bytes are not compiled into the image is not listed and cannot be named — a template you can select is a template that will run. Two of the templates drawn in the design have no prompt authored yet and are therefore absent rather than shown as unavailable.
  • Editing a shipped prompt now fails the lint check until its version is bumped and its hash re-pinned. Prompt files are markdown, and a markdown-only change used to run no tests at all while changing what the product sends to a model.
  • CASSINI_SUMMARY_DISABLED no longer blocks cassini insight run. It turns off the summary written when a meeting publishes; an insight is a document you asked for by name, and turning insights off is removing the endpoint.
  • Configuration lives inside the Operator surface again, behind a left nav, instead of in a Settings tab of its own. Two tabs remain — Browse and Operator — and Operator now opens on a nav with two groups: Console (Recordings) and Settings (AI providers, Publish pipeline, Insight templates). The panels themselves are unchanged; only where you reach them moved. Below 720px the nav is a dropdown, and each panel opens at its own top rather than wherever the previous one was scrolled to.
  • A panel can be linked to directly: #surface=operator&panel=endpoints opens AI providers.
  • The run console splits into a list and a detail pane based on how wide the console itself is, not how wide the browser window is. In Nextcloud the app sits beside Nextcloud's own navigation, so a window wide enough by the old measure could still squeeze the run detail into a couple of hundred pixels.
  • Registering your first AI endpoint now switches meeting summaries on, pointed at it. An install that has just configured an endpoint and still publishes meetings without summaries has done the work and not got the feature. It fires once — on the save that takes a deployment from having no endpoint to having one — so an administrator who turns summarising off keeps it off, whatever they save afterwards. The AI providers page says so before you save, since it is a decision about what leaves your deployment.
  • AI providers configures endpoints and nothing else. Adding one is a form rather than a hidden row of fields, a saved endpoint reports whether its model list actually came back (and says so plainly when it did not, since an endpoint with no /models route still answers completions), and the page says which endpoint an insight will actually reach.
  • Publish pipeline is now the whole pipeline in the order it runs: detected hardware, transcription quality, summarisation, and the device override last. Summarisation moved here from AI providers — it is a step between the call ending and the meeting being published, and it was only living under endpoints because its endpoint is.
  • The summary step names an insight template from the registry instead of asking for a workflow id as free text, and its model field is a combobox that loads the endpoint's models when you open it rather than behind a "Load models" button. Typing a model the list does not have is still accepted: the list is the endpoint's, not a limit on what you may send.
  • The "this needs an AI provider" state is now the same compact card wherever it appears — the summarisation step, Insight templates, and the Prepare panel — with the button that fixes it. Anyone who is not an administrator still gets the explanation instead, because that panel would refuse them.
  • Create an insight in the Prepare panel is the template you pick and what it will ask, in one box: choosing a template shows the question it puts to the meetings and the shape of the document that comes back, or a question box where the template takes one. It opens on a real template rather than on a row reading "this deployment's default", which named nothing and read as a setting instead of a choice.
  • The Prepare panel no longer lists previous insight runs. It was a job console in a panel whose subject is the meetings you just picked, and a second place insights were listed — free to disagree with the browse catalogue behind it. The card now shows only the run it starts, which is feedback for a button press; every insight, that one included, is a card in the browse list as soon as it exists.
  • The publish pipeline's summary step opens on Meeting summary — the template it actually runs — instead of a "Default (the one Cassini ships)" row standing in front of it.
  • Meetings / Insights moved out of the list header into the rooms rail, under "Show", beside the other narrowing of the same archive, with a clearer gap between the two lists.
  • A meeting's header now carries its name, room, date, length and speakers, and stays put while the transcript scrolls under it. The insights a meeting was used in read directly under its summary rather than in a strip below the whole transcript, and an insight's own header carries its room, date and the model that answered.
  • Insight cards in the browse list are filled rather than outlined, so they read as a different kind of thing at a glance rather than only by the rule down their edge.
  • Upgraded sherpa-onnx Go bindings and native runtime libraries to v1.13.7 across the recorder and container base images.
  • Transcription output changes for every meeting, not only for the listed terms. Hotwords are only read under modified beam search, so the balanced and best quality tiers now decode with beam search instead of greedy search. It is a different decoder and it decides differently on marginal audio: measured on real meeting audio it lost a sentence on one clip and gained words on another. Decode cost is 1.3x to 1.5x, still far faster than realtime (worst measured realtime factor 0.091, so an hour of meeting decodes in about five and a half minutes on CPU). Memory is unchanged.
  • The fast quality tier is unaffected. It runs a CTC model, which sherpa-onnx cannot bias, so it keeps greedy search and records the vocabulary as not applied rather than ignoring it silently.
  • The vocabulary previously steered the LLM cleanup prompt and explicitly did not affect the transcript. It now affects the transcript and nothing else.
  • Decoder hints need a model bundle that ships bpe.vocab. Bundles without one record the vocabulary as not applied, and say so, rather than decoding unbiased in silence.
  • Re-running a meeting no longer erases the tags and marks added to its recording after it was published: the rerun carries them into the recording it delivers. Marks made against audio the rerun changed are kept and flagged as no longer matching, never dropped. If marks are being written at the moment of the rerun, the publish fails and asks to be re-run rather than overwrite them.
  • cassini-operator backfill-search recognises an archived recording that has gained marks since it was delivered as the same recording as the local copy — it compares the audio, not the whole file — and keeps indexing it from the local transcript's own segments.
  • The UI-only branch preview job is temporarily disabled (D-739). Every meeting in the published archive uses the exact-pcm audio integrity policy retired on 2026-09-02, so the export can read none of them and the preview failed for every branch on every push, over data no branch can fix. docs/runbooks/repair-published-archive.md is the repair procedure, and re-enabling the job is one line and part of closing D-739. Opt-in processing previews are unaffected: they build their meetings from raw recordings.
  • The endpoint's address stays with administrators. The insight document's provenance records the model and endpoint kind but no longer its base URL; an unnamed endpoint is shown as "Endpoint 1" rather than by its host; and a model list that cannot be fetched is reported to non-administrators without the URL.
  • Registering your first endpoint switches summarising on exactly once in a deployment's life. Removing every endpoint and adding one back no longer switches it on again for an administrator who had turned it off.
  • Removing an AI endpoint asks for confirmation first, naming the endpoint, that its stored key is destroyed, and which steps stop running on it.
  • Finding words inside a meeting now counts the matches, steps between them, and marks where they are on the rail beside the transcript. Hiding the turns that do not match is now a switch, off by default, instead of what always happened.
  • Generating an insight closes the Prepare panel and puts the new run at the top of the browse list, where it updates like every other insight.
  • The selection is bounded to the operator's 20-meeting cap before a request is made, with a sentence saying how many to unpick.
  • Cassini works out who can see recordings when the app is enabled, instead of refusing to record until it is told. A fresh install records as "everyone with a Nextcloud account", from the moment the cassini account exists. An install whose Cassini Team folder already holds recordings keeps "meeting participants", without asking and without moving anything, and recordings stranded in the other root are counted and offered a move. Cassini never widens an existing archive on its own, and "nobody has chosen yet" has stopped being a state an install can be in. A mode resolved this way reports mode_source: "resolved_on_enable"; a mode an older version recorded on its own, or an interrupted switch left behind, is kept and no longer needs confirming. Where Cassini cannot read the recordings it already has, it records no mode, says so on GET /operator/status, and looks again the next time the app is enabled; recording still goes ahead. The CASSINI_STORAGE_MODE deploy option is unchanged and still takes precedence.
  • A problem is one sentence now. Where storage is genuinely broken, Cassini says it cannot save recordings right now, names the cause in words, and offers Try again and Show details; the step, the paths and the commands live inside that disclosure. Somebody who is not an administrator gets the same sentence and no buttons, and nobody is told that an administrator has to decide anything. The cause is written by the operator, one sentence per check, so /status and the app say the same thing about the same failure. A check that has not run yet, as after a container restart, is a neutral band rather than a warning, and the meeting list stays where it is.
  • The storage API reports the install as well as the archive. GET /operator/storage carries first_run, answered once per install, and migration with the phase and counts while a switch runs. GET /operator/setup carries the mode, so the audience can be named for people who are not administrators. recordings_access on /status gains a cause sentence, and /setup carries the form of it that names no account, path or app.
  • The App Store description, the microsite and the documentation say who can see a recording before they say how it works, with the default first and the opt-in after it, in the same words the settings section uses.
  • The warning about widening no longer overstates how permanent it is. Switching to "Everyone with a Nextcloud account" said the recordings it opens will not be re-limited by switching back, which is still true and no longer the whole story: they can be limited again from the same settings section.
  • A public viewer is read-only by construction: the marks in a recording are drawn, and no control that would add or remove one is shown. Previously the viewer hid tags entirely when there was no operator to write them.
  • Tags that no vocabulary knows — every tag in a recording read on its own — are dealt distinct colours from the palette instead of each hashing its own id, which collided often enough to show several tags in one colour.
  • Rewrote the root README around the Nextcloud app: what you get, what leaves your server, the meeting file, requirements, install steps, and the CLI and agent skill.
  • Replaced the documentation site's pages with a set about installing and running the app, and added docs/cli.md for the CLI from a checkout.
  • Renaming or restyling a tag uses the existing visibility-scoped background rewrite flow. Only recordings the caller can read are updated; inaccessible recordings keep their saved appearance.
  • Graceful degradation on unpatched sherpa-onnx runtime. Parakeet v3 speech transcription no longer halts when running against an upstream or unpatched libsherpa-onnx-c-api runtime. Instead of failing recognizer initialization, Cassini logs an informative warning, gracefully falls back to the standard decode profile (defaultVADDecodePolicy()), and records referenceFrontend: false in provenance metadata. This keeps transcription available when the reference frontend is absent.
  • Cassini images now build the newest stable, officially signed FFmpeg source release selected at image-build time. CPU and CUDA image smoke tests gate the media capabilities Cassini requires instead of pinning a release number.
  • Release images now build with the FFmpeg version the current main image was already built and gated against, instead of resolving the newest upstream release again at tag time. Main keeps tracking the newest stable release, so an upstream regression still surfaces there first — but a release is never the first place a given FFmpeg appears, rebuilding a tag reproduces the same binary, and rolling back a release no longer silently rolls back FFmpeg with it.
  • Playback is shown in neutral rather than the accent colour: the word being played is inverted, and the play button, the scrubber and the Follow switch match it. The switch is called Follow at every width, turning it back on takes you straight to the audio, and the "Auto-scroll paused" badge is gone.
  • A meeting reads with less around it: its details sit on two lines under the title, Summary, Insights and Transcript share one heading, and the transcript's search counts its matches and steps between them inside the field. The meeting always leaves a strip of the list beside it to click back to.
  • The meeting list, the rooms rail and the Browse and Operator tabs follow the Cassini design. Dates name the day of the week, and who can open recordings is stated beside Rooms rather than among the filters.
  • The Operator's sidebar is a drawer on a phone, and its settings pages sit on the page rather than in nested cards. First-run setup asks who can see recordings as a choice, and its problems read as warnings with a way forward.
  • Prepare warns only about what stops it: a selection over the limit, or a meeting published in the older format.
  • Portable meetings identify their recording from canonical compressed Opus packets, making integrity checks independent of decoder implementation details.
  • Portable packing now converges on FFmpeg's stable Ogg end-trim granule before embedding that compressed identity, including multi-speaker mixes whose first metadata remux normalizes a few trailing samples.
  • Portable word transcripts no longer carry origin roles or source links. The packer, inspector, viewer, and static exporter accept entries with no role and ignore legacy origin metadata. Display transcripts still pair with their source words; withdrawn cleanup entries are skipped. The reference schema and format documentation also drop unused chapters and duplicate meeting hints.
  • Packed meetings use the published portable meeting format: CASSINI_FORMAT=org.cassini.portable-meeting/1, manifest "version": 1, and a resolvable CASSINI_PAYLOAD_SCHEMA served at https://format.gocassini.com/. Readers accept this contract directly and reject any other format.
  • Portable meeting producers and readers now implement only the published version 1 contract. Transcript bodies contain word items, the media digest comes from integrity.opusAudioSha256, and unsupported shapes are not repaired or upgraded.
  • Add opt-in recording-first scheduling: defer builds/sealing/publication while recorders are active, and yield running builds without spending resource retry attempts or losing captured media.
  • Serialize recorder finalization across subprocesses under recording priority.
  • Credit conservatively reclaimable cgroup file cache during memory admission, fixing waits caused by clean cache rather than memory pressure.
  • Add explicit concurrent/recording-first/auto policies. Auto permits recording overlap using live host/cgroup CPU and memory headroom, with a GPU-aware host CPU budget. Existing deployments retain concurrent behavior by default.
  • Simplified Go helpers and workflow context handling; shortened repetitive implementation comments.
  • Moved the removed Python transcriber's architecture notes into docs/history/.
  • Linux amd64/arm64 CPU builds now use prebuilt Parakeet v3 frontend libraries from codemyriad/sherpa-onnx-go-linux v1.13.7-cassini.4, with matching upstream v1.13.7 Go bindings. Normal developer, Docker and CI builds no longer compile sherpa-onnx. CUDA source builds consume a checksum-verified fork commit instead of applying a local patch. Other platforms retain stock libraries and the graceful fallback. Operator runtime diagnostics coalesce concurrent probes and refresh cached results, including unavailable runtimes, after 30 seconds.
  • Tag edits appear immediately after saving, while recordings update in the background. Delayed recording updates remain visible and can be retried.
  • Pending tag edits now survive operator restarts. Keep the annotation database on durable storage; rebuilding it from recordings cannot recover edits that have not yet reached those recordings.
  • Repairing a recording with the same audio restores saved tags. Replacing its audio clears the old annotations.
  • Recordings skipped during initial annotation import retry automatically and when listed or opened.
  • Large valid annotation documents can update their recordings; permanent recorder rejections show a blocked status instead of retrying indefinitely.
  • An older background status error no longer clears tags saved by a newer edit.
  • Rebuilding tag/search rows restores pending edits from the durable annotation database, even while the archive is unavailable.
  • Browsing the app prepares newly seeded recordings for tagging, imports wait for download capacity instead of being skipped, and bulk tagging automatically retries temporary preparation responses.

Removed

  • Word-level playback highlighting is gone from the meeting view. The transcript lit one word at a time and invited you to click a word to seek, which claimed a precision our transcripts do not carry. A line is the unit now: click any line to play from there. The "Exact words" toggle and the word/mixed/passage timing badge go with it.
  • The LLM readable-transcript cleanup step, which was shelved. Transcripts publish as transcribed, with speaker attribution and paragraph breaks but no model rewriting the words. Gone with it: CASSINI_READABLE_STRICT_BATCHES, the --strict-readable-cleanup build flag, the cassini-readable component, and the transcript.readable.v1.json producer. Meeting summaries are unaffected and remain the only LLM step in the pipeline. Files that already carry a readable transcript still open: the portable format keeps reading them.
  • The Model override field. It accepted only the three models the quality tiers already select, so it duplicated the tier control with no discoverability — and pinning an int8 model alongside CUDA produced a run that fragmented back onto the host CPU while reporting a GPU. Quality and device are now the whole of the policy. A model_override left in an existing settings.json is ignored.
  • The Insights step in AI providers. It configured "run insights somewhere other than the summary endpoint" while looking like the switch that turns insights on, which it never was. The field is still persisted and served, so PUT operator/settings/llm can still point insights at a larger model.
  • The Prepare panel no longer says that a meeting "does not record whether it has a summary". It is true of every meeting published before the catalog carried the field, it changes nothing about what the bundle contains, and it buried the one gap that does stop a bundle being assembled.
  • Removed the top-level ops/ directory; moved AppAPI registration helpers and batch processing scripts to scripts/, relocated production deploy tooling to codemyriad-systems, and purged stray out-of-scope host configuration.
  • The Setup tab and its wizard are gone. Cassini no longer opens on a question that blocks every recording until somebody answers it, and a link to #surface=setup now lands on Browse.
  • Removed an accidentally tracked harness executable; harness scripts continue to run its Go source.
  • Removed obsolete diagnostic reproduction tests; the production setup paths retain regression coverage.

Fixed

  • Manifest validation now rejects environment overrides whose names appear only outside environment declarations or inside XML comments, preventing AppAPI from silently dropping those values.
  • Tagging a selection of meetings now uses one atomic database batch and displays all updated rows together. Safe retries reuse the batch request ID; recordings continue syncing individually across the archive workers.
  • CI no longer fails when a third-party apt repository preinstalled on GitHub's runner image serves a bad package index. Every workflow install step now runs scripts/ci-apt-update.sh, which drops apt sources that are not the Ubuntu archive before updating, so a vendor we never install from cannot take a build down. On 2026-09-09 a corrupt Google Chrome index turned nine jobs across four workflows red on main with apt-get update exit 100; the previous remedy named packages.microsoft.com at three of the nine call sites, and this replaces it at all of them.
  • Restarting the Cassini container no longer stops it publishing until an administrator disables and re-enables the app. Cassini checked how Nextcloud is set up on the app's enabled edge, and that check was the only thing that could mark the storage usable — so a plain restart left it reporting an unknown state and refusing to publish, with nothing obviously wrong. It now re-checks at startup as well, for installations that have already recorded which storage model they use. A first install is unaffected: Nextcloud rejects the calls until registration finishes, so that case still waits for the enabled edge, which fires moments later.
  • cassini-operator backfill-search now indexes recordings this Cassini has no local copy of, by reading the published recording itself. Before, a meeting whose job record or working copy had been lost — which is every meeting published by an earlier installation, since the recordings outlive the app's own volume — was skipped entirely and silently absent from search. On the demo archive that was 25 of 30 meetings. Those are indexed at coarser granularity, and each result says which kind it is.
  • cassini-operator backfill-search now verifies a local transcript against the checksum the archive itself records for the delivered recording, instead of a digest from the job database that could never disagree with the local copy. A meeting whose local transcript belongs to a re-run that never published is detected and indexed from the published recording instead, so search only ever cites words that are in a recording you can play.
  • A read failure during backfill-search — a timeout, a busy Nextcloud — no longer removes a meeting's existing search entries. It is reported as failed and re-running is the fix; entries only change on evidence about the recording itself.
  • backfill-search no longer reads the job database at all, so it can run while the operator is publishing; the old database-is-locked failure is gone. Where the delivery recorded a checksum, a re-run now confirms an unchanged meeting with one metadata query instead of re-downloading the recording.
  • Speaker attribution now calibrates a participant's noise floor across all of their packet streams. A rotation or rejoin produces a fresh stream, and a short one carrying only ongoing speech had that speech as its own floor — so the real speaker measured as barely audible and a quiet track carrying only their bleed won the word by 20 dB.
  • Words keep their attribution provenance through LLM readable cleanup, so summaries actually exclude probable crosstalk. Readable cleanup and summarisation normally share one configured model, so losing the flag there made the summary protection a no-op in exactly the ordinary path.
  • Filtering no longer discards older segments that carry text but no word list.
  • A participant's noise floor is now the quietest floor any of their streams established, rather than a percentile over their pooled frames. Pooling was duration-weighted, so a long speech-only rejoin swamped the short stream that had actually seen the microphone idle, and crosstalk beat the real speaker again.
  • Readable cleanup maps each rewritten word to the single best-overlapping source word, so one contradicted word can no longer flag a legitimate neighbour and delete it from the summary.
  • Portable meetings now carry the per-word attribution evidence: cassini pack copies attributionGapDb and lowConfidenceSpeaker from transcript.words.v1.json into the portable transcript items (and cassini extract writes them back out) instead of stripping them from the published .opus.
  • The synthetic meeting generator now slides a turn that starts before the same speaker's previous one has finished, instead of aborting the whole run. Aborting happened partway through, leaving earlier participants generated and later ones missing — which is how the committed showcase fixture ended up with real speech for only two of its six speakers.
  • Transcription tests now build meetings with production's shape — late-joining tracks, DTX-sparse tracks, and rejoined streams sharing one participant id — instead of tidy tracks that all start together and are fully dense. The previous fixtures could not express the conditions that two thirds of production streams actually have.
  • The meeting list and header now show the date the meeting was recorded, not the date its file was last processed. Reprocessing an archive no longer restamps every meeting with the rebuild day.
  • Meeting list cards now show the meeting's start time alongside its date.
  • The viewer now shows when two people spoke at once. Overlapping speech used to render as ordinary consecutive paragraphs with no signal at all, and because a turn is cut at every speaker change, a listener's "right" in the middle of someone's sentence split that sentence across two paragraphs with an unrelated turn wedged between them — so a continuous utterance read as two turns and an interruption. An interrupted turn is now drawn as one turn containing the interjection, each turn carries a badge naming who it overlapped and for how long, and playback highlights every turn that is sounding rather than only the latest one to start.
  • Overlap is judged on the words that actually sound rather than on paragraph extents, and a word whose end ran past its own audio is pulled back before anything is drawn. Without that, meetings recorded before the timing fix would light up with overlap that never happened: across nine archived meetings their paragraphs intersect for 94.2 s while their words only sound together for 15.7 s. A meeting whose recording already measured where each word ends says so in its own file, and the viewer leaves its timings exactly as recorded — so a word somebody genuinely held for a second and a half stays that long.
  • A word now ends where its speaker's own audio ends. Parakeet stamps a trailing punctuation mark at the next acoustic onset, and the token-to-word conversion took that mark's timestamp as the word's end — so a sentence-final word could run for seconds while its speaker was silent, and across whoever spoke during that pause, which the viewer then drew as an overlap that never happened. The mark still travels in the word's text; it no longer decides the word's extent. Instead the energy gate that already decides whether a word is real follows the speaker's own track forward from the last spoken token and stops at the last active 10 ms of it, bridging a silence of up to 200 ms as a stop closure inside the word. A word built entirely from punctuation carries no audio of its own and is never extended, and no word may pass the end the old rule gave it. Measured against unmodified main on a 36-minute three-participant meeting (5549 words, the production fp32 model), the longest word falls from 4.07 s to 1.44 s and cross-speaker overlap stamped over a silent track from 4.18 s to 1.69 s, against 1.37 s of audible speech given up across the whole meeting, two words of it losing 100 ms or more. Those are whole-release totals; the final refinement alone — dropping an 80 ms grace after the last active frame — accounts for 0.38 s of that speech and for no word losing 100 ms.
  • Playing a long meeting no longer pins a processor core. The viewer rebuilt the entire transcript's highlight sixty times a second and re-checked every word on screen in order to move one of them, so on the longest published meetings playback ran at about twenty frames a second, the page stuttered while scrolling, and the highlight visibly trailed the audio it was meant to track. The highlight is now worked out once per transcript and touched only when the playhead actually crosses a word, which took the worst meeting in the archive from using most of a core to using under a tenth of one, at a steady sixty frames a second. Clicking a word to seek costs about half what it did on the same meetings. The longer you listened the worse it got, too: playback accumulated duplicate timers until it was running forty-four clocks where one was needed, so a five-minute listen was heavier per second than a one-minute listen. Memory and timer count now stay flat — measured over a continuous five-minute listen on the largest meeting.
  • A long answer is paragraphed again. Rebuilding a turn from the pieces the producer cut also undid the breaks it makes when somebody talks for a while, so one standup update rendered as a single 209-second block of text (the longest rows on that meeting were 209 s, 101 s and 97 s against a median of 8.7 s). The reading unit now follows those breaks where nothing came between two of the speaker's blocks, and overrides them only where somebody else's speech cut a sentence in half, which is the case this rebuilding exists for. Same meeting: longest row 58 s, median 10.4 s.
  • A quick back-and-forth is no longer folded into one turn. Rebuilding a speaker's turn now needs the person who spoke in between to have actually overlapped or touched it: if they said their piece in clean air and stopped, both people kept their own turn, in the order they spoke. Without that, five real turns of a fast exchange collapsed into one row with an answer rendered before the question it answered.
  • Playback follows the paragraph it is reading. It used to follow whichever producer fragment was sounding, so during two people talking over each other the page smooth-scrolled between the pieces of the same rebuilt paragraph every few hundred milliseconds.
  • The "probably crosstalk" warning is judged once over everything a turn actually said, instead of once per fragment. One flagged fragment no longer condemns a whole genuine turn, and a short reaction that is entirely flagged keeps its warning now that it renders as a chip.
  • The viewer now shows the meeting summary the recorder seals into the portable .opus. The summary was already inside the file (the summary.md attachment that cassini meetings reads), but the web viewer and the in-Nextcloud app never displayed it, so a meeting with a summary opened as transcript-only. It renders as Markdown in the existing Summary panel; a file without one, or with a damaged attachment, still opens without a panel rather than failing.
  • Selecting Fast now works on a CPU host. The pre-build environment check demanded transducer model files of every non-fp32 model, so the 110M CTC model — which ships a single model.int8.onnx — failed validation although it was bundled and correct.
  • An int8 model can no longer be pinned alongside CUDA. Those graphs fragment back onto the host CPU under the CUDA execution provider, so the combination reported a GPU run while doing something else; it is now refused when saved and when a build is admitted.
  • A stored device_override=cpu is preserved instead of being erased on every operator restart. Pinning cuda on a host without a usable GPU still blocks the build with an actionable message rather than silently using the CPU.
  • Filter values no longer appear in cassini meetings error output. The request URL is printed when a call fails, and its query string was not being masked.
  • Routine recording stops no longer trigger an error banner in the operator view. Successful runs with exit code 0 and normal stop reasons do not display the red error alert with internal runtime cancellation messages. The view displays an error alert only when a run or attempt actually fails.
  • Recordings from Nextcloud Talk display the call room token. The operator view reads the Talk base URL and room token from the trigger request. Sidebar cards and run details now show the call token and a direct link to the room instead of generic placeholder text.
  • A meeting link's timestamp survives a trip through the Operator surface. The shell writes its own hash params — the selected run, and now the selected settings panel — after the viewer's t=, and the viewer only reads t= when it is the last thing in the URL. So opening a recording at a moment, stepping into Operator, and coming back left a link that still named the moment but no longer jumped to it. The shell now keeps t= last, the way the viewing layer already does when it writes a link of its own.
  • Registering an AI provider is now the whole of the setup an insight needs. Before this, an administrator could save an endpoint and still be told "No AI endpoint is available" everywhere in the app, because an insight only resolved an endpoint through a pipeline step somebody had also switched on. An insight now resolves, in order: the endpoint its own step names, then the summary step's, then any registered provider — and the operator hands that resolved endpoint to the run, so what the settings page says and what the run receives cannot disagree. The rule that follows is stated in the AI providers panel and in docs/privacy.md: every endpoint you register is one an insight may reach, and removing them all is how insights are switched off. Summarising stays a separate opt-in that runs on every recording.
  • An insight document no longer opens with a paragraph of YAML. The recorder writes its provenance — which meetings, which prompt version and hash, which model — as front matter at the top of the file, and markdown renders --- as a horizontal rule, so twenty quoted keys arrived on screen above the answer somebody asked for. The provenance is still in the file, and the panel shows the same facts in its header and its "This run" list.
  • Hardened AppAPI registration lifecycle handling in scripts/lib-exapp-register.sh: stop immediately if unregister fails prior to re-registration, verify disabled status before re-enabling during enable cycling, reject arbitrary moving or non-release image tags by default, and require jq with valid manifest index parsing in preflight.
  • Restored word-level playback highlighting, hover and click-to-seek in the meeting viewer, including simultaneous speakers and inline interjections. Keyboard activation seeks to a focused word without also toggling playback; cleaned and untimed transcript text remains intact.
  • A search across meetings no longer lets one talkative meeting fill the whole page of results and hide every other meeting that matched.
  • A summary or insight the model stopped writing at its output limit is no longer recorded as a success. The run fails and says the answer was cut off at the model's output limit, so a half-written document is never published as a whole one.
  • The AI endpoint picker now works on a plain compose deployment too: the /ai/providers routes were mounted only under the ExApp base path, so the Prepare panel's provider list 404ed there.
  • Two insight runs can no longer run at once on one request: a retry whose row had been failed by the stale-run sweep and retried again used to start beside the newer attempt.
  • An insight stopped by an operator restart now says so and asks you to retry it, instead of claiming it took longer than an hour.
  • A finished insight whose outcome could not be written down is retried a few times before giving up, and if it still fails the log names the delivered document so it can be found.
  • A succeeded insight whose document you have since moved or deleted now says where it was, rather than "its document is empty".
  • Meeting-context refusals are never cached by the app proxy, and a meeting list that could not be read answers "unavailable" rather than "not one of yours".
  • The operator no longer logs a false "publish sink unavailable; using local" at startup. Recordings were always delivered to the configured destination; the message was wrong, and it pointed anyone reading the log at a storage problem that did not exist.
  • An insight run always asks the chosen provider for that provider's model; it can no longer inherit the summary step's model on a different endpoint.
  • When the AI endpoints or a model list cannot be read, the app now shows the operator's own error instead of a generic HTTP status. A provider base URL ending in /chat/completions or /models is refused with a hint to give the API root.
  • A non-admin's "No AI endpoint" card in the Prepare panel refreshes each time the panel opens, once an administrator has configured one.
  • The AI providers form warns when an OpenRouter key is pasted without its sk-or-v1- prefix, which used to save cleanly and then fail every run with a 401.
  • Finding more than one word inside a meeting now works. Typing a phrase used to return nothing at all.
  • Ctrl+F (⌘F) now opens the transcript find instead of Nextcloud's own search popup.
  • Narrowing the meeting list by room or tag now applies to search results too. Searching and then picking a tag used to leave meetings from outside the filter in the list, which looked as though the filter had stopped working.
  • Changing the room or tag while a search is active now re-runs the search instead of leaving the previous answer on screen.
  • Fixed selected tags being silently replaced by an existing case-insensitive label match. Meetings can retain multiple selected tag IDs, including legacy case variants, and the tag vocabulary now combines the same ID across recording namespaces. [D-773]
  • A meeting's header no longer writes down facts it does not have: a recording with no room said "No room", and one with no date showed its own filename under a calendar icon.
  • Applying an existing tag to another recording now copies its visible colour and icon into the new recording, including when tagging a selection. Existing local styles remain unchanged. Standalone viewers preserve saved tag colours and explicitly cleared icons. Annotation database upgrades preserve pending edits when moving from either the main or portable-style schema. The portable-file reader retains saved colour and icon fields, so embedded recordings show their archived appearance instead of fallback grey tags.
  • Guest participants in Nextcloud Talk meetings are now attributed under the display names they entered in the Talk UI rather than synthetic placeholder identifiers (such as participant-sXHFMabV). The recorder resolves decoupled Nextcloud room session IDs and actor IDs against active WebRTC signaling sessions, retains attendee display names delivered in presence updates, and upgrades early synthetic placeholder names when the participant's chosen name arrives.
  • Prevented FFmpeg 4.4 crashes when a participant audio track starts several minutes into a meeting.
  • Prevented repeated native decoder failures from filling the ExApp container with multi-gigabyte core dumps.
  • Kept readable truncated recordings buildable when FFprobe reports a non-fatal container warning alongside a valid packet timestamp.
  • Keyboard focus moves into a meeting, an insight or Prepare when it opens and stays there: the page underneath can no longer be reached with Tab or read by a screen reader. Their close buttons are larger.
  • Controls, borders, and hover and selected states stay visible in Nextcloud's high-contrast themes.
  • The meeting and insight drawers stay readable in Nextcloud's high-contrast themes.
  • Nothing in the list hides under the selection bar when the bar grows a second line, and an insights listing that failed reads as an error.
  • Reduce missing speech in Parakeet v3 transcriptions by matching the model’s reference audio preprocessing. Preserve detected utterances with surrounding recorded audio instead of splitting them every ten seconds and appending synthetic silence. Retain beam decoding and vocabulary hints.
  • Keep participant audio on the meeting clock after long mute gaps and codec pre-skip, including recordings with a nonzero container start time.
  • Fix duplicated overlap words when adjacent decodes disagree only on sentence-final punctuation.
  • Include the corrected native runtime in CPU/CUDA builds and the developer CLI.
  • Add recorded-audio comparisons and resumable full-meeting GPU validation tools.
  • Automatic participant-name hints no longer overwhelm uncertain audio. They use a capped decoder score of 0.5 while operator-entered vocabulary keeps its configured score, and each participant's own name is omitted from their track. This prevents short names such as “Silvio” from repeating hundreds of times while retaining useful spelling hints for other speakers. Published provenance records the participant-name policy and its effective score.
  • A portable .opus whose transcript body cannot be read is no longer called a broken file. That transcript is unavailable, named in a warning, and the meeting, the speakers and the other transcripts still read. cassini inspect still exits non-zero.
  • A repeated load-bearing tag now stops cassini inspect from printing any part of the manifest. It used to declare the metadata invalid and then print the title, the meeting id, the speakers and every descriptor anyway.
  • Transcript bodies are verified against the manifest's payloadRef rather than the CASSINI_TX_* tags that copy it. A corrupted body whose tag happened to agree with it was accepted, and a good body whose copied tag had drifted was rejected; a disagreement is now a warning.
  • The transcript a reader opens is the one cassini inspect marks default=yes, including in a file where no entry carries the flag and array order decides.
  • words= is the number of items read out of the chunk set, not the count the body declares.
  • Model ids containing _ no longer produce a transcript id the packer rejects, which let a bundle build and then fail at packing.
  • The exported transcript JSON keeps one segment per speaker turn. It used to collapse a multi-speaker transcript into a single segment with no speaker at all.
  • cassini inspect exits non-zero on unusable manifest metadata instead of printing a summary and reporting success.
  • Portable .opus files whose chunk sets carry base64url = padding now read. Producers write the encoding unpadded, but most base64url encoders pad by default, so a file written by another implementation was rejected outright as invalid metadata.
  • A transcript's chunk count and the default transcript now come from the manifest rather than from the CASSINI_TX_*_CHUNK_COUNT and CASSINI_TRANSCRIPT_DEFAULT tags that mirror it. A file whose two layers had drifted lost a transcript that was entirely present, returned a silently truncated one, or served a different transcript than the manifest names; the disagreement is now reported and the file still reads.
  • cassini inspect reports the word count it actually decoded out of a portable .opus, names any transcript whose body it could not read, and exits non-zero. It used to print the manifest's declared words=900 and cassini=ok for a file whose transcript was unreachable.
  • A meeting's words= is the default transcript's count rather than the sum across every transcript in the file. Alternative transcripts are further passes over the same speech, so a three-word meeting published with two passes was reported as six words.
  • Keep recording-setup edits responsive during background status refreshes and prevent older responses from replacing newly saved configuration.
  • Corrected installation and development guides for CPU transcription, the unified Cassini app, recording access, and optional summary/insight data sharing.
  • Fixed the app’s development proxy so the Operator section, settings and storage controls can reach a root-mounted operator API.
  • Fixed readiness requests and proxy path boundaries, rejected fallback/login responses in Operator detection, and stopped storage setup when its refreshed plan no longer includes the selected mode.
  • Replaced the homepage’s unsupported recording command with the documented CLI syntax.
  • Fixed concurrent atomic file writes by giving each writer its own temporary file.
  • Aligned architecture and API docs with the unified app and two-service Compose bundle, and corrected standalone publish examples.
  • The operator now hands its Talk recording secret to the recorder it starts. Since D-447 the operator generates that secret when the administrator sets none, and the recorder never received it, so on such installs Talk showed a working recording backend and every recording failed with "talk auth mode hpb-internal requires CASSINI_TALK_RECORDING_SECRET to be set".
  • Fix the embedded viewer showing no transcript for newly published portable version 1 meetings whose transcript bodies live in separate OpusTags chunks.
  • Build: the ExApp image installs the web workspace from the root package-lock.json with npm ci instead of resolving it afresh from the registry on every build. On 2026-09-03 the fresh resolution tripped an npm 10.9 bug (Cannot read properties of null (reading 'edgesOut')) on trees that had built minutes earlier.

Security

  • Search results never carry transcript text, and the search index cannot return the words it indexed. A search query is kept out of the operator's request log, where it would otherwise sit indefinitely.
  • A tag filter on the meeting list is kept out of the operator's request log, as search queries are.
  • A search term or tag no longer appears in the CLI's error output when the connection to Nextcloud fails before it answers.
  • Tags travel with a recording. Anyone you share a tagged recording with can see who tagged what, by Nextcloud user id.

Fixed

  • Fixed transcript reruns failing validation when overlapping decoder-window words end out of start-time order.

Fixed

  • Long speech turns admitted by the interjection-sensitive VAD are decoded in bounded overlapping windows, reducing Parakeet's silent omissions from 20–25-second utterances without reverting the short-interjection VAD thresholds. Dense merged-fallback windows also retain deterministic seam ownership when adjacent decodes disagree completely.
  • Production ExApp deploys now use collision-safe temporary manifest paths and clean them after settled runs, so a stale file from another operator cannot block an upgrade.
  • ExApp startup logs now report the effective publication sink instead of contradicting a resolved Nextcloud Files destination with a later local line.

Changed

  • GPU transcription upgrade: existing portable/plain-image ExApp installations continue to capture and preserve Talk audio, but operator-managed transcription now enters build/blocked instead of using CPU ASR. Install the matching -cuda image on a GPU deploy daemon, then choose Rerun for each preserved blocked job in Cassini Admin.

Fixed

  • Short interjections now retain their real timeline position and speaker attribution. Transcription drains final VAD audio, recovers more subsecond turns, and rejects near-silent decoder hallucinations without discarding speech whose decoded timestamp leads its waveform.
  • Transcript construction no longer applies duplicate timeline offsets or retains both sides of a mixed-track fallback, and genuine final words emitted in decoder or VAD padding are preserved at the real audio boundary.
  • Meeting duration now comes from playable audio without discarding source-container provenance or preallocating PCM from corrupt duration metadata.
  • GPU readiness failures are reported explicitly, and generated artifacts record the effective ASR device.

Added

  • cassini retag rewrites an already-packed .opus file's room and job fields without re-encoding its audio, verifying the result against the manifest's own PCM digest before writing it. It is what lets a recovered room survive a republish, and what the maintenance scripts below use.
  • A meeting's portable .opus now records which operator job and attempt produced it (meeting.jobId, meeting.attemptNumber, mirrored as CASSINI_JOB_ID and CASSINI_ATTEMPT_NUMBER). Both are optional; a file packed outside the operator has neither.
  • cassini inspect prints an origin line with a portable file's room, job and attempt — previously it decoded the room and printed nothing, so checking the producer chain by hand meant a raw ffprobe.
  • cassini meetings context includes the room the meeting came from, in both the markdown and the --json form.
  • scripts/backfill-catalog-rooms.sh gains --jobs-db PATH, --no-jobs-db and --no-retag; scripts/reattribute-catalog-room.sh gains --force, --jobs-db PATH, --no-jobs-db and --no-retag.

Changed

  • A recording's room is now recovered from the operator's own job history rather than guessed from its file name. The catalog entry's id is the job id, and jobs.talk_binding still holds the Talk room token for every job the operator ran — so scripts/backfill-catalog-rooms.sh derives the real room id for anything this installation produced, and falls back to a name-derived id only where no job row survives. Re-running it merges rooms an earlier pass split in two.
  • Both maintenance scripts now re-tag the published .opus, not just catalog.json. The exporter re-derives a catalog entry's room from the file on every republish, so a catalog-only change was silently undone by the next re-seal. Pass --no-retag to keep the old behaviour and accept that the change is temporary.
  • scripts/reattribute-catalog-room.sh refuses to move a meeting the operator has a recorded room binding for (new exit code 5, --force to override), including in a dry run. For those meetings the real room id is recoverable and the backfill is the correct tool; asserting a different one would leave a recording whose lineage and published room permanently disagree.
  • A room that got split in two is repaired by re-running the backfill, not merged by hand. 0.2.0-beta.2 said a split — from a pepper rotation, or from a recording made before that release — stayed until you merged it yourself. Re-running scripts/backfill-catalog-rooms.sh now re-derives every meeting whose job row survives and closes the split; only recordings imported from elsewhere still need scripts/reattribute-catalog-room.sh.
  • A meeting's room name now lives in catalog.json, not in the recording. 0.2.0-beta.2 announced the display name as something the portable .opus carries; it no longer does — the file carries the room id, and the operator writes the name into the catalog on every publish, preserving it across a republish. A display name is editable and a sealed recording is not, so honouring a rename used to mean rewriting every file that room ever produced; it is now one edit in a file that is rewritten anyway. The stamped name is the one that job recorded, not the room's current name — a republish restamps the same value — so what this buys is the ability to correct a name, not a name that follows Talk. The name at record time is still in the file, as the meeting title.

Fixed

  • A publish interrupted partway no longer leaves an unprotected recording behind: the next publish denies, removes and re-creates it.
  • A re-publish no longer resets a recording's access rules, so permissions changed by hand in Nextcloud Files now survive it.
  • An interrupted upload is no longer published as a silently truncated recording; the delivery fails and can be re-run.
  • A publish now fails instead of rewriting the archive when the meeting index cannot be read. A server error answered with a JSON body used to read as "this archive is empty", which replaced every meeting in the index with the one being published.
  • A room recovered by the backfill no longer reverts the next time its meeting is re-sealed or republished.

Security

  • Recordings are now given their access rules before they hold any audio. A recording used to be uploaded first and made private in a second request, so it was readable by every account on the instance in between — and permanently readable if that second request failed.
  • The meeting index gets the same treatment, in both places that write it: the one-shot Nextcloud Files migration used to upload the index of every migrated meeting — titles, room names, dates — before locking it to the owner.

Added

  • Data-processing & privacy note. A new docs/privacy.md documents what Cassini stores, where it lives, the single optional step (LLM cleanup and summaries) that sends data off your infrastructure, and what deletion and uninstall remove.
  • The App Store listing now shows what Cassini is: screenshots of the meeting viewer and the admin control panel, an ai category alongside integration, and user/admin documentation links. A screenshot-less entry reads as abandoned; the listing now presents the product.
  • Meetings now know which conversation they came from, and you can list and filter by it. Until now a recording's room name was flattened into its title and the room's identity was discarded, so there was no way to ask for "the meetings from this room". Every recording made from here on carries a room id and display name in its portable .opus and in the published catalog, and the CLI gains two ways to use that: cassini meetings rooms lists the conversations your account has readable recordings from, and cassini meetings list accepts --room, --from and --to (all optional, and they combine — --from 2026-08-01 --to 2026-08-31 means all of August). The agent skill for reading meetings now narrows by room and date before listing, instead of fetching the whole archive and guessing.
  • A new setting, CASSINI_ROOM_ID_PEPPER. The room id published on a meeting is a one-way derivation of the Talk conversation's identity, never the conversation's own token — for a public conversation that token is also the link that joins it, and every signed-in account can read those recordings. Setting this to a stable secret is what makes the derivation resistant to being reversed: a Talk token is short enough that an unpeppered one can be recovered by working through the possibilities offline. Choose it once — changing it changes every room id, and meetings already published keep the ids they were written with, so a room splits in two until you merge it again.

Changed

  • The Nextcloud App Store description now discloses that recording and transcription stay on your infrastructure and that only the optional external-LLM step sends data to a third party.
  • Recordings published before this update can be given their room back, from the files themselves. Run ./scripts/backfill-catalog-rooms.sh once, by hand, on the host where the app container runs; it reports what it would change and writes nothing until you add --apply. It recovers each meeting's room name from its published recording and derives a room id from that name, because those files never carried the conversation's token and it cannot be recovered from them.
  • Because of that, one conversation can appear as two rooms — one identified from its token, for recordings made since the update, and one identified from its name, for everything older. Cassini cannot prove they are the same conversation: two conversations can share a display name, and a room can be renamed between recordings. When you know they are the same, ./scripts/reattribute-catalog-room.sh --from <id> --to <id> merges them, and like the backfill it shows you every meeting it would move before it writes anything.

Fixed

  • Published recordings can be deleted again. Cassini created its Cassini Team folder with a Group Folders flag that, on recent versions, made every file in it permanently undeletable and un-moveable — by everyone, including the Cassini account and Nextcloud administrators. New installations no longer set it, and nothing else changes: who can read a recording is decided by the same per-recording permissions as before. An existing folder cannot be repaired automatically, because Group Folders offers no way to clear the flag after creation; the app's status page now reports the condition and what it costs instead of leaving it invisible.
  • Setting up recordings no longer fails on an installation that is already correct. On a Nextcloud that requires password confirmation for administration, an administrator has to create the recordings folder by hand — after which Cassini re-applied the same group and permission settings anyway, was refused, and reported the setup as broken even though nothing was wrong. It now checks what is already in place and only writes what is missing.
  • The app's status page no longer reports a completed setup as healthy when a step failed along the way.
  • Setting up the recordings folder now reports why it failed. The Group Folders API answers a refused request with HTTP 200 and the failure recorded only inside the response body, so Cassini read a refusal as a success and then failed while decoding it — reporting a JSON error that named neither the problem nor the cause. On Nextcloud installations that require password confirmation for administration, every one of these requests is refused, and the app's status page said only that setup was incomplete. Cassini now reports the actual reason and, when it cannot proceed, names the occ commands that create the Team folder by hand — which it then adopts on the next enable.
  • Setting up recordings no longer fails on Nextcloud installations that require password confirmation for user administration. Cassini creates a cassini service account and its group when the app is enabled; on Nextcloud 34 (and any instance enforcing password confirmation) those calls are refused, because an app has no browser session in which to confirm a password. An administrator can create the account and group by hand — but Cassini did not check whether they already existed before trying to create them, so a correctly prepared installation looked identical to an empty one and setup failed permanently. Cassini now looks first, adopts what is already there, and when it genuinely cannot proceed it names the occ commands that fix it.

Added

  • Cassini now makes the Talk signaling internal secret — the one input required for invisible HPB-internal recording that cannot be auto-provisioned — easy to know and set. When CASSINI_TALK_SIGNALING_INTERNAL_SECRET is missing, the operator logs an actionable WARNING at startup and GET /operator/status returns a signaling_internal_secret_hint, instead of only failing when a recording is attempted. The External Apps deploy-option description and docs/exapp-install.md now explain where to find it (e.g. docker exec nextcloud-aio-talk printenv INTERNAL_SECRET on Nextcloud All-in-One, or the signaling server's [clients] internalsecret on a standalone HPB) and how to set it.
  • Zero-config Talk recording secret: CASSINI_TALK_RECORDING_SECRET is now optional. When it is not supplied, the operator generates a secret on first start and persists it on the AppAPI volume (surviving restart and app update), so installing Cassini from the App Store no longer requires inventing or copying a shared secret. An explicitly supplied secret still wins and is treated as externally managed.
  • New ADMIN-only provisioning endpoint operator/talk/provisioning returns the ready-to-apply spreed recording_servers value (backend URL + secret), so an admin can register Cassini as Talk's recording backend in one step without ever handling the secret by hand.
  • The operator now delivers published recordings into Nextcloud Files. Each published meeting's portable .opus is uploaded before its entry is added to the catalog.json index under the canonical Cassini/Recordings/ folder. The in-Nextcloud viewer then serves its catalog and per-meeting audio authoritatively from Files (with its CSP and routes unchanged), while standalone development keeps its local source. Mutable catalog responses bypass browser caches, and the embedded viewer refreshes its meeting list on focus and every 15 seconds. Delivery failures are visible rather than silently masked by local fallback. Recordings are delivered as .opus only; Talk's recording /store endpoint (which rejects .opus) is not used. This first pass is public and writes as the Nextcloud admin; per-user/per-group access and a dedicated service account follow.
  • Recordings now belong to a dedicated cassini account, created automatically when the app is enabled. Previously they were owned by the Nextcloud administrator, which tied every recording to a human account that may be renamed, handed over or removed, and gave the identity holding all the recordings full administrative rights over the instance. Setup work — creating the recordings folder, its groups and the service account itself — is still performed as an administrator; storing and reading recordings is not. The account owns and serves the recordings; there is no other mode.
  • Per-participant access control for recordings. The operator freezes each recording's audience to the Talk room's participants at publish time — it grants read-only Nextcloud advanced-ACL (nc:acl-list) to each local participant (users, groups, circles) on the meeting's .opus; guests, email and federated participants are skipped. The in-Nextcloud viewer is then served per caller: the meeting list is built directly from catalog.json, filtered to the .opus files visible in the caller's own Nextcloud scan, and each recording is fetched as the caller so Nextcloud enforces access — a recording a user is not granted never appears and cannot be played. The read path fails closed (an empty list, never the full archive) on any error. Access is editable afterwards through Nextcloud's own sharing/ACL tools. Recordings are owned by a dedicated cassini service account, which also manages their ACLs; privileged setup is performed separately as a Nextcloud administrator. Two native prerequisites must be enabled first — the Team folders (groupfolders) and Everyone Group (group_everyone) apps — after which Cassini provisions the recordings folder and ACL topology itself. See docs/exapp-nextcloud-recordings-permissions.md.
  • Automatic setup of the recordings directory and its permissions. Enabling the Cassini ExApp now provisions everything the model needs by itself, over Nextcloud's HTTP APIs — no manual occ Team-folder setup. On the enabled edge the operator creates the Cassini Team folder with its default-deny advanced-ACL floor, gives Nextcloud's virtual everyone group read and the narrow cassini owner group full permissions, delegates cassini as ACL manager, migrates any per-recording permissions left by earlier versions, applies the root container ACL (so the owner can write and every account can traverse), and creates the canonical Cassini/Recordings/meetings collections. Because the audience group is virtual, every account has the mount from the moment it is created — there is no membership sweep and no convergence delay. Two native prerequisites remain, neither of which an external app can install for itself: the Group folders ("Team folders") app and the Everyone Group (group_everyone) app. The local harness enables both automatically.
  • The recordings setup now reports its own health. Setting up the recordings folder used to fail into a log line, leaving an app that looked healthy while showing nobody their recordings. GET /operator/status now reports whether the Team folder, its permissions and the universal everyone mount are actually in place, names the step that failed and the likely cause, and returns 503 when the app cannot serve recordings — so a missing Group folders or Everyone Group app is visible instead of silent.
  • The production deploy is now a committed, parameterised script (ops/deploy/deploy-exapp.sh) driven by an inventory file, instead of a one-off on a maintainer's laptop. It is dry-run by default, reads the live rollback target before touching anything, and refuses to deploy a moving tag (latest, latest-cuda, …) or an image whose child manifests are missing from the registry. CPU and GPU deployments differ by one inventory value, the daemon's compute device — there is no separate GPU path.
  • docs/exapp-update-constraints.md states what the Nextcloud admin UI's Install and Update buttons can and cannot deliver — most importantly that deploy environment is applied at container creation only, so a release adding a required environment variable is a breaking change the Update button cannot deliver.
  • A retention policy for per-run working files, --artifact-retention / CASSINI_ARTIFACT_RETENTION. Disk use grew with every recording and every rerun and nothing ever reclaimed it. The default (sealed) removes a completed run's working copies — all of which are duplicated in the operator's canonical library or were temporary staging — and keeps its sealed meeting file and every log. superseded only removes runs that a rerun has replaced; all keeps everything, as before — except a delivered run's staging copy, which is always removed once the recording reaches its destination so that no full copy of it is left outside the access model. Nothing removes the last copy of anything, and published recordings are never touched.
  • Installing Cassini now tells you whether recordings will actually work. Setting up the recordings folder used to fail into a log line, so an app whose setup had not completed still enabled, answered every request, and showed nobody their recordings. GET /operator/status now reports that setup under recordings_access as provisioned, unavailable (a named thing is missing — it says which app to install, or which setting to fill in), degraded (a setup call failed), or unknown (the app was restarted without being re-enabled), and returns 503 for anything that is not working. The two Nextcloud apps Cassini cannot install for itself — Team folders and Everyone Group — are now checked by name before setup starts, and reported individually.
  • GET /operator/setup — a USER-level route reporting whether recordings can be served (ok and state) and nothing else. It is what lets a non-admin be told that an install was never finished; the diagnosis (which step, which administrator, which paths) stays on the admin-only /operator/status. It answers 200 in every state, including the broken ones, so a browser can tell "Cassini is not set up" apart from "the app is down" — /operator/status remains the endpoint that answers 503 for monitoring. Existing installs pick the route up when the app is next registered or updated.
  • A one-shot migration for recordings published by an older version. Recordings made before Cassini stored them in Nextcloud Files live only on the app's own storage, and nothing publishes them again. ./scripts/backfill-nc-files.sh moves them across, run once by hand after updating; --dry-run reports what it would move without changing anything. Migrated recordings are readable only by the Cassini service account, because the audience a recording had when it was published cannot be recovered afterwards — grant access from the Files app, or use --public to make them readable by every signed-in account as they were before Cassini had access control. The migration changes nothing when Nextcloud Files already holds recordings, or when there is no older archive to move, so it is safe to run when you are unsure whether it is needed — and skipping it entirely is a supported choice: the old recordings simply stay where they are and everything published from now on works normally.
  • Read your meetings from outside Nextcloud: cassini meetings. An agent or script on a laptop or container can now list the recordings a Nextcloud account is allowed to read (cassini meetings list), download one as its single portable .opus (cassini meetings fetch <id>), or print one as readable context — transcript plus generated summary, as markdown or JSON (cassini meetings context <id>). It authenticates as a Nextcloud user with a Nextcloud app password, so no interactive browser login is needed and no new server route was added: the commands drive the app's existing per-recording read surface, which fetches from Nextcloud Files as the calling user. Nextcloud therefore enforces exactly the same permissions the in-Nextcloud viewer obeys — a recording you may not read is reported as not found, never as forbidden, so it never reveals that it exists. The surface is read-only; starting, stopping and deleting recordings remain administrator actions. context labels its transcript derived-from-words: a published meeting carries word-level timings, so paragraph breaks are inferred from pauses and speaker changes rather than editorially cleaned up. Requires ffprobe for context. Downloaded recordings are written readable by their owner only, since they hold a private meeting's audio and transcript. The CLI refuses to send your credentials anywhere other than the Nextcloud you named — it does not follow redirects, and it rejects a catalog entry pointing at another host. See docs/agent-meeting-access.md.
  • Working-file retention is now configurable on an installed app. CASSINI_ARTIFACT_RETENTION is declared as a deploy option, so administrators can choose to keep every per-run working file (all) when diagnosing a problem. The default is unchanged.
  • cassini meetings warns when --insecure is used with an app password, since disabling certificate verification exposes that password to anything able to intercept the connection.
  • Added explicit LAN media, signaling, and Talk callback configuration for running the full installed-ExApp harness on Docker Desktop for Mac, with fail-fast validation and a setup runbook.

Changed

  • Image publishing now verifies that every keep-forever tag — :latest, :latest-cuda, :latest-rocm, :cuda, and each :X.Y.Z[-cuda|-rocm] release pin — is actually pullable, failing the run instead of leaving a tag that resolves but points at a deleted manifest.
  • The operator/status talk block now reports secret_source (env | generated | unset) and the derived recording_backend_url (never the secret value), so admins can see at a glance how the recording secret was provisioned and what to register in Talk.
  • The operator now selects where published meetings are delivered with --sink / CASSINI_PUBLISH_SINK. The only sink today is local — the operator's own site root, which is what publishing has always done — and it is the default, so existing deployments need no change. An unrecognised name is rejected at startup with the valid names listed, rather than quietly publishing somewhere unintended.
  • The faithful installed-ExApp CI gate refuses its negative-test env hooks (D453_EXPECT_CONFIG_FAILURE, D453_FAIL_AT) unless D453_ALLOW_TEST_HOOKS=1 is also set, so a stray hook variable in a CI job fails loudly instead of quietly weakening the gate.
  • The demo sandbox now runs on Nextcloud All-in-One (AIO) instead of a hand-rolled docker-compose stack that reused the CI/dev harness. AIO provides Nextcloud, Postgres, the Talk HPB, and the reverse proxy — mirroring a real deployment. The sandbox no longer references anything under harness/. Cassini is installed/updated on top of AIO with a single idempotent sandbox/wire-cassini.sh (App Store release by default); one-time AIO + reverse-proxy + firewall setup is documented in sandbox/README.md.
  • Cassini's in-Nextcloud control-panel and viewer now render entirely through AppAPI's native embedded-UI mechanism, under Nextcloud's normal Content-Security-Policy. A stock Nextcloud + AppAPI install shows the admin control-panel and viewer with no modifications to AppAPI and no app_api integrity warning — the development-only CSP patch that edited AppAPI's signed proxy controller is gone.
  • Publishing a recording is now lightweight: cassini publish updates the meeting catalog and recordings only and no longer rebuilds or embeds the viewer on every publish. The viewer is served from the app image, so publishing is faster and no longer depends on the viewer build. Pass --rebuild-viewer (or set CASSINI_PUBLISH_REBUILD_VIEWER=1) to produce a self-contained static site with the viewer embedded, as before.
  • Upgrading needs no data migration and no manual step: recordings are stored in a group folder, which is shared storage rather than a personal one, so existing recordings stay where they are and remain accessible to the same people. Cassini installs the dedicated owner group's write mapping and then removes the legacy administrator-group mount and ACL-manager delegation after the service account has successfully managed the migrated root.
  • A recording that does not reach Nextcloud Files no longer counts as published. Delivery used to run after the publish was already marked successful, and its failures were only logged — so a job could report success with the recording nowhere in Nextcloud and the viewer showing nothing. Delivery is now part of publishing: if the upload, the catalog update or the per-meeting access grant fails, the publish fails, says which step failed, and a rerun re-attempts it.
  • An installed app now stores recordings in Nextcloud Files by default. CASSINI_PUBLISH_SINK selects the destination explicitly (nextcloud-files or local), and an installed app whose value is missing defaults to Nextcloud Files rather than quietly keeping recordings on the app's own volume. Selecting nextcloud-files without a usable Nextcloud connection now stops the app at startup with an actionable message instead of failing later, one recording at a time.
  • An installed app no longer keeps its own copy of published recordings. Recordings are stored in Nextcloud Files, but the app also retained a full copy of every meeting it had ever published on its own volume, outside the permissions that govern the originals. That copy is no longer produced: the working directory a publish builds is removed once Nextcloud has accepted the recording, and the app serves meetings only from Nextcloud. A recording the app cannot fetch is reported as missing rather than quietly answered from local disk.
  • Talk still receives recording status (started, stopped, failed) exactly as before, so the Record button, the room's recording indicator and failure reporting are unchanged. CASSINI_TALK_BACKEND_URL now covers status callbacks and OCS calls only.
  • Recording access now uses Nextcloud's virtual everyone group, giving new accounts the recordings Team-folder mount immediately without periodic membership reconciliation. Installers must enable both the Team folders (groupfolders) and Everyone Group (group_everyone) apps — they are the only two manual prerequisites; existing recording-viewers ACLs are migrated without changing whether a recording is private or public.
  • Whether a conversation is public is recorded at the time of the recording and stays with it. Making a conversation public later does not retrospectively widen recordings made while it was private, and making it private does not withdraw recordings people were already told they could see. If Cassini cannot determine the conversation's type, the recording stays restricted to its participants.
  • The recording-permissions runbook now states the access rule accurately: a recording is visible to everyone who had access to the conversation, including people invited who never joined, not only those present during the call.
  • Recordings are now access-controlled for everyone, with no way to turn it off. Per-participant access used to be an opt-in deploy option that defaulted to off, so a stock install shipped an archive where any logged-in Nextcloud user could browse and play every recording. That is gone: an installed Cassini app serves each person only the recordings Nextcloud says they may read, and there is no public-archive mode to fall back to. Nothing to configure — the app sets up the recordings folder and its permissions itself when you enable it. The two things an administrator still does are enable Nextcloud's Group folders ("Team folders") app and the Everyone Group app, neither of which an external app can install on its own.
  • The admin deploy option CASSINI_NC_ACCESS_CONTROL and the local harness's --nc-access-control switch were removed along with it. If you set either, drop it; nothing replaces it.
  • The AppAPI register logic is now one implementation (ops/deploy/lib/exapp-register.sh) shared by the sandbox, the harness install phase and the production deploy, so the rules that protect the recording archive hold everywhere: unregister before re-register, never pass --rm-data, and force PUT /enabled so the navigation entries come back.
  • The ExApp install guide now documents the /exapps/* reverse-proxy route to HaRP. It is required for every HaRP daemon and is what actually blocks an install when missing — the symptom is an app that reports [enabled] with no navigation icon, which points nowhere near the reverse proxy. It must use Caddy's handle, not handle_path, so the prefix survives.
  • The operator's job pipeline has a fourth stage, seal, between build and publish, with its own timestamps, log and progress step in the control panel. Publishing is now reached only through a completed seal.
  • A recording is no longer published when the recordings folder is not set up. Without it, recordings were written into a private folder belonging to Cassini's own service account: the upload succeeded, the job reported success, and nobody could ever open the result. Publishing now fails with the reason and points at GET /operator/status; a rerun works once the cause is fixed. This also applies after a container restart until the app is re-enabled, because setup runs when the app is enabled and nothing has verified where recordings would land until it does. To keep recordings on the app's own volume deliberately, set CASSINI_PUBLISH_SINK=local.
  • The install guide now lists both required Nextcloud apps under Prerequisites, documents every deploy option, and explains how to read the setup report.
  • An unfinished install now explains itself in Cassini, instead of failing with HTTP 502. When the recordings folder is not set up, opening Cassini no longer shows a failed meeting list — it shows what is wrong, and it says something different depending on who is looking. An administrator gets the step that stopped: the missing Nextcloud app named by name and id with the occ app:install line for it, or CASSINI_NC_ADMIN_USER when no administrator could be resolved, or the container log to read when a setup call simply failed and there is nothing to install — plus the disable/enable pair that re-runs setup, and the same sentence the container log carries. Everyone else is told that Cassini is not set up, that it is not their account, and is given a link to this Cassini page to send to an administrator — who, opening it, sees the instructions. Which message you get is decided by the same boundary that decides whether you see the Operator tab at all.
  • Where the message goes depends on whether recordings can still be read, which is not the same question as whether setup finished. When nothing is mounted, it replaces the meeting list, because the list would otherwise be an error or an empty lie. After a plain container restart — where setup has not re-run, so publishing is refused, but every recording already published still opens — it is a strip above the list and the list stays.
  • Enabling or updating the app no longer re-uploads your whole recording archive. It used to copy every recording it had ever published into Nextcloud Files again on each enable, install and update, under a fixed time budget for the entire batch. Past a certain archive size that ran out of time before it could update the recording list — and because the list is written last, it stayed frozen at its previous contents permanently, with new recordings delivered but invisible and nothing said about it. Recordings have gone straight into Nextcloud Files as part of publishing for some time now, so nothing depended on this any more.
  • All three Go modules now declare the same minimum Go version (1.24), matching the toolchain the release images have always built them with. Previously two modules declared 1.22 and one declared 1.24, so the version a module was checked and formatted under depended on which module it was.

Removed

  • sandbox/compose.sandbox.yml, sandbox/deploy.sh, and sandbox/destroy.sh — the harness-coupled compose sandbox they implemented is superseded by the AIO-based sandbox.
  • Cassini no longer uploads recordings to Nextcloud Talk. Every Talk-button recording used to file a raw .mkv into the room owner's /Talk/Recording/<token>/ through Talk's recording-store endpoint, in addition to publishing the meeting as a portable .opus. That second copy sat outside Cassini's canonical recordings root and outside its per-file access controls, so a meeting could be readable there by people it was never shared with. The upload is gone — including the rerun path that re-attempted it — and a meeting now reaches Nextcloud exactly once, as the published .opus.

Fixed

  • CUDA image tags (:X.Y.Z-cuda, :latest-cuda, :cuda) can be pulled again. Image pruning had deleted the manifests those tags pointed at, so docker pull failed and a CUDA deploy daemon silently fell back to the CPU image — running transcription without GPU acceleration and reporting no error. Tags published before this fix stay unpullable until republished; :latest-cuda and :cuda repair themselves on the next push to main.
  • cassini dev stack up --resume now recreates containers and mounts the existing Compose and ExApp data volumes after a normal stack down, instead of requiring the removed containers to still exist.
  • Publishing a recording no longer re-exports the whole library. Every publish used to rebuild and rewrite every meeting in the archive, so the time it took grew with the archive — on a production instance with 67 meetings that was roughly seven and a half minutes per recording, during which the job sits at publish / running and looks stuck. A publish now exports only the meeting being published and merges it into the live site, leaving every other recording's files untouched. Re-publishing a meeting (for example via a rerun) updates its entry in place rather than adding a second one.
  • The three-songs harness sync check now fails loudly when the recorder report is missing or contains fewer than 2 composed sessions, instead of silently skipping; the recorder run now writes the report the check needs.
  • The recorder now rejects --request-offer-interval 0 at startup instead of silently disabling every capture-recovery path, which could leave an empty recording for the whole call after a single dropped requestoffer.
  • Setup no longer assumes the administrator is called admin. Cassini probes for one — the configured account, then admin, then the instance's own account list — so instances that name theirs differently can complete setup. If none of them is an administrator it says so and stops, rather than continuing as an account that may not exist; CASSINI_NC_ADMIN_USER names one directly.
  • Publishing a recording no longer rewrites the whole Nextcloud Files archive. Each publish uploads its own recording and adds it to the shared catalog, so a delivery that fails can no longer remove other meetings from the list.
  • A recording you are not allowed to see now reads as "not found" rather than "forbidden". Reporting a denial told you the recording existed, which is exactly what the access model is meant to prevent. Genuine outages are still reported as errors, and denials are still distinguishable in the operator's logs.
  • The internal site manifest (cassini.json), which lists meeting counts and job ids, is no longer readable by any signed-in user of an installed app.
  • The viewer no longer treats an unreachable meeting list as an empty one. A Nextcloud outage now surfaces as an error instead of an empty archive.
  • Recordings of public conversations are now readable by everyone, as you would expect. A conversation anyone with the link can join was treated exactly like a private one, so its recording was restricted to whoever happened to be on the attendee list. That was worst for the most public meetings: participants who joined by link rather than by invitation cannot be granted access individually, so a public meeting full of link guests produced a recording only its owner could open. Recordings of public conversations are now visible to any account on the Nextcloud.
  • A newly created user can now see public recordings straight away. The recordings Team folder now uses Nextcloud's virtual everyone group, so every account has its mount from creation instead of waiting for a periodic membership sweep. A new account sees everything it is entitled to on its first Cassini visit.
  • A public recording no longer loses its broad access when the app restarts. A periodic integrity sweep re-applies the restrictive permissions to any recording that is missing them, and it mistook a public recording's deliberately broad grant for a missing restriction — quietly turning every public recording back into a participant-only one. The sweep now leaves a recording's own permissions alone, whichever way they were set, and only repairs recordings that carry none at all.
  • A momentary Nextcloud Talk hiccup no longer costs a recording. Working out who a meeting belongs to happens once, at publish time, and now gates publishing — so a single failed request used to fail the recording outright, recoverable only by re-running the job by hand. The lookup now retries, and then falls back to asking as the recordings owner, which is the case retrying alone cannot fix: the person who started the recording may have left the conversation, been removed or been disabled between the meeting and its publication. A meeting whose participants are all guests or federated users still publishes successfully and stays visible to its owner only.
  • The recordings list no longer gets stuck on "Meeting not found". Opening the viewer before its first recording exists — or during any brief Nextcloud hiccup — could leave the right-hand pane showing an error that stayed there even after the list loaded, until you clicked a meeting or reloaded. A link to a specific meeting opened in that window was also dropped instead of opening once the list arrived.
  • bin/cassini no longer leaks a ~17 MB temp directory on every invocation (about 3.7 GB/day on a dev VM running the harness). The wrapper now builds the CLI to a stable path under ${XDG_CACHE_HOME:-$HOME/.cache}/cassini/bin and reuses it, so there is nothing to clean up; concurrent invocations stay safe via an atomic rename, on macOS too.
  • A recording is no longer published before its downloadable meeting file exists. The portable .opus — the single-file meeting you download, share and archive — used to be produced in the background after the recording had already been handed off for publishing. If that step failed, was interrupted by a restart, or was overtaken by a second run of the same recording, the recording could still be reported as finished while its meeting file was missing, stale, or belonged to a different run. Packing that file is now a stage the recording has to pass: it is written once, checked, and fingerprinted before publishing starts, and the file that reaches the viewer is verified to be that exact file. A failure now fails the recording, with the reason recorded and a rerun available, instead of disappearing into the log.
  • Re-running a recording can no longer overwrite the previous run's meeting file. Each run now writes its own, so the two cannot race.
  • A failed publish can no longer leave the recordings page without its viewer. Refreshing the site's viewer files used to delete them before copying the new ones in; an interrupted copy left the page listing recordings it could no longer play. The new files are now staged beside the old ones and swapped in, so a failure leaves the previous, working page exactly as it was.
  • An interrupted replacement can no longer destroy a meeting file. cassini pack removed the previous .opus before moving the new one into place; it now replaces it in a single atomic step.
  • Setup no longer assumes the administrator is called admin. Cassini used to look up the administrator with a request made as the administrator it had not found yet, so on an instance whose administrator is named anything else the lookup failed, it carried on as admin regardless, and every subsequent step was rejected with nothing explaining why. It now asks Nextcloud who the accounts are and finds an administrator among them; if it cannot, it stops and says so, naming CASSINI_NC_ADMIN_USER as the way to point it at one.
  • A recording whose permissions could not be applied is no longer reported as published. Nextcloud accepts a permission change on a file outside a Team folder with a success code that hides a per-property rejection, so the recording could be stored with no restrictions at all while everything looked fine.
  • An administrator whose install was broken no longer loses the Operator surface. Cassini decides whether to show the operator by asking the admin-only status endpoint, and that endpoint answers 503 when recordings cannot be served — which was being read as "not an administrator". The one person who could fix the install saw the same browse-only app, and the same unexplained error, as everyone else.
  • Catalog meeting dates now come from the pack's recorded createdAtUtc (rendered in UTC), so meetings whose file name carries no timestamp — e.g. backfilled dailies — show a real date and sort correctly instead of echoing the raw id. Already-published archives pick this up on their next export.
  • Portable-meeting documentation was brought back in line with the format emitted by that release.
  • A recording delivered to Nextcloud Files is now verified to be the file the recording actually produced. Cassini fingerprints each meeting when it is packed and re-checks that fingerprint before publishing; the check was applied when recordings were stored on the app's own volume, but not on the Nextcloud Files path an installed app uses. A recording whose file does not match what was packed is now refused before anything is uploaded, so a half-delivered recording can no longer appear in the archive.
  • The standalone operator container no longer risks clearing more than its own site directory when started with an unusual CASSINI_OPERATOR_SITE_ROOT.
  • The recordings migration script (backfill-nc-files.sh) now runs on macOS's system bash, where invoking it with no options previously aborted.
  • The local test harness now stops with a clear message when Nextcloud Talk fails to install, instead of continuing and failing much later with an unexplained error.
  • Fixed harness startup under the macOS system Bash when optional Compose or Nextcloud command arguments are empty.

Added

  • Added a unified cassini dev stack up|down|status|plan workflow with explicit --services topology modes, including support for remote harness runs and a visible stack plan.
  • Restored the operator (recording-control) UI inside the single "Cassini" Nextcloud app as an admin-gated surface (D-420 V3): admins now get a Browse | Operator top nav, while non-admins keep the browse-only experience unchanged. The active surface is deep-linkable and back/forward-navigable via the URL fragment (#surface=operator), with no pathname router. Whether the operator surface is shown is decided by probing the operator API (403 → hidden); the operator JSON API stays admin-only in info.xml (the real security boundary), so a non-admin can never drive recordings even if they force the surface.
  • Added negative controls (harness/bin/test-session-artifact-packet-guard.sh, harness/bin/test-av-drift-pair-requirement.sh) that feed the real verifiers synthetic artifacts and assert they go red on captures with no media and green on healthy ones. They run on every pull request in the fast lint job — no Docker, no stack, no media tooling — so the guards' behavior is demonstrated rather than assumed.
  • cassini dev stack plan, up, and destructive down commands now surface non-fatal warnings for ignored settings, surprising recording paths, unreachable private media addresses, and resource-removing lifecycle options.

Changed

  • Standardized dev and CI harness execution around the same stack commands so e2e entrypoints and manual flows now use explicit, deterministic startup/topology flags.
  • Reworked the viewer to load meeting data through a single DataProvider seam instead of hardcoded static fetches, decoupling the viewer UI from its data source (groundwork for unifying the viewer and operator into one in-Nextcloud app). No user-visible behaviour change.
  • Collapsed the two Cassini Nextcloud top-menu entries ("Cassini" viewer + "Cassini Admin" control-panel) into a single "Cassini" entry backed by one in-Nextcloud app (cassini-app, which consumes the viewer as a layer). Every logged-in user gets the meeting-browsing app; the operator/recording-control surface returns as an admin-gated surface inside the one app in a follow-up (D-420 V3), and the operator JSON API stays admin-only throughout (unchanged security boundary). No change for viewer users.
  • Split the in-Nextcloud viewer into a shell (App) plus reusable MeetingList and MeetingView components — the app-shell layer for unifying the viewer and operator into one in-Nextcloud app (MeetingView is also the future standalone single-meeting share surface). No user-visible behaviour change.
  • Release CI now verifies that the exact CPU ExApp image can be installed through AppAPI/HaRP, record a Talk call, and publish a non-empty viewer transcript before image or harness changes merge.
  • Reworked the Cassini operator panel for the in-Nextcloud audience it is now shown to: the recordings list and run detail work as a master/detail pair, each run's attempts collapse into accordions so a long retry history opens at a readable summary, starting a recording is an on-demand composer rather than a permanently parked URL field, and the API-implementation captions are replaced with plain descriptions.
  • Rebuilt the connection indicator to state the connection first: a one-word status (Live / Connecting / Reconnecting / Offline) with the refresh cadence beside it, replacing a label that never said whether the panel was connected.
  • Restyled the transcription settings as three matching sections — detected hardware, quality, and device and model overrides — shown side by side on wide screens, with the overrides no longer hidden behind a collapse.
  • You can now link directly to a recording: the selected run is kept in the URL, and the back button returns to the list.

Fixed

  • Fixed harness teardown and bring-up edge cases so repeated runs no longer leave stale containers/networks or duplicate signaling-public-proxy aliases.
  • Cassini shell polish (D-420 review follow-ups): switching between the Browse and Operator surfaces now keeps the open meeting in the URL (deep-link no longer dropped); the operator surface uses the same light/dark theme as browse (stored preference, then OS) instead of only the OS setting; and the in-Nextcloud mount targets the innermost #content more robustly.
  • Added the missing operator/settings proxy route so the operator surface's STT-quality Settings panel works through the Nextcloud AppAPI proxy (it previously had no route and would have 403'd/404'd), and dropped the now-dead control-panel proxy routes left over from the collapsed second entry.
  • Talk recorder: stabilized subscriber negotiation behind intermittent empty/blank recordings ("no remuxable streams found in session artifact", silent MKVs, and 0-word transcripts). A fresh in-call participant previously caused two empty-SID requestoffer messages; the second could make the MCU tear down and recreate its Janus subscriber, rotating the SID while the first offer was still being answered. Initial and exhausted-peer recovery now issue exactly one request, repeated in-call snapshots keep the valid response throttle, and inbound offers cannot trigger recovery in front of themselves. The recorder also sends its answer without waiting for full ICE gathering, queues trickled candidates behind the matching answer, and emits end-of-candidates only after gathering completes (D-454).
  • Stopping a recording no longer fails with "job is not stoppable" when the operator has just reported the job as running. The stop registration now brackets the whole time the job is advertised as recording, so a stop is accepted from the moment the job goes running until it leaves the record stage, and one arriving before the recorder has finished starting up is delivered as soon as it does.
  • A stop requested after the recording has already ended on its own (while the operator is still uploading to Talk and queueing the build) is now accepted and recorded as a stop request instead of being rejected; the job's stop reason continues to report why the recording actually ended.
  • The release pipeline's required "Faithful installed ExApp" gate now decides whether to run from a deny-list (run unless a change is purely docs/notes/assets) instead of an allow-list of enumerated product paths, so a pull request touching a build input nobody thought to enumerate can no longer report the required check green without actually building and recording.
  • Talk recorder: closed two gaps that could still lose media on a call the recorder appeared to be recording normally. A participant whose handshake completed but whose media never arrived was left untouched for up to ~45 seconds, so short calls (a ~30s 1:1) could still finalize as empty ("no remuxable streams"); such a peer is now rebuilt about 12 seconds after its answer, but only while its connection is not actually established, so a slow-but-healthy participant is never torn down. A participant who turned their camera on mid-call could also be wedged permanently by a dropped signaling answer — their audio kept recording while the newly added video was silently lost for the rest of the call; the recorder now retransmits that answer as the signaling connection recovers, within a bounded budget, instead of skipping the participant forever (D-509).
  • Fixed the CI session-artifact guard passing recordings that captured no media. It verified headers and index framing but never counted packets, so a capture with a valid header and a 0-byte index reported "session artifact verification passed" — the exact empty-capture class (D-454) the per-PR e2e leg is relied on to catch. The guard now walks each stream log and requires real RTP media packets per captured stream, counted kind-aware so a stream carrying only RTCP no longer passes for having non-empty bookkeeping.
  • Fixed the A/V drift check reporting success without measuring anything. It exited 0 when no audio/video pair was found, and also when pairs were found but every one was too short to compare — printing "av drift check passed" after zero comparisons. Drift runs now require at least one pair to be actually compared, and the rejoin leg, which legitimately records video-less, opts down explicitly.
  • Fixed the e2e leg never producing a drift pair the check could measure. The publisher bots play the sample fixture once and stop at its end, and the default fixture is 15s — capping the measurable pair at ~11s against the 15s minimum regardless of the configured publish window. The leg now prepares a fixture sized to the publish window. The fixture is also rendered with a keyframe at least every 0.5s (libvpx's default 128-frame interval delayed decodable video up to ~4.3s behind audio, which the elapsed-difference metric misread as drift), and the leg's tolerance carries a documented budget for the remaining structural skew.
  • Fixed panels, cards and form fields losing their borders and shadows inside Nextcloud, which left content areas with nothing to separate them.
  • Fixed high-contrast themes leaving those same areas undefined, and the selected recording unmarked.
  • Fixed Nextcloud's app chrome applying twice, which added a second header-height gap, doubled the side margins, and clipped the bottom of the app.
  • Fixed the shell tab bar, the operator scrollbar gutter, and the detected-hardware values rendering with the wrong colours under some themes.
  • Fixed the recordings list not appearing on narrow screens.
  • Fixed the settings Save button offering to save when nothing had changed.
  • Fixed the meeting filter drawing a second focus ring inside its own.
  • Fixed the meeting load-in fade stalling and then snapping into place.

Added

  • Cassini marketing and docs microsite (cassini-microsite/): homepage, changelog page, and docs section with sidebar navigation.
  • The Cassini microsite is now published at https://gocassini.com.
  • Release tooling for the Nextcloud App Store: a version-ladder CLI (scripts/release-version.sh), changelog folder (scripts/fold-changelog.sh), local release-prep orchestrator (scripts/prepare-release.sh), App Store package builder/validator, and a manual Release workflow that signs and publishes gocassini.tar.gz. See docs/release.md.

Changed

  • Nextcloud toolbar icon updated to the Cassini brand mark with white fills and a tighter viewBox for correct rendering in NC ExApp containers.
  • Viewer restyled with the Saturn theme (ice blue and Saturn gold) and now shows a favicon. A previously saved light/dark choice resets once and follows your system setting until you set it again.

Fixed

  • Documentation pages on the microsite now show the site footer.
  • The microsite changelog timeline no longer draws a trailing line below the last entry.
  • Viewer meeting dates no longer append a timezone (e.g. "GMT+1") they cannot actually justify. Meeting date labels carry no timezone, so a UTC-derived time could be shown an hour off with a wrong-but-confident zone suffix. The viewer now renders the label's own wall-clock time and makes no timezone claim.
  • The operator control panel no longer flashes to loading spinners every couple of seconds while watching an active recording. Background refreshes (the 2s polling fallback and event-stream reconnects) now update the jobs list and run detail in place instead of re-showing the first-load placeholder each tick.

Added

  • The viewer's meeting list has a filter box for narrowing meetings by name or date.

Fixed

  • Talk recordings now show their conversation name and actual recording date in the viewer, instead of the raw job id as both name and date. When the name cannot be resolved, the meeting is shown as "Untitled meeting" with the recording date.
  • Meetings whose date could not be read no longer scatter through the list; the list stays newest-first and undated entries sort last.
  • Recordings publish to the viewer again. The static-site export crashed on every meeting — a regression that shipped with the D-462 meeting-name/date change (merged in #112) — so no recording reached the viewer. Publishing now completes normally.

First public alpha.

Added

  • Added public project metadata: AGPLv3 license, changelog, security policy, and contribution notes.

Security

  • Marked deterministic harness credentials as development-only values and expanded ignore rules for local dependency and signing artifacts.
Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureqHWu4BvUTmEz8vLKHwiQISBBfpIghrmDS8F2bm92mXtaqmKZ68QLuDaNdJxEIzEve9KtcadKOVv7FvybODZY1NG0wtyty2WAqtSQDgGyg3D6Rcf5UTI6SPH9Fqt9jrj6wrPugbAUZKKrf05SInXCwGineaoBCybE2uSE9lB0dkBi4VEEafNM7Cva/wESlp2/bFyU2f/C6lA1ivxAzwowijN6PB/uzNMfpstH9cW+xDO1pqoGeSw7hjJ1rFQvAIMREwIWcWZnTAiPIag1SzgYoJv0hK/AFa7KsWp2TnQDV0S/QWRafutYmGR2ZnuBowsjfXyKDEmjEWQn7ok0HGiTO0SswqMk1nzTn3B5kPLFSVBj0/+MJ6oAXvgqjqP3Au7Jmp+771rEK94xlcWlUmDi3mW6fNvaD5LPpeRoB1u6CmCEtZSh+cMOYFE7OlhWuXqZC3p5rBeUUYWLUsQDXnG3SeHDaGwcyZ+uvijc1FQ2DhJMMcnY9mhtztR/9vZUs5DttIRa2q1nuP7Nvcer+GRlkD1HgntP9HwAk2RBXHMuuewRfZMVOWGJXSxobZkAip80sg02rrHVgyWmE2jElCiZrWUKcTMd9BkF1d+yWy0msiY7dIrTEEBbOLx1hFI3wWsRnOdIS6WgtbX1xhmB4akWQxMuh6oI+6BAmuRRK+oH9Uw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=33.0.9,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.8
Release Details
UpdatedOct. 6, 2026, 10:06 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureoarCm0opumircwdOtbKX8fyvj4gEcHXlR8t9ga001qpk4LZvkY6SbU3hDM73gQtQYvlI8S3sHsoINzDxxWc+hb5oEJW0rNAOIjUGG4SeE7CGIk4zcVcUFI5R8nN3nirsm8yXhFTMNPVoFhb00Jmh05qbTkhe/A6qpPWfG1dUxMQkhGDo2EB6p7+1v8eOOhChrupJ7NWKxkdvDkQM56ITKti29dP09ajgYIYetM27swILRUFExqHSrZQzhADnSU7VWytj/inWFOYk1hZR6d5m2XYyihk6V8WeK2Jv+orEOlEDdzT1xSHhZAVyrwFEbFkDiO+VpMR1ZTxhh7XbF+htvF9itTBeeaZxZirzlpJ/KOFBvpUrc4bDEv6szcLknHowaKpjqa15/4u0Ssaaf8aJaJSbRi5khMYBGW3fyQ82DWx2nsFWsxVI/lBuG4T6HG+MX8x1YhLx7KledlNJnZlrHp89mv7TxQ2E0pLysWYeFauceBDBv/pdDAdYom3K+AcgD0HGSFBNhQ2BITlmk0DNQ5mGFhAy6Cqr+63vCrRP1Y7QTR5itIKw5BBGMjuI3XE+yfJ4ArjSkY2IciqMdEvk0ULjyV1kqvdlRMyFWuE2sYv87dFmPPvHMqXZNzIY/lAWx0hE56ZxjtrJDVVLfAWD67DxOTqR2Qsyliui96WoVOU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=33.0.9,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.7
Release Details
UpdatedSept. 19, 2026, 10:57 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
Signatureq9Yl5MApgS2v7JvdVeoXX8XY+e9xoIcNMy8HSwsU5UdFir++uQNW1/1oIKL66VVie9CuPCCQLIicFJ3pOjjf1CqjShJ1gaDVyUVHm5EVeHno5ah/lFqycCwQsW8qNlohtBz2wU7MVn0ooIULjGp6rFmSzxA3A9FQpSA/SSDjCnHjLgOymV+qWZjYzQluKES+NOj58Pyx8O19Z0U+5Gmubmc58nlKszodozXnU0K8s0gj58J699Lh9sMm1GNufZs4eE8zAo904wA4rqNGD9v3Bl27I8TVpGNwVLxrM90mOjpzaoYpzAaBWWYbneLBquZUflKpx2pJvinUDOEuD9rTbeCWfGXZ4cqCW5bwEuvx2QsOfCk0B2wOQpwyAfCgI2i3tsVxIvTs3hfb3qVhMJ45uZerqqYFyNW8oj7sqw1XJSZrTaRrk3ZW6RcuOTRMCMlT9y5/PjlSXQglZe2/OvndTi2o5DpttKChtd9EiN4NMbMTAW6ih++H3p11tyf1F8xLFc3dEm6U+aLMQFnMlR1hg8yclowh8klRxJf/r0PTcQoYIYwIBXsoDlowyohaLkJzXK4dsedTwf9gJdzjHOq+0h+gFiYGkPWgjejlL7n0v4jSD7BmA7oxp43YPRc9H1nB57oJKLKPQ4PudZS4gz6eJa/CxyQX7Y5A+oRM+PMJv/4=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.2
Release Details
UpdatedAug. 20, 2026, 7:15 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureCsJE9kLQCmBdKL0g4xSp/yu8dv0UbaGzLk6qCbrH2NOhjN4hr66SS+R66wvY1Q40l9Z7VR08jDLhwNnEeMVEY3C4Pk9YPEBPJAM/3MyRudg6EQ01amV7qgFPPaa96eZXiFD9lozVDbchBcihooe+k5m4HNJjey76F0LHIPNUqtGQjPeDBMJtMCXQRulQORbzDyx2ets+JvPOITr+pu65sE8T16yuoMgvm8qivWEtF8+12bsBOt6qXRNQokDowXzrNeWO/v8BlJg1pEBvmjK8G65EG03xYiRw0F6Vwm38Lx1OY7zWMFco9QLrSlHA7XeYahZZ8CMOWybxYrnPa1GFBbyMaPYoWRQRmRMoPSY7D7g/jAYzjSmh0jmLyfPAOPLV681mi644v1R5rVzbWPjYol+fIb3kUVU2d1z4oyRwM7sfd39NGeWmgI8SvG0LOZPArFpJod3AVrh9fvAD5PkASyzHMRH7cOHI3BI1g/4/lsMjIc/cTJddAZPsOsZ/PlTDD5GfJm87m0eoUWj0cetODqUmUUsJJvDH8siKNTXZIhQmbn/4wDFaqXPnufYgogY2XjyFqEDbdFzyL1hgsWlSO3NX8+t2HQjhTOdqnCTU8QeREpGc5RFOm3XcCTVFaQIEI/4hi+wwA98pkB4UXCGiV9+s0qCwhJdm1yOnjfiD9uU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.1
Release Details
UpdatedAug. 14, 2026, 5:49 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureR3TWjXw2RXfjhTk3UwiSEsLsfneLrrUFnqWhYZoE/rNoYyP0ETh7kkAkHrkIrPuhcZWo5U5L+wxeihkfMlMHwraMhMtmlUDGenmzPoxAuZhbjTuhHMn06+nsLY5mHZqIA7i5s68YT/xrduPvBcEDWqzJktJDXEsN+ZBZdpAoI4pqS8j8aZP+7io78c4wtYIHuA58V+e5yvpDvxnQr11W2CQWH/Q+E52clqSHsmcFRq25eQsKN7mPsvJb2eINY+vyUK0Mz88ZPg1vNFyhOBqDCTnp8xoyMrLHe37rNLriBNuuOuDP6Umuv5d1cW1GGc8fzaZxxoBXtMqQzkKyyxazKMOT4v23eFRgWlOx38y9DrUn9+qbMCxY8fLJ/rbG/XeKpZuEvoZGW74ol0Y62+EXIT1vueJOdlKFrRwIv8DvOxbYeqI7NFk17Zs49+iuUyRoNBAHdTrI2yvneLHL5ePeyMS8J5DuNdOtFNEtgRcXIfGoMHVzN6F33q2UnEeRN6q5iEJZwh7OgxAKQo9RL5kzLzky5A+6v1JP6/40fSrse5JxjzNA98+osNs87JEWA675GtPbWnOM757VjBC5ZwNA1hz97l5UA8WQYdgLoVcLhuWlW302KYnqg2PMQYM5zZkyZRYlyhJ8RDdyJXTMhOdJHQTHLtoNrQxISzeRicyX3bs=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.4
Release Details
UpdatedJuly 27, 2026, 12:25 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureDFZcmcqbeTiTh9ZtdIKLh6MOP647ayzVl9rR20Qxp74ysDBxcTsLs3LQRxmjh6WqZOjse/1t1x1oBdFcHkqbtjvNHsZJUOPb4RMh1b1sh0q5lUnHkCHp5qYoa1rJnrZt/WiqsYysgwwwtk9dMluvBLd/DHA8MY5ICMxII7LQtucl/ZRaH8kGYwsdx/Zn2YY9J0B+jjFeKM8gsZdy8TYwfKgb1k+b6jENXVgDb8WcMi9Vovw6VZ4SRTGPmsbOPOJsBVHIBQY+D+snIXjJ3VkWkU1laTJnYss9Fv7Su4nkUNLI8wpVwCSbrANTDY0HAx00zpbt1tH7rqWvS0rYqtYgfgJqfrrwTdpaCgcOoc6BcG1i4SMCyK9zRpxdXrpwG7kU6C2ubBqO0Zy7UL2c4cJ9npWU20UeWDSWfINngoRtdR0sbxoN5arkD1mvd7B1Doza3WMkT6XBgyk/PVcFnuduhWQYoyQIAIzajLEzTkSFrA5QM38cpWVeIDIRVYYKizya5xweM/mIvEuIArXAd4b+AyDL/glmuPC9L7ftmCMp7EJPTMWctRLheYNM4aMc44l+A5SpoU/nMqo69+/Q2wc23VWbT53hhVH9rbUMxMQ/SgX+EXj0uKDumldDQlpZV3HjCPW81uwCSB9gpLrCi0zREENxn9zSXB24WF+y9cFx7Dw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.3
Release Details
UpdatedJuly 11, 2026, 11:16 a.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureeRHRR6dJjmuxKf+SembwkG/mU3joMZwIrZ1bMXGB49Uc4l2t6HvD3Urbw5AdoofHgERt7RGkTf0YLF1hdSRSxqwCVw6XpvGLfjfAZEUCTM9i2mLTey/66QfctnFcE35wW0mzEQbkYbmQp5YKogsgZ+6X/cxlrio3I4EEOaR3ZBRQp7RxeSqUGhjTpnHomrVCOEwNpvGK2UIzVLqHA7gfLY5W7o/DBfmTiXt1BJ8pGTjbGV9Gi81jcxLp9LaTfjbkMVZf7stRdRtMvIhEuUgSgLyxAZftvoo4oGqB+lnx87rAGeQ90nynKx/6yVvYN9AO6Gzxg1Y5ElIS7qdBqSZxnZ8AJtoGgbCc1GT29aKbrLEAUBKcsuEXgSR21qe9DM/GNQgy8i59viKBsNOeOI+B+K2fZciKxOcv4B7iO94axm4po59voAETJyUej+R8iPfLj6kTSWr/mLhfFiF0vu89Gktb8T1OMYBaDrthHXfXl0D+5FAt6NQNzJOW2S2IDwerOMAKlAff0tkDOUZOGlffvJZC8W6qeh67EMd2UL83nkxO2h6dQpRaXD0wycZxlB2/HCoGDqrIEWvlz7rf25PV3iyaL0yu0Vh3rpFTd7tGOJyYq6y4qmJDEVL/Jd1ImEOloY3jWWeQ2dzNSOW2g6+/89KbkrV78UnOhWAPp6fmc2E=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32

Nextcloud 33

Cassini 0.2.0
Release Details
UpdatedOct. 13, 2026, 4:42 p.m.
Changelog

Added

  • Administrators can select Transcription only (.json) in Settings → Publish pipeline to publish new meetings as transcription-only files in Nextcloud Files, retaining speaker timing, summaries, metadata, tags and annotations without audio. Existing meetings keep their format on reruns. Audio and transcription-only meetings can coexist, are labelled in the meeting list, and follow the same retention policies.
  • Administrators can publish transcription-only meetings and choose whether to retain source media for reruns or delete it after processing succeeds or fails. Each recording keeps the policy selected when it starts.
  • Operator jobs show media deletion progress and errors, offer deletion retries, and explain why processing cannot be rerun when deletion was selected.
  • Proof of concept: separate the voices of people who shared one participant's device. In the app, a meeting's People panel offers Separate voices for a participant, then lets readers listen to each voice, name it, say two voices are the same person, or undo the split; the operator republishes the recording with the new speakers and rewrites its summary. Diarization runs locally on that participant's own track and stores only turn timestamps; nothing is re-transcribed and the audio is not re-encoded, so marks stay attached. After a split the original transcript stays in the recording as raw-asr ("Original"); renaming a speaker without a split only changes the name in the one transcript. Each person can save a limited number of speaker changes in a while, across all meetings; past it the panel says when to try again and keeps what was typed. See docs/speaker-separation.md.
  • Voice separation (optional) in Settings: an administrator downloads the Nemotron 3 Diarization int8 model (about 62 MiB, CPU only) like any optional model; cassini models install|pack|import handle it too.
  • cassini speakers diarize|apply|show, and the operator route annotations/meetings/<id>/speakers (GET/POST, for anyone who can read the meeting). CASSINI_DIARIZATION_THREADS sets the diarizer's CPU threads (default 2).
  • Insights have names. Create an insight fills one in from the template and the meetings it reads (the question is quoted inside the insight instead), and you can change it before generating or later from the pencil beside the insight's title, with Save and Cancel as when renaming a tag. The list, the insight and the meeting's Insights card show the name, search matches it, and the document saved to your Nextcloud files is named after it.
  • An answered insight can now leave Cassini. Export in the insight drawer copies or downloads a markdown copy with a readable header (the insight's name, the question when that differs, when it was written, the template and model, and the meetings it drew from that you can see, each linking back to its meeting), and Open in Files opens the document in Nextcloud Files with its details open, where it can be shared.
  • Administrators can schedule whole-meeting expiry from Nextcloud Files independently of container storage. Expiry removes published audio, transcripts and notes together, with conditional deletion, durable recovery and protection against reruns or indexes recreating expired meetings. Nextcloud versions, trash and backups remain separate.
  • ADMIN-only APIs provide on-demand sweeps and eviction previews with the meeting name, creation and recording timestamps, age in calendar days and an evict/keep decision.
  • ADMIN-only POST /operator/storage/retention/sweep to run the scheduled expiry logic on demand, with overlap protection and synchronous results.
  • Configurable daily retention sweep time and timezone in Operator → Storage, defaulting to 02:00 UTC. Saved schedule changes take effect without restarting.
  • Operator → Storage retention configuration for container recordings, attempt history, current output archives and stage logs. Policies default to keep forever, with 7, 30, 60, 90 or custom days evaluated on UTC dates.
  • Startup/daily cleanup with restart recovery, whole-recording expiry (audio, video and supporting files together), and unavailable-source rerun protection. Only successful publication replaces the current local archive.
  • Storage insights now includes published Nextcloud files in the category comparison and temporal charts, with the same time range and exact-value controls as local assets.
  • Files without a matching job use recordedAtLocal or createdAtUtc metadata for their date, including inspection of older unindexed files. Undated files and partial measurement failures remain visible.
  • Operator → Storage shows local usage by retention category alongside retention settings, plus published Nextcloud usage including legacy archives.
  • Each category has a date bar chart with independent time range and precision controls, exact values and a data table. Attempt history can be combined or split into its fine-grained policy categories. Unknown dates and unclassified local files remain visible.
  • Storage usage is served from an in-memory index with its last-calculated time. Administrators can rebuild it on demand, and Cassini refreshes it on fixed five-minute boundaries.
  • After a Talk recording is first published, Cassini makes a best-effort attempt to post its meeting link as the moderator who started recording. Notification failures are logged without affecting publication or recording access; messages are not retried.

Changed

  • The fp32 Parakeet v3 model now downloads from dist.gocassini.com, like every other model.
  • The native speech runtime now includes Nemotron 3 Diarization support (sherpa-onnx v1.13.7-cassini.6). The runtime accepts the version-2 Nemotron models on dist.gocassini.com and fixes final-chunk padding. Parakeet and Silero model files and transcription behavior are unchanged.
  • The privacy guide describes on-demand voice separation, the summary rewritten after a speaker edit (which sends the transcript and the typed names to the configured summary endpoint), and where names and editors are kept.
  • A participant whose voices were separated stays in the recording's speaker list, marked with the voices that replace it, so every transcript's speakers are listed. Readers older than this release show it as one more speaker beside its voices.
  • Refreshed the App Store listing: a new description with a live demo link, an early-release note inviting feedback, rough hardware guide and Ethical AI rating; six new captioned screenshots; the Social & communication category; gocassini.com as the homepage; and the Codemyriad author link.
  • The documentation and landing page describe recording access as it now works: each recording is shared with the people, groups and Teams in its Talk room, with no audience to choose and no Team folders or Everyone Group app needed.
  • New recordings capture audio only by default, including upgrades without a saved video choice. Administrators can opt into retaining source camera video using Capture video in Operator → Publish pipeline. Retention review or reminder dismissal does not change video consent. Saving affects subsequent recordings; published meeting files remain audio only, and existing source video follows its retention policy.
  • Retention descriptions explain storage growth and the consequences of deleting each category. Existing keep-forever defaults and published Nextcloud recordings are unchanged.
  • Nextcloud Files retention uses the meeting’s recorded local date when present, otherwise its creation timestamp. Existing managed meetings adopt these dates without relying on job completion history; unknown provenance or invalid timestamps keep files unchanged.
  • Preview, operation-status inspection and on-demand sweeps are API-only tools; the retention UI provides policy settings. The durable operation journal and backend logging are preserved.
  • The old artifact-retention flag/environment variable is deprecated and ignored. Successful capture/intermediate duplicate cleanup remains unconditional; historical payloads now default to keep forever.
  • Search backfill against the local work root requires stopping its operator process to avoid racing archive promotion or expiry. Existing AppAPI installations must receive the new ADMIN retention route through a versioned manifest update.
  • Wide storage charts can be scrolled with the scrollbar, keyboard, or Earlier/Later buttons. Days per bar adapt automatically to the selected range.
  • A meeting opens on Summary and Transcript tabs, with insights at the end of the summary. The playback bar shows only under Transcript, and find-in-transcript sits beside the tabs, pinned while you scroll.
  • The meeting header is compact: the date, room, duration and participants sit in a details pill that opens them in full, technical details are folded into "About this recording", and the Raw JSON view is gone. Copy and download actions are one Export menu.
  • Tagging is a deliberate step. Dragging the timeline rail scrolls the transcript instead of selecting text, and a text selection offers Tag selection, Copy text and Cancel. An existing tagged section opens read-only, with Edit to adjust or remove it. A tag on the whole meeting is removed from its menu rather than an always-visible ✕.
  • Selections and hovered tagged sections are highlighted continuously across words, like a normal text selection.
  • On phones, the meeting and Prepare sheets fill the screen, and tapping the covered tab bar closes them.
  • Doctor opens like the other Operator pages: a fixed title and description, with Run all checks beside the title, and the overall verdict in a callout beneath, with when the checks last ran. The verdict also says when a test recording you started is unfinished, and links to it.
  • Each check leads with the next thing to do, under the explanation it acts on: Nextcloud's setup guide and Talk's settings when there is no High Performance Backend, Open test room while a test waits for Talk, Play the recording and an "I could hear the audio" checkbox once it is published, and See why it stopped, which opens the run on the Recordings page, when it fails. Re-check buttons are quieter, and a check's own time appears only when it differs from the rest of the run.
  • When something stops Talk from being checked, such as Talk refusing Cassini, Nextcloud being unreachable or Cassini having no recording secret, that is the one row to act on, and the rows it prevents from being checked wait on it.
  • Cassini says why it could not reach Nextcloud (an address that does not resolve, a refused connection, a timeout, an untrusted certificate, a server error, or a proxy answering instead), with steps for each.
  • The few commands Doctor gives, such as where to read a secret or how to see a log, are labelled with where they apply and come in a block with a copy button.
  • Archive search coverage reads in plain words. A running re-index shows progress and updates the row when it finishes. A failed one says what stopped it, and offers to try again only where that can help.
  • When the panel cannot refresh, it says why, whether a session that needs signing in again, Cassini not responding or the connection, and offers Try again while keeping the last results on screen.
  • Retention setup is now a nonblocking administrator reminder. Browse, playback and recording remain available before review; administrators can save unchanged defaults in Operator → Storage to acknowledge the reminder across sessions.
  • Administrators can choose “Don’t remind again” to keep the current settings and permanently dismiss the reminder for the installation. Unsaved drafts and concurrent policy changes are protected.
  • Meeting audio is encoded to match what its recording carries instead of at a fixed 64 kb/s, and with Opus tuned to reproduce the voices as they were rather than reshape them for a phone line. A 16 kHz phone recording is published at about half its previous size. A Talk call is coded at a third over what its highest-rate participant sent per channel, at most 64 kb/s; a participant whose client sends stereo counts at half their rate, since the meeting mix is mono. On our own meetings this makes the published audio about 30% smaller. Rebuilding a meeting published before this change keeps its original encode, so tags and marks on it carry over.
  • The hardware details under "Change" are one box: GPU, CPU cores, memory and the device choice as label and value, then one tip. The device choice warns only when it names a GPU this server doesn't have.
  • The line above Quality reads as one sentence ("Transcribing with Parakeet TDT 0.6B v3 — fp32 on the CPU (no GPU found).") with a Hardware button that opens the details.
  • Browsers revalidate the app's script and stylesheet on every load, so an update that keeps the version number no longer leaves an old stylesheet in place.
  • A model download says what happens after it finishes: "Downloaded · checks once no meeting is being processed", then "Checking it runs on this server", instead of sitting at "installed · 100%". The progress bar shows only while downloading.
  • With transcription off and the picked model downloaded but not yet checked, the hint says "Check the model below to turn it on." instead of asking to download it.
  • Model actions (Download, Check, Retry, Cancel, Use this model) and Retry media deletion are styled as buttons. They had no style at all inside Nextcloud.
  • The voice separation card leads with what the model is for, matches the transcript model row, and has more room at the bottom.
  • The Publishing card is now "After each meeting", and a strip at its top shows what new recordings get with the saved settings: transcription, summary, what is published and what is kept. A step that depends on one that is off is marked, and each step jumps to its setting.
  • The card asks four numbered questions in the order the steps run. Until transcription is saved on, the questions after it are dimmed and cannot be changed, and saving with transcription off publishes the audio.
  • The keep-on-server options say how long kept audio and video stay, from the Recordings retention policy in Storage.
  • Hardware is now a line in the transcript question; the detected GPU, cores and the device override open under Change.
  • Summarisation is now part of the card: its switch, provider and template sit in the summary question, and the separate Summarisation card is gone.
  • Vocabulary and Search spellings are now one Transcript hints card.
  • The documentation states the supported range as Nextcloud 33.0.9 to 35 everywhere, matching the App Store manifest.
  • The deprecated working-file retention setting no longer says published Nextcloud files are never evicted; their expiry is a separate setting in Operator → Storage.
  • Each Storage date chart says which files the next cleanup removes under its saved retention policy, and a dashed line separates the faded bars it removes from the ones it keeps.
  • Publishing now includes transcription: what is published, then whether and how a transcript is made, then what stays on the server.
  • Retention settings use plainer names on every screen: Recordings, Published meetings (on this server), Previous attempts, Processing logs and Published meetings in Nextcloud. The reminder banner is one sentence with Review and Dismiss.
  • The Storage report shows two totals, On this server and In Nextcloud Files, then space by type and by date. Previous attempts opens onto its four kinds, and each date chart picks its own bar width.
  • The retention editor shows each category as a heading, one line and its control. Previous attempts splits into its four kinds with a link, the daily cleanup is one row with a timezone list, and Discard changes appears only when there are unsaved edits.
  • New installations run the daily cleanup at 03:42 UTC instead of 02:00 UTC. A saved cleanup time is kept.
  • Publishing asks what people receive first, then what stays on the server. Keeping nothing is unavailable while audio is published.
  • Transcription has its on/off switch in the heading, and the model shows as one row with its size, state and one action.
  • Opening a meeting from a search that matched what was said fills the meeting's transcript search with the same words, as opening one of its moments already did.
  • The app asks for its stylesheet by build, not only by app version. An update that keeps the version number (a rebuilt image, a hot-deploy) could leave a browser pairing the new script with an older stylesheet, which unstyled every screen built from components.
  • On wide lists the matched transcript lines take more room and start further left, while the meeting's title, date and tags keep about 34rem.
  • "Who can see recordings" in Settings and the "Shared with participants" note in the rooms rail lead with "Nextcloud Files controls who can read each recording." and then say the same three points, as a list. The rail's note is wider than the rail, so it no longer wraps into a narrow column.
  • A meeting opened with words carried in from a search opens on its Transcript tab, where the matches are highlighted.
  • "Who can see recordings" is two short paragraphs. Its status and a Check again button appear only when sharing needs fixing.
  • Voice separation has its own card in Settings, with the same model row as transcription, instead of sitting inside the transcript step.
  • The transcript step opens with one line naming the model in use and where it runs (CPU or GPU), with Change for the hardware details. The status strip's Transcribe step names that model's tier. A model that the picked quality still needs shows under a small heading, without repeating the one in use.
  • The status strip shows two chips per row when the settings card is narrow, all the same height. Step headings are larger than the text under them.
  • Publish pipeline no longer has a reload button in its header. It discarded unsaved changes without asking; the browser's reload asks first.

Fixed

  • The Cassini app no longer requests the administrators-only operator status on every page load for users who are not administrators (each request answered 404).
  • Guests who join the call at the moment the recording starts are now listed under their own name instead of a participant-<id> placeholder.
  • Preserve the same audio timing when source video is omitted, including single-track recordings remuxed with FFmpeg 6.
  • Doctor's test recording no longer offers a test conversation you cannot record in. Talk lets only a conversation's moderators start a recording, and the connection check creates this one under Cassini's own account — so Open test room could drop you into a call with no record button, directly under steps telling you to press it. The link now appears only for a conversation that is yours, and Prepare test makes you one.
  • Doctor now notices when its test conversation has been deleted, instead of only while Talk has a High Performance Backend. Without one, a deleted conversation stayed on the panel as a dead Open test room link, and preparing a test re-armed it against a conversation that was gone. Cassini asks Talk directly now, and forgets the conversation only when Talk says it no longer exists.
  • Storage chart bars now fill the available width and keep date labels below the plot despite global timeline styles.
  • The browse page and meeting view load without pop-in: the sidebar holds its height while rooms and tags load, the meeting list fades in once ready, and header tags and the timeline rail appear with the meeting.
  • Copying a selection after moving its handles copies the adjusted text, not the original selection.
  • Insight titles no longer truncate to a letter on phones.
  • Status colours meet WCAG AA contrast in light themes, including Nextcloud's.
  • "Connect Talk" is no longer offered when Cassini has no recording secret to give Talk, and a re-check button no longer says "Check again" for a check that never ran.
  • New recordings retain their original recording time through processing, publishing, and transcription-only export, even when filenames change or transcription is disabled or fails.
  • A failed recordings setup check no longer hides the meeting list. It shows as a strip above it, and you can still browse recordings; only saving new recordings stops.
  • When transcription is on but its saved model's files are missing, Settings says so: the transcript step shows a warning instead of "In use", and the status strip's Transcribe step turns amber with "model not installed". Recordings published in that state have no transcript.
  • Downloading the voice separation model no longer shows "until the model is ready" on the Publish step, and that note no longer appears while transcription is on.
  • Voice separation's "(optional)" is lighter and smaller than its heading.
  • A model installed but not yet checked against the current build reads "Installed · checked before the next recording" instead of "runtime check needed".
  • Selecting transcript text by dragging from outside a word (the end of a line, the space between turns) no longer loses the selection and its Tag selection bar the moment the mouse is released.
  • The "Who can see recordings" card and, in high-contrast themes, the "Create an insight" card now have the same inner padding as the cards around them.
  • The dot or icon in a tag chip sits on the centre of its label in every browser, including the tag filter in the rooms rail and the tag picker.
  • Search results keep the meeting's title and details readable at any width: the matched transcript lines sit under them, and beside them only when the list is wide. The meeting's length no longer runs into the first timestamp.
  • Clicking a matched transcript line opens that moment again; the row's click area had been covering it.

Security

  • Doctor's test conversation is now private. It was created as a public conversation, which Nextcloud never lists or shows to ordinary users, but which any signed-in user holding its token could read and join. It is now a group conversation whose only participant is the administrator it was made for, and Talk answers everyone else that it does not exist. Recording is unaffected: Cassini joins through signaling, not as a participant.

Added

  • The recording checks now show their state in colour: green for a check that passed, red for one that is blocking recording, and grey for one nothing has verified yet. The heading takes the colour of the worst news, so the panel can be read at a glance.
  • Configure optional transcription in Settings with visible model download progress, cancellation, retry, and explicit activation.
  • Model downloads resume on their own after a dropped connection or a stalled transfer.
  • Prepare transferable models with cassini models pack and install manually copied packages or files on air-gapped systems with cassini models import.
  • A Doctor section in the operator answers "is recording going to work?" in one place. It reports the recording volume and temporary space, Nextcloud storage, Talk's High Performance Backend and its credential, the Talk connection, a test recording, and how much of your archive search can read. Checks run once when Cassini starts and then only when you ask — with Run all checks, or a single row's own button, which re-runs only the probe behind that row.
  • Cassini says when Talk has no High Performance Backend, which stops recording entirely. It reads Talk's own signaling mode, so this appears even before a recording credential exists; it used to be discoverable only through a connection check that never got that far. The row is on the checklist whatever the outcome, including before anything has been established, so the one check that decides whether recording can work is never simply absent.
  • Archive search coverage reports how many meetings search can actually read, so a partial answer is visible instead of looking like no matches. Where meetings were never indexed, a Re-index now button does it for you. A shortfall nobody can act on — a recording where nobody spoke, or an older meeting search cannot index at all — is reported without being counted against coverage.
  • cassini doctor --json emits its checks as structured data, each with a stable id. Running cassini doctor by hand prints exactly what it printed before.
  • Copy or download a meeting transcript as Markdown (.md) and download its audio from the meeting view or Prepare. Multiple selected meeting files download together in one ZIP.
  • The <cassini-meeting> embed takes layout="inline" for pages that already show the recording's name and details, and --cassini-color-* / --cassini-font-sans custom properties to match the page's colours and type.
  • The <cassini-meeting> embed dispatches a playbackerror event when the browser cannot play the recording, and follows its theme attribute when the page changes it.
  • Scheduled upstream compatibility checks and real-browser coverage of the embedded Cassini transcript and recording playback.

Changed

  • Share each recording with its Talk room audience through Nextcloud Files, without requiring Team Folders or Everyone Group apps.
  • Show only recordings in the caller's current Nextcloud shares and check each file read as that caller.
  • Replace the two recording access choices with one participant-based model; public meeting participants may reshare when Nextcloud permits it.
  • Rewrote the App Store summary and description to match the README and docs site, and pointed the user documentation link at gocassini.com/docs.
  • Brought the App Store description up to date: Nextcloud 33.0.9 to 35, transcription off until a model is installed (and the model download from dist.gocassini.com), the Doctor checklist, and transcript and audio downloads.
  • Rewrote the landing page copy: a new hero, a feature list that leads with the App Store install and Talk's Record button, an AI providers section in place of the summaries one, and reworked CPU and GPU cards.
  • Corrected the supported Nextcloud versions to 32 to 35, said that insights as well as summaries send transcript text to a configured endpoint, and linked the meeting file specification.
  • Renamed GoCassini to Cassini in page titles, link previews and support links.
  • Transcription defaults to off. Recordings remain playable with participant metadata when transcription is disabled, unavailable, or fails.
  • A recording waits, as it does for memory, when its model cannot be checked yet (GPU not visible, not enough free RAM or VRAM). It is published as audio only when the model is missing or the wait runs out.
  • CPU and CUDA images no longer bundle Parakeet or VAD. Models download from dist.gocassini.com into persistent revision directories and survive application upgrades without downloading unchanged weights.
  • A check that depends on another one says what it is waiting for rather than asking for attention itself, and offers no buttons of its own. With no High Performance Backend, the credential and the connection checks no longer each report a problem: the backend is the one row to act on, and fixing it is what makes the others checkable.
  • Testing a recording no longer asks you to supply a Talk room. Cassini makes the conversation for you, in your name so that you can moderate it — Talk only lets a conversation's moderators start a recording — and you join the call and press Talk's own record button, while the row reports what happened until you confirm you could play the audio back. The check appears only once everything it depends on has been checked and passed, so it is never offered as a test that cannot succeed.
  • Checks explain in words, with no shell commands to copy and no questions about how Nextcloud was installed. Where Cassini can fix something it offers a button; where it cannot, it points at Nextcloud's own settings and documentation. An instruction assembled from a guess about your deployment is worse than a reference.
  • Checks no longer re-run themselves every five seconds. A reading is taken when Cassini starts and whenever you ask for one, so the panel stops disabling its own buttons twelve times a minute, and a check costs a probe only when somebody wants it. A test recording still being worked on is followed until it publishes, because that is the one thing that moves on its own.
  • The signaling credential is reported by the High Performance Backend check rather than by a row of its own: it is that server's credential, and that check is what accepts or rejects it. One row now says whether a backend exists, whether Cassini has its secret, and whether the secret works. It says plainly that the value is not a Nextcloud setting and where on the signaling server to read it, it does not ask for the secret where no backend exists, and the form stays reachable once the check passes so the secret can be rotated.
  • The Nextcloud storage check says what it established: Cassini's account exists, its recordings folder is writable, and the sharing API answers. It used to report that a "preflight passed", which named an internal routine and no fact.
  • The operator's readiness endpoint is now GET /operator/health, because it reports on more than recording readiness. An installed app picks the new route up when it updates to a release carrying it.
  • Require Nextcloud 33.0.9–35 for new installations; retire Nextcloud 32 support in this release.
  • Require successful installed-app compatibility evidence for every advertised Nextcloud major before publishing a release, with exact tested versions and artifacts attached to the release.
  • Cache compatibility Go tools, report scenario phase timings, share GPU smoke transcription across assertions, and separate registry maintenance from release qualification.

Removed

  • The "Cassini is ready to record" first-run dialog and its per-install acknowledgement, left over from the two permission models. A fresh install records as soon as its checks pass; creating the cassini account, when Nextcloud refuses it to the app, stays in Operator › Publish pipeline.

Fixed

  • A failed refresh no longer erases the checks. It used to rewrite every row to "could not refresh", so one failed request hid whatever was actually wrong; the last findings stay on screen and the failure is reported once, on its own.
  • A check that was passing no longer turns into "not verified" five minutes later. It keeps what it found and says how long ago, so an idle panel stops looking like a problem — and a check that has never run says so, rather than being reported like one whose result merely aged.
  • A connection check that ran and could not reach Nextcloud or the signaling server now reports a problem instead of "not verified". It tried and failed, which is a finding about the deployment.
  • When Cassini's Talk backend URL override points at Cassini's own app proxy, the check says so and names the setting to clear, instead of blaming connectivity and TLS.
  • Buttons that read "Configure" and opened a panel about an unrelated fault are gone. Every action a check offers is named for what it does, and one the app cannot name offers no button at all.
  • The operator panels can be scrolled to the bottom again. Whenever a check needed attention, the banner saying so pushed the panels down without shortening them, so their last few centimetres sat below the window with no way to reach them.
  • Meeting tags now stay in click order while changes are confirmed or retried: selection and meeting counts update immediately without bouncing, and tag choices remain alphabetically ordered.
  • An embedded recording no longer takes the Space key on the whole page. Space plays and pauses only when pressed inside the viewer.
  • Following playback in an embedded recording scrolls only the transcript, never the page around it.
  • A narrow embedded recording no longer shows a back arrow that led nowhere.
  • An embedded recording no longer reads or rewrites the host page's URL fragment, so switching transcripts no longer replaces the page's #anchor.
  • Meeting tags and transcript annotations now appear immediately while saves run in the background. Editing stays available, pending changes survive closing and reopening the meeting, and failed saves can be retried without duplicating marks.

Added

  • Access control is now a choice rather than a requirement. Cassini used to have exactly one way to store recordings — a Nextcloud Team folder with per-recording permissions — which needs two Nextcloud apps an external app cannot install for itself, so an instance without them recorded meetings happily and then could not publish any of them. Recordings can now be visible to anyone with a Nextcloud account, in the cassini service account's own CassiniNoACL/Recordings, needing no extra apps at all; or to meeting participants only, which is exactly today's model in its Cassini Team folder. Each has its own folder, so neither can shadow the other's path.
  • Cassini can set itself up. It offers to make the changes an administrator used to make by hand: the cassini service account and its group, the Team folder, its group mappings, advanced permissions, and the permission-manager delegation. It lists every change before it makes any of them, and performs them as you — Nextcloud asks you to confirm your password with its own dialog, and Cassini never sees, stores or sends it. The occ recipe is still there for an administrator who would rather run it, and is generated from the same plan Cassini executes, so the two cannot describe different setups.
  • Every missing prerequisite is named, together with the occ command that fixes it. Cassini and GET /operator/status list them all — the service account, each Nextcloud app, the Team folder, its group mappings, its ACL setting and its ACL manager.
  • A development or CI deployment can still declare the mode, with the CASSINI_STORAGE_MODE deploy option (default or access_controlled). It is not for production installs, and it is believed only where it fits: a declared mode whose prerequisites are missing, or that meets recordings in both folders, or the same recording in both, is refused at enable time and not recorded. It seeds the initial value only, and changing the variable never moves an archive that already exists.
  • Switching says what it would do before it does it, and can be re-run. The confirmation reports how many recordings would move and between which folders, whether the destination already holds any, and whether an earlier switch left a copy behind. Working that out is read-only, and nothing happens until you confirm. The switch itself copies the archive, checks every recording arrived, records the new audience, and only then empties the old folder — so whichever audience is in force, that folder holds a complete archive, and a switch interrupted by a restart, a timeout or a closed browser tab strands nothing and can simply be asked for again. Cassini names the folder that still holds a leftover copy, offers one action to clear it, and says when recordings are sitting in the audience that is not in force.
  • cassini meetings summarize <meeting.opus> ... adds a meeting summary to already-published portable files without re-running transcription: the transcript sealed inside each file feeds one LLM call — configured by the same LLM_BASE_URL / OPENROUTER_API_KEY / SUMMARY_* variables as cassini build — and the file is rewritten in place with its audio bytes untouched.
  • Files that already carry a summary are skipped; --force regenerates and replaces one, and --out writes a single file's result to a new path instead of in place. A backfilled summary is marked provenance.meetingSummary.source = "backfill" in the manifest.
  • You can find words inside the meeting you are reading. A search box above the transcript narrows it to the turns that match, shows how many of them there are, and clearing it brings the whole transcript back. It searches what was actually said, so it still finds a turn whose wording was tidied for display.
  • Administrators can tell Cassini the spellings transcription produces for names it gets wrong, in the app's settings beside the existing vocabulary: one name per line, its spellings separated by commas. A search for the name then also finds the recordings where it was written down differently, and each result says whether it matched what you typed or a known mis-hearing. Cassini ships a starting list; anything configured here is added to it, and replaces it for a name that appears in both. It changes no recording — the vocabulary setting is the one that biases new transcriptions.
  • You can now search across your meetings. Cassini indexes each transcript as it is published and answers published/search with the moments that match — the meeting, the speaker, and where in the recording they said it. You only ever see meetings you can already open.
  • Search knows the names transcription gets wrong. Looking for "cassini" also finds the recordings where it was written down as "casino", and each result says whether it matched your words or a known mis-hearing of them.
  • Every answer says how much it covered: how many of the meetings you can read were actually searched. A meeting Cassini could not index is reported as outside coverage rather than passing for one with no matches, and an unreachable archive is an error rather than an empty result.
  • Cassini's search index lives in its own search.sqlite3 file beside the job database and holds nothing that cannot be rebuilt from the published recordings. Deleting it is safe: it costs a rebuild and never touches recordings or job history.
  • cassini-operator backfill-search indexes meetings published before the index existed. It is safe to re-run and safe to interrupt, and it indexes a meeting only when the stored transcript matches the recording that was delivered.
  • Searches are rate limited per account. Each one asks Nextcloud what that person may read, so a client looping on search would put that load on Nextcloud rather than on Cassini; a burst is fine and a loop is asked to slow down, with a reply that says when to retry. The limit is per account, so one runaway client cannot lock anyone else out.
  • Pick several meetings in the Cassini app — across rooms, and the picks survive changing the room or the search — and Prepare hands you all of them as one document to copy or download. It is the same document cassini meetings context prints for the same meetings in the same order, byte for byte, because the app asks the same producer for it rather than assembling its own.
  • Before it hands anything over, Prepare says what the bundle will not contain: how many of the picked meetings have no summary, how many were published before the index recorded whether they have one — a different statement, and never counted as the first — how many predate the single-file format, and whether the word count it shows is a total or a floor. All of it is read from the published meeting index, so nothing is downloaded to answer it.
  • A meeting that leaves the archive while it is picked is removed from the selection and said so, rather than quietly shrinking the document you were about to hand over.
  • A portable export offers no Prepare at all. There is no operator behind an exported site to assemble a bundle, and producing a lookalike in the browser would be a second implementation of a published format.
  • GET /jobs returns room_name, the conversation's display name as it was when the job ran. The operator now stores the room as real columns on the job rather than only inside its Talk binding, and recovers them for every recording it has already made, so a room-scoped question is an ordinary query rather than a JSON parse. The conversation's token gets a column but is deliberately left out of the response, for the same reason the Talk binding has always been withheld from it.
  • The meeting list is organised by room. A rooms nav down the left lists every room the archive knows about with its meeting count, alongside "All meetings" and a "No room" group that keeps recordings without a room — an imported file, a simulated run, an old recording — visible rather than hidden.
  • Meetings are grouped under month headings, and each row now shows its room, its date, its speaker count and how long it ran.
  • cassini insight run asks one question of several meetings and writes the answer. It reads the context bundles cassini meetings context --json --out produces — one --context per bundle, in the order you name them — runs the summarise workflow over all of them at once, and writes the result to --out or to standard output. --model overrides the model for that run alone, and --timestamps shows the model where in each meeting something was said so it can cite it.
  • The document it writes carries its own provenance as frontmatter: which meetings it was produced from, with their rooms; the workflow's id, version and the content hash of the exact prompt bytes that produced it; the endpoint and model that answered; and when. A summary has always been able to say only that some model wrote it at some point, which is not enough to tell two documents from different prompt versions apart. --record writes the same record as JSON beside the document, so a grading harness reads one pair rather than parsing prose.
  • cassini insight run exits with a different code per outcome — no endpoint configured, the endpoint refused, the model failed or timed out, a bad request — instead of warning and carrying on. Skipping quietly is reasonable for a summary nobody asked for and never for a document somebody did. The codes are listed in cassini insight run --help. A failed run still writes its --record file, saying what was attempted and why it did not happen.
  • Transcripts now carry cross-track speaker attribution evidence. Every word records how far the loudest other participant's microphone sat above its own noise floor compared with the speaker's own, so crosstalk the decoder transcribed as speech becomes visible instead of shipping unmarked. Where a meeting's own level distribution shows an unambiguous crosstalk population, affected words are flagged lowConfidenceSpeaker; the canonical transcript keeps every word, and the evidence travels with them as attributionGapDb.
  • Generated summaries now exclude words flagged as probable crosstalk. A reader can see and overrule a marked word in the transcript; a summary has no such reader, and a fabricated interjection there becomes a decision somebody supposedly made.
  • Speech recognition now runs behind a backend interface, so a second engine can be added by registering a factory rather than by changing the pipeline. Select one with CASSINI_STT_BACKEND; the bundled sherpa-onnx decoder remains the default, and an unknown selection fails loudly rather than silently transcribing with a different engine.
  • The viewer now surfaces the crosstalk evidence the transcript carries. A turn built entirely from words the acoustic evidence attributes to somebody else is badged "probably crosstalk", and in the exact-word view individual flagged words are de-emphasised with a dashed underline and a tooltip saying how many dB louder the other microphone was. The words are still shown and still seekable — the reader can listen and decide.
  • Added a standalone waveform challenge miner that finds tricky meeting sections, including overlapping speakers, without rerunning speech recognition.
  • A published meeting now records how its word ends were decided, as provenance.wordTimings.endsBoundedByAudio in the portable-meeting manifest. When the key is absent, a consumer may repair an over-long word by clipping it towards the meeting median; when present, the end was measured against audio and must be preserved.
  • A Settings surface in the app, beside Browse and Operator (admins only). It hosts transcription quality — moved off the operator page — and the new LLM settings: endpoints, whether summaries run and on which endpoint and model (with a fetched model list). Each endpoint carries its own request bounds — timeout and response-token limit — with empty fields meaning the defaults (900s / 4096).
  • The app keeps its own LLM settings (llm-settings.json beside the job database; GET/PUT /settings/llm): named endpoints, and the summary policy. Switching between a hosted provider and a self-hosted model server is one settings change, with no ExApp redeploy. GET /settings/llm/providers/{id}/models lists what an endpoint serves. API keys are stored server-side and never returned.
  • Generate an insight from inside the app. Pick meetings in the browse list, open Prepare context, and the panel now offers a Generate card under Copy and Download: choose one of the templates this deployment ships and press Generate. The transcripts go to the AI endpoint you picked and the answer is written into your own Nextcloud files.
  • Insight runs report themselves honestly rather than as a spinner. A run appears in the card the moment you press Generate and carries queued, then running, then a result or a failure — a local model over five meetings takes minutes, and the card says so instead of implying seconds. A failed run names the cause in words you can act on ("No AI endpoint is configured", "The endpoint rejected the request"), carries Retry, and — for an administrator — links straight to the AI providers panel that fixes it. Retry replays the request as you made it, to the endpoint you picked; it falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — which is what makes adding an endpoint a fix rather than a suggestion.
  • Ask your own question. One shipped template, "Ask your own question", takes a question you type and answers it from the meetings you picked: the answer in at most three sentences, the evidence with who said what, and what these meetings do not answer. The question box appears for that template and for no other — every other template asks its own question, and a question sent to one of those would be dropped without being asked, so it is refused rather than silently discarded. cassini insight run gained the matching --question flag.
  • Insights are reachable from the browser: the manifest declares three new USER routes — insights (create a run, list your own), insights/<id> (read one with its document) and insights/<id>/retry. They are the first routes in Cassini a logged-in non-administrator may POST to, and they are USER rather than ADMIN because a run acts only within what that person could already do by hand: it reads the meetings they picked, as them, and writes the answer into their own Nextcloud Files. On upgrade an existing install picks the new routes up from the release itself — no re-registration needed, because every release bumps <version> (see below).
  • An insight you ask for in the app is answered by the deployment on your behalf and delivered into your own Nextcloud Files, under a Cassini Insights folder in your home. You own the file outright, so Nextcloud's own sharing works on it and nothing new decides who may read it. It is deliberately not filed under Cassini/ — that name is the shared recordings Team folder, which is read-only for everybody but Cassini's service account — and an insight is never written next to the recordings.
  • A document is never overwritten. Each file is named for the day, the template and the run it came from, and if something is already at that name the run writes the next free one and records which it used.
  • Every meeting an insight covers is fetched as you, over WebDAV, at the moment the run happens — so Nextcloud's own per-file permissions decide what is in the bundle, and a run can only ever be assembled out of meetings you could already open yourself. A retry re-checks that: a meeting you can no longer read stops the run rather than quietly dropping out of the answer.
  • A failed run says which of the five things went wrong in words that name the fix — no endpoint configured, the endpoint refused the request, the model did not answer, the answer could not be written, or the request itself — rather than reporting one general failure.
  • Cassini now answers published/meetings-list with the meetings the calling account may read, narrowed by an optional date range or room. The viewer and the cassini meetings CLI previously fetched the whole catalogue and filtered it themselves; the same list is now available already filtered, in the catalogue format both already read.
  • A date-filtered list reports how many meetings it dropped because their published date could not be read, so a short list is visibly partial rather than quietly incomplete.
  • Unlike the catalogue it is built from, this list never answers an unreachable Nextcloud with an empty result: a substrate failure is reported as an error, and an empty list means only that the account may read no matching meeting.
  • Cassini Admin's Transcription quality panel reports the device and model the next build will actually use, so a CPU run is stated up front rather than inferred from a failed build. The device override adds a CPU choice alongside Auto and CUDA.
  • cassini meetings context takes several meeting ids and prints one document holding all of them, so a question can be asked of a set of meetings rather than one at a time. The meetings appear in the order you named them, separated by a --- rule, and the --json form carries them as a meetings array whose every entry is exactly the single-meeting document the command has always produced. An id you cannot read — or one that does not exist, which is answered the same way on purpose — fails the whole run: a bundle that quietly dropped a meeting would answer a question asked of all of them using only some of them, and look right doing it.
  • cassini meetings context --timestamps cites each passage's start time, as MM:SS or H:MM:SS past an hour, beside the speaker. Off by default. The timings were already in the --json output and had no way of reaching the text an agent actually reads, so nothing asked to say where something was said could point at it.
  • The context bundle's format has a published schema, spec/cassini-meetings-context-v1.schema.json, and its output is pinned by fixtures. cassini.meetings.context.v1 was a version string with nothing written down behind it; now a change to what the command emits shows up as a diff in a checked-in file.
  • The published meeting index (catalog.json, and so cassini meetings list --json) now records whether each meeting carries a summary and how many words its transcript holds. Both come out of the file the publish step already reads, so anything listing meetings can say which of them have a summary, and size a selection of them, without downloading a single recording. Meetings published before this update carry neither field, which reads as "not known" — never as "no summary" or "no words"; republishing one fills them in.
  • The Cassini app can ask for a set of meetings as one document: GET published/meetings-context?ids=…,…, optionally &format=json and &timestamps=true. It answers with exactly what cassini meetings context prints for the same meetings — the operator resolves what you may read the same way it resolves the meeting list, fetches each recording from Nextcloud Files as you, and runs the CLI over them, so there is one implementation of the document rather than one per reader. An id you may not read is answered 404, identically to one that does not exist; a request that cannot be answered is 502, never an empty document. At most 20 meetings per request, because each one costs a whole recording download.
  • cassini meetings context --local ./Meeting.opus [...] reads portable .opus files off disk instead of fetching meeting ids from Nextcloud, and needs none of the connection settings. Each meeting's id is its file's basename without the .opus, which is how a published archive names one. --catalog ./catalog.json supplies the room id and room name, which live in the catalog rather than in the file — with it, a run over an archive's files produces the same bytes as a run over the same meeting ids; without it a meeting renders with the room id its file was tagged with and no room name. Ids travel as one comma-separated ids, not as a repeated id: AppAPI's proxy collapses repeated query parameters to the last value, which would have answered a three-meeting request with a one-meeting document and a 200. A caller reaching the operator directly may still repeat id.
  • Insight templates is a real panel in the Operator: it lists every template this build ships, and each row opens onto the exact instruction that template sends to the model — the prompt itself, not a description of it — beside the id, version and content hash an insight document records. So a document you found a month later can be traced to the prompt that produced it, rather than to a name that may since have meant something else.
  • A second template, "Commitments and owners": one section per person who spoke, with what they took on and when they said so, then what was assigned to somebody who never answered, then what nobody claimed. cassini insight run --workflow todos runs it, and every other workflow the panel lists can be named the same way.
  • cassini insight workflows prints the same list on the command line, and --json adds each workflow's instruction verbatim. It is how you check what a given image will actually send, without reading its source. The Operator's templates panel is this command's output.
  • LLM settings carry a second step, Insights, beside the meeting summary, so an insight can run on a different provider and model from the one that writes each meeting's summary — a small local model for every published meeting, a larger one for a question you ask by hand. An insight step with no endpoint of its own runs on the summary endpoint, so an existing install gains insights without configuring anything, and the settings panel says which of the two is answering. Each step also names the workflow it runs.
  • Per-step request bounds (SUMMARY_TIMEOUT_SEC / INSIGHT_TIMEOUT_SEC and the matching _MAX_TOKENS), because two steps on two endpoints cannot share one timeout: a CPU-bound local model needs a far longer leash than a hosted API.
  • Each step's workflow field is checked against the templates the image actually ships, and names them when the id is not one of them. The operator cannot know the registry — it lives in the recorder — so a typo used to save cleanly and only fail at the run.
  • An insight now keeps a record of itself from the moment it is asked for, moving through queued, running and then succeeded or failed. A run that takes minutes against a local model shows what it is doing rather than nothing, and a failed one says why it failed instead of stopping silently.
  • Retrying a failed insight is another attempt on the same insight, not a second one: it keeps its id, workflow, meetings, question and the endpoint you picked, and replays that request. It falls back to the deployment's configured endpoint only if you picked none, or the one you picked has since been removed — so fixing a missing key on your endpoint, or registering one where there was none, is enough to make the retry work. A retry pressed twice at once starts one attempt and refuses the other. A run the operator was restarted out from under is failed with a message asking you to retry it, instead of staying stuck on "running".
  • An insight now appears in the browse list beside the meetings it was drawn from, and under every room those meetings came from: a question asked of three rooms is found in all three, not filed under one of them. A Meetings/Insights switch above the list narrows to one kind or the other, and the last kind showing cannot be switched off. Insights that are queued or still running are listed too, saying so — an answer that takes minutes is visible while it is being written rather than appearing from nowhere afterwards — and a listing that fails says it failed instead of reading as "there are none".
  • Opening an insight shows the question it was asked, the meetings it read — each of which opens as itself, with the way back to the insight — the answer, and who ran it with which workflow and version. Every source meeting lists the insights that read it, so the link works in both directions. A meeting or an insight you may not read is simply absent from both, with nothing anywhere saying that it existed. The standalone viewer export, which has no operator behind it to have run anything, shows none of this and offers no filter for it.
  • GET operator/setup, which any logged-in Nextcloud user may read, now also reports what this deployment's AI configuration allows: features.summaries (a recording will be summarised) and features.insights (a question asked of a set of meetings will reach an endpoint). One bit each — never the endpoint, the model or the key, which stay administrator-only. It makes the promise in docs/privacy.md — that nothing leaves your infrastructure unless an endpoint is configured — checkable by the people whose meetings are being recorded.
  • The Cassini app now explains an unconfigured deployment instead of staying quiet about it: picking meetings on an instance with no AI endpoint says so in the Prepare panel, and an administrator gets a link straight to the AI providers panel that keeps the meeting they had open. Anyone who is not an administrator is told the fact and that only an administrator can change it, rather than being shown a control that would be refused. The standalone viewer export, which has no operator to ask, says nothing at all.
  • Pick the AI endpoint and model when you create an insight. Prepare now offers a Provider and a Model beside the template, defaulting to the first configured endpoint and its own default model. It is offered to everyone, not only administrators: which of the configured endpoints sees your transcripts is the asker's decision, so GET operator/ai/providers is a USER route carrying ids and display names — the base URL, the key and the request bounds stay administrator-only. A retry re-runs what you asked for, falling back to the deployment's endpoint only if the one you chose has since been removed. This adds a route, so the release that ships it must bump <version> in appinfo/info.xml — AppAPI only rewrites an app's route table on a version change.
  • "Ask your own question", the freeform insight template. Pick it in Prepare, type a question, and the answer comes back in three parts: the answer itself in at most three sentences, the evidence with who said what and when, and — never omitted — what these meetings do not answer. The machinery for a caller-supplied question already existed ({{QUESTION}}, Workflow.TakesQuestion); what was missing was a prompt carrying the placeholder. A template that takes a question is offered wherever one can be typed and withheld where one cannot: it is not offered as the publish pipeline's summary step, which runs unattended with nobody to ask.
  • The Participant and project vocabulary setting now guides the transcriber itself. Terms are applied as sherpa-onnx contextual biasing, so the recogniser can prefer "Librocco" or "Aire Spaces" where it used to invent a spelling. The vocabulary is a decoder prior rather than a text rewrite; its score therefore stays conservative so it does not overpower weak acoustic evidence.
  • provenance.speechToText.hints in the build manifest records how many terms were used, the boost, and the decoding method. When a vocabulary is configured but cannot be applied, it records that and why, so a build that ignored the setting never looks like one that honoured it.
  • CASSINI_STT_HINTS_DISABLED turns biasing off and restores greedy search, so an operator can get back to the transcription they had before; CASSINI_STT_HINTS_SCORE overrides the boost. Both are passed through the deployment compose file.
  • The meeting list's search box now searches what was said in your meetings, not just their names and dates. A matching meeting shows the moments that matched, and clicking one opens the meeting at that point with the transcript already filtered to those lines.
  • When search cannot answer — too many searches, an index still building, or an unreachable archive — the list says so instead of showing an empty result that would read as "nothing matched".
  • Cross-meeting search results now carry a short quote of what matched, so a result says why it matched rather than only where.
  • cassini annotate reads and writes the tags and marks a recording carries inside its own .opus file. show prints them. apply takes a batch of edits — tag the whole meeting or a stretch of it, remove a mark, remove a tag, undo everything one run added, rename a tag — and rewrites the file once, without touching the audio, the transcripts or the summary. Sending the same batch twice changes nothing the second time, and a batch that changes nothing does not rewrite the file.
  • cassini annotate carry moves the marks from a delivered recording onto a newly sealed one, so re-running a meeting does not lose them. When the new recording's audio differs, the marks are kept and flagged as made against different audio rather than redrawn on it.
  • A meeting's tags and marks can be read and changed at annotations/meetings/<id>, by people in the app, the CLI and agents alike. Anyone who can read a meeting can mark it, and every mark is attributed to the Nextcloud account that made it, never to a name the request supplies. The marks are written into the recording itself, so they travel with it when it is downloaded or shared.
  • Two people marking the same meeting at once no longer overwrite each other: a write that finds the recording changed underneath it re-reads the newer copy and applies its marks to that. A meeting you cannot read answers exactly as one that does not exist.
  • GET annotations/tags lists the tags on the meetings you can open, each with how many meetings and marks carry it, and says how many of your meetings it could read marks for. A tag that is only on meetings you cannot open never appears and never changes a count.
  • published/search and published/meetings-list take tag= (a label in any case, or a tag id) and answer only from meetings carrying a mark of that tag. Each search result also lists the tags whose marked stretches it falls inside.
  • Cassini keeps its tag index in its own annotations.sqlite3 beside the job database. It holds nothing that cannot be rebuilt from the marks inside the recordings, and cassini-operator backfill-annotations rebuilds it: recordings that have not changed are skipped without a download, and a fresh index keeps the tag namespace the archive already uses.
  • A recording can carry tags. A tag such as hiring can go on the whole meeting or on a stretch of it, and the marks live inside the recording's .opus file, so a meeting keeps its tags when it is downloaded or shared. The portable format and its schema describe them, and a reader that does not understand them still opens the recording normally.
  • cassini meetings tags lists the tags on the meetings you can read. cassini meetings annotations <id> shows one meeting's marks, and cassini meetings annotate <id> --ops <file> adds or removes them in one batch. Every mark records the Nextcloud account that made it, and whether a person or an agent made it, so an agent's whole run can be undone in one step.
  • cassini meetings search and cassini meetings list take --tag to look only at meetings with that tag. Against a Cassini app that does not offer tags, they fail instead of silently returning every meeting.
  • cassini inspect reports a recording's tags: how many there are, and whether they were made against this recording's audio.
  • You can tag meetings from the app. Each row in the meeting list shows its tags; tag one meeting from its row, or select several and tag them together. A filter beside the rooms narrows the list to one tag or several.
  • Inside a meeting, you can tag a section of the transcript: select its words (or, on a wide screen, drag down the rail beside it), choose Tag selection, and set exactly where it begins and ends with the handles in the text or the arrow keys. Tagged sections are underlined in their tag's colour and, on a wide screen, bracketed beside the text with the tag's name; arrows step from one to the next. The last tag you used is offered again beside Tag selection, so tagging several sections alike is one click each. On a phone, a section's tag sits in the text in front of it, and its details and the arrows sit above the player.
  • A tag has a colour and, if you want, an icon. You choose them where you create the tag, and change them in Manage tags, where tags can also be renamed, merged and deleted. A change reaches the meetings you can read; meetings in rooms you cannot open keep the tag as it was. Colours are stored for the installation rather than inside the recordings, so recolouring a tag rewrites nothing.
  • Each AI provider now carries a default model, set in AI providers. Summaries and insights use it unless the step names its own, and the Prepare panel's model field opens pre-filled with it, so a run can use another model without picking one every time.
  • A failed insight can be retried from its card in the browse list and from its document, not only from the panel that started it. A failed run now records only a reason (reason on the run, one of fourteen tokens); the app decides the wording, so the words can improve without touching stored runs.
  • One dialog, once per install. The first administrator to open a fresh Cassini is told that recordings, and the names of the rooms they came from, will be visible to anyone with an account on this Nextcloud, and given one button that creates the cassini Nextcloud account they are kept in. Nextcloud asks them to confirm their password, and Cassini never sees it; the account's own password is generated, set and discarded. The dialog is recorded as seen by the install rather than by the browser, and only once the account exists: an administrator who leaves it to change the audience first is asked again next time, and so is one whose install cannot create the account at all — where Cassini cannot create it, the dialog says so and points at Operator › Settings instead of claiming to be ready to record. On Nextcloud releases that let an app write to user administration, Cassini creates the account when it is enabled and nobody is asked anything.
  • Operator › Settings › "Who can see recordings", with two audiences and one sentence each: Everyone with a Nextcloud account and Meeting participants. The one in force is marked, and the line beneath says how many recordings already exist and who can see them, including after a switch, when the recordings from before it stay visible to everyone. Where recordings are stored, the service account and the occ recipe are behind "Details for administrators". Where the cassini account does not exist yet, the section says so and offers to create it, whichever audience is in force.
  • The meeting list says who can see the recordings in it, beside the meeting count: "Visible to anyone with a Nextcloud account" or "Visible to meeting participants". Administrators and everybody else see the same chip.
  • A switch shows its four steps while it runs, with a count of the recordings copied, and says the page can be closed; one that was interrupted offers Resume. Choosing Meeting participants without Team folders or Everyone Group offers to install them, and says that Everyone Group adds a group called Everyone to the whole of Nextcloud first. The confirmation says what the switch means for new and for existing recordings, in a number, and the direction that widens the audience carries that number in its own button.
  • Added linux/arm64 multi-arch support to the portable Cassini ExApp container image. Releases, pushes to main, and manual workflow runs now publish multi-platform images (linux/amd64 and linux/arm64) with bundled ARM64 sherpa-onnx runtime libraries and Parakeet ASR models.
  • Recordings a switch left visible to everyone can now be limited to the people who were in the call. Choosing "Meeting participants" on an instance that already has recordings copies them into the Team folder and leaves them readable by every account — Cassini had no way of knowing who was in a past meeting, and said so once, in the page that performed the switch. It now keeps that record while each meeting runs: the accounts, groups and teams with access to the room are captured when a recording starts and again when it stops, and frozen, so a room that gains or loses members later cannot change who may open a recording made before the change. Operator › Settings › "Who can see recordings" lists everything still visible to everyone, shows exactly who each one would be limited to, and limits the ones you tick. Groups and teams are granted as themselves rather than expanded, so reorganising a team is how access moves with it.
  • Recordings it cannot limit say why, instead of sitting there unexplained — recorded before Cassini kept the record, everyone in the call was a guest with no account to grant, or no job of this instance owns the file. Recordings of public conversations never appear at all: they are readable by everyone because the conversation was public. Anything needing a bespoke audience is still Files → Advanced permissions, and a recording you want left open can be dismissed from the list and brought back again.
  • The meeting list's search box now finds meetings by the names of their tags. Typing part of a tag's name brings up the meetings carrying it, alongside matches on the meeting's own name, date and what was said in it.
  • A recording published on its own — in a static export, or embedded in any page — now shows the tags and marked stretches that are inside it, read from the file rather than from a server.
  • <cassini-meeting src="…"> embeds one recording in any page from a single script tag, served from gocassini.com/embed/v1/viewer.js. The attribute contract is cassini-viewer/ATTRIBUTES.md.
  • An embed carries a "Recorded with Cassini" footer by default, linking to gocassini.com. Remove it with the hide-badge attribute.
  • Tag colours and icons are now saved in each recording's cassini.annotations.v1 document. Downloaded recordings and the public viewer retain their tag appearance without an installation-local lookup.
  • Cassini doctor and the operator /status diagnostic endpoint now verify the native speech engine runtime and report whether the Parakeet v3 reference frontend optimization (+cassini-parakeet-v3-reference-v1) is active. If an unpatched library is detected, a non-blocking warning is reported with guidance on how to enable the optimized runtime.
  • cassini dev meetings pull --out <dir> mirrors the meetings a Nextcloud account may read into a local seed pack — catalog.json plus meetings/<id>.opus, the shape a published site has always had. It pulls everything by default, --limit keeps the newest N, and --room/--from/--to narrow it the way cassini meetings list does. --dry-run reports the total size before committing to the transfer, and re-running skips what is already on disk at the size the server reports, so an interrupted pull is resumed by repeating the command. Every request is a GET or a HEAD; nothing is written back.
  • cassini dev stack up --seed <dir> loads a seed pack into the harness, and harness/bin/seed-nc-files.sh --pack <dir> does the same against a stack that is already running. A local development stack can now be brought up holding a real archive instead of a synthetic fixture, which is what the meeting list, cross-meeting search and insights actually need to be worked on. Seeding is a filesystem copy plus occ groupfolders:scan rather than a WebDAV upload, so 2 GB of recordings are copied and scanned in about four seconds, followed by a permissions pass of roughly half a second per meeting. The catalog is merged rather than replaced, so meetings a stack recorded itself survive a seed.
  • Seeded meetings are given an explicit read grant, and are therefore readable by every account on the stack, which the seeder says on every run: production's per-meeting permissions name production accounts and do not travel with the data. They also carry no operator job history. Neither affects the published surfaces, and both are stated so that no access-control change is tested against a seeded archive.
  • Recordings now carry a wall-clock anchor per audio track. Each track gains first_packet_wall_ms and first_timeline_ns MKV tags: two readings of the instant the track's first packet arrived, one in recorder wall time and one in meeting time. Because both come from the same monotonic clock, a later stage holding a wall-clock instant can convert it to a position on the meeting timeline exactly, without reopening the session artifact. The tags are emitted as a pair or not at all, and a recording that predates them reads as unknown rather than as the epoch. A third tag, first_rtp_timestamp, is recorded for triage only — Janus rewrites the timestamps it relays to each subscriber, so it is not the sender's clock and cannot place a sender-side time.
  • The selection bar's count opens a list of every meeting you have picked, including the ones the current room or search is hiding. Open a meeting from there, or unpick it.
  • The scripted transcript role, for authored text a recording is a performance of: a read script, a song's words. It is not a transcription, so it never names a source transcript.
  • Recording checks in Operator → Publish pipeline guide administrators through storage, processing, Talk authentication and handoff, followed by a real Talk recording and playback confirmation. Existing recordings remain accessible.
  • Persist the Talk internal secret securely on Cassini's volume, with deployment configuration taking precedence. Diagnostics expire and remain advisory; missing local credentials are reported before recording starts.
  • Provide installation-specific host commands and copyable provider requests, plus first-ExApp, ARM64/amd64 and AIO persistence guidance.

Changed

  • Skip standalone recorder integration jobs on PRs confined to the operator, frontend, microsite, or PR-conflict automation. Shared and unknown inputs still run the scenarios, and main pushes retain full coverage.
  • Storage that does not match the recorded audience is reported as a mismatch that refuses to publish and names both ways out. A Team-folder list Cassini cannot read counts as an unanswered question rather than as "there is no Cassini Team folder" — the folder list arrives with every folder on the instance in one response, so a single unrelated folder in an unexpected shape used to make the whole answer undecodable and indistinguishable from an empty one.
  • Cassini no longer builds Team folders or their ACL topology on its own. On every enable it checks what is there and reports what is not; it makes those changes only when you ask for them. A half-built setup can no longer end up looking healthy.
  • Talk refuses to start a recording when the storage it would be published to is missing a named prerequisite, instead of capturing the whole call and failing afterwards. The moderator sees Talk's own "The recording failed" message; the reason, and the command that fixes it, are in Cassini.
  • Installing the two Nextcloud apps is the one step Cassini still cannot take for you, and it now says so instead of implying otherwise. Nextcloud requires an administrator's password on that particular request, which Cassini declines to handle; it attempts the install through its own backend — which works on Nextcloud 32.x, 33.0.0–33.0.5, 34.0.0, and on any release where allowed_no_password_confirmation_ranges is set — and otherwise links you to Nextcloud's own Apps page. When the App Store refuses, the message says plainly that Nextcloud reports the same error whether the app is unknown, incompatible or the store is unreachable, and that retrying inside five minutes will say the same thing.
  • backfill-nc-files refuses to run where recordings are visible to anyone with a Nextcloud account, instead of writing access rules into a folder where they mean nothing. Everything it writes is protected by Team-folder permissions, and those recordings live in the cassini account's own folder where every signed-in account can already read them — so the migration would be at best a no-op with side effects, and at worst would re-derive an audience for recordings an administrator had deliberately left open. The refusal names the audience, writes nothing, and points at the setting that actually moves an archive between the two.
  • The recordings-permissions, install and privacy documents now describe both audiences. The upgrade section in particular had the Team-folder path collision backwards — a Team folder takes the Cassini path and Nextcloud renames the colliding home directory, rather than the mount being remapped — so its instruction to move the old folder out of the way first was unnecessary.
  • A keyless OpenAI-compatible endpoint now switches meeting summaries on: LLM_BASE_URL alone is enough, and OPENROUTER_API_KEY is needed only when the endpoint requires one. Previously both had to be set, so a self-hosted model server silently produced no summary. On upgrade, an install that had LLM_BASE_URL set without a key starts producing summaries through that endpoint from its next build.
  • With no key configured the recorder omits the Authorization header entirely rather than sending an empty bearer token, which some self-hosted servers reject. New request bounds for slow local models: CASSINI_LLM_TIMEOUT_SEC (default 900) and CASSINI_LLM_MAX_TOKENS (default 4096).
  • cassini meetings search says where a speaker id comes from — the speaker= value a previous result printed — so --speaker can actually be used. The ids are opaque and are not meant to be typed from memory.
  • The control panel names each recording after its conversation. A recording was labelled by its room token — Call 3qbs6123vxx — which identifies the call but not which conversation it was. Rows and the detail heading now show the Talk conversation's display name wherever the operator has it, keeping the token label as the fallback for a non-Talk recording, one whose room-name lookup never completed, and anything recorded before this release.
  • Opening a meeting slides it in over the list from the right, instead of replacing the list with a second column; on a phone it arrives as a bottom sheet. Close it with the × or Escape, and the list is still there, in the same place, with the same room selected.
  • The search box moved to the top of the list and filters within the selected room. It still matches a meeting's name or date, and the room and search narrowings each show as a chip you can clear.
  • A word can now be flagged lowConfidenceSpeaker only when its own track sits well below that speaker's own speech level (the quiet-owner signature). Genuine double-talk between microphones with different signal-to-noise ratios measures large gaps but is never flagged, and the crosstalk threshold is estimated on the quiet-owner population alone. The per-word attributionGapDb measurement itself is unchanged and still annotates every word.
  • The meeting manifest records what the attribution stage did: whether it ran, the mode (annotate/drop/disabled), words measured, flagged and dropped, and the estimated threshold when one was found. A build that deletes words in drop mode is no longer indistinguishable from one that never saw them.
  • CASSINI_STT_BACKEND, CASSINI_ATTRIBUTION_DISABLED and CASSINI_ATTRIBUTION_DROP are documented and passed through the deployment bundle; an unknown backend id now fails the build before any decoding or model download instead of after the mixdown.
  • CASSINI_ATTRIBUTION_DROP=1 removes flagged crosstalk words from the transcript itself instead of only marking them, and CASSINI_ATTRIBUTION_DISABLED=1 skips the measurement. Marking is the default: real crosstalk overlaps the level range genuine simultaneous speech occupies, so deleting canonical words on a threshold is opt-in.
  • The word being spoken now lights up instantly instead of fading in. The fade lasted about as long as a short word, so the highlight was always part-way through changing and never quite agreed with what you were hearing; it now tracks the audio exactly. Hovering a word to seek also highlights it immediately.
  • Overlapping speech reads as speech again. A turn is now one paragraph, however many pieces the producer cut it into: when two people talk over each other their sentences no longer alternate in one-to-three-word fragments. A short reaction someone made while another person was talking appears as a quiet chip where it happened, inside the sentence it landed in, and a turn that genuinely ran over another says so by naming the other speaker.
  • Durations are gone from the transcript. A badge reading "0.2 s during Ivan" on every fragment was precision nobody acts on, and it made the page harder to read than no marking at all. The measurements are still there for anything that wants them; they are no longer shown.
  • captions.vtt is now always produced, from the transcript itself. It used to exist only when an LLM endpoint was configured, as a by-product of cleanup.
  • LLM_BASE_URL, OPENROUTER_API_KEY and LLM_MODEL in the deploy environment now only pre-fill the LLM settings on the app's first start; after that the stored settings are what every build receives.
  • A non-administrator sees no template picker: the template registry is admin-only, so their insight runs whichever template the administrator configured, and the card says so rather than offering a control that would be refused. On a deployment with no AI endpoint the card does not appear at all — the existing "No AI endpoint is available" notice takes its place — and in a standalone static export, which has no operator to ask, neither appears.
  • docs/exapp-update-constraints.md answers the question it used to flag as unverified: an in-place app_api:app:update does rewrite AppAPI's route allowlist, but only when the manifest it is handed carries a new <version> — with the same version it short-circuits on "already updated" and the allowlist is untouched. Routes therefore behave unlike deploy environment variables, which stay creation-time. The rule for release review is that a release changing <routes> must bump <version>, which scripts/bump-exapp-version.sh already does. The measurement is written up with its evidence and is re-runnable as harness/bin/check-route-refresh.sh.
  • The privacy note now covers insights: a question sends the picked meetings' transcript and summary text to the configured LLM endpoint, the call uses the instance's API key — so the request reaches your provider attributable to the deployment, not to the person who asked — and the answer is written into that person's own Nextcloud Files, never beside the recordings.
  • The subprocesses an insight runs no longer inherit the operator's own credentials. The AppAPI shared secret, the Talk recording and signaling secrets and the operator API token are removed from their environment: none of them is read by any cassini command, and the AppAPI secret in particular is what lets a process act as any account on the instance. The step that only assembles the meetings loses the LLM credentials as well, since it never calls a model.
  • An insight is bounded in the three ways that can run away: how many meetings one may cover, how long one attempt may take before it is stopped, and how large the answer may be. Each limit is a refusal that says so, never a silently shortened document, and only two runs are performed at a time — the rest wait, and say they are queued.
  • The in-Nextcloud viewer now reads its meeting list from the app's list endpoint rather than the published catalogue file, so a Nextcloud outage no longer empties the list: it keeps showing the meetings it last loaded and says what went wrong, instead of silently rendering as though the account had no recordings. Standalone exports and the static viewer are unaffected, and the viewer falls back to the catalogue file against an app that does not serve the endpoint.
  • Transcription runs on the CPU when the host has no GPU. Operator builds resolve their device per run — CUDA when the image carries the CUDA runtime and an NVIDIA device is visible, CPU otherwise — instead of blocking every build on a GPU-less install. Recordings already stuck in build/blocked recover with Rerun in Cassini Admin; no image or deploy-daemon change is needed.
  • Each image carries the model of the tier it runs by default, and any other tier downloads once into a model cache on the persistent volume. The portable image bakes the 0.6B int8 model, the GPU image bakes fp32, and both stay small. A download survives a container recreate, and a host with no network egress gets a clear error that names the missing model. Cassini Admin shows the size of that download before the first build of the tier.
  • cassini meetings list now asks the app to filter by date and room, instead of downloading the whole catalogue and narrowing it locally. Against an app that does not offer that yet, it falls back to the previous behaviour, so an older CLI and a newer one both keep working.
  • cassini meetings list, rooms, fetch and context now report an unreachable recordings archive as a failure rather than as an empty result. Previously an outage was indistinguishable from "this account has no recordings" — or, when fetching by id, from "no recording you can read at that id".
  • Stop reasons and recording durations appear in run metadata. The operator view formats raw reason codes into plain labels and displays the total recorded duration in sidebar cards and detail panes.
  • cassini meetings context fetches the recording catalog once per run instead of once per meeting, so every meeting in a bundle is resolved against a single view of what you are allowed to read.
  • --keep-opus is refused when more than one meeting is asked for, and so is the same meeting id given twice. Both used to be silently meaningless; one file cannot hold several recordings, and the same transcript twice spends the context budget on one meeting while reading as coverage of two.
  • The publish pipeline's summary step and the summarise insight now run the same prompt file, not two copies of it. The summary is an insight like any other, so it names the template it runs; the bytes it sends are unchanged.
  • A prompt whose bytes are not compiled into the image is not listed and cannot be named — a template you can select is a template that will run. Two of the templates drawn in the design have no prompt authored yet and are therefore absent rather than shown as unavailable.
  • Editing a shipped prompt now fails the lint check until its version is bumped and its hash re-pinned. Prompt files are markdown, and a markdown-only change used to run no tests at all while changing what the product sends to a model.
  • CASSINI_SUMMARY_DISABLED no longer blocks cassini insight run. It turns off the summary written when a meeting publishes; an insight is a document you asked for by name, and turning insights off is removing the endpoint.
  • Configuration lives inside the Operator surface again, behind a left nav, instead of in a Settings tab of its own. Two tabs remain — Browse and Operator — and Operator now opens on a nav with two groups: Console (Recordings) and Settings (AI providers, Publish pipeline, Insight templates). The panels themselves are unchanged; only where you reach them moved. Below 720px the nav is a dropdown, and each panel opens at its own top rather than wherever the previous one was scrolled to.
  • A panel can be linked to directly: #surface=operator&panel=endpoints opens AI providers.
  • The run console splits into a list and a detail pane based on how wide the console itself is, not how wide the browser window is. In Nextcloud the app sits beside Nextcloud's own navigation, so a window wide enough by the old measure could still squeeze the run detail into a couple of hundred pixels.
  • Registering your first AI endpoint now switches meeting summaries on, pointed at it. An install that has just configured an endpoint and still publishes meetings without summaries has done the work and not got the feature. It fires once — on the save that takes a deployment from having no endpoint to having one — so an administrator who turns summarising off keeps it off, whatever they save afterwards. The AI providers page says so before you save, since it is a decision about what leaves your deployment.
  • AI providers configures endpoints and nothing else. Adding one is a form rather than a hidden row of fields, a saved endpoint reports whether its model list actually came back (and says so plainly when it did not, since an endpoint with no /models route still answers completions), and the page says which endpoint an insight will actually reach.
  • Publish pipeline is now the whole pipeline in the order it runs: detected hardware, transcription quality, summarisation, and the device override last. Summarisation moved here from AI providers — it is a step between the call ending and the meeting being published, and it was only living under endpoints because its endpoint is.
  • The summary step names an insight template from the registry instead of asking for a workflow id as free text, and its model field is a combobox that loads the endpoint's models when you open it rather than behind a "Load models" button. Typing a model the list does not have is still accepted: the list is the endpoint's, not a limit on what you may send.
  • The "this needs an AI provider" state is now the same compact card wherever it appears — the summarisation step, Insight templates, and the Prepare panel — with the button that fixes it. Anyone who is not an administrator still gets the explanation instead, because that panel would refuse them.
  • Create an insight in the Prepare panel is the template you pick and what it will ask, in one box: choosing a template shows the question it puts to the meetings and the shape of the document that comes back, or a question box where the template takes one. It opens on a real template rather than on a row reading "this deployment's default", which named nothing and read as a setting instead of a choice.
  • The Prepare panel no longer lists previous insight runs. It was a job console in a panel whose subject is the meetings you just picked, and a second place insights were listed — free to disagree with the browse catalogue behind it. The card now shows only the run it starts, which is feedback for a button press; every insight, that one included, is a card in the browse list as soon as it exists.
  • The publish pipeline's summary step opens on Meeting summary — the template it actually runs — instead of a "Default (the one Cassini ships)" row standing in front of it.
  • Meetings / Insights moved out of the list header into the rooms rail, under "Show", beside the other narrowing of the same archive, with a clearer gap between the two lists.
  • A meeting's header now carries its name, room, date, length and speakers, and stays put while the transcript scrolls under it. The insights a meeting was used in read directly under its summary rather than in a strip below the whole transcript, and an insight's own header carries its room, date and the model that answered.
  • Insight cards in the browse list are filled rather than outlined, so they read as a different kind of thing at a glance rather than only by the rule down their edge.
  • Upgraded sherpa-onnx Go bindings and native runtime libraries to v1.13.7 across the recorder and container base images.
  • Transcription output changes for every meeting, not only for the listed terms. Hotwords are only read under modified beam search, so the balanced and best quality tiers now decode with beam search instead of greedy search. It is a different decoder and it decides differently on marginal audio: measured on real meeting audio it lost a sentence on one clip and gained words on another. Decode cost is 1.3x to 1.5x, still far faster than realtime (worst measured realtime factor 0.091, so an hour of meeting decodes in about five and a half minutes on CPU). Memory is unchanged.
  • The fast quality tier is unaffected. It runs a CTC model, which sherpa-onnx cannot bias, so it keeps greedy search and records the vocabulary as not applied rather than ignoring it silently.
  • The vocabulary previously steered the LLM cleanup prompt and explicitly did not affect the transcript. It now affects the transcript and nothing else.
  • Decoder hints need a model bundle that ships bpe.vocab. Bundles without one record the vocabulary as not applied, and say so, rather than decoding unbiased in silence.
  • Re-running a meeting no longer erases the tags and marks added to its recording after it was published: the rerun carries them into the recording it delivers. Marks made against audio the rerun changed are kept and flagged as no longer matching, never dropped. If marks are being written at the moment of the rerun, the publish fails and asks to be re-run rather than overwrite them.
  • cassini-operator backfill-search recognises an archived recording that has gained marks since it was delivered as the same recording as the local copy — it compares the audio, not the whole file — and keeps indexing it from the local transcript's own segments.
  • The UI-only branch preview job is temporarily disabled (D-739). Every meeting in the published archive uses the exact-pcm audio integrity policy retired on 2026-09-02, so the export can read none of them and the preview failed for every branch on every push, over data no branch can fix. docs/runbooks/repair-published-archive.md is the repair procedure, and re-enabling the job is one line and part of closing D-739. Opt-in processing previews are unaffected: they build their meetings from raw recordings.
  • The endpoint's address stays with administrators. The insight document's provenance records the model and endpoint kind but no longer its base URL; an unnamed endpoint is shown as "Endpoint 1" rather than by its host; and a model list that cannot be fetched is reported to non-administrators without the URL.
  • Registering your first endpoint switches summarising on exactly once in a deployment's life. Removing every endpoint and adding one back no longer switches it on again for an administrator who had turned it off.
  • Removing an AI endpoint asks for confirmation first, naming the endpoint, that its stored key is destroyed, and which steps stop running on it.
  • Finding words inside a meeting now counts the matches, steps between them, and marks where they are on the rail beside the transcript. Hiding the turns that do not match is now a switch, off by default, instead of what always happened.
  • Generating an insight closes the Prepare panel and puts the new run at the top of the browse list, where it updates like every other insight.
  • The selection is bounded to the operator's 20-meeting cap before a request is made, with a sentence saying how many to unpick.
  • Cassini works out who can see recordings when the app is enabled, instead of refusing to record until it is told. A fresh install records as "everyone with a Nextcloud account", from the moment the cassini account exists. An install whose Cassini Team folder already holds recordings keeps "meeting participants", without asking and without moving anything, and recordings stranded in the other root are counted and offered a move. Cassini never widens an existing archive on its own, and "nobody has chosen yet" has stopped being a state an install can be in. A mode resolved this way reports mode_source: "resolved_on_enable"; a mode an older version recorded on its own, or an interrupted switch left behind, is kept and no longer needs confirming. Where Cassini cannot read the recordings it already has, it records no mode, says so on GET /operator/status, and looks again the next time the app is enabled; recording still goes ahead. The CASSINI_STORAGE_MODE deploy option is unchanged and still takes precedence.
  • A problem is one sentence now. Where storage is genuinely broken, Cassini says it cannot save recordings right now, names the cause in words, and offers Try again and Show details; the step, the paths and the commands live inside that disclosure. Somebody who is not an administrator gets the same sentence and no buttons, and nobody is told that an administrator has to decide anything. The cause is written by the operator, one sentence per check, so /status and the app say the same thing about the same failure. A check that has not run yet, as after a container restart, is a neutral band rather than a warning, and the meeting list stays where it is.
  • The storage API reports the install as well as the archive. GET /operator/storage carries first_run, answered once per install, and migration with the phase and counts while a switch runs. GET /operator/setup carries the mode, so the audience can be named for people who are not administrators. recordings_access on /status gains a cause sentence, and /setup carries the form of it that names no account, path or app.
  • The App Store description, the microsite and the documentation say who can see a recording before they say how it works, with the default first and the opt-in after it, in the same words the settings section uses.
  • The warning about widening no longer overstates how permanent it is. Switching to "Everyone with a Nextcloud account" said the recordings it opens will not be re-limited by switching back, which is still true and no longer the whole story: they can be limited again from the same settings section.
  • A public viewer is read-only by construction: the marks in a recording are drawn, and no control that would add or remove one is shown. Previously the viewer hid tags entirely when there was no operator to write them.
  • Tags that no vocabulary knows — every tag in a recording read on its own — are dealt distinct colours from the palette instead of each hashing its own id, which collided often enough to show several tags in one colour.
  • Rewrote the root README around the Nextcloud app: what you get, what leaves your server, the meeting file, requirements, install steps, and the CLI and agent skill.
  • Replaced the documentation site's pages with a set about installing and running the app, and added docs/cli.md for the CLI from a checkout.
  • Renaming or restyling a tag uses the existing visibility-scoped background rewrite flow. Only recordings the caller can read are updated; inaccessible recordings keep their saved appearance.
  • Graceful degradation on unpatched sherpa-onnx runtime. Parakeet v3 speech transcription no longer halts when running against an upstream or unpatched libsherpa-onnx-c-api runtime. Instead of failing recognizer initialization, Cassini logs an informative warning, gracefully falls back to the standard decode profile (defaultVADDecodePolicy()), and records referenceFrontend: false in provenance metadata. This keeps transcription available when the reference frontend is absent.
  • Cassini images now build the newest stable, officially signed FFmpeg source release selected at image-build time. CPU and CUDA image smoke tests gate the media capabilities Cassini requires instead of pinning a release number.
  • Release images now build with the FFmpeg version the current main image was already built and gated against, instead of resolving the newest upstream release again at tag time. Main keeps tracking the newest stable release, so an upstream regression still surfaces there first — but a release is never the first place a given FFmpeg appears, rebuilding a tag reproduces the same binary, and rolling back a release no longer silently rolls back FFmpeg with it.
  • Playback is shown in neutral rather than the accent colour: the word being played is inverted, and the play button, the scrubber and the Follow switch match it. The switch is called Follow at every width, turning it back on takes you straight to the audio, and the "Auto-scroll paused" badge is gone.
  • A meeting reads with less around it: its details sit on two lines under the title, Summary, Insights and Transcript share one heading, and the transcript's search counts its matches and steps between them inside the field. The meeting always leaves a strip of the list beside it to click back to.
  • The meeting list, the rooms rail and the Browse and Operator tabs follow the Cassini design. Dates name the day of the week, and who can open recordings is stated beside Rooms rather than among the filters.
  • The Operator's sidebar is a drawer on a phone, and its settings pages sit on the page rather than in nested cards. First-run setup asks who can see recordings as a choice, and its problems read as warnings with a way forward.
  • Prepare warns only about what stops it: a selection over the limit, or a meeting published in the older format.
  • Portable meetings identify their recording from canonical compressed Opus packets, making integrity checks independent of decoder implementation details.
  • Portable packing now converges on FFmpeg's stable Ogg end-trim granule before embedding that compressed identity, including multi-speaker mixes whose first metadata remux normalizes a few trailing samples.
  • Portable word transcripts no longer carry origin roles or source links. The packer, inspector, viewer, and static exporter accept entries with no role and ignore legacy origin metadata. Display transcripts still pair with their source words; withdrawn cleanup entries are skipped. The reference schema and format documentation also drop unused chapters and duplicate meeting hints.
  • Packed meetings use the published portable meeting format: CASSINI_FORMAT=org.cassini.portable-meeting/1, manifest "version": 1, and a resolvable CASSINI_PAYLOAD_SCHEMA served at https://format.gocassini.com/. Readers accept this contract directly and reject any other format.
  • Portable meeting producers and readers now implement only the published version 1 contract. Transcript bodies contain word items, the media digest comes from integrity.opusAudioSha256, and unsupported shapes are not repaired or upgraded.
  • Add opt-in recording-first scheduling: defer builds/sealing/publication while recorders are active, and yield running builds without spending resource retry attempts or losing captured media.
  • Serialize recorder finalization across subprocesses under recording priority.
  • Credit conservatively reclaimable cgroup file cache during memory admission, fixing waits caused by clean cache rather than memory pressure.
  • Add explicit concurrent/recording-first/auto policies. Auto permits recording overlap using live host/cgroup CPU and memory headroom, with a GPU-aware host CPU budget. Existing deployments retain concurrent behavior by default.
  • Simplified Go helpers and workflow context handling; shortened repetitive implementation comments.
  • Moved the removed Python transcriber's architecture notes into docs/history/.
  • Linux amd64/arm64 CPU builds now use prebuilt Parakeet v3 frontend libraries from codemyriad/sherpa-onnx-go-linux v1.13.7-cassini.4, with matching upstream v1.13.7 Go bindings. Normal developer, Docker and CI builds no longer compile sherpa-onnx. CUDA source builds consume a checksum-verified fork commit instead of applying a local patch. Other platforms retain stock libraries and the graceful fallback. Operator runtime diagnostics coalesce concurrent probes and refresh cached results, including unavailable runtimes, after 30 seconds.
  • Tag edits appear immediately after saving, while recordings update in the background. Delayed recording updates remain visible and can be retried.
  • Pending tag edits now survive operator restarts. Keep the annotation database on durable storage; rebuilding it from recordings cannot recover edits that have not yet reached those recordings.
  • Repairing a recording with the same audio restores saved tags. Replacing its audio clears the old annotations.
  • Recordings skipped during initial annotation import retry automatically and when listed or opened.
  • Large valid annotation documents can update their recordings; permanent recorder rejections show a blocked status instead of retrying indefinitely.
  • An older background status error no longer clears tags saved by a newer edit.
  • Rebuilding tag/search rows restores pending edits from the durable annotation database, even while the archive is unavailable.
  • Browsing the app prepares newly seeded recordings for tagging, imports wait for download capacity instead of being skipped, and bulk tagging automatically retries temporary preparation responses.

Removed

  • Word-level playback highlighting is gone from the meeting view. The transcript lit one word at a time and invited you to click a word to seek, which claimed a precision our transcripts do not carry. A line is the unit now: click any line to play from there. The "Exact words" toggle and the word/mixed/passage timing badge go with it.
  • The LLM readable-transcript cleanup step, which was shelved. Transcripts publish as transcribed, with speaker attribution and paragraph breaks but no model rewriting the words. Gone with it: CASSINI_READABLE_STRICT_BATCHES, the --strict-readable-cleanup build flag, the cassini-readable component, and the transcript.readable.v1.json producer. Meeting summaries are unaffected and remain the only LLM step in the pipeline. Files that already carry a readable transcript still open: the portable format keeps reading them.
  • The Model override field. It accepted only the three models the quality tiers already select, so it duplicated the tier control with no discoverability — and pinning an int8 model alongside CUDA produced a run that fragmented back onto the host CPU while reporting a GPU. Quality and device are now the whole of the policy. A model_override left in an existing settings.json is ignored.
  • The Insights step in AI providers. It configured "run insights somewhere other than the summary endpoint" while looking like the switch that turns insights on, which it never was. The field is still persisted and served, so PUT operator/settings/llm can still point insights at a larger model.
  • The Prepare panel no longer says that a meeting "does not record whether it has a summary". It is true of every meeting published before the catalog carried the field, it changes nothing about what the bundle contains, and it buried the one gap that does stop a bundle being assembled.
  • Removed the top-level ops/ directory; moved AppAPI registration helpers and batch processing scripts to scripts/, relocated production deploy tooling to codemyriad-systems, and purged stray out-of-scope host configuration.
  • The Setup tab and its wizard are gone. Cassini no longer opens on a question that blocks every recording until somebody answers it, and a link to #surface=setup now lands on Browse.
  • Removed an accidentally tracked harness executable; harness scripts continue to run its Go source.
  • Removed obsolete diagnostic reproduction tests; the production setup paths retain regression coverage.

Fixed

  • Manifest validation now rejects environment overrides whose names appear only outside environment declarations or inside XML comments, preventing AppAPI from silently dropping those values.
  • Tagging a selection of meetings now uses one atomic database batch and displays all updated rows together. Safe retries reuse the batch request ID; recordings continue syncing individually across the archive workers.
  • CI no longer fails when a third-party apt repository preinstalled on GitHub's runner image serves a bad package index. Every workflow install step now runs scripts/ci-apt-update.sh, which drops apt sources that are not the Ubuntu archive before updating, so a vendor we never install from cannot take a build down. On 2026-09-09 a corrupt Google Chrome index turned nine jobs across four workflows red on main with apt-get update exit 100; the previous remedy named packages.microsoft.com at three of the nine call sites, and this replaces it at all of them.
  • Restarting the Cassini container no longer stops it publishing until an administrator disables and re-enables the app. Cassini checked how Nextcloud is set up on the app's enabled edge, and that check was the only thing that could mark the storage usable — so a plain restart left it reporting an unknown state and refusing to publish, with nothing obviously wrong. It now re-checks at startup as well, for installations that have already recorded which storage model they use. A first install is unaffected: Nextcloud rejects the calls until registration finishes, so that case still waits for the enabled edge, which fires moments later.
  • cassini-operator backfill-search now indexes recordings this Cassini has no local copy of, by reading the published recording itself. Before, a meeting whose job record or working copy had been lost — which is every meeting published by an earlier installation, since the recordings outlive the app's own volume — was skipped entirely and silently absent from search. On the demo archive that was 25 of 30 meetings. Those are indexed at coarser granularity, and each result says which kind it is.
  • cassini-operator backfill-search now verifies a local transcript against the checksum the archive itself records for the delivered recording, instead of a digest from the job database that could never disagree with the local copy. A meeting whose local transcript belongs to a re-run that never published is detected and indexed from the published recording instead, so search only ever cites words that are in a recording you can play.
  • A read failure during backfill-search — a timeout, a busy Nextcloud — no longer removes a meeting's existing search entries. It is reported as failed and re-running is the fix; entries only change on evidence about the recording itself.
  • backfill-search no longer reads the job database at all, so it can run while the operator is publishing; the old database-is-locked failure is gone. Where the delivery recorded a checksum, a re-run now confirms an unchanged meeting with one metadata query instead of re-downloading the recording.
  • Speaker attribution now calibrates a participant's noise floor across all of their packet streams. A rotation or rejoin produces a fresh stream, and a short one carrying only ongoing speech had that speech as its own floor — so the real speaker measured as barely audible and a quiet track carrying only their bleed won the word by 20 dB.
  • Words keep their attribution provenance through LLM readable cleanup, so summaries actually exclude probable crosstalk. Readable cleanup and summarisation normally share one configured model, so losing the flag there made the summary protection a no-op in exactly the ordinary path.
  • Filtering no longer discards older segments that carry text but no word list.
  • A participant's noise floor is now the quietest floor any of their streams established, rather than a percentile over their pooled frames. Pooling was duration-weighted, so a long speech-only rejoin swamped the short stream that had actually seen the microphone idle, and crosstalk beat the real speaker again.
  • Readable cleanup maps each rewritten word to the single best-overlapping source word, so one contradicted word can no longer flag a legitimate neighbour and delete it from the summary.
  • Portable meetings now carry the per-word attribution evidence: cassini pack copies attributionGapDb and lowConfidenceSpeaker from transcript.words.v1.json into the portable transcript items (and cassini extract writes them back out) instead of stripping them from the published .opus.
  • The synthetic meeting generator now slides a turn that starts before the same speaker's previous one has finished, instead of aborting the whole run. Aborting happened partway through, leaving earlier participants generated and later ones missing — which is how the committed showcase fixture ended up with real speech for only two of its six speakers.
  • Transcription tests now build meetings with production's shape — late-joining tracks, DTX-sparse tracks, and rejoined streams sharing one participant id — instead of tidy tracks that all start together and are fully dense. The previous fixtures could not express the conditions that two thirds of production streams actually have.
  • The meeting list and header now show the date the meeting was recorded, not the date its file was last processed. Reprocessing an archive no longer restamps every meeting with the rebuild day.
  • Meeting list cards now show the meeting's start time alongside its date.
  • The viewer now shows when two people spoke at once. Overlapping speech used to render as ordinary consecutive paragraphs with no signal at all, and because a turn is cut at every speaker change, a listener's "right" in the middle of someone's sentence split that sentence across two paragraphs with an unrelated turn wedged between them — so a continuous utterance read as two turns and an interruption. An interrupted turn is now drawn as one turn containing the interjection, each turn carries a badge naming who it overlapped and for how long, and playback highlights every turn that is sounding rather than only the latest one to start.
  • Overlap is judged on the words that actually sound rather than on paragraph extents, and a word whose end ran past its own audio is pulled back before anything is drawn. Without that, meetings recorded before the timing fix would light up with overlap that never happened: across nine archived meetings their paragraphs intersect for 94.2 s while their words only sound together for 15.7 s. A meeting whose recording already measured where each word ends says so in its own file, and the viewer leaves its timings exactly as recorded — so a word somebody genuinely held for a second and a half stays that long.
  • A word now ends where its speaker's own audio ends. Parakeet stamps a trailing punctuation mark at the next acoustic onset, and the token-to-word conversion took that mark's timestamp as the word's end — so a sentence-final word could run for seconds while its speaker was silent, and across whoever spoke during that pause, which the viewer then drew as an overlap that never happened. The mark still travels in the word's text; it no longer decides the word's extent. Instead the energy gate that already decides whether a word is real follows the speaker's own track forward from the last spoken token and stops at the last active 10 ms of it, bridging a silence of up to 200 ms as a stop closure inside the word. A word built entirely from punctuation carries no audio of its own and is never extended, and no word may pass the end the old rule gave it. Measured against unmodified main on a 36-minute three-participant meeting (5549 words, the production fp32 model), the longest word falls from 4.07 s to 1.44 s and cross-speaker overlap stamped over a silent track from 4.18 s to 1.69 s, against 1.37 s of audible speech given up across the whole meeting, two words of it losing 100 ms or more. Those are whole-release totals; the final refinement alone — dropping an 80 ms grace after the last active frame — accounts for 0.38 s of that speech and for no word losing 100 ms.
  • Playing a long meeting no longer pins a processor core. The viewer rebuilt the entire transcript's highlight sixty times a second and re-checked every word on screen in order to move one of them, so on the longest published meetings playback ran at about twenty frames a second, the page stuttered while scrolling, and the highlight visibly trailed the audio it was meant to track. The highlight is now worked out once per transcript and touched only when the playhead actually crosses a word, which took the worst meeting in the archive from using most of a core to using under a tenth of one, at a steady sixty frames a second. Clicking a word to seek costs about half what it did on the same meetings. The longer you listened the worse it got, too: playback accumulated duplicate timers until it was running forty-four clocks where one was needed, so a five-minute listen was heavier per second than a one-minute listen. Memory and timer count now stay flat — measured over a continuous five-minute listen on the largest meeting.
  • A long answer is paragraphed again. Rebuilding a turn from the pieces the producer cut also undid the breaks it makes when somebody talks for a while, so one standup update rendered as a single 209-second block of text (the longest rows on that meeting were 209 s, 101 s and 97 s against a median of 8.7 s). The reading unit now follows those breaks where nothing came between two of the speaker's blocks, and overrides them only where somebody else's speech cut a sentence in half, which is the case this rebuilding exists for. Same meeting: longest row 58 s, median 10.4 s.
  • A quick back-and-forth is no longer folded into one turn. Rebuilding a speaker's turn now needs the person who spoke in between to have actually overlapped or touched it: if they said their piece in clean air and stopped, both people kept their own turn, in the order they spoke. Without that, five real turns of a fast exchange collapsed into one row with an answer rendered before the question it answered.
  • Playback follows the paragraph it is reading. It used to follow whichever producer fragment was sounding, so during two people talking over each other the page smooth-scrolled between the pieces of the same rebuilt paragraph every few hundred milliseconds.
  • The "probably crosstalk" warning is judged once over everything a turn actually said, instead of once per fragment. One flagged fragment no longer condemns a whole genuine turn, and a short reaction that is entirely flagged keeps its warning now that it renders as a chip.
  • The viewer now shows the meeting summary the recorder seals into the portable .opus. The summary was already inside the file (the summary.md attachment that cassini meetings reads), but the web viewer and the in-Nextcloud app never displayed it, so a meeting with a summary opened as transcript-only. It renders as Markdown in the existing Summary panel; a file without one, or with a damaged attachment, still opens without a panel rather than failing.
  • Selecting Fast now works on a CPU host. The pre-build environment check demanded transducer model files of every non-fp32 model, so the 110M CTC model — which ships a single model.int8.onnx — failed validation although it was bundled and correct.
  • An int8 model can no longer be pinned alongside CUDA. Those graphs fragment back onto the host CPU under the CUDA execution provider, so the combination reported a GPU run while doing something else; it is now refused when saved and when a build is admitted.
  • A stored device_override=cpu is preserved instead of being erased on every operator restart. Pinning cuda on a host without a usable GPU still blocks the build with an actionable message rather than silently using the CPU.
  • Filter values no longer appear in cassini meetings error output. The request URL is printed when a call fails, and its query string was not being masked.
  • Routine recording stops no longer trigger an error banner in the operator view. Successful runs with exit code 0 and normal stop reasons do not display the red error alert with internal runtime cancellation messages. The view displays an error alert only when a run or attempt actually fails.
  • Recordings from Nextcloud Talk display the call room token. The operator view reads the Talk base URL and room token from the trigger request. Sidebar cards and run details now show the call token and a direct link to the room instead of generic placeholder text.
  • A meeting link's timestamp survives a trip through the Operator surface. The shell writes its own hash params — the selected run, and now the selected settings panel — after the viewer's t=, and the viewer only reads t= when it is the last thing in the URL. So opening a recording at a moment, stepping into Operator, and coming back left a link that still named the moment but no longer jumped to it. The shell now keeps t= last, the way the viewing layer already does when it writes a link of its own.
  • Registering an AI provider is now the whole of the setup an insight needs. Before this, an administrator could save an endpoint and still be told "No AI endpoint is available" everywhere in the app, because an insight only resolved an endpoint through a pipeline step somebody had also switched on. An insight now resolves, in order: the endpoint its own step names, then the summary step's, then any registered provider — and the operator hands that resolved endpoint to the run, so what the settings page says and what the run receives cannot disagree. The rule that follows is stated in the AI providers panel and in docs/privacy.md: every endpoint you register is one an insight may reach, and removing them all is how insights are switched off. Summarising stays a separate opt-in that runs on every recording.
  • An insight document no longer opens with a paragraph of YAML. The recorder writes its provenance — which meetings, which prompt version and hash, which model — as front matter at the top of the file, and markdown renders --- as a horizontal rule, so twenty quoted keys arrived on screen above the answer somebody asked for. The provenance is still in the file, and the panel shows the same facts in its header and its "This run" list.
  • Hardened AppAPI registration lifecycle handling in scripts/lib-exapp-register.sh: stop immediately if unregister fails prior to re-registration, verify disabled status before re-enabling during enable cycling, reject arbitrary moving or non-release image tags by default, and require jq with valid manifest index parsing in preflight.
  • Restored word-level playback highlighting, hover and click-to-seek in the meeting viewer, including simultaneous speakers and inline interjections. Keyboard activation seeks to a focused word without also toggling playback; cleaned and untimed transcript text remains intact.
  • A search across meetings no longer lets one talkative meeting fill the whole page of results and hide every other meeting that matched.
  • A summary or insight the model stopped writing at its output limit is no longer recorded as a success. The run fails and says the answer was cut off at the model's output limit, so a half-written document is never published as a whole one.
  • The AI endpoint picker now works on a plain compose deployment too: the /ai/providers routes were mounted only under the ExApp base path, so the Prepare panel's provider list 404ed there.
  • Two insight runs can no longer run at once on one request: a retry whose row had been failed by the stale-run sweep and retried again used to start beside the newer attempt.
  • An insight stopped by an operator restart now says so and asks you to retry it, instead of claiming it took longer than an hour.
  • A finished insight whose outcome could not be written down is retried a few times before giving up, and if it still fails the log names the delivered document so it can be found.
  • A succeeded insight whose document you have since moved or deleted now says where it was, rather than "its document is empty".
  • Meeting-context refusals are never cached by the app proxy, and a meeting list that could not be read answers "unavailable" rather than "not one of yours".
  • The operator no longer logs a false "publish sink unavailable; using local" at startup. Recordings were always delivered to the configured destination; the message was wrong, and it pointed anyone reading the log at a storage problem that did not exist.
  • An insight run always asks the chosen provider for that provider's model; it can no longer inherit the summary step's model on a different endpoint.
  • When the AI endpoints or a model list cannot be read, the app now shows the operator's own error instead of a generic HTTP status. A provider base URL ending in /chat/completions or /models is refused with a hint to give the API root.
  • A non-admin's "No AI endpoint" card in the Prepare panel refreshes each time the panel opens, once an administrator has configured one.
  • The AI providers form warns when an OpenRouter key is pasted without its sk-or-v1- prefix, which used to save cleanly and then fail every run with a 401.
  • Finding more than one word inside a meeting now works. Typing a phrase used to return nothing at all.
  • Ctrl+F (⌘F) now opens the transcript find instead of Nextcloud's own search popup.
  • Narrowing the meeting list by room or tag now applies to search results too. Searching and then picking a tag used to leave meetings from outside the filter in the list, which looked as though the filter had stopped working.
  • Changing the room or tag while a search is active now re-runs the search instead of leaving the previous answer on screen.
  • Fixed selected tags being silently replaced by an existing case-insensitive label match. Meetings can retain multiple selected tag IDs, including legacy case variants, and the tag vocabulary now combines the same ID across recording namespaces. [D-773]
  • A meeting's header no longer writes down facts it does not have: a recording with no room said "No room", and one with no date showed its own filename under a calendar icon.
  • Applying an existing tag to another recording now copies its visible colour and icon into the new recording, including when tagging a selection. Existing local styles remain unchanged. Standalone viewers preserve saved tag colours and explicitly cleared icons. Annotation database upgrades preserve pending edits when moving from either the main or portable-style schema. The portable-file reader retains saved colour and icon fields, so embedded recordings show their archived appearance instead of fallback grey tags.
  • Guest participants in Nextcloud Talk meetings are now attributed under the display names they entered in the Talk UI rather than synthetic placeholder identifiers (such as participant-sXHFMabV). The recorder resolves decoupled Nextcloud room session IDs and actor IDs against active WebRTC signaling sessions, retains attendee display names delivered in presence updates, and upgrades early synthetic placeholder names when the participant's chosen name arrives.
  • Prevented FFmpeg 4.4 crashes when a participant audio track starts several minutes into a meeting.
  • Prevented repeated native decoder failures from filling the ExApp container with multi-gigabyte core dumps.
  • Kept readable truncated recordings buildable when FFprobe reports a non-fatal container warning alongside a valid packet timestamp.
  • Keyboard focus moves into a meeting, an insight or Prepare when it opens and stays there: the page underneath can no longer be reached with Tab or read by a screen reader. Their close buttons are larger.
  • Controls, borders, and hover and selected states stay visible in Nextcloud's high-contrast themes.
  • The meeting and insight drawers stay readable in Nextcloud's high-contrast themes.
  • Nothing in the list hides under the selection bar when the bar grows a second line, and an insights listing that failed reads as an error.
  • Reduce missing speech in Parakeet v3 transcriptions by matching the model’s reference audio preprocessing. Preserve detected utterances with surrounding recorded audio instead of splitting them every ten seconds and appending synthetic silence. Retain beam decoding and vocabulary hints.
  • Keep participant audio on the meeting clock after long mute gaps and codec pre-skip, including recordings with a nonzero container start time.
  • Fix duplicated overlap words when adjacent decodes disagree only on sentence-final punctuation.
  • Include the corrected native runtime in CPU/CUDA builds and the developer CLI.
  • Add recorded-audio comparisons and resumable full-meeting GPU validation tools.
  • Automatic participant-name hints no longer overwhelm uncertain audio. They use a capped decoder score of 0.5 while operator-entered vocabulary keeps its configured score, and each participant's own name is omitted from their track. This prevents short names such as “Silvio” from repeating hundreds of times while retaining useful spelling hints for other speakers. Published provenance records the participant-name policy and its effective score.
  • A portable .opus whose transcript body cannot be read is no longer called a broken file. That transcript is unavailable, named in a warning, and the meeting, the speakers and the other transcripts still read. cassini inspect still exits non-zero.
  • A repeated load-bearing tag now stops cassini inspect from printing any part of the manifest. It used to declare the metadata invalid and then print the title, the meeting id, the speakers and every descriptor anyway.
  • Transcript bodies are verified against the manifest's payloadRef rather than the CASSINI_TX_* tags that copy it. A corrupted body whose tag happened to agree with it was accepted, and a good body whose copied tag had drifted was rejected; a disagreement is now a warning.
  • The transcript a reader opens is the one cassini inspect marks default=yes, including in a file where no entry carries the flag and array order decides.
  • words= is the number of items read out of the chunk set, not the count the body declares.
  • Model ids containing _ no longer produce a transcript id the packer rejects, which let a bundle build and then fail at packing.
  • The exported transcript JSON keeps one segment per speaker turn. It used to collapse a multi-speaker transcript into a single segment with no speaker at all.
  • cassini inspect exits non-zero on unusable manifest metadata instead of printing a summary and reporting success.
  • Portable .opus files whose chunk sets carry base64url = padding now read. Producers write the encoding unpadded, but most base64url encoders pad by default, so a file written by another implementation was rejected outright as invalid metadata.
  • A transcript's chunk count and the default transcript now come from the manifest rather than from the CASSINI_TX_*_CHUNK_COUNT and CASSINI_TRANSCRIPT_DEFAULT tags that mirror it. A file whose two layers had drifted lost a transcript that was entirely present, returned a silently truncated one, or served a different transcript than the manifest names; the disagreement is now reported and the file still reads.
  • cassini inspect reports the word count it actually decoded out of a portable .opus, names any transcript whose body it could not read, and exits non-zero. It used to print the manifest's declared words=900 and cassini=ok for a file whose transcript was unreachable.
  • A meeting's words= is the default transcript's count rather than the sum across every transcript in the file. Alternative transcripts are further passes over the same speech, so a three-word meeting published with two passes was reported as six words.
  • Keep recording-setup edits responsive during background status refreshes and prevent older responses from replacing newly saved configuration.
  • Corrected installation and development guides for CPU transcription, the unified Cassini app, recording access, and optional summary/insight data sharing.
  • Fixed the app’s development proxy so the Operator section, settings and storage controls can reach a root-mounted operator API.
  • Fixed readiness requests and proxy path boundaries, rejected fallback/login responses in Operator detection, and stopped storage setup when its refreshed plan no longer includes the selected mode.
  • Replaced the homepage’s unsupported recording command with the documented CLI syntax.
  • Fixed concurrent atomic file writes by giving each writer its own temporary file.
  • Aligned architecture and API docs with the unified app and two-service Compose bundle, and corrected standalone publish examples.
  • The operator now hands its Talk recording secret to the recorder it starts. Since D-447 the operator generates that secret when the administrator sets none, and the recorder never received it, so on such installs Talk showed a working recording backend and every recording failed with "talk auth mode hpb-internal requires CASSINI_TALK_RECORDING_SECRET to be set".
  • Fix the embedded viewer showing no transcript for newly published portable version 1 meetings whose transcript bodies live in separate OpusTags chunks.
  • Build: the ExApp image installs the web workspace from the root package-lock.json with npm ci instead of resolving it afresh from the registry on every build. On 2026-09-03 the fresh resolution tripped an npm 10.9 bug (Cannot read properties of null (reading 'edgesOut')) on trees that had built minutes earlier.

Security

  • Search results never carry transcript text, and the search index cannot return the words it indexed. A search query is kept out of the operator's request log, where it would otherwise sit indefinitely.
  • A tag filter on the meeting list is kept out of the operator's request log, as search queries are.
  • A search term or tag no longer appears in the CLI's error output when the connection to Nextcloud fails before it answers.
  • Tags travel with a recording. Anyone you share a tagged recording with can see who tagged what, by Nextcloud user id.

Fixed

  • Fixed transcript reruns failing validation when overlapping decoder-window words end out of start-time order.

Fixed

  • Long speech turns admitted by the interjection-sensitive VAD are decoded in bounded overlapping windows, reducing Parakeet's silent omissions from 20–25-second utterances without reverting the short-interjection VAD thresholds. Dense merged-fallback windows also retain deterministic seam ownership when adjacent decodes disagree completely.
  • Production ExApp deploys now use collision-safe temporary manifest paths and clean them after settled runs, so a stale file from another operator cannot block an upgrade.
  • ExApp startup logs now report the effective publication sink instead of contradicting a resolved Nextcloud Files destination with a later local line.

Changed

  • GPU transcription upgrade: existing portable/plain-image ExApp installations continue to capture and preserve Talk audio, but operator-managed transcription now enters build/blocked instead of using CPU ASR. Install the matching -cuda image on a GPU deploy daemon, then choose Rerun for each preserved blocked job in Cassini Admin.

Fixed

  • Short interjections now retain their real timeline position and speaker attribution. Transcription drains final VAD audio, recovers more subsecond turns, and rejects near-silent decoder hallucinations without discarding speech whose decoded timestamp leads its waveform.
  • Transcript construction no longer applies duplicate timeline offsets or retains both sides of a mixed-track fallback, and genuine final words emitted in decoder or VAD padding are preserved at the real audio boundary.
  • Meeting duration now comes from playable audio without discarding source-container provenance or preallocating PCM from corrupt duration metadata.
  • GPU readiness failures are reported explicitly, and generated artifacts record the effective ASR device.

Added

  • cassini retag rewrites an already-packed .opus file's room and job fields without re-encoding its audio, verifying the result against the manifest's own PCM digest before writing it. It is what lets a recovered room survive a republish, and what the maintenance scripts below use.
  • A meeting's portable .opus now records which operator job and attempt produced it (meeting.jobId, meeting.attemptNumber, mirrored as CASSINI_JOB_ID and CASSINI_ATTEMPT_NUMBER). Both are optional; a file packed outside the operator has neither.
  • cassini inspect prints an origin line with a portable file's room, job and attempt — previously it decoded the room and printed nothing, so checking the producer chain by hand meant a raw ffprobe.
  • cassini meetings context includes the room the meeting came from, in both the markdown and the --json form.
  • scripts/backfill-catalog-rooms.sh gains --jobs-db PATH, --no-jobs-db and --no-retag; scripts/reattribute-catalog-room.sh gains --force, --jobs-db PATH, --no-jobs-db and --no-retag.

Changed

  • A recording's room is now recovered from the operator's own job history rather than guessed from its file name. The catalog entry's id is the job id, and jobs.talk_binding still holds the Talk room token for every job the operator ran — so scripts/backfill-catalog-rooms.sh derives the real room id for anything this installation produced, and falls back to a name-derived id only where no job row survives. Re-running it merges rooms an earlier pass split in two.
  • Both maintenance scripts now re-tag the published .opus, not just catalog.json. The exporter re-derives a catalog entry's room from the file on every republish, so a catalog-only change was silently undone by the next re-seal. Pass --no-retag to keep the old behaviour and accept that the change is temporary.
  • scripts/reattribute-catalog-room.sh refuses to move a meeting the operator has a recorded room binding for (new exit code 5, --force to override), including in a dry run. For those meetings the real room id is recoverable and the backfill is the correct tool; asserting a different one would leave a recording whose lineage and published room permanently disagree.
  • A room that got split in two is repaired by re-running the backfill, not merged by hand. 0.2.0-beta.2 said a split — from a pepper rotation, or from a recording made before that release — stayed until you merged it yourself. Re-running scripts/backfill-catalog-rooms.sh now re-derives every meeting whose job row survives and closes the split; only recordings imported from elsewhere still need scripts/reattribute-catalog-room.sh.
  • A meeting's room name now lives in catalog.json, not in the recording. 0.2.0-beta.2 announced the display name as something the portable .opus carries; it no longer does — the file carries the room id, and the operator writes the name into the catalog on every publish, preserving it across a republish. A display name is editable and a sealed recording is not, so honouring a rename used to mean rewriting every file that room ever produced; it is now one edit in a file that is rewritten anyway. The stamped name is the one that job recorded, not the room's current name — a republish restamps the same value — so what this buys is the ability to correct a name, not a name that follows Talk. The name at record time is still in the file, as the meeting title.

Fixed

  • A publish interrupted partway no longer leaves an unprotected recording behind: the next publish denies, removes and re-creates it.
  • A re-publish no longer resets a recording's access rules, so permissions changed by hand in Nextcloud Files now survive it.
  • An interrupted upload is no longer published as a silently truncated recording; the delivery fails and can be re-run.
  • A publish now fails instead of rewriting the archive when the meeting index cannot be read. A server error answered with a JSON body used to read as "this archive is empty", which replaced every meeting in the index with the one being published.
  • A room recovered by the backfill no longer reverts the next time its meeting is re-sealed or republished.

Security

  • Recordings are now given their access rules before they hold any audio. A recording used to be uploaded first and made private in a second request, so it was readable by every account on the instance in between — and permanently readable if that second request failed.
  • The meeting index gets the same treatment, in both places that write it: the one-shot Nextcloud Files migration used to upload the index of every migrated meeting — titles, room names, dates — before locking it to the owner.

Added

  • Data-processing & privacy note. A new docs/privacy.md documents what Cassini stores, where it lives, the single optional step (LLM cleanup and summaries) that sends data off your infrastructure, and what deletion and uninstall remove.
  • The App Store listing now shows what Cassini is: screenshots of the meeting viewer and the admin control panel, an ai category alongside integration, and user/admin documentation links. A screenshot-less entry reads as abandoned; the listing now presents the product.
  • Meetings now know which conversation they came from, and you can list and filter by it. Until now a recording's room name was flattened into its title and the room's identity was discarded, so there was no way to ask for "the meetings from this room". Every recording made from here on carries a room id and display name in its portable .opus and in the published catalog, and the CLI gains two ways to use that: cassini meetings rooms lists the conversations your account has readable recordings from, and cassini meetings list accepts --room, --from and --to (all optional, and they combine — --from 2026-08-01 --to 2026-08-31 means all of August). The agent skill for reading meetings now narrows by room and date before listing, instead of fetching the whole archive and guessing.
  • A new setting, CASSINI_ROOM_ID_PEPPER. The room id published on a meeting is a one-way derivation of the Talk conversation's identity, never the conversation's own token — for a public conversation that token is also the link that joins it, and every signed-in account can read those recordings. Setting this to a stable secret is what makes the derivation resistant to being reversed: a Talk token is short enough that an unpeppered one can be recovered by working through the possibilities offline. Choose it once — changing it changes every room id, and meetings already published keep the ids they were written with, so a room splits in two until you merge it again.

Changed

  • The Nextcloud App Store description now discloses that recording and transcription stay on your infrastructure and that only the optional external-LLM step sends data to a third party.
  • Recordings published before this update can be given their room back, from the files themselves. Run ./scripts/backfill-catalog-rooms.sh once, by hand, on the host where the app container runs; it reports what it would change and writes nothing until you add --apply. It recovers each meeting's room name from its published recording and derives a room id from that name, because those files never carried the conversation's token and it cannot be recovered from them.
  • Because of that, one conversation can appear as two rooms — one identified from its token, for recordings made since the update, and one identified from its name, for everything older. Cassini cannot prove they are the same conversation: two conversations can share a display name, and a room can be renamed between recordings. When you know they are the same, ./scripts/reattribute-catalog-room.sh --from <id> --to <id> merges them, and like the backfill it shows you every meeting it would move before it writes anything.

Fixed

  • Published recordings can be deleted again. Cassini created its Cassini Team folder with a Group Folders flag that, on recent versions, made every file in it permanently undeletable and un-moveable — by everyone, including the Cassini account and Nextcloud administrators. New installations no longer set it, and nothing else changes: who can read a recording is decided by the same per-recording permissions as before. An existing folder cannot be repaired automatically, because Group Folders offers no way to clear the flag after creation; the app's status page now reports the condition and what it costs instead of leaving it invisible.
  • Setting up recordings no longer fails on an installation that is already correct. On a Nextcloud that requires password confirmation for administration, an administrator has to create the recordings folder by hand — after which Cassini re-applied the same group and permission settings anyway, was refused, and reported the setup as broken even though nothing was wrong. It now checks what is already in place and only writes what is missing.
  • The app's status page no longer reports a completed setup as healthy when a step failed along the way.
  • Setting up the recordings folder now reports why it failed. The Group Folders API answers a refused request with HTTP 200 and the failure recorded only inside the response body, so Cassini read a refusal as a success and then failed while decoding it — reporting a JSON error that named neither the problem nor the cause. On Nextcloud installations that require password confirmation for administration, every one of these requests is refused, and the app's status page said only that setup was incomplete. Cassini now reports the actual reason and, when it cannot proceed, names the occ commands that create the Team folder by hand — which it then adopts on the next enable.
  • Setting up recordings no longer fails on Nextcloud installations that require password confirmation for user administration. Cassini creates a cassini service account and its group when the app is enabled; on Nextcloud 34 (and any instance enforcing password confirmation) those calls are refused, because an app has no browser session in which to confirm a password. An administrator can create the account and group by hand — but Cassini did not check whether they already existed before trying to create them, so a correctly prepared installation looked identical to an empty one and setup failed permanently. Cassini now looks first, adopts what is already there, and when it genuinely cannot proceed it names the occ commands that fix it.

Added

  • Cassini now makes the Talk signaling internal secret — the one input required for invisible HPB-internal recording that cannot be auto-provisioned — easy to know and set. When CASSINI_TALK_SIGNALING_INTERNAL_SECRET is missing, the operator logs an actionable WARNING at startup and GET /operator/status returns a signaling_internal_secret_hint, instead of only failing when a recording is attempted. The External Apps deploy-option description and docs/exapp-install.md now explain where to find it (e.g. docker exec nextcloud-aio-talk printenv INTERNAL_SECRET on Nextcloud All-in-One, or the signaling server's [clients] internalsecret on a standalone HPB) and how to set it.
  • Zero-config Talk recording secret: CASSINI_TALK_RECORDING_SECRET is now optional. When it is not supplied, the operator generates a secret on first start and persists it on the AppAPI volume (surviving restart and app update), so installing Cassini from the App Store no longer requires inventing or copying a shared secret. An explicitly supplied secret still wins and is treated as externally managed.
  • New ADMIN-only provisioning endpoint operator/talk/provisioning returns the ready-to-apply spreed recording_servers value (backend URL + secret), so an admin can register Cassini as Talk's recording backend in one step without ever handling the secret by hand.
  • The operator now delivers published recordings into Nextcloud Files. Each published meeting's portable .opus is uploaded before its entry is added to the catalog.json index under the canonical Cassini/Recordings/ folder. The in-Nextcloud viewer then serves its catalog and per-meeting audio authoritatively from Files (with its CSP and routes unchanged), while standalone development keeps its local source. Mutable catalog responses bypass browser caches, and the embedded viewer refreshes its meeting list on focus and every 15 seconds. Delivery failures are visible rather than silently masked by local fallback. Recordings are delivered as .opus only; Talk's recording /store endpoint (which rejects .opus) is not used. This first pass is public and writes as the Nextcloud admin; per-user/per-group access and a dedicated service account follow.
  • Recordings now belong to a dedicated cassini account, created automatically when the app is enabled. Previously they were owned by the Nextcloud administrator, which tied every recording to a human account that may be renamed, handed over or removed, and gave the identity holding all the recordings full administrative rights over the instance. Setup work — creating the recordings folder, its groups and the service account itself — is still performed as an administrator; storing and reading recordings is not. The account owns and serves the recordings; there is no other mode.
  • Per-participant access control for recordings. The operator freezes each recording's audience to the Talk room's participants at publish time — it grants read-only Nextcloud advanced-ACL (nc:acl-list) to each local participant (users, groups, circles) on the meeting's .opus; guests, email and federated participants are skipped. The in-Nextcloud viewer is then served per caller: the meeting list is built directly from catalog.json, filtered to the .opus files visible in the caller's own Nextcloud scan, and each recording is fetched as the caller so Nextcloud enforces access — a recording a user is not granted never appears and cannot be played. The read path fails closed (an empty list, never the full archive) on any error. Access is editable afterwards through Nextcloud's own sharing/ACL tools. Recordings are owned by a dedicated cassini service account, which also manages their ACLs; privileged setup is performed separately as a Nextcloud administrator. Two native prerequisites must be enabled first — the Team folders (groupfolders) and Everyone Group (group_everyone) apps — after which Cassini provisions the recordings folder and ACL topology itself. See docs/exapp-nextcloud-recordings-permissions.md.
  • Automatic setup of the recordings directory and its permissions. Enabling the Cassini ExApp now provisions everything the model needs by itself, over Nextcloud's HTTP APIs — no manual occ Team-folder setup. On the enabled edge the operator creates the Cassini Team folder with its default-deny advanced-ACL floor, gives Nextcloud's virtual everyone group read and the narrow cassini owner group full permissions, delegates cassini as ACL manager, migrates any per-recording permissions left by earlier versions, applies the root container ACL (so the owner can write and every account can traverse), and creates the canonical Cassini/Recordings/meetings collections. Because the audience group is virtual, every account has the mount from the moment it is created — there is no membership sweep and no convergence delay. Two native prerequisites remain, neither of which an external app can install for itself: the Group folders ("Team folders") app and the Everyone Group (group_everyone) app. The local harness enables both automatically.
  • The recordings setup now reports its own health. Setting up the recordings folder used to fail into a log line, leaving an app that looked healthy while showing nobody their recordings. GET /operator/status now reports whether the Team folder, its permissions and the universal everyone mount are actually in place, names the step that failed and the likely cause, and returns 503 when the app cannot serve recordings — so a missing Group folders or Everyone Group app is visible instead of silent.
  • The production deploy is now a committed, parameterised script (ops/deploy/deploy-exapp.sh) driven by an inventory file, instead of a one-off on a maintainer's laptop. It is dry-run by default, reads the live rollback target before touching anything, and refuses to deploy a moving tag (latest, latest-cuda, …) or an image whose child manifests are missing from the registry. CPU and GPU deployments differ by one inventory value, the daemon's compute device — there is no separate GPU path.
  • docs/exapp-update-constraints.md states what the Nextcloud admin UI's Install and Update buttons can and cannot deliver — most importantly that deploy environment is applied at container creation only, so a release adding a required environment variable is a breaking change the Update button cannot deliver.
  • A retention policy for per-run working files, --artifact-retention / CASSINI_ARTIFACT_RETENTION. Disk use grew with every recording and every rerun and nothing ever reclaimed it. The default (sealed) removes a completed run's working copies — all of which are duplicated in the operator's canonical library or were temporary staging — and keeps its sealed meeting file and every log. superseded only removes runs that a rerun has replaced; all keeps everything, as before — except a delivered run's staging copy, which is always removed once the recording reaches its destination so that no full copy of it is left outside the access model. Nothing removes the last copy of anything, and published recordings are never touched.
  • Installing Cassini now tells you whether recordings will actually work. Setting up the recordings folder used to fail into a log line, so an app whose setup had not completed still enabled, answered every request, and showed nobody their recordings. GET /operator/status now reports that setup under recordings_access as provisioned, unavailable (a named thing is missing — it says which app to install, or which setting to fill in), degraded (a setup call failed), or unknown (the app was restarted without being re-enabled), and returns 503 for anything that is not working. The two Nextcloud apps Cassini cannot install for itself — Team folders and Everyone Group — are now checked by name before setup starts, and reported individually.
  • GET /operator/setup — a USER-level route reporting whether recordings can be served (ok and state) and nothing else. It is what lets a non-admin be told that an install was never finished; the diagnosis (which step, which administrator, which paths) stays on the admin-only /operator/status. It answers 200 in every state, including the broken ones, so a browser can tell "Cassini is not set up" apart from "the app is down" — /operator/status remains the endpoint that answers 503 for monitoring. Existing installs pick the route up when the app is next registered or updated.
  • A one-shot migration for recordings published by an older version. Recordings made before Cassini stored them in Nextcloud Files live only on the app's own storage, and nothing publishes them again. ./scripts/backfill-nc-files.sh moves them across, run once by hand after updating; --dry-run reports what it would move without changing anything. Migrated recordings are readable only by the Cassini service account, because the audience a recording had when it was published cannot be recovered afterwards — grant access from the Files app, or use --public to make them readable by every signed-in account as they were before Cassini had access control. The migration changes nothing when Nextcloud Files already holds recordings, or when there is no older archive to move, so it is safe to run when you are unsure whether it is needed — and skipping it entirely is a supported choice: the old recordings simply stay where they are and everything published from now on works normally.
  • Read your meetings from outside Nextcloud: cassini meetings. An agent or script on a laptop or container can now list the recordings a Nextcloud account is allowed to read (cassini meetings list), download one as its single portable .opus (cassini meetings fetch <id>), or print one as readable context — transcript plus generated summary, as markdown or JSON (cassini meetings context <id>). It authenticates as a Nextcloud user with a Nextcloud app password, so no interactive browser login is needed and no new server route was added: the commands drive the app's existing per-recording read surface, which fetches from Nextcloud Files as the calling user. Nextcloud therefore enforces exactly the same permissions the in-Nextcloud viewer obeys — a recording you may not read is reported as not found, never as forbidden, so it never reveals that it exists. The surface is read-only; starting, stopping and deleting recordings remain administrator actions. context labels its transcript derived-from-words: a published meeting carries word-level timings, so paragraph breaks are inferred from pauses and speaker changes rather than editorially cleaned up. Requires ffprobe for context. Downloaded recordings are written readable by their owner only, since they hold a private meeting's audio and transcript. The CLI refuses to send your credentials anywhere other than the Nextcloud you named — it does not follow redirects, and it rejects a catalog entry pointing at another host. See docs/agent-meeting-access.md.
  • Working-file retention is now configurable on an installed app. CASSINI_ARTIFACT_RETENTION is declared as a deploy option, so administrators can choose to keep every per-run working file (all) when diagnosing a problem. The default is unchanged.
  • cassini meetings warns when --insecure is used with an app password, since disabling certificate verification exposes that password to anything able to intercept the connection.
  • Added explicit LAN media, signaling, and Talk callback configuration for running the full installed-ExApp harness on Docker Desktop for Mac, with fail-fast validation and a setup runbook.

Changed

  • Image publishing now verifies that every keep-forever tag — :latest, :latest-cuda, :latest-rocm, :cuda, and each :X.Y.Z[-cuda|-rocm] release pin — is actually pullable, failing the run instead of leaving a tag that resolves but points at a deleted manifest.
  • The operator/status talk block now reports secret_source (env | generated | unset) and the derived recording_backend_url (never the secret value), so admins can see at a glance how the recording secret was provisioned and what to register in Talk.
  • The operator now selects where published meetings are delivered with --sink / CASSINI_PUBLISH_SINK. The only sink today is local — the operator's own site root, which is what publishing has always done — and it is the default, so existing deployments need no change. An unrecognised name is rejected at startup with the valid names listed, rather than quietly publishing somewhere unintended.
  • The faithful installed-ExApp CI gate refuses its negative-test env hooks (D453_EXPECT_CONFIG_FAILURE, D453_FAIL_AT) unless D453_ALLOW_TEST_HOOKS=1 is also set, so a stray hook variable in a CI job fails loudly instead of quietly weakening the gate.
  • The demo sandbox now runs on Nextcloud All-in-One (AIO) instead of a hand-rolled docker-compose stack that reused the CI/dev harness. AIO provides Nextcloud, Postgres, the Talk HPB, and the reverse proxy — mirroring a real deployment. The sandbox no longer references anything under harness/. Cassini is installed/updated on top of AIO with a single idempotent sandbox/wire-cassini.sh (App Store release by default); one-time AIO + reverse-proxy + firewall setup is documented in sandbox/README.md.
  • Cassini's in-Nextcloud control-panel and viewer now render entirely through AppAPI's native embedded-UI mechanism, under Nextcloud's normal Content-Security-Policy. A stock Nextcloud + AppAPI install shows the admin control-panel and viewer with no modifications to AppAPI and no app_api integrity warning — the development-only CSP patch that edited AppAPI's signed proxy controller is gone.
  • Publishing a recording is now lightweight: cassini publish updates the meeting catalog and recordings only and no longer rebuilds or embeds the viewer on every publish. The viewer is served from the app image, so publishing is faster and no longer depends on the viewer build. Pass --rebuild-viewer (or set CASSINI_PUBLISH_REBUILD_VIEWER=1) to produce a self-contained static site with the viewer embedded, as before.
  • Upgrading needs no data migration and no manual step: recordings are stored in a group folder, which is shared storage rather than a personal one, so existing recordings stay where they are and remain accessible to the same people. Cassini installs the dedicated owner group's write mapping and then removes the legacy administrator-group mount and ACL-manager delegation after the service account has successfully managed the migrated root.
  • A recording that does not reach Nextcloud Files no longer counts as published. Delivery used to run after the publish was already marked successful, and its failures were only logged — so a job could report success with the recording nowhere in Nextcloud and the viewer showing nothing. Delivery is now part of publishing: if the upload, the catalog update or the per-meeting access grant fails, the publish fails, says which step failed, and a rerun re-attempts it.
  • An installed app now stores recordings in Nextcloud Files by default. CASSINI_PUBLISH_SINK selects the destination explicitly (nextcloud-files or local), and an installed app whose value is missing defaults to Nextcloud Files rather than quietly keeping recordings on the app's own volume. Selecting nextcloud-files without a usable Nextcloud connection now stops the app at startup with an actionable message instead of failing later, one recording at a time.
  • An installed app no longer keeps its own copy of published recordings. Recordings are stored in Nextcloud Files, but the app also retained a full copy of every meeting it had ever published on its own volume, outside the permissions that govern the originals. That copy is no longer produced: the working directory a publish builds is removed once Nextcloud has accepted the recording, and the app serves meetings only from Nextcloud. A recording the app cannot fetch is reported as missing rather than quietly answered from local disk.
  • Talk still receives recording status (started, stopped, failed) exactly as before, so the Record button, the room's recording indicator and failure reporting are unchanged. CASSINI_TALK_BACKEND_URL now covers status callbacks and OCS calls only.
  • Recording access now uses Nextcloud's virtual everyone group, giving new accounts the recordings Team-folder mount immediately without periodic membership reconciliation. Installers must enable both the Team folders (groupfolders) and Everyone Group (group_everyone) apps — they are the only two manual prerequisites; existing recording-viewers ACLs are migrated without changing whether a recording is private or public.
  • Whether a conversation is public is recorded at the time of the recording and stays with it. Making a conversation public later does not retrospectively widen recordings made while it was private, and making it private does not withdraw recordings people were already told they could see. If Cassini cannot determine the conversation's type, the recording stays restricted to its participants.
  • The recording-permissions runbook now states the access rule accurately: a recording is visible to everyone who had access to the conversation, including people invited who never joined, not only those present during the call.
  • Recordings are now access-controlled for everyone, with no way to turn it off. Per-participant access used to be an opt-in deploy option that defaulted to off, so a stock install shipped an archive where any logged-in Nextcloud user could browse and play every recording. That is gone: an installed Cassini app serves each person only the recordings Nextcloud says they may read, and there is no public-archive mode to fall back to. Nothing to configure — the app sets up the recordings folder and its permissions itself when you enable it. The two things an administrator still does are enable Nextcloud's Group folders ("Team folders") app and the Everyone Group app, neither of which an external app can install on its own.
  • The admin deploy option CASSINI_NC_ACCESS_CONTROL and the local harness's --nc-access-control switch were removed along with it. If you set either, drop it; nothing replaces it.
  • The AppAPI register logic is now one implementation (ops/deploy/lib/exapp-register.sh) shared by the sandbox, the harness install phase and the production deploy, so the rules that protect the recording archive hold everywhere: unregister before re-register, never pass --rm-data, and force PUT /enabled so the navigation entries come back.
  • The ExApp install guide now documents the /exapps/* reverse-proxy route to HaRP. It is required for every HaRP daemon and is what actually blocks an install when missing — the symptom is an app that reports [enabled] with no navigation icon, which points nowhere near the reverse proxy. It must use Caddy's handle, not handle_path, so the prefix survives.
  • The operator's job pipeline has a fourth stage, seal, between build and publish, with its own timestamps, log and progress step in the control panel. Publishing is now reached only through a completed seal.
  • A recording is no longer published when the recordings folder is not set up. Without it, recordings were written into a private folder belonging to Cassini's own service account: the upload succeeded, the job reported success, and nobody could ever open the result. Publishing now fails with the reason and points at GET /operator/status; a rerun works once the cause is fixed. This also applies after a container restart until the app is re-enabled, because setup runs when the app is enabled and nothing has verified where recordings would land until it does. To keep recordings on the app's own volume deliberately, set CASSINI_PUBLISH_SINK=local.
  • The install guide now lists both required Nextcloud apps under Prerequisites, documents every deploy option, and explains how to read the setup report.
  • An unfinished install now explains itself in Cassini, instead of failing with HTTP 502. When the recordings folder is not set up, opening Cassini no longer shows a failed meeting list — it shows what is wrong, and it says something different depending on who is looking. An administrator gets the step that stopped: the missing Nextcloud app named by name and id with the occ app:install line for it, or CASSINI_NC_ADMIN_USER when no administrator could be resolved, or the container log to read when a setup call simply failed and there is nothing to install — plus the disable/enable pair that re-runs setup, and the same sentence the container log carries. Everyone else is told that Cassini is not set up, that it is not their account, and is given a link to this Cassini page to send to an administrator — who, opening it, sees the instructions. Which message you get is decided by the same boundary that decides whether you see the Operator tab at all.
  • Where the message goes depends on whether recordings can still be read, which is not the same question as whether setup finished. When nothing is mounted, it replaces the meeting list, because the list would otherwise be an error or an empty lie. After a plain container restart — where setup has not re-run, so publishing is refused, but every recording already published still opens — it is a strip above the list and the list stays.
  • Enabling or updating the app no longer re-uploads your whole recording archive. It used to copy every recording it had ever published into Nextcloud Files again on each enable, install and update, under a fixed time budget for the entire batch. Past a certain archive size that ran out of time before it could update the recording list — and because the list is written last, it stayed frozen at its previous contents permanently, with new recordings delivered but invisible and nothing said about it. Recordings have gone straight into Nextcloud Files as part of publishing for some time now, so nothing depended on this any more.
  • All three Go modules now declare the same minimum Go version (1.24), matching the toolchain the release images have always built them with. Previously two modules declared 1.22 and one declared 1.24, so the version a module was checked and formatted under depended on which module it was.

Removed

  • sandbox/compose.sandbox.yml, sandbox/deploy.sh, and sandbox/destroy.sh — the harness-coupled compose sandbox they implemented is superseded by the AIO-based sandbox.
  • Cassini no longer uploads recordings to Nextcloud Talk. Every Talk-button recording used to file a raw .mkv into the room owner's /Talk/Recording/<token>/ through Talk's recording-store endpoint, in addition to publishing the meeting as a portable .opus. That second copy sat outside Cassini's canonical recordings root and outside its per-file access controls, so a meeting could be readable there by people it was never shared with. The upload is gone — including the rerun path that re-attempted it — and a meeting now reaches Nextcloud exactly once, as the published .opus.

Fixed

  • CUDA image tags (:X.Y.Z-cuda, :latest-cuda, :cuda) can be pulled again. Image pruning had deleted the manifests those tags pointed at, so docker pull failed and a CUDA deploy daemon silently fell back to the CPU image — running transcription without GPU acceleration and reporting no error. Tags published before this fix stay unpullable until republished; :latest-cuda and :cuda repair themselves on the next push to main.
  • cassini dev stack up --resume now recreates containers and mounts the existing Compose and ExApp data volumes after a normal stack down, instead of requiring the removed containers to still exist.
  • Publishing a recording no longer re-exports the whole library. Every publish used to rebuild and rewrite every meeting in the archive, so the time it took grew with the archive — on a production instance with 67 meetings that was roughly seven and a half minutes per recording, during which the job sits at publish / running and looks stuck. A publish now exports only the meeting being published and merges it into the live site, leaving every other recording's files untouched. Re-publishing a meeting (for example via a rerun) updates its entry in place rather than adding a second one.
  • The three-songs harness sync check now fails loudly when the recorder report is missing or contains fewer than 2 composed sessions, instead of silently skipping; the recorder run now writes the report the check needs.
  • The recorder now rejects --request-offer-interval 0 at startup instead of silently disabling every capture-recovery path, which could leave an empty recording for the whole call after a single dropped requestoffer.
  • Setup no longer assumes the administrator is called admin. Cassini probes for one — the configured account, then admin, then the instance's own account list — so instances that name theirs differently can complete setup. If none of them is an administrator it says so and stops, rather than continuing as an account that may not exist; CASSINI_NC_ADMIN_USER names one directly.
  • Publishing a recording no longer rewrites the whole Nextcloud Files archive. Each publish uploads its own recording and adds it to the shared catalog, so a delivery that fails can no longer remove other meetings from the list.
  • A recording you are not allowed to see now reads as "not found" rather than "forbidden". Reporting a denial told you the recording existed, which is exactly what the access model is meant to prevent. Genuine outages are still reported as errors, and denials are still distinguishable in the operator's logs.
  • The internal site manifest (cassini.json), which lists meeting counts and job ids, is no longer readable by any signed-in user of an installed app.
  • The viewer no longer treats an unreachable meeting list as an empty one. A Nextcloud outage now surfaces as an error instead of an empty archive.
  • Recordings of public conversations are now readable by everyone, as you would expect. A conversation anyone with the link can join was treated exactly like a private one, so its recording was restricted to whoever happened to be on the attendee list. That was worst for the most public meetings: participants who joined by link rather than by invitation cannot be granted access individually, so a public meeting full of link guests produced a recording only its owner could open. Recordings of public conversations are now visible to any account on the Nextcloud.
  • A newly created user can now see public recordings straight away. The recordings Team folder now uses Nextcloud's virtual everyone group, so every account has its mount from creation instead of waiting for a periodic membership sweep. A new account sees everything it is entitled to on its first Cassini visit.
  • A public recording no longer loses its broad access when the app restarts. A periodic integrity sweep re-applies the restrictive permissions to any recording that is missing them, and it mistook a public recording's deliberately broad grant for a missing restriction — quietly turning every public recording back into a participant-only one. The sweep now leaves a recording's own permissions alone, whichever way they were set, and only repairs recordings that carry none at all.
  • A momentary Nextcloud Talk hiccup no longer costs a recording. Working out who a meeting belongs to happens once, at publish time, and now gates publishing — so a single failed request used to fail the recording outright, recoverable only by re-running the job by hand. The lookup now retries, and then falls back to asking as the recordings owner, which is the case retrying alone cannot fix: the person who started the recording may have left the conversation, been removed or been disabled between the meeting and its publication. A meeting whose participants are all guests or federated users still publishes successfully and stays visible to its owner only.
  • The recordings list no longer gets stuck on "Meeting not found". Opening the viewer before its first recording exists — or during any brief Nextcloud hiccup — could leave the right-hand pane showing an error that stayed there even after the list loaded, until you clicked a meeting or reloaded. A link to a specific meeting opened in that window was also dropped instead of opening once the list arrived.
  • bin/cassini no longer leaks a ~17 MB temp directory on every invocation (about 3.7 GB/day on a dev VM running the harness). The wrapper now builds the CLI to a stable path under ${XDG_CACHE_HOME:-$HOME/.cache}/cassini/bin and reuses it, so there is nothing to clean up; concurrent invocations stay safe via an atomic rename, on macOS too.
  • A recording is no longer published before its downloadable meeting file exists. The portable .opus — the single-file meeting you download, share and archive — used to be produced in the background after the recording had already been handed off for publishing. If that step failed, was interrupted by a restart, or was overtaken by a second run of the same recording, the recording could still be reported as finished while its meeting file was missing, stale, or belonged to a different run. Packing that file is now a stage the recording has to pass: it is written once, checked, and fingerprinted before publishing starts, and the file that reaches the viewer is verified to be that exact file. A failure now fails the recording, with the reason recorded and a rerun available, instead of disappearing into the log.
  • Re-running a recording can no longer overwrite the previous run's meeting file. Each run now writes its own, so the two cannot race.
  • A failed publish can no longer leave the recordings page without its viewer. Refreshing the site's viewer files used to delete them before copying the new ones in; an interrupted copy left the page listing recordings it could no longer play. The new files are now staged beside the old ones and swapped in, so a failure leaves the previous, working page exactly as it was.
  • An interrupted replacement can no longer destroy a meeting file. cassini pack removed the previous .opus before moving the new one into place; it now replaces it in a single atomic step.
  • Setup no longer assumes the administrator is called admin. Cassini used to look up the administrator with a request made as the administrator it had not found yet, so on an instance whose administrator is named anything else the lookup failed, it carried on as admin regardless, and every subsequent step was rejected with nothing explaining why. It now asks Nextcloud who the accounts are and finds an administrator among them; if it cannot, it stops and says so, naming CASSINI_NC_ADMIN_USER as the way to point it at one.
  • A recording whose permissions could not be applied is no longer reported as published. Nextcloud accepts a permission change on a file outside a Team folder with a success code that hides a per-property rejection, so the recording could be stored with no restrictions at all while everything looked fine.
  • An administrator whose install was broken no longer loses the Operator surface. Cassini decides whether to show the operator by asking the admin-only status endpoint, and that endpoint answers 503 when recordings cannot be served — which was being read as "not an administrator". The one person who could fix the install saw the same browse-only app, and the same unexplained error, as everyone else.
  • Catalog meeting dates now come from the pack's recorded createdAtUtc (rendered in UTC), so meetings whose file name carries no timestamp — e.g. backfilled dailies — show a real date and sort correctly instead of echoing the raw id. Already-published archives pick this up on their next export.
  • Portable-meeting documentation was brought back in line with the format emitted by that release.
  • A recording delivered to Nextcloud Files is now verified to be the file the recording actually produced. Cassini fingerprints each meeting when it is packed and re-checks that fingerprint before publishing; the check was applied when recordings were stored on the app's own volume, but not on the Nextcloud Files path an installed app uses. A recording whose file does not match what was packed is now refused before anything is uploaded, so a half-delivered recording can no longer appear in the archive.
  • The standalone operator container no longer risks clearing more than its own site directory when started with an unusual CASSINI_OPERATOR_SITE_ROOT.
  • The recordings migration script (backfill-nc-files.sh) now runs on macOS's system bash, where invoking it with no options previously aborted.
  • The local test harness now stops with a clear message when Nextcloud Talk fails to install, instead of continuing and failing much later with an unexplained error.
  • Fixed harness startup under the macOS system Bash when optional Compose or Nextcloud command arguments are empty.

Added

  • Added a unified cassini dev stack up|down|status|plan workflow with explicit --services topology modes, including support for remote harness runs and a visible stack plan.
  • Restored the operator (recording-control) UI inside the single "Cassini" Nextcloud app as an admin-gated surface (D-420 V3): admins now get a Browse | Operator top nav, while non-admins keep the browse-only experience unchanged. The active surface is deep-linkable and back/forward-navigable via the URL fragment (#surface=operator), with no pathname router. Whether the operator surface is shown is decided by probing the operator API (403 → hidden); the operator JSON API stays admin-only in info.xml (the real security boundary), so a non-admin can never drive recordings even if they force the surface.
  • Added negative controls (harness/bin/test-session-artifact-packet-guard.sh, harness/bin/test-av-drift-pair-requirement.sh) that feed the real verifiers synthetic artifacts and assert they go red on captures with no media and green on healthy ones. They run on every pull request in the fast lint job — no Docker, no stack, no media tooling — so the guards' behavior is demonstrated rather than assumed.
  • cassini dev stack plan, up, and destructive down commands now surface non-fatal warnings for ignored settings, surprising recording paths, unreachable private media addresses, and resource-removing lifecycle options.

Changed

  • Standardized dev and CI harness execution around the same stack commands so e2e entrypoints and manual flows now use explicit, deterministic startup/topology flags.
  • Reworked the viewer to load meeting data through a single DataProvider seam instead of hardcoded static fetches, decoupling the viewer UI from its data source (groundwork for unifying the viewer and operator into one in-Nextcloud app). No user-visible behaviour change.
  • Collapsed the two Cassini Nextcloud top-menu entries ("Cassini" viewer + "Cassini Admin" control-panel) into a single "Cassini" entry backed by one in-Nextcloud app (cassini-app, which consumes the viewer as a layer). Every logged-in user gets the meeting-browsing app; the operator/recording-control surface returns as an admin-gated surface inside the one app in a follow-up (D-420 V3), and the operator JSON API stays admin-only throughout (unchanged security boundary). No change for viewer users.
  • Split the in-Nextcloud viewer into a shell (App) plus reusable MeetingList and MeetingView components — the app-shell layer for unifying the viewer and operator into one in-Nextcloud app (MeetingView is also the future standalone single-meeting share surface). No user-visible behaviour change.
  • Release CI now verifies that the exact CPU ExApp image can be installed through AppAPI/HaRP, record a Talk call, and publish a non-empty viewer transcript before image or harness changes merge.
  • Reworked the Cassini operator panel for the in-Nextcloud audience it is now shown to: the recordings list and run detail work as a master/detail pair, each run's attempts collapse into accordions so a long retry history opens at a readable summary, starting a recording is an on-demand composer rather than a permanently parked URL field, and the API-implementation captions are replaced with plain descriptions.
  • Rebuilt the connection indicator to state the connection first: a one-word status (Live / Connecting / Reconnecting / Offline) with the refresh cadence beside it, replacing a label that never said whether the panel was connected.
  • Restyled the transcription settings as three matching sections — detected hardware, quality, and device and model overrides — shown side by side on wide screens, with the overrides no longer hidden behind a collapse.
  • You can now link directly to a recording: the selected run is kept in the URL, and the back button returns to the list.

Fixed

  • Fixed harness teardown and bring-up edge cases so repeated runs no longer leave stale containers/networks or duplicate signaling-public-proxy aliases.
  • Cassini shell polish (D-420 review follow-ups): switching between the Browse and Operator surfaces now keeps the open meeting in the URL (deep-link no longer dropped); the operator surface uses the same light/dark theme as browse (stored preference, then OS) instead of only the OS setting; and the in-Nextcloud mount targets the innermost #content more robustly.
  • Added the missing operator/settings proxy route so the operator surface's STT-quality Settings panel works through the Nextcloud AppAPI proxy (it previously had no route and would have 403'd/404'd), and dropped the now-dead control-panel proxy routes left over from the collapsed second entry.
  • Talk recorder: stabilized subscriber negotiation behind intermittent empty/blank recordings ("no remuxable streams found in session artifact", silent MKVs, and 0-word transcripts). A fresh in-call participant previously caused two empty-SID requestoffer messages; the second could make the MCU tear down and recreate its Janus subscriber, rotating the SID while the first offer was still being answered. Initial and exhausted-peer recovery now issue exactly one request, repeated in-call snapshots keep the valid response throttle, and inbound offers cannot trigger recovery in front of themselves. The recorder also sends its answer without waiting for full ICE gathering, queues trickled candidates behind the matching answer, and emits end-of-candidates only after gathering completes (D-454).
  • Stopping a recording no longer fails with "job is not stoppable" when the operator has just reported the job as running. The stop registration now brackets the whole time the job is advertised as recording, so a stop is accepted from the moment the job goes running until it leaves the record stage, and one arriving before the recorder has finished starting up is delivered as soon as it does.
  • A stop requested after the recording has already ended on its own (while the operator is still uploading to Talk and queueing the build) is now accepted and recorded as a stop request instead of being rejected; the job's stop reason continues to report why the recording actually ended.
  • The release pipeline's required "Faithful installed ExApp" gate now decides whether to run from a deny-list (run unless a change is purely docs/notes/assets) instead of an allow-list of enumerated product paths, so a pull request touching a build input nobody thought to enumerate can no longer report the required check green without actually building and recording.
  • Talk recorder: closed two gaps that could still lose media on a call the recorder appeared to be recording normally. A participant whose handshake completed but whose media never arrived was left untouched for up to ~45 seconds, so short calls (a ~30s 1:1) could still finalize as empty ("no remuxable streams"); such a peer is now rebuilt about 12 seconds after its answer, but only while its connection is not actually established, so a slow-but-healthy participant is never torn down. A participant who turned their camera on mid-call could also be wedged permanently by a dropped signaling answer — their audio kept recording while the newly added video was silently lost for the rest of the call; the recorder now retransmits that answer as the signaling connection recovers, within a bounded budget, instead of skipping the participant forever (D-509).
  • Fixed the CI session-artifact guard passing recordings that captured no media. It verified headers and index framing but never counted packets, so a capture with a valid header and a 0-byte index reported "session artifact verification passed" — the exact empty-capture class (D-454) the per-PR e2e leg is relied on to catch. The guard now walks each stream log and requires real RTP media packets per captured stream, counted kind-aware so a stream carrying only RTCP no longer passes for having non-empty bookkeeping.
  • Fixed the A/V drift check reporting success without measuring anything. It exited 0 when no audio/video pair was found, and also when pairs were found but every one was too short to compare — printing "av drift check passed" after zero comparisons. Drift runs now require at least one pair to be actually compared, and the rejoin leg, which legitimately records video-less, opts down explicitly.
  • Fixed the e2e leg never producing a drift pair the check could measure. The publisher bots play the sample fixture once and stop at its end, and the default fixture is 15s — capping the measurable pair at ~11s against the 15s minimum regardless of the configured publish window. The leg now prepares a fixture sized to the publish window. The fixture is also rendered with a keyframe at least every 0.5s (libvpx's default 128-frame interval delayed decodable video up to ~4.3s behind audio, which the elapsed-difference metric misread as drift), and the leg's tolerance carries a documented budget for the remaining structural skew.
  • Fixed panels, cards and form fields losing their borders and shadows inside Nextcloud, which left content areas with nothing to separate them.
  • Fixed high-contrast themes leaving those same areas undefined, and the selected recording unmarked.
  • Fixed Nextcloud's app chrome applying twice, which added a second header-height gap, doubled the side margins, and clipped the bottom of the app.
  • Fixed the shell tab bar, the operator scrollbar gutter, and the detected-hardware values rendering with the wrong colours under some themes.
  • Fixed the recordings list not appearing on narrow screens.
  • Fixed the settings Save button offering to save when nothing had changed.
  • Fixed the meeting filter drawing a second focus ring inside its own.
  • Fixed the meeting load-in fade stalling and then snapping into place.

Added

  • Cassini marketing and docs microsite (cassini-microsite/): homepage, changelog page, and docs section with sidebar navigation.
  • The Cassini microsite is now published at https://gocassini.com.
  • Release tooling for the Nextcloud App Store: a version-ladder CLI (scripts/release-version.sh), changelog folder (scripts/fold-changelog.sh), local release-prep orchestrator (scripts/prepare-release.sh), App Store package builder/validator, and a manual Release workflow that signs and publishes gocassini.tar.gz. See docs/release.md.

Changed

  • Nextcloud toolbar icon updated to the Cassini brand mark with white fills and a tighter viewBox for correct rendering in NC ExApp containers.
  • Viewer restyled with the Saturn theme (ice blue and Saturn gold) and now shows a favicon. A previously saved light/dark choice resets once and follows your system setting until you set it again.

Fixed

  • Documentation pages on the microsite now show the site footer.
  • The microsite changelog timeline no longer draws a trailing line below the last entry.
  • Viewer meeting dates no longer append a timezone (e.g. "GMT+1") they cannot actually justify. Meeting date labels carry no timezone, so a UTC-derived time could be shown an hour off with a wrong-but-confident zone suffix. The viewer now renders the label's own wall-clock time and makes no timezone claim.
  • The operator control panel no longer flashes to loading spinners every couple of seconds while watching an active recording. Background refreshes (the 2s polling fallback and event-stream reconnects) now update the jobs list and run detail in place instead of re-showing the first-load placeholder each tick.

Added

  • The viewer's meeting list has a filter box for narrowing meetings by name or date.

Fixed

  • Talk recordings now show their conversation name and actual recording date in the viewer, instead of the raw job id as both name and date. When the name cannot be resolved, the meeting is shown as "Untitled meeting" with the recording date.
  • Meetings whose date could not be read no longer scatter through the list; the list stays newest-first and undated entries sort last.
  • Recordings publish to the viewer again. The static-site export crashed on every meeting — a regression that shipped with the D-462 meeting-name/date change (merged in #112) — so no recording reached the viewer. Publishing now completes normally.

First public alpha.

Added

  • Added public project metadata: AGPLv3 license, changelog, security policy, and contribution notes.

Security

  • Marked deterministic harness credentials as development-only values and expanded ignore rules for local dependency and signing artifacts.
Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureqHWu4BvUTmEz8vLKHwiQISBBfpIghrmDS8F2bm92mXtaqmKZ68QLuDaNdJxEIzEve9KtcadKOVv7FvybODZY1NG0wtyty2WAqtSQDgGyg3D6Rcf5UTI6SPH9Fqt9jrj6wrPugbAUZKKrf05SInXCwGineaoBCybE2uSE9lB0dkBi4VEEafNM7Cva/wESlp2/bFyU2f/C6lA1ivxAzwowijN6PB/uzNMfpstH9cW+xDO1pqoGeSw7hjJ1rFQvAIMREwIWcWZnTAiPIag1SzgYoJv0hK/AFa7KsWp2TnQDV0S/QWRafutYmGR2ZnuBowsjfXyKDEmjEWQn7ok0HGiTO0SswqMk1nzTn3B5kPLFSVBj0/+MJ6oAXvgqjqP3Au7Jmp+771rEK94xlcWlUmDi3mW6fNvaD5LPpeRoB1u6CmCEtZSh+cMOYFE7OlhWuXqZC3p5rBeUUYWLUsQDXnG3SeHDaGwcyZ+uvijc1FQ2DhJMMcnY9mhtztR/9vZUs5DttIRa2q1nuP7Nvcer+GRlkD1HgntP9HwAk2RBXHMuuewRfZMVOWGJXSxobZkAip80sg02rrHVgyWmE2jElCiZrWUKcTMd9BkF1d+yWy0msiY7dIrTEEBbOLx1hFI3wWsRnOdIS6WgtbX1xhmB4akWQxMuh6oI+6BAmuRRK+oH9Uw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=33.0.9,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.8
Release Details
UpdatedOct. 6, 2026, 10:06 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureoarCm0opumircwdOtbKX8fyvj4gEcHXlR8t9ga001qpk4LZvkY6SbU3hDM73gQtQYvlI8S3sHsoINzDxxWc+hb5oEJW0rNAOIjUGG4SeE7CGIk4zcVcUFI5R8nN3nirsm8yXhFTMNPVoFhb00Jmh05qbTkhe/A6qpPWfG1dUxMQkhGDo2EB6p7+1v8eOOhChrupJ7NWKxkdvDkQM56ITKti29dP09ajgYIYetM27swILRUFExqHSrZQzhADnSU7VWytj/inWFOYk1hZR6d5m2XYyihk6V8WeK2Jv+orEOlEDdzT1xSHhZAVyrwFEbFkDiO+VpMR1ZTxhh7XbF+htvF9itTBeeaZxZirzlpJ/KOFBvpUrc4bDEv6szcLknHowaKpjqa15/4u0Ssaaf8aJaJSbRi5khMYBGW3fyQ82DWx2nsFWsxVI/lBuG4T6HG+MX8x1YhLx7KledlNJnZlrHp89mv7TxQ2E0pLysWYeFauceBDBv/pdDAdYom3K+AcgD0HGSFBNhQ2BITlmk0DNQ5mGFhAy6Cqr+63vCrRP1Y7QTR5itIKw5BBGMjuI3XE+yfJ4ArjSkY2IciqMdEvk0ULjyV1kqvdlRMyFWuE2sYv87dFmPPvHMqXZNzIY/lAWx0hE56ZxjtrJDVVLfAWD67DxOTqR2Qsyliui96WoVOU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=33.0.9,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.7
Release Details
UpdatedSept. 19, 2026, 10:57 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
Signatureq9Yl5MApgS2v7JvdVeoXX8XY+e9xoIcNMy8HSwsU5UdFir++uQNW1/1oIKL66VVie9CuPCCQLIicFJ3pOjjf1CqjShJ1gaDVyUVHm5EVeHno5ah/lFqycCwQsW8qNlohtBz2wU7MVn0ooIULjGp6rFmSzxA3A9FQpSA/SSDjCnHjLgOymV+qWZjYzQluKES+NOj58Pyx8O19Z0U+5Gmubmc58nlKszodozXnU0K8s0gj58J699Lh9sMm1GNufZs4eE8zAo904wA4rqNGD9v3Bl27I8TVpGNwVLxrM90mOjpzaoYpzAaBWWYbneLBquZUflKpx2pJvinUDOEuD9rTbeCWfGXZ4cqCW5bwEuvx2QsOfCk0B2wOQpwyAfCgI2i3tsVxIvTs3hfb3qVhMJ45uZerqqYFyNW8oj7sqw1XJSZrTaRrk3ZW6RcuOTRMCMlT9y5/PjlSXQglZe2/OvndTi2o5DpttKChtd9EiN4NMbMTAW6ih++H3p11tyf1F8xLFc3dEm6U+aLMQFnMlR1hg8yclowh8klRxJf/r0PTcQoYIYwIBXsoDlowyohaLkJzXK4dsedTwf9gJdzjHOq+0h+gFiYGkPWgjejlL7n0v4jSD7BmA7oxp43YPRc9H1nB57oJKLKPQ4PudZS4gz6eJa/CxyQX7Y5A+oRM+PMJv/4=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.2
Release Details
UpdatedAug. 20, 2026, 7:15 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureCsJE9kLQCmBdKL0g4xSp/yu8dv0UbaGzLk6qCbrH2NOhjN4hr66SS+R66wvY1Q40l9Z7VR08jDLhwNnEeMVEY3C4Pk9YPEBPJAM/3MyRudg6EQ01amV7qgFPPaa96eZXiFD9lozVDbchBcihooe+k5m4HNJjey76F0LHIPNUqtGQjPeDBMJtMCXQRulQORbzDyx2ets+JvPOITr+pu65sE8T16yuoMgvm8qivWEtF8+12bsBOt6qXRNQokDowXzrNeWO/v8BlJg1pEBvmjK8G65EG03xYiRw0F6Vwm38Lx1OY7zWMFco9QLrSlHA7XeYahZZ8CMOWybxYrnPa1GFBbyMaPYoWRQRmRMoPSY7D7g/jAYzjSmh0jmLyfPAOPLV681mi644v1R5rVzbWPjYol+fIb3kUVU2d1z4oyRwM7sfd39NGeWmgI8SvG0LOZPArFpJod3AVrh9fvAD5PkASyzHMRH7cOHI3BI1g/4/lsMjIc/cTJddAZPsOsZ/PlTDD5GfJm87m0eoUWj0cetODqUmUUsJJvDH8siKNTXZIhQmbn/4wDFaqXPnufYgogY2XjyFqEDbdFzyL1hgsWlSO3NX8+t2HQjhTOdqnCTU8QeREpGc5RFOm3XcCTVFaQIEI/4hi+wwA98pkB4UXCGiV9+s0qCwhJdm1yOnjfiD9uU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.1
Release Details
UpdatedAug. 14, 2026, 5:49 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureR3TWjXw2RXfjhTk3UwiSEsLsfneLrrUFnqWhYZoE/rNoYyP0ETh7kkAkHrkIrPuhcZWo5U5L+wxeihkfMlMHwraMhMtmlUDGenmzPoxAuZhbjTuhHMn06+nsLY5mHZqIA7i5s68YT/xrduPvBcEDWqzJktJDXEsN+ZBZdpAoI4pqS8j8aZP+7io78c4wtYIHuA58V+e5yvpDvxnQr11W2CQWH/Q+E52clqSHsmcFRq25eQsKN7mPsvJb2eINY+vyUK0Mz88ZPg1vNFyhOBqDCTnp8xoyMrLHe37rNLriBNuuOuDP6Umuv5d1cW1GGc8fzaZxxoBXtMqQzkKyyxazKMOT4v23eFRgWlOx38y9DrUn9+qbMCxY8fLJ/rbG/XeKpZuEvoZGW74ol0Y62+EXIT1vueJOdlKFrRwIv8DvOxbYeqI7NFk17Zs49+iuUyRoNBAHdTrI2yvneLHL5ePeyMS8J5DuNdOtFNEtgRcXIfGoMHVzN6F33q2UnEeRN6q5iEJZwh7OgxAKQo9RL5kzLzky5A+6v1JP6/40fSrse5JxjzNA98+osNs87JEWA675GtPbWnOM757VjBC5ZwNA1hz97l5UA8WQYdgLoVcLhuWlW302KYnqg2PMQYM5zZkyZRYlyhJ8RDdyJXTMhOdJHQTHLtoNrQxISzeRicyX3bs=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.4
Release Details
UpdatedJuly 27, 2026, 12:25 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureDFZcmcqbeTiTh9ZtdIKLh6MOP647ayzVl9rR20Qxp74ysDBxcTsLs3LQRxmjh6WqZOjse/1t1x1oBdFcHkqbtjvNHsZJUOPb4RMh1b1sh0q5lUnHkCHp5qYoa1rJnrZt/WiqsYysgwwwtk9dMluvBLd/DHA8MY5ICMxII7LQtucl/ZRaH8kGYwsdx/Zn2YY9J0B+jjFeKM8gsZdy8TYwfKgb1k+b6jENXVgDb8WcMi9Vovw6VZ4SRTGPmsbOPOJsBVHIBQY+D+snIXjJ3VkWkU1laTJnYss9Fv7Su4nkUNLI8wpVwCSbrANTDY0HAx00zpbt1tH7rqWvS0rYqtYgfgJqfrrwTdpaCgcOoc6BcG1i4SMCyK9zRpxdXrpwG7kU6C2ubBqO0Zy7UL2c4cJ9npWU20UeWDSWfINngoRtdR0sbxoN5arkD1mvd7B1Doza3WMkT6XBgyk/PVcFnuduhWQYoyQIAIzajLEzTkSFrA5QM38cpWVeIDIRVYYKizya5xweM/mIvEuIArXAd4b+AyDL/glmuPC9L7ftmCMp7EJPTMWctRLheYNM4aMc44l+A5SpoU/nMqo69+/Q2wc23VWbT53hhVH9rbUMxMQ/SgX+EXj0uKDumldDQlpZV3HjCPW81uwCSB9gpLrCi0zREENxn9zSXB24WF+y9cFx7Dw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.3
Release Details
UpdatedJuly 11, 2026, 11:16 a.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureeRHRR6dJjmuxKf+SembwkG/mU3joMZwIrZ1bMXGB49Uc4l2t6HvD3Urbw5AdoofHgERt7RGkTf0YLF1hdSRSxqwCVw6XpvGLfjfAZEUCTM9i2mLTey/66QfctnFcE35wW0mzEQbkYbmQp5YKogsgZ+6X/cxlrio3I4EEOaR3ZBRQp7RxeSqUGhjTpnHomrVCOEwNpvGK2UIzVLqHA7gfLY5W7o/DBfmTiXt1BJ8pGTjbGV9Gi81jcxLp9LaTfjbkMVZf7stRdRtMvIhEuUgSgLyxAZftvoo4oGqB+lnx87rAGeQ90nynKx/6yVvYN9AO6Gzxg1Y5ElIS7qdBqSZxnZ8AJtoGgbCc1GT29aKbrLEAUBKcsuEXgSR21qe9DM/GNQgy8i59viKBsNOeOI+B+K2fZciKxOcv4B7iO94axm4po59voAETJyUej+R8iPfLj6kTSWr/mLhfFiF0vu89Gktb8T1OMYBaDrthHXfXl0D+5FAt6NQNzJOW2S2IDwerOMAKlAff0tkDOUZOGlffvJZC8W6qeh67EMd2UL83nkxO2h6dQpRaXD0wycZxlB2/HCoGDqrIEWvlz7rf25PV3iyaL0yu0Vh3rpFTd7tGOJyYq6y4qmJDEVL/Jd1ImEOloY3jWWeQ2dzNSOW2g6+/89KbkrV78UnOhWAPp6fmc2E=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32

Nextcloud 32

Cassini 0.2.0-beta.7
Release Details
UpdatedSept. 19, 2026, 10:57 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
Signatureq9Yl5MApgS2v7JvdVeoXX8XY+e9xoIcNMy8HSwsU5UdFir++uQNW1/1oIKL66VVie9CuPCCQLIicFJ3pOjjf1CqjShJ1gaDVyUVHm5EVeHno5ah/lFqycCwQsW8qNlohtBz2wU7MVn0ooIULjGp6rFmSzxA3A9FQpSA/SSDjCnHjLgOymV+qWZjYzQluKES+NOj58Pyx8O19Z0U+5Gmubmc58nlKszodozXnU0K8s0gj58J699Lh9sMm1GNufZs4eE8zAo904wA4rqNGD9v3Bl27I8TVpGNwVLxrM90mOjpzaoYpzAaBWWYbneLBquZUflKpx2pJvinUDOEuD9rTbeCWfGXZ4cqCW5bwEuvx2QsOfCk0B2wOQpwyAfCgI2i3tsVxIvTs3hfb3qVhMJ45uZerqqYFyNW8oj7sqw1XJSZrTaRrk3ZW6RcuOTRMCMlT9y5/PjlSXQglZe2/OvndTi2o5DpttKChtd9EiN4NMbMTAW6ih++H3p11tyf1F8xLFc3dEm6U+aLMQFnMlR1hg8yclowh8klRxJf/r0PTcQoYIYwIBXsoDlowyohaLkJzXK4dsedTwf9gJdzjHOq+0h+gFiYGkPWgjejlL7n0v4jSD7BmA7oxp43YPRc9H1nB57oJKLKPQ4PudZS4gz6eJa/CxyQX7Y5A+oRM+PMJv/4=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.2
Release Details
UpdatedAug. 20, 2026, 7:15 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureCsJE9kLQCmBdKL0g4xSp/yu8dv0UbaGzLk6qCbrH2NOhjN4hr66SS+R66wvY1Q40l9Z7VR08jDLhwNnEeMVEY3C4Pk9YPEBPJAM/3MyRudg6EQ01amV7qgFPPaa96eZXiFD9lozVDbchBcihooe+k5m4HNJjey76F0LHIPNUqtGQjPeDBMJtMCXQRulQORbzDyx2ets+JvPOITr+pu65sE8T16yuoMgvm8qivWEtF8+12bsBOt6qXRNQokDowXzrNeWO/v8BlJg1pEBvmjK8G65EG03xYiRw0F6Vwm38Lx1OY7zWMFco9QLrSlHA7XeYahZZ8CMOWybxYrnPa1GFBbyMaPYoWRQRmRMoPSY7D7g/jAYzjSmh0jmLyfPAOPLV681mi644v1R5rVzbWPjYol+fIb3kUVU2d1z4oyRwM7sfd39NGeWmgI8SvG0LOZPArFpJod3AVrh9fvAD5PkASyzHMRH7cOHI3BI1g/4/lsMjIc/cTJddAZPsOsZ/PlTDD5GfJm87m0eoUWj0cetODqUmUUsJJvDH8siKNTXZIhQmbn/4wDFaqXPnufYgogY2XjyFqEDbdFzyL1hgsWlSO3NX8+t2HQjhTOdqnCTU8QeREpGc5RFOm3XcCTVFaQIEI/4hi+wwA98pkB4UXCGiV9+s0qCwhJdm1yOnjfiD9uU=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-beta.1
Release Details
UpdatedAug. 14, 2026, 5:49 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureR3TWjXw2RXfjhTk3UwiSEsLsfneLrrUFnqWhYZoE/rNoYyP0ETh7kkAkHrkIrPuhcZWo5U5L+wxeihkfMlMHwraMhMtmlUDGenmzPoxAuZhbjTuhHMn06+nsLY5mHZqIA7i5s68YT/xrduPvBcEDWqzJktJDXEsN+ZBZdpAoI4pqS8j8aZP+7io78c4wtYIHuA58V+e5yvpDvxnQr11W2CQWH/Q+E52clqSHsmcFRq25eQsKN7mPsvJb2eINY+vyUK0Mz88ZPg1vNFyhOBqDCTnp8xoyMrLHe37rNLriBNuuOuDP6Umuv5d1cW1GGc8fzaZxxoBXtMqQzkKyyxazKMOT4v23eFRgWlOx38y9DrUn9+qbMCxY8fLJ/rbG/XeKpZuEvoZGW74ol0Y62+EXIT1vueJOdlKFrRwIv8DvOxbYeqI7NFk17Zs49+iuUyRoNBAHdTrI2yvneLHL5ePeyMS8J5DuNdOtFNEtgRcXIfGoMHVzN6F33q2UnEeRN6q5iEJZwh7OgxAKQo9RL5kzLzky5A+6v1JP6/40fSrse5JxjzNA98+osNs87JEWA675GtPbWnOM757VjBC5ZwNA1hz97l5UA8WQYdgLoVcLhuWlW302KYnqg2PMQYM5zZkyZRYlyhJ8RDdyJXTMhOdJHQTHLtoNrQxISzeRicyX3bs=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.4
Release Details
UpdatedJuly 27, 2026, 12:25 p.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureDFZcmcqbeTiTh9ZtdIKLh6MOP647ayzVl9rR20Qxp74ysDBxcTsLs3LQRxmjh6WqZOjse/1t1x1oBdFcHkqbtjvNHsZJUOPb4RMh1b1sh0q5lUnHkCHp5qYoa1rJnrZt/WiqsYysgwwwtk9dMluvBLd/DHA8MY5ICMxII7LQtucl/ZRaH8kGYwsdx/Zn2YY9J0B+jjFeKM8gsZdy8TYwfKgb1k+b6jENXVgDb8WcMi9Vovw6VZ4SRTGPmsbOPOJsBVHIBQY+D+snIXjJ3VkWkU1laTJnYss9Fv7Su4nkUNLI8wpVwCSbrANTDY0HAx00zpbt1tH7rqWvS0rYqtYgfgJqfrrwTdpaCgcOoc6BcG1i4SMCyK9zRpxdXrpwG7kU6C2ubBqO0Zy7UL2c4cJ9npWU20UeWDSWfINngoRtdR0sbxoN5arkD1mvd7B1Doza3WMkT6XBgyk/PVcFnuduhWQYoyQIAIzajLEzTkSFrA5QM38cpWVeIDIRVYYKizya5xweM/mIvEuIArXAd4b+AyDL/glmuPC9L7ftmCMp7EJPTMWctRLheYNM4aMc44l+A5SpoU/nMqo69+/Q2wc23VWbT53hhVH9rbUMxMQ/SgX+EXj0uKDumldDQlpZV3HjCPW81uwCSB9gpLrCi0zREENxn9zSXB24WF+y9cFx7Dw=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32
Cassini 0.2.0-alpha.3
Release Details
UpdatedJuly 11, 2026, 11:16 a.m.
Changelog

Unreleased changes are collected as fragments in changelog.d/. Versioned sections below are script-managed: scripts/fold-changelog.sh (run by scripts/prepare-release.sh) folds those fragments into this file under the new version and removes the consumed fragments. Edit released sections only to fix mistakes; add new entries as fragments. See docs/release.md.

Licenses AGPLv3+
Certificate-----BEGIN CERTIFICATE-----
MIIEBDCCAuwCAhOLMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD
VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI
MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB
dXRob3JpdHkwHhcNMjYwNzA3MTAxNjU1WhcNMzYxMDEyMTAxNjU1WjAUMRIwEAYD
VQQDDAlnb2Nhc3NpbmkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
QsNMKsRO/fAkx32T17T3AKILPdfoCx5KKt2G5eL6RbtB8PyS1SXKT4AImMhbWe0a
OGC2cusiJTApICDQ2SN9jjwxFtkt5Kqgy/kGqaCq+86GgNG2l+e/7I87le5p44gE
y307PGn9wsEoYdOyly9vRuuAUKNVvEcFSWf5oSa/b3zs1jJlrcDfPBnA8tAk+ZFJ
Qp8HYIO2ZUwhH3SJL2VEFS0jY5YBcqBym40nSMjUC21mO4NOJKwMUGVqXkPLEum4
rxVFkD8WY1W65NpGe/DAykLLm8Jqj4mKtXDnUXzNJEmlq/1YsmEmYBUBV0PgDG1l
c44yg7oGtKSWQczroV/RJKwbZpRubaH57r8232hA9DWIhGvPJu2EEGaMY9m/lQ+S
G3cW9Qgpwy/nV5JryWlqGM1tT3aI3TlIMpm9VMhR7USpqP45RlvAlAeWhiO6jBLF
ycchwWxPWTW5pFtCLeDZNWKHjLNeVNuc+0AlSDbxQ41Pnh+c1cHXHcwsf9Qq6Bwg
Gov8G71mLlGFu63tLpZO1dCTH1fBRcjbWJKvsYSuLX13XFxIe0llWDtl6nq0V/mp
ijDwS4jMMImWjC8Fi4QS1ZvFqSs0pJVE3AQ6MXmv2FVaQM6lHsILrCZdnTGwBP68
gwJUxaYI81D5aJHLUvzI/cXdTes++AWpO6JHNwIWCwIDAQABMA0GCSqGSIb3DQEB
CwUAA4IBAQBbHavxArZLSxgpDf8oHD4Imw/t6blKqTATotEblyP67O1GrGXBPFtN
/QcmLVfcdzDxwnp272Oqy77kjQIo4jDA8p6w4a419nKZQV+cnNF/WqlynbWtKzzk
bMCiUuy57Hyd0KeVfLWF6RfwkksM6txSwFMcZSDToIqTcdB4OljCwkQjgNjvUPTu
5hILncyOdMU6Ej3wg1LQcBC76vnKJlMVdi1Yl3DPoh9PNLhN2GT1KEQmr6YkC3uB
17fkPghhnL1j7RWtbqXa/N9tSODp1FeU9BKqDEXgz+HqU0MMhqjUK67TwBUZiTcG
igcqa5OLJ3em45WWKqQNKiI9gdTv+gqv
-----END CERTIFICATE-----
SignatureeRHRR6dJjmuxKf+SembwkG/mU3joMZwIrZ1bMXGB49Uc4l2t6HvD3Urbw5AdoofHgERt7RGkTf0YLF1hdSRSxqwCVw6XpvGLfjfAZEUCTM9i2mLTey/66QfctnFcE35wW0mzEQbkYbmQp5YKogsgZ+6X/cxlrio3I4EEOaR3ZBRQp7RxeSqUGhjTpnHomrVCOEwNpvGK2UIzVLqHA7gfLY5W7o/DBfmTiXt1BJ8pGTjbGV9Gi81jcxLp9LaTfjbkMVZf7stRdRtMvIhEuUgSgLyxAZftvoo4oGqB+lnx87rAGeQ90nynKx/6yVvYN9AO6Gzxg1Y5ElIS7qdBqSZxnZ8AJtoGgbCc1GT29aKbrLEAUBKcsuEXgSR21qe9DM/GNQgy8i59viKBsNOeOI+B+K2fZciKxOcv4B7iO94axm4po59voAETJyUej+R8iPfLj6kTSWr/mLhfFiF0vu89Gktb8T1OMYBaDrthHXfXl0D+5FAt6NQNzJOW2S2IDwerOMAKlAff0tkDOUZOGlffvJZC8W6qeh67EMd2UL83nkxO2h6dQpRaXD0wycZxlB2/HCoGDqrIEWvlz7rf25PV3iyaL0yu0Vh3rpFTd7tGOJyYq6y4qmJDEVL/Jd1ImEOloY3jWWeQ2dzNSOW2g6+/89KbkrV78UnOhWAPp6fmc2E=
Signature digestsha512
Dependencies
Required Nextcloud versions >=32.0.0,<36.0.0
Minimum Integer bits32