Repository navigation
release: v1.40.0 (DEV→main promotion) - #2825
Merged
Merged
Conversation
…pendent
The new URL Health admin tile (app/admin/health) failed to render with a
runtime ModuleError: "failed to load sap/ui/model/odata/v4/library.js ...
script load error". Three defects compounded:
1. manifest + (intended) ui5.yaml listed `sap.ui.model.odata.v4` under
dependency libs. That is NOT a SAPUI5 framework library — the OData V4
model ships inside `sap.ui.core`. `ui5 build` can't resolve it
("Could not find library"), so copy-components.js silently fell back from
the preload build to a raw copy that ships NO Component-preload.js.
UI5 then lazy-loaded each declared lib from the CDN at runtime, and the
bogus odata/v4 path 404'd → the ModuleError.
2. app/admin/health had no ui5.yaml AND no package.json. `ui5 build` needs
both; without them it walked up to app/admin (tutorials-admin-fiori,
ui5:null) and failed project resolution — another path to the same raw-copy
fallback.
Fix: add app/admin/health/{ui5.yaml,package.json} matching the sibling apps,
and drop the non-existent sap.ui.model.odata.v4 lib from both the manifest and
ui5.yaml (sap.m + sap.ui.core only). Result: 52/52 admin components now
preload-build (was 51/52, health raw-copied); health ships a real
Component-preload.js and no longer hits the CDN.
Guard so this class can't ship silently again: discoverComponents() now throws
at build time when a non-excluded admin component lacks its ui5.yaml, with a
message pointing at the raw-copy/CDN failure mode and the PRELOAD_EXCLUDE
opt-out. preload-components.cjs threads PRELOAD_EXCLUDE through so excluded
tiles stay exempt. Unit tests cover the real tree, a throwing fixture, and the
exempt path.
…reload fix(admin-health): make URL Health tile preload-buildable, not CDN-dependent
… formatter
The URL Health tile rendered no further than instantiation: the Last Checked
cell used an expression binding `{= ${lastChecked} ? new Date(${lastChecked})
.toLocaleString() : '—' }`. UI5 expression-binding grammar forbids `new`, so
the XMLTemplateProcessor threw at render time:
SyntaxError: Expected : but instead saw Date
→ routing target 'healthTarget' could not be displayed
(This was the second, independent defect behind #2796: the CDN/preload fix let
the view load far enough to finally hit this binding error.)
Fix: move the date formatting into a controller formatter `formatTimestamp`
(mirrors the existing `formatBadSlots` pattern) and bind
`{ path: 'lastChecked', formatter: '.formatTimestamp' }`. Null/invalid dates
render the em-dash, same as before.
Verified by instantiating the real view in headless Chromium against UI5
1.148.12 (playwright-core, the repo's test/e2e/_browser.js pattern): the
XMLView now creates with no binding error and the Last Checked cell renders a
formatted date ("10/15/2026, 2:00:00 PM") — the formatter runs. The kept `{= }`
expression bindings (!!, ===-ternary) parse fine.
…binding fix(admin-health): replace illegal new-Date expression binding with a formatter
The 'Rebuild this tutorial' object-page button gave no feedback and
looked like it didn't fire. Root cause: it was rendered from a
@UI.DataFieldForAction annotation, so Fiori Elements V4 invoked the
bound rebuildContent action through its generic path and never called
the SourceMarkdownHandler.onRebuildTutorial controller extension — the
code that owns the confirm dialog, success/error MessageBox, and
context refresh. The POST went out and returned 200 silently.
Fix (AskJoule pattern — the one custom header action that works here):
- manifest.json: add content.header.actions.RebuildTutorialAction with
press -> SourceMarkdownHandler.onRebuildTutorial.
- admin-annotations.cds: drop the @UI.DataFieldForAction for
rebuildContent (keeps checkFreshness) so the button isn't rendered
twice. The bound-action FQN form was correct; do NOT revert to the
slash form (that is the featureFlags 'Unknown action import' break).
- onRebuildTutorial: take the press event, disable the triggering
button across the dispatch round-trip (re-enable on resolve/reject)
so the same slug can't be re-submitted immediately, and surface the
server's existing flag ('already in progress') instead of
implying a fresh dispatch.
- i18n: add RebuildTutorialToastDebounced.
Server rebuildContent action + rebuild-trigger.js unchanged; its unit
test (admin-rebuild-tutorial.test.js) stays green (5 passed).
Admin-UI change -> needs a FULL deploy (bundle-gated Step 3.5) to verify
in-browser on DEV.
…feedback fix(admin-ui): wire Rebuild-tutorial button to its handler + feedback
The static guard check-ui5-controller-extensions failed on #2799: the manifest press ref RebuildTutorialAction pointed at ...ext.SourceMarkdownHandler.onRebuildTutorial, but that handler lived only in SourceMarkdownHandler.controller.js. FE V4 resolves a manifest press reference as a PLAIN module (loader path <name>.js), not a controller extension (<name>.controller.js) — so the click would 404 at runtime and the guard blocks the build. Fix (Concepts #537 / AskJoule #405 canonical pattern): - Add a plain SourceMarkdownHandler.js exporting onRebuildTutorial. The sibling .controller.js keeps the Source-Markdown SECTION logic; both share the base name and FE resolves each by suffix (same shape as the Concepts ConceptActionsController .js/.controller.js pair, and the manifest already registers SourceMarkdownHandler as controllerName). - Correct the handler arg shape: a manifest press handler is invoked with the binding CONTEXT (or [context] / legacy event), NOT a UI event — resolveCtx() handles all shapes (my earlier getSource()-based button-disable could never have worked here). - Debounce without a button: a manifest press handler isn't handed the triggering control, so replace button-disable with a module-level in-flight guard + the server's existing result flag. - Remove the now-duplicated onRebuildTutorial from the .controller.js. check-ui5-controller-extensions now OK (54 manifests); full static-guards exit 0; guard unit test + backend rebuild action test green (21 pass).
…plainjs fix(admin-ui): resolve Rebuild-button press to a plain .js module (fixes guards on #2799)
A tutorial-updated repository_dispatch that changed N>1 tutorials sent no usable client_payload.slug, so rebuild-content.yml's mode step fell through to the default 'full' mode — a ~10-min full-corpus rebuild (whole-catalog fetch + publish + render-concepts over ~10.8k concepts) for what was usually a handful of changed slugs. Single-slug pushes were fine (~2 min); the variance was bimodal (slug-targeted vs demoted-full), not latency. Changes: - publish-content.ts: add filterToSlugs() + --slugs/PUBLISH_SLUGS so a publish can scope to N slugs (O(N), not O(catalog)); the delta/ carry-forward pipeline already handles any-size sets. Scope gate generalized from to (slug OR slugs) so the page/author/concept full-catalog merges stay off scoped publishes. allow-revert stays single-slug only. - rebuild-content.yml: mode step validates client_payload.slugs (strict charset, each entry) → emits dispatch_slugs → slug-targeted; threads into fetch (TUTORIAL_SLUGS) + publish (PUBLISH_SLUGS). Emits a loud ::warning:: when a tutorial-updated dispatch has no usable slug/slugs (previously a silent demotion to full). - rebuild-content-qa.yml: same client_payload.slugs wiring (fetch + publish), payload-only. - notify-qa.yml.template: emit a comma-separated slugs payload for 2..CAP(20) changed tutorials (single slug for N=1; empty → full for 0 / >CAP / invalid charset). Tests: filterToSlugs unit tests (TDD, 10 pass incl. dedup/blank-skip/ loud-fail-on-missing); content-delta suites green. PROPAGATION NOTE: notify-qa.yml.template is a TEMPLATE — the live per-repo push workflows across the sap-tutorials/* source repos must be regenerated from it (and the prod tutorial-updated dispatcher template) for the upstream slugs payload to actually flow. That rollout is a separate step (provision-tutorial-repo / a sync). The receiver-side changes here work the moment any dispatcher sends client_payload.slugs. CI-only change (workflows + build script) — no app deploy required.
The publish-content-qa regression guard pinned the literal `if (!opts.slug && channel === 'prod')`, but this PR generalized the scope guard to `!isScoped` (--slug OR --slugs). Widen the regex to match either form while keeping the load-bearing `channel === 'prod'` invariant strict — the production gate (publish-content.ts:1580) still enforces prod-only render-concepts, so this is a test-guard update, not a behavior change. Also rebased onto current origin/DEV to pick up #2804 (the UI5 SourceMarkdownHandler.js fix), which clears the inherited guards failure.
…patch fix(rebuild): keep multi-tutorial pushes slug-targeted instead of demoting to full
Bump deploy version 1.38.0 -> 1.39.0 (.deploy/mta.yaml) for the DEV->main promotion and refresh the user-facing What's New digest (hugo/data/whats_new.json) with 24 summarized PRs merged since 1.38.0. Headline: URL Health subsystem for tutorials/missions/groups (#2787) — nightly + on-demand broken/slow link detection and membership-integrity reporting in a new Admin UI screen. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Bump deploy version 1.39.0 -> 1.39.1 (.deploy/mta.yaml) for the DEV->main promotion and refresh the What's New digest (hugo/data/whats_new.json). Patch release: carries #2796 — make the URL Health admin tile preload-buildable (bundled into the admin UI, not CDN-dependent), a follow-up to the URL Health subsystem shipped in 1.39.0 (not yet deployed to PROD; this is the version that will deploy). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Read-only profiling spike. Per-phase wall-clock from PROD runs shows two dominant costs on unscoped publishes (not the single conflated phase the issue assumed): - render-concepts ~95-106s: per-concept delta skip defeated by shell churn (full-doc hash embeds the __shell__, so any CSS/island/nav change flips all ~10,789 concept hashes). 10,620/10,789 'changed' on a 65-slug delta. - append/commit ~130-165s: per-row N+1 SELECT-then-UPDATE-or-INSERT upserts (metadata/steps/meta/authorship/bodytext/branchspecs). Corrects the issue's guessed defaults (actual 6/50, not 4/25); confirms the #1932 carry-forward OOM fix still holds; flags a 361s image-refresh outlier. Refs #2802
Two-level concept-phase delta (phase-gate via conceptPhaseToken + shell-only re-compose via new bodyHash column), N+1 upsert batching in appendToSession, and pre-session GET micro-wins. Corrects the spike's premise: the dominant concept cost is buildConceptsPayload (~50-60s, 12 table scans) run every call, not just the render loop — so the whole phase must be gated, not just the loop. Refs #2802
11 TDD tasks: F1 concept-phase delta (schema, bodyHash, conceptInputDigest, phase skip + shell guard, commit persistence), F2 batch the four N+1 upserts (chunk 500, row-equivalence tests), F3 fold duplicate hashes GET + file image-refresh issue. Verified against real code: carry-forward covers concept-* keys, stale-chrome guard required, CommunityEventConceptLinks lacks modifiedAt (uses extractedAt). Refs #2802
…#2802) Replace per-slug N+1 SELECT+write loops in upsertBodyTexts and upsertBranchSpecs with a single batched SELECT IN per 500-slug chunk, then one multi-row INSERT for new slugs and one UPDATE per existing slug. The 500-item chunk ceiling matches the HANA packet-safe limit established at content-publish-session.js:265. At scale (full rebuild ~7k slugs) this collapses ~14k DB round-trips to ~28 per entity.
`fetchRemoteHashes` now reads `X-Pipeline-Token` from the same response
and returns `{ hashes, pipelineToken }`, eliminating the separate
`fetchRemotePipelineToken` GET in the delta path of publish-content.ts.
`fetchRemotePipelineToken` had no other callers and is removed from the
import. The auto-verify call site destructures `.hashes` as before.
…up (#2802) - scripts/publish-content.ts: capture rc.conceptPhaseToken from renderConceptsPhase result (localConceptPhaseToken); forward to commitSession alongside pipelineToken. - scripts/lib/publish-client.ts: add conceptPhaseToken to CommitInput interface, commitSession POST body, and RenderConceptsResult; delete dead fetchRemotePipelineToken (0 callers). - packages/content/content-store.js: commitHandler now destructures conceptPhaseToken from req.body and forwards it to sessionHelpers.commitSession. - test/unit/commit-conceptphasetoken-wire.test.js: new wire test — Part A asserts publish-client includes conceptPhaseToken in POST body (fetch mock); Part B asserts commitHandler persists conceptPhaseToken on ContentManifest (CDS in-memory). - test/unit/upsert-tutorial-metadata-batch.test.js: fix Task-8 assertion from 1 to 2 rawSlugInSelects — linkTutorialAuthorship (Task 9) legitimately adds a second batched LOWER(slug) IN lookup; both are intentional N+1 wins. - test/unit/concept-input-digest.test.js: remove unused vi import.
…ead (#2806) - cds build --production: stage db/last-dev/csn.json + ContentFiles/ ContentManifest hdbmigrationtable (additive ALTER ADD for bodyHash + conceptPhaseToken) — the check-cds-build-staging guard requires these committed alongside the schema change. - publish-client.ts fetchRemoteHashes: optional-chain res.headers?.get so a Response without a headers object (pre-existing test mock, defensive in prod) returns pipelineToken null instead of throwing TypeError. - publish-client.test.ts: update the two fetchRemoteHashes assertions to the new { hashes, pipelineToken } return shape (#2802 folded the token read in). Refs #2802 #2806
perf(publish): concept-phase delta + batch N+1 upserts (#2802)
…ine) Implements Task 2 of #2811 (tutorial variables). - scripts/parsers/variables.ts: new module with TutorialVariable interface, VariablesValidationError class, validateVariables(), wrapTokens(), SENTINEL_RE, and sentinelFor() helper. validateVariables: hard-fails on duplicate name, undeclared body token, bad regex pattern, off-allowlist default.from, invalid type. Allows declared-but-unused variables (soft). wrapTokens: line-by-line via createFenceTracker. Outside fenced code (prose + inline code) → <span class="tut-var" data-tut-var="NAME">PLACEHOLDER</span>. Inside fenced code → TUTVARxNAMEx sentinel (Chroma-safe, lexer-opaque). - scripts/parsers/types.ts: add variables? field to TutorialFrontmatter. - scripts/parsers/compose.ts: add TutorialVariable[] to ComposeResult; call validateVariables after frontmatter extraction (hard-fail propagates through publish pipeline); apply wrapTokens to processedBody after all other body transforms and before step splitting. - scripts/parsers/__tests__/variables.test.ts: 26 tests covering all validateVariables error paths, wrapTokens prose/inline/fenced cases, SENTINEL_RE/sentinelFor round-trip, and composeTutorial integration. No-variables tutorials are completely untouched (zero-cost fast path).
In render-codeblock.html, capture the highlighted (or plain-text) output into a variable before emitting, then apply replaceRE to turn sentinel tokens `TUTVARx([A-Za-z0-9_]+)x` (emitted by the TS parser for fenced-block occurrences) into `<span class="tut-var" data-tut-var="NAME"></span>` before emitting via safeHTML. Covers both branches: Chroma-highlighted (lang != text) and plain-text (lang == text). The span shape matches what the downstream island queries via [data-tut-var]. Three security-reviewed: comments added to satisfy check-hugo-safe-html.cjs (131 files scanned, OK). Spike-verified: sentinel is one Chroma token in bash/js/text; replaceRE is a no-op when no sentinel present so existing tutorials are unchanged.
Pattern `TUTVARx([A-Za-z0-9_]+)x` was greedy: on a line with two sentinels back-to-back (e.g. `TUTVARxfooxTUTVARxbarx`) the `+` consumed `fooxTUTVARxbar` through to the final `x`, producing one garbled span instead of two. Change `+` → `+?` (lazy) on both the text and highlight branches. RE2 (Hugo's regex engine) supports lazy quantifiers. Verified with a two-sentinel fixture: `cf deploy TUTVARxgroupNumberxTUTVARxappNamex.mtar` renders as two separate spans with the literal `-` and `.mtar` intact.
[#2811] Task 4 — threads `variables?: TutorialVariable[]` from the parser's ComposeResult through the Hugo page pipeline: - `render-frontmatter.ts`: add `variables?: TutorialVariable[]` to `RenderHugoFrontmatterArgs`; import type from `variables.ts`; emit `fm.variables = args.variables` when non-empty (same guard style as `fm.osList`/`fm.hasAi`). - `fetch-tutorials.ts` (`writeHugoPage`): add optional `variables` param; pass it into `renderHugoFrontmatter`; pass `composed.variables` at the call site. - `u1-object-page.html`: - Config script: `<script id="tutorial-variables" type="application/json">` sibling to `tutorial-data`, guarded by `{{ with .Params.variables }}`. - Mount div: `<div id="tutorial-variables-panel" data-slug="…" data-mission-slug="…">` at the top of `<section id="op-steps">`, before the Steps heading and wizard — below description/prerequisites, above Step 1. Also guarded by `{{ with .Params.variables }}`. - Module script: `<script type="module" src="…tutorial-variables…">` guarded by `{{ if and .Params.variables … }}` so pages without variables ship zero extra fetches. Island `tutorial-variables` does not exist yet (Task 5); `island-src.html` falls back to `/js/tutorial-variables.js` which 404s until Task 5 ships the bundle — intentional. Note: Hugo's `.Params` YAML round-trip encodes the variables array as a JSON string (same as the existing `steps`/`tutorial-data` pattern). Task 5's island must double-parse `JSON.parse(JSON.parse(el.textContent))` mirroring the `initDoneButtonGate` pattern in tutorial.ts:409-410.
Adds the tutorial-variables Vue island (Task 5): - hugo-apps/src/tutorial-variables/substitution.ts — pure exported helpers applyValue() and resolveDefault() + TutorialVariable types; importable by vitest without DOM side-effects. - hugo-apps/src/tutorial-variables/TutorialVariables.vue — panel with one labelled <input> per variable, identity default-filler (/auth/user, credentials:include, fail-open), live applyValue() on every keystroke. Two TASK-6 SEAM comments mark where Task 6 inserts readValue/writeValue. - hugo-apps/src/tutorial-variables/main.ts — island entry: double-parse guard (matches validation/main.ts pattern), mounts into #tutorial-variables-panel. - hugo-apps/src/tutorial-variables/__tests__/substitution.test.ts — jsdom unit tests (TDD: written first, confirmed failing before implementation); 9 tests pass. - hugo-apps/vite.config.ts — adds 'tutorial-variables' rollup entry. Build: tutorial-variables-*.js emitted + island_manifest.json entry confirmed. Tests: 9/9 pass (vitest --project unit, happy-dom).
Adds localStorage-backed persistence (Task 6) to the tutorial-variables island so variable values survive browser restarts and carry across tutorials within the same mission/group. - New storage.ts: readValue / writeValue with key scheme tutvar:<scope>:<name>; mission key beats tutorial key; all localStorage access guarded in try/catch for private-mode safety. - New __tests__/storage.test.ts: 9 happy-dom tests (mission carry-over, per-tutorial isolation, precedence, empty missionSlug edge cases). - TutorialVariables.vue: wired at both TASK-6 SEAMs — onMounted reads stored values before identity defaults; onInput writes on every keystroke.
- Add TutorialVariables entity (cuid, LegacyKeyed) with HANA-safe column names (varName/varLabel/varType/maxLength/isRequired/placeholder/defaultFrom/ defaultPattern/configJson/sortOrder) — avoids reserved words name/type/ required/key/value/default/order - Add Tutorials.variables Composition back-ref in db/schema.cds - Add @cds.persistence.journal in db/persistence.cds for .hdbmigrationtable gen - Create srv/lib/variables-publish.js: replaceVariablesForSlug (DELETE+INSERT in cds.tx, mirrors contributors-publish.js) + upsertTutorialVariables session helper - Wire upsertTutorialVariables into appendToSession (packages/content/ content-publish-session.js) after linkTutorialAuthorship — best-effort, errors swallowed so variable write never fails a content publish - Add @readonly TutorialVariables projection to AdminService (srv/admin-service.cds) - Hybrid test: test/hybrid/2811-tutorial-variables.test.js (isSafeForWrites guard) - SQLite unit test: test/unit/variables-publish.test.js (6 tests, all pass)
- app/admin-annotations.cds: annotate AdminService.TutorialVariables with column labels and @UI.LineItem (varName, varLabel, varType, isRequired, sorted by sortOrder); add VariablesFacet ReferenceFacet to the Tutorials ObjectPage Facets list (after ContributorsFacet), mirroring the same pattern. CDS compile verified (exit 0, VariablesFacet in EDMX). - test/e2e/tutorial-variables.spec.ts: Playwright e2e spec asserting panel presence, live substitution in prose/inline-code/fenced-code contexts, localStorage persistence across reload, and mission carry-over (it.skip with TODO). Self-skips without SMOKE_BASE_URL (exit 0, 4 skipped). Lists 3 active tests with dummy SMOKE_BASE_URL (exit 0).
…n; enforce number maxLength (#2811) C1: wrapTokens now detects CommonMark inline-code spans (backtick runs) within non-fenced lines and emits the TUTVARxNAMEx sentinel inside them instead of a raw <span>. Goldmark HTML-escapes <code> content, so a raw <span> injected inside backticks rendered as escaped literal text and substitution silently failed. Client-side expandSentinels() in substitution.ts walks text nodes at island mount, finds TUTVARxNAMEx literals, and replaces each with a real [data-tut-var] span (built via createElement/setAttribute — zero XSS surface). Called from main.ts before Vue mount so applyValue() finds all spans on first render. I1: expandSentinels is covered by 8 new happy-dom unit tests in hugo-apps/src/tutorial-variables/__tests__/substitution.test.ts, including the exact da164-TUTVARxgroupNumberx@x -> da164-007@x flow, multiple sentinels, and double-processing guard. Parser test updated: inline-code case now asserts sentinel output (not span). All 52 variables-feature tests pass. I2: onInput in TutorialVariables.vue clamps numeric field values to maxLength chars and reflects back into the DOM input, since browsers ignore :maxlength on type=number.
…TutorialVariables (#2811)
…es-design feat: tutorial variables (#2811)
Replace CF-egress-blocked InnerTube transcript fetch with official YouTube Data API v3 captions.download (OAuth, SAP Developers channel). Three-tier source resolution (uploaded/ASR captions -> description), two-tier learner notice, warn-loud-and-continue on empty quiz.
…p + source tag - Replace fetchTranscriptViaHttp HTTP round-trip with in-process call to fetchTranscript from srv/lib/youtube-official-transcript.js (Task 4). The old endpoint was blocked in CF during builds; the new impl uses the YouTube OAuth + Data API directly without needing a deployed CAP backend. - Widen TranscriptFetcher type to include 'description' so it matches Task 4's three-tier return shape (uploaded|auto|description|none). - Change drop logic: only source='none' (or missing/empty segments) drops the video quiz. 'description', 'uploaded', and 'auto' all flow through to generation. The drop now emits a loud greppable [ai-author][EMPTY-QUIZ] marker including the videoId for build-log searchability. - Thread a per-step questionSource discriminator: 'captions' when transcript source is 'uploaded' or 'auto', 'description' when 'description'. Stamped onto emitted questions as the carrier; lifted to step.questionSource by fetch-tutorials.ts at the target.validation assignment site; stripped from individual questions in the anti-leak pass so it does not duplicate in every question object in public Hugo frontmatter. - Add questionSource? field to TutorialStep type for Task 6 (Vue island). - Add 5 new TDD tests covering: description flows through + stamps questionSource:description; uploaded/auto stamp captions; none emits [ai-author][EMPTY-QUIZ] + drops; short description drops via existing MIN_SUBSTANTIVE_WORDS guard (no duplication).
Renders a <p class="validation-source-notice"> below the quiz form when questionSource is 'captions' or 'description', with verbatim copy per the task brief. Absent for hand-authored / non-video quizzes (undefined). - StepData += questionSource? in main.ts; passed as new prop - Props += questionSource? in Validation.vue (withDefaults: undefined) - Template: <p v-if="props.questionSource"> after </form>, inside <template v-else> - Style: .validation-source-notice (small, muted neutral color) - Test: Validation.source-notice.test.ts (3 cases; TDD — red before green)
…OAuth secrets - .github/workflows/rebuild-content.yml: tee fetch-tutorials stdout+stderr to /tmp/fetch-tutorials.log; add non-failing step "Surface dropped video quizzes (EMPTY-QUIZ)" (if: always()) that greps -F for the exact marker '[ai-author][EMPTY-QUIZ]' and appends any matches under a warning heading to $GITHUB_STEP_SUMMARY. Silent when clean; step never fails the job. - docs/developers/operations/secrets-tracking.md: document three new BTP Credential Store aliases (YOUTUBE_OAUTH_CLIENT_ID, YOUTUBE_OAUTH_CLIENT_SECRET, YOUTUBE_OAUTH_REFRESH_TOKEN) — one-time OAuth consent as SAP Developers channel owner, rotated via /admin-ui/#secrets, consumed by the official YouTube captions.download transcript tier in scripts/lib/youtube-transcript.ts. References the design doc and explains the fail-open three-tier fallback.
Line 207 cited the wrong path `scripts/lib/youtube-transcript.ts`; the actual module is `srv/lib/youtube-official-transcript.js`, consumed in-process by `scripts/fetch-tutorials.ts` at build time.
… paths Three drop paths in the video branch logged without the greppable marker, making them invisible to the workflow step-summary grep. Add the marker to: - invalid YouTube URL drop - no transcript fetcher available drop - transcript fetch threw / catch block (off-channel reject + OAuth failures) The source:'none' path already had the marker and is unchanged. Also adds a test asserting that a fetchTranscript rejection emits [ai-author][EMPTY-QUIZ] and includes the videoId — this was the only drop path with no test coverage.
fix(#2800): official-API video transcripts for AUTOAUTHOR_VIDEO quizzes
…r OPTION blocks (#2820) Two rendering bugs reported on the hana-clients tutorials (QA channel, Tutorials-Contribution), both around the [OPTION] feature: 1. Images rendered centred instead of left-aligned, especially after an [OPTION] block. options.ts emits OPTION blocks flush-left (closing the surrounding <ol>, #2703), so a figure authored inside an OPTION body or trailing the group lands outside any <li> and reverts to the globally centred .tutorial-figure. Re-apply the list-item left-align treatment to figures inside .os-panel/.option-tabs and to figures that trail an OPTION group within a step. CSS-only; regenerated sap-fundamental.css. 2. Selecting Windows on a step that only has Linux/macOS panels silently showed macOS (the old cross-OS FALLBACK_CHAIN). Replace the chain with exact-match-only rendering: show the OS's panel if present, otherwise a 'No <OS> instructions for this step.' placeholder — never another OS's content. init() still snaps a fresh visitor's INITIAL selection to an offered OS so no step starts blank, but an explicit pick is honoured verbatim. Updated test/unit/os-toggle.test.ts to the new spec. Bug 2 in the report (wrong-OS content) is the same root cause as (2) plus inconsistent per-step OPTION coverage in the author's Oct-14 markdown; see the issue reply for per-step guidance.
…g-2820 fix(options): honest missing-OS placeholder + left-align figures after OPTION blocks (#2820)
The `*,, *` pattern on line 286 had a stray space, so bash parsed the arm as two whitespace-separated patterns (`*,,` and `*`) and failed with `syntax error near unexpected token '*'`. This broke the "Determine effective rebuild mode" step on EVERY dispatch (exit 2 at parse time, before any logic ran) — all content rebuilds were blocked. Fixed to `*,,*` (empty-element = two adjacent commas anywhere), matching the correct form already used on line 323. Verified with `bash -n` plus the 5 validation cases (valid list accepted; empty-element / leading / trailing comma / bad charset all rejected).
fix(rebuild-content): bash case syntax error blocking all content rebuilds
Bump deploy version 1.39.1 -> 1.40.0 (.deploy/mta.yaml) for the DEV->main promotion and refresh the What's New digest (hugo/data/whats_new.json) with the 16 PRs merged since 1.39.1 (#2798-#2824). Minor release: carries two user-facing features — tutorial variables (#2818) and sort/filter on the admin URL Health table (#2817) — plus the content-rebuild unblock (#2824), faster content publishing (#2806), scheduler failover hardening (#2815), multi-tutorial slug-targeted publish fixes (#2801/#2808/#2809), admin Rebuild-button fixes (#2799/#2804), OS-tab rendering fixes (#2822), more accurate video quizzes (#2821), and test/CI hardening. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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.
Release v1.40.0 — DEV → main promotion
Promotes DEV to
mainfor the next PROD deploy. Bumps the deploy version1.39.1 → 1.40.0(.deploy/mta.yaml) and refreshes the public What's Newchange log (
hugo/data/whats_new.json) with the 16 PRs merged since 1.39.1.Minor bump — two user-facing features landed alongside fixes.
Features
Fixes & improvements
casesyntax fix (fix(rebuild-content): bash case syntax error blocking all content rebuilds #2824)[OPTION]) rendering: honest missing-OS placeholder + left-aligned figures (fix(options): honest missing-OS placeholder + left-align figures after OPTION blocks (#2820) #2822)Changelog
hugo/data/whats_new.json: +16 entries (fix(admin-health): replace illegal new-Date expression binding with a formatter #2798–fix(rebuild-content): bash case syntax error blocking all content rebuilds #2824), newest-first, deduped (832 total). No existing entries changed.Verification
.deploy/mta.yamlparses; version =1.40.0.build:whats-new-snapshotruns clean; srv snapshot matches source.🤖 Generated with Claude Code