Skip to content

Release 1.22.0 — What's New refresh + version bump - #2176

Merged
jung-thomas merged 178 commits into
mainfrom
DEV
Sep 6, 2026
Merged

Release 1.22.0 — What's New refresh + version bump#2176
jung-thomas merged 178 commits into
mainfrom
DEV

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Release 1.22.0 (DEV → Main)

Promotes the current DEV state to main for the production deploy.

Release housekeeping in this PR

  • What's New digest refreshed — 45 new entries (411 total) covering:
    • Features (15): Channels Hub Phases 0–3, Channel Atlas SPA, community submission loop, editorial collections, standard reports, Devtoberfest banner, per-event identity, partial completions, related-channels band, unified navigation.
    • Fixes (16): Atlas dark-mode + blank-page fixes, Channels hub UX/health polish, admin list→object row navigation, nav palette, report filter value help, promo video, puzzle clue order, MCP OAuth, sitemap guard, no-step tutorials, api-docs install.
    • Maintenance (11): CI build guards, seed/pipeline reliability, ESM/CJS renames, rebuild-content fix, release housekeeping.
    • Docs (3): Joule Work Desktop MCP, MCP OAuth client_id, channels docs sidebar.
  • MTA version bumped 1.21.1 → 1.22.0 (minor — feature release).

Deploy note

.deploy/mta.yaml version drives the mtar filename (tutorials-ims_1.22.0.mtar) and cf mtas. Deploy is manual/blue-green after merge.

/sitemap.xml is served from the HANA page-sitemap.xml blob and republished
by publish-content.ts on every non-slug rebuild (discoverPageFiles, gated
`if (!opts.slug)`). catalog-only skips "Fetch tutorials", so Hugo bakes a
sitemap with NO /tutorials/ URLs and republishing it WIPES the live sitemap
down to ~180 links (reported 2026-09-03 by the Intelligent Search Data
Crawling team: crawled docs ~1700 -> ~1500). Same wipe class as the
/browse/ and /authors/ catalog-only wipes.

- scripts/seed-sitemap-from-deployed.ts: catalog-only preserve step. Runs
  AFTER the Hugo build (the sitemap is a build output, not an input) and
  overwrites the tutorial-less sitemap with the one the approuter is
  currently serving. Fails the run if the deployed sitemap is unreadable or
  names zero tutorials (ALLOW_EMPTY_SITEMAP=1 escape hatch for seed envs).
- scripts/check-sitemap-tutorials.cjs: fail-closed guard. Runs in full +
  catalog-only before publish; fails if the baked sitemap has zero
  /tutorials/ <loc> (the exact wipe signature).
- rebuild-content.yml: wire both steps in after "Build Hugo site".
- seo-files.test.js: smoke assertion that /sitemap.xml contains /tutorials/.
- unit tests cover both tutorial-<loc> counters (shared logic, run through
  both impls so they can't diverge).
fix(sitemap): guard catalog-only rebuilds from wiping /tutorials/ URLs
…ials (#2127)

Devtoberfest "validation" tutorials are published with zero real steps on
purpose: the page must be live (visible in the mission) but NOT completable —
no Done button, no completion, no Devtoberfest points — until real questions
are added. Two defects blocked this.

1. Commented-out steps broke rendering. parseV2Steps was only fence-aware, so
   a `### Question 1` wrapped in a multi-line `<!-- ... -->` comment was lifted
   as a phantom step and its `<!--`/`-->` markers stranded across the split,
   breaking Hugo rendering of the whole page. parseV2Steps now consults
   commentLineFlags() (already used by frontmatter/intro extraction): H3s inside
   a spanning comment no longer delimit a step, and commented lines are dropped
   from step content.

2. No-step tutorials were quarantined. validate-tutorials.ts required
   stepCount > 0 for every tutorial. A new repo-scoped exception
   (stepCountReason + NO_STEP_ALLOWED_REPOS) permits exactly 0 steps ONLY for
   the developer-advocates repo family (looked up via the fetch discovery
   cache); every other repo still requires >=1 step. Missing/corrupt discovery
   cache degrades safely (still requires a step).

The Hugo layout already omits Done buttons and completion when stepCount == 0
(step shortcodes gone, `total > 0` guards short-circuit). Additionally suppress
the now-meaningless progress ring, progress-bar island, PiP launcher, and
expand/collapse-all controls for step-less tutorials so the page renders cleanly.

Verified: parser + validate unit suites green (adds v2-commented-h3.test.ts and
validate-tutorials-stepcount.test.ts); local Hugo build of a 0-step tutorial
renders the intro with no Done button and no progress affordances.
…ep-commented

fix(tutorials): support no-step + commented-out-step validation tutorials (#2127)
The npm i -g sap-devs instruction 404s — the CLI is no longer
published to npm. Replace with the Homebrew/Scoop/GitHub Releases
binary install methods and fix the project link to the sap-devs-cli
repo.

Closes #2123
…s-install

fix(api-docs): sap-devs CLI install no longer on npm (#2123)
…nger-hunt

Update Devtoberfest scavenger hunt clue captions (#2130)
Add a prominent top-of-homepage banner linking to /devtoberfest/. Before
the event it shows a live countdown to the start; during the event it shows
a live-now message with the contest date range. Hidden off-season, after
the event ends, or on error -- no layout shift.

The event window is read at runtime from the existing anonymous
GET /api/devtoberfest/status (admin-managed via /admin-ui/#/devtoberfest),
so there are no hardcoded dates. Reuses the tested countdown.ts phase/label
logic; new phase->view logic in view.ts is unit-tested (9 cases).
…nner

feat(homepage): Devtoberfest banner with live countdown (#2131)
…pp-space (#2133)

Stop hardcoding "TechEd" and pull real event identity from the related event.

Schema:
- Events gains description (app-space hero), hasLogo, logoUpdatedAt.
- New EventLogo entity: 1:1 composition child holding a WebP BLOB
  (mirrors DevtoberfestBanner — plain hdbtable, no journal annotation).
- Events hdbmigrationtable regenerated (version=4).

Backend:
- srv/lib/event-logo-store.js — sharp -> WebP q82 pipeline, sha256,
  upsert + hasLogo flip, HANA-raw / SQLite-CDS-QL fetch (LOB locator rule).
- AdminService bound actions uploadEventLogo / clearEventLogo + handlers.
- Anonymous GET /api/event-logo?eventLegacyId=N (ETag + 1d cache),
  with matching approuter authenticationType:none route.
- getEventBuckets now returns { eventName, eventType, hasLogo, buckets };
  getAppSpaceProgress adds eventDescription + hasLogo.

Frontend:
- event-display: real event name in the hero label + logo lockup.
- app-space: event name, description, and logo from the related event.
- Admin Events object page: Upload/Clear Logo header actions.

Tests: test/unit/events/event-logo-store.test.js (pipeline + upsert +
fetch + clear). Migration verified idempotent via canonical cds build.
…omization

feat(events): per-event name, description & logo on event-display + app-space (#2133)
The mcp-remote --static-oauth-client-info blocks hardcoded the dev client
sb-tutorials!t676072. Prod runs a distinct xsappname (tutorials-prod) in the
same XSUAA tenant, so its OAuth client is sb-tutorials-prod!t676072. Against
developers.sap.com the dev client_id + prod-owned Tutorial.MCP scope (emitted
by the .well-known discovery) is rejected at /oauth/authorize as an invalid
authorization request.

Switch the example blocks to the prod client_id and add a dev/prod mapping
table plus a cf env lookup so users pick the id matching their <base>.
…-uri whitelist

Some mcp-remote builds bind the callback to the loopback IP 127.0.0.1
(RFC 8252's preferred default), producing http://127.0.0.1:<port>/oauth/callback.
XSUAA matches redirect_uri literally and the client whitelists only
http://localhost:*/oauth/callback, so the IP form fails at /oauth/authorize with
'redirect_uri does not match the configuration'. Pin --host localhost in both
example blocks and add a troubleshooting note.
…uer)

MCP clients (mcp-remote / MCP SDK) read authorization_servers[0] from the
protected-resource metadata and run RFC 8414 discovery against it. Option A
pointed that at the raw XSUAA URL, but XSUAA does not implement RFC 8414:
GET <xsuaa>/.well-known/oauth-authorization-server 302-redirects to /login,
which returns 200 (HTML). The SDK follows the redirect, parses HTML as JSON,
and every required field is undefined -> ZodError. Because the response is 200
(not 404) the SDK never falls back to XSUAA's working openid-configuration, so
the token exchange dies after a successful authorize.

Fix: advertise the approuter itself as the authorization server. protected-
resource authorization_servers and the AS metadata issuer now use the request-
derived self base URL; authorize/token endpoints still point at XSUAA. Clients
discover our valid 200 RFC 8414 doc and never touch XSUAA's broken well-known.
Token validation on the resource side is unchanged (XSUAA binding).

Reverses the authorization_servers/issuer=XSUAA half of the frozen Option A
(2026-08-28 spec); the rejected 'Option B' (relocating discovery to the CAP
origin) remains out of scope. Updates unit + hybrid tests, architecture doc,
and adds a superseding note to the design spec.
fix(mcp-oauth): advertise approuter as authorization server (self-issuer)
…rod-clientid

docs(mcp): fix OAuth quickstart to use prod client_id
… loadLiveTags; generic media-diet error messages
…crosswalk

feat(channels-hub): Phase 2 — crosswalk Direction 2, media-diet signed-in + export
… wiring

- srv/server.js: add GET /build/channel-atlas handler (after /build/channel-collections);
  selects published Channels + separate REVIEWED ChannelTopicMap SELECT; groups
  topicTags by channel_ID; lazy import of buildAtlasChannels inside handler (srv-qa safe)
- srv/lib/build-channel-atlas.js: convert module.exports → ESM export (project type=module)
- approuter/xs-app.json: add /channel-atlas-ui/ SPA route (after /explore-ui/);
  add /channels/atlas/ static route (before /browse/ routes); /build/ allowlist
  already contained channel-atlas (confirmed present, no change needed)
- .deploy/mta.yaml: add ../../srv/lib/build-channel-atlas.js to srv-qa cp list
- test/unit/channels-atlas-endpoint.test.js: 4/4 passing unit tests
…es, styles)

Adds all base files for the channel-atlas Vue 3 + Vite + TS SPA:
- package.json with pinned deps (sigma 3.0.3, graphology 0.26.0, vue 3.5.34)
- vite.config.ts with /channel-atlas-ui/ base, gzip budget plugin (150KB), dev proxy
- index.html, tsconfig.json, src/main.ts, src/styles.css, src/App.vue (minimal stub)
- Replaces types.ts stub with full domain types: OwnerType, AtlasChannelDTO,
  AtlasNode, AtlasEdge (shape preserved), AtlasPayload

Verified: 20/20 graph.test.ts pass; vue-tsc --noEmit 0 errors.
…tlas2)

- Add AtlasGraph.vue: Sigma 3 renderer with graphology MultiDirectedGraph,
  forceAtlas2 layout, node/edge styling, nodeClick emit
- Add AtlasGraph.test.ts: 5 unit tests (mount, nodes, edge dedup,
  nodeClick emit, prop-change rebuild)
- Fix tsconfig.json: add "types": ["vite/client"] so import './styles.css'
  resolves (TS2882)
…ate, doc star exclusion, ref-based click-outside
The vitest `unit` project (run from repo root in CI) includes
app/channel-atlas/src/**/__tests__, and App.test.ts + AtlasGraph.test.ts
transitively import AtlasGraph.vue → graphology/sigma. Those deps live only
in app/channel-atlas/package.json, not root, so unit-tests.yml's `npm run
setup` must install that workspace the same way it already installs
app/explore. Without it CI fails: 'Failed to resolve import graphology from
AtlasGraph.vue'. Passed locally only because the SPA node_modules was
already present.
…atlas

feat(channels): Phase 3 — Channel Atlas SPA (/channels/atlas/)
#2169 added the channel-detail serve route to srv/server.js but not
srv-qa/server.js, tripping check-srv-qa-route-drift. The channel-detail
page is a public prod content surface (like /content/topics/:slug), not
tutorial-draft author preview, and its publish sibling
POST /content/publish/render-channels is already allowlisted. Add the
serve counterpart to ALLOWLIST_ONLY_ON_SRV.
…il-route-drift

fix(ci): allowlist GET /content/channel-detail/:slug (route-drift guard)
…result

The seed sent all ~236 published channels plus a 236-entry sourceId enum
and 160-entry topicTag enum in one chatCompletion capped at max_tokens:4000.
The response truncated to invalid/empty tool args, so the whole seed silently
produced 0 drafts (exit 0 masked it). Batch channels 25 at a time; one failing
batch is logged and skipped rather than aborting the run. Full DEV run now
seeds 602 drafts across all 236 channels.
…ed-chunking

fix(channels): chunk ChannelTopicMap AI seed to avoid token-cap zero-result
check-slug-lookups flagged srv/lib/build-channel-detail.js:35 as an
unmarked direct slug lookup, failing the 'unit tests' CI job (introduced
via #2171). The slug is already lowercased into canonSlug at the top of
buildChannelDetailPayload, so add the // slug-canonical: pre-canonicalized
marker the guard requires.
…enrich health

- ChannelsDirectory: remove Learn↔Follow hub card linking to /channels/crosswalk/
  (page was never built; xs-app.json reserves but does not serve it → 404).
  Crosswalk data already surfaces on topic pages + channel-detail pages.
- MediaDiet: two-column layout (sticky picker rail + results), live match
  count so results are never invisible below the fold, raise focus-area cap 3→6.
- ChannelsHealth: add narrative summary, active-rate % headline (color-coded
  good/warn/poor), proportion bars per panel, and 'thin' coverage flags —
  replaces the bare number table. No new data fields required.

Tests updated: hub band 4→3 cards + no-crosswalk assertion; media-diet cap→6
+ live-count; health narrative + active-rate. 32/32 island tests pass.
…tail

fix(ci): mark pre-canonicalized slug lookup in build-channel-detail
fix(channels): hub UX — crosswalk 404, media-diet layout, health enrichment
Hugo's {{ . | jsonify }} emits the atlas-payload inline JSON double-encoded
(a JSON string whose contents are themselves JSON), so a single JSON.parse
in useAtlasData yielded a string, not an AtlasPayload. App.vue then crashed
on payload.value.channels.map(...) — blank Atlas.

useAtlasData now re-parses when the first parse returns a string and
validates that the result is an object with a channels array before
assigning; otherwise it fails open with an error state. Also repairs
already-served pages (the fix ships in the SPA bundle).

Tests: added double-encoded + missing-channels cases (34/34 pass).
The atlas SPA never consumed the site's --sap* theme variables (flipped by
the html.dark class), so in dark mode: owner-type filter gave no visible
selected/deselected indication, detail-panel text was black-on-black, and
Sigma graph labels rendered black on the dark canvas.

- styles.css: body/loading/error/side-border now use --sap* vars w/ light fallbacks
- OwnerTypeFilter.vue: add <style> with clear selected (highlight + checkmark)
  vs deselected (dimmed + faded swatch) states; add role/aria-selected
- ChannelDetailPanel.vue: add <style> using theme vars for text/labels/link/borders
- AtlasGraph.vue: Sigma labelColor from computed --sapTextColor; MutationObserver
  live-updates label color on theme toggle (setting refresh, no re-layout)
fix(channel-atlas): dark-mode theming — legible text + visible filter selection
- Add 45 What's New entries (Channels hub Phases 0-3, Channel Atlas, community submissions, nav improvements, admin FE fixes, and more)
- Bump .deploy/mta.yaml version 1.21.1 -> 1.22.0 for the release deploy
@jung-thomas
jung-thomas merged commit 5cdedcb into main Sep 6, 2026
12 checks passed
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