Gundi Keycloak login theme (KC 11 + 26) - #482
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ripts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ages Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d-off Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…status, smoke h1 assertion - kc26.css: the parent theme fixes the login grid track at 34rem with no media query, so the card overflowed any viewport under ~576px. Track is now minmax(0, 34rem) and the phone gutter comes from container padding. - RUNBOOK.md: Keycloak answers a bad redirect_uri with HTTP 400 on both 11 and 26; the post-swap check now expects 400 instead of a -f'd 200. - smoke.sh: assert the kc26 h1 itself reads 'Sign in to Gundi' (tolerating the dev-mode template comment) instead of a page-wide grep the <title> already satisfied. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PatternFly 3 positions the alert icon absolutely and reserves room for it with a 47px left padding. The theme's uniform 14px padding removed that space, so the message rendered under the icon. The icon is now a flex item sized to the text's line height, so padding stays uniform and wrapped lines align under the first line. The screenshot script gains a login-error shot: it submits bad credentials with curl and renders the returned page, since that alert only exists on the POST response and was never captured before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both Keycloak versions link img/favicon.ico through the theme's resource path, so a file at that location shadows the stock Keycloak icon with no template change. The ICO packs the portal's Gundi mark at 64, 48, 32 and 16px. The portal's own favicon.ico is the leftover React scaffold logo, which browsers never show, so the icon is built from the mark PNG instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follows the EarthRanger login layout: the Gundi mark centred at the top of the card, "Welcome" as the h1 and "Sign in to Gundi" beneath it. The realm display-name header above the card is hidden and the wordmark dropped, as the subtitle already names the product. Each version draws the mark on its own card-header container in kc11.css and kc26.css. Keycloak 26 gets the title from loginAccountTitle; Keycloak 11 titles the page with the button's doLogIn key, so kc11.css hides the h1 text and draws "Welcome" in its place. Title swap and subtitle are scoped by the login form's presence, so other pages keep their own titles under the mark. On 26 the header grid's column gap beside the empty locale-menu column is zeroed so the title centres. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
amicavi
left a comment
There was a problem hiding this comment.
Automated review (Claude Code), findings verified against the diff and CI runs. Headline: the kc26 CI leg is currently red — see the smoke.sh comment; one-line fix.
One finding that can't be anchored inline because the file is empty:
keycloak/themes/gundi/login/theme.properties is a committed zero-byte file (the compose bind-mount mountpoint). Any consumer that copies the theme directory whole — e.g. the RUNBOOK's KC26 hand-off allows "copy the theme folder into its own image" — ships a theme with no parent= and no styles=, rendering unstyled login pages with no error. Suggestion: make theme.properties the real KC11 file and keep only theme.kc26.properties as the variant, removing the trap and the rename COPY.
| if [[ $version == kc26 ]]; then | ||
| # The h1 text follows a newline (and, in dev mode, a <!-- template: … --> comment) after the | ||
| # opening tag, so collapse newlines and allow whitespace/comments before matching. | ||
| tr -d '\n' <<<"$html" | grep -qE 'id="kc-page-title"[^>]*>([[:space:]]|<!--[^>]*-->)*Sign in to Gundi' \ |
There was a problem hiding this comment.
CI is red on the kc26 leg because of this assertion. Commit 6cde07d changed loginAccountTitle to "Welcome", but this still expects the h1 to read "Sign in to Gundi" (that text now only exists as CSS ::after content, which never appears in the HTML this grep reads). Latest run fails with exactly this fail message; the two runs before the Welcome commit were green. Fix: assert on "Welcome".
There was a problem hiding this comment.
Fixed in 43bd4be. The kc26 assertion now expects the h1 to read "Welcome". Verified the smoke test passes locally against both versions.
| done | ||
|
|
||
| # 2. Every /resources/ asset the page links must be served. | ||
| assets=$(grep -oE '(href|src)="[^"]*/resources/[^"]+"' <<<"$html" | sed -E 's/^(href|src)="//; s/"$//' | sort -u) |
There was a problem hiding this comment.
Under set -euo pipefail, if the page contains zero /resources/ links, grep exits 1 and kills the script during this assignment — so the "no /resources/ links found" diagnostic on the next line is unreachable. Line 40's refs= pipeline already guards with || true; this one needs the same.
There was a problem hiding this comment.
Fixed in 43bd4be. The assets= pipeline now ends in || true like the refs= one, so an empty page reaches the "no /resources/ links found" diagnostic.
| /* Login page title. Keycloak 11 titles the login page with doLogIn, the same key as the submit | ||
| button, so the h1 text is hidden and "Welcome" drawn in its place. Keycloak 26 has its own key | ||
| (loginAccountTitle in messages_en.properties). */ | ||
| body:has(#kc-form-login) .login-pf-page #kc-page-title { |
There was a problem hiding this comment.
The "Welcome" title swap (here) and the "Sign in to Gundi" subtitle (gundi.css:31) are gated on body:has(...). Browsers without :has() (Firefox <121 incl. ESR 115, Safari <15.4, Chrome <105) drop these rules with no fallback: the h1 shows the stock "Log In" and the subtitle vanishes — for exactly the field users least likely to be on a current browser. No test covers this.
There was a problem hiding this comment.
Partly addressed in 43bd4be. The subtitle selector is now :is(body[data-page-id="login-login"], body:has(#kc-form-login)) #kc-page-title::after. :is() takes a forgiving selector list, so a browser without :has() drops only the 11 branch and still applies the subtitle on 26, where the h1 is real text from loginAccountTitle.
On 11 there is no pure-CSS alternative: the body carries no page marker (kcBodyClass is theme-wide) and the h1 uses the button's doLogIn key, so :has() or a FreeMarker override are the only options, and the design ruled out template overrides. Without :has() the 11 page degrades to the stock "Log In" title with the rest of the theme intact. Given 11 is the outgoing version, I documented that in the CSS comment rather than adding a template override. Happy to revisit if the team wants the override.
| } | ||
| /* Login page only: "Sign in to Gundi" subtitle under the h1. The login form's id exists on | ||
| both versions and only on the login page, so its presence identifies the page. */ | ||
| body:has(#kc-form-login) #kc-page-title::after { |
There was a problem hiding this comment.
Title/subtitle copy injected as CSS generated content has two costs: screen readers compute the KC11 heading as roughly "Welcome Log In Sign in to Gundi" (font-size: 0 doesn't remove text from the accessibility tree, and pseudo-element content joins the accessible name), and browser auto-translate/find-in-page can't touch CSS strings. Also "Sign in to Gundi" is duplicated verbatim here and in messages_en.properties (loginTitle) with nothing tying them together — a later rebrand will update one and miss the other.
There was a problem hiding this comment.
Addressed in 43bd4be. On 11 the h1 now uses visibility: hidden with visibility: visible on the pseudo-elements, which removes "Log In" from the accessibility tree; the accessible name becomes "Welcome Sign in to Gundi". On 26 the title is real text from the message bundle. The subtitle remains CSS content on both, so auto-translate and find-in-page cannot reach it; that is the cost of the no-template-override design and is noted in the comment. The copy is now cross-referenced in both gundi.css and messages_en.properties.
| matrix: | ||
| include: | ||
| - version: kc11 | ||
| keycloak: "11.0.2" |
There was a problem hiding this comment.
Keycloak versions are pinned twice — here in the push-job matrix (image tag) and in each Dockerfile FROM — with nothing linking them, and the RUNBOOK's rollout keys off the tag's version prefix. A bump to Dockerfile.kc26 alone would push an image built on the new base but tagged 26.7-gundi-<sha>, which operators would deploy believing it matches the rehearsed base. Suggestion: derive the tag version from the Dockerfile FROM, or feed both from one ARG.
There was a problem hiding this comment.
Fixed in 43bd4be. The vars job parses FROM quay.io/keycloak/keycloak:<tag> from each Dockerfile and the push job tags with needs.vars.outputs[matrix.version], so the matrix no longer carries its own version list. A Dockerfile without a Keycloak FROM tag fails the job.
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - id: vars | ||
| run: echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
This job spins up a runner + full checkout solely for git rev-parse --short HEAD, and runs on every PR although its only consumer (push) is main-gated. sha=${GITHUB_SHA::7} gives the same value with no checkout (a run step is still needed — Actions expressions can't substring), and the job can take if: github.ref == 'refs/heads/main'.
There was a problem hiding this comment.
Fixed in 43bd4be. vars now runs only on main, takes the sha from ${GITHUB_SHA::7}, and keeps a sparse checkout of keycloak/ because it now reads the Dockerfile FROM tags (see the tag-linkage thread).
|
|
||
| ```bash | ||
| docker compose -f keycloak/compose.theme-dev.yml up -d # KC 11 on :8081, KC 26 on :8082, admin/admin | ||
| keycloak/tests/wait-ready.sh http://localhost:8081/auth/realms/cdip-dev |
There was a problem hiding this comment.
This block waits for readiness only on KC11 (:8081) and then immediately smokes both ports. KC26 is typically still running --import-realm, so a developer pasting this verbatim gets "login page did not return 200" on :8082 and concludes the theme is broken. The CI workflow waits per port; add a second wait-ready.sh line for :8082 here.
There was a problem hiding this comment.
Fixed in 43bd4be. The local-dev block waits for both :8081 and :8082 before running the smoke tests.
| shot_login_error() { | ||
| local jar html action | ||
| jar=$(mktemp) && html="$out/$version-login-error.html" | ||
| action=$(curl -s -c "$jar" "$local_auth?client_id=cdip-kong-gateway&response_type=code&scope=openid&$ok_redirect" \ |
There was a problem hiding this comment.
shot_login_error fails silently and leaks its mktemp cookie jar when the fetch or action-parse fails (curl -s without -f; the grep exiting 1 under pipefail aborts mid-function before the rm). Separately, the sed 's#<head>#…#' base-injection only matches a literal attribute-less <head> — a Keycloak bump that adds any attribute makes it a no-op, so every stylesheet 404s and the login-error visual gets signed off against a screenshot that never loaded the theme. Suggestions: -f on the curls, || fail around the action parse, a trap for the jar, and sed -E 's#<head([^>]*)>#…#'.
There was a problem hiding this comment.
Fixed in 43bd4be. Both curls use -f, the action parse and the POST are wrapped in || fail, a RETURN EXIT trap removes the cookie jar and the temp page on every exit path, and the <base> injection is sed -E 's#<head([^>]*)>#...#' with a follow-up check that the tag was actually inserted. Verified against a dead port: it fails with a message and leaves no temp files.
| @@ -0,0 +1,148 @@ | |||
| { | |||
There was a problem hiding this comment.
This fixture is a manual copy of keycloak/cdip-dev-realm.json whose only regeneration recipe is a python3 heredoc in the plan markdown — no checked-in script, no CI guard against drift. Drift starts as soon as the base realm changes (a pending branch already adds a cdip-integrations client this fixture lacks), and the duplicated client secrets now have to be rotated in two files. Suggestion: check the generator in as keycloak/dev/generate-realm.py and point the RUNBOOK at it.
There was a problem hiding this comment.
Fixed in 43bd4be. keycloak/dev/generate-realm.py is checked in and derives the fixture from cdip-dev-realm.json; --check exits 1 when the fixture is stale and runs as a CI step in the test job. The RUNBOOK points at it. The only drift so far was a trailing newline.
|
Looks great ! <3 I will skip the testing until it is in the dev environment, but Im looking forward to this update 🙌 |
…11y, realm generator, tag linkage - smoke.sh asserts the 26 login h1 reads "Welcome" (the CI failure) and guards the asset grep so an empty page reports its diagnostic instead of aborting. - screenshot.sh fails loudly on fetch or parse errors, cleans its cookie jar and temp page on every exit path, and injects <base> into a <head> with attributes. - The subtitle selector wraps the 26 page-id branch and the 11 :has() branch in :is(), so browsers without :has() still apply it on 26. On 11 the hidden h1 text uses visibility: hidden so the accessible name is "Welcome Sign in to Gundi" rather than including "Log In". The shared copy is cross-referenced between gundi.css and messages_en.properties. - theme.properties is now the real Keycloak 11 file, so copying the theme folder whole yields a working theme; theme.kc26.properties stays the variant. - keycloak/dev/generate-realm.py derives the dev realm fixture from the base export; CI fails when the fixture is stale, and the RUNBOOK points at it. - The workflow derives each image tag's version from the Dockerfile FROM tag, takes the short sha from GITHUB_SHA, and runs the vars job only on main. - RUNBOOK local-dev block waits for both ports before smoking them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Re the empty |
Jira: GUNDI-5744
Implements
docs/superpowers/specs/2026-09-22-keycloak-gundi-login-theme-design.md(spec and plan are in this branch).A
gundiKeycloak login theme that restyles the stock layout to match the React Gundi portal, built from one source folder for both Keycloak 11.0.2 (prod today) and Keycloak 26.7 (the planned upgrade). CSS and properties only, no FreeMarker overrides, so every login-flow page inherits it.What's in here
keycloak/themes/gundi/login/— shared tokens + stylesheet against stable#kc-*ids, one small PatternFly 3 file for 11 and one PatternFly 5 file for 26, two properties files, bundled Inter, logo mark, favicon, two message overrideskeycloak/Dockerfile.kc11/Dockerfile.kc26— official image + theme folder, no RUN stepskeycloak/compose.theme-dev.yml+keycloak/tests/*— local 11+26 harness, smoke test (asserts every theme asset is served), cascade guard, headless-Chrome screenshots.github/workflows/keycloak-theme.yml— builds both images, boots each with the dev realm, smoke-tests, and only then pushes toserca-artifact-registry/gundi/keycloakfrommainkeycloak/RUNBOOK.md— manual prod rollout (image swap →cdip-devrealm →cdip-prodrealm), rollback, and the hand-off requirement for the 26 upgradeNothing deploys automatically. Prod Keycloak 11 is not ArgoCD-managed; rollout is a manual
kubectl set imageplus a realm setting.Screenshots (local harness)
Before (the standard issue keycloak theme)
After (Gundi theme similar to EarthRanger ID)
Login, invalid-credentials, mobile and error pages on 11 and 26 are covered by
keycloak/tests/screenshot.sh. Runkeycloak/tests/screenshot.sh kc11 8081against the compose harness to regenerate.Notes for review
loginAccountTitlewhilekc11.csshides the h1 text and draws "Welcome" in its place. Both are scoped with:has(#kc-form-login), so they apply on the login page only. The<title>tag reads "Sign in to Gundi" on both.loginThemesetting lives in the DB and will already point atgundiafter the DB copy.keycloak-26-upgrade-doc) still lists the theme as stock; update it there when it lands.🤖 Generated with Claude Code