Files
schalli e72814abd9
Tessera CI/CD / Lint & Type Check (push) Successful in 52s
Tessera CI/CD / Tests (push) Successful in 1m20s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m21s
docs(quick-260929-lh3): Favoriten-Symbole, Browser-Pruefung
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:48:39 +02:00

5.7 KiB

quick_id, phase, plan, subsystem, tags, status, commits, plan_head_before, plan_head_after, actuals, key-files
quick_id phase plan subsystem tags status commits plan_head_before plan_head_after actuals key-files
260929-lh3 quick 260929-lh3 favorites
favorites
icons
icon-discovery
browser-fallback
complete 3 8c644de5da 0e72ad45f8
tasks commits
3 3
modified
apps/api/src/favorites/favorites.service.ts
apps/api/src/favorites/icon-discovery.service.ts
apps/api/src/favorites/dto/create-favorite.dto.ts
apps/api/src/favorites/dto/update-favorite.dto.ts
apps/web/src/components/dashboard/widgets/favorites-widget.tsx
apps/web/src/lib/favorites-api.ts
apps/web/src/messages/de.json
apps/web/src/messages/en.json
CHANGELOG.md

Quick 260929-lh3: Favoriten-Symbol-Adresse Summary

Explicit icon URLs are now stored even when the server cannot fetch them, the tile loads them directly in the browser, discovery uses <link rel=icon> from non-2xx HTML pages, and a newly entered icon URL replaces a previously uploaded icon.

Commits

  • 7188c5b fix(favorites): geaenderte Symbol-Adresse wird sofort angezeigt (Task 1)
  • b15c746 fix(favorites): Symbol-Adresse auch speichern, wenn nur der Browser sie laden kann (Task 2)
  • 0e72ad4 docs(changelog): Favoriten-Symbole (Task 3)

Root cause, bug 2 ("Symbol-Adresse wirkt nicht")

Measured, not assumed:

  • Local reproduction (API with curl, and the real widget in headless Chromium via CDP): changing iconUrl on a favorite WITHOUT an uploaded icon works. iconVersion is bumped, the tile is remounted, the <img> src changes (?v=1 -> ?v=2), and the bytes change (naturalWidth 626 -> 48). The proxy, Cache-Control and Next rewrite are not the cause.
  • The alpha database (read-only psql) shows the save path works there too: favorite "Medon" holds the Nextcloud URL with iconVersion = 1. No favorite on alpha has uploadedIconMime set, and the alpha web image contains the ?v= code.
  • The real defect found in the code: getIconBytes always serves the uploaded file when uploadedIconMime is set, and update() never cleared it. A newly entered iconUrl was therefore saved but invisible for any favorite with an uploaded icon (the form even said "Ein hochgeladenes Symbol hat Vorrang"). Fixed: a new, different, non-empty iconUrl now clears uploadedIconMime, removes the file, and bumps iconVersion. An unchanged iconUrl (the form resends it on every save) leaves the upload alone.
  • Caveat for the orchestrator: for the exact alpha "Medon" case (no upload) I could not reproduce a stale display; the alpha row and local browser behavior are both correct. The browser check on alpha (https, NPM, basic auth) remains the only place this can still show. If it still fails there with the new build, capture the network request of /api-proxy/favorites/<id>/icon?v=1 in the alpha browser.

Root cause, bug 1 (black circle with "V")

fetchHtml dropped every non-2xx response, so docuvita (answers the server with 400 but ships <link rel="SHORTCUT ICON" href="/webclient/.../favicon.ico">) fell back to {origin}/favicon.ico; the proxy failed and the browser showed the root favicon (the "V"). And an explicit iconUrl was rejected by the 422 fetch probe because docuvita returns 404 HTML to the server.

Changes

  • API: assertIconUrlLoadable (422) replaced by assertIconUrlWellFormed (http/https, <= 2048 chars, else 400); DTO @MaxLength(2048) on both DTOs. Decision, documented in code: even a response the server DID receive with a non-image type does not reject, because "server gets no image" does not mean "browser gets none". SSRF guard for server-side fetches is unchanged.
  • Discovery: fetchWithRedirectGuard got allowErrorStatus (used only by the HTML search; fetchIconBytes stays strict). On a non-2xx page only <link rel=...icon> counts, not og:image.
  • Web tile: proxy -> iconUrl direct (referrerPolicy="no-referrer", http/https only) -> {origin}/favicon.ico (skipped when identical) -> letter. Existing chain for discovered icons behaves as before.
  • Removed the iconUrlUnreachable reason, 422 handling and de/en texts; adjusted the upload hint (a newly entered address replaces an upload).
  • CHANGELOG: two plain-German bullets under Unveröffentlicht / Behoben.
  • Rebuilt web and api (docker compose up -d --build, healthy). Smoke test against the rebuilt API: URL the server cannot fetch is saved (proxy answers 502, tile falls back to browser), javascript: is rejected with 400. Test favorite deleted afterwards.

Verification

  • API favorites specs: 99 tests green. Web favorites widget, favorites-api and messages tests: 47 green. tsc --noEmit clean for web and api.
  • Biome: web 55 warnings, api 82 warnings (at the limits, not above).

Deviations from Plan

  • [Rule 2 - Missing validation] Plan said "keep validation of scheme/length", but none existed for iconUrl; added form validation (service + DTO length) so the SSRF-relevant direct browser load never receives non-http(s) schemes.
  • Task 1 needed no web change: the existing test "Speichern mit neuer Logo-Adresse ... ?v=1" already covers the src change; API regression tests were added.
  • Commits were made on main as instructed (no worktree).

Known Stubs

None.

Self-Check: PASSED

Commits 7188c5b, b15c746, 0e72ad4 exist; SUMMARY location correct; PLAN/SUMMARY/STATE not committed.

Browser-Pruefung (Orchestrator, 29.09.)

  • Favorit „Claude“: iconUrl -> Nextcloud-PNG (PATCH 200), nach Neuladen Proxy-Bild ?v=5 mit 626 px Breite geladen.
  • iconUrl -> docuvita-Brand-Icon (vom Dev-Host nicht aufloesbar): PATCH 200 (frueher 422), Kachel faellt sauber zurueck. Im Firmennetz laedt der Browser direkt.
  • Zurueckgesetzt auf das urspruengliche Symbol.