Skip to content

Release 1.27.0 → main (production) - #2415

Merged
jung-thomas merged 152 commits into
mainfrom
DEV
Sep 18, 2026
Merged

jung-thomas merged 152 commits into
mainfrom
DEV

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Production release 1.27.0

Promotes DEV → main for the production deploy. 152 commits ahead of main.

Highlights since 1.26.0

Full list in hugo/data/whats_new.json.

Post-merge (not part of this PR)

Merging to main does not redeploy CF — the MTA deploy is a separate, explicitly-authorized step. Deploy from a fresh origin/main after this merges.

Generated with Claude Code

jung-thomas and others added 30 commits September 15, 2026 10:39
…-event flash (#2314)

The app-space page is a public kiosk/QR landing. An anonymous visitor
opening it with ?eventId=<id> saw the DEFAULT event, not the one in the
URL: getAppSpaceProgress required authenticated-user, so anon requests
401'd and the client fell back to the static default-event JSON.

- srv: getAppSpaceProgress is now @requires:'any'. It already degrades
  gracefully for anon (resolveUserSapId → null → empty progress); only
  public content (event name/description/tracks/tutorial URLs) is
  exposed. New `authenticated` flag in the payload tells the client
  whether to enable the logged-in-only realtime/confetti path — truthy
  even before a Users row exists (identity from the JWT user_uuid).
- client: read the payload before deriving login state from the
  server's `authenticated` flag (a 200 no longer implies a login). Gate
  the hero text on !loading with a skeleton so the computed default name
  ('SAP TechEd') never flashes before the URL event resolves.

Tests: anon + authenticated getAppSpaceProgress server tests; anon
event-resolution + no-flash island tests.
…e-anon-event

fix(app-space): correct event for anonymous visitors + no default-event flash (#2314)
The default socket.io transport (polling→upgrade) starts with a long-polling
handshake whose follow-up POST 400s behind the multi-instance CF approuter,
leaving display-app stuck 'Reconnecting…'. Match the sibling clients
(event-display, gameboard, app-space) which already pin transports:['websocket'].
…-app-ws

fix(display-app): pin websocket-only transport (#2306)
…2308)

provenance-keys.js read process.env.PROVENANCE_SIGNING_KEY directly, but a
BTP Credential Store binding does not auto-inject entries into process.env.
A key rotated in via /admin-ui/#secrets lands in the credstore, so provenance
never saw it -> jwks.json empty + provenance endpoint 503 on PROD.

Route the read through resolveSecret('PROVENANCE_SIGNING_KEY') (credstore ->
env fallback -> null), the same seam as CONTENT_API_KEY and every other secret,
matching the #2245 design intent. Env fallback is preserved for local/dev.

- add regression test that primes the resolver cache (not process.env) and
  asserts the signer loads
- register PROVENANCE_SIGNING_KEY in seed-secrets so the presence/expiry cron
  surfaces a missing key
- fix gotchas doc line that wrongly claimed the credstore binding surfaces the
  key as an env var
Import @cap-js/ord's OpenResourceDiscoveryService model into the project's
own srv model (srv/ord-annotations.cds) so it compiles into the production
CSN. Previously cds build --production treated the plugin-contributed
cds.requires["SAP ORD Service"] model (in node_modules) as an external
required service and omitted it from gen/srv/srv/csn.json; the deployed
runtime loads only that precompiled CSN and never instantiated the service,
so its init() never registered the express routes -> 404 on both
/.well-known/open-resource-discovery and /ord/v1/documents/ord-document.

Verified: cds compile srv now yields the local service
{kind:service, @path:'/.well-known/open-resource-discovery', @rest};
dev server serves both endpoints 200 (32 apiResources incl. 5 mcp).

Approuter already routes both paths anonymously ahead of the catch-all;
plugin defaults to Open access. Un-skip the two ORD smoke tests.
…y-2308

fix(provenance): read signing key credstore-first via resolveSecret (#2308)
…-2307

fix(ord): serve ORD discovery endpoints at runtime (#2307)
Author-facing skill to promote a QA-previewed tutorial from its private
<product>-Contribution repo (QA source) to the canonical public
sap-tutorials/Tutorials repo (PROD source), opening a cross-linked PR in
each. Handles create (first promotion) vs update, resolves each repo's
default branch dynamically (Tutorials=master, -Contribution=main), and
drift-guards updates before overwriting PROD.

Complements sap-tutorial-dual-pr (edit/mirror); this covers QA->PROD
promotion. Closes #2310.
…ill-2310

feat(skills): tutorial-qa-to-prod author skill (#2310)
…odes (#2311)

Ingest rich Devtoberfest Planner sessions (title/abstract/speaker/YouTube)
from the cross-container facade (external.devtoberfest.*) into the Knowledge
Graph as a new Phase-4 external-content type, DevtoberfestSessions, linked to
concepts via predicate 'presents'. Sessions also join the semantic-search
'external' corpus and surface in concept-page neighborhoods/related content.

- Schema: DevtoberfestSessions + DevtoberfestSessionConceptLinks
  (db/external-content.cds), with embedding/embeddingVec for search.
- Projection: buildDevtoberfestSessionTriples + a session->tutorial bridge
  edge (aboutTutorial) for TUTORIAL-typed Planner Activities, so sessions
  connect into the tutorial subgraph instead of sitting isolated
  (srv/lib/kg-projection.js; TTL 730d in external-content-ttl.js; short-code
  'dtf' in kg-explore-data.js).
- Cron: fetch-devtoberfest-sessions-job.js + extractor
  devtoberfest-session-extract.js, scheduled Mon+Thu 04:43 UTC. Double-gated
  (KG master switch + KG_DEVTOBERFEST_SESSIONS_ENABLED) and fail-closed when
  cross-container Leg B isn't deployed.
- Feature flag: DB-backed flag.kg.devtoberfestSessions (kind:'db', default
  OFF, admin-editable via /admin-ui/#featureFlags) — no env var (survives
  blue-green MTA deploy).
- Semantic search: DevtoberfestSessions added to EXTERNAL_SOURCES + the
  freshness-corpus embedding backfill.
- Sidebar: resource-type config entry + OtherResource widening.
- Tests: extractor, projection (incl. bridge edge), fetch job (#708/budget/
  links/flag gates), semantic-search external corpus; lockstep counts bumped.
- Docs: knowledge-graph.md section + cross-container link registry update.

Ships flag-gated + fixture-tested so it merges to DEV independently of Leg B;
enable only after Leg B (ACTIVITY_SESSION_V1 synonym+grant) is deployed.
Follow-up to the KG/search integration: make the new devtoberfest-session
nodes and their presents/aboutTutorial edges renderable in /graph/explore-data
and the Explore UI, and satisfy the slug-lookup static guard.

- KG_QUERY.hdbprocedure EXPLORE_GRAPH_BULK: add kg:presents + kg:aboutTutorial
  to the edge-predicate FILTER allowlist (9 -> 11 predicates).
- Explore + related-graph type unions: add 'devtoberfest-session' NodeType and
  'presents'/'aboutTutorial' PredicateType (app/explore/src/types.ts +
  hugo-apps/src/related-graph/types.ts mirror; OtherResource widened with the
  session-specific fields).
- Explore UI filter/render maps: ALL_NODE_TYPES + ALL_PREDICATES (useFilters),
  NODE_COLORS + EDGE_COLORS (ExploreGraph.vue), SECTION_ORDER/LABELS/expanded
  (MobileTypedList.vue) — the Record<NodeType|PredicateType> maps that would
  otherwise drop the new type/edges from the graph.
- Mark the two write-path-canonicalized slug lookups in the fetch job
  (// slug-canonical: write-path-canonicalizes) to pass check-slug-lookups.
…2311

feat(kg): Devtoberfest sessions as first-class KG + semantic-search nodes (#2311)
…eed (#2312)

Unit F FOUNDATION for TechEd 2026 in the KG:
- db/external/teched.cds: TechEdSessions/Speakers/Tracks + junction + KG link table (external ns, cuid+managed, delta-ingest chassis: sourceId/slug/contentHash/lastExtractedHash/firstSeenAt/lastSeenAt/pinUntil). Imported from external-content.cds so CAP loads it.
- srv/lib/teched/rainfocus-fetcher.js: fetchAllTechEdSessions() over both venues (Virtual tev26, Berlin te26) via POST events.rainfocus.com/api/search; retry+jitter, AbortController timeout, incremental MAX_BODY_BYTES cap, Promise.allSettled, dedup, past/invalid filtering, _fetch seam.
- srv/lib/teched/normalize.js: order-independent contentHash of source fields, kebab slug dedup, row normalizers.
- srv/lib/teched/seed-core.js + scripts/seed-teched.cjs: idempotent SELECT-then-UPSERT (source-owned cols only; never overwrites pinUntil/lastExtractedHash), junction add+prune; dry-run default, --commit/--force/--file.
- /build/teched in srv/server.js: explicit public projection, LargeString abstract/bio/description read in dedicated LOB-only queries (LOB-safe rule), Cache-Control 60s.
- Tests: teched-normalize/fetcher/seed (21 green). Fixtures under test/fixtures/teched/.

RainFocus contract confirmed reachable live; per-venue widget/apiProfile IDs are Akamai-gated and TBD — fixture is constructed, flagged verify-live in srv/lib/teched/README.md.
feat(teched): RainFocus fetcher + data model + seed + /build/teched feed (Unit F, #2312)
- New docs/developers/reference/teched.md: RainFocus contract, TechEd*
  data model + using side-effect gotcha, idempotent seed, /build/teched
  feed, and the planned cron/page/KG/search/Devtoberfest cross-link units
  (mirrors channels.md structure/depth)
- CLAUDE.md Top Gotchas: one bullet linking the new doc
- rebuild-content-workflow.md: note that /teched/ is a page-<key> HANA
  page baked in build:all + published via the rebuild workflow
Unit 10 of #2312. Adds upcoming SAP TechEd 2026 sessions (from
external.TechEdSessions) to the homepage events band as always-on
EventCards, region-agnostic like Devtoberfest, each linking to its
session URL (falling back to /teched/).

Gated behind the new TECHED_HOMEPAGE_ENABLED DB feature flag
(ImsConfig key flag.homepage.teched, default OFF, dev-only). Fail-open:
an empty catalog, an unset/cold flag cache, or a query error yields no
TechEd cards, so merging is inert until an admin flips the flag.

- srv/homepage-service.js: _techedAlways(db) helper, merged into
  _communityEventsForBand alongside manual/Devtoberfest/CodeJam sources.
- srv/lib/feature-flags/registry.js: register TECHED_HOMEPAGE_ENABLED.
- test/unit/homepage-events-teched.test.js: flag off/on, url fallback,
  virtual venue mapping, empty catalog, past-session drop.
…Unit 3)

Add TechEd 2026 sessions to the knowledge-graph projection so sessions
surface as graph nodes linked to concepts, their track, and speakers.
Additive and TTL-gated; empty until Unit 2's job populates
TechEdSessionConceptLinks.

Mirrors the community-event / devtoberfest-session external-content-type
pattern exactly:
- KG_IRI_PREFIXES + iri* helpers for teched-session/speaker/track.
- buildTechEdSessionTriples: node + literals, concept `covers` edges,
  structural `inTrack` (session→track) and `presentedBy` (session→speaker)
  edges, plus track/speaker nodes (emitted only when referenced by a
  visible session). Date-aware TTL (scheduledEnd + 30-day grace, falls
  back to scheduledStart), mirroring community-event gating.
- loadFixtures block reading TechEdSessions/Tracks/Speakers, the
  session↔speaker junction, and TechEdSessionConceptLinks from the CDS
  snapshot (LOB-safe; fail-open).
- Section 16 call in projectFromFixtures.
- external-content-ttl.js: 'teched-session' key (null → date-aware).
- kg-resource-type-config.js: teched-session entry (priority 100).
- kg-explore-data.js SHORT_BY_TYPE entries (lockstep) + KG_QUERY
  EXPLORE_GRAPH_BULK predicate allow-list gains kg:inTrack/kg:presentedBy
  so TechEd nodes surface in /graph/explore-data.

Tests: new build-teched-session-triples unit test; lockstep + config
count assertions updated. Full DB-bound projectTriples path verified
against in-memory SQLite (loadFixtures FK mappings); the full graphRebuild
SPARQL round-trip is HANA/hybrid-only, so the triple-builder unit test is
the primary gate.
…ks (#2312)

Unit 9: relate Devtoberfest and TechEd sessions via shared KG concepts.

- New srv/lib/teched-devtoberfest-crosslink.js: pure overlap-ranking helpers
  (top 3 by shared-concept count) plus flag-gated, fail-open DB orchestrators.
  Devtoberfest session -> tutorial (Activity.TASKSLUG) -> TutorialConceptLinks;
  TechEd session -> TechEdSessionConceptLinks; both reference the same Concepts.
- assembleFeed attaches relatedTechEdSessions per Devtoberfest session.
- /build/teched attaches relatedDevtoberfestSessions per TechEd session
  (reverse path scoped to the current Devtoberfest edition, symmetric with
  the edition-scoped forward feed).
- New DB feature flag TECHED_DEVTOBERFEST_CROSSLINK_ENABLED
  (flag.teched.devtoberfestCrosslink, default OFF, dev-only).
- Fail-open: empty arrays when the flag is OFF, concept links are cold, or the
  cross-container Devtoberfest planner facades are absent (unit SQLite) --
  never throws into feed assembly or the build handler.
- 60s in-process cache for the TechEd concept map (feed + iCal + RSS + build
  all share the loader).
- Unit tests: pure ranking, cap-3, assembleFeed wiring, and SQLite-backed
  orchestrator (flag ON populates, flag OFF empty, facades-absent fail-soft).
Unit 6 of #2312 — let agents query the TechEd 2026 session catalog.

- srv/lib/mcp-teched-search.js: handleSearchTechEd() modeled on
  handleSearchEvents. Validates/clamps inputs, filters by venue
  (BERLIN|VIRTUAL), track (slug or case-insensitive name substring), and
  free-text over title+abstract; orders by scheduledStart; fails open [].
  The abstract LargeString (NCLOB) is never used in a SQL predicate — it's
  read in a dedicated LOB-only query and matched in JS, so behaviour is
  identical on SQLite + HANA (CLAUDE.md LOB rule). Resolves track slug and
  speaker names via separate queries. Exports mapRow().
- srv/search-service-mcp.cds: @requires:'any' search_teched function
  (query, venue, track, limit) mirroring search_events.
- srv/search-service.js: .on('search_teched', handleSearchTechEd).
- srv/mcp/mcp-manifest.js: advertise search_teched in the anonymous tier.
- tests: new unit test (13 cases) + contract-test enumeration/params.
Filterable TechEd 2026 session browser mounting on #teched-sessions-grid-mount.
Berlin/Virtual sections with a venue toggle, track facet chips, speaker
dropdown, and text search over title/abstract/speaker/track. Filters sync to
the querystring (deep-linkable). Loads from an embedded #teched-data blob or
falls back to GET /build/teched.

- filter.ts: pure filterSessions(sessions, state) — venue/track/speaker/query
- url-state.ts: parse/serialize deep-link state (trimmed, venue-normalized)
- App.vue + main.ts: island UI, accessible headings/labels
- __tests__: filter, url-state, render (vitest)
- vite.config.ts: teched-sessions-grid rollup entry
Make TechEd 2026 sessions visible to the HANA native property graph so
Louvain community detection and PageRank see them alongside the existing
concept/tutorial/mission/group/tag/product/category structure.

KG_PG_VERTICES_V — three new UNION-ALL arms:
  - teched_session (keyed on SLUG, LABEL=TITLE)
  - teched_track   (keyed on SLUG, LABEL=NAME)
  - teched_speaker (keyed on SLUG, LABEL=NAME)
  All three carry the same ROW_NUMBER() OVER (PARTITION BY SLUG ...) dedup
  guard as the concept arm: TechEdSessions/Tracks/Speakers.slug is
  @assert.unique at the CAP runtime layer only, but the delta-ingest seed
  (srv/lib/teched/seed-core.js) writes via raw db.run() keyed on sourceId
  (not slug) and bypasses the service layer, so a re-slug race could leave
  two rows sharing a SLUG and crash the whole KG workspace ([4907] range 1).

KG_PG_EDGES_V — three new UNION-ALL arms:
  - covers      teched_session -> concept (TechEdSessionConceptLinks,
                filtered PREDICATE='covers', concept STATUS='ACTIVE')
  - inTrack     teched_session -> teched_track (TechEdSessions.track_ID)
  - presentedBy teched_session -> teched_speaker (TechEdSessionSpeakers)
  All use SELECT DISTINCT (defense-in-depth against duplicate EDGE_KEY).
  Track/speaker vertex arms were added specifically to back the inTrack /
  presentedBy edges (mirrors how #919 added mission/group vertices for the
  partOf/inCategory arms) so no edge endpoint is a dangling vertex key.

Additive and HANA-only: these .hdbview artifacts live under db/src/ and are
deployed by the HDI deployer, never compiled by the SQLite unit-test model
(cds.requires.db[development] = db/sqlite). Column shapes/order/types and
NVARCHAR key sizing mirror the existing arms; new vertexType values fit the
String(16) persistence columns.
Create the SAP TechEd 2026 Sessions page that mounts the Unit 8 island
(#teched-sessions-grid-mount) and serves via the pages-from-HANA pipeline.

- hugo/content/teched/_index.md + hugo/layouts/teched/list.html: page shell
  with <h1>, separate Berlin/Virtual section headings, per-venue <noscript>
  fallback lists, and the content-hashed island via the island-src.html
  partial. Uses a <div> wrapper (not a nested <main>) since baseof already
  provides the sole <main> landmark — avoids axe landmark-no-duplicate-main.
- scripts/fetch-teched.ts: fetch ${CAP_BASE}/build/teched -> hugo/data/teched.json,
  fail-open to empty {sessions,speakers,tracks} on error (mirrors fetch-channels).
  Wired into build:all before build:hugo; teched.json gitignored (build artifact).
- srv/lib/page-key-map.js: add page-teched (teched/index.html) to IN_SCOPE_PAGES.
- approuter/xs-app.json: route /teched/ -> /content/pages/teched/ (anonymous).
- test/smoke/pages-routes.smoke.test.js: add /teched/ HTML route with marker.

The teched-sessions-grid island itself is Unit 8; until it lands the
island-src partial falls back to the bare /js path (expected).
…#2312)

Unit 5 of #2312: make TechEd 2026 sessions searchable via the existing
public semantic (vector) search.

- db/external/teched-embedding.cds: extend TechEdSessions with dual-column
  embedding (embedding LargeBinary for SQLite, embeddingVec Vector(1536) for
  HANA), mirroring the ApiDocs/Samples/Devtoberfest convention. Loaded as a
  side-effect using from db/external-content.cds.
- freshness-corpus-embedding-job.js: embedEntity now takes a textColumn
  override (TechEd body text lives in 'abstract', not 'description'); add a
  fault-isolated TechEdSessions pass so a missing table on an un-migrated env
  logs and continues instead of failing apiDocs/samples. Existing scheduler
  entry runs it (03:17 UTC).
- semantic-search.js: add 'teched' to VALID_CORPORA and a TECHED entry to
  EXTERNAL_SOURCES (per-source descCol override); 'teched' scans only TechEd,
  'external'/'all' include it. Dual HANA COSINE_SIMILARITY / SQLite JS cosine,
  fail-open [].
- Tests: SQLite cosine gate for corpus 'teched' + 'external', and backfill
  via abstract column.

HANA vector path can't run on SQLite; SQLite cosine test is the gate.
…t + delta cron jobs (#2312)

Unit 2 of #2312. First corrects the merged foundation's fetcher (built against
a wrong /api/search + separate-speaker-scrape guess) to the real, live-proven
contract, then adds the scheduled delta ingest jobs.

Fetcher (srv/lib/teched/rainfocus-fetcher.js):
- POST events.rainfocus.com/api/sessions with rfapiprofileid/rfwidgetid headers
  (per-venue, env-overridable via RAINFOCUS_TE26/TEV26_PROFILE/WIDGET).
- Offset pagination: from += size (size=50, server hard-caps at 50) until
  from >= totalSearchItems; empty-page safety break. Retrieves full catalog
  (~297 Berlin + 49 Virtual), verified live 2026-09-15.
- Speakers extracted from embedded participants[] (speakerId, globalJobtitle,
  photoURL); tracks from attributevalues[attribute=='Track'] keyed on
  rf_attributevalue_id. utcStartTime "YYYY/MM/DD HH:MM:SS" normalized as UTC.
- Routed through safeFetch (SSRF guard, #895); retry/backoff via img-cdn-retry
  helpers with Retry-After honored up to 60 s; streaming body-size cap kept.
- normalize.js: toKebabSlug now delegates to shared slugify (diacritics).

Jobs:
- fetch-teched-sessions (weekly, Sun 05:43): fetch + runSeed upsert/delta/junction
  core (always); double-gated (KG master + KG_TECHED_SESSIONS_ENABLED, default
  OFF) fail-open concept-link enrichment with #708 crash-safety.
- refresh-teched-sessions (every 6h :23): metadata-only upsert (seed-core
  metadataOnly mode) — never touches contentHash/lastExtractedHash.
- Registered both in scheduler; wired TechEdSessions into gc ITERATION_SET +
  PER_TYPE_TTL_DAYS (teched-session, 730d); added KG_TECHED_SESSIONS_ENABLED flag.

Tests: replaced constructed fixtures with a REAL trimmed capture (both venues);
teched-feed.json derived from the real fetcher output; new teched-jobs e2e test
(core insert, idempotent delta, KG gate, enrichment SELECTs, metadata-only
refresh); updated scheduler-registry (53) + gc ITERATION_SET counts.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
feat(teched): real RainFocus /api/sessions fetcher + delta cron jobs (#2312 Unit 2)
…arch-2312

feat(teched): TechEd sessions in semantic search + embedding backfill (#2312)
feat(teched): /teched/ top-level page (Unit 7, #2312)
…h-2312

feat(kg): add TechEd sessions to HANA property graph (#2312)
feat(teched): teched-sessions-grid Vue island (#2312)
feat(teched): consolidate track color legend into filter chips (item 7/10)
…l-2392

feat(teched): speaker author-links + bio tooltip in detail pane (#2392 items 3, 9, 10)
fix(teched): construct sap.com session URL from sourceId+venue (items 2+8, #2392)
…t" (#2392)

Speaker filter dropdown now sorts by last name and shows each speaker as
"Last, First" instead of first-name-first. Adds lastNameKey() and
displayNameLastFirst() helpers; single-token names are shown unchanged.
The option value stays sp.slug so filter logic is unaffected. Shared
surnames tie-break by full name for deterministic ordering.

Issue #2392 item 4.
All-day activities (Developer Garage, AI Showfloor Expo, etc.) live
under a SEPARATE RainFocus catalog tab (`tab.alldayactivities=<id>`),
NOT in the regular `type=session` feed. This PR wires them in end-to-end.

**Model** (`db/external/teched.cds`)
- Add `allDay Boolean default false` to `TechEdSessions`.

**Fetcher** (`srv/lib/teched/rainfocus-fetcher.js`)
- New `parseAllDayActivity(item, venue)` — lenient parser (synthesises
  `allday-<sourceId>` code when absent, room from `location`/`venue`
  fallback, `allDay: true`).
- `fetchAllTechEdSessions` issues a best-effort per-venue all-day POST
  (own try/catch; `allDayFilter` env-configurable; empty ⇒ skipped).
- `parseVenuePayload` accepts `allDayItems`; dedup via `seenSourceIds`
  (regular session wins on collision).
- `dropPast` never drops a row where `s.allDay === true`.
- Defaults: Berlin filter `1742815606768001gh9v` (Tom's capture),
  Virtual unset; tab param `tab.alldayactivities`.

**Normalize** (`srv/lib/teched/normalize.js`)
- `normalizeSession` maps `allDay: raw.allDay === true` (part of hash).

**Seed** (`srv/lib/teched/seed-core.js`)
- `SESSION_COLS` includes `'allDay'`.

**Feed** (`srv/lib/teched-feed.js`)
- SELECT includes `allDay`; emits `allDay: isAllDay(r.allDay)`.
- Shared `isAllDay(v)` helper (coerces `true`/`1` → boolean).
- `upcoming=true` filter uses tagged-template CQL (safe ISO binding)
  with explicit `OR allDay = true OR scheduledEnd is null` carve-out
  so timeless all-day rows survive the filter.

**UI** (`hugo-apps/src/teched-sessions-grid/App.vue`, `filter.ts`)
- `allDay?: boolean` on `TechEdSession` interface.
- `filteredAllDay` / `filteredTimed` computed splits.
- Distinct `<section aria-label="All-day activities">` with "All-day"
  badge rendered above the timed grid; timed grid unaffected.

- `test/unit/teched-allday.test.js` — parseAllDayActivity, dedup,
  fetchAllTechEdSessions all-day tab, normalizeSession allDay flag.
- `test/unit/srv/build-teched-allday.test.js` — allDay flows through
  seed → /build/teched; upcoming filter keeps all-day rows.
- `hugo-apps/src/teched-sessions-grid/__tests__/App.allday.test.ts` —
  all-day section renders, excluded from timed grid, badge, detail
  panel, filter across both sections.
- `test/unit/teched-normalize.test.js` — allDay flag coverage extended.
- `test/fixtures/teched/rainfocus-alldayactivities.json` — synthetic
  raw all-day tab capture (live params unconfirmed — see OPEN ITEM).
- `test/fixtures/teched/teched-allday-feed.json` — derived feed shape.

The exact live RainFocus request param name/value for the all-day tab
is NOT yet confirmed against the real API. Parsing, model, seed, feed,
and UI are proven against the synthetic fixture. Tune
`RAINFOCUS_TE26_ALLDAY_FILTER` / `RAINFOCUS_ALLDAY_TAB_PARAM` when a
live capture is available — a mismatch yields 0 all-day rows and never
breaks the regular session ingest.

- #2 iCal all-day gap: graceful 404 out of scope for this unit.
- #3 NULL-first ordering on SQLite vs HANA: catalog << 1000 cap, latent.
- #5/#6 UI/parser duplication: deliberate (scoped CSS + tested parseSession).
- #7 mass UPDATE on first ingest: one-time benign, expected behaviour.
…2392

feat(teched): speaker dropdown sorted by last name, shown "Last, First" (#2392 item 4)
…ties-2392

feat(teched): list all-day activities like Developer Garage (issue #2392 item 6)
Adds a discrete 'Community Clubhouse' filter to the TechEd session browser,
identifying Clubhouse sessions by room === 'Community Theater'
(case-insensitive, trimmed). There is no dedicated track/flag.

- filter.ts: new optional clubhouse facet on filterSessions; exported
  CLUBHOUSE_ROOM + isClubhouse() helpers (ANDs with existing facets,
  default off, backward compatible).
- sessions-grid + schedule: Community Clubhouse toggle wired into
  filterSessions, hasActiveFilters, and clearFilters.
- sessions-grid url-state: persists clubhouse=1 (parse/serialize/init/
  applyFromUrl/watch) for deep-linking and back/forward restore.
- calendar: equivalent toggle in its inline filter, reusing isClubhouse();
  clubhouse=1 added to its url-state for deep-link parity with the grid.
- tests: filter (case/trim/AND), grid App + url-state, and calendar App
  (toggle + deep-link). All 113 teched unit tests pass.
…ter-2392

feat(teched): Community Clubhouse session filter (#2392 item 5)
The loadTechEdFeed session query seeded its WHERE with `where`1 = 1`` and
bound the all-day flag as `allDay = ${true}`. Both trip the HANA hdb driver
at param-binding time ("Cannot set parameter at row: 1. Argument must be a
string"), so /build/teched returned {error:...} on DEV and every /teched
island (grid/schedule/calendar) failed to load real data. SQLite tolerates
both forms, so the in-memory unit tests passed and the regression only
surfaced against deployed HANA.

Build the WHERE incrementally without the 1=1 seed — the first active
predicate uses `.where`, later ones chain via `.and` — and use the SQL
literal `TRUE` instead of a bound boolean. Verified against DEV HANA: all
opt paths ({}, upcoming, venue, venue+upcoming) return rows; 344 sessions
carry the sap.com url, 2 all-day rows present, upcoming filter keeps them.

Note: the same `where`1 = 1`` pattern exists in srv/lib/mcp-events-search.js
and mcp-teched-search.js, and a bound `${true}` in mcp-channels-search.js —
latent HANA hazards on those MCP search paths, out of scope here.

Refs #2392
…-fix

fix(teched): make /build/teched feed query HANA-compatible
…m HANA

Two DEV-only /teched regressions surfaced testing issue #2392 on deployed DEV:

1. Duplicate speakers. RainFocus issues the SAME person a DISTINCT speakerId
   per venue catalog (te26 vs tev26), so anyone speaking in both Berlin AND
   Virtual arrived as two speaker records — showing "Name" twice in the
   sessionCode dropdown and, because only one record usually carries a photo,
   leaving many detail-panel avatars blank. fetchAllTechEdSessions now collapses
   speakers by normalized name after the venue merge, keeping the richer record
   (prefer photoUrl, then bio) and remapping every session's speakerSourceIds to
   the survivor so the (session, speaker) links still resolve in seed-core.
   Live feed had 85 such duplicate names (84 confirmed same-person by matching
   company; the 1 outlier is a company-string variant of the same person).
   Verified on the real 2-venue fixture: Philipp Herzig (both venues) folds
   10 raw speakers → 9, and his Berlin session link is preserved.

2. Empty schedule + calendar. /teched/ is served from HANA (page-teched), but
   /teched/schedule/ and /teched/calendar/ had NO approuter route or page-key
   entry, so they fell through to the STATIC copy baked into the approuter mtar.
   That static copy was baked by build:all's fetch-teched against the still-
   broken backend (0 sessions) → both pages rendered empty. Add
   /teched/schedule/ + /teched/calendar/ approuter routes (→ page-teched-
   schedule / page-teched-calendar, auth none, like /teched/) and the matching
   IN_SCOPE_PAGES entries so publish-content uploads them to HANA and they pick
   up the healthy feed via the content rebuild, exactly like the grid.

Refs #2392
…hana-speaker-merge

fix(teched): dedup cross-venue speakers + serve schedule/calendar from HANA
…hedule/calendar (#2392)

All-day activities (Developer Garage etc.) were under-counted: RainFocus lists
17/19 all-day items in BOTH the main catalog and the all-day tab. parseVenuePayload's
'regular session wins' dedup left those 17 tagged allDay:false, so the UI showed
only the 2 tab-exclusive activities. Membership in the all-day tab is now
authoritative for the allDay flag (richer timed data is still kept).

Speaker photos were missing in the schedule + calendar detail panels: both islands
built speaker objects without photoUrl and never set speakersEnriched, so DetailPanel
rendered name-only cards. Both now enrich speakers (photoUrl/role/company/authorLogin/bio)
into speakersEnriched — parity with the grid — so photos, author links, and bio tooltips render.

Tests: added photo-render assertions (schedule + calendar), updated the both-tabs
dedup test to assert allDay flips true, corrected stale speakersUpserted (10→9,
Herzig cross-venue merge #2403) + authorLogin typing + calendar doc comment.
…-photos-2392

fix(teched): surface all 19 all-day activities + speaker photos in schedule/calendar (#2392)
…calendar (#2392)

The srv pageServeHandler canonicalizes the /content/pages/<remainder> URL into a
ROUTE and looks it up in IN_SCOPE_PAGES._byRoute. The approuter routes for the two
TechEd subpages targeted /content/pages/teched-schedule/ and /content/pages/teched-calendar/
(dash), which canonicalize to routes /teched-schedule/ and /teched-calendar/ — NOT in
the allow-list (the real routes are /teched/schedule/ and /teched/calendar/). Result:
both subpages 404'd even though the page-teched-schedule / page-teched-calendar BLOBs
were published correctly (verified: GET /content/pages/teched/schedule returns 200).

Fix: target /content/pages/teched/schedule/ and /content/pages/teched/calendar/ (slash),
matching the published route keys — same pattern as the working /teched/ → /content/pages/teched/.
…ter-route-2392

fix(teched): correct approuter target for /teched/schedule + /teched/calendar (#2392)
…otos (#2392)

RainFocus hosts TechEd speaker photos at https://static.rainfocus.com/...; the
img-src CSP directive did not allow that host, so every speaker photo was blocked
("violates the following Content Security Policy directive: img-src ..."). The photoUrl
flows through the feed → island → DetailPanel correctly; the browser was blocking the
fetch. Added https://static.rainfocus.com to img-src.
Speaker names in the TechEd detail panel/cards only linked when the speaker also
had a GitHub-login author page (author_index.json). Advocates like Rekha D R and
Witalij Rudnicki have /developer-advocates/{slug}/ pages but no author-index entry,
so they showed as plain text.

Add a name→advocate-slug matcher (author-match.js, mirrors the author matcher:
normalized-name, 1:1 unambiguous only, fail-open) and enrich speakers with
advocateSlug in fetch-teched.ts (roster read from hugo/content/developer-advocates/*.md
title+slug). The islands + shared DetailPanel now link via a speakerHref() that
PREFERS the advocate page (/developer-advocates/{slug}/) over the author page
(/authors/{login}/), falling back to author, then plain span.

Tests: advocate-matcher unit tests (1:1, ambiguity exclusion, fail-open),
DetailPanel tests (advocateSlug preferred over authorLogin; advocate-only link).
271 teched unit tests green.

Note: baked into hugo/data/teched.json (the blob the islands read); needs a
--ref DEV content rebuild after deploy for the links to appear.
fix(teched): allow static.rainfocus.com in CSP img-src for speaker photos (#2392)
…-2392

fix(teched): link speakers to their developer-advocate page (#2392)
…n in (#2409)

The Sign in link on the Devtoberfest schedule/sessions/calendar pages
(PointsBanner.vue) used a bare /login href. login-redirect.html defaults
returnTo to '/' when the param is absent, so signing in from a Devtoberfest
page bounced the user to the homepage instead of back to the page they
were on.

Build /login?returnTo=<pathname+search> at render time, matching the
site header's profile-click flow. Adds a happy-dom test locking the
returnTo behavior in.
…rfest-login-returnto

fix(devtoberfest): Sign in from Devtoberfest pages returns to current page (#2409)
… the header

EasyList ships an exact-class rule (##.adv-header), so uBlock Origin,
AdBlock Plus, AdGuard etc. hid the entire Developer Advocates header —
region/topic filters and search included — for any visitor with an ad
blocker (issue #2405). 'adv' reads as 'advertisement' to the filter list.

Rename the adv-* class namespace to dev-advocate-* across the advocates
and advocate-profile islands (incl. DEV's new sessions/tutorial-rail
sections) and the Hugo layout. Exact-class filter rules do not match
dev-advocate-header, and it stays semantic/readable. Tests reference the
same classes and were renamed in lockstep.

Closes #2405
…lock

fix(advocates): rename adv-* CSS namespace so ad blockers stop hiding the header (#2405)
Add §7 'AI-assisted authoring' to the meta-tutorials recommended path
(AutoAuthor incl. [AUTOAUTHOR_VIDEO_*], CodeCheck, Validate) and replace
the 'three slugs are stale (404)' caveat with a restoration note pointing
at meta-tutorials-Contribution#11.

Refs #2374, #2345
70 new What's New entries (33 Feature, 30 Fix, 3 Docs, 3 Maintenance,
1 Performance) covering merged PRs since 1.26.0. Bump MTA version
1.26.0 → 1.27.0 for the production release.

Generated with Claude Code
…atures-2374-wt

docs(authors): list restored AI-authoring meta-tutorials (#2374)
chore(release): 1.27.0 — What's New digest + version bump
@jung-thomas
jung-thomas merged commit 3a0f36c into main Sep 18, 2026
14 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.

2 participants