diff --git a/apps/gittensory-ui/src/components/site/ams-observability-callout.tsx b/apps/gittensory-ui/src/components/site/ams-observability-callout.tsx new file mode 100644 index 0000000000..614b0a544d --- /dev/null +++ b/apps/gittensory-ui/src/components/site/ams-observability-callout.tsx @@ -0,0 +1,27 @@ +import { Callout } from "@/components/site/primitives"; + +// AMS (gittensory-miner) observability cross-reference (#5191). A dual-role self-hoster running both ORB (the +// review service) and AMS (the miner) on one box otherwise has no in-app pointer from the operations / quickstart / +// workflow docs to the miner's observability setup. Keeping the callout — and its link target — in one place keeps +// the wording byte-identical across all three routes instead of relying on three hand-copied copies staying in sync. +// +// The target is the in-repo "Observing your miner" guide (landed in #5190): the single AMS observability entry point, +// which itself covers pointing Grafana at the redacted AMS ledger datasources AND loading an AMS dashboard from +// grafana/dashboards/. It is a markdown guide, not an in-app /docs/* route, so a GitHub-blob link is the correct +// target here — the same convention the docs already use for in-repo file references (see docs.self-hosting-configuration.tsx). +export const AMS_OBSERVABILITY_DOC_URL = + "https://github.com/JSONbored/gittensory/blob/main/packages/gittensory-miner/docs/observability.md"; + +/** A `note` callout pointing a dual-role ORB+AMS operator at the "Observing your miner" observability guide. */ +export function AmsObservabilityCallout() { + return ( + + If you also run AMS (the gittensory-miner) on this host, see{" "} + + Observing your miner + {" "} + to point Grafana at the redacted AMS ledger datasources and load its Grafana dashboard — + separate from the ORB review-service observability above. + + ); +} diff --git a/apps/gittensory-ui/src/routeTree.gen.ts b/apps/gittensory-ui/src/routeTree.gen.ts index 1cfcf9f5a7..5cf0eca531 100644 --- a/apps/gittensory-ui/src/routeTree.gen.ts +++ b/apps/gittensory-ui/src/routeTree.gen.ts @@ -43,9 +43,9 @@ import { Route as DocsScoreabilityRouteImport } from './routes/docs.scoreability import { Route as DocsQuickstartRouteImport } from './routes/docs.quickstart' import { Route as DocsPrivacySecurityRouteImport } from './routes/docs.privacy-security' import { Route as DocsOwnerChecklistRouteImport } from './routes/docs.owner-checklist' -import { Route as DocsMinerCodingAgentRouteImport } from './routes/docs.miner-coding-agent' import { Route as DocsMinerWorkflowRouteImport } from './routes/docs.miner-workflow' import { Route as DocsMinerQuickstartRouteImport } from './routes/docs.miner-quickstart' +import { Route as DocsMinerCodingAgentRouteImport } from './routes/docs.miner-coding-agent' import { Route as DocsMcpClientsRouteImport } from './routes/docs.mcp-clients' import { Route as DocsMaintainerWorkflowRouteImport } from './routes/docs.maintainer-workflow' import { Route as DocsMaintainerSelfHostingRouteImport } from './routes/docs.maintainer-self-hosting' @@ -252,11 +252,6 @@ const DocsOwnerChecklistRoute = DocsOwnerChecklistRouteImport.update({ path: '/owner-checklist', getParentRoute: () => DocsRoute, } as any) -const DocsMinerCodingAgentRoute = DocsMinerCodingAgentRouteImport.update({ - id: '/miner-coding-agent', - path: '/miner-coding-agent', - getParentRoute: () => DocsRoute, -} as any) const DocsMinerWorkflowRoute = DocsMinerWorkflowRouteImport.update({ id: '/miner-workflow', path: '/miner-workflow', @@ -267,6 +262,11 @@ const DocsMinerQuickstartRoute = DocsMinerQuickstartRouteImport.update({ path: '/miner-quickstart', getParentRoute: () => DocsRoute, } as any) +const DocsMinerCodingAgentRoute = DocsMinerCodingAgentRouteImport.update({ + id: '/miner-coding-agent', + path: '/miner-coding-agent', + getParentRoute: () => DocsRoute, +} as any) const DocsMcpClientsRoute = DocsMcpClientsRouteImport.update({ id: '/mcp-clients', path: '/mcp-clients', @@ -556,6 +556,7 @@ export interface FileRoutesById { '/docs/maintainer-self-hosting': typeof DocsMaintainerSelfHostingRoute '/docs/maintainer-workflow': typeof DocsMaintainerWorkflowRoute '/docs/mcp-clients': typeof DocsMcpClientsRoute + '/docs/miner-coding-agent': typeof DocsMinerCodingAgentRoute '/docs/miner-quickstart': typeof DocsMinerQuickstartRoute '/docs/miner-workflow': typeof DocsMinerWorkflowRoute '/docs/owner-checklist': typeof DocsOwnerChecklistRoute @@ -746,6 +747,7 @@ export interface FileRouteTypes { | '/docs/maintainer-self-hosting' | '/docs/maintainer-workflow' | '/docs/mcp-clients' + | '/docs/miner-coding-agent' | '/docs/miner-quickstart' | '/docs/miner-workflow' | '/docs/owner-checklist' @@ -1036,13 +1038,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsMinerWorkflowRouteImport parentRoute: typeof DocsRoute } - '/docs/miner-coding-agent': { - id: '/docs/miner-coding-agent' - path: '/miner-coding-agent' - fullPath: '/docs/miner-coding-agent' - preLoaderRoute: typeof DocsMinerCodingAgentRouteImport - parentRoute: typeof DocsRoute - } '/docs/miner-quickstart': { id: '/docs/miner-quickstart' path: '/miner-quickstart' @@ -1050,6 +1045,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsMinerQuickstartRouteImport parentRoute: typeof DocsRoute } + '/docs/miner-coding-agent': { + id: '/docs/miner-coding-agent' + path: '/miner-coding-agent' + fullPath: '/docs/miner-coding-agent' + preLoaderRoute: typeof DocsMinerCodingAgentRouteImport + parentRoute: typeof DocsRoute + } '/docs/mcp-clients': { id: '/docs/mcp-clients' path: '/mcp-clients' diff --git a/apps/gittensory-ui/src/routes/docs.ams-observability-callout.test.tsx b/apps/gittensory-ui/src/routes/docs.ams-observability-callout.test.tsx new file mode 100644 index 0000000000..08b7263f25 --- /dev/null +++ b/apps/gittensory-ui/src/routes/docs.ams-observability-callout.test.tsx @@ -0,0 +1,81 @@ +import { render, screen } from "@testing-library/react"; +import { type ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +import { + AMS_OBSERVABILITY_DOC_URL, + AmsObservabilityCallout, +} from "../components/site/ams-observability-callout"; +import { MinerQuickstart } from "./docs.miner-quickstart"; +import { MinerWorkflow } from "./docs.miner-workflow"; +import { SelfHostingOperations } from "./docs.self-hosting-operations"; + +vi.mock("@tanstack/react-router", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + Link: ({ to, children }: { to: string; children: ReactNode }) => {children}, + }; +}); + +vi.mock("@/components/site/docs-page", () => ({ + DocsPage: ({ children, title }: { children: ReactNode; title: string }) => ( +
+

{title}

+ {children} +
+ ), +})); + +vi.mock("@/components/site/primitives", () => ({ + Callout: ({ children, title }: { children: ReactNode; title?: string }) => ( +
+ {title ? {title} : null} +
{children}
+
+ ), + CodeBlock: ({ code }: { code: string }) =>
{code}
, + FeatureRow: ({ items }: { items: Array<{ title: string; description: string }> }) => ( +
+ {items.map((item) => ( +
+
{item.title}
+
{item.description}
+
+ ))} +
+ ), +})); + +vi.mock("@/components/site/workflow-mirror", () => ({ + WorkflowMirror: () =>
, +})); + +// Every route that embeds the shared callout, so a new route add/remove can't silently skip one (#5191). +const ROUTES_WITH_CALLOUT: ReadonlyArray<[string, () => ReactNode]> = [ + ["/docs/self-hosting-operations", SelfHostingOperations], + ["/docs/miner-quickstart", MinerQuickstart], + ["/docs/miner-workflow", MinerWorkflow], +]; + +describe("AMS observability cross-reference callout", () => { + it("renders a link to the Observing your miner guide", () => { + render(); + const link = screen.getByRole("link", { name: "Observing your miner" }); + expect(link.getAttribute("href")).toBe(AMS_OBSERVABILITY_DOC_URL); + }); + + it("targets a well-formed, non-empty absolute https URL (guards against a blank/copy-paste link)", () => { + expect(AMS_OBSERVABILITY_DOC_URL).toBeTruthy(); + const url = new URL(AMS_OBSERVABILITY_DOC_URL); + expect(url.protocol).toBe("https:"); + expect(url.hostname).toBe("github.com"); + }); + + it.each(ROUTES_WITH_CALLOUT)("wires the callout into %s", (_path, RouteComponent) => { + const { container } = render(); + const link = container.querySelector(`a[href="${AMS_OBSERVABILITY_DOC_URL}"]`); + expect(link).not.toBeNull(); + expect(link?.textContent).toBe("Observing your miner"); + }); +}); diff --git a/apps/gittensory-ui/src/routes/docs.miner-quickstart.tsx b/apps/gittensory-ui/src/routes/docs.miner-quickstart.tsx index 385848b25c..183f0ea76d 100644 --- a/apps/gittensory-ui/src/routes/docs.miner-quickstart.tsx +++ b/apps/gittensory-ui/src/routes/docs.miner-quickstart.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; +import { AmsObservabilityCallout } from "@/components/site/ams-observability-callout"; import { DocsPage } from "@/components/site/docs-page"; import { CodeBlock, Callout } from "@/components/site/primitives"; import { Link } from "@tanstack/react-router"; @@ -26,7 +27,7 @@ export const Route = createFileRoute("/docs/miner-quickstart")({ component: MinerQuickstart, }); -function MinerQuickstart() { +export function MinerQuickstart() { return ( miner workflow for the full loop and{" "} privacy & security for the boundary details. + ); } diff --git a/apps/gittensory-ui/src/routes/docs.miner-workflow.tsx b/apps/gittensory-ui/src/routes/docs.miner-workflow.tsx index a85c1b2fd7..f1d3c333ed 100644 --- a/apps/gittensory-ui/src/routes/docs.miner-workflow.tsx +++ b/apps/gittensory-ui/src/routes/docs.miner-workflow.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router"; +import { AmsObservabilityCallout } from "@/components/site/ams-observability-callout"; import { DocsPage } from "@/components/site/docs-page"; import { CodeBlock, Callout } from "@/components/site/primitives"; import { WorkflowMirror, type MirroredStep } from "@/components/site/workflow-mirror"; @@ -25,7 +26,7 @@ export const Route = createFileRoute("/docs/miner-workflow")({ component: MinerWorkflow, }); -function MinerWorkflow() { +export function MinerWorkflow() { const steps: MirroredStep[] = [ { title: "Plan", @@ -136,6 +137,7 @@ function MinerWorkflow() { commits, prefer cleaning open work over opening more — risk-adjusted priority is part of the score model. + ); } diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index c94fdba420..e7862a0b0e 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -1,5 +1,6 @@ import { createFileRoute, Link } from "@tanstack/react-router"; +import { AmsObservabilityCallout } from "@/components/site/ams-observability-callout"; import { DocsPage } from "@/components/site/docs-page"; import { Callout, CodeBlock, FeatureRow } from "@/components/site/primitives"; @@ -25,7 +26,7 @@ export const Route = createFileRoute("/docs/self-hosting-operations")({ component: SelfHostingOperations, }); -function SelfHostingOperations() { +export function SelfHostingOperations() { return ( +

Host clock sync (NTP)