fix(paperless): review feedback — paperlessUrl passthrough, design tokens, a11y#367
fix(paperless): review feedback — paperlessUrl passthrough, design tokens, a11y#367steilerDev wants to merge 7 commits into
Conversation
Implements the Document Browser page for Paperless-ngx integration: - `client/src/lib/paperlessApi.ts`: API client for status, list, get, tags, thumbnail URL, preview URL - `client/src/hooks/usePaperless.ts`: React hook managing status/documents/tags/pagination/search/filter state - `client/src/components/documents/DocumentBrowser.tsx`: Main reusable component with page/modal modes, search bar, tag filter strip, document grid, detail panel, and pagination - `client/src/components/documents/DocumentCard.tsx`: Individual card with thumbnail, title, date, tags, accessible button role - `client/src/components/documents/DocumentDetailPanel.tsx`: Expanded detail panel with metadata, content snippet, external link - `client/src/components/documents/DocumentSkeleton.tsx`: Loading skeleton cards with shimmer animation - All CSS modules use design tokens, no hardcoded hex values, responsive grid (3/2/1 col) - Replaces stub DocumentsPage with full implementation delegating to DocumentBrowser - Tests for all new modules (95%+ coverage target) Fixes #356 Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com> Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Replace inline import() type annotations with top-level import type * patterns to satisfy @typescript-eslint/consistent-type-imports rule. Also fix TypeScript type assertion for mock call args in usePaperless.test.tsx. Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
The test `does not render date when created is null` used `/2025/` which also matched the document title "Test Invoice 2025". Narrowed to a pattern matching formatted date strings (e.g. "Mar 15, 2025") only. Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
…advisory (PR #364) Advances wiki submodule pointer to include the proactive security advisory for searchHit.highlights HTML rendering risk, added during Story 8.3 security review. Co-Authored-By: Claude security-engineer (Sonnet 4.6) <noreply@anthropic.com>
…esign tokens, a11y - Add paperlessUrl field to PaperlessStatusResponse shared type - Expose paperlessUrl in server status endpoint and propagate through service, hook, and component props so "View in Paperless-ngx" link renders - Replace hardcoded spacing/padding/font-size/border-radius values with CSS custom properties (design tokens) across all document components - Fix card border weight (2px → 1px), grid breakpoint overlap (1024 → 1023), and selected-card box-shadow to use primary-bg ring - Add focus-visible states with shadow-focus-subtle on buttons - Add aria-label on tag filter chips, aria-expanded/aria-controls on cards, id on detail panel for accessible pairing - Add 44px min-height touch targets for tag chips on mobile - Add prefers-reduced-motion: reduce media query for skeleton shimmer - Fix deferred type imports in test files (after mock registration) - Add tests for paperlessUrl passthrough and null cases Fixes #356 Co-Authored-By: Claude <noreply@anthropic.com>
3e31b02 to
31e649b
Compare
steilerDev
left a comment
There was a problem hiding this comment.
[product-architect]
Architecture Review: PR #367 — Review Feedback Fixes (Story #356)
Verdict: APPROVED
This refinement PR addresses the product-owner's AC #7 failure and the ux-designer's design/accessibility findings from PR #364. I reviewed all 25 changed files against the API Contract, shared types, and established architectural patterns.
paperlessUrl passthrough chain — VERIFIED CORRECT
The fix correctly threads the URL through all four layers:
-
Shared types (): added to . Correct field name, correct nullable type.
-
Service (): Returns as a placeholder. The comment explains this is overridden at the route layer. This is architecturally correct — the service's job is to check reachability, not to know the config URL.
-
Route (): The route handler appends to the response via object spread. When is false, it returns . Both code paths are consistent.
-
Frontend ( line 833): — correctly converts to so the optional prop is absent (not ), which prevents from rendering a broken link.
The service returning while the route overrides it is slightly awkward architecturally. A future refactor could pass the URL into as a parameter. But it works correctly and the comment explains the intent — acceptable for now.
Test coverage for new behavior — VERIFIED
- : Asserts for not-configured, for configured+reachable and configured+unreachable. All three code paths covered.
- : Updated to reflect the shape.
- Client tests (, ): The helper includes in the status shape, keeping tests consistent with the updated type.
CSS token compliance — VERIFIED (with minor residual gaps)
The bulk of the MEDIUM token findings from PR #364 are addressed. All four CSS module files now use , , and consistently. The breakpoint overlap fix ( instead of ) is present and correct.
Two residual hardcoded values remain in :
- — should be per the ux-designer's original finding
- — same
These are LOW severity (the ux-designer's original note listed in as a token finding, but the same pattern applies to card tag chips). Not blocking, but should be tracked.
ARIA and accessibility — VERIFIED
- (not ) on — correct
- wired up when a card is selected — correct pairing
- on tag filter chips with count context — correct
- states on , , — present
- 44px mobile touch target for — present
- in — present
Three LOW items from the ux-designer remain unaddressed: search input , search bar icon, and tag strip mask gradient. These were LOW severity and the PR summary does not claim to address them — acceptable deferral.
No architectural regressions
No new patterns introduced. All changes follow existing conventions. The field name is appropriate (not which would be ambiguous, not which would be too generic).
Summary
The critical AC #7 failure from PR #364 is fully resolved. The paperlessUrl passthrough chain is clean and correctly handled at each layer. Token compliance, ARIA attributes, and focus states are substantially improved. Three LOW ux-designer items remain deferred — no architecture concerns with that.
steilerDev
left a comment
There was a problem hiding this comment.
[product-architect]
Architecture Review: PR #367 — Review Feedback Fixes (Story #356)
Verdict: APPROVED
This refinement PR addresses the product-owner's AC #7 failure and the ux-designer's design/accessibility findings from PR #364. I reviewed all 25 changed files against the API Contract, shared types, and established architectural patterns.
paperlessUrl passthrough chain — VERIFIED CORRECT
The fix correctly threads the URL through all four layers:
-
Shared types (
shared/src/types/document.ts):paperlessUrl: string | nulladded toPaperlessStatusResponse. Correct field name, correct nullable type. -
Service (
server/src/services/paperlessService.ts): ReturnspaperlessUrl: nullas a placeholder. The comment explains this is overridden at the route layer. This is architecturally correct — the service's job is to check reachability, not to know the config URL. -
Route (
server/src/routes/paperless.ts): The route handler appendspaperlessUrl: fastify.config.paperlessUrl ?? nullto the response via object spread. WhenpaperlessEnabledis false, it returnspaperlessUrl: null. Both code paths are consistent. -
Frontend (
client/src/components/documents/DocumentBrowser.tsxline 833):paperlessBaseUrl={hook.status.paperlessUrl ?? undefined}— correctly convertsnulltoundefinedso the optional prop is absent (notnull), which preventsDocumentDetailPanelfrom rendering a broken link.
The service returning paperlessUrl: null while the route overrides it is slightly awkward architecturally. A future refactor could pass the URL into getStatus() as a parameter. But it works correctly and the comment explains the intent — acceptable for now.
Test coverage for new behavior — VERIFIED
server/src/routes/paperless.test.ts: AssertspaperlessUrl: nullfor not-configured,paperlessUrl: 'http://paperless:8000'for configured+reachable and configured+unreachable. All three code paths covered.server/src/services/paperlessService.test.ts: Updated to reflect thepaperlessUrl: nullshape.- Client tests (
DocumentBrowser.test.tsx,DocumentsPage.test.tsx): ThemakeHook()helper includespaperlessUrl: nullin the status shape, keeping tests consistent with the updated type.
CSS token compliance — VERIFIED (with minor residual gaps)
The bulk of the MEDIUM token findings from PR #364 are addressed. All four CSS module files now use var(--spacing-*), var(--font-size-*), and var(--radius-*) consistently. The breakpoint overlap fix (max-width: 1023px instead of 1024px) is present and correct.
Two residual hardcoded values remain in DocumentCard.module.css:
.tagChip font-size: 0.625rem— should bevar(--font-size-xs)per the ux-designer's original finding.tagChipMore font-size: 0.625rem— same
These are LOW severity. Not blocking, but should be tracked.
ARIA and accessibility — VERIFIED
aria-expanded(notaria-pressed) onDocumentCard— correctaria-controls="detail-panel"wired up when a card is selected — correct pairingaria-labelon tag filter chips with count context — correctfocus-visiblestates oncloseButton,retryButton,pageButton— present- 44px mobile touch target for
tagChip— present prefers-reduced-motioninDocumentSkeleton.module.css— present
Three LOW items from the ux-designer remain unaddressed: search input aria-controls, search bar icon, and tag strip mask gradient. These were LOW severity and the PR summary does not claim to address them — acceptable deferral.
No architectural regressions
No new patterns introduced. All changes follow existing conventions. The paperlessUrl field name is appropriate (not baseUrl which would be ambiguous, not url which would be too generic).
Summary
The critical AC #7 failure from PR #364 is fully resolved. The paperlessUrl passthrough chain is clean and correctly handled at each layer. Token compliance, ARIA attributes, and focus states are substantially improved. Three LOW ux-designer items remain deferred — no architecture concerns with that.
steilerDev
left a comment
There was a problem hiding this comment.
[security-engineer]
Security Review: PR #367 — Review Feedback Fixes (Story #356)
Verdict: APPROVED — No new security concerns introduced
This refinement PR makes targeted fixes to address review feedback from PR #364. My original security review found no issues with the frontend rendering (no dangerouslySetInnerHTML, XSS-safe content rendering, correct rel="noopener noreferrer" on external links). This review focuses on the new changes introduced in PR #367.
New surface: paperlessUrl exposed in API response
Assessment: PASS
The GET /api/paperless/status endpoint now returns paperlessUrl (the value of PAPERLESS_URL from server config) when Paperless-ngx is configured. Security considerations:
Authentication coverage: The /api/paperless/status endpoint is protected by the global Fastify auth plugin (all /api/* routes require authentication). An unauthenticated caller cannot retrieve the paperlessUrl. The URL is only exposed to authenticated Cornerstone users.
Information disclosure: The paperlessUrl value (e.g., http://paperless:8000) is the internal Docker network address of the Paperless-ngx instance. Exposing this to authenticated clients is acceptable — self-hosted Cornerstone instances have 1-5 users who are all trusted. The URL is used client-side to construct the "View in Paperless-ngx" external link (the same URL that would appear in the browser's address bar when navigating to the document).
No token exposure: The PAPERLESS_API_TOKEN remains server-side only. Only the base URL is passed through, not the API token.
External link security — VERIFIED UNCHANGED
The DocumentDetailPanel rel="noopener noreferrer" attributes were correct in PR #364 and remain correct in this PR. No regression.
Shared type change — VERIFIED SAFE
paperlessUrl: string | null added to PaperlessStatusResponse in shared/src/types/document.ts. This is a TypeScript-only type definition used for compile-time safety. No runtime security impact.
No new dependencies introduced
Zero new npm packages. No new attack surface from third-party code.
Proactive advisory from PR #364 — Still applicable
My original advisory about searchHit.highlights (HTML content from Paperless-ngx that must not be rendered via dangerouslySetInnerHTML) remains relevant for future stories. This PR does not render searchHit — the advisory stands for Story 8.4/8.5.
Summary
The paperlessUrl passthrough is the only new server-side surface in this PR. It is protected by authentication and exposes only the Paperless-ngx base URL (not the API token) to authenticated users. No security regressions from the CSS/ARIA/UX fixes. Approved.
steilerDev
left a comment
There was a problem hiding this comment.
[product-owner]
Story #356: Document Browser & Search UI — Requirements Review (PR #367)
Verdict: APPROVED
My original REQUEST CHANGES on PR #364 had one failure: AC #7 — "View in Paperless-ngx" link never rendered because paperlessUrl was not passed through the stack. This PR directly addresses that failure.
AC #7 Re-assessment: PASS
The complete fix chain I specified in my original review has been implemented:
- Shared type:
paperlessUrl: string | nulladded toPaperlessStatusResponse— done. - Server service: Returns
paperlessUrl: nullas a placeholder — done. - Server route: Returns
paperlessUrl: fastify.config.paperlessUrl ?? nullin the status response — done. - Hook:
status.paperlessUrlis already available as part of thestatusobject returned byusePaperless. No separate exposure needed — done by inclusion. - DocumentBrowser:
paperlessBaseUrl={hook.status.paperlessUrl ?? undefined}passed toDocumentDetailPanel— done at line 833.
The "View in Paperless-ngx" link will now render correctly when PAPERLESS_URL is configured in the server environment.
Updated Acceptance Criteria Scorecard
| # | Criterion | Verdict |
|---|---|---|
| 1 | Documents page replaced from stub with functional browser | PASS |
| 2 | Document list displays thumbnail, title, created date, correspondent, tags | PASS |
| 3 | Search input for filtering by title/content (Paperless-ngx search API) | PASS |
| 4 | Tag chips clickable to filter document list by tag | PASS |
| 5 | Pagination supported | PASS |
| 6 | Document detail view with full metadata | PASS |
| 7 | "View in Paperless-ngx" link opens document in new tab | PASS |
| 8 | Not configured state with setup message | PASS |
| 9 | Unreachable state with retry option | PASS |
| 10 | Loading states shown | PASS |
| 11 | Empty state shown | PASS |
| 12 | Reusable component for modal embedding | PASS |
Result: 12/12 criteria pass.
Test verification
The new test assertions in server/src/routes/paperless.test.ts verify paperlessUrl is present in both the configured+reachable and configured+unreachable responses. The DocumentDetailPanel test for the "View in Paperless" link was already passing in PR #364 (the component implemented the prop correctly; the issue was that the prop was never passed from DocumentBrowser).
Scope assessment
No scope creep. All changes are within the boundaries of Story #356 or are clean-up of gaps identified in the original review.
steilerDev
left a comment
There was a problem hiding this comment.
[ux-designer]
Design Review — PR #367: Review Feedback Fixes (Story #356)
Verdict: APPROVED (with two residual low items noted below)
This refinement PR addresses all 7 MEDIUM findings and 3 of the 5 LOW findings I raised in my PR #364 review. I verified each item against the implementation.
MEDIUM Findings — All Resolved
1. Hardcoded spacing/size values — RESOLVED
All four CSS module files now use design tokens consistently. Spot-checked:
DocumentBrowser.module.css:gap: var(--spacing-4),padding: var(--spacing-2) var(--spacing-3),border-radius: var(--radius-md),font-size: var(--font-size-sm), etc.DocumentCard.module.css:border-radius: var(--radius-lg),padding: var(--spacing-3),gap: var(--spacing-1),font-size: var(--font-size-sm), etc.DocumentDetailPanel.module.css:padding: var(--spacing-5),gap: var(--spacing-4),font-size: var(--font-size-lg),gap: var(--spacing-1-5) var(--spacing-4), etc.DocumentSkeleton.module.css:border-radius: var(--radius-lg),padding: var(--spacing-3),gap: var(--spacing-2),border-radius: var(--radius-sm).
2. Grid breakpoint overlap — RESOLVED
Tablet upper bound is now max-width: 1023px (was 1024px). The three-tier breakpoint (desktop ≥1024px: 3 cols; tablet 768–1023px: 2 cols; mobile <768px: 1 col) is now correct with no edge-case overlap.
3. Card border thickness — RESOLVED
DocumentCard.module.css now uses border: 1px solid var(--color-border). Confirmed at line 874 of the diff.
4. Card selected state ring — RESOLVED
.cardSelected now uses box-shadow: 0 0 0 2px var(--color-primary-bg) — the subtle glow from the spec, not the full keyboard focus ring. The visual distinction between "selected" and "keyboard focused" is now correct.
5. prefers-reduced-motion — RESOLVED
DocumentSkeleton.module.css has the @media (prefers-reduced-motion: reduce) block suppressing animation on all four skeleton elements. Confirmed.
6. Tag chip aria-label — RESOLVED
Tag chips now carry aria-label="Filter by tag: {name} ({documentCount} documents)". Screen readers will announce the full context including count. Confirmed in DocumentBrowser.tsx.
7. aria-pressed → aria-expanded on cards — RESOLVED
DocumentCard.tsx now uses aria-expanded={isSelected}. The aria-controls="detail-panel" pairing is also present when a card is selected (the ariaControls prop is set to "detail-panel" only for the selected card). Confirmed.
LOW Findings — Partially Resolved
Resolved:
closeButtonandretryButtonandpageButton:focus-visiblestyles — RESOLVED. All three now havebox-shadow: var(--shadow-focus-subtle)on:focus-visible. Confirmed inDocumentDetailPanel.module.cssandDocumentBrowser.module.css.- Mobile touch targets (44px for tagChip) — RESOLVED.
@media (max-width: 767px)block inDocumentBrowser.module.cssaddsmin-height: 44pxto.tagChip. Confirmed.
Not yet addressed (2 remaining LOW items):
LOW-1: Search input aria-controls linking to results region
DocumentBrowser.tsx search input still does not carry aria-controls="document-results", and the results grid does not have id="document-results". This assistive technology relationship helps AT announce when typing updates the document list. Deferred — LOW severity, acceptable for now.
LOW-2: Search bar leading magnifying-glass icon
Still plain symmetric padding with no decorative search icon. Visual spec deviation. Deferred — LOW severity, acceptable for now.
(The tag strip mask gradient I noted as LOW-3 is also unaddressed. Similarly deferred.)
Also noted: two residual token gaps in DocumentCard.module.css:
.tagChip font-size: 0.625rem— should bevar(--font-size-xs)(informational from original review).tagChipMore font-size: 0.625rem— same
These are minor. The 0.625rem value equals 10px which matches --font-size-xs, but the token should be used for consistency. Not blocking.
What Passes
- All MEDIUM design token, breakpoint, border weight, selected-state, reduced-motion, and accessibility findings from PR #364 are resolved.
- Dark mode still works automatically via CSS custom properties — no regressions.
aria-expanded+aria-controlspairing is architecturally correct: AT will announce "expanded/collapsed" state and can navigate to the associated detail region.- Mobile touch targets for tag chips and pagination buttons meet the 44px minimum.
- Focus rings are consistent with the project's established
var(--shadow-focus)/var(--shadow-focus-subtle)system.
Summary
All MEDIUM design findings addressed. Three LOW items deferred (search icon, aria-controls, mask gradient) — acceptable. Two minor token gaps in card tag chip font-size noted as informational. The visual quality bar is substantially higher than PR #364. Approved.
|
Closing to recreate on a fresh branch — Quality Gates workflow did not trigger on this reused branch. |
Summary
Addresses review feedback from PR #364 (Story #356 — Document Browser & Search UI):
paperlessUrlfield toPaperlessStatusResponseshared type, exposed in server status endpoint, propagated through hook and component props so "View in Paperless-ngx" link actually rendersprimary-bgfocus-visiblestates on buttons,aria-expanded/aria-controlspairing on cards↔detail panel, descriptivearia-labelon tag filter chips, 44px mobile touch targets,prefers-reduced-motionfor skeleton shimmerFixes #356
Changed Files (16)
shared/src/types/document.tsserver/src/routes/paperless.ts,server/src/services/paperlessService.tsserver/src/routes/paperless.test.ts,server/src/services/paperlessService.test.tsclient/src/hooks/usePaperless.tsDocumentBrowser.tsx,DocumentCard.tsx,DocumentDetailPanel.tsxDocumentBrowser.module.css,DocumentCard.module.css,DocumentDetailPanel.module.css,DocumentSkeleton.module.cssDocumentBrowser.test.tsx,DocumentCard.test.tsx,DocumentsPage.test.tsxTest plan
🤖 Generated with Claude Code