As a user I want the dashboard resume strip to show the actual document thumbnail so I recognize what I was working on at a glance #314
Reference in New Issue
Block a user
Delete Branch "feat/issue-309-resume-strip-thumbnail"
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?
Closes #309
Summary
Replaces the generic parchment SVG in the dashboard resume strip with the real document thumbnail, so the "pick up where you left off" card actually shows the user what they were last working on.
Per the Discussion Resolutions, this also unifies thumbnail URL construction on the backend as the single source of truth:
Document.getThumbnailUrl()is now a@JsonPropertycomputed getter on the entity, so everyDocumentJSON response automatically carries the URL.$lib/thumbnails.tshelper has been retired;DocumentThumbnail.sveltereadsdoc.thumbnailUrldirectly.DashboardService.getResume()populates the resume DTO'sthumbnailUrlvia the same getter, replacing the previousnullplaceholder.Design decisions (from the resolutions comment)
DocumentThumbnail) when the document has no thumbnail yet — one "no thumbnail" vocabulary across the app. The parchment SVG survives only in the fully-empty state (no resume document at all).DocumentThumbnaileverywhere else in the app). Issue said "~180×246"; six pixels aligns the card with the rest of the thumbnail surfaces.alt=""(decorative — title carries the semantics), sized container to prevent CLS,dark:mix-blend-multiplyfor paper-scan glare,aria-hidden="true"on the fallback wrapper.Document.getThumbnailUrl()documents that the?v={thumbnailGeneratedAt}query param is load-bearing for the thumbnail endpoint'simmutableheader — dropping it would create a CWE-525 cross-user cache poisoning risk.Coverage
DocumentTest.javawith 3 cases on the getter (null, no-cache-buster, with-cache-buster including exactURLEncoderencoding), plus one wiring test inDashboardServiceTestprovinggetResumepopulates the DTO field from the Document.DashboardResumeStrip.svelte.spec.ts(thumbnail<img>renders with correct attrs when URL set; fallback heroicon renders when URL null), using stabledata-testidselectors on both branches.Final status
./mvnw clean package -DskipTestsBUILD SUCCESS.svelte-checkerrors (242 pre-existing ones untouched).👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
What I checked
Document.getThumbnailUrl()keeps the happy path at the lowest indentation. ✓getThumbnailUrl,mockResumeWithThumbnail,resume-thumbnail-img,resume-thumbnail-fallback. ✓DocumentThumbnail.sveltePick type trimmed to what it actually uses (id | thumbnailUrl | contentType) instead of dragging along retired fields. ✓$derived(doc.thumbnailUrl ?? null)— correct Svelte 5 runes usage, not$state+$effect. ✓Suggestions (non-blocking)
DashboardService.java:85— the inlinedoc.getThumbnailUrl()in the constructor call is fine as-is at two args, but if a next field joins it I'd pull it to a local. Not worth touching now."/api/documents/"is duplicated betweenDocument.getThumbnailUrl()andDocumentController— if the controller's@RequestMapping("/api/documents")ever changes, the getter won't follow. A tiny suggestion for a follow-up: either extract aDocumentEndpoints.BASE_PATHconstant both sides reference, or wire the base URL viaServletUriComponentsBuilderat serialisation time. Not a blocker — the issue's out-of-scope clause on generalisation applies here too.LGTM
Clean, small, and the commit history reads as a TDD textbook. Ship it.
🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
Structure
dashboardandmodelfeature packages. No cross-boundary repository access, no new service dependencies, no new transport protocol. ✓DashboardService.getResume()still routes throughDocumentService.getDocumentById()— the layering rule is intact. ✓thumbnailUrlis computed from fields on the already-loadedDocument. No N+1 risk. ✓Documententity'sthumbnailKeyandthumbnailGeneratedAtcolumns are already present. ✓One caveat I want on the record
Documententity now carries URL-shape knowledge (/api/documents/{id}/thumbnail). This was the design decision from the Resolutions comment and I signed off there, but calling it out explicitly: the entity model and the controller's@RequestMappingare now coupled through this string. If the controller path ever moves (e.g. to/v2/documents), two places need to stay in sync. The counter-argument — that this keeps everyDocumentJSON response carrying the URL with zero per-controller plumbing — was the winning one, and I still agree with it. Just be aware of the trade for future maintainers.No ADR needed
One computed getter on one entity. The Resolutions comment on #309 is the decision log.
LGTM
Boring, small, and the architectural posture is consistent with the monolith-first philosophy. Merge when the loop clears.
🔒 Nora Steiner — Application Security Engineer
Verdict: ✅ Approved
Surface review
id(UUID), a hardcoded path literal, and aLocalDateTime(backend-controlled). None of it comes from user input. No SQLi/XSS/SSRF/path-traversal vector.GET /api/documents/{id}/thumbnailis unchanged; this PR just embeds its relative URL in a DTO the user is already authorized to receive. Follow-up GETs carry the user's cookie through the standard app chain.private, max-age=31536000, immutableatDocumentController.java:114is still safe because the?v={thumbnailGeneratedAt}cache-buster changes when the file does. The load-bearing comment onDocument.getThumbnailUrl()(lines 131-136) documents this for future readers — exactly what I asked for. ✓?v=query "for cleanup" sees the risk inline. ✓Encoding parity
URLEncoder.encode(timestamp, StandardCharsets.UTF_8)vs. the frontend's oldencodeURIComponentproduces identical output for the characters that appear inLocalDateTime.toString()(digits,T,-,:,.). For any hypothetical future input containing spaces the two would diverge (+vs%20), butLocalDateTime.toString()never emits spaces — and the frontend builder is gone now anyway, so the concern is moot. ✓Audit trail
DocumentTest.javalocks the exact encoded URL shape including the%3Afor the:separator — any future change that breaks the convention fails at test time, not at "users report stale thumbnails" time.LGTM
No new vulnerabilities introduced and the security-relevant comment on the getter is a meaningful addition to the defensive commentary.
🧪 Sara Holt — QA Engineer
Verdict: ⚠️ Approved with concerns
What's solid
DocumentTest.javacovers the three behavioural branches of the getter and locks the exact encoded URL shape —%3Afor:is asserted literally. A future developer who accidentally broke the encoding convention would fail at test time. ✓DashboardServiceTest.getResume_populatesThumbnailUrl_fromDocumentcorrectly tests the wiring (DTO field populated from the Document) without re-testing the URL shape. Right division of responsibility. ✓data-testidselectors on both branches (resume-thumbnail-img,resume-thumbnail-fallback) — stable against future DOM refactors. ✓...mockResume, thumbnailUrl: '...'). ✓Concerns (non-blocking, worth considering)
No test covers the Jackson serialisation path itself. The backend assertion is
result.thumbnailUrl()— a method call on the DTO record, not "the JSON that ships to the browser hasthumbnailUrlas a field". If someone removed@JsonProperty("thumbnailUrl")tomorrow, all current tests would still pass, but the frontend would seeundefined. A singleObjectMappertest — "serialising a Document with a thumbnailKey produces JSON with athumbnailUrlproperty" — would close that gap. Low cost, high signal.Frontend empty-state branch coverage is implicit, not explicit. The existing tests that use
mockResume(which has nothumbnailUrl) now exercise the fallback icon path — that's fine coverage, but the dedicatedrenders fallback icon when thumbnailUrl is nulltest is the one I'd rely on if something broke. Just noting: the implicit coverage is a happy accident, not the intended test strategy.No Playwright/E2E coverage for "thumbnail actually loads on the dashboard." This is intentional per the issue's test strategy (unit-level coverage only), but worth flagging that the end-to-end path
MinIO → thumbnail endpoint → DTO serialisation → SSR → browser <img>has no automated proof. A smoke test in the existing dashboard E2E spec (if one exists) would be a nice future addition.LGTM overall
The concerns are improvements, not blockers — the existing coverage is solid enough for a merge. Flagging #1 as the one I'd most like to see addressed: one small test, one big invariant protected.
⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
docker-compose.ymluntouched,minioservice handles thumbnail objects as before,archive-documentsbucket bootstrap unchanged. ✓immutablefor a year — zero cost on repeat visits. First-load proxies through the JVM to MinIO, same pattern as the document list. ✓One operational note
chore(api): regenerate Document type with thumbnailUrl fieldis honest about being a manual edit rather than a realnpm run generate:apipass. That's correct given the worktree constraint, and the comment is explicit so a future maintainer knows to regen properly after merge. I'd just add to the post-merge checklist: runnpm run generate:apiagainst a rebuilt dev backend and confirm the diff is empty. If it's not, we learn something.Monitoring suggestion (future work)
/api/documents/{id}/thumbnailp95 in Grafana as dashboard usage grows. If it ever becomes a hot path, pre-signed S3 URLs take MinIO's bytes off the JVM heap. Not needed today — the dashboard resume card is one image per page, not a grid.LGTM
Zero infrastructure surface change, zero runbook impact. Ship it.
🎨 Leonie Voss — UX Designer & Accessibility Advocate
Verdict: ⚠️ Approved with concerns
What's right
DocumentThumbnaileverywhere else in the app. The strip no longer looks squatter than the rest of the thumbnail surfaces. ✓h-[252px] w-[180px]on the wrapper). No CLS. ✓alt=""— correct, decorative image, the<h2>title + caption already label the card. Screen readers won't double-announce. ✓dark:mix-blend-multiplyon the img — paper scans won't glare on a dark page background. ✓aria-hidden="true"on the fallback icon wrapper — decorative-only. ✓loading="lazy"+decoding="async"— the img doesn't block first paint. ✓DocumentThumbnail.svelte:44-55. Users learn one shape for one meaning. ✓One concern (non-blocking)
Fallback icon size on the larger container. The fallback SVG uses
class="h-16 w-16"(64×64). That's the same sizeDocumentThumbnailuses for itslgcontainer (120×168). On the resume strip's 180×252 container — significantly bigger — a 64×64 icon looks visually undersized: it's only ~25% of the height, compared to ~38% in thelgdocument thumbnail. The fallback feels a bit lost in the larger box.Fix would be one number: bump to
h-20 w-20(80×80, ~32%) orh-24 w-24(96×96, ~38%, matching the lg ratio). I leanh-24 w-24— keeps visual weight parity with thelgfallback.Suggestions (not for this PR)
mix-blend-multiplyon paper scans depends on theme — worth a pair ofaxe-playwrightpasses (light + dark) next time the dashboard E2E suite grows.LGTM pending the icon size
The only real ask is the fallback icon scale bump — a single Tailwind class change. Everything else matches
DocumentThumbnail's deliberate decisions exactly.Review Cycle 1 — Changes
Addressed
thumbnailUrl_isSerialisedToJson_soFrontendReceivesIttoDocumentTest.java. Serialises aDocumentviaObjectMapperand asserts the JSON contains"thumbnailUrl":"...", locking the wire contract (protects against getter rename,@JsonIgnore, visibility drop). Commit0a683a61.h-16 w-16toh-24 w-24so the icon occupies ~38% of the container height, matching the visual weight ratio ofDocumentThumbnail'slgfallback. Commitac6a7359.Not addressed (explicitly non-blocking, deferred per the persona's own phrasing)
renders fallback icon when thumbnailUrl is nulltest.npm run generate:apiagainst a rebuilt dev backend — can't execute from this worktree; follow-up action after merge.Re-running review cycle 2.
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
Cycle 2 delta
thumbnailUrl_isSerialisedToJson_soFrontendReceivesItinDocumentTest.javais exactly the shape I'd want: small, pure, no Spring context, asserts on the wire contract rather than the Java method. Comment explains why (getter rename / @JsonIgnore / visibility drop) rather than what, which is the right discipline.h-16 → h-24) is a single-number change, exactly as Leonie asked.LGTM
Nothing new from my angle. Ship it.
🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
Cycle 2 delta
DocumentTest.java(model package) — right layer. Doesn't pull in Spring context, doesn't cross module boundaries.DashboardResumeStrip.svelte). No architectural drift.LGTM
The architectural shape hasn't changed since cycle 1. Merge when the loop clears.
🔒 Nora Steiner — Application Security Engineer
Verdict: ✅ Approved
Cycle 2 delta
LGTM
Surface unchanged, no new security concerns. Ship it.
🧪 Sara Holt — QA Engineer
Verdict: ✅ Approved
Cycle 2 delta
thumbnailUrl_isSerialisedToJson_soFrontendReceivesItlocks the wire contract —ObjectMapperround-trip, asserts the exact JSON fragment. Comment explains what it protects against (getter rename,@JsonIgnore, visibility drop), which is the right anchor for a future maintainer.ObjectMapperwithJavaTimeModulerather than pulling in a Spring context — fast, focused, right layer.contains("\"thumbnailUrl\":\"...\"")over a full-treeJsonNodewalk). Small and blunt, survives field-order changes in Jackson.Concerns from cycle 1 not actioned (by agreement)
LGTM
Test pyramid shape is right. Ship it.
⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
Cycle 2 delta
LGTM
Zero operational surface change from cycle 1. Ship it.
🎨 Leonie Voss — UX Designer & Accessibility Advocate
Verdict: ✅ Approved
Cycle 2 delta
h-24 w-24(96×96 in a 180×252 container, ~38% of container height) now matches the visual weight ratio ofDocumentThumbnail'slgfallback (64×64 in 120×168, ~38%). Consistent across every "no thumbnail yet" surface in the app — users learn one shape at one relative size.LGTM
Everything else was already correct per cycle 1. Design-wise I'm happy to merge.
ac6a735918to7007491d8cRebased onto latest
mainMain had advanced 32 commits since this PR opened, adding
thumbnailAspect/pageCountcolumns toDocumentand a related refactor in the generated API types. The one conflict was infrontend/src/lib/generated/api.ts— main added two optional fields in the same region where this PR addedthumbnailUrl?: string. Resolved by keeping both.All 12 commits replayed cleanly onto
origin/mainafter that single resolution. Backend suite: 1257/1257 green. Frontend spec forDashboardResumeStrip: 7/7 green.Force-pushed (
--force-with-lease). Merge conflict is resolved — no code-review concerns changed.