All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m8s
CI / Build container images (pull_request) Successful in 4m3s
CI / Auth e2e pack (pull_request) Successful in 9m1s
CI / Import/export fidelity gate (pull_request) Successful in 1m3s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 1m21s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m49s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m42s
CI / Import/export fidelity gate (push) Successful in 58s
Built on #306's storage, serving and crop control — a layer, not a parallel implementation. `resolveBranding` in shared is the ONE place that answers "which asset applies here?", and both the sidebar logo and the favicon swap read it. The decision most likely to be "fixed" by accident, so it is pinned by name in `branding.test.ts`: **a logo set belongs to one level and variants are never mixed across levels.** A pond that uploaded only a light logo shows THAT logo in dark mode; it does not borrow the instance's dark variant. Decided 2026-08-01 — a logo silently swapping to a different image when the viewer switches theme is a change nobody ordered, and a design that looks wrong is more honest than one that is quietly substituted. Only a pond with no logo at all inherits the instance's set, again as a set. The settings screen warns about a missing dark variant; it never blocks. Consequences that fall out of that rule and are easy to get wrong: - The serving route does NOT fall back when given a pond scope. The caller already decided which level applies; a "helpful" fallback in the route would mix variants across levels behind the resolver's back. - The logo link's accessible name follows the LEVEL: a pond logo is named by the pond, an instance logo by the instance. It is the link home, and a link's name has to say where it goes. - **Charged to the pond's storage quota**, before the write, like attachments. Without it branding would be a way around the quota, and replacing a logo repeatedly would consume disk with no ceiling. The replaced asset's bytes are released FIRST, so re-uploading the same logo costs nothing — and a refused upload puts the released reservation back, so a rejection cannot leave the pond with more room than it had. - **Purge removes the branding files.** The purge standard is absolute: after it nothing referencing the pond survives, rows or files. Asserted against the real purge path, not the new code alone. - Security unchanged from #306 and not relaxed because the uploader is now an ordinary Pond Admin: SVG refused, magic bytes and IHDR checked server-side, size caps, content type pinned, no image parsing. - The favicon swap is driven by the RESOLVED pond, never the raw route parameter — an unreadable or unknown slug must not leave a stale icon in the tab. That it happens after first paint is accepted and stated in the code and the UI: avoiding it would mean server-rendering index.html, which is #179's territory. Same audit id as #306 (`branding.changed`) with `scope: 'pond'` — the catalogue already carries the field, so no version bump. Verified: api suite 105 files / 592 tests green; 5 pond-branding e2e tests (pond scope serves the pond's bytes while the instance level still 404s, the quota is charged and released exactly, SVG refused at pond level, a reader may read but not change, purge deletes the files); 9 shared unit tests on the resolution order including both mixing directions.
50 lines
3.4 KiB
JSON
50 lines
3.4 KiB
JSON
{
|
||
"admin": {
|
||
"title": "Erscheinungsbild der Instanz",
|
||
"intro": "Lade ein Logo und ein Favicon hoch. Das Logo steht in der Seitenleiste oben und verlinkt auf die Startseite; das Favicon zeigt der Browser im Tab. Ohne Logo erscheint dort der Name der Instanz als Text.",
|
||
"publicNote": "Beides ist ohne Anmeldung sichtbar: der Anmeldebildschirm trägt das Logo, und das Favicon lädt der Browser, bevor sich jemand anmeldet.",
|
||
"darkMissing": "Für den Dunkelmodus ist kein eigenes Logo hinterlegt. Dann wird dort das helle Logo verwendet — auf dunklem Grund kann das schlecht aussehen. Das ist ein Hinweis, keine Sperre.",
|
||
"uploading": "Das Bild wird hochgeladen …",
|
||
"logo": {
|
||
"light": "Logo (Hellmodus)",
|
||
"lightHint": "Empfohlen: PNG mit transparentem Hintergrund. Wird auch im Dunkelmodus verwendet, solange dort kein eigenes Logo hinterlegt ist.",
|
||
"dark": "Logo (Dunkelmodus, optional)",
|
||
"darkHint": "Nur nötig, wenn das helle Logo auf dunklem Grund nicht funktioniert.",
|
||
"current": "Hinterlegt: {{width}} × {{height}} px.",
|
||
"currentAlt": "Vorschau des hinterlegten Logos",
|
||
"none": "Es ist kein Logo hinterlegt.",
|
||
"remove": "Logo entfernen",
|
||
"submit": "Logo speichern",
|
||
"saved": "Das Logo wurde gespeichert."
|
||
},
|
||
"favicon": {
|
||
"title": "Favicon",
|
||
"hint": "Ein quadratischer Ausschnitt; daraus entstehen die Größen 32 × 32 px (Browser-Tab) und 180 × 180 px (Startbildschirm).",
|
||
"present": "Ein eigenes Favicon ist hinterlegt.",
|
||
"default": "Es ist kein eigenes Favicon hinterlegt — ausgeliefert wird das mitgelieferte Standard-Favicon.",
|
||
"remove": "Favicon entfernen",
|
||
"submit": "Favicon speichern",
|
||
"saved": "Das Favicon wurde gespeichert."
|
||
}
|
||
},
|
||
"crop": {
|
||
"file": "Bilddatei",
|
||
"fileHint": "PNG, JPEG oder WebP. SVG wird nicht angenommen, weil darin Skripte stecken können. Ausgeliefert wird immer PNG — Achtung: ein JPEG hat keinen Transparenzkanal, aus einem JPEG entsteht also ein PNG mit deckendem Hintergrund. Zuschneiden und Umwandeln geht, Transparenz lässt sich nicht nachträglich erzeugen.",
|
||
"x": "Ausschnitt von links (px)",
|
||
"y": "Ausschnitt von oben (px)",
|
||
"width": "Breite des Ausschnitts (px)",
|
||
"height": "Höhe des Ausschnitts (px)",
|
||
"size": "Kantenlänge des Ausschnitts (px)",
|
||
"reset": "Ausschnitt zurücksetzen",
|
||
"result": "Ergebnis: {{width}} × {{height}} px (Ausgangsbild {{sourceWidth}} × {{sourceHeight}} px)."
|
||
},
|
||
"pond": {
|
||
"intro": "Dieser Teich kann ein eigenes Logo und ein eigenes Favicon führen. Beides überschreibt das der Instanz — nur für diesen Teich.",
|
||
"quotaNote": "Die Dateien zählen auf das Speicher-Kontingent dieses Teichs, wie Anhänge auch.",
|
||
"inherited": "Nichts hinterlegt — es gilt die Einstellung der Instanz.",
|
||
"reset": "Auf die Instanz-Einstellung zurücksetzen",
|
||
"darkMissing": "Für den Dunkelmodus ist kein eigenes Teich-Logo hinterlegt. Dann wird dort das helle Logo dieses Teichs verwendet — NICHT das dunkle Logo der Instanz. Ein Logo-Satz gehört zu einer Ebene und wird nie ebenenübergreifend gemischt. Das ist ein Hinweis, keine Sperre.",
|
||
"faviconHint": "Das Favicon wird beim Betreten des Teichs im Tab gesetzt und beim Verlassen wieder zurückgestellt. Beim direkten Öffnen eines Teich-Links erscheint kurz das Instanz-Favicon, bevor es wechselt."
|
||
}
|
||
}
|