Skip to content

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) ​

URLDescriptionAuth
/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-spaceEvent-themed tutorial space (Vue 3 AppSpace component)None
/event-displayReal-time event display dashboard launcherNone
/missions/{slug}Mission detail page (generated by Hugo at build time)None
/groups/{slug}Group/completion-path detail pageNone
/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.

URLDescriptionAuth
/tutorials-qa/{slug}QA-channel tutorial preview (yellow "QA preview" banner injected)XSUAA + Tutorial.Author
/tutorials-qa/_nav.jsonNavigation metadata for QA-channel tutorialsXSUAA + 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) ​

URLDescriptionAuth
/display-app/Standalone event dashboard (rotating views: Board, Statistics, Leaderboard)XSUAA
/display-app/index.htmlDirect entry pointXSUAA

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.

URLDescriptionAuth
/admin-ui/Admin shell β€” sap.tnt.ToolPage with side navigationXSUAA + Admin
/admin-ui/components/events/Events management (Fiori Elements)XSUAA + Admin
/admin-ui/components/missions/Missions managementXSUAA + Admin
/admin-ui/components/groups/Groups / completion paths managementXSUAA + Admin
/admin-ui/components/tutorials/Tutorials managementXSUAA + Admin
/admin-ui/components/tags/Tags managementXSUAA + Admin
/admin-ui/components/accomplishments/Accomplishments managementXSUAA + Admin
/admin-ui/components/prizes/Prizes managementXSUAA + Admin
/admin-ui/components/operations/Operations dashboardXSUAA + Admin
/admin-ui/components/accounts/User accounts managementXSUAA + Admin
/admin-ui/components/changelog/Change tracking logXSUAA + Admin

Drawings UI (Vue 3 β€” Prize Drawing) ​

All drawings routes require the Admin XSUAA scope.

URLDescriptionAuth
/drawings-ui/Standalone event drawing app β€” operator selects event, draws random winner from eligible prize claimants, reveals winner name and emailXSUAA + 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) ​

URLDescriptionAuth
/analytics-ui/Vue 3 SPA with entity browser + SQL editor over AnalyticsServiceXSUAA + 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.

URLDescriptionAuth
/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.

URLMethodDescriptionAuth
/scanner/getContestant(accountNumber='...')GETOData function β€” contestant stats + prize infoXSUAA
/scanner/claimPrize(recordId='...')GETOData function β€” mark prize as CLAIMEDXSUAA

CAP Server Index & Swagger UI ​

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

URLDescriptionAuth
http://localhost:4004/CAP index page β€” lists all services, entities, and linksNone
/api/$api-docs/Swagger UI for DeveloperServiceNone
/admin/$api-docs/Swagger UI for AdminServiceNone
/admin/analytics/$api-docs/Swagger UI for AnalyticsServiceNone
/admin/exports/$api-docs/Swagger UI for ExportsServiceNone
/display/$api-docs/Swagger UI for DisplayServiceNone
/api/v1/$api-docs/Swagger UI for ConsolidationServiceNone
/search/$api-docs/Swagger UI for SearchServiceNone
/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:

URLDescriptionAuth
/_devCAP index page (lists all services)XSUAA + Admin
/_dev/api/$api-docs/Swagger UI for DeveloperServiceXSUAA + Admin
/_dev/admin/$api-docs/Swagger UI for AdminServiceXSUAA + Admin
/_dev/admin/analytics/$api-docs/Swagger UI for AnalyticsServiceXSUAA + Admin
/_dev/admin/exports/$api-docs/Swagger UI for ExportsServiceXSUAA + Admin
/_dev/display/$api-docs/Swagger UI for DisplayServiceXSUAA + Admin
/_dev/api/v1/$api-docs/Swagger UI for ConsolidationServiceXSUAA + Admin
/_dev/search/$api-docs/Swagger UI for SearchServiceXSUAA + Admin
/_dev/chat/$api-docs/Swagger UI for ChatServiceXSUAA + Admin

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


OData Services (CAP backend) ​

URLDescriptionAuth
/api/$metadataDeveloperService OData metadata (EDMX)XSUAA
/api/DeveloperService β€” progress tracking, user-facing APIXSUAA
/admin/$metadataAdminService OData metadataXSUAA + Admin
/admin/AdminService β€” full CRUD for events, missions, etc.XSUAA + Admin
/admin/analytics/$metadataAnalyticsService OData metadataXSUAA + Admin
/admin/analytics/AnalyticsService β€” @analytics.exposed entity surface + runSelectQuery actionXSUAA + Admin
/admin/exports/$metadataExportsService OData metadataXSUAA + Admin
/admin/exports/ExportsService β€” exportLegacyData(format) actionXSUAA + Admin
/display/$metadataDisplayService OData metadataXSUAA
/display/DisplayService β€” read-only data for event dashboardsXSUAA
/drawings/$metadataDrawingService OData metadataXSUAA + Admin
/drawings/DrawingService β€” prize drawing functions (events, winner pool, random selection)XSUAA + Admin
/api/v1/$metadataConsolidationService (v1 compat) OData metadataXSUAA
/api/v1/ConsolidationService β€” legacy API compatibilityXSUAA
/search/$metadataSearchService OData metadataNone
/search/SearchService β€” tutorial/mission search (word-boundary matching)None
/chat/$metadataChatService OData metadata (no entities; ORD-symmetric only)XSUAA

Custom REST Endpoints (non-OData) ​

URLMethodDescriptionAuth
/healthGETLiveness check β€” returns { status: "ok" }None
/health/dbGETDB connectivity check (runs SELECT 1 FROM DUMMY)None
/auth/userGETCurrent user profile (id, email, name)XSUAA
/api/qrcode?url=...GETQR code PNG generationXSUAA
/api/recommendationsGETPersonalized "what's next" recommendations (embedding centroid + co-completion blend)XSUAA
/build/catalogGETMission/group/tutorial catalog (JSON, for build pipeline)None
/build/co-completionsGETCo-completion graph data (used by recommendations)None
/build/navigatorGETMissions, groups (incl. standalone), tutorial→mission/group mappings, and checkpoint milestonesNone
/build/slug-mappingGETSlug→ID mapping for all missions/groupsNone
/build/repo-catalogGETSlug-keyed DiscoveredTutorial map (third-tier discovery fallback)None
/build/repo-catalogPOSTWrite the discovered-tutorial baseline (CI-as-canonical-writer)Bearer (CONTENT_API_KEY)
/build/conceptsGETPublished 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/publishConceptPOSTAdmin action β€” sets publishedAt + publishedBy on a ConceptXSUAA + KnowledgeGraph.Admin
/graph/unpublishConceptPOSTAdmin action β€” clears publishedAt + publishedBy on a ConceptXSUAA + KnowledgeGraph.Admin
/graph/explore-dataGETBulk graph JSON for the /explore/ page (nodes + edges + generatedAt; 5-min LRU cache)None
/graph/pathGETShortest path between two tutorial/concept slugs (?from=<slug>&to=<slug>). 400 on same-slug. Extracted from Phase 2 Joule tool.None
/api/advocatesGETPublic 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]GETPhoto 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/submitPOSTTutorial feedback form (rate-limited; submitter IP hashed via SUBMISSION_SALT_SECRET)None
/chat/streamPOSTJoule chat streaming endpoint (Server-Sent Events)XSUAA
/a2aPOSTA2A 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.jsonGETA2A 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.mdGETA2A consumption guide (how to authenticate + call)None
/.well-known/tutorial-provenance/jwks.jsonGETEd25519 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/codecheckPOSTAI 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/generateOsVariantsPOSTAI-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/statsGETTutorial embedding coverage / drift statisticsXSUAA + Admin
/drawings/listEvents()GETOData function β€” events with β‰₯1 claimed prize (newest-first). Returns { legacyId, name, startDate }.XSUAA + Admin
/drawings/pool(eventId=<legacyId>)GETOData 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>)GETOData 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/alertsGETActive alerts with audience=ALL. 60 s cache.None
/api/alerts/meGETALL + AUTHENTICATED + ADMIN (if admin). 30 s private cache.XSUAA
/api/homepage/eventsGET3-4 upcoming events (merged from DB + events calendar). 60 s cache.None
/api/homepage/videosGET@sapdevs video feed (Developer News + recent uploads). 15-min cache; requires YOUTUBE_API_KEY.None
/api/homepage/communityBlogsGETSAP Community blog RSS feed (latest posts). 30-min cache.None
/api/homepage/newsGETSAP News headlines RSS feed. 30-min cache.None
/api/homepage/shelves?verb=<v>GETAll active HomepageShelves entries for one verb (LEARN|BUILD|INTEGRATE|OPERATE|AI|CONNECT). 5-min cache.None
/api/homepage/redirectsActiveGETActive LegacyRedirects rows (approuter-only consumer; refreshes hourly).None
/api/homepage/recordRedirectHitsPOSTIdempotent batch hit counter for legacy redirects (approuter-only writer). Body: { hits: [{ id, count }] }.None
/build/homepage-shelvesGETAll HomepageShelves entries keyed by verb (Hugo build-time only; bakes hugo/data/homepage_shelves.json). Payload includes tagline / whyItMatters / authoringStatus (#759).None
/build/verb-definitionsGETAll 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-definitionsGETAll 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 ​

URLMethodDescriptionAuth
/content/tutorials/{slug}GETServe tutorial HTML from HANA (ETag, Cache-Control)None
/content/tutorials/{slug}/provenanceGETSigned 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/hashesGETSHA-256 map of active content ({ slug: hash })None
/content/navGETNavigation metadata for published tutorialsNone
/content/publishPOSTDeprecated β€” single-shot publish (base64-gzipped files). Kept for one release cycle; new clients use the chunked protocol below.Bearer (CONTENT_API_KEY)
/content/publish/beginPOSTOpen a chunked publish session. Returns { sessionId, version, expiresAt } (201) or 409 if a publish is already in progress.Bearer (CONTENT_API_KEY)
/content/publish/appendPOSTAppend a batch of files to an open session. Files shape: { slug: base64gzip }. Idempotent for (sessionId, slug).Bearer (CONTENT_API_KEY)
/content/publish/commitPOSTActivate the session's manifest. Idempotent (returns alreadyActive: true on repeat).Bearer (CONTENT_API_KEY)
/content/publish/abortPOSTDiscard an open session. Idempotent.Bearer (CONTENT_API_KEY)
/content/code-check-specsPOSTAI 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/rollbackPOSTRevert to previous manifest versionBearer (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:

bash
# 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 ​

MountServiceCurated toolsAuth (at mount)
/mcp/searchSearchServicesearch_tutorials, list_missions, get_mission, get_tutorialNone (anonymous)
/mcp/graphKnowledgeGraphServicekg_shared_concepts, kg_neighborhood, kg_search_concepts, kg_community (+ resources + prompts)None (anonymous)
/mcp/homepageHomepageServiceget_my_recommended_tutorials, get_my_recommended_missionsauthenticated-user
/mcp/apiDeveloperServiceget_my_tutorials, get_my_missions, get_my_events, get_my_completed_steps, get_tutorial_step, complete_step, reset_tutorial_progressauthenticated-user
/mcp/adminAdminServicemerge_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 prefixApprouter authRewrites toPurpose
/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) ​

URLMethodDescriptionAuth
/patsGETPatService β€” list the caller's own PATs (MyPATs, row-scoped to user.email = $user.id)XSUAA (authenticated-user)
/pats/mintPATPOSTMint 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.revokePATPOSTBound action β€” revoke one of the caller's PATsXSUAA (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 ​

URLProtocolDescriptionAuth
/ws/event-streamSocket.IOAnonymous live tutorial-completion stream (backed by EventStreamService)None
/ws/displaySocket.IODisplay dashboard live updates (backed by DisplayService)XSUAA DisplayApp (enforced at namespace join, not approuter)
/socket.io/Socket.IOUnderlying 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:

    jsonc
    {
      "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:

    markdown
    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 ​

URLMethodDescriptionAuth
/loginGETTriggers XSUAA login redirectXSUAA
/login/callbackGETOAuth2 callback (handled by AppRouter)XSUAA
/admin/rebuildPOSTHot-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.

ScopeRole CollectionUsed 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 ​

bash
# 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.

FileTypeScope
test/unit/srv/analytics-sql-validator.pen.test.jsunitMalicious 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.jsunitOWASP XSS Filter Evasion Cheat Sheet payloads against tutorial-source sanitizer. Covers scripts/parsers/sanitize-html.ts.
test/unit/scripts/check-hugo-safe-html.test.jsunitUnit 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.jssmokePOST 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.jssmokeSweeps 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.jssmokeRead-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.jssmokeCSP, X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy on approuter HTML responses.
scripts/check-hugo-safe-html.cjsbuild guardFails 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:

bash
# 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.