Skip to content

feat: add CAP search service with HANA fuzzy search - #3

Merged
jung-thomas merged 14 commits into
mainfrom
feat/search-service
Apr 30, 2026
Merged

jung-thomas merged 14 commits into
mainfrom
feat/search-service

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Summary

  • Adds a new SearchService at /search (unauthenticated) replacing AEM's Solr-backed /bin/ims/search endpoint
  • Implements SearchableItems UNION view across Tutorials, Missions, and Groups (active only)
  • Uses HANA full-text CONTAINS() with FUZZY(0.7) for typo-tolerant search and @Search.ranking annotations for field-weighted relevance
  • before READ handler augments results with tag-matched tutorials (OR expansion)
  • getFacets function returns aggregated counts by type, experience, and tag
  • Frontend: useSearch Vue 3 composable with debounced server-side search, integrated into TutorialNavigator.vue as hybrid browse/search mode
  • AppRouter route configured for public access

Test plan

  • 16 unit tests pass (SQLite in-memory) — npm test
  • Vue build completes cleanly — npm run build:apps
  • Run npm run test:hybrid to validate HANA fuzzy search and { and: [...] } CQL (requires cf login)
  • Deploy and run npm run test:smoke to verify end-to-end
  • Manual: search for "hanna" → expect HANA results (typo tolerance)
  • Manual: verify facet counts update when filtering by type/experience

Addresses all review findings:
- Add AppRouter /search/ route (deployment requirement)
- Fix tag-search to use before READ handler (correct pagination)
- Move @Search.fuzzinessThreshold to service entity
- Document nullable slug contract for GROUP results
- Use named types for getFacets return (OData serialization)
- Add risk callout for CONTAINS on UNION view
- Filter view to active-status items only
Add comprehensive Frontend Integration section covering:
- TutorialNavigator.vue rewrite from client-side to server-side search
- useSearch.ts composable for extracted search logic
- SearchableItem/SearchFacets TypeScript interfaces
- OData $filter mapping from UI filters
- Server-side pagination ($top/$skip/$count)
- Progressive enhancement strategy (Phase 1: server search + local filters)
- UX behavior changes and filter panel population from Tags entity
Replace the phased migration approach with a permanent hybrid model:
- Browse mode (no search term): fully client-side, same as today
- Search mode (2+ chars): server-side via OData with fuzzy matching

This preserves instant filter toggles while adding fuzzy search quality.
@jung-thomas
jung-thomas merged commit 5a79dbb into main Apr 30, 2026
jung-thomas added a commit that referenced this pull request May 29, 2026
…it (#70)

Tom retested DEV after a17db86 (fix attempt #3 for issue #70) — drag still
does nothing, items still snap back. Network and console panels showed the
real story this time:

* Console (with verbose enabled): zero `ItemReorder:` warn/error output —
  the defensive logs added in a17db86 never fire.
* Network panel filtered for 'ItemReorder': no row appears anywhere across a
  full page lifecycle.

The handler module was never being fetched. `drop="handler.onDrop"` resolves
against an undefined alias, so it's a silent no-op and the only thing the
user sees is the row-rebind on activation that looks like a snap-back.

Root cause: the file was named `ItemReorder.controller.js` (module ID
`sap/tutorials/admin/groups/ext/ItemReorder.controller`). FE V4 reserves the
`*.controller.js` path for its controller-extension machinery, and a
`core:require` pointing at that module ID resolves to undefined silently —
no console error, no network 404 visible in DevTools. The working sibling
next door is `TaskColumnHandler.js` (no `.controller` suffix), which is why
the cell-edit input on the Mission Tasks table works fine.

Three previous fixes (#86, #94, a17db86) all edited the body of `onDrop` on
theories that assumed the handler ran. None of them did. This commit is
deliberately scoped to the rename + `core:require` update only — the body of
the handler (including the `%{IsActiveEntity}` gate, defensive active-row
check, and refresh sequence from a17db86) is untouched, so once it actually
loads, the existing logic gets its first real test.

Files:
* `app/admin/groups/webapp/ext/ItemReorder.controller.js` →
  `app/admin/groups/webapp/ext/ItemReorderHandler.js`
* `app/admin/missions/webapp/ext/ItemReorder.controller.js` →
  `app/admin/missions/webapp/ext/ItemReorderHandler.js`
* Both `ItemReorder.fragment.xml` files updated to the new module ID.

The `ItemReorder.fragment.xml` filenames stay the same — `manifest.json`
references the section as `sap.tutorials.admin.{groups,missions}.ext.ItemReorder`
(fragment), which is independent of the handler module ID.

Closes #70
jung-thomas added a commit that referenced this pull request Jun 2, 2026
Phase-by-phase TDD plan covering issue #171 capability #3 (code-check)
per the spec at docs/superpowers/specs/2026-06-02-ai-code-check-spike-design.md.

- Phase 1 (Tasks 1.1-1.9): backend foundation behind a flag.
  Schema, parser, prompt builder, dispatch core, Express endpoint
  with rate limits, real LLM via forced tool-call, publish endpoint,
  Joule chat tool registration.
- Phase 2 (Tasks 2.1-2.4): publish CLI extension, Hugo mount,
  frontend island, @analytics.exposed.
- Phase 3 (Tasks 3.1-3.3): hybrid + smoke + manual eval harness.
- Phase 4 (Tasks 4.1-4.2): aggregate evidence, decide, comment on issue.

Cross-cutting concerns covered: security, CAP 10 readiness, Windows
CRLF, Hugo gotchas, srv-qa cp list audit, branch hygiene, CLAUDE.md
update.

Refs #171
jung-thomas added a commit that referenced this pull request Jun 19, 2026
* docs(spec): #394 disable change tracking during migration design

Spec for issue #394: REST migrators (migrate-reference-data.js,
migrate-user-progress.js) currently POST to /admin/<Entity> and
trip @cap-js/change-tracking, polluting sap.changelog.Changes with
thousands of bogus migration entries.

Approach: opt-in 'x-migration-mode: true' request header honored only
when req.user.is('admin'). Sets the same 'ct.skip' session variable
that the plugin already reads. Self-cleaning per request; no global
toggle to forget.

* docs(spec): #394 spec rev 2 — incorporate spec-review feedback

Reviewer caught multiple issues:
- Blocker: role is 'Admin' (capital A), not 'admin'
- Blocker: migrate-from-hana DOES trip change-tracking on HANA
  (DB-level AFTER INSERT/UPDATE/DELETE triggers fire regardless of
  whether the write came through CAP). Out-of-scope for #394 but
  documented in the runbook with operator mitigation.
- Major: populateSlugs() PATCH path is also affected and was missed
- Major: srv/admin-service.js is ESM with class extends ApplicationService;
  pseudocode rewritten as ESM
- Major: header access via cds.context.http?.req?.headers is the
  documented stable API
- Major: register on cds.db.before(['INSERT','UPDATE','DELETE']) for
  parity with the plugin's own implementation; req._tx is reliably
  the DB tx there
- Major: migrate-user-progress entities are NOT @changelog-tracked
  today — header is defense-in-depth, called out
- Major: split tests into plugin-contract pin + handler behavior;
  added hybrid test for HANA SESSION_CONTEXT semantics
- Minor: register new docs page in vitepress sidebar
- Minor: paired after handler resets ct.skip for pooled connections

* docs(spec): #394 spec — align cds.db.before event list (rev 2 nit)

Reviewer round-2 minor: spec text alternated between cds.db.before('*')
and the explicit ['INSERT','UPDATE','DELETE'] array form. Pick the
array form everywhere to match the plugin's own pattern verbatim.

Spec verdict: Approved (round 2).

* docs(plan): #394 implementation plan

7 tasks: contract pin → handler → migrator headers → hybrid test →
docs → manual verification → PR. Bite-sized TDD steps with exact
file paths, paste-ready code, and explicit RED→GREEN→COMMIT loops.

Plan derived from rev-2 spec at
docs/superpowers/specs/2026-06-18-394-disable-change-tracking-during-migration-design.md

* docs(plan): #394 plan rev 2 — fix test scaffolding bugs from review

Reviewer round 1 caught real defects in the paste-ready code blocks:

- cds.test() pattern: project handle from cds.test('serve', ...) with
  project.url, not invented cds.test.port
- Auth: 'admin:admin' (real password from .cdsrc.json), not 'admin:'
- Hybrid: named-import isSafeForWrites + describe.runIf gate; previous
  side-effect import did nothing
- Entities API: cds.entities('com.sap.developers.ims') (function call),
  not bracket index
- Contract pin uses Advocates (DB-level @changelog) instead of Missions
  (AdminService-level) to remove service-vs-DB ambiguity in the test

Plan structure unchanged — only paste-ready snippets fixed.

* docs(plan): #394 plan rev 3 — drop stale ALLOW_HYBRID_WRITES from success criteria

Reviewer round 2 flagged a stale ALLOW_HYBRID_WRITES env-var prefix in
the 'What success looks like' section that contradicted the round-1
fix (the actual write gate is isSafeForWrites() in test/hybrid/_guard.js).

Other round-2 reviewer notes:
- Issue #1 (cds.entities bracket vs function call for sap.changelog.Changes):
  false-positive. Both forms work in CAP; bracket-by-fully-qualified-name
  is fine. No change.
- Issue #3 (--profile hybrid in cds.test for hybrid suite):
  false-positive. test/hybrid/admin-crud.test.js uses exactly this
  pattern. No change.
- Issue #4 (branch name mismatch): false-positive. The reviewer was
  reading the session-start gitStatus header which preceded the worktree
  creation; the worktree's actual branch matches the plan.

* docs(plan): #394 plan revision after Task 1 SQLite-harness blocker

Implementer subagent surfaced that:
1. tx.set in cds.tx callback errors on @cap-js/sqlite (no dbc yet)
2. SQLite triggers don't fire in cds.test('serve','--in-memory'),
   so POST /admin/Missions produces 0 Changes rows even without
   any suppression — meaning the unit-level end-to-end test is
   infeasible regardless of what we do with the handler.

Tom chose hybrid-only end-to-end coverage. Plan revised:
- Drop unit Changes-row assertions
- Add unit spy test for req._tx.set routing only
- Hybrid test (Task 4) is the sole end-to-end pin
- Manual DEV verification (Task 6) gains importance

* feat(srv): add migration-mode handler that suppresses change tracking (#394)

When an admin-authenticated request carries x-migration-mode: true,
sets the @cap-js/change-tracking plugin's ct.skip session variable on
the DB tx so changelog triggers no-op. Paired after-handler resets.

End-to-end Changes-row verification lives in the hybrid test
(test/hybrid/migration-mode.test.js, separate commit) since
in-memory SQLite doesn't fire the plugin's triggers. Unit tests here
verify only the handler's routing logic via spies.

* feat(scripts): send x-migration-mode header from REST migrators (#394)

Three header sites:
- migrate-reference-data.js importData() POSTs
- migrate-reference-data.js populateSlugs() PATCHes (covers both
  Missions + CompletionPaths via shared headers object)
- migrate-user-progress.js importUsers() POSTs (defense-in-depth;
  current user-progress entities are not @changelog-tracked)

* test(hybrid): pin HANA SESSION_CONTEXT('ct.skip') suppression (#394)

Verifies the migration-mode handler's session-variable contract on
real HANA. Targets AdminService.ImsConfig (element-level @changelog on
key/value, not draft-enabled, no extra business validators) so the
test exercises the cds.db before('INSERT') hook end-to-end via plain
HTTP — no draft round-trip needed.

The plan's original target was Advocates, then AdminService.Missions:
- Advocates has only entity-level @changelog: true and would not fire
  the change-tracking triggers (plugin only emits trigger SQL for
  entities with element-level annotations).
- AdminService.Missions has element-level @changelog but is also
  @odata.draft.enabled and guarded by a Tag-required validator, so
  driving it via plain HTTP would require draft-then-activate plumbing
  or bypassing unrelated business rules.
ImsConfig is the cleanest exerciser of the actual handler contract.

This is the sole end-to-end coverage for #394: in-memory SQLite
doesn't fire change-tracking triggers in the cds.test('serve') unit
harness, so a regression that breaks header propagation or the
SESSION_CONTEXT('ct.skip') write would slip past unit tests.

The control test (no header → ≥ 1 new Changes row) proves change
tracking IS firing for the same table; without it, the suppression
assertion could pass for the wrong reason.

* docs(migration): runbook + corrected change-tracking comment (#394)

- New /developers/operations/migration-from-ims.md runbook covering
  reference-data, user-progress, HANA-to-HANA paths + verification
- Sidebar registration in docs/.vitepress/config.ts
- db/change-tracking.cds header comment corrected (the original claim
  that DB-level writes are excluded was wrong on HANA — plugin's HANA
  implementation deploys AFTER triggers that fire on every write)
- CLAUDE.md Data Migration section gains a one-line pointer
- docs/developers/architecture/hana-kge-access.md: pre-existing Vue parse
  error and dead link fixed so docs:build succeeds (escape <>/>)

---------

Co-authored-by: Thomas Jung <12159356+jung-thomas@users.noreply.github.com>
jung-thomas added a commit that referenced this pull request Jun 27, 2026
… + edge dedup

Adds a :data-graph-id binding to defeat Vue 3.5 SFC template hoisting
(which made container ref null under @vue/test-utils 2.4.10 + happy-dom
15 — the hoisted vnode warning prevents the ref from being attached).
The binding has no runtime effect; it just signals the compiler to keep
the vnode in the render function.

Test mocks now capture Sigma + graphology instances so:
- Edge dedup is verified by mockGraphInstances[0].edges.size
- The clickNode → emit('nodeClick') path has explicit coverage

Resolves code-review item #3 from PR #687.
jung-thomas added a commit that referenced this pull request Jul 1, 2026
…reopen) (#876)

## What Riley saw

`GET /author/MyAuthoredTutorials` returned 36 tutorials Riley didn't
author — Fiori Elements, HANA anonymization, CodeJam entries, AI Core
tutorials. Expected: 1 (rbrainey-sandbox-1, his declared authorship).

Additionally every row had `ID: undefined` in Sage's log — the projected
view's key is `tutorial_ID`, not `ID`.

## Root cause

Two compounding issues:

1. **Resolver's Phase (c) `ownerEmail` fallback was architecturally
   wrong.** `TutorialMeta.ownerEmail` encodes a *monitoring* signal
   (who watches for staleness), not authorship. The resolver treated
   it as a last-resort authorship signal, so stale monitoring
   assignments (from the legacy IMS migration) silently promoted the
   monitoring user to `Tutorials.author_ID`. On DEV, Riley was
   `ownerEmail` on 58 tutorials for legacy reasons; 36 of those had
   no frontmatter author + no matching contributors → Phase (c)
   fired → author_ID = Riley.

2. **`MyTutorialsView` exposes the tutorial key as `tutorial_ID`, not
   `ID`.** Sage's `imsApiClient` reads `row.ID` and got `undefined`
   for every row. The alias was there for the underlying view's
   naming reasons (#777) but never surfaced to OData clients.

Verified against DEV via hana-cli:

    SELECT COUNT(*) FROM COM_SAP_DEVELOPERS_IMS_TUTORIALS
     WHERE AUTHOR_ID = '<riley.id>';
    → 36  (matches Riley's log exactly)

    SELECT SLUG, OWNER, OWNEREMAIL
      FROM COM_SAP_DEVELOPERS_IMS_TUTORIALMETA tm
      JOIN COM_SAP_DEVELOPERS_IMS_TUTORIALS t ON t.ID = tm.TUTORIAL_ID
     WHERE t.AUTHOR_ID = '<riley.id>';
    → most rows: OWNER = "Bill Jiang" / "Madeline Schaefer" / etc.
                 OWNEREMAIL = riley.rainey@sap.com   ← the smoking gun

## Fix (three pieces)

### 1. Remove Phase (c) from `resolveTutorialAuthor`

The resolver now stops at Phase 0 (frontmatter) / Phase (a) role-match /
Phase (b) any-contributor. If none of those hit, `authorUserId` stays
null and the tutorial's `author_ID` is left NULL. The `ownerEmail`
parameter is still passed for orphan-CSV reporting (backfill script
uses this to surface unresolved rows).

Consequence: some tutorials that had `author_ID` set only via the old
Phase (c) will end up with `author_ID = NULL` after the next publish
cycle. That's correct — they should have been NULL all along. The
broader `MyTutorials` endpoint (which reads `MyTutorialsView`, a
four-source UNION) still surfaces those tutorials via source #3
(TutorialMeta.ownerEmail = Users.email) so admin Tutorial Health and
the advocate roster keep working. Only `MyAuthoredTutorials` (strict
authorship) is affected.

### 2. Data repair via `scripts/repair-author-id-phase-c.cjs`

One-shot ops script:
- Iterates every Tutorial with author_ID IS NOT NULL.
- Re-runs the resolver (Phase 0 unavailable — frontmatter isn't
  persisted on the row, so this is a conservative reproduction).
- Three-way classification: ok / conflict / null-out.
- `--dry-run` by default; writes `.migration-data/repair-author-id-phase-c-<ts>.csv`.
- `--commit` applies UPDATE ... SET author_ID = NULL for the null-out
  set, gated by WHERE AUTHOR_ID = <current> so concurrent publishes
  don't get clobbered.
- Idempotent (WHERE author_ID IS NOT NULL narrows re-runs to a no-op).
- Tutorials whose frontmatter genuinely declares an author will be
  re-linked on the next publish via Phase 0 (linkTutorialAuthorship
  overrides on frontmatter hits). No data loss for legitimate cases.

Run against DEV after this PR merges:

    npx cds bind --exec -- node scripts/repair-author-id-phase-c.cjs
    npx cds bind --exec -- node scripts/repair-author-id-phase-c.cjs --commit

### 3. Alias `tutorial_ID as ID` on both projections

`AuthorService.MyTutorials` and `AuthorService.MyAuthoredTutorials` now
project `MyTutorialsView { *, tutorial_ID as ID }`. Both aliases coexist
on the OData wire so:
- Sage's `row.ID` reads correctly (unblocks the Sage panel).
- Existing consumers reading `row.tutorial_ID` keep working.

## Tests

- `test/unit/resolve-tutorial-author.test.js`:
  - Retitled the "empty contributors → ownerEmail falls through" case
    as a **regression guard** that asserts the resolver does NOT
    elevate ownerEmail.
  - Replaced "falls through to ownerEmail (Phase B c)" with a mirror
    assertion that the resolver returns null in that case.
  - 20/20 tests pass.

- `test/unit/author-service.test.js`:
  - New "exposes tutorial_ID also as ID for OData clients" regression
    test on `MyAuthoredTutorials`.
  - 31/31 tests pass.

- `test/hybrid/frontmatter-owner.test.js`:
  - Test 2 flipped from "fallback fill-NULL populates author_ID" to
    "no-Phase-c: author_ID stays NULL when only ownerEmail matches".
  - Test 3 updated to reflect the doubly-guarded admin-correction
    invariant.

- `test/hybrid/tutorial-author-backfill.test.js`:
  - Comment refresh only. seedTriplet already creates a
    TutorialContributors row with role='author' so Phase (a) matches;
    the test never depended on Phase (c). Comment corrected.

Note: the failing test in `admin-shell-explainer-registration.test.js`
is pre-existing on main (verified via stash+rerun) and unrelated to
this fix.

## Not in this PR

- Sage-side switch to read `row.ID` (no change needed — Sage already
  reads it; this PR makes the field non-undefined).
- Cleanup of the 58 `TutorialMeta.ownerEmail = riley.rainey@sap.com`
  rows themselves. Those are monitoring assignments; whether they're
  actually Riley's responsibility is a separate ops question.
- Fixing the same class of issue (ownership vs authorship confusion)
  in other consumers. This PR is scoped to the resolver and the two
  projections most immediately affected.

Closes #862.

Co-authored-by: Thomas Jung <12159356+jung-thomas@users.noreply.github.com>
jung-thomas added a commit that referenced this pull request Jul 9, 2026
… band + blogs bootstrap (#1116)

* fix(homepage): 4-way demo bundle — events unwrap, for-you cards, carousel band, blogs bootstrap

Bundled fixes for four homepage issues visible on DEV.

**#1 Events band — cards not rendering**
homepage-events-band/EventsBand.vue read `await resp.json()` and assigned
the entire OData envelope `{@odata.context, value:[...]}` to `rows`.
Iterating an object with v-for never produced .event-card children, so
the empty-state fallback rendered instead. Unwrap `body.value` with a
bare-array fallback so fixtures + non-OData mounts still work.

**#2 For-you row — no cards, no header, vertical stack, top gap**
The Hugo partial only emitted a screen-reader-only <h2>, the section had
no hp-band wrapper, and the Vue component emitted a plain <ul><li><a>
with zero CSS. Result: browser-default list of links between two padded
bands, and a 200px min-height reservation added visible whitespace even
when the row was hidden. This change:

- Wraps the section in `hp-band`, adds visible "Recommended for you"
  <h2> plus a "Because you're a <role> <deployment> on <cloud>"
  explainer chip populated from the personalization envelope.
- Rewrites for-you-row.vue to emit `.for-you-card` anchors with thumb,
  kind pill, title, and 2-line description.
- Adds `.for-you-cards.cards` grid CSS matching the tutorials-teaser
  and featured-carousel grammar (4 → 3 → 2 → 1 columns).
- Drops the `min-height: 200px` reservation — coordinator hides the
  section entirely when there's no data.
- mount-for-you.ts now populates the because-chip from the session-cached
  envelope profile and mounts cards into a dedicated child so the header
  survives Vue's innerHTML replace.

**#3 Featured missions carousel — no background, slow rotate**
Section used `.hp-featured-carousel` alone, missing the shared
`.hp-band` padding + background that the events, videos, and community
sections use. Added `hp-band` to the class list. Also dropped the
30-second auto-advance interval to 8 s so the demo actually shows
rotation — `prefers-reduced-motion` still pauses it for a11y.

**#4 Community Blog Posts — empty column with just a fallback link**
CommunityBlogPosts is empty in DEV: the community-blogs-fetch cron ran
successfully but the SAP Community RSS endpoints return Cloudflare 403
to CF outbound IPs, so every source erros and inserts zero rows.
JobLastRun still records LASTSUCCESSAT because the job returns instead
of throwing.

- Fail-loud: runCommunityBlogsFetch now throws when
  `errored >= sources` so the cron chassis populates LASTERRORAT +
  LASTERRORMESSAGE, and the admin Cron health tile surfaces the outage.
- DEV bootstrap: 4 admin-pinned rows seeded directly in HANA
  (`ID = 11111111-1111-1111-1111-00000000000{1..4}`,
  `adminOverride='ALLOW'`, `pinned=true`, `aiVerdict='DEVELOPER_RELEVANT'`)
  so the column renders content immediately. Not part of this diff — SQL
  applied directly to DEV HANA. The Community Blogs admin UI at
  `/admin-ui/#homepage/blog-posts` can curate them post-demo.

**Test note**
EventsBand.test.ts has 3 pre-existing failing tests (verified by stashing
this branch and running on main — same failures). Root cause is
Vue-test-utils / happy-dom timing: `onMounted → await refetch` leaves
`loading = true` after one microtask. Added a new OData-envelope
regression test in the same shape as the existing broken tests so it
covers the fix but doesn't force a separate rewrite of the whole
suite. Follow-up issue owed.

Verified live on DEV:
- Homepage HTML contains hp-band wrappers + Recommended-for-you header.
- /homepage/communityBlogs → 3 pinned rows.
- /homepage/events?region=ALL → 6 cards.

* fix(homepage): round 2 — event card visual chrome, gap, featured header

Follow-up fixes on top of the initial demo bundle (previous commit
0ee75d4). Three issues Tom flagged after the first deploy:

**Event cards — no visual chrome.** `.event-card` had CSS defined in
`hugo/assets/css/homepage/_events-band.css` but that file was hooked
in via a raw `@import` statement in `homepage.css`, which Hugo's
`resources.Get` pipeline emits as-is (no postcss). Browsers dutifully
fetched `/css/homepage/_events-band.css` and got a 404 — Hugo doesn't
publish partial CSS files as static assets. Result: 6 zero-border,
zero-background, zero-padding stacked text divs since #1030 shipped.

Fix: inlined the events-band chunk into `homepage.css` directly.
Original partial kept as source-of-truth for future edits (documented
in the inline comment). Refreshed the card look while I was there:
softer border, 8px radius, hover lift + shadow, chip-style event type
badge, meta row separated by top-border.

**Gap between verb spine and events band.** `.developer-homepage`
used `gap: 3rem` (48 px) which combined with the two hp-band's own
1.5 rem padding felt cavernous. Tightened to `gap: 2rem` (32 px).

**Featured missions header disappearing on hydration.** Vue mounts
`Carousel` directly on the `<section>` — `createApp(...).mount(el)`
replaces `el.innerHTML` wholesale, so the SSR-emitted
`<div class="hp-featured-carousel__header"><h2>Featured missions</h2>"
…</div>` was wiped on first hydration tick. Users only saw the
Vue-rendered slide title ("SAP BTP Cockpit Navigation" etc.), never
the section title.

Fix: refactored `main.ts` to mount into a dedicated
`[data-vue-root]` child element inserted after the SSR header. The
`root` prop still points at the `<section>` so
`useAutoAdvance`'s hover/focus listeners and the
`.hp-featured-carousel--pending` toggle still work as before.

Verified live on DEV after redeploy:
- event-card computed border 0.8 px solid, bg #fff, padding 16 px,
  border-radius 8 px, display flex — actual cards.
- .hp-featured-carousel__header + <h2>Featured missions</h2> present in
  mounted DOM.
- verb-spine → events-band gap = 32 px (was 48 px).

---------

Co-authored-by: Thomas Jung <12159356+jung-thomas@users.noreply.github.com>
jung-thomas added a commit that referenced this pull request Sep 17, 2026
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.
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