From 059b52bb27ccc4ad3ae103244a06d8ad162de2d0 Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Wed, 9 Sep 2026 08:35:01 -0500 Subject: [PATCH 1/3] feat: improve search and agent discoverability Restore page-specific descriptions, publish useful homepage Markdown, strengthen observability guidance, and defer inactive AppHost examples. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/frontend/astro.config.mjs | 6 +- src/frontend/config/apphost-examples.mjs | 53 ++++++ ...spire-version-placeholders-integration.mjs | 30 +++- src/frontend/config/head.attrs.ts | 8 - src/frontend/config/homepage-markdown.mjs | 111 +++++++++++++ .../src/components/AppHostBuilder.astro | 7 +- .../src/components/AppHostBuilder.client.ts | 51 +++++- .../src/components/home/HomePage.astro | 38 +++-- .../app-host/migrate-from-docker-compose.mdx | 34 ++-- .../docs/dashboard/ai-coding-agents.mdx | 103 +++++++++--- .../src/content/docs/dashboard/overview.mdx | 22 +-- .../docs/dashboard/standalone-for-nodejs.mdx | 14 +- .../docs/dashboard/standalone-for-python.mdx | 13 +- .../src/content/docs/dashboard/standalone.mdx | 12 +- .../content/docs/fundamentals/telemetry.mdx | 20 ++- .../docs/get-started/ai-coding-agents.mdx | 15 +- .../docs/get-started/aspire-mcp-server.mdx | 9 +- src/frontend/src/content/i18n/da.json | 6 +- src/frontend/src/content/i18n/de.json | 6 +- src/frontend/src/content/i18n/en.json | 8 +- src/frontend/src/content/i18n/es.json | 6 +- src/frontend/src/content/i18n/fr.json | 6 +- src/frontend/src/content/i18n/hi.json | 6 +- src/frontend/src/content/i18n/id.json | 6 +- src/frontend/src/content/i18n/it.json | 6 +- src/frontend/src/content/i18n/ja.json | 6 +- src/frontend/src/content/i18n/ko.json | 6 +- src/frontend/src/content/i18n/pt-BR.json | 6 +- src/frontend/src/content/i18n/ru.json | 6 +- src/frontend/src/content/i18n/tr.json | 6 +- src/frontend/src/content/i18n/uk.json | 6 +- src/frontend/src/content/i18n/zh-CN.json | 6 +- src/frontend/src/utils/page-metadata.ts | 15 +- .../tests/e2e/api-markdown-routes.spec.ts | 25 ++- .../tests/e2e/custom-components.spec.ts | 106 ++++++++++++ src/frontend/tests/e2e/homepage.spec.ts | 18 +++ src/frontend/tests/e2e/og-metadata.spec.ts | 83 ++++++++++ .../unit/apphost-examples.vitest.test.ts | 54 +++++++ .../unit/homepage-markdown.vitest.test.ts | 151 ++++++++++++++++++ 39 files changed, 950 insertions(+), 140 deletions(-) create mode 100644 src/frontend/config/apphost-examples.mjs create mode 100644 src/frontend/config/homepage-markdown.mjs create mode 100644 src/frontend/tests/unit/apphost-examples.vitest.test.ts create mode 100644 src/frontend/tests/unit/homepage-markdown.vitest.test.ts diff --git a/src/frontend/astro.config.mjs b/src/frontend/astro.config.mjs index e00b34af9..3874cdc1d 100644 --- a/src/frontend/astro.config.mjs +++ b/src/frontend/astro.config.mjs @@ -29,6 +29,8 @@ import Icons from 'starlight-plugin-icons'; const modeArgIndex = process.argv.indexOf('--mode'); const isSkipSearchBuild = modeArgIndex >= 0 && process.argv[modeArgIndex + 1] === 'skip-search'; const isBuildTimingEnabled = process.env.BUILD_TIMING === '1'; +const siteDescription = + 'Aspire is a multi-language local dev-time orchestration tool chain for building, running, debugging, and deploying distributed applications.'; // Astro renders pages mostly on the main JS thread. Default `build.concurrency` // is 1, so a multi-vCPU CI runner is largely idle during the generate phase. @@ -61,6 +63,7 @@ export default defineConfig({ starlight: { pagefind: !isSkipSearchBuild, title: 'Aspire', + description: siteDescription, routeMiddleware: ['./src/route-data-middleware'], defaultLocale: 'root', locales, @@ -149,8 +152,7 @@ export default defineConfig({ starlightGitHubAlerts(), starlightLlmsTxt({ projectName: 'Aspire', - description: - 'Aspire is a multi-language local dev-time orchestration tool chain for building, running, debugging, and deploying distributed applications.', + description: siteDescription, // Strip transient annotations injected by expressive-code-twoslash from the // rendered HTML before it's converted back to Markdown. Without this, the // TypeScript hover popovers (type signatures, JSDoc, error boxes, etc.) diff --git a/src/frontend/config/apphost-examples.mjs b/src/frontend/config/apphost-examples.mjs new file mode 100644 index 000000000..1e42a27ae --- /dev/null +++ b/src/frontend/config/apphost-examples.mjs @@ -0,0 +1,53 @@ +import { createHash } from 'node:crypto'; +import { select, selectAll } from 'hast-util-select'; +import rehypeParse from 'rehype-parse'; +import { unified } from 'unified'; + +/** + * Move the builder's highlighted examples to one lazy-loaded static file. + * @param {string} html + * @returns {{ html: string; examples: string; filename: string }} + */ +export function deferAppHostExamples(html) { + const tree = unified().use(rehypeParse).parse(html); + const builder = select('[data-apphost-builder]', tree); + const groups = selectAll('[data-apphost-builder] .code-lang-group', tree); + const initial = select( + '[data-apphost-builder] [data-code-lang="typescript"] [data-variant="frontend"]', + tree + ); + if (!builder || groups.length !== 2 || !initial) { + throw new Error('Cannot defer AppHost examples: missing builder or default example.'); + } + + const source = (node) => html.slice(node.position.start.offset, node.position.end.offset); + const sourceWithoutCopyControls = (node) => { + let markup = source(node); + for (const copy of selectAll('.copy', node).toReversed()) { + const start = copy.position.start.offset - node.position.start.offset; + const end = copy.position.end.offset - node.position.start.offset; + markup = markup.slice(0, start) + markup.slice(end); + } + return markup; + }; + const examples = groups.map(sourceWithoutCopyControls).join('\n'); + const hash = createHash('sha256').update(examples).digest('hex').slice(0, 16); + const filename = `apphost-examples.${hash}.html`; + + // Edit the original source ranges so unrelated homepage markup stays byte-identical. + for (const group of groups.toReversed()) { + const start = group.position.start.offset; + const openingTag = html.slice(start, html.indexOf('>', start) + 1); + const body = + group.properties.dataCodeLang === 'typescript' ? sourceWithoutCopyControls(initial) : ''; + html = + html.slice(0, start) + openingTag + body + '' + html.slice(group.position.end.offset); + } + const attributeOffset = builder.position.start.offset + builder.tagName.length + 1; + html = + html.slice(0, attributeOffset) + + ` data-apphost-examples="/_astro/${filename}"` + + html.slice(attributeOffset); + + return { html, examples, filename }; +} diff --git a/src/frontend/config/aspire-version-placeholders-integration.mjs b/src/frontend/config/aspire-version-placeholders-integration.mjs index 0bc1197c3..9afb037bc 100644 --- a/src/frontend/config/aspire-version-placeholders-integration.mjs +++ b/src/frontend/config/aspire-version-placeholders-integration.mjs @@ -3,12 +3,15 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { replaceAspireVersionPlaceholders } from './remark-aspire-version-placeholders.mjs'; import { orderTypeScriptFirstAppHostTabsInMarkdown } from './remark-typescript-first-apphost-tabs.mjs'; +import { renderHomepageMarkdown } from './homepage-markdown.mjs'; +import { locales } from './locales.ts'; +import { deferAppHostExamples } from './apphost-examples.mjs'; // Per-page Markdown copies emitted by `starlight-page-actions` bypass the // remark transforms that replace Aspire version placeholders and order AppHost // language tabs: // that plugin `viteStaticCopy`s `src/content/docs/**/*.{md,mdx}` straight to -// `dist/**/*.md` through a regex-only transform, so it never runs through the +// `dist/**/*.md` through source cleanup, so it never runs through the // configured remark pipeline. // // Everything else is already handled before it reaches `dist`: @@ -17,10 +20,9 @@ import { orderTypeScriptFirstAppHostTabsInMarkdown } from './remark-typescript-f // - `llms*.txt` -> `starlight-llms-txt` sources rendered HTML (`render(entry)`) // - `reference/**.md` -> generated from API/sample data, not docs content // -// So this post-build pass only needs to touch `.md` files. Scoping it this way -// (instead of walking every `.html`/`.txt` in `dist`) avoids re-reading the bulk -// of the output — including the large `llms-full.txt` assets — which is what -// previously exhausted the Node heap. +// Version normalization only walks `.md` files. The homepage finalization +// below also reads the known homepage HTML files, not every `.html`/`.txt` +// asset in `dist`, which previously exhausted the Node heap. const markdownCopyExtensions = new Set(['.md']); // Process the Markdown copies through a small worker pool rather than a single @@ -33,7 +35,23 @@ export function aspireVersionPlaceholdersIntegration() { name: 'aspire-version-placeholders', hooks: { 'astro:build:done': async ({ dir }) => { - await replaceAspireVersionPlaceholdersInDirectory(fileURLToPath(dir)); + const directory = fileURLToPath(dir); + // Page-actions copies raw MDX, so component-only homepages need their + // rendered content instead. Ordinary documentation keeps its existing path. + for (const locale of Object.keys(locales)) { + const localePath = locale === 'root' ? '' : locale; + const html = await readFile(path.join(directory, localePath, 'index.html'), 'utf8'); + const markdown = await renderHomepageMarkdown(html); + await writeFile(path.join(directory, `${localePath || 'index'}.md`), markdown, 'utf8'); + const deferred = deferAppHostExamples(html); + await writeFile( + path.join(directory, '_astro', deferred.filename), + deferred.examples, + 'utf8' + ); + await writeFile(path.join(directory, localePath, 'index.html'), deferred.html, 'utf8'); + } + await replaceAspireVersionPlaceholdersInDirectory(directory); }, }, }; diff --git a/src/frontend/config/head.attrs.ts b/src/frontend/config/head.attrs.ts index 960bc0b17..32b32feb0 100644 --- a/src/frontend/config/head.attrs.ts +++ b/src/frontend/config/head.attrs.ts @@ -6,14 +6,6 @@ export type HeadAttr = { export const headAttrs: HeadAttr[] = [ // SEO meta tags for discoverability (including legacy ".NET Aspire" branding) - { - tag: 'meta', - attrs: { - name: 'description', - content: - 'Aspire is a multi-language local dev-time orchestration tool chain for building, running, debugging, and deploying distributed applications.', - }, - }, { tag: 'meta', attrs: { diff --git a/src/frontend/config/homepage-markdown.mjs b/src/frontend/config/homepage-markdown.mjs new file mode 100644 index 000000000..b8e802205 --- /dev/null +++ b/src/frontend/config/homepage-markdown.mjs @@ -0,0 +1,111 @@ +import { select, selectAll } from 'hast-util-select'; +import rehypeParse from 'rehype-parse'; +import rehypeRemark from 'rehype-remark'; +import remarkGfm from 'remark-gfm'; +import remarkStringify from 'remark-stringify'; +import { unified } from 'unified'; +import { remove } from 'unist-util-remove'; + +const decorativeContent = [ + 'script', + 'style', + 'svg', + 'button', + 'input', + 'label', + 'img[alt=""]', + 'i[aria-hidden="true"]', + 'span[aria-hidden="true"]', + '.home-hero-eyebrow', + '.section-index > span', + '.principle-number', + '.quote-mark', + '.model-terminal', + '.model-graph', + '[data-model-dashboard]', + '.dashboard-stage', + '.agent-visual', + '.environment-topology', + '.environment-stage-bar', + '[data-home-integration-rail]', + '.agent-badge-popover > strong', +].join(', '); + +/** + * Convert the homepage's authored content, not its animated demonstrations. + * @param {string} html + * @returns {Promise} + */ +export async function renderHomepageMarkdown(html) { + const tree = unified().use(rehypeParse).parse(html); + const hero = select('main .home-hero-copy', tree); + const content = select('main .aspire-home', tree); + if (!hero || !content || !select('h1', hero)) { + throw new Error('Cannot export homepage Markdown: missing homepage content landmarks.'); + } + + tree.children = [hero, content]; + const decorativeNodes = new Set(selectAll(decorativeContent, tree)); + remove(tree, (node) => node.type === 'comment' || decorativeNodes.has(node)); + + for (const actions of selectAll( + '.home-hero-actions, .observability-links, .closing-actions', + tree + )) { + const links = selectAll('a', actions); + actions.tagName = 'ul'; + actions.children = links.map((link) => ({ + type: 'element', + tagName: 'li', + properties: {}, + children: [link], + })); + } + for (const command of selectAll('.environment-command', tree)) { + command.tagName = 'ul'; + command.children = command.children + .filter((child) => child.type === 'element') + .map((child) => ({ + type: 'element', + tagName: 'li', + properties: {}, + children: [child], + })); + } + for (const caption of selectAll('.testimonial figcaption', tree)) { + const name = select('strong', caption); + const attribution = select('small', caption); + if (name && attribution) { + caption.children = [name, { type: 'text', value: ' — ' }, ...attribution.children]; + } + } + + // These are meaningful examples and environment descriptions, even when + // the interactive homepage initially hides their tab or animation stage. + for (const element of selectAll('[hidden], [aria-hidden]', tree)) { + delete element.properties.hidden; + delete element.properties.ariaHidden; + } + for (const pre of selectAll('pre[data-language]', tree)) { + const code = select('code', pre); + if (code) { + code.properties.className = [`language-${pre.properties.dataLanguage}`]; + const lines = selectAll('.ec-line .code', code); + if (lines.length > 0) { + code.children = [ + { + type: 'text', + value: lines.map((line) => textContent(line).replace(/\n/g, '')).join('\n'), + }, + ]; + } + } + } + + const processor = unified().use(rehypeRemark).use(remarkGfm).use(remarkStringify); + return processor.stringify(await processor.run(tree)); +} + +function textContent(node) { + return node.type === 'text' ? node.value : (node.children ?? []).map(textContent).join(''); +} diff --git a/src/frontend/src/components/AppHostBuilder.astro b/src/frontend/src/components/AppHostBuilder.astro index e3d8f7d23..ccea9967b 100644 --- a/src/frontend/src/components/AppHostBuilder.astro +++ b/src/frontend/src/components/AppHostBuilder.astro @@ -1022,7 +1022,12 @@ await builder.build().run();`, }; --- -
+
{heading} {description &&

{description}

} diff --git a/src/frontend/src/components/AppHostBuilder.client.ts b/src/frontend/src/components/AppHostBuilder.client.ts index 757ffab63..9ad6eb425 100644 --- a/src/frontend/src/components/AppHostBuilder.client.ts +++ b/src/frontend/src/components/AppHostBuilder.client.ts @@ -148,11 +148,16 @@ function initializeAppHostBuilder(root: HTMLElement): void { let processing = false; let caretLineIndex = 0; let caretColumn = 0; - - const getTemplate = (language: AppHostLanguage, variant: string): HTMLElement | undefined => - root.querySelector( - `.code-lang-group[data-code-lang="${language}"] .code-variant[data-variant="${variant}"]` - ) ?? undefined; + const examples = document.createElement('template'); + + const getTemplate = (language: AppHostLanguage, variant: string): HTMLElement | undefined => { + const selector = `.code-lang-group[data-code-lang="${language}"] .code-variant[data-variant="${variant}"]`; + return ( + root.querySelector(selector) ?? + examples.content.querySelector(selector) ?? + undefined + ); + }; const setEditorState = (state: EditorState) => { stage.dataset.editorState = state; @@ -518,6 +523,34 @@ function initializeAppHostBuilder(root: HTMLElement): void { processing = true; codeDisplay.setAttribute('aria-busy', 'true'); + const examplesUrl = root.dataset.apphostExamples; + if (examplesUrl && !examples.content.childElementCount) { + try { + const response = await fetch(examplesUrl); + if (!response.ok) { + throw new Error(`AppHost examples request failed: ${response.status}`); + } + const content = await response.text(); + const parsed = document.createElement('template'); + parsed.innerHTML = content; + if ( + !parsed.content.querySelector('[data-code-lang="csharp"] .code-variant') || + !parsed.content.querySelector('[data-code-lang="typescript"] .code-variant') + ) { + throw new Error('AppHost examples response does not contain both languages.'); + } + examples.content.append(parsed.content); + } catch (error) { + console.error('Could not load AppHost examples.', error); + status.textContent = root.dataset.examplesError ?? 'Could not load AppHost examples.'; + status.classList.remove('sr-only'); + codeDisplay.setAttribute('aria-busy', 'false'); + processing = false; + return; + } + } + status.classList.add('sr-only'); + try { while ( root.isConnected && @@ -617,7 +650,13 @@ function initializeAppHostBuilder(root: HTMLElement): void { languageButtons.forEach((button) => { button.addEventListener('click', () => { const language = button.dataset.lang; - if (!isAppHostLanguage(language) || desiredLanguage === language) return; + if (!isAppHostLanguage(language)) return; + if (desiredLanguage === language) { + if (currentLanguage !== desiredLanguage || currentVariant !== desiredVariant) { + void processRequestedState(); + } + return; + } languageButtons.forEach((candidate) => { const isSelected = candidate === button; diff --git a/src/frontend/src/components/home/HomePage.astro b/src/frontend/src/components/home/HomePage.astro index 85004e018..beee69a06 100644 --- a/src/frontend/src/components/home/HomePage.astro +++ b/src/frontend/src/components/home/HomePage.astro @@ -592,15 +592,26 @@ const localizedPrinciples = principles.map((principle) => ({ description={t('home.observability.badgeDescription')} align="center" /> - - {t('home.observability.link')} - - +
({ } .dashboard-heading .text-link { - margin-top: 1.35rem; color: var(--home-purple); } + .observability-links { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem 1.5rem; + margin-top: 1.35rem; + } + .dashboard-stage { width: min(100%, 82rem); margin: clamp(3rem, 5vw, 4.25rem) auto 0; diff --git a/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx b/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx index 5fb84b651..249a48645 100644 --- a/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx +++ b/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx @@ -1,37 +1,39 @@ --- title: Migrate from Docker Compose to Aspire -description: Migrate your Docker Compose applications to Aspire — map services, volumes, networks, and environment variables to AppHost APIs and modernize your developer workflow. +description: Compare Aspire and Docker Compose for local development, service discovery, and observability. Map Compose services and dependencies to TypeScript or C# AppHosts. --- import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; import LearnMore from '@components/LearnMore.astro'; -This guide helps you understand how to migrate applications from Docker Compose to Aspire, highlighting the key conceptual differences and providing accurate, practical examples for common migration scenarios. +Docker Compose and Aspire both describe and run multi-service applications. This guide compares their local development workflows and maps Compose services, dependencies, and configuration to an Aspire AppHost written in TypeScript or C#. ## Understand the differences -While Docker Compose and Aspire might seem similar at first glance, they serve different purposes and operate at different levels of abstraction. +Keep Docker Compose when a container-focused YAML workflow meets your needs. Consider Aspire when you want to compose containers with processes running directly on the host, manage service references in code, and inspect application telemetry alongside resource health. ### Docker Compose vs Aspire -| | Docker Compose | Aspire | -|--|--|--| -| **Primary purpose** | Container orchestration | Development-time orchestration and app composition | -| **Scope** | Container-focused | Multi-resource (containers, .NET projects, cloud resources) | -| **Configuration** | YAML-based | C#-based, strongly typed | -| **Target environment** | Any Docker runtime | Development and cloud deployment | -| **Service discovery** | DNS-based container discovery | Built-in service discovery with environment variables | -| **Development experience** | Manual container management | Integrated tooling, dashboard, and telemetry | +| Feature | Docker Compose | Aspire | +| ------- | -------------- | ------ | +| **Primary purpose** | Define and run multi-container applications | Compose application resources for development and deployment | +| **Scope** | Containers | Containers, Python and Node.js apps, .NET projects, executables, and cloud resources | +| **Configuration** | Declarative YAML | Strongly typed TypeScript or C# AppHost | +| **Target environment** | Docker environments | Local development and deployment through publishing integrations, including Docker Compose | +| **Service discovery** | Service names and DNS on Compose networks | Service references and connection information passed to resources | +| **Local observability** | Container logs and health checks; add OpenTelemetry tooling for application telemetry | Integrated resource health, console logs, and an OpenTelemetry dashboard; application instrumentation is still required | ### Key conceptual shifts When migrating from Docker Compose to Aspire, consider these conceptual differences: -- **From YAML to C#** — Configuration moves from declarative YAML to imperative, strongly-typed C# code -- **From containers to resources** — Aspire manages not just containers, but .NET projects, executables, parameters, and cloud resources -- **From manual networking to service discovery** — Aspire automatically configures service discovery and connection strings -- **From development gaps to integrated experience** — Aspire provides dashboard, telemetry, and debugging integration -- **Startup orchestration differs** — Docker Compose `depends_on` controls startup order, while Aspire `WithReference` only configures service discovery; use `WaitFor` for startup ordering +- **From YAML to an AppHost** — Express configuration in strongly typed TypeScript or C# code +- **From containers to resources** — Compose containers with local application processes, parameters, and cloud resources +- **From container DNS to resource references** — Pass service endpoints and connection information to dependent resources +- **From separate tools to a shared dashboard** — Inspect resource health and instrumented application logs, traces, and metrics together +- **Startup orchestration differs** — Compose `depends_on` supports startup order and health conditions; Aspire references supply connection information, while wait relationships control startup dependencies + +You don't need to migrate orchestration just to view OpenTelemetry. Point instrumented Compose services at the [standalone Aspire dashboard](/dashboard/standalone/) to inspect logs, traces, and metrics, including through [coding-agent CLI or MCP workflows](/dashboard/ai-coding-agents/#standalone-mode). Standalone mode doesn't add AppHost resource controls to Compose. For detailed API mappings, see [Docker Compose to Aspire AppHost API reference](/app-host/docker-compose-to-apphost-reference/). diff --git a/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx b/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx index 1b36b157a..7a39570d6 100644 --- a/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx +++ b/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx @@ -1,16 +1,15 @@ --- -title: AI coding agents and the Aspire Dashboard -seoTitle: Aspire dashboard and AI coding agents for distributed apps -description: Learn how AI coding agents use the Aspire CLI and MCP server to read logs and telemetry from the Aspire dashboard, diagnose failures, and propose code changes. +title: Debug app failures with AI coding agents +description: Investigate a local request failure using the same application logs, traces, and resource health in the Aspire dashboard and coding-agent CLI or MCP tools. --- -import { Aside, Steps } from '@astrojs/starlight/components'; +import { Steps } from '@astrojs/starlight/components'; -AI coding agents can use the [Aspire CLI](/reference/cli/overview/) and [Aspire MCP server](/get-started/aspire-mcp-server/) to fetch logs and telemetry from the Aspire dashboard. This gives agents the same observability data that developers see in the dashboard UI — structured logs, distributed traces, resource status, and console output — so they can diagnose issues, verify fixes, and add new features with full context. +Give a coding agent the same application evidence you inspect in the Aspire dashboard: resource status and health, console output, structured logs, and distributed traces. Use it to diagnose local request failures and verify fixes, not to monitor the agent's reasoning or token usage. Start with [Aspire skills](/get-started/aspire-skills/) and the [Aspire CLI](/reference/cli/overview/); the [MCP server](/get-started/aspire-mcp-server/) is optional. ## How agents use dashboard data -When an Aspire app is running, the dashboard collects OpenTelemetry data from all resources. AI coding agents access this data through two channels: +When an Aspire app is running, the dashboard receives OpenTelemetry from instrumented applications and resource information from the AppHost. AI coding agents access this data through two channels: - **Aspire CLI** — Commands like [`aspire logs`](/reference/cli/commands/aspire-logs/), [`aspire otel logs`](/reference/cli/commands/aspire-otel-logs/), [`aspire otel traces`](/reference/cli/commands/aspire-otel-traces/), and [`aspire describe`](/reference/cli/commands/aspire-describe/) retrieve resource status, console logs, and telemetry directly from the terminal. All commands support `--format Json` for structured output that agents can parse. - **Aspire MCP server** — The [MCP server](/get-started/aspire-mcp-server/) exposes tools such as `list_resources`, `list_structured_logs`, `list_traces`, and `list_console_logs` that agents call directly through the Model Context Protocol. @@ -31,20 +30,80 @@ The following workflow is an example — [Aspire skills](/get-started/aspire-ski +## Investigate one failed request together + +This exercise uses the existing [FastAPI weather sample](https://github.com/microsoft/aspire-samples/tree/383d5d025b53ac37cd4dd86f50a036ccf39f76f5/samples/aspire-with-python), pinned to revision `383d5d0`. It runs a Python API named `app`, Redis named `cache`, and a React frontend. The sample uses a C# AppHost; the inspection commands also work with TypeScript AppHosts. + +Use a separate local copy of the [pinned sample source](https://github.com/microsoft/aspire-samples/archive/383d5d025b53ac37cd4dd86f50a036ccf39f76f5.zip), not an application someone else is using. Follow the sample's prerequisites: the Aspire CLI, .NET 10 SDK, Python 3.13 or later, Node.js 22.21.1 or later, and a running container runtime. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) for the Python package setup. Keep dashboard authentication enabled and don't share login tokens or runtime credentials with the agent. + + + +1. Open a terminal in the sample's `aspire-with-python` directory and start an isolated instance: + + ```bash title="Start only the sample AppHost" + aspire start --apphost apphost.cs --isolated + aspire wait app --apphost apphost.cs + aspire describe app --apphost apphost.cs --format Json + ``` + + Open the dashboard login URL printed by `aspire start`. Find the `app` endpoint on **Resources**, then visit `/api/weatherforecast` at that endpoint. The baseline request returns HTTP 200 and five synthetic forecasts. If your terminal doesn't resolve the generated `.dev.localhost` hostname, use the `localhost` endpoint alias in `aspire describe`. + +1. In the `app` directory's `main.py`, temporarily change `random.randint(-20, 55)` to `random.randint(55, -20)`. This reverses the valid temperature range. Restart only the Python resource: + + ```bash title="Restart the sample API" + aspire resource app restart --apphost apphost.cs + aspire wait app --apphost apphost.cs + ``` + + Wait for the sample's five-second forecast cache to expire, then request `/api/weatherforecast` again. It returns HTTP 500 even though `app` is running and its `/health` check passes. + +1. Investigate as a human. In **Console**, select `app` and find the `ValueError` stack trace pointing to `random.randint(55, -20)`. In **Traces**, open `GET /api/weatherforecast` at the failure's timestamp. Its server span has error status and HTTP 500; the Redis `GET` span succeeds. Copy the trace ID so the agent can inspect this exact request, rather than an unrelated health-check trace. + + The exception is in Uvicorn's console output. Don't assume every console line is also exported as a structured OpenTelemetry log. + +1. Ask the coding agent to investigate the same evidence: + + > In this sample directory, use `apphost.cs` to inspect the failed weather request with trace ID `` and the `app` console logs. Explain why the health check passes, propose the smallest fix, and verify the same request after the fix. + + The agent can query the same resource, console output, and trace: + + ```bash title="Query the evidence shown in the dashboard" + aspire describe app --apphost apphost.cs --format Json + aspire logs app --apphost apphost.cs --format Json + aspire otel traces app --apphost apphost.cs --has-error true --format Json + aspire otel traces --apphost apphost.cs --trace-id "" --format Json + ``` + + Replace `` with the ID from the dashboard. With optional MCP setup, `list_resources`, `list_console_logs`, and `list_traces` provide the corresponding evidence. + +1. Restore `random.randint(-20, 55)`, restart `app` with the commands above, and repeat `/api/weatherforecast`. Confirm HTTP 200 and five forecasts. In **Traces**, inspect the new request, or query it: + + ```bash title="Verify the new weather request" + aspire otel traces app --apphost apphost.cs --search "name:weatherforecast" --limit 1 --format Json + ``` + + The new trace has no error. Old failed traces remain in the dashboard; success means the repeated request now works, not that error history disappears. Stop only this sample when you're finished: + + ```bash title="Stop the sample AppHost" + aspire stop --apphost apphost.cs + ``` + + + ## Get started The fastest way to set up AI coding agents with Aspire is the `aspire agent init` command. See [Use AI coding agents](/get-started/ai-coding-agents/) for the full setup guide, including skill files, MCP server configuration, and supported AI assistants. ## Standalone mode -The Aspire CLI and MCP server work with the [standalone dashboard](/dashboard/standalone/) — you don't need an Aspire AppHost project. This is useful when monitoring any application that sends OpenTelemetry data to the dashboard. +The Aspire CLI and MCP server can query telemetry from the [standalone dashboard](/dashboard/standalone/) without an AppHost. This works with applications configured to send OTLP data, including [Python](/dashboard/standalone-for-python/) and [Node.js](/dashboard/standalone-for-nodejs/). Standalone telemetry doesn't provide AppHost process health or lifecycle commands. ### Start the standalone dashboard Start the dashboard using the Aspire CLI: ```bash title="Aspire CLI" -aspire dashboard run --allow-anonymous +aspire dashboard run ``` The dashboard starts with the following defaults: @@ -53,18 +112,16 @@ The dashboard starts with the following defaults: - **OTLP/gRPC** endpoint at `http://localhost:4317` - **OTLP/HTTP** endpoint at `http://localhost:4318` -:::caution -The `--allow-anonymous` flag starts the dashboard without authentication. Only use this on your local machine. See [Dashboard security considerations](/dashboard/security-considerations/#anonymous-access) for more information. -::: +Keep the default browser-token authentication. The CLI prints a login URL; use it locally and don't commit or share its token. See [Dashboard security considerations](/dashboard/security-considerations/). ### Use Aspire CLI with the standalone dashboard -Pass `--dashboard-url` with the full frontend URL to point CLI commands at a standalone dashboard: +Pass the full login URL through `--dashboard-url` to point CLI commands at the standalone dashboard. The CLI exchanges the browser token for an API key: ```bash title="Aspire CLI" -aspire otel logs --dashboard-url "http://localhost:18888" -aspire otel traces --dashboard-url "http://localhost:18888" -aspire otel spans --dashboard-url "http://localhost:18888" +aspire otel logs --dashboard-url "http://localhost:18888/login?t=" +aspire otel traces --dashboard-url "http://localhost:18888/login?t=" +aspire otel spans --dashboard-url "http://localhost:18888/login?t=" ``` ### Use Aspire MCP with the standalone dashboard @@ -72,10 +129,10 @@ aspire otel spans --dashboard-url "http://localhost:18888" Start the MCP server in dashboard-only mode using `--dashboard-url`: ```bash title="Aspire CLI" -aspire agent mcp --dashboard-url "http://localhost:18888" +aspire agent mcp --dashboard-url "http://localhost:18888/login?t=" ``` -This exposes the dashboard's telemetry tools (structured logs, traces, and resource data) to any MCP-compatible AI assistant. +This exposes the dashboard's telemetry tools to an MCP-compatible AI assistant, not the AppHost's resource-management tools. Configuration is required in the agent to use the MCP server. For configuration details, see [Aspire MCP server configuration](/get-started/aspire-mcp-server/#configuration). The `--dashboard-url` must be passed as a command line argument when configuring the MCP server for standalone use. @@ -94,35 +151,37 @@ description: Use the Aspire standalone dashboard for observability. Start the da ## Start the dashboard ```bash -aspire dashboard run --allow-anonymous +aspire dashboard run ``` The dashboard UI is at http://localhost:18888. Apps should send OpenTelemetry to http://localhost:4317 (gRPC) or http://localhost:4318 (HTTP). +Keep authentication enabled and use the login URL printed by the CLI. ## Query telemetry View structured logs: ```bash -aspire otel logs --dashboard-url http://localhost:18888 +aspire otel logs --dashboard-url "http://localhost:18888/login?t=" ``` View distributed traces: ```bash -aspire otel traces --dashboard-url http://localhost:18888 +aspire otel traces --dashboard-url "http://localhost:18888/login?t=" ``` View trace spans: ```bash -aspire otel spans --dashboard-url http://localhost:18888 +aspire otel spans --dashboard-url "http://localhost:18888/login?t=" ``` ## Rules -- Ensure the dashboard has started querying telemetry. The dashboard may already be running. +- Check whether the dashboard is already running before starting it. +- Replace with the local login token; never commit or include it in reports. - Use `--format Json` with CLI commands when you need to parse the output. - Check `aspire otel logs` for errors after making code changes. - Use `aspire otel traces` to investigate cross-service latency. diff --git a/src/frontend/src/content/docs/dashboard/overview.mdx b/src/frontend/src/content/docs/dashboard/overview.mdx index 8777c3707..1089f459c 100644 --- a/src/frontend/src/content/docs/dashboard/overview.mdx +++ b/src/frontend/src/content/docs/dashboard/overview.mdx @@ -1,6 +1,6 @@ --- -title: Aspire dashboard overview and getting started -description: Overview of the Aspire dashboard — what it shows, how the AppHost wires it up, and how to start using it for telemetry, resource management, and debugging. +title: Aspire dashboard for local OpenTelemetry +description: View application logs, distributed traces, and metrics in the Aspire dashboard. Inspect local resource health with an AppHost or receive OTLP data standalone. --- import { Image } from 'astro:assets'; @@ -9,20 +9,20 @@ import projectsImage from '@assets/dashboard/explore/projects.png'; import architectureDiagramDark from '@assets/dashboard/architecture-diagram-dark.svg'; import architectureDiagramLight from '@assets/dashboard/architecture-diagram-light.svg'; -[Aspire](/get-started/what-is-aspire/) project templates include a sophisticated dashboard for comprehensive app monitoring and inspection. The dashboard is also available in [standalone mode](#standalone-mode). +The Aspire dashboard is a local OpenTelemetry viewer for application logs, distributed traces, and metrics. Use it to follow requests across instrumented services, find errors, and inspect timing without leaving your development environment. -The dashboard enables real-time tracking of key aspects of your app, including logs, traces, and environment configurations. It's designed to enhance the development experience by providing a clear and insightful view of your app's state and structure. +With an [Aspire AppHost](/get-started/app-host/), the dashboard also shows resource state, health checks, console output, and configuration. You can use the [standalone dashboard](#standalone-mode) to receive telemetry without adopting Aspire orchestration. Key features of the dashboard include: -- Real-time tracking of logs, traces, and environment configurations. -- User interface to [stop, start, and restart resources](/dashboard/explore/#resource-actions). -- Collects and displays logs and telemetry; [view structured logs, traces, and metrics](/dashboard/explore/#monitoring-pages) in an intuitive UI. -- Enhanced debugging with [AI coding agents](/dashboard/ai-coding-agents/) that use the Aspire CLI and MCP server to fetch logs and telemetry from the dashboard. +- [View structured logs, traces, and metrics](/dashboard/explore/#monitoring-pages) from apps configured to export OpenTelemetry. +- Follow a distributed trace across local services and correlate its logs. +- Inspect resource health and [stop, start, or restart resources](/dashboard/explore/#resource-actions) when connected to an AppHost. +- Share application observability data with [AI coding agents](/dashboard/ai-coding-agents/) through the Aspire CLI or optional MCP server. ## Use the dashboard with Aspire projects -The dashboard is integrated into the [Aspire _*.AppHost_](/get-started/app-host/). During development the dashboard is automatically launched when you start the project. It's configured to display the Aspire project's resources and telemetry. +The dashboard is integrated with [TypeScript and C# AppHosts](/get-started/app-host/). During development, starting your AppHost launches the dashboard and configures its connection to your resources. Applications still need instrumentation and an OpenTelemetry exporter to send telemetry. A screenshot of the Aspire dashboard Resources page. @@ -30,7 +30,7 @@ For more information about using the dashboard during Aspire development, see [E ## Standalone mode -The Aspire dashboard can run standalone, without the rest of Aspire. The standalone dashboard provides a great UI for viewing telemetry and can be used by any application that sends OpenTelemetry data. You can start it with the Aspire CLI or run it from the standalone container image. For more information, see the [Standalone Aspire dashboard](/dashboard/standalone/). +The standalone dashboard is an OTLP viewer for any application configured to send OpenTelemetry data, including [Python](/dashboard/standalone-for-python/) and [JavaScript on Node.js](/dashboard/standalone-for-nodejs/). Start it with the Aspire CLI or a container image. Standalone mode doesn't provide AppHost resource lifecycle controls; see [Standalone Aspire dashboard](/dashboard/standalone/) for setup and limitations. ## Configuration @@ -58,3 +58,5 @@ For more information, see [Aspire dashboard security considerations](/dashboard/ ## Next steps - [Explore the Aspire dashboard](/dashboard/explore/) +- [Understand OpenTelemetry and distributed tracing](/fundamentals/telemetry/) +- [Investigate application failures with a coding agent](/dashboard/ai-coding-agents/) diff --git a/src/frontend/src/content/docs/dashboard/standalone-for-nodejs.mdx b/src/frontend/src/content/docs/dashboard/standalone-for-nodejs.mdx index 2c87f0176..60c9f31ec 100644 --- a/src/frontend/src/content/docs/dashboard/standalone-for-nodejs.mdx +++ b/src/frontend/src/content/docs/dashboard/standalone-for-nodejs.mdx @@ -1,15 +1,17 @@ --- -title: Aspire dashboard standalone for Node.js apps -description: Use the Aspire dashboard standalone with Node.js applications — wire up OpenTelemetry, point OTLP exporters at the dashboard, and visualize logs, traces, and metrics. +title: Node.js OpenTelemetry with the Aspire dashboard +description: View JavaScript and Node.js telemetry in a local Aspire dashboard. Instrument an Express app and export OpenTelemetry traces and metrics over OTLP. --- import { Steps } from '@astrojs/starlight/components'; import { Kbd } from 'starlight-kbd/components'; -The [Aspire dashboard](/dashboard/overview/) provides a great user experience for viewing telemetry. You can run it standalone with the [Aspire CLI](/reference/cli/overview/) or the standalone dashboard container image for any OpenTelemetry-enabled app. In this article, you'll learn how to: +Use the [standalone Aspire dashboard](/dashboard/standalone/) to inspect JavaScript application telemetry from Node.js locally, without an AppHost. This tutorial configures the OpenTelemetry JavaScript SDK and Node.js instrumentation for an Express app, then exports traces and metrics over OTLP. + +In this article, you'll learn how to: - Start the Aspire dashboard in standalone mode. -- Use the Aspire dashboard with a Node.js app. +- Instrument a Node.js app and inspect its requests in the dashboard. ## Prerequisites @@ -228,6 +230,8 @@ The **Traces** page shows distributed traces for HTTP requests. Each request to The **Metrics** page displays various metrics collected from your Node.js application, including HTTP request metrics, Node.js runtime metrics, and custom metrics if you choose to add them. +The `console.log` calls in this example write to your terminal, not the dashboard's **Structured logs** page. To export logs, configure OpenTelemetry logging with a supported logging library and an OTLP log exporter. See the [OpenTelemetry JavaScript documentation](https://opentelemetry.io/docs/languages/js/). + ## Add custom telemetry (optional) You can enhance your application with custom spans and metrics. Here's an example of adding a custom metric to track API requests: @@ -305,4 +309,4 @@ app.listen(port, () => { You have successfully used the Aspire dashboard with a Node.js application. To learn more about the Aspire dashboard, see the [Aspire dashboard overview](/dashboard/overview/) and how to orchestrate a Node.js application with the Aspire AppHost. -To learn more about OpenTelemetry instrumentation for Node.js applications, see the [OpenTelemetry JavaScript documentation](https://opentelemetry.io/docs/languages/js/). \ No newline at end of file +To follow requests across instrumented services, see [OpenTelemetry and distributed tracing](/fundamentals/telemetry/). To let a coding agent query the same telemetry, see [AI coding agents with the standalone dashboard](/dashboard/ai-coding-agents/#standalone-mode). \ No newline at end of file diff --git a/src/frontend/src/content/docs/dashboard/standalone-for-python.mdx b/src/frontend/src/content/docs/dashboard/standalone-for-python.mdx index 84e204916..373bd3b6c 100644 --- a/src/frontend/src/content/docs/dashboard/standalone-for-python.mdx +++ b/src/frontend/src/content/docs/dashboard/standalone-for-python.mdx @@ -1,6 +1,6 @@ --- -title: Aspire dashboard standalone for Python apps -description: Use the Aspire dashboard standalone with Python applications — configure OpenTelemetry exporters, send OTLP data to the dashboard, and inspect telemetry in real time. +title: Python OpenTelemetry with the Aspire dashboard +description: Send Python FastAPI logs, traces, and metrics to a local Aspire dashboard. Configure OpenTelemetry and OTLP exporters without running an AppHost. --- import { Image } from 'astro:assets'; @@ -13,10 +13,12 @@ import ThemeImage from '@components/ThemeImage.astro'; import aspireDashboardPythonLogs from '@assets/dashboard/standalone/aspire-dashboard-python-logs.png'; import aspireDashboardPythonLogsLight from '@assets/dashboard/standalone/aspire-dashboard-python-logs-light.png'; -The [Aspire dashboard](/dashboard/overview/) provides a great user experience for viewing telemetry. You can run it standalone with the [Aspire CLI](/reference/cli/overview/) or the standalone dashboard container image for any OpenTelemetry-enabled app. In this article, you'll learn how to: +Use the [standalone Aspire dashboard](/dashboard/standalone/) to view Python application telemetry locally. This tutorial configures the OpenTelemetry Python SDK and FastAPI instrumentation to export logs, traces, and metrics over OTLP. The Python app runs directly, without an AppHost; the dashboard doesn't instrument it automatically. + +In this article, you'll learn how to: - Start the Aspire dashboard in standalone mode. -- Use the Aspire dashboard with a Python app. +- Configure and inspect telemetry from a Python FastAPI app. ## Prerequisites @@ -215,6 +217,8 @@ With both the dashboard and your Python application running, you can now view te +The `/simulate-error` endpoint writes warning and error logs but returns a successful HTTP response. An error-level log isn't itself a failed request. For a request failure investigated through both the dashboard and CLI, see [Debug application failures with AI coding agents](/dashboard/ai-coding-agents/). + The structured logs page displays logs from your application with rich filtering and search capabilities: `. These APIs correspond to telemetry features like logging, tracing, and metrics. -Aspire projects define OpenTelemetry SDK configurations in the service defaults project. By default, the `ConfigureOpenTelemetry` method enables logging, tracing, and metrics for the app. It also adds exporters for these data points so they can be collected by other monitoring tools. +.NET projects using service defaults define OpenTelemetry SDK configuration there. By default, the `ConfigureOpenTelemetry` method enables logging, tracing, and metrics for the app. It also configures exporters so other monitoring tools can collect these signals. For more information, see [Service defaults](/get-started/csharp-service-defaults/). @@ -74,7 +81,7 @@ All of these steps happen internally, so in most cases you simply need to run th ### Use telemetry with AI coding agents -AI coding agents can access Aspire telemetry to diagnose issues, inspect app behavior, and monitor resources without requiring you to manually copy data from the dashboard. The Aspire CLI is built for agent-driven workflows—commands support non-interactive execution and `--format Json` for structured output that agents can parse. +AI coding agents can access the same application telemetry you inspect in the dashboard to diagnose failures and verify fixes. This is observability of your application for coding agents, not monitoring of the agents' reasoning or token usage. [Aspire skills](/get-started/aspire-skills/) teach CLI-first workflows, including non-interactive commands and `--format Json` output. When running an Aspire AppHost, agents use CLI commands like `aspire otel logs`, `aspire otel traces`, and `aspire describe` to fetch structured logs, distributed traces, and resource status directly from the dashboard. A typical agent workflow starts the app with `aspire start`, waits for resources with `aspire wait`, then queries telemetry to diagnose issues and verify fixes. @@ -83,6 +90,7 @@ When using a standalone dashboard, agents pass `--dashboard-url` to point CLI co - [AI coding agents and Aspire](/get-started/ai-coding-agents/) — using AI agents with an Aspire AppHost +- [Debug application failures with AI coding agents](/dashboard/ai-coding-agents/) — investigate the same logs and traces as a human - [AI coding agents and the Aspire Dashboard](/dashboard/ai-coding-agents/#standalone-mode) — using AI agents with a standalone dashboard diff --git a/src/frontend/src/content/docs/get-started/ai-coding-agents.mdx b/src/frontend/src/content/docs/get-started/ai-coding-agents.mdx index 0641dfcb9..b70720a19 100644 --- a/src/frontend/src/content/docs/get-started/ai-coding-agents.mdx +++ b/src/frontend/src/content/docs/get-started/ai-coding-agents.mdx @@ -1,7 +1,6 @@ --- -title: Use AI coding agents -seoTitle: Use AI coding agents with Aspire AppHost projects today -description: Set up AI coding agents to work with Aspire — install skills, add optional tools, and guide agents through distributed-app workflows. +title: Use AI coding agents with Aspire observability +description: Set up Aspire skills so coding agents can inspect application logs, traces, and resource health, diagnose local failures, and verify fixes with runtime evidence. --- import { Aside, Steps, TabItem, Tabs } from '@astrojs/starlight/components'; @@ -9,11 +8,11 @@ import AsciinemaPlayer from '@components/AsciinemaPlayer.astro'; import LearnMore from '@components/LearnMore.astro'; import LoopingVideo from '@components/LoopingVideo.astro'; -Aspire provides a first-class setup experience for AI coding agents. Run `aspire agent init` in your project and your AI assistant — whether it's GitHub Copilot, Claude Code, or another MCP-compatible tool — can immediately understand, build, debug, and monitor your distributed applications. +Give your AI coding agent access to application observability, not just source code. Run `aspire agent init` in your project to install skills that teach your assistant to use the Aspire CLI, inspect local services, and work with TypeScript or C# AppHosts. A runtime MCP server is optional. ## Why Aspire for coding agents -Aspire gives coding agents the same visibility into your running application that a developer has. The resource data, structured logs, and distributed traces you see in the [Aspire Dashboard](/dashboard/overview/) are exposed to agents through the [Aspire MCP server](/get-started/aspire-mcp-server/) and the [Aspire CLI](/get-started/install-cli/). Whether a person is debugging in the dashboard or an agent is diagnosing through MCP, they see the same picture. +Aspire gives coding agents access to the resource status, health checks, application logs, and distributed traces you inspect in the [Aspire dashboard](/dashboard/overview/). Start with [Aspire skills](/get-started/aspire-skills/) and the [Aspire CLI](/get-started/install-cli/); add the [Aspire MCP server](/get-started/aspire-mcp-server/) if you want runtime tools exposed through MCP. Both paths let an agent investigate the same request evidence as a developer. The Aspire CLI is built for agent-driven workflows — commands support non-interactive execution to avoid blocking on prompts, and many commands support `--format Json` for structured plain text output. Key commands include `aspire start` (background execution), `aspire start --isolated` (parallel worktrees), `aspire wait` (block until healthy), `aspire describe`, `aspire logs`, and `aspire docs search`. @@ -62,7 +61,7 @@ Aspire skill files teach your AI coding agent how to use Aspire CLI workflows, r ### Aspire MCP server -The MCP server gives your AI agent direct runtime access to your running Aspire application — resource status, logs, traces, and commands. See [Aspire MCP server](/get-started/aspire-mcp-server/) for configuration details, available tools, and the security model. +The optional MCP server gives your AI agent direct runtime access to your running Aspire application — resource status, logs, traces, and commands. CLI skills can already query runtime data without MCP. See [Aspire MCP server](/get-started/aspire-mcp-server/) for configuration details, available tools, and the security model. ## Migrate from AGENTS.md @@ -92,8 +91,12 @@ Once configured, start your preferred AI coding environment. Try asking your age > "Analyze HTTP request performance for my API." +> "Investigate this failed request using its trace ID and correlated logs. After the fix, repeat the request and verify the response and new trace." + > "Add a Redis cache to my AppHost." +For a worked investigation using the same evidence in the dashboard and CLI, see [Debug application failures with AI coding agents](/dashboard/ai-coding-agents/). Passing resource health checks alone doesn't prove that a failing request is fixed. + MCP server gives AI coding agents direct runtime access to your running Aspire application. Through the Model Context Protocol (MCP), agents can query resource status, read logs, inspect distributed traces, and execute commands — without you copy-pasting terminal output. +The Aspire MCP server exposes application observability to AI coding agents. Agents can query resource status and health, read application logs, inspect distributed traces, and execute resource commands. They use the same runtime evidence you inspect in the [Aspire dashboard](/dashboard/overview/), rather than inferring application behavior from source code alone. :::tip[Aspire skills are preferred] -For most AI coding-agent workflows, install [Aspire skills](/get-started/aspire-skills/) first. Aspire skills are the preferred way to teach agents Aspire commands, workflows, and AppHost conventions. Add the Aspire MCP server when the agent also needs live runtime data such as resource status, logs, traces, or resource commands. +For most AI coding-agent workflows, install [Aspire skills](/get-started/aspire-skills/) first. Skills teach agents Aspire CLI commands, workflows, and TypeScript or C# AppHost conventions, including how to query live runtime data. Add the optional MCP server when you prefer to expose that data through MCP tools instead. ::: To set up your project for AI coding agents, see [Use AI coding agents](/get-started/ai-coding-agents/). + For a request-level investigation, see [Debug application failures with AI coding agents](/dashboard/ai-coding-agents/). ## Configuration diff --git a/src/frontend/src/content/i18n/da.json b/src/frontend/src/content/i18n/da.json index 8c7134c57..fa2e936f3 100644 --- a/src/frontend/src/content/i18n/da.json +++ b/src/frontend/src/content/i18n/da.json @@ -245,7 +245,10 @@ "body": "Følg en forespørgsel på tværs af ressourcer, skift mellem strukturerede logge og traces, inspicér metrikker, og handl på ressourcesundhed fra ét udviklerkontrolpanel.", "badgeLabel": "Agenter handler på disse signaler", "badgeDescription": "Aspire giver tilsluttede AI-værktøjer kontekst fra kontrolpanelet, herunder logge, traces, metrikker, sundhed og ressourcekommandoer.", - "link": "Udforsk Aspire-kontrolpanelet" + "link": "Udforsk Aspire-kontrolpanelet", + "standaloneLink": "Selvstændigt kontrolpanel", + "telemetryLink": "OpenTelemetry-begreber", + "agentsLink": "Fejlfind med kodningsagenter" }, "integrations": { "index": "Udvidelig som standard", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "AppHost-eksemplerne kunne ikke indlæses. Vælg en mulighed for at prøve igen.", "heading": "Byg din {{appHost}}", "description": "Slå forskellige funktioner til/fra for at se hvordan Aspire definerer dele af din stack.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/de.json b/src/frontend/src/content/i18n/de.json index 1c4faed64..ba83dae99 100644 --- a/src/frontend/src/content/i18n/de.json +++ b/src/frontend/src/content/i18n/de.json @@ -245,7 +245,10 @@ "body": "Verfolge eine Anfrage über Ressourcen hinweg, wechsle zwischen strukturierten Logs und Traces, prüfe Metriken und reagiere in einem Entwickler-Dashboard auf den Ressourcenzustand.", "badgeLabel": "Agenten handeln auf Basis dieser Signale", "badgeDescription": "Aspire gibt verbundenen KI-Tools Dashboard-Kontext, einschließlich Logs, Traces, Metriken, Zustand und Ressourcenbefehlen.", - "link": "Aspire-Dashboard entdecken" + "link": "Aspire-Dashboard entdecken", + "standaloneLink": "Eigenständiges Dashboard", + "telemetryLink": "OpenTelemetry-Konzepte", + "agentsLink": "Mit Coding-Agenten debuggen" }, "integrations": { "index": "Standardmäßig erweiterbar", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "AppHost-Beispiele konnten nicht geladen werden. Wähle eine Option, um es erneut zu versuchen.", "heading": "Baue dein {{appHost}}", "description": "Funktionen an/aus schalten um zu sehen wie Aspire Teile deines Stacks definiert.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/en.json b/src/frontend/src/content/i18n/en.json index bc088476f..62d070fed 100644 --- a/src/frontend/src/content/i18n/en.json +++ b/src/frontend/src/content/i18n/en.json @@ -237,10 +237,13 @@ "observability": { "index": "Observability included", "title": "See the whole application.", - "body": "Follow requests across resources, logs, traces, metrics, and health from one developer dashboard.", + "body": "View OpenTelemetry logs, traces, and metrics locally. Follow requests across services with your team or coding agent.", "badgeLabel": "Agents act on these signals", "badgeDescription": "Aspire gives connected AI tools dashboard context, including logs, traces, metrics, health, and resource commands.", - "link": "Explore the Aspire dashboard" + "link": "Explore the Aspire dashboard", + "standaloneLink": "Standalone dashboard", + "telemetryLink": "OpenTelemetry concepts", + "agentsLink": "Debug with coding agents" }, "integrations": { "index": "Extensible by default", @@ -493,6 +496,7 @@ } }, "appHostBuilder": { + "examplesError": "Could not load AppHost examples. Select an option to try again.", "heading": "Build your {{appHost}}", "description": "Toggle different features on/off to see how Aspire defines different parts of your stack.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/es.json b/src/frontend/src/content/i18n/es.json index 4d685cf3d..a48087ed5 100644 --- a/src/frontend/src/content/i18n/es.json +++ b/src/frontend/src/content/i18n/es.json @@ -245,7 +245,10 @@ "body": "Sigue una solicitud entre recursos, alterna entre registros estructurados y trazas, inspecciona métricas y actúa sobre el estado de los recursos desde un solo panel para desarrolladores.", "badgeLabel": "Los agentes actúan sobre estas señales", "badgeDescription": "Aspire proporciona a las herramientas de IA conectadas contexto del panel, incluidos registros, trazas, métricas, estado y comandos de recursos.", - "link": "Explora el panel de Aspire" + "link": "Explora el panel de Aspire", + "standaloneLink": "Panel independiente", + "telemetryLink": "Conceptos de OpenTelemetry", + "agentsLink": "Depura con agentes de programación" }, "integrations": { "index": "Extensible de forma predeterminada", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "No se pudieron cargar los ejemplos de AppHost. Selecciona una opción para volver a intentarlo.", "heading": "Crea tu {{appHost}}", "description": "Activa o desactiva diferentes características para ver cómo Aspire define las distintas partes de tu pila.", "frontend": "Interfaz web", diff --git a/src/frontend/src/content/i18n/fr.json b/src/frontend/src/content/i18n/fr.json index ff01c7060..0ed90a9ae 100644 --- a/src/frontend/src/content/i18n/fr.json +++ b/src/frontend/src/content/i18n/fr.json @@ -245,7 +245,10 @@ "body": "Suivez une requête entre les ressources, passez des journaux structurés aux traces, inspectez les métriques et agissez sur l’état d’intégrité des ressources depuis un seul tableau de bord développeur.", "badgeLabel": "Les agents agissent sur ces signaux", "badgeDescription": "Aspire fournit aux outils d’IA connectés le contexte du tableau de bord, y compris les journaux, traces, métriques, l’état d’intégrité et les commandes de ressources.", - "link": "Explorer le tableau de bord Aspire" + "link": "Explorer le tableau de bord Aspire", + "standaloneLink": "Tableau de bord autonome", + "telemetryLink": "Concepts OpenTelemetry", + "agentsLink": "Déboguer avec des agents de programmation" }, "integrations": { "index": "Extensible par défaut", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "Impossible de charger les exemples AppHost. Sélectionnez une option pour réessayer.", "heading": "Construisez votre {{appHost}}", "description": "Activez/désactivez des fonctionnalités pour voir comment Aspire définit les parties de votre pile.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/hi.json b/src/frontend/src/content/i18n/hi.json index 27dd04396..341da99a9 100644 --- a/src/frontend/src/content/i18n/hi.json +++ b/src/frontend/src/content/i18n/hi.json @@ -245,7 +245,10 @@ "body": "एक डेवलपर डैशबोर्ड से संसाधनों के पार अनुरोध को ट्रैक करें, संरचित लॉग और ट्रेस के बीच जाएँ, मेट्रिक्स देखें और संसाधन स्वास्थ्य पर कार्रवाई करें.", "badgeLabel": "एजेंट इन संकेतों पर कार्रवाई करते हैं", "badgeDescription": "Aspire जुड़े हुए AI टूल्स को डैशबोर्ड संदर्भ देता है, जिसमें लॉग, ट्रेस, मेट्रिक्स, स्वास्थ्य और संसाधन कमांड शामिल हैं.", - "link": "Aspire डैशबोर्ड देखें" + "link": "Aspire डैशबोर्ड देखें", + "standaloneLink": "स्टैंडअलोन डैशबोर्ड", + "telemetryLink": "OpenTelemetry की अवधारणाएँ", + "agentsLink": "कोडिंग एजेंट के साथ डीबग करें" }, "integrations": { "index": "डिफ़ॉल्ट रूप से विस्तार योग्य", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "AppHost उदाहरण लोड नहीं हो सके। फिर से कोशिश करने के लिए कोई विकल्प चुनें।", "heading": "अपना {{appHost}} बनाएँ", "description": "देखें Aspire आपके स्टैक के भागों को कैसे परिभाषित करता है — फीचर्स को ऑन/ऑफ करें।", "frontend": "फ्रंटएंड", diff --git a/src/frontend/src/content/i18n/id.json b/src/frontend/src/content/i18n/id.json index 251936dda..639461485 100644 --- a/src/frontend/src/content/i18n/id.json +++ b/src/frontend/src/content/i18n/id.json @@ -245,7 +245,10 @@ "body": "Ikuti permintaan melintasi sumber daya, berpindah antara log terstruktur dan jejak, periksa metrik, dan tangani kesehatan sumber daya dari satu dasbor pengembang.", "badgeLabel": "Agen bertindak berdasarkan sinyal ini", "badgeDescription": "Aspire memberi alat AI yang terhubung konteks dasbor, termasuk log, jejak, metrik, kesehatan, dan perintah sumber daya.", - "link": "Jelajahi dasbor Aspire" + "link": "Jelajahi dasbor Aspire", + "standaloneLink": "Dasbor mandiri", + "telemetryLink": "Konsep OpenTelemetry", + "agentsLink": "Debug dengan agen pengodean" }, "integrations": { "index": "Mudah diperluas secara default", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "Contoh AppHost tidak dapat dimuat. Pilih opsi untuk mencoba lagi.", "heading": "Bangun {{appHost}} Anda", "description": "Aktif/nonaktifkan fitur untuk melihat bagaimana Aspire mendefinisikan stack Anda.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/it.json b/src/frontend/src/content/i18n/it.json index c5f40bdcf..8bbc69506 100644 --- a/src/frontend/src/content/i18n/it.json +++ b/src/frontend/src/content/i18n/it.json @@ -245,7 +245,10 @@ "body": "Segui una richiesta tra le risorse, passa tra log strutturati e tracce, ispeziona le metriche e intervieni sull'integrità delle risorse da un unico pannello per sviluppatori.", "badgeLabel": "Gli agenti agiscono su questi segnali", "badgeDescription": "Aspire offre agli strumenti di IA connessi il contesto del pannello, inclusi log, tracce, metriche, integrità e comandi delle risorse.", - "link": "Esplora il pannello di controllo di Aspire" + "link": "Esplora il pannello di controllo di Aspire", + "standaloneLink": "Pannello autonomo", + "telemetryLink": "Concetti di OpenTelemetry", + "agentsLink": "Debug con agenti di programmazione" }, "integrations": { "index": "Estendibile per impostazione predefinita", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "Impossibile caricare gli esempi AppHost. Seleziona un'opzione per riprovare.", "heading": "Crea il tuo {{appHost}}", "description": "Attiva/disattiva funzionalità per vedere come Aspire definisce parti dello stack.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/ja.json b/src/frontend/src/content/i18n/ja.json index 03647cde7..8fda7b2c6 100644 --- a/src/frontend/src/content/i18n/ja.json +++ b/src/frontend/src/content/i18n/ja.json @@ -245,7 +245,10 @@ "body": "1 つの開発者向けダッシュボードから、リソースをまたいでリクエストを追跡し、構造化ログとトレースを行き来し、メトリックを確認し、リソースの正常性に対応できます。", "badgeLabel": "エージェントはこれらのシグナルに対応", "badgeDescription": "Aspire は、ログ、トレース、メトリック、正常性、リソース コマンドを含むダッシュボード コンテキストを、接続された AI ツールに提供します。", - "link": "Aspire ダッシュボードを詳しく見る" + "link": "Aspire ダッシュボードを詳しく見る", + "standaloneLink": "スタンドアロン ダッシュボード", + "telemetryLink": "OpenTelemetry の概念", + "agentsLink": "コーディング エージェントでデバッグ" }, "integrations": { "index": "既定で拡張可能", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "AppHost の例を読み込めませんでした。オプションを選択して再試行してください。", "heading": "{{appHost}} を構築", "description": "機能をオン/オフして Aspire がスタックをどう定義するか確認。", "frontend": "フロントエンド", diff --git a/src/frontend/src/content/i18n/ko.json b/src/frontend/src/content/i18n/ko.json index 9496090c0..64e6e4e06 100644 --- a/src/frontend/src/content/i18n/ko.json +++ b/src/frontend/src/content/i18n/ko.json @@ -245,7 +245,10 @@ "body": "하나의 개발자 대시보드에서 리소스 전반의 요청을 추적하고, 구조화된 로그와 추적 사이를 이동하고, 메트릭을 살펴보고, 리소스 상태에 대응하세요.", "badgeLabel": "에이전트가 이 신호에 따라 동작합니다", "badgeDescription": "Aspire는 연결된 AI 도구에 로그, 추적, 메트릭, 상태, 리소스 명령을 포함한 대시보드 컨텍스트를 제공합니다.", - "link": "Aspire 대시보드 살펴보기" + "link": "Aspire 대시보드 살펴보기", + "standaloneLink": "독립 실행형 대시보드", + "telemetryLink": "OpenTelemetry 개념", + "agentsLink": "코딩 에이전트로 디버깅" }, "integrations": { "index": "기본적으로 확장 가능", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "AppHost 예제를 로드할 수 없습니다. 옵션을 선택하여 다시 시도하세요.", "heading": "{{appHost}} 빌드", "description": "기능을 켜거나 꺼서 Aspire가 스택을 어떻게 정의하는지 확인.", "frontend": "프런트엔드", diff --git a/src/frontend/src/content/i18n/pt-BR.json b/src/frontend/src/content/i18n/pt-BR.json index 01e58b83c..e296721a5 100644 --- a/src/frontend/src/content/i18n/pt-BR.json +++ b/src/frontend/src/content/i18n/pt-BR.json @@ -245,7 +245,10 @@ "body": "Acompanhe uma requisição entre recursos, alterne entre registros estruturados e rastreamentos, inspecione métricas e aja sobre a integridade dos recursos em um único painel de desenvolvedor.", "badgeLabel": "Agentes agem sobre esses sinais", "badgeDescription": "O Aspire dá às ferramentas de IA conectadas contexto do painel, incluindo registros, rastreamentos, métricas, integridade e comandos de recursos.", - "link": "Explore o painel do Aspire" + "link": "Explore o painel do Aspire", + "standaloneLink": "Painel independente", + "telemetryLink": "Conceitos do OpenTelemetry", + "agentsLink": "Depure com agentes de programação" }, "integrations": { "index": "Extensível por padrão", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "Não foi possível carregar os exemplos do AppHost. Selecione uma opção para tentar novamente.", "heading": "Construa seu {{appHost}}", "description": "Ative/desative recursos para ver como o Aspire define sua stack.", "frontend": "Frontend", diff --git a/src/frontend/src/content/i18n/ru.json b/src/frontend/src/content/i18n/ru.json index 6912bc653..e7859133b 100644 --- a/src/frontend/src/content/i18n/ru.json +++ b/src/frontend/src/content/i18n/ru.json @@ -245,7 +245,10 @@ "body": "Отслеживайте запрос между ресурсами, переходите между структурированными журналами и трассировками, изучайте метрики и реагируйте на состояние ресурсов из единой панели мониторинга для разработчиков.", "badgeLabel": "Агенты действуют по этим сигналам", "badgeDescription": "Aspire даёт подключённым ИИ-инструментам контекст панели мониторинга, включая журналы, трассировки, метрики, состояние и команды ресурсов.", - "link": "Изучить панель мониторинга Aspire" + "link": "Изучить панель мониторинга Aspire", + "standaloneLink": "Автономная панель мониторинга", + "telemetryLink": "Концепции OpenTelemetry", + "agentsLink": "Отладка с агентами программирования" }, "integrations": { "index": "Расширяемость по умолчанию", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "Не удалось загрузить примеры AppHost. Выберите вариант, чтобы повторить попытку.", "heading": "Постройте свой {{appHost}}", "description": "Включайте и выключайте функции, чтобы увидеть, как Aspire определяет части вашего стека.", "frontend": "Фронтенд", diff --git a/src/frontend/src/content/i18n/tr.json b/src/frontend/src/content/i18n/tr.json index 91a70b00b..4df987a72 100644 --- a/src/frontend/src/content/i18n/tr.json +++ b/src/frontend/src/content/i18n/tr.json @@ -245,7 +245,10 @@ "body": "Bir isteği kaynaklar arasında izleyin, yapılandırılmış günlükler ile izler arasında geçiş yapın, metrikleri inceleyin ve tek bir geliştirici panosundan kaynak sağlığına göre işlem yapın.", "badgeLabel": "Ajanlar bu sinyallerle hareket eder", "badgeDescription": "Aspire, bağlı yapay zekâ araçlarına günlükler, izler, metrikler, sağlık ve kaynak komutları dahil pano bağlamı sağlar.", - "link": "Aspire panosunu keşfedin" + "link": "Aspire panosunu keşfedin", + "standaloneLink": "Bağımsız pano", + "telemetryLink": "OpenTelemetry kavramları", + "agentsLink": "Kodlama ajanlarıyla hata ayıklayın" }, "integrations": { "index": "Varsayılan olarak genişletilebilir", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "AppHost örnekleri yüklenemedi. Yeniden denemek için bir seçenek seçin.", "heading": "{{appHost}} Oluştur", "description": "Aspire'ın yığını nasıl tanımladığını görmek için özellikleri aç/kapat.", "frontend": "Ön Uç", diff --git a/src/frontend/src/content/i18n/uk.json b/src/frontend/src/content/i18n/uk.json index 4cd1086d1..ca69dbbf2 100644 --- a/src/frontend/src/content/i18n/uk.json +++ b/src/frontend/src/content/i18n/uk.json @@ -245,7 +245,10 @@ "body": "Відстежуйте запит між ресурсами, переходьте між структурованими журналами й трасами, переглядайте метрики та реагуйте на справність ресурсів з одного дашборду розробника.", "badgeLabel": "Агенти діють за цими сигналами", "badgeDescription": "Aspire надає під’єднаним інструментам ШІ контекст дашборду, зокрема журнали, траси, метрики, стан справності та команди ресурсів.", - "link": "Дослідити дашборд Aspire" + "link": "Дослідити дашборд Aspire", + "standaloneLink": "Автономний дашборд", + "telemetryLink": "Концепції OpenTelemetry", + "agentsLink": "Налагодження з агентами програмування" }, "integrations": { "index": "Розширюваність за замовчуванням", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "Не вдалося завантажити приклади AppHost. Виберіть опцію, щоб спробувати ще раз.", "heading": "Побудуйте свій {{appHost}}", "description": "Вмикайте/вимикайте функції, щоб побачити як Aspire визначає частини стеку.", "frontend": "Фронтенд", diff --git a/src/frontend/src/content/i18n/zh-CN.json b/src/frontend/src/content/i18n/zh-CN.json index 48d25186d..d71252b0b 100644 --- a/src/frontend/src/content/i18n/zh-CN.json +++ b/src/frontend/src/content/i18n/zh-CN.json @@ -245,7 +245,10 @@ "body": "在一个开发者仪表板中跟踪请求跨资源的路径,在结构化日志与跟踪之间切换,检查指标,并根据资源健康状态采取行动。", "badgeLabel": "智能体可基于这些信号行动", "badgeDescription": "Aspire 为已连接的 AI 工具提供仪表板上下文,包括日志、跟踪、指标、健康状态和资源命令。", - "link": "浏览 Aspire 仪表板" + "link": "浏览 Aspire 仪表板", + "standaloneLink": "独立仪表板", + "telemetryLink": "OpenTelemetry 概念", + "agentsLink": "使用编码智能体调试" }, "integrations": { "index": "默认可扩展", @@ -487,6 +490,7 @@ } }, "appHostBuilder": { + "examplesError": "无法加载 AppHost 示例。请选择一个选项以重试。", "heading": "构建你的 {{appHost}}", "description": "切换不同功能,查看 Aspire 如何定义你的技术栈各部分。", "frontend": "前端", diff --git a/src/frontend/src/utils/page-metadata.ts b/src/frontend/src/utils/page-metadata.ts index 7658efd5c..e3e00a4e2 100644 --- a/src/frontend/src/utils/page-metadata.ts +++ b/src/frontend/src/utils/page-metadata.ts @@ -22,18 +22,9 @@ export const DEFAULT_OG_IMAGE_WIDTH = 1200; export const DEFAULT_OG_IMAGE_HEIGHT = 630; /** - * Marketing-grade fallback description used for the home page and for any - * page that somehow lacks a frontmatter `description`. This intentionally - * differs from `structured-data.ts`'s `organizationDescription` and from the - * site-wide `description` meta in `config/head.attrs.ts` because each is - * sized for its own audience: - * - * - `astro.config.mjs` / `head.attrs.ts` — long-form marketing copy, no - * length cap, shown only on the home page. - * - `structured-data.ts` `organizationDescription` — JSON-LD organization - * summary, slightly tighter and product-focused. - * - This constant — Open Graph fallback, kept short enough for social-card - * previews and truncated to `OG_DESCRIPTION_MAX_LENGTH` before emission. + * Social-card fallback for pages without a frontmatter `description`. + * Starlight owns the standard description meta tag; this fallback only + * applies to Open Graph and Twitter previews. */ export const FALLBACK_DESCRIPTION = 'Aspire streamlines your development workflow with code-first control, ' + diff --git a/src/frontend/tests/e2e/api-markdown-routes.spec.ts b/src/frontend/tests/e2e/api-markdown-routes.spec.ts index b933a0c15..ffdbcae74 100644 --- a/src/frontend/tests/e2e/api-markdown-routes.spec.ts +++ b/src/frontend/tests/e2e/api-markdown-routes.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { locales } from '../../config/locales'; const markdownRoutes = [ { @@ -85,4 +86,26 @@ for (const route of markdownRoutes) { expect(body).toContain(route.expectedText); expect(body).not.toContain(''); }); -} \ No newline at end of file +} + +test('built homepages publish useful Markdown through their existing companion paths', async ({ + request, +}) => { + test.skip(!process.env.CI, 'Homepage Markdown is finalized by the production build.'); + + for (const locale of Object.keys(locales)) { + const prefix = locale === 'root' ? '' : `/${locale}`; + const response = await request.get(`${prefix || '/index'}.md`); + expect(response.ok()).toBe(true); + expect(response.headers()['content-type']).toContain('text/markdown'); + const markdown = await response.text(); + + expect(markdown).toMatch(/^# .+/m); + expect(markdown).toContain('OpenTelemetry'); + expect(markdown).toContain(`](${prefix}/get-started/first-app/)`); + expect(markdown).toContain(`](${prefix}/dashboard/standalone/)`); + expect(markdown).toContain('```typescript\n'); + expect(markdown).toContain('```csharp\n'); + expect(markdown).not.toMatch(/|class="code-variant"/); + } +}); diff --git a/src/frontend/tests/e2e/custom-components.spec.ts b/src/frontend/tests/e2e/custom-components.spec.ts index eaed1158a..195d405af 100644 --- a/src/frontend/tests/e2e/custom-components.spec.ts +++ b/src/frontend/tests/e2e/custom-components.spec.ts @@ -1,6 +1,112 @@ import { expect, test } from '@playwright/test'; import { dismissCookieConsentIfVisible } from '@tests/e2e/helpers'; +import { deferAppHostExamples } from '../../config/apphost-examples.mjs'; + +test.describe('deferred AppHost examples', () => { + test.beforeEach(async ({ page, request, baseURL }) => { + // Exercise the production transformation locally without a full site build. + const response = await request.get('/'); + const html = await response.text(); + if (!html.includes('data-apphost-examples=')) { + const deferred = deferAppHostExamples(html); + await page.route(new URL('/', baseURL).href, (route) => + route.fulfill({ response, body: deferred.html }) + ); + await page.route(`**/_astro/${deferred.filename}`, (route) => + route.fulfill({ contentType: 'text/html', body: deferred.examples }) + ); + } + await page.emulateMedia({ reducedMotion: 'reduce' }); + }); + + test('loads examples once on interaction and keeps the initial default useful', async ({ + page, + }) => { + const exampleRequests: string[] = []; + page.on('request', (request) => { + if (/\/_astro\/apphost-examples\./.test(request.url())) { + exampleRequests.push(request.url()); + } + }); + await page.goto('/'); + await dismissCookieConsentIfVisible(page); + const builder = page.locator('[data-apphost-builder]').first(); + const stage = builder.locator('[data-code-stage]'); + await expect(stage).toHaveAttribute('data-code-variant', 'frontend'); + await expect(stage).toContainText('.addViteApp("frontend"'); + await expect(builder.locator('.code-variant')).toHaveCount(1); + expect(exampleRequests).toHaveLength(0); + + await builder.locator('[data-toggle="database"]').click(); + await expect(stage).toHaveAttribute('data-code-variant', 'databaseFrontend'); + await builder.locator('[data-lang="csharp"]').click(); + await expect(stage).toHaveAttribute('data-code-lang', 'csharp'); + await expect(stage).toContainText('AddPostgres("db")'); + expect(exampleRequests).toHaveLength(1); + }); + + for (const failure of ['unavailable', 'invalid content']) { + test(`keeps the last preview and allows retry after ${failure}`, async ({ page }) => { + let requests = 0; + await page.route('**/_astro/apphost-examples.*.html', async (route) => { + requests++; + if (requests === 1) { + await route.fulfill({ + status: failure === 'unavailable' ? 503 : 200, + contentType: 'text/html', + body: 'Examples unavailable', + }); + } else { + await route.fallback(); + } + }); + await page.goto('/'); + await dismissCookieConsentIfVisible(page); + const builder = page.locator('[data-apphost-builder]').first(); + const stage = builder.locator('[data-code-stage]'); + const status = builder.locator('[data-code-status]'); + await builder.locator('[data-lang="csharp"]').click(); + await expect(status).toBeVisible(); + await expect(status).toContainText('Select an option to try again.'); + await expect(stage).toHaveAttribute('data-code-lang', 'typescript'); + await expect(stage).toContainText('.addViteApp("frontend"'); + await expect(builder.locator('[data-apphost-code-display]')).toHaveAttribute( + 'aria-busy', + 'false' + ); + + await builder.locator('[data-lang="csharp"]').click(); + await expect(stage).toHaveAttribute('data-code-lang', 'csharp'); + await expect(status).not.toContainText('Could not load'); + expect(requests).toBe(2); + }); + } + + test('uses the latest selection when controls change during loading', async ({ page }) => { + const gate = Promise.withResolvers(); + await page.route('**/_astro/apphost-examples.*.html', async (route) => { + await gate.promise; + await route.fallback(); + }); + await page.goto('/'); + await dismissCookieConsentIfVisible(page); + const builder = page.locator('[data-apphost-builder]').first(); + await builder.locator('[data-toggle="database"]').click(); + await expect(builder.locator('[data-apphost-code-display]')).toHaveAttribute( + 'aria-busy', + 'true' + ); + await builder.locator('[data-toggle="api"]').click(); + await builder.locator('[data-lang="csharp"]').click(); + gate.resolve(); + + const stage = builder.locator('[data-code-stage]'); + await expect(stage).toHaveAttribute('data-code-lang', 'csharp'); + await expect(stage).toHaveAttribute('data-code-variant', 'databaseApiFrontend'); + await expect(stage).toContainText('AddPostgres("db")'); + }); +}); test('app host builder swaps visible code when toggles and language change', async ({ page }) => { await page.goto('/'); diff --git a/src/frontend/tests/e2e/homepage.spec.ts b/src/frontend/tests/e2e/homepage.spec.ts index 0f4c6843e..c67e77bd4 100644 --- a/src/frontend/tests/e2e/homepage.spec.ts +++ b/src/frontend/tests/e2e/homepage.spec.ts @@ -8,6 +8,24 @@ test.beforeEach(async ({ page }) => { await dismissCookieConsentIfVisible(page); }); +test('links directly to local observability and agent debugging guides', async ({ page }) => { + const links = page.locator('.observability-links a'); + await expect(links).toHaveText([ + 'Explore the Aspire dashboard', + 'Standalone dashboard', + 'OpenTelemetry concepts', + 'Debug with coding agents', + ]); + expect( + await links.evaluateAll((anchors) => anchors.map((anchor) => anchor.getAttribute('href'))) + ).toEqual([ + '/dashboard/overview/', + '/dashboard/standalone/', + '/fundamentals/telemetry/', + '/dashboard/ai-coding-agents/', + ]); +}); + test('renders a complete semantic landing page without horizontal overflow', async ({ page }) => { await expect(page.locator('main h1')).toHaveCount(1); await expect( diff --git a/src/frontend/tests/e2e/og-metadata.spec.ts b/src/frontend/tests/e2e/og-metadata.spec.ts index 067761d46..0ab9026d3 100644 --- a/src/frontend/tests/e2e/og-metadata.spec.ts +++ b/src/frontend/tests/e2e/og-metadata.spec.ts @@ -1,4 +1,8 @@ import { expect, test } from '@playwright/test'; +import { select, selectAll } from 'hast-util-select'; +import rehypeParse from 'rehype-parse'; +import { unified } from 'unified'; +import { FALLBACK_DESCRIPTION } from '../../src/utils/page-metadata'; /** * Smoke tests for the page-specific Open Graph metadata wired up in @@ -36,6 +40,85 @@ const PAGES: PageExpectation[] = [ }, ]; +for (const url of [ + '/', + '/dashboard/overview/', + '/dashboard/standalone/', + '/dashboard/ai-coding-agents/', + '/dashboard/standalone-for-python/', + '/dashboard/standalone-for-nodejs/', + '/get-started/ai-coding-agents/', + '/get-started/aspire-mcp-server/', + '/fundamentals/telemetry/', + '/app-host/migrate-from-docker-compose/', + '/da/', + '/uk/', +]) { + test(`uses the page description in standard and social metadata for ${url}`, async ({ + request, + }) => { + const response = await request.get(url); + expect(response.ok()).toBe(true); + const tree = unified() + .use(rehypeParse) + .parse(await response.text()); + const descriptions = selectAll('meta[name="description"]', tree); + const ogDescriptions = selectAll('meta[property="og:description"]', tree); + + expect(descriptions).toHaveLength(1); + expect(ogDescriptions).toHaveLength(1); + const description = descriptions[0].properties.content; + expect(description).toBeTruthy(); + expect(description).toBe(ogDescriptions[0].properties.content); + expect(description).not.toBe( + 'Aspire is a multi-language local dev-time orchestration tool chain for building, running, debugging, and deploying distributed applications.' + ); + }); +} + +test('preserves Starlight content languages and canonical links for translations and fallbacks', async ({ + request, +}) => { + for (const [url, locale, contentLanguage] of [ + ['/da/', 'da', 'da'], + ['/uk/', 'uk', 'uk'], + ['/ja/fundamentals/telemetry/', 'ja', 'ja'], + ['/da/fundamentals/telemetry/', 'da', 'en'], + ['/uk/fundamentals/telemetry/', 'uk', 'en'], + ]) { + const response = await request.get(url); + expect(response.ok()).toBe(true); + const tree = unified() + .use(rehypeParse) + .parse(await response.text()); + expect(select('html', tree)?.properties.lang).toBe(locale); + expect(select('main', tree)?.properties.lang).toBe(contentLanguage); + expect(select('link[rel="canonical"]', tree)?.properties.href).toBe(`https://aspire.dev${url}`); + expect(select(`link[hreflang="${locale}"]`, tree)?.properties.href).toBe( + `https://aspire.dev${url}` + ); + } +}); + +test('uses Starlight site description only when page frontmatter has no description', async ({ + request, +}) => { + const response = await request.get('/ja/architecture/resource-publishing/'); + expect(response.ok()).toBe(true); + const tree = unified() + .use(rehypeParse) + .parse(await response.text()); + const descriptions = selectAll('meta[name="description"]', tree); + const ogDescriptions = selectAll('meta[property="og:description"]', tree); + + expect(descriptions).toHaveLength(1); + expect(descriptions[0].properties.content).toBe( + 'Aspire is a multi-language local dev-time orchestration tool chain for building, running, debugging, and deploying distributed applications.' + ); + expect(ogDescriptions).toHaveLength(1); + expect(ogDescriptions[0].properties.content).toBe(FALLBACK_DESCRIPTION); +}); + function escape(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/src/frontend/tests/unit/apphost-examples.vitest.test.ts b/src/frontend/tests/unit/apphost-examples.vitest.test.ts new file mode 100644 index 000000000..af0356171 --- /dev/null +++ b/src/frontend/tests/unit/apphost-examples.vitest.test.ts @@ -0,0 +1,54 @@ +import { selectAll } from 'hast-util-select'; +import rehypeParse from 'rehype-parse'; +import { unified } from 'unified'; +import { describe, expect, test } from 'vitest'; +import { deferAppHostExamples } from '../../config/apphost-examples.mjs'; + +const csharp = (copyLabel = 'Copy') => + ``; +const typescript = (copyLabel = 'Copy') => + `
await builder.addViteApp("frontend");
await builder.addPostgres("database");
`; +const prefix = 'Aspire & apps'; +const suffix = '

Other homepage content

'; +const homepage = (copyLabel = 'Copy') => + `${prefix}

AppHost

${csharp(copyLabel)}${typescript(copyLabel)}
${suffix}`; +const html = homepage(); + +describe('deferred AppHost examples', () => { + test('keeps only the default frame while preserving other HTML and scoped classes', () => { + const result = deferAppHostExamples(html); + const tree = unified().use(rehypeParse).parse(result.html); + const variants = selectAll('[data-apphost-builder] .code-variant', tree); + + expect(variants).toHaveLength(1); + expect(variants[0].properties.dataVariant).toBe('frontend'); + expect(result.html).toContain('
Typing animation'); + }); + + test('uses one content-addressed file containing the original highlighted examples', () => { + const result = deferAppHostExamples(html); + + expect(result.examples).not.toContain('class="copy"'); + expect(result.filename).toMatch(/^apphost-examples\.[a-f0-9]{16}\.html$/); + expect(result.html).toContain(`data-apphost-examples="/_astro/${result.filename}"`); + expect(deferAppHostExamples(html).filename).toBe(result.filename); + expect(deferAppHostExamples(html.replace('addPostgres', 'addSqlServer')).filename).not.toBe( + result.filename + ); + expect(deferAppHostExamples(homepage('Kopier')).filename).toBe(result.filename); + }); + + test('fails instead of publishing a broken default preview', () => { + expect(() => deferAppHostExamples('
No builder
')).toThrow( + 'missing builder or default example' + ); + expect(() => + deferAppHostExamples(html.replace('data-code-lang="typescript"', 'data-code-lang="unknown"')) + ).toThrow('missing builder or default example'); + }); +}); diff --git a/src/frontend/tests/unit/homepage-markdown.vitest.test.ts b/src/frontend/tests/unit/homepage-markdown.vitest.test.ts new file mode 100644 index 000000000..e30344933 --- /dev/null +++ b/src/frontend/tests/unit/homepage-markdown.vitest.test.ts @@ -0,0 +1,151 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { describe, expect, test } from 'vitest'; +import { aspireVersionPlaceholdersIntegration } from '../../config/aspire-version-placeholders-integration.mjs'; +import { currentAspireVersion } from '../../config/aspire-versions.mjs'; +import { renderHomepageMarkdown } from '../../config/homepage-markdown.mjs'; +import { locales } from '../../config/locales'; + +function homepage(title = 'Compose distributed apps in code.', prefix = '') { + return ` + +
+
+
+

Free and open source

+

${title}

+

Model, run, observe, and deploy your application.

+ Build your first app +
+
+
Inactive builder variants
+
Default builder variant
Inactive builder variants
+
+
+
+

Local OpenTelemetry observability

+

Read application logs, distributed traces, and resource health.

+ Standalone dashboard +

Application context for coding agents

+ Set up your agent +
Simulated terminal output
+ + +
aspire run4 resources healthy
+

Useful quote.

Steven PriceSoftware Engineering Manager
+ + + +
+
Footer navigation
+
+ `; +} + +describe('homepage Markdown', () => { + test('keeps the real hero, explanations, links, and warnings without decorative UI', async () => { + const markdown = await renderHomepageMarkdown(homepage()); + + expect(markdown).toContain('# Compose distributed apps in code.'); + expect(markdown).toMatch(/^# Compose distributed apps in code\./); + expect(markdown).toContain('## Local OpenTelemetry observability'); + expect(markdown).toContain('Read application logs, distributed traces, and resource health.'); + expect(markdown).toContain('[Build your first app](/get-started/first-app/)'); + expect(markdown).toContain('[Standalone dashboard](/dashboard/standalone/)'); + expect(markdown).toContain('[Set up your agent](/get-started/ai-coding-agents/)'); + expect(markdown).toContain('### Deploy the model'); + expect(markdown).toContain('* `aspire run`\n* 4 resources healthy'); + expect(markdown).toContain( + '**[Steven Price](https://example.com)** — Software Engineering Manager' + ); + expect(markdown).toContain('Keep application telemetry private.'); + expect(markdown).not.toMatch( + /Free and open source|Site navigation|Footer navigation|Inactive builder|Simulated terminal|Animated topology|not content| { + const markdown = await renderHomepageMarkdown(homepage()); + + expect(markdown).toContain( + '```typescript\nconst builder = await createBuilder();\nawait builder.build().run();\n```' + ); + expect(markdown).toContain( + '```csharp\nvar builder = DistributedApplication.CreateBuilder(args);\nbuilder.Build().Run();\n```' + ); + expect(markdown).not.toContain('Copy'); + }); + + test('uses the rendered locale content and links without inventing English copy', async () => { + const markdown = await renderHomepageMarkdown(homepage('Lokale apps', '/da')); + expect(markdown).toContain('# Lokale apps'); + expect(markdown).toContain('(/da/dashboard/standalone/)'); + expect(markdown).not.toContain('Compose distributed apps in code.'); + }); + + test('fails explicitly if a redesign removes the required content landmarks', async () => { + await expect(renderHomepageMarkdown('

Aspire

')).rejects.toThrow( + 'missing homepage content landmarks' + ); + }); + + test('finalizes every existing homepage companion and retains ordinary Markdown normalization', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'aspire-homepage-markdown-')); + try { + await mkdir(path.join(directory, '_astro')); + for (const locale of Object.keys(locales)) { + const prefix = locale === 'root' ? '' : `/${locale}`; + const htmlDirectory = path.join(directory, locale === 'root' ? '' : locale); + await mkdir(htmlDirectory, { recursive: true }); + await writeFile(path.join(htmlDirectory, 'index.html'), homepage(locale, prefix)); + await writeFile( + path.join(directory, `${locale === 'root' ? 'index' : locale}.md`), + '# Aspire\n\n' + ); + } + await writeFile(path.join(directory, 'guide.md'), 'Use Aspire %ASPIRE_VERSION%.'); + + await aspireVersionPlaceholdersIntegration().hooks['astro:build:done']({ + dir: pathToFileURL(`${directory}${path.sep}`), + }); + + for (const locale of Object.keys(locales)) { + const markdown = await readFile( + path.join(directory, `${locale === 'root' ? 'index' : locale}.md`), + 'utf8' + ); + expect(markdown).toContain(`# ${locale}`); + expect(markdown).toContain('Read application logs'); + expect(markdown).not.toContain(' Date: Wed, 9 Sep 2026 10:30:40 -0500 Subject: [PATCH 2/3] fix: keep mobile homepage within layout budget Tighten spacing in the expanded observability section so the production mobile viewport remains within the existing compactness gate. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/frontend/src/components/home/HomePage.astro | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/components/home/HomePage.astro b/src/frontend/src/components/home/HomePage.astro index beee69a06..3f5523eb8 100644 --- a/src/frontend/src/components/home/HomePage.astro +++ b/src/frontend/src/components/home/HomePage.astro @@ -3154,6 +3154,15 @@ const localizedPrinciples = principles.map((principle) => ({ padding: 3.5rem 1rem; } + .home-dashboard { + padding-block: 2.25rem; + } + + .observability-links { + gap: 0.25rem 1rem; + margin-top: 0.75rem; + } + .model-focus-border { display: none; } @@ -3257,7 +3266,7 @@ const localizedPrinciples = principles.map((principle) => ({ } .dashboard-stage { - margin-top: 3rem; + margin-top: 2rem; border-radius: 0.75rem; } From 34a4d9b10d98f8a0894b383a2a7bc7703da9bb15 Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:11:48 -0500 Subject: [PATCH 3/3] fix: align agent docs and search indexing Document authenticated standalone MCP with an explicit API key and keep deferred AppHost examples out of the homepage Pagefind index. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/components/AppHostBuilder.astro | 7 ++++- .../docs/dashboard/ai-coding-agents.mdx | 12 ++++++-- .../src/content/docs/dashboard/standalone.mdx | 12 +++++++- .../cli/commands/aspire-agent-mcp.mdx | 12 ++------ src/frontend/tests/e2e/site-search.spec.ts | 19 ++++++++++++ .../unit/custom-components.vitest.test.ts | 1 + .../dashboard-agent-commands.vitest.test.ts | 29 +++++++++++++++++++ 7 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 src/frontend/tests/unit/dashboard-agent-commands.vitest.test.ts diff --git a/src/frontend/src/components/AppHostBuilder.astro b/src/frontend/src/components/AppHostBuilder.astro index ccea9967b..79d81f61f 100644 --- a/src/frontend/src/components/AppHostBuilder.astro +++ b/src/frontend/src/components/AppHostBuilder.astro @@ -1097,7 +1097,12 @@ await builder.build().run();`,
-
+
diff --git a/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx b/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx index 7a39570d6..3816a7f46 100644 --- a/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx +++ b/src/frontend/src/content/docs/dashboard/ai-coding-agents.mdx @@ -126,15 +126,21 @@ aspire otel spans --dashboard-url "http://localhost:18888/login?t=" ### Use Aspire MCP with the standalone dashboard -Start the MCP server in dashboard-only mode using `--dashboard-url`: +The MCP command doesn't exchange the browser token from a dashboard login URL. To use dashboard-only MCP with authentication, start the dashboard with an explicit telemetry API key: ```bash title="Aspire CLI" -aspire agent mcp --dashboard-url "http://localhost:18888/login?t=" +aspire dashboard run --Dashboard:Api:AuthMode=ApiKey --Dashboard:Api:PrimaryApiKey="" +``` + +Then start the MCP server in a separate terminal with the dashboard base URL and the same key: + +```bash title="Aspire CLI" +aspire agent mcp --dashboard-url "http://localhost:18888" --api-key "" ``` This exposes the dashboard's telemetry tools to an MCP-compatible AI assistant, not the AppHost's resource-management tools. -Configuration is required in the agent to use the MCP server. For configuration details, see [Aspire MCP server configuration](/get-started/aspire-mcp-server/#configuration). The `--dashboard-url` must be passed as a command line argument when configuring the MCP server for standalone use. +Use a high-entropy key and don't commit or share it. Configuration is required in the agent to use the MCP server. For configuration details, see [Aspire MCP server configuration](/get-started/aspire-mcp-server/#configuration). For telemetry API security details, see [Secure the telemetry API endpoint](/dashboard/security-considerations/#telemetry-api-endpoint). ### Sample skill for standalone dashboard diff --git a/src/frontend/src/content/docs/dashboard/standalone.mdx b/src/frontend/src/content/docs/dashboard/standalone.mdx index 749188145..d514ba45c 100644 --- a/src/frontend/src/content/docs/dashboard/standalone.mdx +++ b/src/frontend/src/content/docs/dashboard/standalone.mdx @@ -204,10 +204,20 @@ For more details, see the [`aspire otel logs`](/reference/cli/commands/aspire-ot The [`aspire agent mcp`](/reference/cli/commands/aspire-agent-mcp/) command starts an MCP (Model Context Protocol) server that AI assistants can connect to. When used with `--dashboard-url`, it runs in dashboard-only mode and exposes telemetry tools (structured logs and traces) to MCP-compatible clients: +Unlike `aspire otel`, the MCP command doesn't exchange the browser token from a login URL. Start the dashboard with an explicit telemetry API key: + +```bash title="Aspire CLI" +aspire dashboard run --Dashboard:Api:AuthMode=ApiKey --Dashboard:Api:PrimaryApiKey="" +``` + +Then start the MCP server in a separate terminal with the dashboard base URL and the same key: + ```bash title="Aspire CLI" -aspire agent mcp --dashboard-url "http://localhost:18888/login?t=" +aspire agent mcp --dashboard-url "http://localhost:18888" --api-key "" ``` +Use a high-entropy key and don't commit or share it. For more information, see [Secure the telemetry API endpoint](/dashboard/security-considerations/#telemetry-api-endpoint). + ## Sample For a sample of using the standalone dashboard, see the [Standalone Aspire dashboard sample app](https://github.com/microsoft/aspire-samples/tree/main/samples/standalone-dashboard). diff --git a/src/frontend/src/content/docs/reference/cli/commands/aspire-agent-mcp.mdx b/src/frontend/src/content/docs/reference/cli/commands/aspire-agent-mcp.mdx index 7e3d9f2f7..cc240d448 100644 --- a/src/frontend/src/content/docs/reference/cli/commands/aspire-agent-mcp.mdx +++ b/src/frontend/src/content/docs/reference/cli/commands/aspire-agent-mcp.mdx @@ -34,11 +34,11 @@ The following options are available: - **`--dashboard-url `** - The URL of a standalone Aspire Dashboard to connect to instead of discovering one from an AppHost. Accepts a base URL (for example, `http://localhost:18888`) or a full login URL including a browser token (for example, `http://localhost:18888/login?t=`). When a login URL is provided, the token is automatically exchanged for an API key. + The base URL of a standalone Aspire Dashboard to connect to instead of discovering one from an AppHost, for example `http://localhost:18888`. Browser login tokens aren't used to authenticate MCP telemetry tools. - **`--api-key `** - The API key used to authenticate with the dashboard's Telemetry API. Only required when `--dashboard-url` is specified and the dashboard is configured with `ApiKey` authentication and no login URL is provided. + The API key used to authenticate with the dashboard's Telemetry API. Provide it when `--dashboard-url` targets a dashboard configured with `ApiKey` authentication. - @@ -60,16 +60,10 @@ The following options are available: aspire agent mcp ``` -- Start the MCP server in dashboard-only mode using a login URL: - - ```bash title="Aspire CLI" - aspire agent mcp --dashboard-url "http://localhost:18888/login?t=" - ``` - - Start the MCP server in dashboard-only mode with an API key: ```bash title="Aspire CLI" - aspire agent mcp --dashboard-url "http://localhost:18888" --api-key "" + aspire agent mcp --dashboard-url "http://localhost:18888" --api-key "" ``` ## See also diff --git a/src/frontend/tests/e2e/site-search.spec.ts b/src/frontend/tests/e2e/site-search.spec.ts index 14c5450c3..8c92154f7 100644 --- a/src/frontend/tests/e2e/site-search.spec.ts +++ b/src/frontend/tests/e2e/site-search.spec.ts @@ -34,6 +34,25 @@ async function typeSearchQuery(page: Page, query: string): Promise { } test.describe('site search dialog', () => { + test('does not index AppHost examples deferred from the homepage', async ({ page }) => { + test.skip(!process.env.CI, 'Pagefind is generated by the production build.'); + await page.goto('/'); + + const urls = await page.evaluate(async () => { + const pagefind = (await import( + /* @vite-ignore */ `${window.location.origin}/pagefind/pagefind.js` + )) as { + search: (query: string) => Promise<{ + results: Array<{ data: () => Promise<{ url: string }> }>; + }>; + }; + const response = await pagefind.search('publishAsKubernetes'); + return Promise.all(response.results.map(async (result) => (await result.data()).url)); + }); + + expect(urls).not.toContain('/'); + }); + test('renders keyboard shortcut hints in the footer', async ({ page }) => { await page.goto('/'); await dismissCookieConsentIfVisible(page); diff --git a/src/frontend/tests/unit/custom-components.vitest.test.ts b/src/frontend/tests/unit/custom-components.vitest.test.ts index 0b2eef97b..c6889c99f 100644 --- a/src/frontend/tests/unit/custom-components.vitest.test.ts +++ b/src/frontend/tests/unit/custom-components.vitest.test.ts @@ -626,6 +626,7 @@ const basicRenderCases: BasicRenderCase[] = [ 'data-editor-caret', 'data-editor-motion-toggle', 'data-disable-copy', + 'data-pagefind-ignore', 'data-toggle="database"', ], }, diff --git a/src/frontend/tests/unit/dashboard-agent-commands.vitest.test.ts b/src/frontend/tests/unit/dashboard-agent-commands.vitest.test.ts new file mode 100644 index 000000000..a3da54c0e --- /dev/null +++ b/src/frontend/tests/unit/dashboard-agent-commands.vitest.test.ts @@ -0,0 +1,29 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, test } from 'vitest'; + +const testsDir = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(testsDir, '..', '..', 'src', 'content', 'docs'); + +describe('standalone dashboard MCP commands', () => { + for (const file of [ + 'dashboard/standalone.mdx', + 'dashboard/ai-coding-agents.mdx', + 'reference/cli/commands/aspire-agent-mcp.mdx', + ]) { + test(`${file} uses an explicit API key instead of a browser login token`, () => { + const source = readFileSync(path.join(docsRoot, file), 'utf8'); + const commands = [...source.matchAll(/^\s*aspire agent mcp .*--dashboard-url.*$/gm)].map( + (match) => match[0].trim() + ); + + expect(commands).not.toHaveLength(0); + for (const command of commands) { + expect(command).toContain('--dashboard-url "http://localhost:18888"'); + expect(command).toContain('--api-key ""'); + expect(command).not.toContain('/login?t='); + } + }); + } +});