All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m28s
CI / Build container images (pull_request) Successful in 2m7s
CI / Auth e2e pack (pull_request) Successful in 9m37s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m47s
CD / Promote to Int (push) Successful in 16s
CI / Lint, typecheck, test (push) Successful in 7m25s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m41s
CI / Import/export fidelity gate (push) Successful in 1m12s
An instance had no way to look like itself: the top bar said "Dorfteich" whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`.
42 lines
2.2 KiB
JSON
42 lines
2.2 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)."
|
||
}
|
||
}
|