diff --git a/src/components/CodespacesButton.tsx b/src/components/CodespacesButton.tsx
index 33afdfe61..28f6c4f5a 100644
--- a/src/components/CodespacesButton.tsx
+++ b/src/components/CodespacesButton.tsx
@@ -11,11 +11,11 @@ export const CodespacesButton = ({ href, fullWidth = false }: CodespacesButtonPr
Open in Codespaces
- (opens in new tab)
+
Free GitHub account required
diff --git a/src/components/CommunitySection.tsx b/src/components/CommunitySection.tsx
index 84a1d8ae1..8104d15a3 100644
--- a/src/components/CommunitySection.tsx
+++ b/src/components/CommunitySection.tsx
@@ -65,10 +65,10 @@ export const CommunitySection = ({ aside }: { aside?: ReactNode }): JSX.Element
- {card.cta} (opens in new tab)
+ {card.cta}
))}
diff --git a/src/components/CommunitySidebar.tsx b/src/components/CommunitySidebar.tsx
index 7e8b7475f..c18a14135 100644
--- a/src/components/CommunitySidebar.tsx
+++ b/src/components/CommunitySidebar.tsx
@@ -131,13 +131,13 @@ export const CommunitySidebar = ({
{hasThread ? "Share & Discuss" : "Join the Community"}
- (opens in new tab)
+
Get help or share your solution
diff --git a/src/components/ConsentBanner.tsx b/src/components/ConsentBanner.tsx
index ba20eb24f..d9a6b70c7 100644
--- a/src/components/ConsentBanner.tsx
+++ b/src/components/ConsentBanner.tsx
@@ -34,8 +34,7 @@ export function ConsentBanner(): JSX.Element {
diff --git a/src/components/ContributorBadge.tsx b/src/components/ContributorBadge.tsx
index 016843788..2c6674a73 100644
--- a/src/components/ContributorBadge.tsx
+++ b/src/components/ContributorBadge.tsx
@@ -28,12 +28,12 @@ export const ContributorBadge = ({ name, url, glow = false, label = "Challenge B
{content}
- (opens in new tab)
+
);
}
diff --git a/src/components/DiscussionSection.tsx b/src/components/DiscussionSection.tsx
index 6b40b322a..11bc1d50f 100644
--- a/src/components/DiscussionSection.tsx
+++ b/src/components/DiscussionSection.tsx
@@ -24,10 +24,10 @@ export const DiscussionSection = ({ adventureId, levelId, discussionUrl }: Discu
- Join the Discussion on {COMMUNITY_DISPLAY_NAME} (opens in new tab)
+ Join the Discussion on {COMMUNITY_DISPLAY_NAME}
);
@@ -58,7 +58,7 @@ export const DiscussionSection = ({ adventureId, levelId, discussionUrl }: Discu
key={`${post.username}-${post.created_at}`}
href={post.topicUrl}
target="_blank"
- rel="noopener noreferrer"
+ rel="noopener noreferrer" aria-describedby="new-tab-hint"
className="block card-glow rounded-xl border border-border bg-[hsl(var(--surface))] p-5 transition-all focus-ring"
>
diff --git a/src/components/LevelCard.tsx b/src/components/LevelCard.tsx
index 5b84bfd08..761fab366 100644
--- a/src/components/LevelCard.tsx
+++ b/src/components/LevelCard.tsx
@@ -37,10 +37,10 @@ export const LevelCard = ({ level, headingLevel = "h2" }: LevelCardProps): JSX.E
- Open in GitHub Codespaces (opens in new tab)
+ Open in GitHub Codespaces
@@ -49,8 +49,8 @@ export const LevelCard = ({ level, headingLevel = "h2" }: LevelCardProps): JSX.E
Discussion
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 58d533302..ae04f726c 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -44,11 +44,11 @@ const NavLinks = ({ onNavigate }: NavLinksProps): JSX.Element => (
- Community (opens in new tab)
+ Community
@@ -135,7 +135,7 @@ export const Navbar = (): JSX.Element => {
ref={triggerRef}
onClick={() => setMenuOpen((o) => !o)}
className="flex h-11 w-11 items-center justify-center rounded-md border border-border bg-[hsl(var(--surface))] text-foreground/70 hover:text-foreground transition-all focus-ring"
- aria-label={menuOpen ? "Close menu" : "Open menu"}
+ aria-label="Menu"
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
diff --git a/src/components/PageHero.tsx b/src/components/PageHero.tsx
index 968144ce1..099dd7b3a 100644
--- a/src/components/PageHero.tsx
+++ b/src/components/PageHero.tsx
@@ -22,9 +22,9 @@ const renderCta = (cta: Cta, isPrimary: boolean): JSX.Element => {
if (cta.external) {
return (
-
+
{cta.label}
- (opens in new tab)
+
);
}
diff --git a/src/components/PersonNameLink.tsx b/src/components/PersonNameLink.tsx
index bd2853f8a..bf25b0ff6 100644
--- a/src/components/PersonNameLink.tsx
+++ b/src/components/PersonNameLink.tsx
@@ -15,12 +15,12 @@ export const PersonNameLink = ({ name, url }: PersonNameLinkProps): JSX.Element
{name}
- (opens in new tab)
+
);
};
diff --git a/src/components/RewardsCard.tsx b/src/components/RewardsCard.tsx
index c39f8be8c..13f080ac9 100644
--- a/src/components/RewardsCard.tsx
+++ b/src/components/RewardsCard.tsx
@@ -28,12 +28,12 @@ export const RewardsCard = ({ rewards, compact = false, levelDeadline, deadlineP
Community Voices
- (opens in new tab)
+
.
@@ -67,12 +67,12 @@ export const RewardsCard = ({ rewards, compact = false, levelDeadline, deadlineP
See the points & ranking rules for the full breakdown
- (opens in new tab)
+
)}
diff --git a/src/data/adventures/blind-by-design.generated.ts b/src/data/adventures/blind-by-design.generated.ts
index d588f0c33..9367a04de 100644
--- a/src/data/adventures/blind-by-design.generated.ts
+++ b/src/data/adventures/blind-by-design.generated.ts
@@ -59,17 +59,17 @@ export const BLIND_BY_DESIGN: Adventure = {
"Your mission: replace that hard-coded label with an OpenFeature client, point that client at the flagd sidecar that already runs next to your Codespace, and let flags.json drive what gets recorded as the subject's vision_state. Prove the lab can change what it records without restarting anything.",
],
objective: [
- "curl http://localhost:8080/ (opens in new tab) returns a vision_state reading resolved from flags.json (not the hard-coded 'untreated' fallback)",
+ "curl http://localhost:8080/ returns a vision_state reading resolved from flags.json (not the hard-coded 'untreated' fallback)",
"The response payload includes OpenFeature evaluation details: flag key, variant, reason, and value",
"Editing flags.json to change defaultVariant causes the next request to return the new variant without restarting the app or flagd",
],
architecture: [
"This level runs as two containers side-by-side in your Codespace: the Spring Boot lab and a flagd sidecar.
",
- "The Spring Boot service runs on http://localhost:8080/ (opens in new tab) with one endpoint, GET /. flags.json is mounted read-only into the flagd sidecar; edit it through the IDE Integrated Development Environment and flagd's file watcher picks up the change within about a second. The flagd sidecar serves flag evaluations over gRPC on :8013. The OpenFeature SDK reads FLAGD_HOST and FLAGD_PORT from the environment (pre-set by the devcontainer), so there is no host or port to hard-code.
",
+ "The Spring Boot service runs on http://localhost:8080/ with one endpoint, GET /. flags.json is mounted read-only into the flagd sidecar; edit it through the IDE Integrated Development Environment and flagd's file watcher picks up the change within about a second. The flagd sidecar serves flag evaluations over gRPC on :8013. The OpenFeature SDK reads FLAGD_HOST and FLAGD_PORT from the environment (pre-set by the devcontainer), so there is no host or port to hard-code.
",
],
toolbox: [
{ name: "./mvnw", description: "Maven wrapper checked in next to pom.xml, builds and runs the Spring Boot lab" },
- { name: "curl", description: "makes requests to http://localhost:8080/ (opens in new tab) and shows the reading the lab records", url: "https://curl.se/" },
+ { name: "curl", description: "makes requests to http://localhost:8080/ and shows the reading the lab records", url: "https://curl.se/" },
{ name: "jq", description: "pretty-prints and filters the JSON evaluation details returned by the SDK", url: "https://jqlang.org/" },
{ name: "flagd sidecar", description: "already running in the devcontainer compose stack on the docker-internal network, no port forwarding needed" },
],
@@ -133,7 +133,7 @@ export const BLIND_BY_DESIGN: Adventure = {
toolbox: [
{ name: "Java 21 (Temurin)", description: "pre-installed in the devcontainer" },
{ name: "./mvnw", description: "Spring Boot Maven Wrapper, no global Maven install required" },
- { name: "curl", description: "sends requests to http://localhost:8080/ (opens in new tab) to test each targeting branch", url: "https://curl.se/" },
+ { name: "curl", description: "sends requests to http://localhost:8080/ to test each targeting branch", url: "https://curl.se/" },
{ name: "jq", description: "pretty-prints the JSON evaluation details", url: "https://jqlang.org/" },
{ name: "tail -f", description: "watches the application log live for [AUDIT] lines" },
],
@@ -141,7 +141,7 @@ export const BLIND_BY_DESIGN: Adventure = {
{ title: "Wait for Setup", content: "Wait ~2-3 minutes for the Java toolchain to install. Use Cmd/Ctrl + Shift + P then View Creation Log to watch progress. When the post-create finishes you'll have Java 21, the Maven wrapper, and the broken-state lab ready in adventures/04-blind-by-design/intermediate/.
" },
{ title: "Explore the UIs", content: `Open the Ports tab and navigate to each service:
-Port 8080: Spring Boot lab. The application under test. Access via the Ports tab or curl http://localhost:8080/ (opens in new tab) .
+Port 8080: Spring Boot lab. The application under test. Access via the Ports tab or curl http://localhost:8080/.
` },
{ title: "Confirm the Broken State", content: `Start the lab and confirm the broken state, where no targeting fires yet:
./mvnw spring-boot:run
@@ -233,8 +233,8 @@ curl -s 'http://localhost:8080/?species=zyklop&dose=underdose' | jq .value
toolbox: [
{ name: "Java 21 (Temurin)", description: "pre-installed in the devcontainer", url: "https://adoptium.net/" },
{ name: "./mvnw", description: "Spring Boot Maven Wrapper, no global Maven install required" },
- { name: "curl", description: "sends requests to http://localhost:8080/ (opens in new tab) to test the lab, and to Prometheus on http://localhost:9090/ (opens in new tab) to query metrics directly", url: "https://curl.se/" },
- { name: "Grafana", description: "browser UI at http://localhost:3000 (opens in new tab) (admin/admin) for the Feature Flag Metrics dashboard and Tempo trace explorer" },
+ { name: "curl", description: "sends requests to http://localhost:8080/ to test the lab, and to Prometheus on http://localhost:9090/ to query metrics directly", url: "https://curl.se/" },
+ { name: "Grafana", description: "browser UI at http://localhost:3000 (admin/admin) for the Feature Flag Metrics dashboard and Tempo trace explorer" },
{ name: "jq", description: "pretty-prints the JSON evaluation details", url: "https://jqlang.org/" },
],
howToPlay: [
@@ -243,7 +243,7 @@ curl -s 'http://localhost:8080/?species=zyklop&dose=underdose' | jq .value
Port 8080: Spring Boot lab. Add ?userId=subject-42 for a stable fractional-rollout bucketing key.
Port 3000: Grafana (admin / admin). Open Dashboards > Feature Flag Metrics (empty until metrics are wired). Try Explore > Tempo to see flag evaluations as span events.
-Port 9090: Prometheus. Query metrics directly via the Prometheus UI or curl http://localhost:9090/api/v1/query (opens in new tab) .
+Port 9090: Prometheus. Query metrics directly via the Prometheus UI or curl http://localhost:9090/api/v1/query.
Port 3200: Tempo. Tempo HTTP API used by the verify script to assert traces are flowing.
flagd runs on the docker-internal network only. No port forwarding needed.
` },
diff --git a/src/data/adventures/dead-reckoning.generated.ts b/src/data/adventures/dead-reckoning.generated.ts
index 5c8970ebb..b621dc8ea 100644
--- a/src/data/adventures/dead-reckoning.generated.ts
+++ b/src/data/adventures/dead-reckoning.generated.ts
@@ -39,10 +39,10 @@ export const DEAD_RECKONING: Adventure = {
topics: ["Backstage", "Gitea"],
audience: "Platform engineers, developers, and anyone curious about internal developer platforms and self-service scaffolding. No prior Backstage experience is needed, but familiarity with YAML and basic Git concepts will help.",
learnings: [
- "How Backstage software templates (opens in new tab) are structured: parameters, steps, and output",
- "How scaffolder actions (opens in new tab) work, such as fetch:template, publish:gitea, and catalog:register",
- "How the catalog registration (opens in new tab) step connects a scaffolded repository to the Backstage catalog",
- "How to use Backstage's built-in template tooling (opens in new tab) : the installed-actions browser and the Template Editor's live preview and dry-run",
+ "How Backstage software templates are structured: parameters, steps, and output",
+ "How scaffolder actions work, such as fetch:template, publish:gitea, and catalog:register",
+ "How the catalog registration step connects a scaffolded repository to the Backstage catalog",
+ "How to use Backstage's built-in template tooling : the installed-actions browser and the Template Editor's live preview and dry-run",
],
codespacesUrl: `${CODESPACES_BASE}?devcontainer_path=.devcontainer%2Fdead-reckoning_beginner%2Fdevcontainer.json&quickstart=1`,
discussionUrl: "https://community.offon.dev/t/repair-a-broken-backstage-software-template-july-2026-adventure-beginner/1657",
@@ -113,10 +113,10 @@ vessel through it to confirm the repair.
topics: ["Backstage", "Gitea", "Argo Events", "Argo Workflows", "Argo CD"],
audience: "Platform and DevOps engineers who have met these tools before and want to see how they fit together. You should be comfortable with Kubernetes, YAML, and reading a tool's logs and UI. Prior exposure to Backstage, Gitea, and the Argo projects helps, but the focus here is the integration between them, not any one tool.",
learnings: [
- "How a Git webhook drives a workflow engine: Argo Events Sensors (opens in new tab) turn a push into a parameterized (opens in new tab) workflow run",
- "How Argo Workflows (opens in new tab) runs a multi-step delivery pipeline, and the RBAC (opens in new tab) its steps need",
- "How an Argo CD ApplicationSet (opens in new tab) auto-discovers repos and syncs (opens in new tab) them into the cluster",
- "How Backstage annotations (opens in new tab) tie a catalog entity to its live deployment status",
+ "How a Git webhook drives a workflow engine: Argo Events Sensors turn a push into a parameterized workflow run",
+ "How Argo Workflows runs a multi-step delivery pipeline, and the RBAC its steps need",
+ "How an Argo CD ApplicationSet auto-discovers repos and syncs them into the cluster",
+ "How Backstage annotations tie a catalog entity to its live deployment status",
"How to trace a silent failure across tools from each one's logs and UI",
],
codespacesUrl: `${CODESPACES_BASE}?devcontainer_path=.devcontainer%2Fdead-reckoning_intermediate%2Fdevcontainer.json&quickstart=1&machine=standardLinux32gb`,
diff --git a/src/data/adventures/lex-imperfecta.generated.ts b/src/data/adventures/lex-imperfecta.generated.ts
index 08e83b7f4..858d1b49a 100644
--- a/src/data/adventures/lex-imperfecta.generated.ts
+++ b/src/data/adventures/lex-imperfecta.generated.ts
@@ -39,10 +39,10 @@ export const LEX_IMPERFECTA: Adventure = {
topics: ["Kyverno", "Kubernetes"],
audience: "Platform engineers, SREs Site Reliability Engineers , and developers curious about Kubernetes security — no prior Kyverno experience needed, but familiarity with basic kubectl and YAML will help.",
learnings: [
- "How Kyverno ValidatingPolicy (opens in new tab) resources and CEL validation expressions (opens in new tab) work",
- "The difference between Audit, Deny, and Warn (opens in new tab) validation actions",
- "How to use custom label keys (opens in new tab) to enforce workload identity standards",
- "How Kyverno MutatingPolicy (opens in new tab) resources automatically patch incoming workloads at admission",
+ "How Kyverno ValidatingPolicy resources and CEL validation expressions work",
+ "The difference between Audit, Deny, and Warn validation actions",
+ "How to use custom label keys to enforce workload identity standards",
+ "How Kyverno MutatingPolicy resources automatically patch incoming workloads at admission",
],
codespacesUrl: `${CODESPACES_BASE}?devcontainer_path=.devcontainer%2F05-lex-imperfecta_01-beginner%2Fdevcontainer.json&quickstart=1`,
discussionUrl: "https://community.offon.dev/t/restore-proper-admission-control-using-kyverno-june-2026-adventure-beginner/1576",
@@ -123,10 +123,10 @@ kyverno apply manifests/policies/stamp-travel-permit.yaml --resource manifests/p
topics: ["Kyverno", "Policy Reporter", "Kubernetes"],
audience: "Platform engineers and SREs Site Reliability Engineers who have some familiarity with Kyverno, ideally after completing the Beginner level. You should be comfortable reading Kubernetes YAML and basic kubectl commands.",
learnings: [
- "How to scope policies using ValidatingPolicy (opens in new tab) (cluster-wide) and NamespacedValidatingPolicy (opens in new tab) (per-namespace), and when to use each",
- "How CEL expressions (opens in new tab) in ValidatingPolicy and PolicyException express fine-grained admission conditions",
- "How to write and scope a PolicyException (opens in new tab) correctly so only the intended workloads are exempt",
- "How to use Policy Reporter (opens in new tab) and the OpenReports (opens in new tab) format to audit and debug a policy estate across multiple namespaces",
+ "How to scope policies using ValidatingPolicy (cluster-wide) and NamespacedValidatingPolicy (per-namespace), and when to use each",
+ "How CEL expressions in ValidatingPolicy and PolicyException express fine-grained admission conditions",
+ "How to write and scope a PolicyException correctly so only the intended workloads are exempt",
+ "How to use Policy Reporter and the OpenReports format to audit and debug a policy estate across multiple namespaces",
],
codespacesUrl: `${CODESPACES_BASE}?devcontainer_path=.devcontainer%2F05-lex-imperfecta_02-intermediate%2Fdevcontainer.json&quickstart=1`,
discussionUrl: "https://community.offon.dev/t/fix-a-broken-kyverno-policy-estate-june-2026-adventure-intermediate/1581",
@@ -218,11 +218,11 @@ policy reports, the OpenReports data behind Policy Reporter, as the audit of rec
topics: ["Kyverno", "Policy Reporter", "Kubernetes"],
audience: "Security engineers and platform engineers who want to explore the boundary between admission control and runtime security. Completing the Intermediate level first is helpful but not required. You should be comfortable reading Kyverno ValidatingPolicies and CEL expressions. No prior Falco experience required.",
learnings: [
- "How Falco rules (opens in new tab) are structured: conditions, output, and kernel-level fields, and how to write a rule targeting a specific runtime behaviour",
- "Why privileged: false is not enough: how Linux capabilities (opens in new tab) grant host-level access without the privileged flag",
- "How to use spec.variables (opens in new tab) in a ValidatingPolicy to share reusable CEL expressions across validations",
+ "How Falco rules are structured: conditions, output, and kernel-level fields, and how to write a rule targeting a specific runtime behaviour",
+ "Why privileged: false is not enough: how Linux capabilities grant host-level access without the privileged flag",
+ "How to use spec.variables in a ValidatingPolicy to share reusable CEL expressions across validations",
"How pod volumes reference secrets, and why a volume's name and the secret it mounts are two separate fields in the pod spec",
- "How Falcosidekick (opens in new tab) aggregates Falco alerts and how to use its UI to watch a runtime incident in real time",
+ "How Falcosidekick aggregates Falco alerts and how to use its UI to watch a runtime incident in real time",
],
codespacesUrl: `${CODESPACES_BASE}?devcontainer_path=.devcontainer%2F05-lex-imperfecta_03-expert%2Fdevcontainer.json&quickstart=1`,
discussionUrl: "https://community.offon.dev/t/catch-the-intruder-the-guard-couldnt-see-june-2026-adventure-expert/1591",
@@ -268,11 +268,11 @@ kubectl get namespacedvalidatingpolicies -A
the intruder left no trace at admission. That is part of the problem.` },
{ title: "Act 1: Wake the Praetorian Guard", content: `The Falco rule in falco-rules.yaml has a defect: find the break, fix it, and run make apply;
alerts streaming into the Falcosidekick UI at port 30111 are your signal. The
-Falco condition fields reference (opens in new tab)
+Falco condition fields reference
documents every available field.
` },
{ title: "Act 2: Close the Gate", content: `The intruder passed admission: find the policy gap that let it through, close it, and run
make apply; re-admission denied and the Falcosidekick UI going quiet confirm Act 2 is done.
-The existing policy already uses spec.variables (opens in new tab)
+The existing policy already uses spec.variables
to share expressions across validations, a pattern worth exploring.
` },
{ title: "Act 3: Seal the Archive", content: `Open manifests/policies/: something is missing; the other policies show the structure, write
what's needed, then run make apply.
diff --git a/src/data/adventures/summaries.ts b/src/data/adventures/summaries.ts
index 503f2c263..dc4a9a94b 100644
--- a/src/data/adventures/summaries.ts
+++ b/src/data/adventures/summaries.ts
@@ -22,10 +22,10 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Beginner",
topics: ["Backstage", "Gitea"],
learnings: [
- "How Backstage software templates (opens in new tab) are structured: parameters, steps, and output",
- "How scaffolder actions (opens in new tab) work, such as fetch:template, publish:gitea, and catalog:register",
- "How the catalog registration (opens in new tab) step connects a scaffolded repository to the Backstage catalog",
- "How to use Backstage's built-in template tooling (opens in new tab) : the installed-actions browser and the Template Editor's live preview and dry-run",
+ "How Backstage software templates are structured: parameters, steps, and output",
+ "How scaffolder actions work, such as fetch:template, publish:gitea, and catalog:register",
+ "How the catalog registration step connects a scaffolded repository to the Backstage catalog",
+ "How to use Backstage's built-in template tooling : the installed-actions browser and the Template Editor's live preview and dry-run",
],
},
{
@@ -34,10 +34,10 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Intermediate",
topics: ["Backstage", "Gitea", "Argo Events", "Argo Workflows", "Argo CD"],
learnings: [
- "How a Git webhook drives a workflow engine: Argo Events Sensors (opens in new tab) turn a push into a parameterized (opens in new tab) workflow run",
- "How Argo Workflows (opens in new tab) runs a multi-step delivery pipeline, and the RBAC (opens in new tab) its steps need",
- "How an Argo CD ApplicationSet (opens in new tab) auto-discovers repos and syncs (opens in new tab) them into the cluster",
- "How Backstage annotations (opens in new tab) tie a catalog entity to its live deployment status",
+ "How a Git webhook drives a workflow engine: Argo Events Sensors turn a push into a parameterized workflow run",
+ "How Argo Workflows runs a multi-step delivery pipeline, and the RBAC its steps need",
+ "How an Argo CD ApplicationSet auto-discovers repos and syncs them into the cluster",
+ "How Backstage annotations tie a catalog entity to its live deployment status",
"How to trace a silent failure across tools from each one's logs and UI",
],
},
@@ -62,10 +62,10 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Beginner",
topics: ["Kyverno", "Kubernetes"],
learnings: [
- "How Kyverno ValidatingPolicy (opens in new tab) resources and CEL validation expressions (opens in new tab) work",
- "The difference between Audit, Deny, and Warn (opens in new tab) validation actions",
- "How to use custom label keys (opens in new tab) to enforce workload identity standards",
- "How Kyverno MutatingPolicy (opens in new tab) resources automatically patch incoming workloads at admission",
+ "How Kyverno ValidatingPolicy resources and CEL validation expressions work",
+ "The difference between Audit, Deny, and Warn validation actions",
+ "How to use custom label keys to enforce workload identity standards",
+ "How Kyverno MutatingPolicy resources automatically patch incoming workloads at admission",
],
},
{
@@ -74,10 +74,10 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Intermediate",
topics: ["Kyverno", "Policy Reporter", "Kubernetes"],
learnings: [
- "How to scope policies using ValidatingPolicy (opens in new tab) (cluster-wide) and NamespacedValidatingPolicy (opens in new tab) (per-namespace), and when to use each",
- "How CEL expressions (opens in new tab) in ValidatingPolicy and PolicyException express fine-grained admission conditions",
- "How to write and scope a PolicyException (opens in new tab) correctly so only the intended workloads are exempt",
- "How to use Policy Reporter (opens in new tab) and the OpenReports (opens in new tab) format to audit and debug a policy estate across multiple namespaces",
+ "How to scope policies using ValidatingPolicy (cluster-wide) and NamespacedValidatingPolicy (per-namespace), and when to use each",
+ "How CEL expressions in ValidatingPolicy and PolicyException express fine-grained admission conditions",
+ "How to write and scope a PolicyException correctly so only the intended workloads are exempt",
+ "How to use Policy Reporter and the OpenReports format to audit and debug a policy estate across multiple namespaces",
],
},
{
@@ -86,11 +86,11 @@ export const ADVENTURE_SUMMARIES: AdventureCardSummary[] = [
difficulty: "Expert",
topics: ["Kyverno", "Policy Reporter", "Kubernetes"],
learnings: [
- "How Falco rules (opens in new tab) are structured: conditions, output, and kernel-level fields, and how to write a rule targeting a specific runtime behaviour",
- "Why privileged: false is not enough: how Linux capabilities (opens in new tab) grant host-level access without the privileged flag",
- "How to use spec.variables (opens in new tab) in a ValidatingPolicy to share reusable CEL expressions across validations",
+ "How Falco rules are structured: conditions, output, and kernel-level fields, and how to write a rule targeting a specific runtime behaviour",
+ "Why privileged: false is not enough: how Linux capabilities grant host-level access without the privileged flag",
+ "How to use spec.variables in a ValidatingPolicy to share reusable CEL expressions across validations",
"How pod volumes reference secrets, and why a volume's name and the secret it mounts are two separate fields in the pod spec",
- "How Falcosidekick (opens in new tab) aggregates Falco alerts and how to use its UI to watch a runtime incident in real time",
+ "How Falcosidekick aggregates Falco alerts and how to use its UI to watch a runtime incident in real time",
],
},
],
diff --git a/src/data/adventures/the-ai-observatory.generated.ts b/src/data/adventures/the-ai-observatory.generated.ts
index e85de10a1..e1a83b4ec 100644
--- a/src/data/adventures/the-ai-observatory.generated.ts
+++ b/src/data/adventures/the-ai-observatory.generated.ts
@@ -19,7 +19,7 @@ export const THE_AI_OBSERVATORY: Adventure = {
"Recently, the station's bandwidth usage has spiked to 847% above baseline, but no one knows why. As the systems engineer, it's your job to instrument the AI, trace its activities, and uncover the root cause of the anomaly.",
"Your mission: bring visibility to the station's AI and solve the mystery.",
`
-Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells (opens in new tab) , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
+Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
`,
],
levels: [
@@ -43,12 +43,12 @@ export const THE_AI_OBSERVATORY: Adventure = {
"Three weeks in, you notice something odd in your morning diagnostics: communication module usage at 847% above baseline. Nobody's streaming. Nobody's running large data transfers. The planet surveys are on schedule. So what's consuming all that bandwidth?",
"As the station's systems engineer, you decide to investigate. Time to instrument HubSystem with OpenTelemetry and find out what's really going on.",
`
-Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells (opens in new tab) , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
+Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
`,
],
objective: [
"Enable OpenTelemetry instrumentation for HubSystem using OpenLLMetry",
- "Send traces to the OpenTelemetry Collector at http://localhost:30107 (opens in new tab) ",
+ "Send traces to the OpenTelemetry Collector at http://localhost:30107",
"Analyze traces in Jaeger to find what causes the high bandwidth usage",
"Provide the correct answer in quiz.txt",
],
@@ -111,7 +111,7 @@ interact with Kubernetes directly. The cluster is already running, so focus on t
That's not normal. ART is never vague. You access the ship's diagnostic systems (something you're not supposed to be able to do, but ART hasn't locked you out yet).
`,
"Your mission: diagnose ART's distraction using OpenTelemetry and fix the navigation system before you miss your jump.",
`
-Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells (opens in new tab) , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
+Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
`,
],
objective: [
@@ -185,7 +185,7 @@ make traffic
ART: "...Fine."
`,
"The engineer hands you access to the collector config and the application code, then walks away. Two problems to fix. ART's spans don't follow OTel GenAI semantic conventions, and the collector is forwarding everything.",
`
-Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells (opens in new tab) , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
+Credits: The characters of this adventure are borrowed from the Murderbot Diaries series by Martha Wells , a brilliant series that is funny, action-packed, and surprisingly heartwarming. It follows a security unit that hacked its own governor module and now just wants to be left alone to watch media, but keeps getting pulled into human nonsense.
`,
],
objective: [
diff --git a/src/pages/Accessibility.tsx b/src/pages/Accessibility.tsx
index 55f955af7..ed3b5b848 100644
--- a/src/pages/Accessibility.tsx
+++ b/src/pages/Accessibility.tsx
@@ -42,10 +42,10 @@ const Accessibility = (): JSX.Element => {
Tested with{" "}
-
- axe-core (opens in new tab)
+
+ axe-core
{" "}
on every pull request preview, in both light and dark mode.
@@ -203,10 +203,10 @@ const Accessibility = (): JSX.Element => {
- open an accessibility issue (opens in new tab)
+ open an accessibility issue
. The form prompts for the page, your assistive technology, and severity, which helps
us reproduce and prioritize.
@@ -264,10 +264,10 @@ const Accessibility = (): JSX.Element => {
- ACCESSIBILITY.md (opens in new tab)
+ ACCESSIBILITY.md
{" "}
and applies to every change.
@@ -286,10 +286,10 @@ const Accessibility = (): JSX.Element => {
- mgifford/ACCESSIBILITY.md (opens in new tab)
+ mgifford/ACCESSIBILITY.md
, a community resource for accessibility best practices, testing criteria, and
issue severity frameworks. Thank you to Mike Gifford and all contributors for
diff --git a/src/pages/ChallengeDetail.tsx b/src/pages/ChallengeDetail.tsx
index 730ea5ac1..50da9cb22 100644
--- a/src/pages/ChallengeDetail.tsx
+++ b/src/pages/ChallengeDetail.tsx
@@ -219,7 +219,7 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
{
title: "Get Started",
content: [
- `
Open in GitHub Codespaces . The devcontainer development container: a portable, reproducible coding environment defined by a configuration file is pre-configured and starts automatically. When you push from Codespaces, GitHub forks the repository to your account automatically.
`,
+ `
Open in GitHub Codespaces . The devcontainer development container: a portable, reproducible coding environment defined by a configuration file is pre-configured and starts automatically. When you push from Codespaces, GitHub forks the repository to your account automatically.
`,
`
Prefer working locally? Clone the repo and open it in any editor that supports the Dev Containers specification (VS Code Visual Studio Code , JetBrains, and others). The devcontainer config will be detected automatically.
`,
].join("\n"),
},
@@ -262,10 +262,10 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
- challenge thread (opens in new tab)
+ challenge thread
{" "}
on {COMMUNITY_DISPLAY_NAME}.
@@ -276,12 +276,12 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
Share your achievement on LinkedIn
- (opens in new tab)
+
@@ -323,12 +323,12 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
{tool.name}
- (opens in new tab)
+
) : (
{tool.name}
@@ -354,12 +354,12 @@ const StructuredLayout = ({ adventure, level, rewardsBelowFold, hasSolution }: S
{link.title}
- (opens in new tab)
+
diff --git a/src/pages/CommunityGuide.tsx b/src/pages/CommunityGuide.tsx
index c752e2acf..4da385e83 100644
--- a/src/pages/CommunityGuide.tsx
+++ b/src/pages/CommunityGuide.tsx
@@ -55,8 +55,8 @@ const CommunityGuide = (): JSX.Element => {
Create your account to access challenges, Q&A, community discussions, and everything else.
-
- Create an account (opens in new tab)
+
+ Create an account
@@ -65,8 +65,8 @@ const CommunityGuide = (): JSX.Element => {
Say hello and tell the community who you are and what you're working on. It's the first step.
-
- Say hello (opens in new tab)
+
+ Say hello
@@ -75,8 +75,8 @@ const CommunityGuide = (): JSX.Element => {
Stuck on a technical problem? Post a clear question and get answers from the community. No question is too basic.
-
- Ask a question (opens in new tab)
+
+ Ask a question
@@ -85,8 +85,8 @@ const CommunityGuide = (): JSX.Element => {
Share a tutorial, showcase a project, or write about what you've learned. Every contribution makes the community stronger.
-
- Share something (opens in new tab)
+
+ Share something
@@ -101,8 +101,8 @@ const CommunityGuide = (): JSX.Element => {
Hands-on scenarios with broken environments, misconfigured pipelines, and real-world failures to debug and fix. Post your solution and see how others approached it.
-
- Start a challenge (opens in new tab)
+
+ Start a challenge
@@ -111,8 +111,8 @@ const CommunityGuide = (): JSX.Element => {
Bring your expertise and experience. Help build adventures for others to learn from open source tech with a broken scenario, a real-world failure, or a skill gap. Shape the next adventure.
-
- Submit an idea (opens in new tab)
+
+ Submit an idea
@@ -130,8 +130,8 @@ const CommunityGuide = (): JSX.Element => {
].map((item) => (
-
- {item.label} (opens in new tab)
+
+ {item.label}
))}
diff --git a/src/pages/Contribute.tsx b/src/pages/Contribute.tsx
index b2d68a30c..cb2209e53 100644
--- a/src/pages/Contribute.tsx
+++ b/src/pages/Contribute.tsx
@@ -62,8 +62,8 @@ const Contribute = (): JSX.Element => {
Browse the forum, see what the community is building, and sign up to get started. Introduce yourself and get a feel for how {BRAND_NAME} works.
-
- Create an account (opens in new tab)
+
+ Create an account
@@ -83,21 +83,21 @@ const Contribute = (): JSX.Element => {
Have a scenario that would make a great challenge? Submit your idea on GitHub. Ready to go further? Pick up an approved idea and implement the full adventure.
-
- Read the adventure ideas guide (opens in new tab)
+
+ Read the adventure ideas guide
-
- Browse open adventure ideas (opens in new tab)
+
+ Browse open adventure ideas
@@ -107,8 +107,8 @@ const Contribute = (): JSX.Element => {
Stay up to date with what is happening across the open source community. Answer the question of the week and tell us what topics you would like to see covered next.
-
- Read the weekly digest (opens in new tab)
+
+ Read the weekly digest
@@ -117,8 +117,8 @@ const Contribute = (): JSX.Element => {
Running a local meetup, user group, or open source community? Use {BRAND_NAME} as a vendor-agnostic home for your discussions and announcements. Find existing groups, join as a speaker, or bring open source to your local community.
-
- Explore meetups and groups (opens in new tab)
+
+ Explore meetups and groups
@@ -129,17 +129,17 @@ const Contribute = (): JSX.Element => {
Follow us, repost what we share, and mention {BRAND_NAME} to your network. Writing about us in a blog post or podcast episode? Share it with us by email or mention us on social media and we would love to repost.
@@ -169,11 +169,11 @@ const Contribute = (): JSX.Element => {
Found a gap or something that could work better? Your feedback shapes what we build next.