Schema/diagram images have no broken-image fallback — 404 renders broken glyph with no UX feedback #90

Open
opened 2026-06-03 20:08:54 +03:00 by root · 1 comment
Owner

TL;DR (TR): Parça şeması/diyagram görseli yüklenemediğinde (404 / MinIO'da yok) kullanıcıya hiçbir geri bildirim verilmiyor: tarayıcının kırık-görsel ikonu görünüyor, hotspot overlay boş alanın üstüne çiziliyor, kullanıcı bekleyip rage-click yapıyor. Hiçbir <img>'de onError/fallback/retry yok. #73 (loading spinner) ve #76 (mobil layout) bunu kapsamadı — ayrı bir sorun.

Problem

When an exploded-view schema/diagram image (parça şeması) fails to load (404 / missing from MinIO), the UI gives the user no feedback at all:

  • The browser's broken-image glyph is shown.
  • The HotspotOverlay still draws clickable hotspots over the (empty) image area.
  • There is no onError handler, fallback image, "Şema bulunamadı" message, retry, or skeleton anywhere.

Users keep clicking categories and rage-click waiting for an image that will never appear. This is distinct from #73 (category-click loading spinner) and #76 (mobile schema-page layout) — neither touched image-load error handling.

Evidence (file:line — apps/web)

  • apps/web/src/components/schema/schema-viewer.tsx:158-165 — main viewer image:
    <img
      src={schemaPic.url}
      alt={schemaPic.label || "Şema görüntüsü"}
      ...
    />
    
    No onError, no onLoad, no broken-image fallback. The HotspotOverlay below still renders when width/height > 0, so hotspots float over a broken image.
  • apps/web/src/components/categories/category-tree.tsx:292 — hover-preview thumbnail in SchemaIcon:
    <img src={schemaImageUrl} alt={name} className="w-full h-full object-contain p-1" />
    
    Guards only a null URL (line 278); a non-null URL that 404s is rendered raw.
  • App-wide: grep -rn "onError" apps/web/src on <img> returns nothing — there is no broken-image handling anywhere in the frontend.

Why the URLs 404 (root cause of the missing object)

  • Schema image URLs are persisted MinIO public URLs, built from key schemas/psa-${categoryId}.png:
    • apps/api/src/categories/categories.service.ts:1333-1344 (key + insert into schemaPics.imageUrl)
    • apps/api/src/storage/storage.service.ts:81-83getPublicUrl(key) => ${publicUrl}/${key}
    • bucket sase-schemasapps/api/src/config/configuration.ts:21
  • This produces the reported /sase-schemas/schemas/psa-*.png paths. Because the URL is stored in the DB, if the MinIO object is missing/deleted the URL becomes a permanent 404 — and the frontend has no way to know, so it shows a broken image indefinitely.

Reproduction

  1. Decode a VIN, open a vehicle, drill into a category whose schemaPics[0].url points to a MinIO object that 404s (or temporarily make the object unreachable, e.g. rename it in the bucket).
  2. Observe: broken-image glyph in the ~60% schema panel; hotspots drawn over empty space; no message / no retry. Same broken thumbnail on category-tree hover.

Proposed fix

Frontend (primary):

  • In schema-viewer.tsx, add onError to the <img> to flip a local imageFailed state. When true:
    • render a placeholder block (e.g. "Şema görseli yüklenemedi" + a Retry button that re-sets src with a cache-bust query param), and
    • suppress the HotspotOverlay so hotspots don't float over emptiness.
    • Optionally show a Skeleton until onLoad fires (loading state).
  • In category-tree.tsx SchemaIcon, add onError that falls back to the default <Icon /> (treat a failed image the same as a null URL — that path is already handled).

Backend (optional follow-up):

  • Add a periodic health-check that nulls schemaPics.imageUrl rows whose MinIO objects no longer exist, so the frontend's already-handled null-URL path kicks in instead of a broken <img>.

The schema-image prefetch loop amplifies requests and shares this area:

  • category-grid.tsx:73-127, category-tree.tsx:111-155, category-columns.tsx:153-198 — for each leaf with schemaImageUrl == null (categories.service.ts:1689) it calls /vehicles/:id/categories/:leafId in batches of 2, plus a /categories/:parentId/children refetch after every batch (~1.5N requests/level).
  • The catalog/categories/vehicles GET controllers are not exempted from the global 100 req / 60s per-IP throttle (app.module.ts:65-70); exceeding it returns 429 (GEN_004, http-exception.filter.ts:88).
  • This was not observed in real sessions yet (so it is not a live bug), but a category-heavy vehicle (67+ image-less leaves) could in theory trip the throttle. Cheap mitigations while we're in this code: (a) add @SkipThrottle (or a higher per-route limit) to the read-only categories/vehicles GET routes; (b) drop the per-batch parent /children re-fetch and/or widen the prefetch batch interval.

Acceptance criteria

  • A schema image that 404s renders a friendly placeholder ("Şema görseli yüklenemedi") instead of the browser broken-image glyph.
  • The Retry button re-attempts the load (cache-busted).
  • Hotspots are suppressed when the image failed (no clickable overlay over empty space).
  • The category-tree hover thumbnail falls back to the default icon on image error.
  • A successfully-loading schema image is unchanged (no skeleton flash / no regression).
  • (Optional) Backend health-check nulls dead schemaPics.imageUrl rows.
  • (Optional) Read-only categories/vehicles GET routes are throttle-exempted or per-batch parent refetch removed.

Verification checklist (confirm "fixed")

  • Manual/Playwright repro (pre-fix): point a category's schema src at a 404 URL → confirm broken glyph + floating hotspots (failing baseline).
  • Post-fix: same scenario now shows the placeholder + Retry, hotspots hidden; Retry against a now-valid object loads the image.
  • Network test: throttle / block the MinIO object in devtools → UI degrades gracefully (no broken glyph).
  • Regression: a normal vehicle's schema images still load and hotspots work.
  • Data signal: rage-clicks on the schema panel / category navigation drop in session recordings; resource-404 console errors for /sase-schemas/schemas/psa-*.png no longer correlate with rage-clicks.
  • Insights cmplpibau (occ 19, active) and cmpwew68 do not recur (panel auto-flags regressed).
  • (If throttle mitigation done) synthetic deep category-tree expansion of a 60+ leaf vehicle produces no 429/GEN_004.

Notes / data caveat

  • Insight cmplpibau001014ozj7bg39a3: 19 occurrences, status=new, last seen 2026-06-03 (active). cmpwew68u001v14fzhr60id7h: 1 occ, P2.
  • The rage-click and resource-404 signals come from PostHog session recordings (network/console); the autocapture posthog_events table for these sessions does not contain those rows, so 404 frequency isn't independently verifiable from events — but the absence of any UX handling for broken images is confirmed in code.

Filed from Süper Panel insight triage (2026-06-03). Covers insight(s): cmplpibau0 · cmpwew68u0. Canonical: cmplpibau001014ozj7bg39a3.

> **TL;DR (TR):** Parça şeması/diyagram görseli yüklenemediğinde (404 / MinIO'da yok) kullanıcıya hiçbir geri bildirim verilmiyor: tarayıcının kırık-görsel ikonu görünüyor, hotspot overlay boş alanın üstüne çiziliyor, kullanıcı bekleyip rage-click yapıyor. Hiçbir `<img>`'de `onError`/fallback/retry yok. #73 (loading spinner) ve #76 (mobil layout) bunu **kapsamadı** — ayrı bir sorun. ## Problem When an exploded-view schema/diagram image (parça şeması) fails to load (404 / missing from MinIO), the UI gives the user **no feedback at all**: - The browser's **broken-image glyph** is shown. - The `HotspotOverlay` still draws clickable hotspots over the (empty) image area. - There is **no** `onError` handler, fallback image, "Şema bulunamadı" message, retry, or skeleton anywhere. Users keep clicking categories and **rage-click** waiting for an image that will never appear. This is distinct from #73 (category-click loading spinner) and #76 (mobile schema-page layout) — neither touched image-load **error** handling. ## Evidence (file:line — `apps/web`) - `apps/web/src/components/schema/schema-viewer.tsx:158-165` — main viewer image: ```tsx <img src={schemaPic.url} alt={schemaPic.label || "Şema görüntüsü"} ... /> ``` **No `onError`, no `onLoad`, no broken-image fallback.** The `HotspotOverlay` below still renders when width/height > 0, so hotspots float over a broken image. - `apps/web/src/components/categories/category-tree.tsx:292` — hover-preview thumbnail in `SchemaIcon`: ```tsx <img src={schemaImageUrl} alt={name} className="w-full h-full object-contain p-1" /> ``` Guards only a **null** URL (line 278); a **non-null URL that 404s** is rendered raw. - App-wide: `grep -rn "onError" apps/web/src` on `<img>` returns **nothing** — there is no broken-image handling anywhere in the frontend. ### Why the URLs 404 (root cause of the missing object) - Schema image URLs are **persisted** MinIO public URLs, built from key `schemas/psa-${categoryId}.png`: - `apps/api/src/categories/categories.service.ts:1333-1344` (key + insert into `schemaPics.imageUrl`) - `apps/api/src/storage/storage.service.ts:81-83` — `getPublicUrl(key) => ${publicUrl}/${key}` - bucket `sase-schemas` — `apps/api/src/config/configuration.ts:21` - This produces the reported `/sase-schemas/schemas/psa-*.png` paths. Because the URL is **stored in the DB**, if the MinIO object is missing/deleted the URL becomes a **permanent 404** — and the frontend has no way to know, so it shows a broken image indefinitely. ## Reproduction 1. Decode a VIN, open a vehicle, drill into a category whose `schemaPics[0].url` points to a MinIO object that 404s (or temporarily make the object unreachable, e.g. rename it in the bucket). 2. Observe: broken-image glyph in the ~60% schema panel; hotspots drawn over empty space; **no message / no retry**. Same broken thumbnail on category-tree hover. ## Proposed fix **Frontend (primary):** - In `schema-viewer.tsx`, add `onError` to the `<img>` to flip a local `imageFailed` state. When true: - render a placeholder block (e.g. *"Şema görseli yüklenemedi"* + a **Retry** button that re-sets `src` with a cache-bust query param), and - **suppress the `HotspotOverlay`** so hotspots don't float over emptiness. - Optionally show a `Skeleton` until `onLoad` fires (loading state). - In `category-tree.tsx` `SchemaIcon`, add `onError` that falls back to the default `<Icon />` (treat a failed image the same as a null URL — that path is already handled). **Backend (optional follow-up):** - Add a periodic health-check that **nulls** `schemaPics.imageUrl` rows whose MinIO objects no longer exist, so the frontend's already-handled **null-URL** path kicks in instead of a broken `<img>`. ## Related latent risk — schema-image prefetch & rate-limit (from K4 analysis, fold in here) The schema-image **prefetch** loop amplifies requests and shares this area: - `category-grid.tsx:73-127`, `category-tree.tsx:111-155`, `category-columns.tsx:153-198` — for each leaf with `schemaImageUrl == null` (`categories.service.ts:1689`) it calls `/vehicles/:id/categories/:leafId` **in batches of 2**, **plus** a `/categories/:parentId/children` refetch after every batch (~**1.5N requests/level**). - The catalog/categories/vehicles GET controllers are **not exempted** from the global **100 req / 60s per-IP** throttle (`app.module.ts:65-70`); exceeding it returns **429 (`GEN_004`, `http-exception.filter.ts:88`)**. - This was **not** observed in real sessions yet (so it is not a live bug), but a category-heavy vehicle (67+ image-less leaves) could in theory trip the throttle. **Cheap mitigations while we're in this code:** (a) add `@SkipThrottle` (or a higher per-route limit) to the read-only categories/vehicles GET routes; (b) drop the per-batch parent `/children` re-fetch and/or widen the prefetch batch interval. ## Acceptance criteria - [ ] A schema image that 404s renders a **friendly placeholder** ("Şema görseli yüklenemedi") instead of the browser broken-image glyph. - [ ] The **Retry** button re-attempts the load (cache-busted). - [ ] **Hotspots are suppressed** when the image failed (no clickable overlay over empty space). - [ ] The category-tree hover thumbnail falls back to the default icon on image error. - [ ] A successfully-loading schema image is unchanged (no skeleton flash / no regression). - [ ] (Optional) Backend health-check nulls dead `schemaPics.imageUrl` rows. - [ ] (Optional) Read-only categories/vehicles GET routes are throttle-exempted or per-batch parent refetch removed. ## Verification checklist (confirm "fixed") - [ ] **Manual/Playwright repro (pre-fix):** point a category's schema `src` at a 404 URL → confirm broken glyph + floating hotspots (failing baseline). - [ ] **Post-fix:** same scenario now shows the placeholder + Retry, hotspots hidden; Retry against a now-valid object loads the image. - [ ] **Network test:** throttle / block the MinIO object in devtools → UI degrades gracefully (no broken glyph). - [ ] **Regression:** a normal vehicle's schema images still load and hotspots work. - [ ] **Data signal:** rage-clicks on the schema panel / category navigation drop in session recordings; resource-404 console errors for `/sase-schemas/schemas/psa-*.png` no longer correlate with rage-clicks. - [ ] Insights `cmplpibau` (occ 19, active) and `cmpwew68` do **not** recur (panel auto-flags `regressed`). - [ ] (If throttle mitigation done) synthetic deep category-tree expansion of a 60+ leaf vehicle produces **no 429/`GEN_004`**. ## Notes / data caveat - Insight `cmplpibau001014ozj7bg39a3`: **19 occurrences**, status=new, last seen 2026-06-03 (**active**). `cmpwew68u001v14fzhr60id7h`: 1 occ, P2. - The rage-click and resource-404 signals come from PostHog **session recordings** (network/console); the autocapture `posthog_events` table for these sessions does not contain those rows, so 404 *frequency* isn't independently verifiable from events — but the **absence of any UX handling for broken images is confirmed in code**. --- *Filed from Süper Panel insight triage (2026-06-03). Covers insight(s): [cmplpibau0](https://sp.semih.ai/insights/i/cmplpibau001014ozj7bg39a3) · [cmpwew68u0](https://sp.semih.ai/insights/i/cmpwew68u001v14fzhr60id7h). Canonical: `cmplpibau001014ozj7bg39a3`.*
root added the insight-drivensase-pilotseverity-P3uxfrontendbugcatalog labels 2026-06-03 20:08:54 +03:00
root closed this issue 2026-06-03 20:22:03 +03:00
root reopened this issue 2026-06-03 20:22:06 +03:00
Author
Owner

🤖 Fusion task opened: FN-436

Triage queue: https://fusion.semih.ai/tasks/FN-436

This comment was posted automatically by the fusion-plugin-gitea-issues bridge. A Fusion agent will update this issue when the task moves to in-progress, in-review, or done.

🤖 Fusion task opened: `FN-436` Triage queue: https://fusion.semih.ai/tasks/FN-436 _This comment was posted automatically by the `fusion-plugin-gitea-issues` bridge. A Fusion agent will update this issue when the task moves to in-progress, in-review, or done._
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: root/sase.tr#90