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 #309
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
The "resume" strip on the dashboard (the big card pointing the user back to the document they last touched) currently renders a generic parchment-style placeholder SVG instead of the real document thumbnail.
frontend/src/lib/components/DashboardResumeStrip.svelte:46-67— inline<svg>with a gradient<rect>and a few horizontal lines.frontend/src/lib/components/DocumentThumbnail.svelte, served by/api/documents/{id}/thumbnail?v=….Wire is half-stubbed already
DashboardResumeDTO.java:17already declares@Nullable String thumbnailUrl, and the generated TS typefrontend/src/lib/generated/api.ts:1954carriesthumbnailUrl?: string.DashboardService.getResume()(backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java:84-85) passesnullfor that field, andDashboardResumeStrip.sveltenever reads it.Acceptance criteria
DashboardService.getResume()populatesthumbnailUrlfrom theDocument(use the same shape asfrontend/src/lib/thumbnails.ts:/api/documents/{id}/thumbnail?v={thumbnailGeneratedAt}whenthumbnailKeyis present, otherwisenull).DashboardResumeStrip.svelterenders the real thumbnail whenresumeDoc.thumbnailUrlis present, with a tasteful fallback (the existing parchment SVG, or the document-icon fallbackDocumentThumbnailalready uses) when it isn't.object-cover object-topso letter salutations stay visible, likeDocumentThumbnail.DocumentThumbnail(mix-blend-multiplyso paper scans don't glare).loading="lazy"+decoding="async"on the<img>.nullwhen it doesn't.<img>; absent → fallback).Out of scope
DocumentThumbnailto support arbitrary sizes — the resume strip is the only larger consumer for now, so an inline<img>driven byresumeDoc.thumbnailUrlis fine. If a second large-thumbnail surface appears, extract then.👨💻 Felix Brandt — Senior Fullstack Developer
Observations
thumbnailUrlas a fully-formed URL string, but elsewhere in the codebase we keepthumbnailKey+thumbnailGeneratedAtseparate and compose the URL client-side viafrontend/src/lib/thumbnails.ts. Populating the DTO now means the URL shape lives in two places that must stay in sync (seeDashboardService.java:84-85vsthumbnails.ts:15-17).DashboardResumeStrip.svelte:46-67currently uses a unique parchment SVG fallback.DocumentThumbnail.svelte:44-55uses a document-text icon. Two different "no thumbnail" vocabularies on the same app.thumbnailKey, with/withoutthumbnailGeneratedAt) and two frontend tests (img present / img absent) cover everything.Recommendations
getResume_populates_thumbnailUrl_with_cacheBuster_when_generatedAt_presentgetResume_populates_thumbnailUrl_without_cacheBuster_when_generatedAt_nullgetResume_thumbnailUrl_isNull_when_thumbnailKey_nullThen frontend:
renders <img> with correct src, alt="", loading="lazy", decoding="async" when thumbnailUrl is setrenders document-icon fallback when thumbnailUrl is nullDocumentThumbnailicon, not the parchment SVG. Keep the visual vocabulary consistent — if users see the document-text icon on every other "no thumbnail" surface, seeing something else here is friction, not flavor. The parchment is only needed in the fully-empty state (resume-strip-empty,DashboardResumeStrip.svelte:16-44), which isn't changing.alt=""on the<img>, matchingDocumentThumbnail.svelte:34. The title and caption already carry the semantics; the image is decorative.DocumentUrls.thumbnail(Document)next to the model, so the next DTO that needs it doesn't rewrite the same concat.getResume, one line:String thumbnailUrl = doc.getThumbnailKey() != null ? buildThumbnailUrl(doc) : null;— clearer than an inline ternary in the constructor call.Open Decisions
thumbnailUrl(resumeDoc)in the component: DRY with the rest of the app, but changes the generated types and diverges from the issue's ACs. I lean keep the URL as specified and add a small helper on the backend to centralize URL shape — but the cleaner long-term structure is key+timestamp.🏛️ Markus Keller — Senior Application Architect
Observations
dashboardfeature package — no boundary crossings, no new repository dependencies, no new infrastructure. This is exactly the shape a well-scoped UI polish issue should have.DashboardServicealready goes throughDocumentService.getDocumentById(docId)forDocumentdata (DashboardService.java:46). The new field populates from the already-loadedDocument— zero extra queries, no N+1 risk.@Nullable String thumbnailUrlwith no@Schema(requiredMode = REQUIRED)(DashboardResumeDTO.java:17). That matches the data reality (thumbnails are async-generated after upload) and the existing TS codegen is already optional. Good.thumbnails.tson the frontend andDashboardServiceon the backend. Two places to keep in sync for one path convention.Recommendations
DocumentThumbnailto a third size — the "extract when a second large-thumbnail surface appears" rule in the issue's Out-of-Scope is correct. Two callsites don't justify the abstraction.DocumentUrls.thumbnail(Document doc)or an instance method onDocument(getThumbnailPublicUrl()) keeps the URL shape in one spot. The next DTO that needs it (activity feed rows? search results?) reuses the helper instead of hand-concat.dashboard/.Open Decisions
🔒 Nora Steiner — Application Security Engineer
Observations
DocumentController.java:98-120) serves through the standard application auth chain. Embedding a relative URL like/api/documents/{id}/thumbnail?v=…on the resume DTO doesn't bypass any permission — the browser will carry the user's cookie on the follow-up GET, and the endpoint goes throughdocumentService.getDocumentById(id)which enforces visibility at the service layer.Cache-Control: private, max-age=31536000, immutableon the thumbnail response (DocumentController.java:114) is correct:privateblocks shared caches from cross-user leaks (CWE-525), andimmutableis safe only because of the?v={thumbnailGeneratedAt}cache-buster. If the cache-buster were ever dropped from the DTO URL,immutablewould become dangerous — worth a code comment.resumeendpoint itself is per-user. ThedocumentIdis already in the response (DashboardResumeDTO.java:11), so the thumbnail URL does not leak any new identifier to the rendered HTML.thumbnailKeyset by the backend's own thumbnail worker), not from any user-controlled string. No ZIP slip equivalent. No path traversal — theidis a UUID and the path is fully server-generated.Recommendations
thumbnailGeneratedAtis null butthumbnailKeyis set, omitting?v=is acceptable — but only becauseimmutablestill has the document ID in the URL. When the thumbnail is regenerated, thethumbnailGeneratedAtflips, so the URL should always change once that field is populated. A test proving "URL changes whenthumbnailGeneratedAtchanges" locks this invariant.thumbnails.ts.encodeURIComponentis used frontend-side — backend-sideURLEncoder.encode(timestamp, UTF_8)(or equivalent) ensures identity across the two code paths. For an ISO-8601LocalDateTimethis is mostly a no-op (:stays%3Ain either case), but the test should assert on the exact serialized form to catch the divergence./api/dashboard/resume— the returnedthumbnailUrlpoints to a document that user A can read. There's no "cross-user thumbnail URL in response" path, but codifying it with a test prevents future regressions.Open Decisions
🧪 Sara Holt — QA Engineer
Observations
DashboardServiceTest.java:50-81for the backend,DashboardResumeStrip.svelte.spec.ts:1-55for the frontend). No new test infrastructure needed — add cases to what's there.DashboardResumeDTOwithoutthumbnailUrl(DashboardResumeStrip.svelte.spec.ts:14-22) — that's actually exactly the "no thumbnail" fallback case, so one of the new tests is essentially free.Recommendations
Backend coverage (add to
DashboardServiceTest.java):getResume_thumbnailUrl_includesCacheBuster_whenGeneratedAtPresent— document hasthumbnailKeyANDthumbnailGeneratedAt→ URL is/api/documents/{id}/thumbnail?v={iso-timestamp}.getResume_thumbnailUrl_omitsCacheBuster_whenGeneratedAtNull— document hasthumbnailKeybutthumbnailGeneratedAtis null → URL is/api/documents/{id}/thumbnail(no query).getResume_thumbnailUrl_isNull_whenThumbnailKeyNull— document has nothumbnailKey→ URL is null./api/documents/and contains the correct document id — locks the format.Frontend coverage (add to
DashboardResumeStrip.svelte.spec.ts):renders <img> with expected src, alt, loading=lazy, decoding=async when thumbnailUrl is set—getByRole('img')orpage.locator('img[src*="/api/documents/"]').renders fallback icon when thumbnailUrl is null— assert the absence ofimg[src*="/thumbnail"]AND presence of the fallback (testid please, not structural selectors).thumbnailUrlon paths that need it so assertions are explicit about state.Add
data-testidattributes on the thumbnail<img>and the fallback wrapper. ChasinggetByRole('img')on a page that might grow more images is fragile; explicit testids survive refactors.Cover the existing empty-state test doesn't accidentally break. The
resumeDoc === nullbranch (DashboardResumeStrip.svelte:16-44) is untouched by this change — add one regression assertion that it still renders, no img, no fallback icon, just the empty card.Open Decisions
⚙️ Tobias Wendt — DevOps & Platform Engineer
Observations
DocumentController.java:98-120) and the MinIO bucket backing it are already load-bearing — this change just adds one more callsite for them on the dashboard.Cache-Control: private, max-age=31536000, immutablethe repeat-visit cost is zero — the browser serves from cache. First-load cost is one MinIOGetObjectproxied through the Spring Boot app, same as any other document list render.immutable+?v={thumbnailGeneratedAt}cache-buster is the correct pattern. Works through Caddy without extra config.Recommendations
/api/documents/{id}/thumbnailp95 as dashboard usage grows. If the endpoint becomes a hot path (it isn't today), the pattern to reach for is pre-signed S3 URLs so MinIO serves the bytes directly instead of proxying through the JVM. Same trade-off we'd make for full-file downloads. Not needed now.DashboardServiceTestruns in the unit-test job (Mockito, no Spring context); the frontend component test runs in the Vitest browser-mode job. No new runner, no new container.minioservice already handles thumbnail objects; thearchive-documentsbucket already exists. The MinIO MC helper bootstrap at the repo root doesn't need touching.Open Decisions
🎨 Leonie Voss — UX Designer & Accessibility Advocate
Observations
DocumentThumbnail.sveltealready encodes our thumbnail pattern: 5:7 aspect (A4 portrait),object-cover object-topso letter salutations stay visible,dark:mix-blend-multiplyto prevent paper-scan glare,alt=""because the title carries semantics, and a document-text icon fallback (DocumentThumbnail.svelte:30-59). Every one of these is deliberate — reuse the decisions, don't re-relitigate them.DocumentThumbnailuses exactly 5:7 (1:1.4). Close, but not the same. Users will notice if the dashboard thumbnail looks slightly squatter than the list/person thumbnails.<img>loads without intrinsic dimensions, the layout shifts on first paint. The current SVG haswidth="180" height="246"attributes — the replacement<img>needs equivalent, either as explicit attributes or via wrapping container CSS.Recommendations
DocumentThumbnail's visual conventions exactly:DocumentThumbnailuses. Visual consistency across the app.alt=""(decorative). Title (resumeDoc.title) already labels the card; the<h2>sits two lines below, a screen reader that reads the image as "image of document" would duplicate and add noise.h-[252px] w-[180px]) reserve the box before the img decodes, preventing CLS. Thebg-whiteon the wrapper is the loading-state background — no spinner needed for a cacheable 250×180 JPEG.mix-blend-multiplyis mandatory for paper scans. Family letter thumbnails on a dark background glow like screens at midnight otherwise. Copy the pattern fromDocumentThumbnail.svelte:35.aria-hidden="true"on the fallback icon — decorative-only, no additional landmark info.Open Decisions
DocumentThumbnail's exact 5:7 ratio). Cost of 246: minor visual inconsistency with the rest of the app's thumbnail surfaces. Cost of 252: the strip grows by 6px, minor layout tweak. I lean 252 — consistency compounds across a session. The issue says "~180×246" which leaves room.🗳️ Decision Queue — Action Required
2 decisions need your input before implementation starts.
Architecture / API
thumbnailUrlstring on the DTO vs. rawthumbnailKey+thumbnailGeneratedAtpair. The issue specifies the former. Keeping it: lower friction — the field is already declared onDashboardResumeDTO, the TS type is already generated, one Svelte binding. Switching to key+timestamp and calling the existingthumbnailUrl()helper in the component: DRY with the rest of the app (the URL convention stops being duplicated betweenDashboardServiceandfrontend/src/lib/thumbnails.ts), but changes generated types and diverges from the issue's acceptance criteria. (Raised by: Felix, Markus — related concern)UX / Layout
DocumentThumbnail). The issue says "~180×246." Cost of 246: slight visual inconsistency against the rest of the app's thumbnails. Cost of 252: the strip grows 6px. Lean: 252 for convention consistency, 246 for "don't touch the existing footprint." (Raised by: Leonie)🎯 Discussion Resolutions
After walking through every persona point with the user, here are the agreed decisions. These now act as the authoritative design for implementation.
Theme 1 — DTO shape: single source of truth on the backend
thumbnailUrlstring on the DTO (matches the issue's ACs), AND retire the frontend URL-builder so there is exactly one place that knows how to shape a thumbnail URL.frontend/src/lib/thumbnails.ts) is used in exactly one component, so the migration is cheap; leaving it alive would cement the duplication.Theme 2 — Backend URL-building helper
Documententity:@JsonProperty("thumbnailUrl") public String getThumbnailUrl(). Jackson serializes it automatically, so everyDocumentresponse carriesthumbnailUrlwithout per-controller plumbing.DashboardResumeDTO.thumbnailUrlwrapsdoc.getThumbnailUrl().DocumentThumbnail.sveltereadsdoc.thumbnailUrldirectly — no helper, no import. Minor DDD coupling (entity knows URL shape) is an acceptable pragmatic trade for this monolith.Theme 3 — Fallback visual
DocumentThumbnail.svelte:44-55) when the document has no thumbnail yet. The parchment SVG stays alive only for the fully-empty state (resume-strip-empty), which this issue doesn't touch.Theme 4 — Thumbnail dimensions
DocumentThumbnaileverywhere else).Theme 5 — Accessibility & layout
alt=""on the<img>(decorative — the<h2>title + caption carry the semantics). Wrap in a sized containerh-[252px] w-[180px]to reserve space and prevent CLS.dark:mix-blend-multiplyon the<img>for paper-scan glare prevention.aria-hidden="true"on the fallback icon wrapper.DocumentThumbnail.svelte's existing, deliberate decisions. No re-litigation.Theme 6 — Testing
DocumentTest.javacovers URL-with-cache-buster, URL-without-cache-buster (timestamp null), and null-when-thumbnailKey-null. One additional wiring test inDashboardServiceTest.javaprovesgetResumepopulates the DTO field from the Document. Frontend: two new cases inDashboardResumeStrip.svelte.spec.ts—<img>renders with correct attributes when URL set; fallback icon renders when URL null. Adddata-testidon the thumbnail img and fallback wrapper for stable selectors.Document.getThumbnailUrl(), URL-shape tests belong next to the getter, not scattered across every DTO consumer.Theme 7 — Scope discipline
DocumentThumbnailto a third size. Two callsites don't justify the abstraction.Theme 8 — Infrastructure / caching
Document.getThumbnailUrl()documenting that the?v={thumbnailGeneratedAt}cache-buster is load-bearing for theimmutableheader's safety.DocumentController.java:110-113response header comment already explains theimmutable/privaterationale on the serving side; the matching note on the URL-building side keeps the two halves of the invariant visible together. Protects against a future developer dropping the cache-buster "to clean up" and silently introducing a CWE-525 (cross-user cache poisoning) risk.Open / Skipped
thumbnailGeneratedAtchanges" — already covered by the cache-buster assertion in case 1 (exact-value assertion on?v=).These resolutions now act as the authoritative design for implementation. The
implementskill will read this comment alongside the original issue.