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.0 KiB
JSON
50 lines
3.0 KiB
JSON
{
|
||
"admin": {
|
||
"title": "Instance appearance",
|
||
"intro": "Upload a logo and a favicon. The logo sits at the top of the sidebar and links to the start page; the favicon is what the browser shows in the tab. Without a logo the instance name is rendered there as text.",
|
||
"publicNote": "Both are visible without signing in: the login screen carries the logo, and the browser fetches the favicon before anyone signs in.",
|
||
"darkMissing": "No separate dark-mode logo is set. The light logo is then used there too — which can look wrong on a dark surface. This is advice, not a block.",
|
||
"uploading": "Uploading the image …",
|
||
"logo": {
|
||
"light": "Logo (light mode)",
|
||
"lightHint": "Recommended: PNG with a transparent background. Also used in dark mode as long as no separate logo is set there.",
|
||
"dark": "Logo (dark mode, optional)",
|
||
"darkHint": "Only needed when the light logo does not work on a dark surface.",
|
||
"current": "Stored: {{width}} × {{height}} px.",
|
||
"currentAlt": "Preview of the stored logo",
|
||
"none": "No logo is stored.",
|
||
"remove": "Remove the logo",
|
||
"submit": "Save the logo",
|
||
"saved": "The logo was saved."
|
||
},
|
||
"favicon": {
|
||
"title": "Favicon",
|
||
"hint": "A square crop; it produces the 32 × 32 px (browser tab) and 180 × 180 px (home screen) sizes.",
|
||
"present": "A custom favicon is stored.",
|
||
"default": "No custom favicon is stored — the shipped default is served.",
|
||
"remove": "Remove the favicon",
|
||
"submit": "Save the favicon",
|
||
"saved": "The favicon was saved."
|
||
}
|
||
},
|
||
"crop": {
|
||
"file": "Image file",
|
||
"fileHint": "PNG, JPEG or WebP. SVG is not accepted because it can carry script. The result is always PNG — note that a JPEG has no alpha channel, so converting one produces a PNG with an opaque background. Cropping and conversion are offered; transparency cannot be invented.",
|
||
"x": "Crop from the left (px)",
|
||
"y": "Crop from the top (px)",
|
||
"width": "Crop width (px)",
|
||
"height": "Crop height (px)",
|
||
"size": "Crop edge length (px)",
|
||
"reset": "Reset the crop",
|
||
"result": "Result: {{width}} × {{height}} px (source image {{sourceWidth}} × {{sourceHeight}} px)."
|
||
},
|
||
"pond": {
|
||
"intro": "This pond can carry its own logo and favicon. Both override the instance's — for this pond only.",
|
||
"quotaNote": "The files count against this pond's storage quota, just like attachments.",
|
||
"inherited": "Nothing set — the instance's setting applies.",
|
||
"reset": "Reset to the instance setting",
|
||
"darkMissing": "No separate dark-mode logo is set for this pond. The pond's light logo is then used there — NOT the instance's dark one. A logo set belongs to one level and is never mixed across levels. This is advice, not a block.",
|
||
"faviconHint": "The favicon is applied to the tab when the pond is entered and restored on leaving. Opening a pond link directly shows the instance favicon briefly before it changes."
|
||
}
|
||
}
|