Skip to content

release v1.40.3 DEV to main promotion - #2871

Merged
jung-thomas merged 55 commits into
mainfrom
release/v1.40.3
Oct 21, 2026
Merged

jung-thomas merged 55 commits into
mainfrom
release/v1.40.3

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Release 1.40.3 — DEV → main promotion

Patch release promoting the fixes and features merged to DEV since 1.40.2 to main for a PROD deploy.

Version bumps

MTA File Change
Main (tutorials-ims) .deploy/mta.yaml 1.40.2 → 1.40.3
MCP (tutorials-mcp) mta-mcp.yaml 1.0.2 → 1.0.3

Changes in this window

What's New digest

hugo/data/whats_new.json regenerated from the 14 merged PRs in this window (12 substantive + the 1.40.1/1.40.2 release entries).

Pre-merge: confirm cf target points at the intended space before the PROD deploy.

- LegacyRedirects.csv row 018: /abap -> https://pages.community.sap.com/topics/abap
  (repo seed drifted from live PROD DB, edited out-of-band; realigning source of truth)
- redirects smoke: expect the Community ABAP topic URL, not the old same-origin path
- content-signal smoke: probe a served HANA tutorial page (/tutorials/abap-dev-create-table/)
  instead of /tutorials/, which now 301s to /tutorial-navigator/ (yielded null headers)

Verified: both smoke files 7/7 green against live PROD.
…smoke

fix(smoke): align /abap redirect seed + smoke probes to live behavior
The tutorial-variables island JS was gated by (not site.Params.qa)
(not site.Params.previewMode), so interactive {{var:NAME}} fields never
mounted on /tutorials-qa/ — the token flattened to its frontmatter
placeholder with no input field. Authors previewing a tutorial in the QA
channel could not verify the variables feature before promoting to prod.

Drop the qa/previewMode conditions from the island loader, keeping the
.Params.variables guard so variable-free tutorials still ship no extra
script. The panel div and config JSON were already gated only by
.Params.variables (render on QA), and sync-island-manifest-qa.cjs copies
the whole island manifest, so the tutorial-variables fingerprint is
already available to the QA build — no other wiring needed.
feat(#2811): load tutorial-variables island in QA preview
…-variables

feat: author-fixed and hidden tutorial variables
Trailing substep content after an [OPTION] group (prose, images,
`> [!TIP]` alerts) was leaking as raw markdown inside a code block on
rendered tutorials (e.g. hana-clients-install step 3).

Root cause: a Hugo shortcode that emits a block-level <div>
(os-options/os-panel) closes the surrounding tight numbered list
regardless of source indentation (verified against production-pinned
Hugo 0.147.7). Once the list closes, Goldmark measures the still-4-space
-indented tail from column 0, where the >=4-space rule turns it into a
literal indented-code block. Commit 19db8d0 deleted the "Bug B"
tail-dedent on the false premise that indented wrappers keep the list
open — the Hugo renders disprove that; the list always closes, so the
dedent is mandatory.

Fix (two parts):
- Parser: restore the tail-dedent in convertOptionBlocks — trailing
  substep content is dedented to column 0 so it renders as sibling
  markdown; the author's `N.` ordinal reopens the list as <ol start="N">
  and numbering continues.
- CSS: split the trailing-figure rule so `margin-top/bottom` sets the
  rhythm without the `margin` shorthand clobbering `margin-left: 1.5rem`
  — a latent bug the parser fix exposes (the trailing image otherwise
  jams to the far-left column).

Verified before/after on the real production path (composeTutorial ->
renderHugoFrontmatter {{% tutorial-step %}} -> Hugo) across
hana-clients-install/-hdbsql/-jdbc/-golang: all leaked BEFORE, all CLEAN
AFTER. New render-level regression test test/unit/option-tail-leak.test.ts
reproduces the leak via the percent-shortcode path (the only path that
reproduces it) and asserts the corrected contract. Updated two tests
that encoded the old (impossible "nest in <li>") behavior.

Design: docs/superpowers/specs/2026-10-20-option-block-rendering-design.md
…rendering

fix(options): dedent OPTION-group trailing content to col 0 (#2820)
/app-space/?eventId= worked but ?eventid= (or any other casing) silently
fell back to the default event, because URLSearchParams.get('eventId') is
case-sensitive. Iterate the params and match the key case-insensitively so
any casing of the eventid key resolves the event.

Adds a regression test asserting the backend getAppSpaceProgress call carries
the URL-supplied id (not the default 0) for eventId/eventid/EVENTID/EventId.
…-insensitive

fix(app-space): read eventId query param case-insensitively
Adds GET /api/my-prize-barcode?eventId=<legacyId> — a session-authenticated
endpoint that mints a QR PNG encoding { uid: <Users.ID>, payload: { eventId } }
for the logged-in visitor. Identity is resolved from the XSUAA session via
resolveDbUserScoped; client cannot inject a uid via query string.

Cache-Control: private, no-store (identity-bearing; no CDN/browser cache).
401 when no DB user resolves; 500 on QR generation failure.

Route registered immediately after app.use(basicAuthMiddleware) in server.js,
alongside the existing /api/qrcode handler.
Tasks 3 + 4:
- Add pure helpers checkpointBarcodeUrl / shouldShowCheckpointBarcode
  in checkpoint-barcode.ts (testable without mounting the component)
- Wire authenticated ref from getAppSpaceProgress data.authenticated
- Replace dead PRIZE/EARNED QR block with CHECKPOINT barcode section:
  shows <img src="/api/my-prize-barcode?eventId=..."> when track
  complete + authenticated; shows sign-in hint when not authenticated
- Remove dead qrCodeUrl helper (no other callers confirmed via grep)
- Add 10 tests covering URL shape, gate conditions, and render paths
…helper

shouldShowCheckpointBarcode now takes (item, unlocked, authenticated)
instead of (track, index, authenticated), eliminating the copy of the
CHECKPOINT unlock logic that was already in isItemUnlocked(). The
component wrapper passes isItemUnlocked(track, index) as the unlocked
arg — single source of truth. Tests updated to the flat signature.
- getContestant now accepts accountNumber as a UUID string (Users.ID),
  dropping the parseInt / legacyId lookup path.
- Adds eventId: Integer param; resolves Events by legacyId then scopes
  PrizeRecords count to that event_ID. Degrades gracefully (no event filter)
  when eventId is absent or unresolvable.
- Returns claimedCount: Integer + prizeRecords: '<N> of 2 prizes claimed'
  replacing the per-prize-title enumeration. Completion counts unchanged.
- claimPrize is untouched; all existing ownership tests still pass.
- New test: test/unit/scanner-contestant-count.test.js (4 cases: scoped
  count, event isolation, completion fields present, unknown UUID → 404).
Add ScannerService.claimPrizeForContestant(accountNumber, eventId):
- Inserts a CLAIMED PrizeRecords row for (user UUID, event legacyId) iff
  current CLAIMED count < 2; returns { claimedCount, message }.
- Atomicity: uses SELECT.forUpdate() on the user row to acquire an exclusive
  row-level write lock before reading the prize count. On HANA this
  serializes concurrent calls for the same user. On SQLite (unit tests)
  forUpdate() is a no-op but Node.js single-threaded event loop serializes
  the async chain within one cds.tx.
- Old claimPrize kept in place (still works on legacy pre-existing rows).
- legacyId left null on new rows — nullable on HANA, no @mandatory in CDS.

Tests: test/unit/scanner-claim-capped.test.js (5 tests, all pass)
  (a) 1 CLAIMED → claim → claimedCount===2, new row exists
  (b) 2 CLAIMED → claim → cap message, no new row
  (c) Promise.all two concurrent claims from count 1 → final count 2
…5 regression)

scanner-service-auth.test.js seeded { ID: 'u-alice', legacyId: 8001 } but
called getContestant({ accountNumber: '8001' }) — the legacyId. Task 5 changed
getContestant to resolve by Users.ID (UUID), so '8001' no longer matched.
Changed all three call sites to accountNumber: 'u-alice' (the UUID). Auth
assertions (MobileApp scope guard) are unchanged.
getAppSpaceProgress / getEventProgress emitted one output item per
CompletionPathItems row and hard-coded each path's description to the
mission's. Two visible bugs on the App-Space event page:

1. A path whose item is a GROUP rendered "0 tutorials" — the GROUP row
   was emitted verbatim as type:'GROUP', which the frontend island
   (filters type==='TUTORIAL') dropped. Now expand GROUP items into their
   child tutorials via GroupPathItems (mirrors build-catalog.js), batching
   the child TaskRecord lookups so progress still renders.

2. Every path card showed the same mission description. Now use the
   path's own CompletionPaths.description (fallback to mission).

Shared paths-building extracted to buildPathItems/expandGroupTutorials/
groupChildLegacyIds in path-item-task-resolver.js so both twin handlers
stay in sync. Adds regression tests (GROUP expansion + per-path
description) to test/app-space-fk-resolution.test.js.
…sion-path-desc

fix(app-space): expand GROUP path items + use per-path description
- types.ts: add eventId to ScanResult.payload; add claimedCount to ContestantData
- useScannerApi.ts: getContestant(uid, eventId) passes eventId in URL;
  replace claimPrize with claimPrizeForContestant(uid, eventId) returning
  {claimedCount, message}; old claimPrize wrapper removed (was dead code)
- ScannerApp.vue: parseScanData reads payload.eventId; loadContestant passes
  eventId to getContestant and stores claimedCount; template shows
  "Prizes claimed: N of 2"; handover button disabled at claimedCount >= 2;
  on success shows returned message and refreshes via getContestant;
  manual placeholder updated to new QR shape
- test: scanner-claim.test.ts covers URL shape for both new functions (7/7 pass)
…ybrid test

Finding #1 — UI5 App at /scanner-ui/ still used old payload.recordId shape and
called legacy claimPrize(). Updated to new contract:
- CONTESTANT_DEFAULTS: recordId/status -> eventId/claimedCount
- _getContestant: now accepts (uid, eventId) and sets /claimedCount from result
- _processScanResult: reads payload.eventId (Number), drops recordId/status
- onClaimPrize: calls /claimPrizeForContestant(...) with accountNumber + eventId;
  caps at claimedCount >= 2 (shows MessageBox.warning); shows returned message
- View: primary prize display is "N of 2" counter; button label "Record prize
  handover"; button disabled when claimedCount >= 2 or not loaded; removed
  stale recordId/status sub-line from identity header

Finding #2 — hybrid test scanner.getContestant call passed seed.userLegacyId
(numeric legacyId); getContestant now resolves Users by ID (UUID). Fixed to
pass seed.userId (UUID). No assertions weakened. Needs npm run test:hybrid to
confirm on real HANA.
…ckpoint-qr

# Conflicts:
#	srv/developer-service.js
…oint-qr

feat(app-space): checkpoint prize barcode + capped scanner handover
AppSpace track view passed averageTimeToComplete (stored in minutes,
from frontmatter `time`) into formatTime(seconds), which divided by 60.
A 15-min tutorial rendered as "0 min" and 30-89 min as "1 min", while
the tutorial object page and mission/group cards render the same value
as minutes directly.

Treat formatTime's input as minutes (drop the /60); hr/min rollover now
works on the real minute count. Adds unit coverage asserting 15 -> "15 min"
and 45+45 -> "1 hr 30 min" (both fail against the old /60 behaviour).
…e-units

fix(app-space): render tutorial time as minutes, not seconds
GET /api/my-prize-barcode returned 401 for every authenticated visitor,
so the App Space checkpoint "Claim your prize" barcode rendered as a
broken image. Two defects, both fixed:

1. The route was a bootstrap `app.get('/api/my-prize-barcode', handler)`
   registered on Express BEFORE CAP's protocol middleware chain, so the
   XSUAA auth strategy never ran — cds.context.user/req.user were null
   for every request. Mount cds.middlewares.context()/auth() in front of
   the route (same lazy pattern as /api/alerts/me, /admin/analytics/export).

2. The handler resolved identity with the read-only resolveDbUserScoped,
   which returns null for a logged-in user without a provisioned DB row
   yet → 401. Switch to the canonical tiered getOrProvisionDbUser(req,
   { sapId }, ['ID']) — the same front door the Devtoberfest/gameboard
   write path uses (#2730/#2651), so social / IAS / GitHub / null-sapId
   logins map to their one canonical Users row. The scanner
   (ScannerService.getContestant) trusts the uid in the scanned QR
   verbatim, so the barcode must encode exactly that canonical Users.ID.

Verified live (cds watch, mocked auth): anon → 401, authenticated →
200 image/png (200x200 QR). New integration test boots the real server
to guard both defects (a handler-only unit test cannot catch defect #1).
…e-401

fix(#2861): App Space prize-barcode 401 for logged-in users
…l 401 fix)

The App Space checkpoint barcode still 401'd for logged-in visitors after the
previous #2861 fix. Root cause: on a raw Express bootstrap route CAP's auth()
middleware populates the authenticated identity on cds.context.user (the
AsyncLocalStorage continuation), NOT on req.user — req.user is only set in CAP
service handlers and in the mocked-basic-auth test harness. The handler read
bare req.user, so every real XSUAA/IAS login resolved to anonymous → provision
returned null → a hard 401. The integration test stayed green because that
harness DOES set req.user, hiding the production-only defect.

Fix (matches the existing idiom used by the working /api/alerts/me and
/auth/user routes, and the devtoberfest Express routes):
- prize-barcode-handler.js: resolve `req.user || cds.context.user`, guard the
  anonymous sentinel, and provision via `provisionDbUser(user, ['ID'])` — the
  canonical get-or-create for raw Express routes (tiered iss/sub resolver, mints
  on first visit; takes the user object directly, no CAP-request .tx coupling,
  unlike getOrProvisionDbUser).
- test/unit/prize-barcode-handler.test.js: rewritten to the new contract and
  adds the operative regression guard — identity present on cds.context.user,
  absent on req.user, must 200 (not 401).
- Corrected the misleading "bootstrap bypasses the middleware chain" comments in
  srv/server.js and the integration test; the eager-bootstrap middleware mount
  is correct (cds.middlewares is a lazy getter, populated on first access).

Verified: unit 8/8, integration (real-server boot, in-memory sqlite) 2/2,
package-boundaries OK.
…arcode

fix(#2861): prize-barcode 401 — resolve identity from cds.context.user
- Add temporarilyUnavailable/unavailableReason fields to BrowseCardItem
  (scripts/fetch-tutorials.ts) and CardItem (hugo-apps/src/shared/types.ts)
- Populate both fields in buildAllCards mission literal from mMeta lookup
- Add badge element to MissionCard.vue (v-if="item.temporarilyUnavailable")
  and card-mission.html Hugo partial ({{- if $item.temporarilyUnavailable -}})
  immediately after the nav-card__type--mission div
- Add .nav-card__unavailable-badge CSS rule to card.css reusing badge visual
  conventions (orange background, white text, rounded, small caps)
- Add temporarilyUnavailable:true to mission fixture in cards.fixtures.json
  so parity test exercises the badge branch
- Regenerate card-mission.expected.html, card-tutorial.expected.html,
  card-group.expected.html, and browse-page-1.html via regen-card-parity-fixtures.sh
- Fix regen script: pass (dict "item" ...) to card partials which expect
  .item context, not a raw item (pre-existing script/partial interface mismatch
  that caused the partial to silently produce empty fixtures)
- Add two new vitest cases: positive (flag set) and negative (flag absent)
  in fetch-tutorials-browse-channel.test.ts

Both vitest suites pass (20 tests total).
…pspace-window

# Conflicts:
#	srv/developer-service.js
…ace-window

feat(#2866): gate App Space completions to the event time window
…able

feat(#2864): Temporarily Unavailable toggle for mission paths
@jung-thomas
jung-thomas merged commit 6b39774 into main Oct 21, 2026
8 checks passed
@jung-thomas
jung-thomas deleted the release/v1.40.3 branch October 21, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant