You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Super Admin PKI + Local Password Fallback** - PKI-authenticated super admins can retain local password as fallback (#127)
128
128
129
+
#### Upload Modal Redesign
130
+
-**6-Step Stepper Flow** - Replaced the accordion-inside-modal upload UX with a linear stepper: Media → Tags → Collections → Speakers → Options → Submit. Conditional Extract step appears automatically for large video files
131
+
-**Unified Across All Upload Sources** - File, URL, and recording uploads now share steps 2-6, so URL downloads and recordings gain full access to tags/collections/speaker settings (previously file-only)
132
+
-**Remember Previous Values** - Upload modal pre-fills tags, collections, speaker settings, whisper model, and skip-summary from the last upload. One-click "Review with defaults" shortcut lets power users jump straight to submit
133
+
-**Clickable Stepper Navigation** - Users can click any previously-visited step to go back and edit. Dot + label is a single clickable button per step (Fitts's Law / Apple HIG 44×44pt touch-target compliance)
134
+
-**Decomposed Monolith** - The 4,603-line `FileUploader.svelte` split into a 1,294-line coordinator plus 9 focused components under `frontend/src/components/upload/` (each under ~470 lines). New `upload-shared.css` provides a unified chip/dropdown pattern reused across tags and collections
135
+
-**Conditional Extraction Step** - Large video files (>100MB by default) trigger an inline Extract step with radio-button choice (Extract Audio Only vs Upload Full Video). Extraction runs on final Submit, not at selection time, so users can still change their mind while stepping through tags/collections
136
+
-**Backdrop-Click No Longer Closes** - Modal only closes via X button or Escape key, preventing data loss from stray clicks on in-progress upload state
137
+
138
+
#### Skeleton Loaders on Major Pages
139
+
-**Structural Loading States** - Replaced generic `<Spinner size="large">` on home gallery, search results, file detail page, and speaker clusters/profiles/inbox with skeleton components that mirror the final layout. Perceived load time ~20% faster per Nielsen Norman research
140
+
-**Reusable Skeleton Components** - New `FileDetailSkeleton.svelte` (full 2-column layout with header/video/transcript), `ui/CardGridSkeleton.svelte` (parametric with media/profile/search variants), and `ui/ListRowSkeleton.svelte` (avatar + title + actions rows)
141
+
-**Gallery Click Feedback** - Clicking a file card now dims + scales it instantly (opacity 0.72, scale 0.985) with `pointer-events: none` to prevent double-clicks. Prefetch kicks off on `mousedown`~50-100ms before the click handler runs
142
+
143
+
#### Collection & Share Modal Polish
144
+
-**Help Text and Empty States** - Create/Edit Collection modals gained intro banners explaining what collections are, field hints with `maxlength` indicators, and proper `aria-labelledby` wiring
145
+
-**Universal Content Analyzer Default** - New collections auto-select the system-default prompt (via `is_system_default` lookup), matching the behavior users typically want without requiring manual selection
146
+
-**Share Modal Intro and Permission Guide** - Share Collection modal now includes an introductory explanation, a collection name banner with folder icon, and a visible permission-level reference card showing Viewer/Editor labels inline with descriptions (previously only in tooltips). Empty state added for collections with no existing shares
147
+
-**Manage Collections Visual Fix** - Fixed nested-card glitch where the inner `.collections-panel` had its own surface background inside the outer modal container, producing a visible "card in a card" look
148
+
149
+
### Security
150
+
151
+
#### Frontend Session Hardening
152
+
-**Flash of Authenticated Content (FOAC) fix** - `+layout.svelte` now gates all protected content behind `authReady && isAuthenticated && !isPublicPath`, showing a loading screen in route-mismatch states while async redirects are in flight. Previously, unauthenticated users hitting `/` briefly saw the gallery slot render before the redirect fired, leaking ~1-2 frames of protected UI and triggering `/files` API calls
153
+
-**Centralized User State Cleanup** - New `frontend/src/lib/session/clearUserState.ts` is the single source of truth for session teardown. Clears 17+ subsystems on every login/logout transition: toast, websocket, uploads, gallery filters, search results, sharing, LLM status, settings modal, transcript, groups, downloads, notifications, recording (with media track cleanup), thumbnail cache, media URL cache, speaker colors, plus user-scoped localStorage keys. Preferences (theme, locale, view mode, recording settings) are explicitly preserved. Replaces ad-hoc cleanup previously scattered across `auth.ts`
154
+
-**Session-Scoped Request Cancellation** - Session-scoped `AbortController` in `lib/axios.ts` attached to every request via interceptor (except `/auth/login`, `/auth/logout`, `/auth/token/refresh` which must always complete). `logout()` now calls `abortAllRequests()` before `clearUserState()`, closing the race window where a late API response could repopulate a cleared store with stale data from the previous session. New `isRequestCancelled()` helper exported for catch blocks to suppress error toasts on cancelled requests
155
+
-**bfcache Invalidation on Back Button** - `+layout.svelte` now listens for `pageshow` events with `event.persisted === true` and forces `window.location.reload()` to discard the restored DOM/JS snapshot. Prevents users from hitting the back button after logout and seeing the previously-protected page restored from memory on shared devices
156
+
-**Toast Cross-Session Leak Fixed** - `toastStore.clear()` is called from every login success path (local, Keycloak callback, PKI, MFA) and from `logout()` via `clearUserState()`. Previously, notifications from User A's session could persist into User B's login screen or the next user's session
157
+
-**Keycloak Redirect URL Validation** - `loginWithKeycloak()` now parses and validates the `authorization_url` returned by `/auth/keycloak/login` (requires `http:` or `https:` protocol) before calling `window.location.href`. Prevents open-redirect or `javascript:`/`data:` URL injection if upstream config drifts
158
+
159
+
#### XSS Hardening
160
+
-**DOMPurify-Backed HTML Sanitization** - New `lib/utils/sanitizeHtml.ts` provides `sanitizeHighlightHtml()` (whitelist allows `mark`, `span`, `br`, `ul`, `li`, `em`, `strong`, `div`, `p` with `class` and `data-match-index` attributes) and `sanitizeToPlainText()`. Added `dompurify` and `@types/dompurify` as dependencies
161
+
-**Defense-in-Depth Across 8 Render Sites** - Wrapped every `{@html}` directive that renders API-sourced or LLM-generated content with `sanitizeHighlightHtml()`: TopicsList, TranscriptDisplay, TranscriptModal, SearchTranscriptModal, SearchOccurrence, SearchResultCard, SummaryDisplay
162
+
-**Bypassable Regex Sanitizer Replaced** - `SearchOccurrence.svelte` and `SearchResultCard.svelte` previously used `html.replace(/<(?!\/?mark[\s>])[^>]*>/g, '')` which was bypassable via `</mark><script>alert(1)</script><mark>` payloads (the regex only matched opening tags). Now uses DOMPurify with a strict tag whitelist
163
+
164
+
#### Build & Configuration Hardening
165
+
-**Production Source Maps Disabled** - `vite.config.ts` now uses `sourcemap: mode !== 'production'`, ensuring `.js.map` files are only generated for dev/preview builds. Previously, production builds shipped source maps exposing variable names, API endpoint URIs, error messages, and full business logic to any visitor via DevTools or automated crawlers
166
+
-**Defense-in-Depth Home Page Guard** - `routes/+page.svelte``onMount` now early-returns if `!get(isAuthenticated)`, preventing `fetchFiles()` and WebSocket subscriptions from running if the component is somehow mounted unauthenticated (belt-and-suspenders beyond the layout-level route guard)
167
+
129
168
### Changed
130
169
131
170
-**Default Whisper Model** - Changed from `large-v2` to `large-v3-turbo` for significantly faster transcription with maintained accuracy
@@ -171,6 +210,13 @@ Major release combining enterprise-grade authentication, native transcription pi
171
210
- Admin bypass and shared editor access across all API endpoints
172
211
- Alembic migration chain linearized after branch merges
173
212
- LDAP user bcrypt crash when verifying non-local passwords
213
+
-**WebSocket notification queue leak** - `clearAll()` now called on logout; previously persisted in localStorage across sessions, exposing User A's notification history to User B on shared devices
214
+
-**Upload queue persistence leak** - `localStorage['upload_queue']` is now cleared on logout via new `uploadsStore.reset()`; previously leaked file UUIDs, metadata, and processing status across sessions
215
+
-**Dropdown clipping in upload modal** - Removed nested `overflow-y: auto` on the stepper body that was clipping tag and collection dropdowns. Primary modal container now handles all scrolling with `z-index: 200` on the dropdown list
216
+
-**Double-card visual in Manage Collections** - `.collections-panel` previously had its own `surface-color` background + border inside the outer modal container, producing a visible "card in a card" look. Root set to `background: transparent` when rendered inside the modal
217
+
-**Debug console.logs removed** - `AuthenticationSettings.svelte` no longer logs full auth config on every load; `files/[id]/+page.svelte` no longer logs every 5 minutes on video URL refresh
218
+
-**Dead code removed** - Deleted unused `routes/Tasks.svelte.old` (868 lines) and the unused `AudioExtractionModal.svelte` (replaced by inline stepper step)
219
+
-**Avatar lazy-loading** - Profile and cluster avatars on the Speakers page now use `loading="lazy"` and `decoding="async"`, preventing synchronous load-block on page init
174
220
175
221
### Upgrade Notes
176
222
@@ -184,6 +230,8 @@ docker compose pull
184
230
docker compose up -d
185
231
```
186
232
233
+
After upgrading, users should **hard-reload the frontend** (Ctrl+Shift+R / Cmd+Shift+R) to pick up the new service worker and clear any stale cached assets. The service worker will automatically cache the new build on next visit.
234
+
187
235
The system will automatically detect the authentication configuration mode and function correctly. To use new authentication features:
Copy file name to clipboardExpand all lines: docs-site/blog/2026-02-07-v0.4.0-release.md
+65Lines changed: 65 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -130,6 +130,71 @@ There's too much to list exhaustively, but here are the highlights:
130
130
-**Russian UI language** -- 8th supported interface language (added in v0.3.3)
131
131
-**Protected media auth** -- Plugin architecture for downloading from password-protected corporate video portals (v0.3.3)
132
132
133
+
## The Final Polish: Frontend Hardening Sprint
134
+
135
+
After the backend and pipeline work stabilized, we took a hard look at the frontend and found that two months of feature sprints had accumulated technical debt and security gaps that needed attention before shipping. A dedicated audit sprint closed those gaps.
136
+
137
+
### Session Security
138
+
139
+
The biggest finding: **Svelte stores persisted across logout**. Logging out as User A and logging in as User B on the same device leaked data from the previous session — toasts, search queries, gallery filters, upload queues, WebSocket notifications, transcript segments, speaker colors, and a dozen other pieces of state remained in memory. The auth cookie was invalidated on the backend, but the frontend kept showing User A's data until the next page reload.
140
+
141
+
We fixed this with a **single source of truth for session teardown**: `frontend/src/lib/session/clearUserState.ts`. It clears 17+ subsystems in parallel on every login/logout transition, plus the user-scoped localStorage keys (notification queue, upload queue, remembered upload values). Preferences like theme, locale, and view mode are explicitly preserved — those are user choices, not user data.
142
+
143
+
While we were in there, we also found and fixed three more session-level leaks:
144
+
145
+
-**Flash of Authenticated Content (FOAC)** on fresh page loads. The layout's `{:else}` branch rendered the protected page slot for ~1-2 frames while the async `goto('/login')` was in flight. Unauthenticated users briefly saw the gallery UI AND triggered `/files` API calls. Fixed by gating all rendering behind `authReady && isAuthenticated && !isPublicPath`, showing a loading screen in the route-mismatch state.
146
+
147
+
-**In-flight request race**. When a user clicks a file and immediately logs out, the `GET /files/{uuid}` response can still resolve after `clearUserState()` and repopulate the store. Fixed with a session-scoped `AbortController` in `lib/axios.ts` that cancels all pending requests on logout (except auth endpoints like `/auth/logout`, which must always complete).
148
+
149
+
-**Back-button (bfcache) after logout**. Modern browsers restore pages from in-memory snapshots when users navigate back, bypassing all our auth guards and store clearing because the DOM is served from the cached snapshot. Fixed with a `pageshow` event listener that detects `event.persisted === true` and forces `window.location.reload()` to discard the snapshot.
150
+
151
+
### XSS Hardening
152
+
153
+
Audit turned up a **bypassable regex sanitizer** in the search result highlighting utility. The regex `/<(?!\/?mark[\s>])[^>]*>/g` only matched opening `<` characters, so a payload like `</mark><script>alert(1)</script><mark>` would pass through unscathed. Replaced with **DOMPurify**, using a strict tag whitelist that allows only the specific markup produced by the highlight pipeline (`mark`, `span`, `br`, `ul`, `li`, `em`, `strong`, `div`, `p` with `class` and `data-match-index` attributes).
154
+
155
+
As defense-in-depth, we wrapped every other `{@html}` render site — 8 in total — with the same sanitizer: transcript segments, search highlights, LLM-generated topic summaries, AI summary sections. The underlying escape pipeline was already correct, but running everything through DOMPurify as a final pass means a future code change that forgets to escape can't introduce an XSS.
156
+
157
+
And one config fix: **production source maps were enabled**. `.js.map` files exposed variable names, API endpoints, error messages, and the entire business logic to any visitor via DevTools. Disabled with a one-line `sourcemap: mode !== 'production'` change to `vite.config.ts`.
158
+
159
+
### Upload Modal Redesign
160
+
161
+
The old upload modal was a 4,603-line monolith with accordion sections that didn't scroll. We redesigned it as a **6-step linear stepper**:
Plus a conditional Extract step for large video files. The new flow decomposes the monolith into a 1,294-line coordinator plus 9 focused components (each under ~470 lines). All three upload sources (file, URL, recording) share steps 2-6, so URL downloads and recordings now get full tag/collection/speaker configuration — previously file-only.
166
+
167
+
The stepper has a "Review with defaults" shortcut for power users who want to skip the middle, a "Remember previous values" feature that pre-fills from the last upload, and clickable step dots that let users jump back to any visited step. The backdrop no longer closes the modal (prevents data loss), and dropdowns for tags/collections were converted from auto-open search boxes to clean checkbox lists that don't visually explode on focus.
168
+
169
+
### Skeleton Loaders Replace Spinners
170
+
171
+
Research shows skeleton screens feel **~20% faster** than spinners for the same actual load time (Nielsen Norman). We replaced generic `<Spinner size="large"/>` loading states on four high-traffic pages with skeleton components that mirror the final layout:
172
+
173
+
-**File detail page** — full 2-column skeleton with header/video/transcript shapes
174
+
-**Home gallery** — card grid skeleton matching the media file layout
-**Speaker clusters/profiles/inbox** — list row and profile card skeletons
177
+
178
+
The new `FileDetailSkeleton`, `CardGridSkeleton`, and `ListRowSkeleton` components live in `frontend/src/components/` and `frontend/src/components/ui/` and are reusable across any page with a predictable layout. Loading feels structured and anticipatory instead of "waiting."
179
+
180
+
### Gallery Click Feedback
181
+
182
+
Clicking a file card used to have a visible delay before the detail page appeared, leading users to click twice (and briefly see the same file loaded twice). Fixed with a three-layer approach:
183
+
184
+
1.**Instant press state** — dims the clicked card (opacity 0.72, scale 0.985) and disables `pointer-events` on click
185
+
2.**Double-click guard** — the `navigatingTo` state variable prevents the second click from firing
186
+
3.**Prefetch on mousedown** — kicks off `/files/{uuid}`~50-100ms before the click handler runs, giving the browser a head start
187
+
188
+
Users now get immediate visual feedback (~16ms) plus a shorter actual wait.
189
+
190
+
### Collection & Share Modal Polish
191
+
192
+
The Create/Edit Collection modals were bare forms with no explanation. Added intro banners, field hints with `maxlength` indicators, and pre-selected the Universal Content Analyzer prompt (the system default) so new collections work out of the box.
193
+
194
+
The Share Collection modal got the biggest facelift: an introductory sentence explaining what sharing does, a collection name banner with a folder icon, and a permission-level reference card showing Viewer/Editor labels inline with descriptions (previously those descriptions only appeared in tooltips, invisible to most users). Empty state added for collections with no existing shares so the modal doesn't look broken on first open.
195
+
196
+
Also fixed a long-standing visual glitch where the Manage Collections modal showed a gray "card within a card" — the inner panel had its own surface background that duplicated the outer modal container.
197
+
133
198
## Lessons from 1,400 Podcasts
134
199
135
200
Processing real data at scale taught us things that unit tests never would:
0 commit comments