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/displaynamespace 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 |
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
AnalyticsServiceat/admin/analytics. SQL tab uses Monaco (lazy-loaded); queries pass throughsrv/lib/analytics-sql-validator.cjs(SELECT-only, allowlisted tables, wrapped withLIMIT 5001). Entity browser is gated by@analytics.exposedannotations 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
BarcodeDetectorsupport.
| 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 athttp://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-srvDisable:cf unset-env tutorials-srv EXPOSE_CAP_UI && cf restart tutorials-srvDo 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 |
/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). Skill via metadata.skillId; defaults to conversational tutorial-chat. Enable/config via /admin-ui/#joule (ChatSettings a2aEnabled). | XSUAA + Tutorial.MCP |
/.well-known/agent-card.json | GET | A2A 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.md | GET | A2A consumption guide (how to authenticate + call) | 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 |
/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/navigatorreturns a shape withmissions[],groups[](incl. standalone published Groups),tutorialMappings[], andcheckpointMappings[](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=1needed. Implementation: srv/lib/navigator-catalog.js.
/feedback/submitreturns 503 whenSUBMISSION_SALT_SECRETis 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/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. WhenbranchingEnabled = false, the tool is not registered and the LLM falls back to general guidance.Params β
missionSlug?(string),tutorialSlug?(string),branchPointId?(string). At least one ofmissionSlug/tutorialSlugis required.branchPointIdrequirestutorialSlug.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
BranchDecisionsrow per branch / alt-group / skip recommendation withsource: 'jouleTool'. Skip-point telemetry is only emitted whenskip === 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. WhenkgPathBetweenEnabled = false(default), the tool is not registered and the LLM falls back to general guidance viasearchTutorials.Params β
toSlug(string, required, lowercase alphanumeric + hyphens, 1-80 chars),fromSlug?(string, optional β same shape). IffromSlugis omitted, the handler infers it from the user's most-recent COMPLETED TaskRecord; if no completion history, the search is unanchored (usestoSlugas its own neighborhood center).Return shape β rendered markdown string that the LLM paraphrases or quotes verbatim:
markdownHere'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_requestedat dispatch start ({ fromSlug, toSlug, hasUserId, fromSlugInferred, unanchored }) andkg.joule.path_returnedat dispatch end ({ ..., resultCount, pathTypeBreakdown: { PREREQ, CO_COMPLETED, SHARED_CONCEPT }, latencyMs, exactTargetReached, error? }). ThepathTypeBreakdownlets 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'orerror: 'unknown'.
- Malformed
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(runsapprouter/server.js) - CAP backend:
http://localhost:4004(runscds watch) - Hugo dev server:
http://localhost:1313(runsnpm 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 timelineSecurity 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-annotationsPolicy 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.