Skip to content

Latest commit

 

History

History
495 lines (380 loc) · 35.4 KB

File metadata and controls

495 lines (380 loc) · 35.4 KB

UI & API Endpoints — Testing Reference

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


Public Frontend (Hugo static — via AppRouter)

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

QA Channel — Author Preview (XSUAA-gated)

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

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

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


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

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

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


Admin UI (SAPUI5 — Fiori Elements)

All admin routes require the Admin XSUAA scope.

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

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

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

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


Scanner Apps (Badge Scanner — Event Tool)

All scanner routes require the MobileApp XSUAA scope.

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

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

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

CAP Server Index & Swagger UI

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

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

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

Exposed via /_dev prefix (DEV/QA only)

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

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

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


OData Services (CAP backend)

URL Description Auth
/api/$metadata DeveloperService OData metadata (EDMX) XSUAA
/api/ DeveloperService — progress tracking, user-facing API XSUAA
/admin/$metadata AdminService OData metadata XSUAA + Admin
/admin/ AdminService — full CRUD for events, missions, etc. XSUAA + Admin
/admin/analytics/$metadata AnalyticsService OData metadata XSUAA + Admin
/admin/analytics/ AnalyticsService — @analytics.exposed entity surface + runSelectQuery action XSUAA + Admin
/admin/exports/$metadata ExportsService OData metadata XSUAA + Admin
/admin/exports/ ExportsService — exportLegacyData(format) action XSUAA + Admin
/display/$metadata DisplayService OData metadata XSUAA
/display/ DisplayService — read-only data for event dashboards XSUAA
/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/navigator returns a shape with missions[], groups[] (incl. standalone published Groups), tutorialMappings[], and checkpointMappings[] (milestone markers). The 5-minute in-memory cache is automatically invalidated when the Admin UI saves changes to Missions, Groups, or CompletionPath entities — no ?nocache=1 needed. Implementation: srv/lib/navigator-catalog.js.

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


Content Persistence Endpoints

URL Method Description Auth
/content/tutorials/{slug} GET Serve tutorial HTML from HANA (ETag, Cache-Control) None
/content/hashes GET SHA-256 map of active content ({ slug: hash }) None
/content/nav GET Navigation metadata for published tutorials None
/content/publish POST Deprecated — single-shot publish (base64-gzipped files). Kept for one release cycle; new clients use the chunked protocol below. Bearer (CONTENT_API_KEY)
/content/publish/begin POST Open a chunked publish session. Returns { sessionId, version, expiresAt } (201) or 409 if a publish is already in progress. Bearer (CONTENT_API_KEY)
/content/publish/append POST Append a batch of files to an open session. Files shape: { slug: base64gzip }. Idempotent for (sessionId, slug). Bearer (CONTENT_API_KEY)
/content/publish/commit POST Activate the session's manifest. Idempotent (returns alreadyActive: true on repeat). Bearer (CONTENT_API_KEY)
/content/publish/abort POST Discard an open session. Idempotent. Bearer (CONTENT_API_KEY)
/content/code-check-specs POST AI code-check spike (issue #171). Upserts CodeCheckSpecs rows from *.codecheck.json sidecars produced by fetch-tutorials. Body: { specs: [{ slug, stepNumber, goal, language?, hints?, referenceSolution? }] }. Carry-forward semantics (no DELETE on absent specs). Server-only — referenceSolution never reaches the client. Bearer (CONTENT_API_KEY)
/content/rollback POST Revert to previous manifest version Bearer (CONTENT_API_KEY)

Chunked publish protocol

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

Typical client flow:

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

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

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

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

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


Hosted MCP Server

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

Per-service mounts

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

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

Auth-tier route prefixes (approuter → srv rewrite)

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

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

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

Personal Access Tokens (PAT)

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

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


WebSocket / Real-time

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

Joule Chat Tools

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

getBranchRecommendation

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

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

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

  • Return shape:

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

  • Error envelopes (validation failures, no telemetry):

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

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

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

findLearningPath

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

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

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

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

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

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

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

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

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

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


Approuter-only Endpoints

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

XSUAA Scopes Reference

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

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

Local Development Notes

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

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

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

Quick Smoke Test Checklist

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

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

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

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

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

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

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

Security Testing Reference

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

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

Running locally:

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

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

# Hugo safeHTML guard
npm run check:security-annotations

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