Skip to content

Latest commit

 

History

History
514 lines (394 loc) · 37.8 KB

testing-endpoints.md

File metadata and controls

514 lines (394 loc) · 37.8 KB

UI & API Endpoints — Testing Reference

This page lists all major endpoints exposed by the application for manual testing and smoke verification.


Public Frontend (Hugo static — via AppRouter)

URL Description Auth
/ Homepage — tutorial navigator (Vue 3 component) None
/tutorials/{slug} Individual tutorial (HTML served from HANA BLOB via CAP) None
/concepts/{slug} Knowledge-graph concept landing page (HTML served from HANA BLOB via CAP; 404 if slug not in PublishedConcepts) None
/explore/ Interactive knowledge-graph visualization (Vue+Sigma.js, CAP-rendered shell with inline graph JSON). Mobile (<768px) falls back to typed-list view. None
/app-space Event-themed tutorial space (Vue 3 AppSpace component) None
/event-display Real-time event display dashboard launcher None
/missions/{slug} Mission detail page (generated by Hugo at build time) None
/groups/{slug} Group/completion-path detail page None
/me/ Profile + Recent Activity timeline (public Hugo page; calls authenticated APIs from JS) None (page) / XSUAA (API calls)

QA Channel — Author Preview (XSUAA-gated)

The QA channel is a separate author-preview surface backed by a dedicated CAP app (tutorials-srv-qa) and HDI container (tutorials-hana-qa). All routes require the Tutorial.Author XSUAA scope.

URL Description Auth
/tutorials-qa/{slug} QA-channel tutorial preview (yellow "QA preview" banner injected) XSUAA + Tutorial.Author
/tutorials-qa/_nav.json Navigation metadata for QA-channel tutorials XSUAA + Tutorial.Author
/tutorials-qa/search/... QA-channel search (rewrites to /qa/search/...) XSUAA + Tutorial.Author
/qa-search/... Direct QA-search alias (rewrites to /search/...) XSUAA + Tutorial.Author

Bootstrap: the role collection "Tutorials Author" must be assigned via BTP cockpit; users must log out / back in for the new scope to appear in their JWT. Until then, /tutorials-qa/* returns 403.


Display App (Vue 3 — Real-time Event Dashboard)

URL Description Auth
/display-app/ Standalone event dashboard (rotating views: Board, Statistics, Leaderboard) XSUAA
/display-app/index.html Direct entry point XSUAA

The display-app is a standalone Vue 3 + Vite application deployed as static files at approuter/static/display-app/. It connects via Socket.IO on the /ws/display namespace for real-time updates. Used on big monitors at SAP events.


Admin UI (SAPUI5 — Fiori Elements)

All admin routes require the Admin XSUAA scope.

URL Description Auth
/admin-ui/ Admin shell — sap.tnt.ToolPage with side navigation XSUAA + Admin
/admin-ui/components/events/ Events management (Fiori Elements) XSUAA + Admin
/admin-ui/components/missions/ Missions management XSUAA + Admin
/admin-ui/components/groups/ Groups / completion paths management XSUAA + Admin
/admin-ui/components/tutorials/ Tutorials management XSUAA + Admin
/admin-ui/components/tags/ Tags management XSUAA + Admin
/admin-ui/components/accomplishments/ Accomplishments management XSUAA + Admin
/admin-ui/components/prizes/ Prizes management XSUAA + Admin
/admin-ui/components/operations/ Operations dashboard XSUAA + Admin
/admin-ui/components/accounts/ User accounts management XSUAA + Admin
/admin-ui/components/changelog/ Change tracking log XSUAA + Admin

Drawings UI (Vue 3 — Prize Drawing)

All drawings routes require the Admin XSUAA scope.

URL Description Auth
/drawings-ui/ Standalone event drawing app — operator selects event, draws random winner from eligible prize claimants, reveals winner name and email XSUAA + Admin

The drawings-ui is a Vue 3 application deployed as static files at approuter/static/drawings-ui/. It calls the DrawingService at /drawings to list events with claimed prizes, query the eligible winner pool, and perform the random winner selection. Winner contact information (displayName, email) is returned directly and gated by the Admin scope due to PII exposure.


Analytics Explorer (Vue 3 + Monaco — Ad-hoc Analytics)

URL Description Auth
/analytics-ui/ Vue 3 SPA with entity browser + SQL editor over AnalyticsService XSUAA + Admin

Backed by AnalyticsService at /admin/analytics. SQL tab uses Monaco (lazy-loaded); queries pass through srv/lib/analytics-sql-validator.cjs (SELECT-only, allowlisted tables, wrapped with LIMIT 5001). Entity browser is gated by @analytics.exposed annotations on CDS views/entities.


Scanner Apps (Badge Scanner — Event Tool)

All scanner routes require the MobileApp XSUAA scope.

URL Description Auth
/scanner-ui/ Badge Scanner — UI5 implementation (sap.ndc.BarcodeScanner) XSUAA + MobileApp
/scanner-vue/ Badge Scanner — Vue 3 implementation (native BarcodeDetector API) XSUAA + MobileApp

Both scanner apps provide the same functionality: scan a contestant badge QR code, display progress stats, and claim prizes. The Vue version is mobile-optimized with camera-first UX and SAP Fundamental Styles. Falls back to manual JSON input on browsers without BarcodeDetector support.

URL Method Description Auth
/scanner/getContestant(accountNumber='...') GET OData function — contestant stats + prize info XSUAA
/scanner/claimPrize(recordId='...') GET OData function — mark prize as CLAIMED XSUAA

CAP Server Index & Swagger UI

These are only accessible when hitting the CAP backend directly (http://localhost:4004), not through the AppRouter.

URL Description Auth
http://localhost:4004/ CAP index page — lists all services, entities, and links None
/api/$api-docs/ Swagger UI for DeveloperService None
/admin/$api-docs/ Swagger UI for AdminService None
/admin/analytics/$api-docs/ Swagger UI for AnalyticsService None
/admin/exports/$api-docs/ Swagger UI for ExportsService None
/display/$api-docs/ Swagger UI for DisplayService None
/api/v1/$api-docs/ Swagger UI for ConsolidationService None
/search/$api-docs/ Swagger UI for SearchService None
/chat/$api-docs/ Swagger UI for ChatService (no entities; ORD-symmetric) None

Note: The CAP index page is not visible through the AppRouter because the catch-all route serves Hugo static content at /. Access it directly at http://localhost:4004/ during development.

Exposed via /_dev prefix (DEV/QA only)

When EXPOSE_CAP_UI=true is set on the CAP srv app, these are accessible through the AppRouter with XSUAA + Admin scope:

URL Description Auth
/_dev CAP index page (lists all services) XSUAA + Admin
/_dev/api/$api-docs/ Swagger UI for DeveloperService XSUAA + Admin
/_dev/admin/$api-docs/ Swagger UI for AdminService XSUAA + Admin
/_dev/admin/analytics/$api-docs/ Swagger UI for AnalyticsService XSUAA + Admin
/_dev/admin/exports/$api-docs/ Swagger UI for ExportsService XSUAA + Admin
/_dev/display/$api-docs/ Swagger UI for DisplayService XSUAA + Admin
/_dev/api/v1/$api-docs/ Swagger UI for ConsolidationService XSUAA + Admin
/_dev/search/$api-docs/ Swagger UI for SearchService XSUAA + Admin
/_dev/chat/$api-docs/ Swagger UI for ChatService XSUAA + Admin

Enable: cf set-env tutorials-srv EXPOSE_CAP_UI true && cf restart tutorials-srv Disable: cf unset-env tutorials-srv EXPOSE_CAP_UI && cf restart tutorials-srv Do NOT set this on production.


OData Services (CAP backend)

URL Description Auth
/api/$metadata DeveloperService OData metadata (EDMX) XSUAA
/api/ DeveloperService — progress tracking, user-facing API XSUAA
/admin/$metadata AdminService OData metadata XSUAA + Admin
/admin/ AdminService — full CRUD for events, missions, etc. XSUAA + Admin
/admin/analytics/$metadata AnalyticsService OData metadata XSUAA + Admin
/admin/analytics/ AnalyticsService — @analytics.exposed entity surface + runSelectQuery action XSUAA + Admin
/admin/exports/$metadata ExportsService OData metadata XSUAA + Admin
/admin/exports/ ExportsService — exportLegacyData(format) action XSUAA + Admin
/display/$metadata DisplayService OData metadata XSUAA
/display/ DisplayService — read-only data for event dashboards XSUAA
/drawings/$metadata DrawingService OData metadata XSUAA + Admin
/drawings/ DrawingService — prize drawing functions (events, winner pool, random selection) XSUAA + Admin
/api/v1/$metadata ConsolidationService (v1 compat) OData metadata XSUAA
/api/v1/ ConsolidationService — legacy API compatibility XSUAA
/search/$metadata SearchService OData metadata None
/search/ SearchService — tutorial/mission search (word-boundary matching) None
/chat/$metadata ChatService OData metadata (no entities; ORD-symmetric only) XSUAA

Custom REST Endpoints (non-OData)

URL Method Description Auth
/health GET Liveness check — returns { status: "ok" } None
/health/db GET DB connectivity check (runs SELECT 1 FROM DUMMY) None
/auth/user GET Current user profile (id, email, name) XSUAA
/api/qrcode?url=... GET QR code PNG generation XSUAA
/api/recommendations GET Personalized "what's next" recommendations (embedding centroid + co-completion blend) XSUAA
/build/catalog GET Mission/group/tutorial catalog (JSON, for build pipeline) None
/build/co-completions GET Co-completion graph data (used by recommendations) None
/build/navigator GET Missions, groups (incl. standalone), tutorial→mission/group mappings, and checkpoint milestones None
/build/slug-mapping GET Slug→ID mapping for all missions/groups None
/build/repo-catalog GET Slug-keyed DiscoveredTutorial map (third-tier discovery fallback) None
/build/repo-catalog POST Write the discovered-tutorial baseline (CI-as-canonical-writer) Bearer (CONTENT_API_KEY)
/build/concepts GET Published Knowledge-Graph concepts (slug, name, description, teaches/requires/requiredBy/relatedTo). Shares buildConceptsPayload with the CAP concept-render pipeline (GET /content/concepts-index list page + the POST /content/publish/render-concepts detail phase, #1327). None
/graph/publishConcept POST Admin action — sets publishedAt + publishedBy on a Concept XSUAA + KnowledgeGraph.Admin
/graph/unpublishConcept POST Admin action — clears publishedAt + publishedBy on a Concept XSUAA + KnowledgeGraph.Admin
/graph/explore-data GET Bulk graph JSON for the /explore/ page (nodes + edges + generatedAt; 5-min LRU cache) None
/graph/path GET Shortest path between two tutorial/concept slugs (?from=<slug>&to=<slug>). 400 on same-slug. Extracted from Phase 2 Joule tool. None
/api/advocates GET Public list of active developer advocates (sorted by lastName, topics + links denormalized, ETag + max-age=60, stale-while-revalidate=600) None
/api/advocates/:slug/photo[?size=thumb] GET Photo bytes (256-WebP default, 64-WebP with ?size=thumb) served from HANA AdvocatePhotos. ETag is the sha256, max-age=86400. 404 when the advocate has no photo or slug is unknown. None
/feedback/submit POST Tutorial feedback form (rate-limited; submitter IP hashed via SUBMISSION_SALT_SECRET) None
/chat/stream POST Joule chat streaming endpoint (Server-Sent Events) XSUAA
/a2a POST A2A JSON-RPC 2.0 endpoint (message/send, message/stream, tasks/get, tasks/cancel) for central Joule consumption (#1220); machine-to-machine. Skill via metadata.skillId, defaults to conversational tutorial-chat. Enable/config via /admin-ui/#joule (ChatSettings a2aEnabled). Agent card at /.well-known/agent-card.json. IAS (client-credentials)
/.well-known/agent-card.json GET A2A Agent Card — public discovery document (5 skills, streaming, IAS security scheme). Base URL + token URL from /admin-ui/#joule (ChatSettings). None
/.well-known/a2a-instructions.md GET A2A consumption guide (how to authenticate + call) None
/.well-known/tutorial-provenance/jwks.json GET Ed25519 JWKS for verifying tutorial provenance JWS tokens ({ keys: [...] }). Returns 404 when PROVENANCE_ENVELOPE_ENABLED flag is OFF. Anonymous Express route — @requires not applicable. Pair: /content/tutorials/:slug/provenance. None
/api/codecheck POST AI code-check spike (issue #171, gated on ChatSettings.codeCheckEnabled). Body: { tutorialSlug, stepNumber, submittedCode, language? }. Returns { verdict: 'pass'|'partial'|'fail', summary, suggestions[], correctAspects[] }. 503 when flag off; 429 with Retry-After on per-user 30/hr or per-(user,slug,step) 5/5min cap. XSUAA
/author/generateOsVariants POST AI-assisted OS variant generation for the VS Code authoring plugin (issue #173). Body: { sourceMarkdown, sourceOS, targetOSes[], context? }. Returns { variants[], model, tokensUsed, requestId }. 60/hr per author. See spec #173 §5. XSUAA + Tutorial.Author
/admin/embeddings/stats GET Tutorial embedding coverage / drift statistics XSUAA + Admin
/drawings/listEvents() GET OData function — events with ≥1 claimed prize (newest-first). Returns { legacyId, name, startDate }. XSUAA + Admin
/drawings/pool(eventId=<legacyId>) GET OData function — eligible winner pool statistics for an event. Returns { eventId, eligibleCount } (count of distinct non-anonymized users with CLAIMED prize claims, excludes PII). XSUAA + Admin
/drawings/draw(eventId=<legacyId>) GET OData function — select one random winner and return their contact info. Returns { winner: { displayName, email } | null, eligibleCount }. Server-side winner selection; returns PII. XSUAA + Admin
/api/alerts GET Active alerts with audience=ALL. 60 s cache. None
/api/alerts/me GET ALL + AUTHENTICATED + ADMIN (if admin). 30 s private cache. XSUAA
/api/homepage/events GET 3-4 upcoming events (merged from DB + events calendar). 60 s cache. None
/api/homepage/videos GET @sapdevs video feed (Developer News + recent uploads). 15-min cache; requires YOUTUBE_API_KEY. None
/api/homepage/communityBlogs GET SAP Community blog RSS feed (latest posts). 30-min cache. None
/api/homepage/news GET SAP News headlines RSS feed. 30-min cache. None
/api/homepage/shelves?verb=<v> GET All active HomepageShelves entries for one verb (LEARN|BUILD|INTEGRATE|OPERATE|AI|CONNECT). 5-min cache. None
/api/homepage/redirectsActive GET Active LegacyRedirects rows (approuter-only consumer; refreshes hourly). None
/api/homepage/recordRedirectHits POST Idempotent batch hit counter for legacy redirects (approuter-only writer). Body: { hits: [{ id, count }] }. None
/build/homepage-shelves GET All HomepageShelves entries keyed by verb (Hugo build-time only; bakes hugo/data/homepage_shelves.json). Payload includes tagline / whyItMatters / authoringStatus (#759). None
/build/verb-definitions GET All VerbDefinitions rows (6 verbs) — labels, icons, taglines, "why it matters" copy for the homepage verb-spine flip tiles (#759). Bakes hugo/data/verb_definitions.json. None
/build/shelf-definitions GET All ShelfDefinitions rows (4 shelf categories: LEARN / START / REFERENCE / COMMUNITY) — taglines + "why it matters" copy for verb sub-page shelf headers (#759). Bakes hugo/data/shelf_definitions.json. None

/build/navigator returns a shape with missions[], groups[] (incl. standalone published Groups), tutorialMappings[], and checkpointMappings[] (milestone markers). The 5-minute in-memory cache is automatically invalidated when the Admin UI saves changes to Missions, Groups, or CompletionPath entities — no ?nocache=1 needed. Implementation: srv/lib/navigator-catalog.js.

/feedback/submit returns 503 when SUBMISSION_SALT_SECRET is missing — set it in CI secrets and locally before testing the form. The request body is capped at 8 KB.


Content Persistence Endpoints

URL Method Description Auth
/content/tutorials/{slug} GET Serve tutorial HTML from HANA (ETag, Cache-Control) None
/content/tutorials/{slug}/provenance GET Signed provenance JWS for a tutorial ({ jws, jwks_url }). jws is a compact Ed25519-signed JWS attesting contentHash, sourceCommit, builtAt, and freshness (confidence: high/medium/low/unknown). Returns 404 when PROVENANCE_ENVELOPE_ENABLED flag is OFF or slug unknown. Anonymous Express route — @requires not applicable. Pair: /.well-known/tutorial-provenance/jwks.json. Requires PROVENANCE_SIGNING_KEY credstore secret (Ed25519 PKCS8 PEM). None
/content/hashes GET SHA-256 map of active content ({ slug: hash }) None
/content/nav GET Navigation metadata for published tutorials None
/content/publish POST Deprecated — single-shot publish (base64-gzipped files). Kept for one release cycle; new clients use the chunked protocol below. Bearer (CONTENT_API_KEY)
/content/publish/begin POST Open a chunked publish session. Returns { sessionId, version, expiresAt } (201) or 409 if a publish is already in progress. Bearer (CONTENT_API_KEY)
/content/publish/append POST Append a batch of files to an open session. Files shape: { slug: base64gzip }. Idempotent for (sessionId, slug). Bearer (CONTENT_API_KEY)
/content/publish/commit POST Activate the session's manifest. Idempotent (returns alreadyActive: true on repeat). Bearer (CONTENT_API_KEY)
/content/publish/abort POST Discard an open session. Idempotent. Bearer (CONTENT_API_KEY)
/content/code-check-specs POST AI code-check spike (issue #171). Upserts CodeCheckSpecs rows from *.codecheck.json sidecars produced by fetch-tutorials. Body: { specs: [{ slug, stepNumber, goal, language?, hints?, referenceSolution? }] }. Carry-forward semantics (no DELETE on absent specs). Server-only — referenceSolution never reaches the client. Bearer (CONTENT_API_KEY)
/content/rollback POST Revert to previous manifest version Bearer (CONTENT_API_KEY)

Chunked publish protocol

The chunked endpoints replace single-shot POST /content/publish. They split a publish across many small batches so a flaky TCP connection or a 53 MB JSON body doesn't kill the whole run, and the server's commit step does carry-forward of unchanged slugs (so a partial payload no longer drops the rest of the catalog).

Typical client flow:

# 1. Begin a session — server returns sessionId + version
curl -sX POST http://localhost:4004/content/publish/begin \
  -H "Authorization: Bearer $CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "trigger": "manual", "hugoVersion": "0.x" }'
# → 201 { "sessionId": "...", "version": 42, "expiresAt": "..." }

# 2. Append batches (50 slugs at a time by default; idempotent per (sessionId, slug))
curl -sX POST http://localhost:4004/content/publish/append \
  -H "Authorization: Bearer $CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sessionId": "...", "files": { "slug-a": "<base64gzip>", "slug-b": "<base64gzip>" } }'

# 3. Commit — activates the manifest; idempotent (returns alreadyActive: true on repeat)
curl -sX POST http://localhost:4004/content/publish/commit \
  -H "Authorization: Bearer $CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sessionId": "..." }'

# Or, abandon the session:
curl -sX POST http://localhost:4004/content/publish/abort \
  -H "Authorization: Bearer $CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sessionId": "..." }'

begin returns 409 if another publish session is already in progress; abort it (or wait for the reaper, which sweeps stale sessions every 5 minutes after a 30-minute idle threshold) before retrying.


Hosted MCP Server

The CAP backend serves a hosted Model Context Protocol surface via the @cap-js/mcp adapter (tools-only) plus a custom compose router (srv/lib/mcp-compose-router.js) that adds resources + prompts on the RP-bearing mounts. Each CDS service is mounted separately at /mcp/<svc> — there is no aggregate /mcp root. End-user connection guide: docs/end-users/mcp-quickstart.md; parameter reference: docs/developers/reference/mcp-server.md.

Per-service mounts

Mount Service Curated tools Auth (at mount)
/mcp/search SearchService search_tutorials, list_missions, get_mission, get_tutorial None (anonymous)
/mcp/graph KnowledgeGraphService kg_shared_concepts, kg_neighborhood, kg_search_concepts, kg_community (+ resources + prompts) None (anonymous)
/mcp/homepage HomepageService get_my_recommended_tutorials, get_my_recommended_missions authenticated-user
/mcp/api DeveloperService get_my_tutorials, get_my_missions, get_my_events, get_my_completed_steps, get_tutorial_step, complete_step, reset_tutorial_progress authenticated-user
/mcp/admin AdminService merge_concepts, promote_community_to_mission, trigger_rebuild, publish_content (+ resources + prompts) Admin + per-action scope

describe and query are auto-generated on every mount by @cap-js/mcp. Each service also serves resources (tutorial://<slug>, mission://<slug>, concept://<id>) and prompts (prompts/list) where the compose router is mounted (/mcp/graph, /mcp/admin).

Auth-tier route prefixes (approuter → srv rewrite)

The approuter (approuter/xs-app.json) fronts three additional prefixes that srv/server.js rewrites onto the real /mcp/* mounts:

Public prefix Approuter auth Rewrites to Purpose
/mcp/* none (csrf off) (direct) Anonymous read tier — public services
/mcp-pat/* none (csrf off) /mcp/* PAT tier — requires Bearer pat_... (else JSON-RPC 401)
/mcp-auth/* xsuaa, scope Tutorial.MCP /mcp/* OAuth/JWT tier — forwards the user JWT
/mcp-admin/* xsuaa, scope Tutorial.MCP /mcp/admin/* Phase 3 admin-curation tools

Kill switches: MCP_AUTH_ENABLED=false → /mcp-auth/* + /mcp-pat/* return 503. MCP_PHASE3_ENABLED=false or MCP_ADMIN_TOOLS_ENABLED=false → /mcp-admin/* returns 503. Transport is Streamable HTTP (MCP protocol 2025-06); clients negotiate JSON vs SSE via Accept.

Personal Access Tokens (PAT)

URL Method Description Auth
/pats GET PatService — list the caller's own PATs (MyPATs, row-scoped to user.email = $user.id) XSUAA (authenticated-user)
/pats/mintPAT POST Mint a PAT: mintPAT(name, scopes, ttlDays) → { ID, token, prefix, expiresAt }. Plaintext token returned once; server stores only a SHA-256 hash. Scopes: read (read tools) / write (allows complete_step, reset_tutorial_progress). XSUAA (authenticated-user)
/pats/MyPATs(<ID>)/PatService.revokePAT POST Bound action — revoke one of the caller's PATs XSUAA (authenticated-user)

PATs are recognized by srv/lib/mcp-pat-middleware.js (SHA-256 hash lookup, 60 s cache) on the /mcp-pat/* prefix. Minting is surfaced in the Admin UI at /admin-ui/#pats for users in the Tutorials MCP Users role collection. An expired or revoked PAT returns 401. Write tools reject PATs lacking the write scope with 403.


WebSocket / Real-time

URL Protocol Description Auth
/ws/event-stream Socket.IO Anonymous live tutorial-completion stream (backed by EventStreamService) None
/ws/display Socket.IO Display dashboard live updates (backed by DisplayService) XSUAA DisplayApp (enforced at namespace join, not approuter)
/socket.io/ Socket.IO Underlying transport endpoint for both namespaces (?EIO=4&transport=websocket) None

Joule Chat Tools

Tools registered with the Joule chat runtime (LLM tool-calling surface, not standalone HTTP endpoints). Reachable via POST /chat/stream. Each tool's registration is gated on a ChatSettings flag so the LLM only sees the tool when the feature is enabled.

getBranchRecommendation

Author-aware branch + skip recommender for tutorials and missions (issue #172 PR 4).

  • Registration gate — registered when ChatSettings.enabled = true && ChatSettings.branchingEnabled = true. When branchingEnabled = false, the tool is not registered and the LLM falls back to general guidance.

  • Params — missionSlug? (string), tutorialSlug? (string), branchPointId? (string). At least one of missionSlug / tutorialSlug is required. branchPointId requires tutorialSlug.

  • Return shape:

    {
      "branchPoints": [
        { "id": "...", "picked": "...", "reason": "...", "confidence": 0.0,
          "allBranches": [{ "key": "...", "label": "..." }] }
      ],
      "altGroups": [
        { "id": "...", "groupKey": "...", "picked": "...", "reason": "...", "confidence": 0.0,
          "allBranches": [{ "key": "...", "label": "..." }] }
      ],
      "skipPoints": [
        { "stepNumber": 4, "skip": true, "reason": "...", "skipLabel": "...", "skipReason": "..." }
      ],
      "note": "...",   // empty-shape signal (see below)
      "error": "..."   // validation failure (see below)
    }
  • Telemetry — writes one BranchDecisions row per branch / alt-group / skip recommendation with source: 'jouleTool'. Skip-point telemetry is only emitted when skip === true.

  • Error envelopes (validation failures, no telemetry):

    • error: 'requires_at_least_one_of: missionSlug, tutorialSlug, branchPointId'
    • error: 'branchPointId requires tutorialSlug'
    • error: 'unknown_branch_point: <id>'
  • Empty-shape envelopes (not errors, no recommendation possible):

    • note: 'tutorial_has_no_branches'
    • note: 'mission_not_found'
    • note: 'mission_has_no_alt_groups'

Implementation: srv/lib/branch/joule-tool.js. Telemetry helper: srv/lib/branch/branch-telemetry.js.

findLearningPath

Hybrid pathBetween Joule tool — translates "I want to learn X" / "what should I learn next" / "show me a path to Y" prompts into an ordered tutorial sequence (issue #445 / Phase 2 of #381).

  • Registration gate — registered when ChatSettings.enabled = true && ChatSettings.kgPathBetweenEnabled = true. When kgPathBetweenEnabled = false (default), the tool is not registered and the LLM falls back to general guidance via searchTutorials.

  • Params — toSlug (string, required, lowercase alphanumeric + hyphens, 1-80 chars), fromSlug? (string, optional — same shape). If fromSlug is omitted, the handler infers it from the user's most-recent COMPLETED TaskRecord; if no completion history, the search is unanchored (uses toSlug as its own neighborhood center).

  • Return shape — rendered markdown string that the LLM paraphrases or quotes verbatim:

    Here's a path from `<fromSlug>` to `<toSlug>`:
    
    1. **<title>** — [<slug>](https://developers.sap.com/tutorials/<slug>.html)
       ~<minutes> min · <reason>

    Where <reason> is one of "Prerequisite chain", "Often completed together", "Shares concepts". Empty result set returns a friendly "couldn't find a path" message pointing at the catalog.

  • Telemetry — emits kg.joule.path_requested at dispatch start ({ fromSlug, toSlug, hasUserId, fromSlugInferred, unanchored }) and kg.joule.path_returned at dispatch end ({ ..., resultCount, pathTypeBreakdown: { PREREQ, CO_COMPLETED, SHARED_CONCEPT }, latencyMs, exactTargetReached, error? }). The pathTypeBreakdown lets ops see which arm produces results post-rollout — useful for validating the Phase 2.5 prereq-enrichment hypothesis.

  • Error envelopes — handler returns friendly strings for the LLM to paraphrase (never throws into the LLM-tool-result):

    • Malformed toSlug / fromSlug — "That tutorial slug doesn't look right…"
    • SparqlTimeoutError — "I couldn't find a learning path right now — the query timed out…" Telemetry tag: error: 'timeout'.
    • SparqlSyntaxError / generic — "Internal error finding a learning path…" Telemetry tag: error: 'syntax' or error: 'unknown'.
  • AI-judge fixture — test/hybrid/joule-tool-pick-find-path.test.js — 12-prompt fixture asserting the LLM picks the right tool (findLearningPath vs getRelevantSteps vs checkCode vs no-tool) at ≥90% accuracy. Gated by HYBRID_AI_TESTS=true. Regression guard against tool-descriptor changes.

Implementation: srv/lib/kg/joule-tool-find-path.js + srv/lib/kg/concepts-for-user.js (user-coverage helper). Architecture details: docs/developers/architecture/joule.md.


Approuter-only Endpoints

URL Method Description Auth
/login GET Triggers XSUAA login redirect XSUAA
/login/callback GET OAuth2 callback (handled by AppRouter) XSUAA
/admin/rebuild POST Hot-reload static content (tar.gz upload) Bearer (REBUILD_API_KEY)

XSUAA Scopes Reference

The application defines its roles in xs-security.json. Endpoints in this document indicate which scope is required where applicable.

Scope Role Collection Used by
$XSAPPNAME.Admin "Tutorials Admin" /admin-ui/, /analytics-ui/, /_dev, /admin/* OData, /admin/embeddings/stats
$XSAPPNAME.SuperAdmin "Tutorials SuperAdmin" promote_community_to_mission, publish_content (emergency), other SuperAdmin-gated admin actions
$XSAPPNAME.MobileApp "Tutorials Scanner" /scanner-ui/, /scanner-vue/
$XSAPPNAME.Tutorial.Author "Tutorials Author" /tutorials-qa/*, /qa-search/*, /author/generateOsVariants, trigger_rebuild
$XSAPPNAME.KnowledgeGraph.Admin "Tutorials Admin" /graph/publishConcept, /graph/unpublishConcept, merge_concepts
$XSAPPNAME.Tutorial.API "Tutorials API Consumer" /graphql (authenticated GraphQL over DeveloperService)
$XSAPPNAME.Tutorial.MCP "Tutorials MCP Users" /mcp-auth/*, /mcp-admin/* (hosted MCP OAuth/JWT tiers); PAT minting at /admin-ui/#pats
(authenticated-user) (any logged-in user) /api/*, /display/*, /chat/*, /pats, /mcp-pat/*

Local Development Notes

  • AppRouter: http://localhost:5000 (runs approuter/server.js)
  • CAP backend: http://localhost:4004 (runs cds watch)
  • Hugo dev server: http://localhost:1313 (runs npm run dev)

When running locally, the approuter proxies all API calls to CAP_BASE_URL (default http://localhost:4004) and injects mock Basic auth:

  • /admin/* → admin:admin
  • /display/* → display:display
  • All others → developer:developer

Quick Smoke Test Checklist

# 1. Health
curl http://localhost:4004/health
curl http://localhost:4004/health/db

# 2. Public data (build pipeline)
curl http://localhost:4004/build/catalog       | jq '.missions | length'
curl http://localhost:4004/build/navigator     | jq '.tutorials | length'
curl http://localhost:4004/build/co-completions | jq '. | length'
curl http://localhost:4004/build/repo-catalog  | jq '. | length'

# 3. OData metadata
curl http://localhost:4004/api/\$metadata             -H "Accept: application/xml"
curl http://localhost:4004/admin/\$metadata           -H "Accept: application/xml"
curl http://localhost:4004/admin/analytics/\$metadata -H "Accept: application/xml"
curl http://localhost:4004/admin/exports/\$metadata   -H "Accept: application/xml"

# 4. Content serving
curl http://localhost:4004/content/hashes | jq 'keys | length'
curl -I http://localhost:4004/content/tutorials/abap-dev-get-started

# 5. Search (unauthenticated)
curl "http://localhost:4004/search/Tutorials?\$search=abap" | jq '.value | length'

# 6. Auth (via approuter)
curl http://localhost:5000/auth/user  # expect 401 or user JSON

# 7. UIs (via approuter — open in browser)
open http://localhost:5000/admin-ui/      # ToolPage shell  (Admin scope)
open http://localhost:5000/analytics-ui/  # SQL + entity browser (Admin scope)
open http://localhost:5000/scanner-vue/   # Badge scanner (MobileApp scope)
open http://localhost:5000/me/            # Profile + Recent Activity timeline

Security Testing Reference

Focused pen-test suite added in #797. All framework defaults (CAP CSRF auto-enforcement, Fiori Elements token prefetch, sanitize-html, approuter CSP) cover the primary attack surface — these tests pin the coverage against regression.

File Type Scope
test/unit/srv/analytics-sql-validator.pen.test.js unit Malicious SQL against AnalyticsService.runSelectQuery validator (DDL/DML, stacked queries, comment bypasses, disallowed tables, oversize input). Covers srv/lib/analytics-sql-validator.cjs.
test/unit/scripts/sanitize-html.pen.test.js unit OWASP XSS Filter Evasion Cheat Sheet payloads against tutorial-source sanitizer. Covers scripts/parsers/sanitize-html.ts.
test/unit/scripts/check-hugo-safe-html.test.js unit Unit coverage for the Hugo safeHTML grep guard (marker recognition, 3-line window, allow/deny cases). Covers scripts/check-hugo-safe-html.cjs.
test/smoke/csrf-enforcement.test.js smoke POST to /admin/Tags, /admin/Missions, /api/completeStep without a valid x-csrf-token → 4xx. Covers the CAP OData mutation surface.
test/smoke/express-route-mutations.test.js smoke Sweeps all 10 bearer-token-protected app.post(...) routes in srv/server.js (/content/publish, /content/rollback, /content/code-check-specs, /content/validate-answer-specs, and the 6 build endpoints), plus XSUAA-scoped and public-POST hardening.
test/smoke/xss-reflection.test.js smoke Read-only reflection check: probes /homepage/ and /tutorials/tutorial-platform-feature-cookbook for unescaped payloads, /search/?q=<payload> for reflected query strings, and /tutorials/<payload>/ for reflection on the 404 path.
test/smoke/security-headers.test.js smoke CSP, X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy on approuter HTML responses.
scripts/check-hugo-safe-html.cjs build guard Fails the build if any new safeHTML / safeHTMLAttr / printf "<%s>" lands in hugo/layouts/**/*.{html,xml,xsl} without a <!-- security-reviewed: ... --> marker within 3 lines above. Runs as npm run check:security-annotations (wired into prebuild).

Running locally:

# All unit pen tests (fast, no external deps)
npx vitest run test/unit/srv/analytics-sql-validator.pen.test.js \
              test/unit/scripts/sanitize-html.pen.test.js \
              test/unit/scripts/check-hugo-safe-html.test.js \
              --project unit

# All smoke pen tests (needs deployed DEV; SRV and/or approuter URLs)
SMOKE_BASE_URL="https://tutorial-system-dev-tutorials-approuter.cfapps.eu10-005.hana.ondemand.com" \
SMOKE_SRV_URL="https://tutorial-system-dev-tutorials-srv.cfapps.eu10-005.hana.ondemand.com" \
  npx vitest run test/smoke/csrf-enforcement.test.js \
                 test/smoke/express-route-mutations.test.js \
                 test/smoke/xss-reflection.test.js \
                 test/smoke/security-headers.test.js \
                 --project smoke

# Hugo safeHTML guard
npm run check:security-annotations

Policy for new findings: if a pen test uncovers a real vulnerability (not a test-expectation mismatch), file a follow-up issue referencing #797, skip that individual case with a .skip and a // TODO(#XXX) comment, and address it in a dedicated PR. This suite is a regression net, not a fix-it-all PR.