Release 1.26.0 — DEV → main (PROD promotion) - #2305
Merged
Merged
Conversation
Follow-on to #1904. A stale session no longer always surfaces as the 200-HTML XSUAA login interstitial — AppRouter/CAP can now reject the mutating request (or the /auth/user handshake) with a genuine 401 Unauthorized. csrfFetch only treated a followed redirect or a 2xx-HTML body as session-expiry, so the 401 was returned to the caller; the tutorial Done button's apiPost saw res.ok === false, returned false, and reverted with no re-login (POST /api/completeStep 401 in the console). Treat a 401 as the stale-session tell in isSessionExpiredResponse so csrfFetch forces the same top-level /login?returnTo navigation and the OAuth redirect actually executes. 403 is deliberately excluded (CSRF retry signal / Akamai edge block) and the safe-method GET path is untouched, so read 401s (anonymous /auth/user, getProgress) still pass through.
…ession-401 fix(csrf): re-authenticate on a real 401 in a stale session (#2239)
The /tutorials/<slug>.md endpoint served relative image paths, so an agent/LLM consuming the .md off-domain could not resolve them. Rewrite relative image paths to absolute raw.githubusercontent.com URLs at serve time, using repo+branch from RepoCatalog (keyed by slug, populated on publish, covers live tutorials). This avoids the schema change / re-publish / backfill a publish-time fix would need, and keeps stored sourceContent pristine. Provenance is fail-open: no RepoCatalog row leaves image paths relative (pre-#2235 behavior). Mirrors the render-time rule in scripts/parsers/images.ts: skips absolute http(s) and ../ paths, strips leading ./ or /, resolves under tutorials/<slug>/ (covers both images/ subdir and flat abap layout). Direct raw.githubusercontent.com image src URLs are not Akamai-blocked (only encoded URLs in a u= query are).
…md-image-urls feat(md): absolutize image URLs in served tutorial markdown (#2235)
…#2240) Consume the SURVEYURL column now computed in the planner's DTF_SESSION_V1 view (base URL from the new DB-backed AppConfig singleton + session code). Add SURVEYURL to the Session facade, the schedule route SELECT whitelist, and map it to surveyUrl in the assembled feed. Render a "Session Survey" link in the detail panel only (not grid cards); empty when unset. Depends on the planner producer change (developer-relations/devtoberfest-planner PR #62) being deployed first.
feat(devtoberfest): Session Survey link in schedule detail panel (#2240)
… choice (#2244) Slice the source markdown per step (Option B) alongside the existing HTML slicer, and add a `format` param ('markdown' default | 'html') that selects a single body representation to avoid doubling agent context. - srv/lib/tutorial-markdown-steps.js (new): pure ESM per-step markdown splitter (v2 H3 steps, fence- and HTML-comment-aware), ports build-time parser logic; strips image-directive comments + [VALIDATE_n]/[DONE]. - srv/lib/tutorial-step-slicer.js: sliceStepMarkdown() — version-keyed cache (slice-md: key, shares slice-slug:<slug> tag so content.published invalidates both), single-column sourceContent BLOB read, gunzip, per-step absolutizeImagePaths via RepoCatalog provenance. HTML sliceStep untouched. - srv/lib/mcp-developer-tools.js: format validated against ['markdown','html'], branches to sliceStep/sliceStepMarkdown; returns {slug, stepNumber, stepTitle, content, contentFormat, textLength, totalSteps} — collapses html->content. - srv/{developer,search}-service-mcp.cds: format param + collapsed return shape, updated doc-comments (both MCP tiers). - srv/mcp/mcp-manifest.js: get_tutorial_step description reflects format choice. - Tests: new pure parser suite (9); slicer md-agreement + image-strip + null; progress/recommend/contract tools cover default+markdown+html+400. cp-list audit: tutorial-markdown-steps.js sits downstream of tutorial-step-slicer.js (already absent from srv-qa cp list, boot-safe) and is not reachable from content-store.js — no mta.yaml change required.
The check-slug-lookups static guard flagged the new getRepoProvenance
.where({ slug }) lookup. The MCP handler lowercases the slug (lcSlug)
before it reaches the slicer, so annotate with slug-canonical:
caller-canonicalizes.
Phase A CAP ecosystem to latest-compatible-with-cds-10.1 (required), Phase B broader tree within-major only (majors deferred). Full suite + build gate each phase. Records maintainer decisions.
…ial URL Agents/LLMs that send `Accept: text/markdown` to /tutorials/<slug> now get the normalized Markdown source (via the existing markdownServeHandler) instead of HTML, without rewriting the URL to the .md variant. - New pure helper prefersMarkdown() honours q-values; browsers (no text/markdown in Accept) are unaffected. - Vary: Accept set on BOTH the HTML and markdown branches so the edge cache keys on the header and cannot poison one representation with the other. - Non-tutorial slugs (concept-/topic-/puzzle-/channel-/group-/mission-/__) never negotiate, matching the existing Link-alternate guard.
…2246) Expose an anonymous HTTP function + MCP tool on SearchService that embeds caller TEXT server-side and returns scored content references only — never raw vectors, never accepts a caller-supplied vector. - semanticSearchEnabled ChatSettings flag (default OFF; 503 when off), registered in feature-flags/registry.js - corpora: tutorials | concepts | external | all (cosine over stored Vector(1536); HANA COSINE_SIMILARITY, SQLite JS-cosine) - topK clamped [1,50]; minScore default from ChatSettings; query-embedding cached by hash(query+model) - per-IP rate limiter (429) reusing DB-backed SearchSettings budget - MCP manifest tool entry + @requires:'any' CDS function - unit + handler tests (SQLite); caches on globalThis to survive Windows Vitest module duplication across served vs imported instances
…d-step feat(mcp): expose per-step markdown via get_tutorial_step with a format choice (#2244)
…negotiation feat(content): Accept: text/markdown negotiation on primary tutorial URL
…ch-2246 # Conflicts: # test/unit/mcp-contract.test.js
…2246 feat(search): public anonymous semantic_search over content corpus (#2246)
Mirror CAP/Capire's sitemap.md with a human- and AI-navigable map: verb-lane navigation plus every mission expanded to its ordered tutorials. Distinct from sitemap.xml (flat crawler XML) and llms-full.txt (flat metadata dump) — this is a navigable hierarchy. - scripts/fetch-sitemap-catalog.ts: build-time GET /build/catalog fetch into hugo/data/sitemap_catalog.json; fail-open (empty -> /missions/ index fallback) when CAP_BASE_URL absent, like llms.txt. - hugo.toml: sitemapmd output format on home; dedicated text/x-web-markdown media type so the file is written as .md. - sitemapmd.md template: Navigation / Missions / Topics / Reference. - Cross-links from llms.txt, public AGENTS.md, ai-consumption.md (#16). - Smoke test for /sitemap.md (sparse-catalog tolerant).
feat(ai): add navigable /sitemap.md site map
…2249) @cap-js/ord 1.9.1 dropped protocols missing from its CAP→ORD protocol map, so the 5 MCP-enabled services never appeared in the generated ORD document. 1.9.2 added `mcp` to the map; 1.9.3 is the current patch. Verified via `cds compile srv --to ord`: apiResources now include 5 mcp entries (AdminService, DeveloperService, HomepageService, KnowledgeGraphService, SearchService) alongside odata-v4/graphql/rest, with no "Unknown protocol 'mcp'" warnings. Remaining websocket skips (DisplayService, EventStreamService) are expected — not an ORD protocol.
Adds sourceCommit : String(64) to ContentFilesAspect, ContentCurrentAspect, and ContentHistoryAspect in db/_content-shape.cds. Threads the per-slug sourceCommits map from appendHandler in content-store.js through appendToSession in content-publish-session.js, stamping it on the ContentFiles entry and carrying it forward to ContentCurrent/ContentHistory in dualWriteCurrentAndHistory (both HANA SQL and SQLite CQL paths). Generates migration=3 for ContentFiles (ALTER TABLE ADD NVARCHAR(64)).
Bug A (dark mode): SkillTreeMap nodes, Lesson choice cards, Results and landing/coming-soon text hardcoded light-mode backgrounds with no explicit color, so under the site's `html.dark` they inherited white text on a white card (light-on-light, unreadable). Add scoped `html.dark` overrides across SkillTreeMap.vue, Lesson.vue, Results.vue and App.vue. Verified WCAG contrast on the live DEV page: unlocked 4.95, done 4.69, locked 4.18 (dimmed/disabled). Bug B (completion/navigation): the active Lesson screen had no way back to the map, and a missed drill showed "Try Again" even though the engine re-queues the beat to the end and advance() always moves forward — so it read as "starting over on the same lesson". Add a "Back to map" control to Lesson.vue (new `back` emit wired to App's existing handleBack) and relabel the wrong-answer button to "Got it - continue" to match the actual behaviour. Engine semantics unchanged.
fix(ktt): dark-mode contrast + lesson back button and honest labels
Bug C: a lesson was marked "mastered" whenever the beat queue emptied,
regardless of score — so answering every drill wrong still showed the
success banner and banked XP, then silently returned to the map ("started
over"). Mastery is now score-gated client-side: correct / distinct-drills
must be >= 0.7 (PASS_RATIO). Below threshold shows a dedicated retry screen
and banks nothing (no XP, no streak, no mastered).
XP was awarded (+10) but never surfaced. New StatsBar on the map shows the
XP total, a cat-themed level tier (Kitten -> TLA Sage), and the streak, so
XP is both meaningful and visible.
- engine.ts: PASS_RATIO, totalDrills, scoreRatio, passed
- progress.ts: levelForXp + KttLevel tiers
- Lesson.vue: complete emits { lessonId, xp, passed, correct, total }
- Results.vue: dual pass/fail template; fail shows score + Try Again
- StatsBar.vue (new): XP / level / streak
- App.vue: fail path banks nothing + retry remount via :key
- tests: 42 passing across engine/progress/Lesson/App
fix(ktt): gate mastery on 70% pass + surface XP levels/streak
- ABAP (Advanced Business Application Programming) → Unit 2, lesson 2 (next to RAP) - BDC (SAP Business Data Cloud) → Unit 3, lesson 2 (next to DWC) - New Unit 5 'SAP & AI' (🤖, order 5): - Lesson 5.1 AI Foundations: AI, ML, LLM, RAG - Lesson 5.2 SAP's AI Stack: BAIP, NLP, ISLM, MCP - New 'ai' category; regenerated ktt_lessons.json + KttLessons seed CSV - Tests: derive lesson count from CSV; allow up to 5 units (was hardcoded 12/4)
feat(ktt): add ABAP, BDC + new Unit 5 'SAP & AI' lessons
beginSession was the one publish HTTP call in the happy path with neither
withRetry nor a try/catch, so a transient HANA pool-acquire 500 ('Pool
resource could not be acquired within 1s') bubbled to the top-level Fatal
catch and aborted the whole prod rebuild. Wrap begin (and the delta-detection
/content/hashes fetch) in withRetry with jittered backoff, mirroring the
existing commit posture; 500/502/503/504 are already classified transient.
Also add cds.requires.db.pool config (acquireTimeoutMillis 5000, max 100) for
hana [hybrid]/[production] so brief pool contention waits instead of erroring
at the 1s default.
Tests: beginSession under withRetry recovers from a transient pool-timeout
500 and does not retry a permanent 409.
fix(publish-content): retry transient HANA pool-timeout 500s (#2286)
…2287) normalizeListContinuationFences only matched fences at exactly 4 spaces. A partial list-marker outdent can leave the continuation fence at 5-7 spaces (the #2287 repro was 6), which Goldmark still treats as a doc-level indented code block -> literal backticks + a never-closed block that swallows all following steps. Generalize the opening-fence match to 4-7 spaces and strip (indent - 3) leading spaces from the fence and its content/close lines, bringing the delimiter to 3 while preserving relative indentation. 8+ spaces are left alone (genuine deeper nesting). Existing safety guards unchanged: matching close required, idempotent, blank-line preserving. Adds regression fixtures at 5/6/7-space indents plus 8-space no-op, relative-indent preservation, idempotency, and unterminated cases.
…nce-4-7 fix(list-fence): repair list-continuation fences indented 4–7 spaces (#2287)
…eenshot edits publish (#2288) Same-filename image byte changes are invisible to the content publish delta detector: publish-content.ts hashes rendered HTML + source markdown, never the image bytes. Images are referenced by an external raw.githubusercontent.com URL via /img-cdn?u=<encoded>, so editing a screenshot in place (unchanged filename → unchanged URL → unchanged HTML hash) never triggers a republish — the stale image is served indefinitely. Text-only and text+image edits work because the HTML hash moves; renaming the image works because the URL (and thus the HTML) changes. Image-only, same-filename edits are the blind spot the issue reports. Fix: on a slug-targeted rebuild, re-warm just the changed tutorial's images through the already content-aware ingest endpoint (POST /content/image dedups on image bytes). Changed bytes are re-stored; unchanged ones report 'unchanged'. Scoped to slug-targeted (not full) rebuilds — full rebuilds re-warm the whole catalog already. Best-effort (continue-on-error) so a warm failure never fails the content rebuild. Covers both prod/dev/qa (rebuild-content.yml) and the QA author-preview channel (rebuild-content-qa.yml). backfill-images.ts gains --slug (scope to one tutorial's built HTML) and --channel qa (reads CAP_QA_BASE_URL/CONTENT_API_KEY_QA + hugo/public-qa, mirroring publish-content.ts), plus a main-module guard so the pure enumeration helpers are unit-testable.
…changes fix(images): republish images on slug rebuilds when edited in place (#2288)
Replace invalid tutorials-admin@example.sap with thomas.jung@sap.com and add Developer Tutorial Authors MS Teams channel link.
The participate QR fell back to a bare external https://developers.sap.com/app-space.html when no participateUrl param was set, dropping the event ID. AppSpace.vue reads eventId from the URL query, so scanners landed in the default app space instead of the event's instance. Build the fallback as ${origin}/app-space?eventId=<id> so the barcode carries the event through. Display label updated to match. Fixes #2295
…mpletions (#2294) getEventBuckets returns { eventName, eventType, hasLogo, buckets:[...] } since #2133, but event-stream.ts still treated the response as an array and called .map on the object -> 'K.map is not a function'. Read json.buckets (with legacy array fallbacks). Also drop the empty-buckets early-return: an event with zero completions is not an error (404 events are already rejected server-side), so proceed to the Socket.IO connection and let late completions render live.
…-buckets fix(display-app): getEventBuckets object shape + zero-completions (#2294)
…s-email fix(admin): correct NoAccess contact info (#2293)
…tid-qr fix(display-app): QR carries eventId to /app-space (#2295)
…pace (#2296) Event descriptions are admin-authored (Events.description, @UI.MultiLineText) and shown on the App Space hero, but were rendered escaped as plain text, so Markdown syntax appeared literally. - Extract the existing markdown-it config (html:false, breaks, linkify) from catalog-renderer.js into a shared srv/lib/markdown.js so the safety policy lives in one place; catalog-renderer now imports it (no behaviour change). - developer-service.js getAppSpaceProgress returns eventDescription as rendered safe HTML (raw HTML in the source is escaped by html:false — no injection). - AppSpace.vue renders it via v-html in a <div> (markdown emits block <p>; <p>-in-<p> is invalid), with :deep() child-margin resets for scoped styles. - Admin label hint: "Description (Markdown)" + QuickInfo tooltip. Tests: srv renderMarkdown unit (format + HTML-escape + coercion); AppSpace v-html render test (guards regression back to escaped {{ }}).
buildConceptsPayload issued its ~11 independent link-table reads (teaches, edges, journeys, blogs, missions, videos, apiDocs, samples, helpDocs, events) strictly sequentially. On HANA that is ~11 serial round-trips over multi- thousand-row tables per cold call, pushing /build/concepts to ~50-60s and past the 30s smoke-gate timeout (4 smoke tests failed as pure timeouts on DEV 1.25.0; endpoint returned correct 200s, just slowly). The reads are all keyed off idsSet (computed once in step 1) with no inter- query dependencies, so wrap them in a single Promise.all wave. Steps 3+4 stay grouped (they jointly build requires/relatedTo/requiredBy) and now run their two edge reads in parallel too. Stitch (step 6) and groupBy unchanged; same columns, filters, sorts, and per-concept caps. No user impact was in play - /concepts/* pages are served from HANA BLOBs (fast); this endpoint feeds only the Hugo concept-render build pipeline and the smoke gate. Not a 1.25.0 regression (query helper last changed 2026-08-09); the slowness is structural and pre-existing. All 744 concepts-related unit tests pass; payload shape unchanged.
perf(concepts): parallelize /build/concepts link-table reads
New catalog-renderer import is a content-store transitive dep; srv-qa would boot then crash on first render without it.
…own-2296 feat(events): Event description Markdown → safe HTML on App Space (#2296)
Add AdminService.copyMission(ID) action that deep-copies a Mission into a new unpublished Mission: the mission row, every CompletionPath and its CompletionPathItems, and the MissionTags/MissionCategories link rows. Referenced Tutorials/Groups/Tags/Prizes are linked (via *_ID), never duplicated. Slug becomes <slug>-copy (disambiguated on collision against the @assert.unique.slug index). published/legacyId/community identity are reset. All writes run in the request tx. Wire a 'Copy Mission' List Report action (requiresSelection, single) in app/admin/missions that POSTs the action with CSRF and deep-links the admin-shell into the new mission's Object Page. Tests: 2 new unit tests (deep-copy graph + slug disambiguation), 30 green.
check-slug-lookups flagged the uniqueSlug SELECT: the CompletionPaths path probed by raw p.slug. Lowercase the base once in the helper (slugs are lowercase-canonical repo-wide) so both the uniqueness probe and the inserted slug are canonical; mark the probe pre-canonicalized.
feat(admin): Copy Mission — deep-copy action + LR button (#2300)
chore(release): 1.26.0 — What's New digest + version bump
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.
Promotes DEV to
mainfor the 1.26.0 production cutover.Includes the release-prep commit (#2304): What's New digest refreshed (+40 entries) and MTA version bumped 1.25.0 → 1.26.0, plus all other DEV changes merged since the 1.25.0 release.
Deploy from a fresh
origin/mainafter merge (never from a feature branch).