Base layout with top bar, role-based menu, profile menu and phone layout; theme, language and German form of address kept in cookies; German formal, German informal and English text; Fluent avatar bundled in the image by a Node build stage; placeholder pages for the Publisher and Admin groups. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
10 KiB
title, type, ticket, created, status, baseline_revision, route, route_source, risk, review, review_source, context
| title | type | ticket | created | status | baseline_revision | route | route_source | risk | review | review_source | context | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Page shell, preferences and translations | feature | 6 | 2026-10-17 | built | 3c69c2a |
full | auto | medium | quick | pinned |
|
Intent
Problem: After sign-in the user sees an unstyled English page. Every later epic needs one shared layout, the three per-device preferences and German/English text before it can add a page.
Approach: One base layout (top bar, left menu with role-based groups, profile menu, skip link) in plain server-rendered HTML styled with the design tokens, with the Fluent web components used for the interactive controls and bundled into the image. Theme, language and German form of address are stored in cookies and read by the server to render the page. All existing text moves to a German formal, German informal and English catalogue.
Boundaries & Constraints
Always: AD-15 and AD-16 apply. No inline script, inline style or inline event handler (CSP stays script-src 'self'); no network request for fonts, icons or scripts at runtime. The theme is an attribute on <html>; system is resolved by CSS (prefers-color-scheme) only. Preferences are never sent to the server except as cookies. Every user-visible string goes through translation. The menu is declared in one place with the role each entry needs; a hidden entry is also refused by its view (decorators from 1.5). Touch targets are at least 44 px; the break between wide and narrow layout is 720 px. The page works for navigation and sign-out without JavaScript.
Never: No dashboard content (account card, announcements): that is the account epic. No Admin colour or background settings: the accent colour is a CSS variable with the default #0F6CBD, set by a later entry. No server-stored preferences. No Node.js or npm in the runtime image. No CDN.
I/O & Edge-Case Matrix
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|---|---|---|---|
| Shell after sign-in | Signed-in user | Top bar with portal name and avatar (initials), left menu, content; their display name is shown in the profile menu | n/a |
| Menu by role | User / Publisher / Admin | User: Startseite. Publisher adds group "Veröffentlichen". Admin adds group "Administration". A Publisher who is not Admin sees no Administration group | A direct address to a hidden page shows "Dazu fehlt Ihnen die Berechtigung." (403) |
| First visit | No preference cookies | Theme System, language from Accept-Language (de or en, else en), German form formal |
n/a |
| Choose preference | Profile menu | Cookie set, page re-renders with the new value; with JavaScript it applies at once without reloading | An unknown value is ignored, the cookie is not set |
| Informal form | Language English, form "du" | Form has no effect on English and the control is hidden | n/a |
| Informal German | Language German, form "du" | Strings use "du" forms; a string with no informal entry falls back to the formal one | n/a |
| Unsafe return address | next outside the site |
Ignored; the user goes to / |
n/a |
| Phone width | 360 px | Menu behind a menu button, one column, no horizontal scroll | n/a |
| Signed-out and error pages | Not signed in | Same layout without menu and avatar; preferences still readable from the cookies | n/a |
Code Map
portal/templates/base.html-- new shared layout;accounts/base.htmlbecomes a thin extension of it. All 1.5 pages move onto it.portal/shell/-- new app:menu.py(declaration of entries: label, URL name, required role, group),preferences.py(read and validate the three cookies, cookie names and lifetime),middleware.py(activates the language, setsrequest.preferences),context.py(context processor: menu filtered by role, preferences),views.py(POST /preferences),urls.py.portal/frontend/-- new:package.json,package-lock.json, an entry file that imports only the Fluent components used, and a build script.portal/static/portal/--shell.css(tokens from DESIGN.md, light and dark, phone layout),shell.js(menu button, profile menu, apply preference at once; no inline code).portal/locale/{de,de_DU,en}/LC_MESSAGES/django.po-- catalogues;de_DUholds only the informal differences and falls back tode.Dockerfile-- afrontendbuild stage onnode:*-slimrunsnpm ciand the bundle; the result is copied intoportal/static/vendor/beforecollectstatic. Message catalogues are compiled in the builder stage.portal/config/settings.py-- add the app, middleware and context processor,LANGUAGESwith the informal variant.portal/config/middleware.py-- CSP gainsstyle-src 'self'andimg-src 'self' data:only if the Fluent bundle needs them (decided at build; nounsafe-inline).- Placeholder pages for the Publisher and Admin groups (one view each, decorated with
publisher_required/admin_required, text "Diese Seite folgt.") so role visibility and the direct-address refusal can be checked now; later epics replace them. portal/tests/--test_preferences.py,test_shell.py,test_translations.py.
Tasks & Acceptance
Execution:
portal/frontend/and Dockerfile stage -- build the Fluent bundle into the image, pinned by lockfile -- AD-16portal/shell/preferences.py,middleware.py-- cookies in, validated preferences out, language activated per request -- AD-15portal/templates/base.html,shell.css,shell.js-- layout, themes, profile menu, phone menu, skip linkportal/shell/menu.py,context.py-- role-filtered menu and active-item markerportal/locale/-- German formal, German informal and English text for every existing string; compile step in the Docker build- Move the 1.5 pages onto the shell; placeholder Publisher and Admin pages
- Tests for every matrix row
Acceptance Criteria:
- Given a signed-in user, when the home page loads, then the top bar, menu and their display name are in the page and no script, style or event-handler attribute is inline.
- Given a User, a Publisher and an Admin, when the menu renders, then it shows Startseite, plus "Veröffentlichen" only for Publishers and "Administration" only for Admins, and a direct request to a hidden page returns 403.
- Given cookies theme=dark, lang=de, form=du, when a page renders, then
<html>carries the dark theme and the page text uses the informal form; with lang=en it is English and ignores the form. - Given no cookies and
Accept-Language: de-DE, when a page renders, then it is German formal and the theme is System. - Given an invalid preference value, when it is posted, then no cookie changes.
- Given every
{% trans %}andgettextstring in the code, when the catalogue test runs, then each has a German entry and no entry is fuzzy or empty. - Given a viewport of 360 px, when the page loads, then the menu is behind a button and there is no horizontal scrolling (checked by hand at the done step; the CSS rule is checked by test).
Design Notes
Preferences are cookies named pm_theme, pm_lang, pm_form, SameSite=Lax, Secure, one year, not HttpOnly so shell.js can apply a change without a reload; they hold no personal data. The informal German register is a language code de-du whose catalogue holds only differing strings; Django's gettext lookup falls back to de, which falls back to msgid. If that fallback does not behave as expected in the build, the plan changes to a template filter, not to duplicated catalogues.
msgfmt and xgettext (GNU gettext) are not available on the Windows development machine, so strings are added to the catalogues by hand and a test (using polib) keeps them complete; the compile step runs in the Docker builder stage, which installs gettext. Message extraction with makemessages can be run in a container later when the string count grows.
Fluent web components render inside shadow DOM; if a component needs inline styles that the CSP refuses, the control falls back to plain HTML and CSS rather than loosening the policy.
Verification
Commands:
uv run pytest-- expected: all tests pass, including the new onesuv run ruff check portal manage.py docker-- expected: no findingsuv run python manage.py makemigrations --check --dry-run-- expected: no changes detecteddocker compose up --buildon the Linux host -- expected: image builds with the front-end stage; pages load with no CSP violations in the browser console
Manual: sign in as ptest-user1, ptest-publisher, ptest-admin; check the menu groups, change the three preferences and reload, narrow the window to phone width, and open / in a private window to see the defaults.
Implementation Notes
- Built inline;
pytest247 passed, 6 skipped (live LDAP),ruff checkclean,makemigrations --checkno changes. - Judgement calls: the Fluent bundle contains only the avatar (the one control the shell needs that works with a fallback); buttons and menus are native HTML so the shell works without JavaScript. The profile menu is a
<details>element. Theme applies at once; language and form reload the page because the server renders the text.LocaleMiddlewareis replaced byPreferencesMiddleware. Thede_DUcatalogue fell back todeas hoped (tested). Nolocale/encatalogue: the message ids are English. Placeholder pages live at/publish/announcementsand/administration/{appearance,settings,audit,status}. - Not verified: the Docker build (Node stage,
compilemessages) and the visual result in a browser, including the Fluent avatar tokens and the phone layout, since neither runs on the Windows machine.