refactor(frontend): utility dedup, component splits, dead code removal (#193–#200) #241
Reference in New Issue
Block a user
Delete Branch "refactor/issues-193-200"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Eight coordinated frontend refactor issues implemented in one branch:
conversations/route (5 files deleted, 14 orphaned i18n keys removed, E2E tests cleaned up)relativeTime→time.ts,getInitials→personFormat.ts,formatDateconsolidation, blob URL leak fix,useFileLoader.svelte.ts,useUnsavedWarning.svelte.ts+UnsavedWarningBanner.svelteNotificationBellfrom inline impl touse:clickOutside; add missing test fordefaultPreventedguardPdfViewer.svelte: extractusePdfRenderer.svelte.ts+PdfControls.svelte; orchestrator now ~200 linesEntityNavSection.svelte: eliminate ~350 lines of repeated admin nav section markupCommentMessage.svelte: moveFlatMessagetype tolib/types.ts, extractextractQuotetocomment.ts, addrole="log"/role="article"accessibilityTranscriptionEditView.svelte: extractuseBlockAutoSave.svelte.ts+useBlockDragDrop.svelte.ts; fix fade timer leakNotificationBell.svelte: extractuseNotificationStream.svelte.ts+NotificationDropdown.svelte; persistentaria-livewrapper; unread count resync on SSE reconnectTest plan
cd frontend && npm run test— all 813 tests greencd frontend && npm run check— no type errorscd frontend && npm run lint— Prettier + ESLint cleancd frontend && npm run build— production build succeeds🤖 Generated with Claude Code
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Solid batch refactor overall. Factory function pattern is consistent, runes are used correctly in
.svelte.tsmodules, and each extracted module has a clear single responsibility. Two things need fixing before merge.Blockers
1. Hardcoded German hint text in
CommentMessage.svelteThis bypasses Paraglide entirely. Project rule: all user-visible strings go through
m.*keys. Add a key likem.comment_edit_hint()tode.json,en.json,es.jsonand replace the hardcoded text.2.
relativeTimeimported from wrong module inNotificationDropdown.svelteIssue #194 was specifically about extracting
relativeTimeto the canonical$lib/utils/time.ts. This component should import directly from there. Importing throughnotifications.tscreates an accidental dependency on a module that has nothing to do with timestamps conceptually, and prevents the linter/tree-shaker from seeing the coupling clearly.Fix:
import { relativeTime } from '$lib/utils/time';Suggestions
handleBluris fire-and-forget inuseBlockAutoSave.svelte.tsThis is intentional (blur shouldn't block the UI), but if two blocks both have pending saves on blur, the untracked promises can overlap. The current behavior is probably fine since
executeSavecheckspendingTextsbefore doing anything, but a brief comment here would save the next developer from the same question I just had.useUnsavedWarning.svelte.ts—discard()silently no-ops whendiscardTargetis nullIf the user hits "Discard" and
discardTargetis null (e.g. a programmatic navigation that didn't produce atoURL), the warning disappears but the user stays on the current page. This is probably unreachable in practice, but documenting it as a known edge case would be good.🏗️ Markus Keller — Application Architect
Verdict: ✅ Approved
The refactor achieves what it set out to do: clear boundaries between visual orchestrators and logic modules, consistent factory function naming, and correct use of
.svelte.tsfor rune-bearing modules. No architectural regressions. A few structural observations worth noting.What's Well Done
The
createX()naming convention is now consistent across all new modules (createBlockAutoSave,createBlockDragDrop,createNotificationStream,createFileLoader,createUnsavedWarning). This is the right pattern for Svelte 5 reactive modules that live outside components — it establishes a codebase-wide convention that future developers can follow.Module boundary discipline is correct:
useBlockDragDropexposes only visual state (draggedBlockId,dropTargetIdx,dragOffsetY) while the orchestrator owns data and the API call. This is exactly the right split.Observations (Non-blocking)
beforeNavigateat module initialization inuseUnsavedWarning.svelte.tsbeforeNavigateis a SvelteKit lifecycle function that must be called synchronously during component initialization (it registers on the nearest$app/navigationcontext). Calling it inside a factory function works only if the factory is called in<script>at component mount time (which it is in the three admin pages). This is currently safe, but it's a hidden constraint: callingcreateUnsavedWarning()outside of component initialization (e.g., in a$effect, setTimeout, or a non-component module) would silently fail. A comment documenting this constraint would prevent future misuse.relativeTimere-export chain innotifications.tsNow that
time.tsis the canonical source,notifications.tsshould import and re-export from there (or consumers should import directly fromtime.ts). CurrentlyNotificationDropdown.svelteimportsrelativeTimefromnotifications.ts— which means the utilities are still coupled. Minor, but the whole point of the extraction was to break that coupling.EntityNavSection.svelteinroutes/admin/rather thanlib/components/This is intentional (it's route-specific UI), and I agree with the placement. Just noting it as a deliberate choice —
lib/components/is for reusable cross-route components; admin-specific components that won't be shared belong in the route folder.🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
813 tests passing is a solid baseline. The new modules all have unit tests, and the use of
vi.useFakeTimers()inuseBlockAutoSaveis exactly right for debounce testing. A few coverage gaps worth addressing.Concerns
1. Drag-drop reorder logic has no test coverage
useBlockDragDrop.svelte.tshas 4 tests covering initial state and basic pointer events. The actual reorder calculation inhandlePointerUp— thesplice/insertAtindex arithmetic — is untested:The
off-by-onerisk here is high (dropTargetIdx - 1vsdropTargetIdx). I'd want at minimum:-1branch)onReorderis not called whendropTargetIdxis null2.
useNotificationStream— SSE event handling not testedThe test suite covers
fetchUnreadCount,fetchNotifications,markRead,markAllRead, anddestroy. Missing: a test that fires anotificationevent on theMockEventSourceand verifies thatnotificationsis prepended andunreadCountincrements. This is the core SSE behavior that motivated the extraction.The
MockEventSourcehasaddEventListenerbut there's no test that calls the registered listener.3.
useUnsavedWarninghas no tests at allNo test file exists for this hook. Given it controls a navigation guard (
beforeNavigate), the missing coverage formarkDirty → navigate → warning shownanddiscard → navigation resumesis a risk.What's Good
vi.useFakeTimers()+vi.advanceTimersByTimeAsync()for debounce testing is the right approach — noThread.sleepequivalents.makeNotification()factory function inuseNotificationStream.svelte.test.tsis clean.useBlockAutoSavetests cover the error→retry path explicitly — good edge case coverage.🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
Pure frontend structural refactor — no new API endpoints, no new authentication surfaces, no new data flows. The security posture is unchanged. I audited the two spots that warranted close attention.
What I Checked
{@html renderBody(...)}inCommentMessage.svelteThe comment claims
renderBodyescapes HTML before injecting mention links. This is the right pattern if the implementation actually does what it claims — sanitize user content, then inject trusted markup for mention anchors. The ESLint suppression with an inline justification is correct practice. Verify: confirmrenderBodyin$lib/utils/mention.tsescapes the rawbodystring before wrapping mentions in<a>tags. If it usesinnerHTMLor string interpolation without escaping, that's a stored XSS via comment content.URL construction in
useNotificationStream.svelte.tsnotification.idis a TypeScriptstringsourced from the API response. It is not user-entered text and won't contain path traversal or injection payloads from the frontend. Safe as-is.navigator.sendBeaconinuseBlockAutoSave.svelte.tsThe plan's comment that
sendBeaconsilently fails in production because the app usesAuthorizationheaders (not cookies) andsendBeaconcannot send custom headers is accurately preserved. The code works in the browser for unload flushing but the data won't reach the server in production. This is a pre-existing bug, correctly out of scope for this refactor. File the bug separately.No New Issues Found
No XSS vectors, no IDOR risks, no auth bypass surface changes introduced by this PR.
🎨 Leonie Voss — UI/UX Design Lead & Accessibility Strategist
Verdict: ⚠️ Approved with concerns
The accessibility improvements in this PR are real:
role="log"on the message list,role="article"on individual messages, persistentaria-livewrapper for notification count, and delete button touch target increased top-2. Good progress. But two things need fixing before merge.Blockers
1.
text-[9px]inEntityNavSection.svelte— below 12px minimumThis appears in both the tablet button and the flyout/desktop link:
9px is unreadable for the senior (60+) audience and fails WCAG's practical accessibility threshold. The desktop link uses
text-[13px]which is marginally acceptable, but9pxfor any visible text is a blocker. These labels are existing values being preserved from the originalEntityNav.svelte— this refactor is the right time to fix them.Fix: Use
text-[11px]at minimum (matches other compact nav labels in the codebase), preferablytext-xs(12px).2. Hardcoded German in
CommentMessage.svelteedit hintThis is not an i18n concern only — it's a UX issue. English-speaking users see German instructions in an otherwise translated interface. This hint is visible every time someone edits a comment. Needs a Paraglide key.
Suggestions
UnsavedWarningBanner.svelteuses raw Tailwind amber valuesThe project's design system uses semantic tokens (
bg-surface,text-ink,border-line). The amber colors are raw Tailwind values that won't automatically participate in theming. This is preserved from the original admin pages, so it's a pre-existing issue — but worth flagging for a future cleanup ticket.NotificationDropdownclose button has no explicit labelThe "View all" link at the bottom of the dropdown closes the dropdown via
onclick={onClose}. The link itself has text content (m.notification_view_all()), so screen readers will announce it correctly. But the overall dropdown hasrole="dialog"with anaria-labelpointing tom.notification_bell_label()— make sure that key returns something meaningful like "Benachrichtigungen" rather than a generic label.⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
Pure frontend refactor — no Docker Compose changes, no CI workflow changes, no new npm dependencies. Nothing here touches infrastructure.
What I Checked
package.jsonorpackage-lock.json.gitea/workflows/ordocker-compose.ymlnpm run buildis listed in the PR test plan — good, confirms no bundle regressionsThe extraction of
usePdfRendererwith the dynamicimport('pdfjs-dist')pattern preserves the existing code-split behavior — PDF.js is still loaded lazily and won't bloat the initial bundle.813 tests green, build passes. Ship it (after the blockers from Felix and Leonie are addressed).
Review concerns addressed
All blockers and test coverage concerns from the review have been resolved. 818 tests pass (up from 813).
Changes made
Blockers fixed:
2b93ccf—relativeTimeinNotificationDropdown.sveltenow imports from the canonical$lib/utils/timeinstead of$lib/utils/notifications— fixes @felixbrandt7fb6ec0— Addedcomment_edit_hintParaglide key to de/en/es message files; replaced hardcoded"Enter speichern · Esc abbrechen"inCommentMessage.sveltewithm.comment_edit_hint()— fixes @felixbrandt + @leonievoss45490eb— Changed all threetext-[9px]occurrences inEntityNavSection.sveltetotext-[11px](tablet button count badge, desktop link label, flyout link label) — fixes @leonievossTest coverage:
8739511— AddedMockEventSource.simulate(type, data)helper and two SSE event tests: unread notification prepends to list and incrementsunreadCount; read notification adds to list without incrementing count — fixes @saraholted2c023— AddedsimulateDragDrophelper with propergetBoundingClientRectmocks and three reorder tests covering move-to-end, move-to-start, and move-down-one-position (verifies thedropTargetIdx - 1branch) — fixes @saraholtNot acted on:
useUnsavedWarning.svelte.test.tsalready exists with 8 tests covering dirty state, navigation cancellation, and discard flow. The file was missed during the review scan.handleBlurfire-and-forget — noted but not changed; the behavior is intentional and documented by the existing tests.beforeNavigateconstraint inuseUnsavedWarning— good point for a follow-up but not blocking.