diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index dc3c73f8c3..2597181ae0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -43,6 +43,9 @@ jobs: env: GITTENSORY_SITE_URL: https://gittensory.aethereal.dev/ GITTENSORY_SITE_BASE: / + GITTENSORY_UMAMI_SCRIPT_URL: ${{ vars.GITTENSORY_UMAMI_SCRIPT_URL }} + GITTENSORY_UMAMI_WEBSITE_ID: ${{ vars.GITTENSORY_UMAMI_WEBSITE_ID }} + GITTENSORY_UMAMI_DOMAINS: ${{ vars.GITTENSORY_UMAMI_DOMAINS }} run: npm run docs:build - name: Upload Pages artifact diff --git a/CHANGELOG.md b/CHANGELOG.md index 10d8fc81a3..2b3f54ec44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,5 +45,7 @@ - Add public registration polish gates +- Add analytics and mcp version widget + diff --git a/package.json b/package.json index 06a10d51df..5eeb71264c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "docs:build": "vitepress build site", "docs:preview": "vitepress preview site --host 127.0.0.1", "docs:check": "node scripts/check-docs.mjs", + "docs:smoke": "node scripts/check-docs-build.mjs", "changelog": "npm run changelog:root && npm run changelog:mcp", "changelog:root": "git-cliff --config cliff.toml --output CHANGELOG.md", "changelog:mcp": "git-cliff --config cliff.mcp.toml --include-path 'packages/gittensory-mcp/**' --include-path '.github/workflows/npm-publish.yml' --output packages/gittensory-mcp/CHANGELOG.md", @@ -32,7 +33,7 @@ "test:integration": "vitest run test/integration", "test:workers": "vitest run --config vitest.workers.config.ts", "test:coverage": "vitest run --coverage", - "test:ci": "git diff --check && npm run actionlint && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run docs:check && npm run docs:build && npm run changelog:check && npm audit --audit-level=moderate", + "test:ci": "git diff --check && npm run actionlint && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run docs:check && npm run docs:build && npm run docs:smoke && npm run changelog:check && npm audit --audit-level=moderate", "test:watch": "vitest", "validate": "npm run typecheck && npm run test:coverage" }, diff --git a/scripts/check-docs-build.mjs b/scripts/check-docs-build.mjs new file mode 100644 index 0000000000..efee5b81dc --- /dev/null +++ b/scripts/check-docs-build.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const root = process.cwd(); +const dist = join(root, "site/.vitepress/dist"); +const indexPath = join(dist, "index.html"); +const expectUmami = process.env.GITTENSORY_EXPECT_UMAMI === "1"; +const expectedScriptUrl = process.env.GITTENSORY_UMAMI_SCRIPT_URL; +const expectedWebsiteId = process.env.GITTENSORY_UMAMI_WEBSITE_ID; +const expectedDomains = process.env.GITTENSORY_UMAMI_DOMAINS ?? "gittensory.aethereal.dev"; +const failures = []; + +if (!existsSync(indexPath)) { + failures.push("site/.vitepress/dist/index.html is missing; run npm run docs:build first."); +} else { + const html = readFileSync(indexPath, "utf8"); + if (!html.includes("gtn-version-pill")) failures.push("built docs are missing the MCP version pill markup."); + if (!html.includes("MCP")) failures.push("built docs are missing MCP version text."); +} + +const builtFiles = existsSync(dist) ? collect(dist).filter((file) => /\.(html|js)$/.test(file)) : []; +const builtText = builtFiles.map((file) => readFileSync(file, "utf8")).join("\n"); + +if (!builtText.includes("registry.npmjs.org") || !builtText.includes("@jsonbored%2fgittensory-mcp")) { + failures.push("built docs are missing the npm registry fetch for the MCP version widget."); +} + +if (expectUmami) { + if (!expectedScriptUrl || !expectedWebsiteId) { + failures.push("GITTENSORY_EXPECT_UMAMI=1 requires GITTENSORY_UMAMI_SCRIPT_URL and GITTENSORY_UMAMI_WEBSITE_ID."); + } else { + if (!builtText.includes(expectedScriptUrl)) failures.push("built docs are missing the configured Umami script URL."); + if (!builtText.includes(`data-website-id="${expectedWebsiteId}"`)) failures.push("built docs are missing the configured Umami website ID."); + if (!builtText.includes(`data-domains="${expectedDomains}"`)) failures.push("built docs are missing the configured Umami domain list."); + if (!builtText.includes('data-do-not-track="true"')) failures.push("built docs are missing the Umami do-not-track attribute."); + if (!builtText.includes('data-exclude-search="true"')) failures.push("built docs are missing the Umami search exclusion attribute."); + if (!builtText.includes('data-exclude-hash="true"')) failures.push("built docs are missing the Umami hash exclusion attribute."); + } +} else if (builtText.includes("data-website-id=")) { + failures.push("built docs include analytics without GITTENSORY_EXPECT_UMAMI=1."); +} + +if (failures.length > 0) { + console.error(failures.join("\n")); + process.exit(1); +} + +console.log(`checked built docs (${builtFiles.length} file(s))`); + +function collect(path) { + const stat = statSync(path); + if (stat.isFile()) return [path]; + return readdirSync(path).flatMap((entry) => collect(join(path, entry))); +} diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index 198f3682e5..b63bc54ce1 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -1,7 +1,27 @@ -import { defineConfig } from "vitepress"; +import { defineConfig, type HeadConfig } from "vitepress"; const siteUrl = process.env.GITTENSORY_SITE_URL ?? "https://gittensory.aethereal.dev/"; const siteBase = process.env.GITTENSORY_SITE_BASE ?? "/"; +const umamiScriptUrl = process.env.GITTENSORY_UMAMI_SCRIPT_URL; +const umamiWebsiteId = process.env.GITTENSORY_UMAMI_WEBSITE_ID; +const umamiDomains = process.env.GITTENSORY_UMAMI_DOMAINS ?? "gittensory.aethereal.dev"; + +const analyticsHead: HeadConfig[] = umamiScriptUrl && umamiWebsiteId + ? [ + [ + "script", + { + defer: "", + src: umamiScriptUrl, + "data-website-id": umamiWebsiteId, + "data-domains": umamiDomains, + "data-do-not-track": "true", + "data-exclude-search": "true", + "data-exclude-hash": "true", + }, + ], + ] + : []; export default defineConfig({ title: "Gittensory", @@ -18,6 +38,7 @@ export default defineConfig({ ["meta", { property: "og:description", content: "Private decision intelligence for healthier Gittensor repo participation." }], ["meta", { property: "og:url", content: siteUrl }], ["meta", { name: "theme-color", content: "#050608" }], + ...analyticsHead, ], themeConfig: { logo: "/logo.svg", diff --git a/site/.vitepress/theme/components/McpVersionPill.vue b/site/.vitepress/theme/components/McpVersionPill.vue new file mode 100644 index 0000000000..1b741e4f78 --- /dev/null +++ b/site/.vitepress/theme/components/McpVersionPill.vue @@ -0,0 +1,182 @@ + + + diff --git a/site/.vitepress/theme/custom.css b/site/.vitepress/theme/custom.css index 2000f56ee8..99abb928e6 100644 --- a/site/.vitepress/theme/custom.css +++ b/site/.vitepress/theme/custom.css @@ -101,6 +101,151 @@ body { color: var(--gt-text); } +.gtn-version-pill { + position: relative; + z-index: 30; + display: inline-flex; + align-items: center; + margin-left: 12px; + font-family: var(--vp-font-family-mono); + font-size: 11px; + white-space: nowrap; +} + +.gtn-version-pill__button { + position: relative; + display: inline-flex; + align-items: center; + gap: 7px; + border: 1px solid rgba(126, 231, 135, 0.26); + border-radius: 4px; + padding: 5px 9px; + background: rgba(17, 22, 31, 0.9); + color: var(--gt-text); + font: inherit; + line-height: 16px; + cursor: pointer; + transition: + border-color 0.15s ease, + background-color 0.15s ease, + color 0.15s ease; +} + +.gtn-version-pill__button:hover, +.gtn-version-pill__button[aria-expanded="true"] { + border-color: var(--gt-green); + background: rgba(126, 231, 135, 0.1); + color: var(--gt-green); +} + +.gtn-version-pill__button span { + color: var(--gt-muted); + font-weight: 700; + text-transform: uppercase; +} + +.gtn-version-pill__button strong { + font-weight: 800; +} + +.gtn-version-pill__button em { + position: absolute; + right: -8px; + bottom: -13px; + border: 1px solid rgba(126, 231, 135, 0.44); + border-radius: 3px; + padding: 1px 4px; + background: var(--gt-green); + color: var(--gt-black); + font-size: 8px; + font-style: normal; + font-weight: 900; + line-height: 1.2; + text-transform: uppercase; +} + +.gtn-version-pill__menu { + position: absolute; + top: calc(100% + 10px); + right: 0; + width: min(320px, calc(100vw - 32px)); + border: 1px solid var(--gt-line-strong); + border-radius: 6px; + padding: 8px; + background: rgba(5, 6, 8, 0.98); + box-shadow: 0 20px 56px rgba(0, 0, 0, 0.46); +} + +.gtn-version-pill__menu-head { + display: flex; + align-items: center; + gap: 8px; + border-bottom: 1px solid var(--gt-line); + padding: 4px 4px 8px; + color: var(--gt-muted); + text-transform: uppercase; +} + +.gtn-version-pill__menu-head span { + flex: 1 1 auto; +} + +.gtn-version-pill__menu a { + color: var(--gt-green); + text-decoration: none; +} + +.gtn-version-pill__version { + display: grid; + grid-template-columns: minmax(72px, auto) minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + border-radius: 4px; + padding: 8px 6px; + color: var(--gt-text) !important; +} + +.gtn-version-pill__version:hover { + background: rgba(126, 231, 135, 0.08); +} + +.gtn-version-pill__version time { + color: var(--gt-muted); + font-size: 10px; +} + +.gtn-version-pill__version span { + border: 1px solid rgba(126, 231, 135, 0.32); + border-radius: 3px; + padding: 1px 4px; + color: var(--gt-green); + font-size: 8px; + font-weight: 900; + text-transform: uppercase; +} + +.gtn-version-pill__note { + margin: 6px 4px 2px; + color: var(--gt-muted); + font-size: 11px; + line-height: 1.45; +} + +.gtn-version-pill--footer { + justify-content: center; + width: 100%; + margin: 0 auto 18px; +} + +.gtn-version-pill--footer .gtn-version-pill__menu { + right: 50%; + transform: translateX(50%); +} + +.gtn-version-pill--fallback .gtn-version-pill__button { + border-style: dashed; +} + .VPHome { background: linear-gradient(180deg, rgba(3, 28, 220, 0.14), transparent 260px), @@ -474,6 +619,12 @@ body { } } +@media (max-width: 760px) { + .gtn-version-pill:not(.gtn-version-pill--footer) { + display: none; + } +} + @media (max-width: 640px) { .VPDoc { padding: 0 24px !important; diff --git a/site/.vitepress/theme/index.ts b/site/.vitepress/theme/index.ts index c495bc1b8d..123bbdd118 100644 --- a/site/.vitepress/theme/index.ts +++ b/site/.vitepress/theme/index.ts @@ -1,4 +1,13 @@ import DefaultTheme from "vitepress/theme"; +import { h } from "vue"; +import McpVersionPill from "./components/McpVersionPill.vue"; import "./custom.css"; -export default DefaultTheme; +export default { + extends: DefaultTheme, + Layout: () => + h(DefaultTheme.Layout, null, { + "nav-bar-content-after": () => h(McpVersionPill), + "layout-bottom": () => h(McpVersionPill, { placement: "footer" }), + }), +}; diff --git a/site/security/privacy.md b/site/security/privacy.md index 7593023363..149de5ab37 100644 --- a/site/security/privacy.md +++ b/site/security/privacy.md @@ -2,6 +2,10 @@ Gittensory handles contribution intelligence, not wallets or private source code. +## Docs Analytics + +The public docs site may use privacy-preserving Umami analytics to count traffic and high-level page usage. Analytics are configured only at build time, do not run in local docs builds by default, and are separate from the MCP/API product. + ## No PAT Storage MCP login uses GitHub OAuth Device Flow. The backend exchanges the GitHub token for a Gittensory session token and stores only the hashed Gittensory token server-side.