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

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
/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). Skill via metadata.skillId; defaults to conversational tutorial-chat. Enable/config via /admin-ui/#joule (ChatSettings a2aEnabled).XSUAA + Tutorial.MCP
/.well-known/agent-card.jsonGETA2A Agent Card β€” public discovery document (5 skills, streaming, xsuaa security scheme). Base URL + token URL from /admin-ui/#joule (ChatSettings).None
/.well-known/a2a-instructions.mdGETA2A consumption guide (how to authenticate + call)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
/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/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.