refactor(frontend): move statusDotClass/statusLabel from person/ to document/ #424
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Context
Flagged by @mkeller and @felixbrandt in PR #422 review.
statusDotClassandstatusLabelare helpers that return CSS class strings and display labels forDocumentStatusvalues (PLACEHOLDER,UPLOADED,TRANSCRIBED,REVIEWED,ARCHIVED). Document status is a concept owned by the document domain.Currently these functions live in
$lib/person/personFormat.ts— a pre-existing placement that this refactor was the right moment to fix.DocumentStatusChipimports them from the person package, creating a domain boundary leak.Current state
Target state
Move
statusDotClassandstatusLabelinto$lib/document/documentStatusLabel.ts, which already exists and exportsformatDocumentStatus. The document domain becomes self-contained.Update
DocumentStatusChip.svelteand any other consumers to import from$lib/document/documentStatusLabel.Acceptance criteria
statusDotClassandstatusLabelare defined in$lib/document/documentStatusLabel.ts$lib/person/personFormat.tsno longer exports document-status functionsDocumentStatusChip.svelteimports from$lib/document/documentStatusLabelnpm run checktype-checks cleanRelated
🏛️ Markus Keller — Application Architect
Observations
$lib/person/personFormat.ts, creating an inward dependency from the person domain into the document domain. This breaks the rule that each module owns its own data, logic, and API surface.507fa088andd9a4faf4on the current branch (feat/issue-411-legibility-cleanup) completed the migration:statusDotClassand the formerstatusLabelalias are now in$lib/document/documentStatusLabel.ts. The alias was subsequently eliminated and callers useformatDocumentStatusdirectly — which is the right call, since a one-liner alias with no additional behaviour is noise.boundaries/dependenciesESLint rule is now in place (eslint-plugin-boundariesineslint.config.js), withpersononly allowed to depend onshared. This means the previous violation would be caught automatically going forward — the structural fix and the enforcement mechanism land together, which is the correct order.DocumentMetadataDrawer.svelteimport now correctly reads from$lib/document/documentStatusLabel(verified in the working tree). The invites page has a localstatusLabelfunction, but that handles invite statuses (active/exhausted/revoked/expired), notDocumentStatus— no confusion there.CLAUDE.mdor architectural diagram updates appear necessary: no new routes, packages, or domain concepts were introduced. This is a file-level relocation within an existing domain.Recommendations
feat/issue-411-legibility-cleanupbranch merges. The acceptance criteria are fully met in the working tree.npm run lintwith the boundaries rule enabled as part of the PR CI gate — it will catch any future regression of this pattern at the import level rather than requiring a code review to spot it.statusDotClassfunction currently returnsstringbut TypeScript will warn because theswitchhas nodefaultarm. SinceDocumentStatusis a union type the compiler knows is exhaustive, this is fine withnoImplicitReturns: false— but adding adefault: return 'bg-gray-400'as a defensive fallback costs nothing and removes the TypeScript noise if the status enum ever gains a new value before the function is updated.👨💻 Felix Brandt — Fullstack Developer
Observations
feat/issue-411-legibility-cleanup. Two commits tell the story clearly:507fa088moves the functions,d9a4faf4removes the one-linerstatusLabel()alias that was just forwarding toformatDocumentStatus(). That second commit is the right refactor call — don't create a named alias for a function that already has a good name.documentStatusLabel.tsnow owns three distinct export shapes:DocumentStatus(the type),formatDocumentStatus(localized label), andstatusDotClass(Tailwind CSS class). All three are document-domain concerns. The file name and the exports are aligned.DocumentStatusChip.svelteis small (23 lines) and uses$derivedcorrectly for both derived values. The component is a single visual concern — a coloured dot with an accessible title. Naming and size are both correct.documentStatusLabel.spec.ts) coversformatDocumentStatusandstatusDotClassacross all five status values plus the unknown fallback forformatDocumentStatus. That is complete coverage for pure functions of this shape.statusDotClasstests grouped under adescribe('statusDotClass', ...)block but each test reads'PLACEHOLDER → bg-gray-400'— the→arrow is a nice convention, keep it. It reads like a truth table, which is exactly what this is.statusDotClassswitch has nodefaultarm and its return type isstring(inferred). TypeScript will catch this if a newDocumentStatusvalue is added without updating the switch — but only ifstrictmode includesnoImplicitReturns. Worth checking; if it is not enabled, the function silently returnsundefinedfor an unknown status value, which passes astringreturn type via TypeScript's implicitanywidening.Recommendations
defaultarm tostatusDotClassreturning a neutral fallback (e.g.,'bg-gray-300') and add a corresponding spec test. This makes the exhaustive contract visible, protects against silentundefinedreturns, and documents intent:DocumentStatusChip.svelte, cleanpersonFormat.ts, tests passing, andnpm run checkwill be clean.⚙️ Tobias Wendt — DevOps & Platform Engineer
Observations
eslint-plugin-boundariesrule introduced alongside this fix (feat/issue-411-legibility-cleanupbranch) enforces the domain boundary at lint time. That means CI will block future violations automatically — this is the correct approach. A manual code-review catch like the one that triggered this issue should not be the primary enforcement mechanism.npm run lintstep in the CI workflow should runboundaries/dependenciesas an error-level rule (which it already is, pereslint.config.js). Confirm this step is not skipped or gated behind a flag in the Gitea Actions workflow.Recommendations
npm run lintis a required step and not advisory. If it currently only runsprettierandeslintwithout the boundaries plugin enabled, the automated guard has a gap. Runnpm run lintlocally on the feature branch and confirm the boundaries rule fires on a deliberate cross-domain import before merging — that confirms the guard is live in CI too.📋 Elicit — Requirements Engineer
Observations
refactor(frontend):prefix, which correctly signals: no user-visible behaviour change, no new feature, pure structural improvement. This is important for stakeholder communication — family members using the archive will notice nothing; the codebase becomes more maintainable.P3-laterandrefactor. Given that the fix landed in the same session (commits507fa088andd9a4faf4same day), the priority label is now moot — but the label taxonomy is correct.Recommendations
P3-laterrefactor that is already done, this is fine. If the epic (#406) has a milestone, consider assigning it retroactively for completeness of the roadmap history.🔒 Nora "NullX" Steiner — Security Engineer
Observations
statusDotClassreturns a Tailwind CSS class string that is interpolated directly into theclassattribute inDocumentStatusChip.svelte:dotClassis derived from a backend-controlledDocumentStatusenum value via a closedswitchstatement — there is no user-controlled string interpolated here. The values are hardcoded CSS class names. No XSS vector exists.aria-labelon the span uses the localizedlabelstring (fromformatDocumentStatus), which also comes from a closed switch over a backend enum. No user input reaches the label.Recommendations
🧪 Sara Holt — QA Engineer
Observations
documentStatusLabel.spec.tscovers bothformatDocumentStatusandstatusDotClassacross all fiveDocumentStatusvalues, plus the unknown-input fallback forformatDocumentStatus. That is complete coverage for pure functions with a finite, closed domain.→arrow convention (e.g.,'PLACEHOLDER → bg-gray-400') which reads as a truth table — clean and appropriate for this shape of test.personFormat.spec.tsno longer contains document-status test cases (confirmed — the functions were removed frompersonFormat.tsalong with their specs). No orphaned tests remain../documentStatusLabel(relative path), not via the$libalias. This is consistent with how co-located specs are typically written and is fine.statusDotClasshas nodefaultarm. If a newDocumentStatusvalue is added (e.g.,'DELETED'), the function returnsundefinedat runtime, but the existing spec would still pass because it only tests the five known values. The gap in the tests mirrors the gap in the production code — they rise and fall together.Recommendations
statusDotClassunknown-value fallback, parallel to theformatDocumentStatusunknown-value test that already exists:defaultarm is added to the production function (see Felix's recommendation). Do both together, red/green.npm run testto confirm before merging.DocumentStatusChip.svelteis not present as a separate spec — the component is simple enough (pure derived values, no interactivity) that unit coverage viadocumentStatusLabel.spec.tsis sufficient. No gap here.🎨 Leonie Voss — UI/UX Designer & Accessibility Strategist
Observations
DocumentStatusChip.svelteis identical before and after — same Tailwind classes, samearia-label, sametitleattribute.aria-labelfor screen readers andtitlefor tooltip-on-hover. This is a reasonable approach, but there is a nuance:aria-labelon a<span>with no interactive role or ARIA role does not create an accessible name that screen readers consistently announce. Arole="img"would make thearia-labelsemantically meaningful:role="img", thearia-labelis present but may be ignored by some screen readers because the element has no implicit ARIA role that accepts naming.hidden md:block— invisible on small screens. This means mobile users (and this audience includes seniors on tablets) see no status indicator at all. That is a pre-existing design decision, not introduced by this issue, but it is worth flagging as a separate accessibility gap.statusDotClassuse Tailwind's semantic palette (emerald, blue, amber, gray). These should be verified for contrast against the card background (bg-surface). A 16x16px colour dot with no text inside is exempt from the 4.5:1 text contrast ratio, but the dot should still be visually distinguishable — colour alone carries the meaning here, which is a WCAG 1.4.1 (Use of Color) concern. Thetitle/aria-labelprovides the textual alternative, which mitigates this.Recommendations
role="img"to the<span>inDocumentStatusChip.svelte. This is a one-character fix that makes thearia-labelsemantically binding for screen readers. This can go in the same PR as the refactor since the component is already being touched.Decision Queue — Consolidated Open Items
No genuine blocking decisions remain for this issue — the core refactor is complete and the acceptance criteria are met. However, the review surfaced three small, concrete action items worth tracking before closing:
1. Defensive fallback in
statusDotClass+ matching test (Felix, Sara)The function's
switchhas nodefaultarm. If a newDocumentStatusvalue arrives from the backend before the frontend is updated, the function silently returnsundefinedand the dot renders with no class. Add adefault: return 'bg-gray-300'arm and a spec test for it. Do both in one red/green cycle. This is a two-line production change and a three-line test.2. Add
role="img"toDocumentStatusChip's<span>(Leonie)The
aria-labelattribute on a plain<span>without a named ARIA role is not reliably announced by all screen readers. Addingrole="img"makes the label semantically binding per the ARIA spec. One attribute, zero visual change, measurable accessibility improvement. Can go in the same commit as the refactor since the file is already being touched.3. Verify
npm run lint(withboundaries/dependencies) is a required CI gate (Tobias)The ESLint boundaries rule enforces domain isolation automatically. Confirm it runs as a required step in the Gitea Actions workflow — not advisory, not skipped. A quick manual confirmation (
npm run linton a deliberate cross-domain import should return a non-zero exit code) is sufficient to close this item.The mobile-invisible status dot (
hidden md:block) is a pre-existing design gap flagged by Leonie — it is explicitly out of scope for this issue and should be filed as a separate accessibility issue if prioritised.