Skip to content

Guided lessons that run inside the console - #162

Merged
DaviReisVieira merged 2 commits into
mainfrom
feat/learn-in-console
Sep 11, 2026
Merged

DaviReisVieira merged 2 commits into
mainfrom
feat/learn-in-console

Conversation

@DaviReisVieira

Copy link
Copy Markdown
Owner

Closes #155, closes #156, closes #157, closes #158. Closes the tracking issue #159.

Learn is back. Not the parked version: the verification engine was always the good part of it, but the presentation was a static list of cards and every step sent the learner out to a terminal. Two things changed since it was parked. The UI is Cloudscape now, so practising in StackPort transfers directly to the real console. And Cloudscape ships the AWS console's own onboarding components (AnnotationContext, Hotspot, the tutorial data model), already installed.

So the tutorial runs inside the console. A panel docks in the right rail, hotspots pulse on the actual buttons, and StackPort calls the real API to check the result, so a step turns green only when the resource genuinely exists.

What a lesson feels like

Open Learn (sidebar, or g l), hit Start lesson, and you land on /resources/s3 with the popover already pointing at Create bucket.

Every step offers two paths and verifies the same way either one is taken:

  • Do it here highlights the control, states what to type, and gives you the values with a copy button.
  • With the CLI gives the command already filled in with your endpoint and your resource names, plus a flag-by-flag explanation of what it does and why.

Verified steps poll while the panel is open, so you can run the command in your own terminal and watch the step go green on its own, then watch the popover move to the next control. No clicking through. That moment is where the design budget went.

You name your own resources. A lesson declares its variables; the server generates them once, persists them, and substitutes them into every instruction, command and field. So the commands on screen can be copied and run without editing, and they still match tomorrow. Change the name under "Names used in this lesson" and everything follows, validated against the lesson's own rule.

Nothing is a gate. Any step can be skipped, and a skip is recorded as a skip rather than as done, so it can be re-checked later. Verification is a record of what you built, never a toll.

Delight, no cheese. No points, no streaks, no confetti. The completion screen tells you what now exists in your emulator, by name, and offers to take you there.

Lesson 1: Store a file

Five steps, about six minutes: find S3, create a bucket, look inside it, put a file in it, read back what S3 recorded. It teaches the things that actually bite people later: that bucket names live in a shared namespace, that a bucket is flat and folders are a fiction the console invents, that content type is a guess made at upload time and never revisited, and that one flag is all that separates a local command from one that hits a real account.

The security defaults are taught as knowledge rather than faked as controls: the copy says real S3 encrypts with SSE-S3 unconditionally and blocks public access by default, because that is true and worth knowing, and because a toggle that silently does nothing teaches a lie.

Backend

Revived by hand from the parked branch rather than cherry-picked, since that commit predates the Cloudscape work.

  • backend/learn/verify.py keeps the CHECKS registry and its two best properties: fresh boto3 calls that bypass the TTL cache, and service_unreachable kept distinct from "not done yet". Added s3_object_exists (which can say what is missing, unlike a count), s3_prefix_exists, and all_of.
  • backend/learn/content.py validates trail content at import. An unknown verify.type, a hotspot the UI never renders, a step with no way forward, an undeclared {{variable}}, a duplicate id, two steps sharing an anchor: all fail in CI rather than in front of someone trying to learn.
  • backend/routes/learn.py serves trails with substitution applied, and POST /progress takes force for a skip.
  • /api/learn is allowlisted in ReadOnlyMiddleware, since progress is local state rather than an AWS write.
  • Feature-flagged on STACKPORT_LEARN, surfaced to the UI as learn_enabled in GET /api/health.

Four Cloudscape traps

These came out of reading the compiled library, and each one breaks a lesson silently. They are in CLAUDE.md so the next person does not rediscover them:

  1. AnnotationContext resets to step 0 whenever currentTutorial changes identity. So the tutorial object is built once per run and step content is a stable element that reads live state from context. Rebuilding it on every verification would throw the learner back to step one.
  2. Next is gated on the next step's hotspot being mounted, not on app logic, and the step index is private. Gating Next on verification directly is impossible, but mounting the next anchor only once the current step verifies achieves exactly that using the library's own mechanism.
  3. completed: true makes every hotspot vanish. The object given to AnnotationContext always has completed: false; the finished state is rendered by the panel.
  4. CloudscapeShell remounts on every navigation, because each page renders its own. So the provider lives above the router in main.tsx. There is a test that fails loudly if anyone moves it.

Trap 1 also turned out to be the mechanism for moving between steps: to go to step N, hand Cloudscape a tutorial that begins at step N, and its reset lands exactly there. run.offset translates indices back, and the popover's step counter reads from a ref so it reports the position in the lesson rather than in the sliced object.

Three things only the real app found

Worth writing down, because none of them would have shown up in tests:

  • The popover covered the thing it was pointing at. The anchor on the Buckets table header opened over the Name column, which is the column the step tells you to click. Table-header anchors now open away from the table.
  • A stale closure wiped the learner's place. AnnotationContext retains the onStepChange handler from mount, so pathname read from the render closure was whatever route the lesson started on. Every step change then "navigated" to the step's route and dropped the query string, kicking the learner out of the bucket they had just opened. It reads a ref now.
  • auto-fallback is a navigation problem, not a step change. When a step's anchor is not on screen, Cloudscape moves its popover to whichever anchor is. Following that in the panel walked the learner backwards. The panel now holds its position and tells them where to go, and the next completion resyncs both.

Tests

  • Backend: 539 passing, 54 of them new. Route tests run against a fixture trail rather than the shipped content, so authoring a lesson can never break the API tests; the shipped content has its own validation tests. Covers substitution, variable stability across requests, verification following a renamed resource, the three result branches, skip semantics, and read-only mode.
  • Frontend: 298 passing, 30 of them new. Integration tests over real routes with URL-routed fetch mocks, asserting real payloads. The ones that matter: zero hotspots on the page when no lesson is running, the next anchor appears only after a pass (the frontier gate), the step index survives navigating between pages (the test that catches trap 4), verifying does not reset the step index, and a test that reads both hotspot registries off disk and fails if the backend and frontend lists drift.
  • npx tsc -b clean, npx eslint . clean, npm run build clean, ui/dist rebuilt.

Walked end to end against MiniStack

Whole lesson, headless, from a clean progress file: start, land on the right route, create the bucket from a terminal, watch auto-verify pass it with nothing clicked, open the bucket, upload from the terminal, watch that pass too, finish. Server-side progress ends with all five steps recorded, and the completion screen names the bucket that was actually created. Screenshots below.

AWS parity

Cloudscape is the console's design system, and AnnotationContext / Hotspot / the tutorial model are the same components AWS ships for its own in-console hands-on tutorials. They are used as intended: panel docked in the layout's right rail, tutorials started by an explicit click, hotspot popovers anchored on real controls. Live verification is our addition, and it is one AWS cannot offer, because their version runs against the learner's real account.

Lesson copy was written against the docs rather than from memory (S3 welcome, bucket naming rules, uploading objects, HeadObject), all original prose, each step linking the AWS page for its concept. Every command is a real API call: s3 ls, s3api create-bucket, s3 cp, s3api head-object, with --endpoint-url substituted from the active endpoint rather than hardcoded, which a test enforces.

Deliberately out of scope

Lessons are hard-blocked against real AWS, since they create and delete resources. The AI tutor stays out until real beginners have gotten stuck in real places. And the rest of the trail waits until this lesson has been played with, so the format can be judged before forty minutes of content gets written against it.

Learn is back, rebuilt around what changed since it was parked: the UI is
Cloudscape now, and Cloudscape ships the AWS console's own onboarding
components. So the tutorial runs inside StackPort instead of asking the
learner to read a page of cards and go elsewhere. A panel docks in the
right rail, hotspots point at the actual controls, and verification calls
the real API, so a step turns green only when the resource genuinely
exists.

Two ways through every step, same verification either way: do it here,
with the control highlighted and the exact values to copy, or with the
CLI, with the command already filled in and each flag explained. Verified
steps also poll, so running the command in a terminal turns the step
green on its own and moves the lesson forward without a click.

The learner names their own resources. A lesson declares its variables,
the server generates and persists them, and every instruction, command
and field on screen is substituted, so nothing has to be edited before it
can be run. The endpoint URL is substituted per request too.

Nothing is a gate. Any step can be skipped, and a skip is recorded as a
skip rather than as done, so it can be checked later. Verification is a
record of what the learner built, not a toll.

Four Cloudscape behaviours dictate the architecture and are documented in
CLAUDE.md, because each of them breaks a lesson silently: the provider
lives above the router, the tutorial object is built once per run, the
object handed to AnnotationContext is never marked completed, and Next is
gated on the next step's hotspot being mounted, which is what makes it
wait for verification.

Ships one lesson, Store a file, behind STACKPORT_LEARN.

Closes #155, closes #156, closes #157, closes #158, closes #159.
The substitution test asserted the machine's own AWS_ENDPOINT_URL, which
CI does not set. The endpoint is now fixed per test, so the assertion
checks the substitution rather than the environment.
@DaviReisVieira
DaviReisVieira merged commit bc472bc into main Sep 11, 2026
2 checks passed
@DaviReisVieira
DaviReisVieira deleted the feat/learn-in-console branch September 11, 2026 01:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant