Release 1.18.2 → main (PROD promotion) - #1997
Merged
Merged
Conversation
…uard PR #1884 (feat/image-ingest-force) added GET /content/image-source and POST /content/image to srv/server.js without mirroring to srv-qa, tripping check-srv-qa-route-drift and reddening the postbuild:apps unit job on DEV and main. Both routes are dark-launched: no approuter route proxies to image-source on either channel (served HTML still emits raw.githubusercontent.com URLs via scripts/parsers/images.ts), and the QA publish flow never POSTs images. Mirroring now would pull image-store/image-ingest deps into the srv-qa cp list for an unreachable surface. Allowlist both as srv-only (matching the POST /content/orphan-purge maintenance precedent) with a note to re-evaluate QA parity when the image proxy goes live.
…-drift-images fix(ci): allowlist dark-launched image routes in srv-qa route-drift guard
…on fail-open (#1882) When /img-cdn fail-opens to GitHub on a store miss, the approuter now fire-and-forgets the fetched ORIGINAL bytes back to the srv's bytes-in POST /content/image so the store self-populates on first view — closing the gap between full backfills without depending on the srv's flagged GitHub egress. - approuter/lib/img-cdn-heal.js: injectable createHealer() with an in-memory TTL dedup map (one heal attempt per url per window), self-disabling when CONTENT_API_KEY is unresolvable. Non-blocking, never awaited, never throws. - approuter/server.js: instantiate healer; hook heal() in loadOriginalBytes on the github-source path. Gated by IMG_CDN_HEAL (default on) and off when IMG_CDN_SOURCE=github. - Auth: Bearer CONTENT_API_KEY via the credstore the approuter already binds. Channel derived server-side (channelFor); slug omitted. - Tests: test/unit/img-cdn-heal.test.js (9 cases).
Two independent breakages on the tutorial feedback UI, both wrongly suspected to be CDN/Akamai — verified live on PROD that neither is. #1890 — "Submit detailed feedback" opened an empty popup. The lazy-load in feedback-share.html built the island src via `{{ partial "island-src.html" ... | jsonify }}`. In a <script> JS context Hugo re-escapes the already-quoted jsonify output, embedding literal quote chars into the value: `s.src='"/js/tutorial-feedback-<hash>.js"'`. The browser resolves that relative to /tutorials/ → /tutorials/%22...%22 → 404, so mountTutorialFeedback never loads and the popup stays empty. Fixed by using island-src.html's documented contract (bare URL placed INSIDE quotes). Verified via a minimal Hugo render: old emits `"\"/js/…\""`, new emits `"/js/…"`. #1889 — star rating POST /feedback/submit returned 503 "feedback service unavailable". The Express guard read process.env.SUBMISSION_SALT_SECRET directly, but the hashing path (feedback-salt.js) resolves the same alias credstore-first via resolveSecret(). A secret set only in the BTP Credential Store (the intended admin-UI rotation path) therefore still 503'd the whole form. Routed the guard through resolveSecret() so it is consistent with the hashing path and admin-UI rotations take effect without cf set-env. Env fallback preserved → strictly backward-compatible. Adds a post-deploy e2e guard (tutorial-serve.test.js) that clicks the button and asserts the island loads (no 404) and the form renders.
…1882 feat(images): heal-on-request — approuter self-populates image store on fail-open (#1882)
A full QA content publish peaks ~883M RSS during append-batch BLOB uploads. At the old 768M the single srv-qa instance OOM-crashes (exit 134 / 502-503 no available endpoints), orphaning the publish JobLock and forcing re-runs to 409 until the 30-min TTL. Manual cf scale reverted on every MTA deploy; pin it in the base mta.yaml. Fixes #1893
…r joined users Add a keyword search box to the Devtoberfest sessions grid that matches across session title, abstract, and each speaker's name, role, company and bio (issue #1895). Bio was not previously in the feed: extend the speaker SELECT and mapper to carry it (LargeString, safe alongside metadata like the existing ABSTRACT). Also fix the sessions grid never passing :joined to PointsBanner, which made already-joined users see the 'Join Devtoberfest to earn points' CTA instead of their score (flagged on the issue). Mirror the schedule island wiring. Closes #1895
…1895 feat(devtoberfest): keyword search over sessions + fix Join banner
…p MCP OAuth Joule Desktop (and similar MCP connectors) use an authorization-code+PKCE public-client flow with a dynamic-port loopback redirect of the form http://localhost:<port>/mcp-callback. The XSUAA redirect-uri allow-list previously covered only /callback and /oauth/callback, so the authorize request failed with 'redirect_uri does not match the configuration'. Adds http://localhost:*/mcp-callback to oauth2-configuration.redirect-uris in all four descriptors (dev tutorials + prod tutorials-prod, root + .deploy) to keep them in sync.
fix(xsuaa): allow localhost /mcp-callback redirect for desktop MCP OAuth (Joule Desktop)
End users need a way to mint MCP Personal Access Tokens without the admin UI, which most don't have access to. The PatService (/pats, @requires authenticated-user, per-user row scoping) already supports self-service mint/revoke — only an end-user surface was missing. Adds a 'tokens' Vue island (hugo-apps/src/tokens) hosted on a private Hugo page at /me/tokens/. It lists the signed-in user's tokens and mints new ones (name, read | read+write scope, TTL) via the existing /pats/* XSUAA route, revealing the plaintext once, with per-row revoke. Reuses the /me islands' authenticated-fetch + csrfFetch pattern; no backend change. Linked from /me/ (new 'API Tokens' panel) and the API page (/api-docs/). Updates docs/end-users/mcp-quickstart.md to point PAT minting at /me/tokens/ instead of /admin-ui/#pats. Tests: hugo-apps/src/tokens/__tests__/ApiTokens.test.ts (list, auth gate, mint reveal-once, scope mapping, bound-action revoke). Full hugo-apps unit suite green (1098 passed); csrf-clients + postbuild guards pass.
…istration Adds top-level skills/<slug>/SKILL.md entries in the layout the SAP AI Skills Library (github.com/SAP/ai-skills-library) discovers, so this public repo can be registered there: - graphify: knowledge-graph builder (wraps third-party graphifyy; attribution preserved) - whats-new: self-contained gh-based merged-PR digest builder (repo-agnostic) - sap-tutorials-content: read-only access to public developers.sap.com tutorial content (catalog, HTML/JSON, OData search, and the anonymous /mcp/search MCP endpoint) Inert to the Hugo/CAP build; exists only for skills-registry discovery.
feat(mcp): self-service /me/tokens PAT minting page (#1897)
fix(deploy): pin tutorials-srv-qa memory to 1536M (#1893)
…ublish docs(skills): publish skills/ folder for SAP AI Skills Library
#1901) PR #1897 added the /me/tokens PAT page island under hugo-apps/src/tokens/, which uses <ui5-text>. The ui5-entry-page-map only mapped the /me area's islands under hugo-apps/src/me/ to the ui5-me entry (the only entry that imports Text.js), so the tokens island fell back to ui5-core and the check-ui5-entry-coverage build guard failed (<ui5-text> not registered). Extend the ui5-me glob to include hugo-apps/src/tokens/. Runtime rendering already worked (ui5-bootstrap.ts registers Text.js); this fixes the build guard so a full DEV build/deploy passes. Verified: guard now reports OK.
Ships the DEV release train to PROD (1.16.0 → 1.17.0): Features: - feat(mcp): self-service /me/tokens PAT minting page (#1897) - feat(devtoberfest): keyword search over sessions + Join banner fix (#1896) - feat(images): heal-on-request — approuter self-populates image store on fail-open (#1882) Fixes / chores: - fix(ui5): map /me/tokens island to ui5-me entry so ui5-text is covered (#1901) - fix(deploy): pin tutorials-srv-qa memory to 1536M (#1894) - fix(xsuaa): allow http://localhost:*/mcp-callback redirect for desktop MCP OAuth (#1898) - fix(feedback): unblock tutorial feedback form (#1889, #1890) - fix(ci): allowlist dark-launched image routes in srv-qa route-drift guard (#1887) - docs(skills): add public skills/ folder for SAP AI Skills Library registration (#1900)
… (#1906) The verdict badge (✓/✗/⚠) sits in an inline-flex legend next to the question text. With only `width: 1.5rem` and the default `flex-shrink: 1`, a long question squeezes the fixed-width circle below 1.5rem while its height stays 1.5rem — rendering as a squished vertical oval. Add `min-width: 1.5rem` + `flex-shrink: 0` so the badge always stays a 1.5rem circle regardless of question length. Verified in-browser: long-question legend rendered the old badge at 10.5px x 24px (oval) vs 24px x 24px (circle) with the fix.
… swallowing (#1904) (#1907) In an expired session AppRouter answers /auth/user and API calls with the XSUAA login interstitial: HTTP 200 + text/html whose inline <script> navigates the top-level document to /oauth/authorize. Consumed via fetch() that script never runs, so csrfFetch's token handshake threw a generic CsrfFetchError that callers swallowed (e.g. the tutorial 'Done' button flipped to 'Saving...' then reverted, with no progress and no re-login). Detect the interstitial (a followed redirect, or a 200 HTML body where JSON was expected) on both the /auth/user handshake and the mutating request itself, and force a top-level navigation to /login?returnTo=<path> so the OAuth redirect actually executes. Mirrors header.html's checkAuth pattern. 204/JSON responses are never flagged; detection is header-bag defensive for minimal Response stubs. Covers every mutating action routed through the shared csrfFetch.
…1908) (#1913) Three easter-egg clues, one per public page, sourced from hugo/data/scavenger_hunt.json and reusing a shared figure partial: - Home: subtle right-gutter hotspot; the advocate hero (Ajay Soreng) pops out on hover/focus with the '1st letter of first name' clue. - /ai/: DJ Adams peeks up from the bottom edge and pops fully up on hover/focus (no letter, congratulatory clue). - /api-docs/: Shilpa Shankar embedded inline at the bottom of the article via a shortcode, with the '4th letter of first name' clue. Hero images come from the public /api/advocates/:slug/photo route (transparent-background WebP). Interactions are pure CSS (:hover/:focus-within), honour prefers-reduced-motion, and load no JS. CSS + partial are gated to the three pages only. Adds an anonymous post-deploy e2e spec (self-skips locally).
…1909) (#1915) The native <input type=file> behind core:HTML #importFileInput only had its change listener attached in the view's onAfterRendering. That input lives inside the edit-mode VBox (visible bound to mode==='edit'), so at initial (list-mode) render its DOM does not exist and no listener is wired. Clicking 'Create New' re-renders only that VBox subtree via invalidation — the view's onAfterRendering does NOT re-fire — so the materialized <input> never got a listener. onImportPress (repaired in #1834) opened the file dialog, the user picked a file, but the change event landed on an input with no listener, so onImportFile never ran and the import silently did nothing. Fix: extract _wireImportInput() and also call it from onImportPress, where the input is guaranteed present. onAfterRendering keeps wiring too (belt-and-suspenders). Tests: - test/unit/puzzle-import-wiring.test.js loads the real controller (+ real puzzle-io/geometry, UI5 deps stubbed) and drives onImportPress → change with the exact #1909 file shape (numeric-string rows/cols); fails before the fix. - puzzle-designer e2e: Create New → Import JSON via the file chooser asserts the 'Puzzle imported' toast and populated title/slug (Export was covered, Import was not).
Shuffle the fetched slideshow entries with Fisher-Yates in onMounted so visitors don't always see the newest uploads first. Server still returns a deterministic order; randomization is purely presentational and re-rolls on each page load. next/prev/dots stay consistent since they're index-based. Adds unit tests: entry-preservation invariant + deterministic mocked-RNG shuffle.
Adds an owner-scoped 'withdraw' action to PetoberfestService so a user can take down their submission even after admin approval. Behaviour: - Hard-deletes the PetSubmissions row (and its inline image blobs). - Revokes the PETOBERFEST completion (sets the TaskRecord to SUPERSEDED) only when it was the user's last remaining entry for the contest; withdrawing one of several keeps the credit. - SUPERSEDED (not deleted) matches the award idempotency check in petoberfest-upload.js, so a later re-upload re-awards cleanly. - POST action (not DELETE) to survive Akamai's bare-verb block in PROD. Frontend: adds a Withdraw button + confirm to the 'Your pets' list in the Vue island; on success it drops the row locally and refreshes the slideshow. Note: does not recall a badge already auto-sent to NGDS (no un-send path); local credit is revoked. Tests: new test/unit/petoberfest-withdraw.test.js covers owner delete, owner-scoping (non-owner rejected), last-entry supersede vs keep-credit, and re-award after full withdraw.
* Release 1.17.0 — DEV → main (PROD promotion) (#1903) * fix(ci): allowlist dark-launched image routes in srv-qa route-drift guard PR #1884 (feat/image-ingest-force) added GET /content/image-source and POST /content/image to srv/server.js without mirroring to srv-qa, tripping check-srv-qa-route-drift and reddening the postbuild:apps unit job on DEV and main. Both routes are dark-launched: no approuter route proxies to image-source on either channel (served HTML still emits raw.githubusercontent.com URLs via scripts/parsers/images.ts), and the QA publish flow never POSTs images. Mirroring now would pull image-store/image-ingest deps into the srv-qa cp list for an unreachable surface. Allowlist both as srv-only (matching the POST /content/orphan-purge maintenance precedent) with a note to re-evaluate QA parity when the image proxy goes live. * feat(images): heal-on-request — approuter self-populates image store on fail-open (#1882) When /img-cdn fail-opens to GitHub on a store miss, the approuter now fire-and-forgets the fetched ORIGINAL bytes back to the srv's bytes-in POST /content/image so the store self-populates on first view — closing the gap between full backfills without depending on the srv's flagged GitHub egress. - approuter/lib/img-cdn-heal.js: injectable createHealer() with an in-memory TTL dedup map (one heal attempt per url per window), self-disabling when CONTENT_API_KEY is unresolvable. Non-blocking, never awaited, never throws. - approuter/server.js: instantiate healer; hook heal() in loadOriginalBytes on the github-source path. Gated by IMG_CDN_HEAL (default on) and off when IMG_CDN_SOURCE=github. - Auth: Bearer CONTENT_API_KEY via the credstore the approuter already binds. Channel derived server-side (channelFor); slug omitted. - Tests: test/unit/img-cdn-heal.test.js (9 cases). * fix(feedback): unblock tutorial feedback form (#1889, #1890) Two independent breakages on the tutorial feedback UI, both wrongly suspected to be CDN/Akamai — verified live on PROD that neither is. #1890 — "Submit detailed feedback" opened an empty popup. The lazy-load in feedback-share.html built the island src via `{{ partial "island-src.html" ... | jsonify }}`. In a <script> JS context Hugo re-escapes the already-quoted jsonify output, embedding literal quote chars into the value: `s.src='"/js/tutorial-feedback-<hash>.js"'`. The browser resolves that relative to /tutorials/ → /tutorials/%22...%22 → 404, so mountTutorialFeedback never loads and the popup stays empty. Fixed by using island-src.html's documented contract (bare URL placed INSIDE quotes). Verified via a minimal Hugo render: old emits `"\"/js/…\""`, new emits `"/js/…"`. #1889 — star rating POST /feedback/submit returned 503 "feedback service unavailable". The Express guard read process.env.SUBMISSION_SALT_SECRET directly, but the hashing path (feedback-salt.js) resolves the same alias credstore-first via resolveSecret(). A secret set only in the BTP Credential Store (the intended admin-UI rotation path) therefore still 503'd the whole form. Routed the guard through resolveSecret() so it is consistent with the hashing path and admin-UI rotations take effect without cf set-env. Env fallback preserved → strictly backward-compatible. Adds a post-deploy e2e guard (tutorial-serve.test.js) that clicks the button and asserts the island loads (no 404) and the form renders. * fix(deploy): pin tutorials-srv-qa memory to 1536M A full QA content publish peaks ~883M RSS during append-batch BLOB uploads. At the old 768M the single srv-qa instance OOM-crashes (exit 134 / 502-503 no available endpoints), orphaning the publish JobLock and forcing re-runs to 409 until the 30-min TTL. Manual cf scale reverted on every MTA deploy; pin it in the base mta.yaml. Fixes #1893 * feat(devtoberfest): keyword search over sessions + fix Join banner for joined users Add a keyword search box to the Devtoberfest sessions grid that matches across session title, abstract, and each speaker's name, role, company and bio (issue #1895). Bio was not previously in the feed: extend the speaker SELECT and mapper to carry it (LargeString, safe alongside metadata like the existing ABSTRACT). Also fix the sessions grid never passing :joined to PointsBanner, which made already-joined users see the 'Join Devtoberfest to earn points' CTA instead of their score (flagged on the issue). Mirror the schedule island wiring. Closes #1895 * fix(xsuaa): allow http://localhost:*/mcp-callback redirect for desktop MCP OAuth Joule Desktop (and similar MCP connectors) use an authorization-code+PKCE public-client flow with a dynamic-port loopback redirect of the form http://localhost:<port>/mcp-callback. The XSUAA redirect-uri allow-list previously covered only /callback and /oauth/callback, so the authorize request failed with 'redirect_uri does not match the configuration'. Adds http://localhost:*/mcp-callback to oauth2-configuration.redirect-uris in all four descriptors (dev tutorials + prod tutorials-prod, root + .deploy) to keep them in sync. * feat(mcp): self-service /me/tokens PAT minting page (#1897) End users need a way to mint MCP Personal Access Tokens without the admin UI, which most don't have access to. The PatService (/pats, @requires authenticated-user, per-user row scoping) already supports self-service mint/revoke — only an end-user surface was missing. Adds a 'tokens' Vue island (hugo-apps/src/tokens) hosted on a private Hugo page at /me/tokens/. It lists the signed-in user's tokens and mints new ones (name, read | read+write scope, TTL) via the existing /pats/* XSUAA route, revealing the plaintext once, with per-row revoke. Reuses the /me islands' authenticated-fetch + csrfFetch pattern; no backend change. Linked from /me/ (new 'API Tokens' panel) and the API page (/api-docs/). Updates docs/end-users/mcp-quickstart.md to point PAT minting at /me/tokens/ instead of /admin-ui/#pats. Tests: hugo-apps/src/tokens/__tests__/ApiTokens.test.ts (list, auth gate, mint reveal-once, scope mapping, bound-action revoke). Full hugo-apps unit suite green (1098 passed); csrf-clients + postbuild guards pass. * docs(skills): add public skills/ folder for SAP AI Skills Library registration Adds top-level skills/<slug>/SKILL.md entries in the layout the SAP AI Skills Library (github.com/SAP/ai-skills-library) discovers, so this public repo can be registered there: - graphify: knowledge-graph builder (wraps third-party graphifyy; attribution preserved) - whats-new: self-contained gh-based merged-PR digest builder (repo-agnostic) - sap-tutorials-content: read-only access to public developers.sap.com tutorial content (catalog, HTML/JSON, OData search, and the anonymous /mcp/search MCP endpoint) Inert to the Hugo/CAP build; exists only for skills-registry discovery. * fix(ui5): map /me/tokens island to ui5-me entry so ui5-text is covered (#1901) PR #1897 added the /me/tokens PAT page island under hugo-apps/src/tokens/, which uses <ui5-text>. The ui5-entry-page-map only mapped the /me area's islands under hugo-apps/src/me/ to the ui5-me entry (the only entry that imports Text.js), so the tokens island fell back to ui5-core and the check-ui5-entry-coverage build guard failed (<ui5-text> not registered). Extend the ui5-me glob to include hugo-apps/src/tokens/. Runtime rendering already worked (ui5-bootstrap.ts registers Text.js); this fixes the build guard so a full DEV build/deploy passes. Verified: guard now reports OK. * chore(release): bump MTA version to 1.17.0 for PROD release Ships the DEV release train to PROD (1.16.0 → 1.17.0): Features: - feat(mcp): self-service /me/tokens PAT minting page (#1897) - feat(devtoberfest): keyword search over sessions + Join banner fix (#1896) - feat(images): heal-on-request — approuter self-populates image store on fail-open (#1882) Fixes / chores: - fix(ui5): map /me/tokens island to ui5-me entry so ui5-text is covered (#1901) - fix(deploy): pin tutorials-srv-qa memory to 1536M (#1894) - fix(xsuaa): allow http://localhost:*/mcp-callback redirect for desktop MCP OAuth (#1898) - fix(feedback): unblock tutorial feedback form (#1889, #1890) - fix(ci): allowlist dark-launched image routes in srv-qa route-drift guard (#1887) - docs(skills): add public skills/ folder for SAP AI Skills Library registration (#1900) * fix(petoberfest): don't show upload form to anonymous visitors probeAuth() checked only r.ok on GET /auth/user. Through the approuter, an anonymous visitor's request to the XSUAA-gated /auth/user route is answered with HTTP 200 + an HTML login-redirect page (not a 401), so r.ok was true for logged-out users and the 'Add your pet' upload form rendered for everyone instead of the sign-in prompt. Mirror the canonical robust probe from homepage-personalizer/coordinator.ts: require a JSON content-type AND a truthy body.authenticated. Verified against live PROD edge responses (anon 200-HTML -> false, authed JSON -> true). Adds probeAuth unit tests (incl. the 200-HTML anon case) and App.vue login-gate render tests (anon -> sign-in prompt, no form; authed -> form).
Puzzle solver pages were Hugo-static — one hand-authored hugo/content/puzzles/<slug>.md per puzzle, baked into the approuter static dir. A puzzle created (or slug-renamed) in the admin UI therefore 404'd: its row is in HANA and served by /puzzle-api, but no static page existed and nothing generated one from the catalog (documented as an unimplemented fast-follow in hugo/layouts/puzzles/single.html). Serve the page from CAP for any puzzle that exists in HANA, so an admin-created puzzle works immediately with no rebuild/deploy. The page is a thin island shell (the puzzle Vue island fetches grid/clue data from /puzzle-api at runtime), composed into the __shell__ chrome via the same composeShell path group/mission/concept pages use. - srv/lib/puzzle-page.js: renderPuzzleBody + createPuzzlePage handler factory (per-slug version+modifiedAt-keyed gzip cache, fail-open stale/503, styled 404 short-TTL for unknown slugs). Selects only scalar columns (never the layout/solution LOBs). - chrome-shell.js: canonicalUrlFor + breadcrumb for kind 'puzzle'. - srv/server.js: GET /content/puzzles/:slug. - approuter/xs-app.json: /puzzles/<slug> -> /content/puzzles/<slug> (anonymous). Bare /puzzles/ and /puzzle-api/* untouched. Verified: 66 unit tests green; all four postbuild guards pass; live cds-watch boot returns the styled 404 for an unknown slug.
…draw feat(petoberfest): let users withdraw their own uploaded photo
…errors
The multipart/form-data upload POST was silently stalled/blocked by the Akamai
edge on developers.sap.com while working fine on the raw cfapps approuter URL.
Switch the transport to a JSON base64 body over the same xsuaa + csrfFetch path
that /chat/stream and /api/* islands already use successfully through the CDN.
Also stop swallowing errors (the 'no response, no error, nothing' symptom):
- client uploadPet now sends {petName,filename,mimeType,photoBase64} JSON,
surfaces HTTP status + any Akamai reference id on non-JSON edge errors, and
times out after 60s (AbortController) instead of hanging silently.
- server /petoberfest-api/:slug/upload parses express.json(16mb) + new
decodePhotoUpload() (10MB decoded cap); multer removed from this route.
- tests: uploadPet JSON/error/timeout, decodePhotoUpload unit, e2e upload leg -> JSON.
…-upload fix(petoberfest): JSON base64 upload to survive Akamai CDN + surface errors
…oute-1914 fix(puzzles): serve /puzzles/<slug> dynamically from CAP (#1914)
…ation-accuracy # Conflicts: # hugo-apps/src/tutorial-prefs/TutorialPrefsPopover.vue # hugo-apps/src/tutorial-prefs/main.ts # hugo-apps/src/tutorial-prefs/prefs-store.ts
…ty sort (final review C1/I1/I2)
…on-accuracy Camera input: accuracy fixes + per-user calibration (eye + hand)
…or-spec feat(freshness): AI tutorial freshness detector (v1, reviewer-assist)
Re-captured against deployed DEV. region cleared on /, /topics/, /tags/ (and home image-redundant-alt); developer-advocates dropped from 8 violations (2 serious) to 3 (0 serious) — nested-interactive / heading-order / scrollable-region-focusable all resolved. Baseline: 27 → 16 rule-instances. Criticals 0, serious 1 (a lone color-contrast on one tutorial). What remains is almost entirely the duplicate-<main> landmark family (browse/tutorials/advocates) — the next structural follow-up — plus one region on /concepts/ from a different stray element than nav-progress.
…cepts landmark Clears the remaining baseline items (all landmark/contrast, no criticals): - Duplicate <main>: baseof wrapped every page in <main> while browse/advocates/ tutorial layouts opened their own. Convert main#browse-results and main#advocates-mount to <div> (islands mount by id, nothing selects them by <main> tag). For tutorial detail pages, suppress baseof's OUTER main (emit <div class=site-main> with the same body>main flex) and keep <main class=tutorial-main> as the sole landmark — publish-content.ts's search body-extraction regex depends on that exact tag, so it is untouched. - Browse double-banner: drop role=banner from .browse-banner (a section header nested under the shellbar's banner landmark). - color-contrast (serious): .tutorial-figcaption used --sapContent_LabelColor (Horizon #556B82 ~4.1:1); pin to #47566a ~6.4:1. - /concepts/ region: the concept body was spliced into the shell (whose <main> is a marker) without its own <main>, leaving the page with zero landmarks. Wrap the concept body in <main> (mirrors the browse/topics bodies). Verified: hugo build parses/builds clean (exit 0); concept-detail-render + publish-concepts + publish-content tests green; sap-fundamental.css rebuilt via build:css. Landmark/contrast wins confirm post-deploy via the a11y gate.
a11y: ratchet baseline down after advocates/nav-progress fixes deployed
a11y: duplicate-main + browse banner + figcaption contrast + concepts landmark
Extend the Text > Reading width control (Full/Narrow) with a third opt-in 'wide' option that uncaps .op-page to the full browser width. The .op-twocol grid is left intact so the sticky right TOC stays and the content column absorbs the freed space. Default remains 'full'. - constants: ReadWidth += 'wide' - prefs-store: getReadWidth accepts 'wide' (junk -> full) - head.html: pre-paint accepts 'wide' (no layout flash) - ui5-overrides.css: data-tut-read-width=wide -> .op-page max-width:none - popover: third 'Super wide' segmented item + onWidthSelect guard - tests: prefs-store + display-chrome cover the wide path
…Next Swipes clicked the inter-tutorial Prev/Next pills (.tutorial-stepnav__slot--*), so a detected gesture jumped to a different tutorial instead of moving between steps within the current one. Retarget nav-dispatch at the layout's step machinery: u1-object-page now exposes window.opStepNav(dir), reusing the same getCurrentStep/scrollToStep/ resync path as the mobile sheet's prev/next buttons. Boundaries (first/last step) no-op. Drop the now-dead SEL_NAV_* selectors and rewrite nav-dispatch tests to pin the opStepNav contract.
…1966 feat(prefs): add Super wide reading-width option (#1966)
Eye auto-scroll couldn't fire for real users: it required iris gazeY > threshold
AND headForward (pitch < 0.55), but live telemetry showed gazeY reads ~0 and
inverted while normalized pitch runs 0.65-1.11 — so headForward was never true
and the gaze envelope sat at the reject floor.
Switch the calibrated signal from iris-gazeY to HEAD PITCH:
- EyeProfile now stores {pitchMin,pitchMax}; CAL_PROFILE_VERSION bumped to 2 so
stale gaze profiles auto-invalidate and users recalibrate once.
- Calibration samples pitch during the top->bottom scan; GazeDetector fires DOWN
when pitch enters the top (looking down) end of the envelope and UP the bottom
end, with a resting-center deadband (CAL_EYE_DOWN/UP_FRACTION). EMA, dwell,
grace, cooldown unchanged.
- headForward gate dropped; uncalibrated eye tracking is inert (waits for a
profile rather than guessing an absolute baseline).
- cam-debug overlay shows pitch vs up/down thresholds; gazeY kept for info only.
- Tests + end-user docs updated.
…ps-eye-pitch fix(tutorial-prefs): hand gestures navigate steps + calibrated head-pitch eye scroll (up/down)
onSizeSelect/onWidthSelect now skip the emit when the picked value equals the current pref, matching the existing Header guard (mode !== headerMode). UI5's ui5-segmented-button fires selection-change for the already-pressed item on mount; the guard stops that redundant localStorage re-write + applyDisplayChrome() call. onSizeSelect is shared across text/code/img, so it maps each event to its matching prop. Adds 8 unit tests (same-value → no emit, changed-value → emit) across all four controls.
…-guard fix(prefs): guard size/width segmented-button emit on mount (#1980)
Add advocate cut-out frames from D:/tmp/NewPhotos: Ad, Ajay, Antonio_Sport, cwb, Kasimir_Hat, Kasimir_Josh, Kasimir_Smiles, Kasimir_Types, Rekha, Sheena, Shilpa, Shrini, Singers. Full frames downscaled to <=1600px longest side (alpha preserved), 320px-wide thumbnails generated. Registered in data-frames CSV in hugo/layouts/devtoberfest/selfie.html. Frames force-added past the global *.png gitignore, matching the existing 24 tracked frames.
The 'blank' entry in data-frames provides a transparent no-advocate frame, but frames/blank.png was untracked while its thumbnail was committed — a clean checkout would 404 the frame when 'blank' is picked. Force-add the 600x600 fully-transparent PNG past the *.png gitignore, matching the other tracked frames.
…frames feat(selfie): add 13 advocate frames + thumbnails
#1989 removed tutorials' outer <main> (baseof) to fix duplicate-main. That un-scoped the tutorial's <header class=op-header> (a sibling of main.tutorial-main), so it resolved to a second banner landmark colliding with the shellbar (axe landmark-no-duplicate-banner). Make op-header a <div> — its class-based CSS + the .op-page > .op-header ResizeObserver selector are unaffected, and the shellbar stays the sole banner. The h1 title remains. Also bump the a11y scan settle 2500ms -> 4000ms: on heavy tutorial pages UI5 theme application wasn't complete at 2500ms, so axe sampled a pre-theme transient figcaption colour (#6f7b8a, 4.3:1) and false-flagged color-contrast. At full settle every figcaption is the fixed #47566a (6.4:1).
a11y: fix tutorial duplicate-banner regression + lengthen scan settle
…serious Post-deploy retest of the landmark/contrast fixes. The scan now injects a style tag zeroing animation/transition durations before axe runs: tutorial figures fade opacity 0→1 (.tutorial-figure), and axe sampling one mid-fade composited the caption colour toward white and reported a transient color-contrast failure (~4.3:1) though the settled caption is #47566a (6.4:1). Collapsing fades to their rest state removes the false positive (verified: all .tutorial-figure reach opacity 1, color-contrast gone). Re-baselined against deployed DEV: 27 → 2 rule-instances, criticals 0, serious 0. 6 of 8 sampled pages fully clean; the two tutorials retain only 'region' (moderate) — the h1/tab-strip sit outside the single <main>, accepted as-is (fixing it needs a tutorial-layout + search-extraction refactor).
a11y: neutralize animations in scan + ratchet baseline to 0 critical/serious
Patch bump ahead of the DEV → main PROD promotion.
chore(release): bump MTA version to 1.18.2 for PROD release
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PROD promotion of DEV → main at version 1.18.2.
Version
1.18.1 → 1.18.2in.deploy/mta.yaml(maintainer decision: patch for this release).Scope
239 commits ahead of main. Highlights: content freshness subsystem, a11y landmarks + baseline ratchet, tutorial reading-prefs (super-wide width, hand/eye navigation), selfie advocate frames.
main is a strict subset of DEV (0 main-only commits) — clean fast-forwardable promotion, no reconcile merge needed.
Deploy
After merge, deploy from a FRESH
origin/mainper the PROD runbook (fullbuild:all+mbt build+cf deploy -e ../deploy/prod.mtaext).