Skip to content

Commit cc404f7

Browse files
committed
docs(nav): nest self-hosting under Maintainers, fix gate review nits
Gate review round on PR #2638 flagged three real content gaps, all fixed: - Route meta description didn't mention the 5 new runbook topics. - Only the REST cache hit-rate PromQL was shown despite documenting both REST and GraphQL cache metrics -- added the GraphQL equivalent. - The Qdrant collection-drop guidance didn't name the fixed collection ("gittensory", hard-coded, not env-configurable) or warn that dropping it temporarily removes ALL indexed RAG context until reindexing completes. Also adds a drift-guard test (mirrors #2556's check-openapi-settings- parity.mjs pattern): every gittensory_*_total metric name and Gittensory* alert name referenced in the troubleshooting doc is cross-checked against the actual source files and prometheus/rules/alerts.yml, so a future rename/removal fails this test instead of the docs silently going stale. Mutation-tested (a deliberately wrong metric name correctly fails it). Separately: the self-hosting docs section was flat (13 pages under one sidebar heading) and sat as its own top-level nav category alongside "Maintainers" -- misleadingly implying self-hosting is an alternative to, rather than a maintainer concern under, "Maintainers". Nests it as 4 sub-categories (setup / integrations / operations / release & security) inside the Maintainers group instead, alongside the existing maintainer pages as a "Hosted app" sub-category. No routes changed, only the nav data model (extended to support one level of subgroups) and its render logic. Verified in a live preview: nested titles render, active-link highlighting and prev/next navigation both correctly span subgroup and group boundaries.
1 parent 6d0b065 commit cc404f7

3 files changed

Lines changed: 157 additions & 57 deletions

File tree

apps/gittensory-ui/src/components/site/docs-nav.tsx

Lines changed: 95 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { Link, useRouterState } from "@tanstack/react-router";
33
import { cn } from "@/lib/utils";
44

55
type DocsItem = { to: string; label: string };
6-
type DocsGroup = { title: string; items: DocsItem[] };
6+
type DocsSubgroup = { title: string; items: DocsItem[] };
7+
// A group is either a flat list (`items`) or a nested category/sub-category/step hierarchy
8+
// (`subgroups`) — never both. Self-hosting is deliberately nested UNDER "Maintainers" (a maintainer
9+
// concern: running your own instance) rather than sitting as its own top-level sibling category, and
10+
// its own pages are grouped into sub-categories instead of one long flat list.
11+
type DocsGroup = { title: string } & ({ items: DocsItem[] } | { subgroups: DocsSubgroup[] });
712

813
export const docsNav: DocsGroup[] = [
914
{
@@ -21,28 +26,48 @@ export const docsNav: DocsGroup[] = [
2126
},
2227
{
2328
title: "Maintainers",
24-
items: [
25-
{ to: "/docs/maintainer-workflow", label: "Maintainer workflow" },
26-
{ to: "/docs/github-app", label: "GitHub App" },
27-
{ to: "/docs/maintainer-install-trust", label: "Maintainer install & trust" },
28-
],
29-
},
30-
{
31-
title: "Self-hosting",
32-
items: [
33-
{ to: "/docs/maintainer-self-hosting", label: "Overview" },
34-
{ to: "/docs/self-hosting-quickstart", label: "Quickstart" },
35-
{ to: "/docs/self-hosting-configuration", label: "Configuration" },
36-
{ to: "/docs/self-hosting-github-app", label: "GitHub App & Orb" },
37-
{ to: "/docs/self-hosting-ai-providers", label: "AI providers" },
38-
{ to: "/docs/self-hosting-rees", label: "REES enrichment" },
39-
{ to: "/docs/self-hosting-rees-analyzers", label: "REES analyzers" },
40-
{ to: "/docs/self-hosting-rag", label: "RAG indexing" },
41-
{ to: "/docs/self-hosting-operations", label: "Operations" },
42-
{ to: "/docs/self-hosting-backup-scaling", label: "Backup & scaling" },
43-
{ to: "/docs/self-hosting-releases", label: "Releases & images" },
44-
{ to: "/docs/self-hosting-security", label: "Security" },
45-
{ to: "/docs/self-hosting-troubleshooting", label: "Troubleshooting" },
29+
subgroups: [
30+
{
31+
title: "Hosted app",
32+
items: [
33+
{ to: "/docs/maintainer-workflow", label: "Maintainer workflow" },
34+
{ to: "/docs/github-app", label: "GitHub App" },
35+
{ to: "/docs/maintainer-install-trust", label: "Maintainer install & trust" },
36+
],
37+
},
38+
{
39+
title: "Self-hosting: setup",
40+
items: [
41+
{ to: "/docs/maintainer-self-hosting", label: "Overview" },
42+
{ to: "/docs/self-hosting-quickstart", label: "Quickstart" },
43+
{ to: "/docs/self-hosting-configuration", label: "Configuration" },
44+
],
45+
},
46+
{
47+
title: "Self-hosting: integrations",
48+
items: [
49+
{ to: "/docs/self-hosting-github-app", label: "GitHub App & Orb" },
50+
{ to: "/docs/self-hosting-ai-providers", label: "AI providers" },
51+
{ to: "/docs/self-hosting-rees", label: "REES enrichment" },
52+
{ to: "/docs/self-hosting-rees-analyzers", label: "REES analyzers" },
53+
{ to: "/docs/self-hosting-rag", label: "RAG indexing" },
54+
],
55+
},
56+
{
57+
title: "Self-hosting: operations",
58+
items: [
59+
{ to: "/docs/self-hosting-operations", label: "Operations" },
60+
{ to: "/docs/self-hosting-backup-scaling", label: "Backup & scaling" },
61+
{ to: "/docs/self-hosting-troubleshooting", label: "Troubleshooting" },
62+
],
63+
},
64+
{
65+
title: "Self-hosting: release & security",
66+
items: [
67+
{ to: "/docs/self-hosting-releases", label: "Releases & images" },
68+
{ to: "/docs/self-hosting-security", label: "Security" },
69+
],
70+
},
4671
],
4772
},
4873
{
@@ -64,6 +89,38 @@ export const docsNav: DocsGroup[] = [
6489
},
6590
];
6691

92+
function groupItems(group: DocsGroup): DocsItem[] {
93+
return "items" in group ? group.items : group.subgroups.flatMap((sub) => sub.items);
94+
}
95+
96+
function DocsItemList({ items, pathname }: { items: DocsItem[]; pathname: string }) {
97+
return (
98+
<ul className="space-y-0.5">
99+
{items.map((it) => {
100+
const active = pathname === it.to;
101+
return (
102+
<li key={it.to}>
103+
<Link
104+
to={it.to as "/docs"}
105+
className={cn(
106+
"relative block rounded-token px-3 py-1.5 text-token-sm transition-colors",
107+
active
108+
? "bg-mint/10 text-mint"
109+
: "text-foreground/75 hover:bg-accent/50 hover:text-foreground",
110+
)}
111+
>
112+
{active && (
113+
<span className="absolute left-0 top-1/2 h-4 w-px -translate-y-1/2 bg-mint" />
114+
)}
115+
{it.label}
116+
</Link>
117+
</li>
118+
);
119+
})}
120+
</ul>
121+
);
122+
}
123+
67124
export function DocsNav() {
68125
const pathname = useRouterState({ select: (s) => s.location.pathname });
69126
return (
@@ -73,29 +130,20 @@ export function DocsNav() {
73130
<div className="mb-2 font-mono text-token-2xs uppercase tracking-wider text-muted-foreground">
74131
{group.title}
75132
</div>
76-
<ul className="space-y-0.5">
77-
{group.items.map((it) => {
78-
const active = pathname === it.to;
79-
return (
80-
<li key={it.to}>
81-
<Link
82-
to={it.to as "/docs"}
83-
className={cn(
84-
"relative block rounded-token px-3 py-1.5 text-token-sm transition-colors",
85-
active
86-
? "bg-mint/10 text-mint"
87-
: "text-foreground/75 hover:bg-accent/50 hover:text-foreground",
88-
)}
89-
>
90-
{active && (
91-
<span className="absolute left-0 top-1/2 h-4 w-px -translate-y-1/2 bg-mint" />
92-
)}
93-
{it.label}
94-
</Link>
95-
</li>
96-
);
97-
})}
98-
</ul>
133+
{"items" in group ? (
134+
<DocsItemList items={group.items} pathname={pathname} />
135+
) : (
136+
<div className="space-y-4">
137+
{group.subgroups.map((sub) => (
138+
<div key={sub.title}>
139+
<div className="mb-1 pl-3 text-token-2xs font-medium text-foreground/50">
140+
{sub.title}
141+
</div>
142+
<DocsItemList items={sub.items} pathname={pathname} />
143+
</div>
144+
))}
145+
</div>
146+
)}
99147
</div>
100148
))}
101149
</nav>
@@ -104,7 +152,7 @@ export function DocsNav() {
104152

105153
export function DocsPrevNext() {
106154
const pathname = useRouterState({ select: (s) => s.location.pathname });
107-
const flat = docsNav.flatMap((g) => g.items);
155+
const flat = docsNav.flatMap(groupItems);
108156
const idx = flat.findIndex((i) => i.to === pathname);
109157
const prev = idx > 0 ? flat[idx - 1] : null;
110158
const next = idx >= 0 && idx < flat.length - 1 ? flat[idx + 1] : null;

apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ export const Route = createFileRoute("/docs/self-hosting-troubleshooting")({
1010
{
1111
name: "description",
1212
content:
13-
"Troubleshoot self-hosted Gittensory reviews: webhook delivery, AI unavailable, REES silent, RAG empty, queue stuck, and readiness failures.",
13+
"Troubleshoot self-hosted Gittensory reviews: webhook delivery, AI unavailable, REES silent, RAG empty, queue stuck, GitHub rate limits, Qdrant, Orb, AI provider circuit breakers, and readiness failures.",
1414
},
1515
{ property: "og:title", content: "Self-host troubleshooting — Gittensory docs" },
1616
{
1717
property: "og:description",
1818
content:
19-
"Troubleshoot self-hosted Gittensory reviews: webhook delivery, AI unavailable, REES silent, RAG empty, queue stuck, and readiness failures.",
19+
"Troubleshoot self-hosted Gittensory reviews: webhook delivery, AI unavailable, REES silent, RAG empty, queue stuck, GitHub rate limits, Qdrant, Orb, AI provider circuit breakers, and readiness failures.",
2020
},
2121
{ property: "og:url", content: "/docs/self-hosting-troubleshooting" },
2222
],
@@ -174,10 +174,15 @@ sum(rate(gittensory_github_rest_rate_limit_responses_total[10m]))`}
174174
</p>
175175
<CodeBlock
176176
lang="promql"
177-
code={`# Hit rate by endpoint class over the last 15m
177+
code={`# REST hit rate by endpoint class over the last 15m
178178
sum by (class) (rate(gittensory_github_response_cache_total{result="hit"}[15m]))
179179
/
180-
sum by (class) (rate(gittensory_github_response_cache_total[15m]))`}
180+
sum by (class) (rate(gittensory_github_response_cache_total[15m]))
181+
182+
# GraphQL hit rate — same shape, separate metric
183+
sum by (class) (rate(gittensory_github_graphql_cache_total{result="hit"}[15m]))
184+
/
185+
sum by (class) (rate(gittensory_github_graphql_cache_total[15m]))`}
181186
/>
182187

183188
<h2>Qdrant / vector-store errors</h2>
@@ -197,16 +202,21 @@ sum by (class) (rate(gittensory_github_response_cache_total[15m]))`}
197202
deployment&apos;s configuration.
198203
</li>
199204
<li>
200-
A dimension-mismatch error means the existing collection was created with a different
201-
embedding model than the one currently configured (<code>AI_EMBED_MODEL</code>) — recreate
202-
the collection (drop it in Qdrant and let the next index run recreate it at the current
203-
width) rather than trying to reuse it across embedding models.
205+
A dimension-mismatch error means the existing <code>gittensory</code> collection (the
206+
fixed collection name self-host always uses) was created with a different embedding model
207+
than the one currently configured (<code>AI_EMBED_MODEL</code>). Recreating it — delete
208+
the collection and let the next index run recreate it at the current width — is the fix,
209+
but it temporarily removes ALL indexed RAG context for every repo until re-indexing
210+
completes, so treat it as a deliberate, disruptive step, not a routine one.
204211
</li>
205212
</ul>
206213
<CodeBlock
207214
lang="bash"
208-
code={`curl "$QDRANT_URL/collections"
209-
docker compose --profile qdrant ps qdrant`}
215+
code={`curl "$QDRANT_URL/collections/gittensory"
216+
docker compose --profile qdrant ps qdrant
217+
218+
# Only after confirming a dimension mismatch is the actual cause:
219+
curl -X DELETE "$QDRANT_URL/collections/gittensory"`}
210220
/>
211221

212222
<h2>Orb export or relay problems</h2>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { readFileSync } from "node:fs";
2+
import { describe, expect, it } from "vitest";
3+
4+
// Drift guard (#1943 gate review finding): the self-hosting troubleshooting runbooks reference exact
5+
// Prometheus metric names and alert names. If a metric is ever renamed/removed in src/, or an alert is
6+
// renamed/removed in prometheus/rules/alerts.yml, this test fails instead of the docs silently going stale
7+
// — mirrors the same source-of-truth-diff approach as scripts/check-openapi-settings-parity.mjs (#2556).
8+
9+
const DOC_PATH = "apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx";
10+
const doc = readFileSync(DOC_PATH, "utf8");
11+
12+
// The exact source files that emit every gittensory_*_total metric referenced in the runbooks, per an
13+
// audit against the real incr()/gauge()/observe() call sites (src/selfhost/metrics.ts's API).
14+
const METRIC_SOURCE_FILES = [
15+
"src/github/client.ts",
16+
"src/github/graphql-cache.ts",
17+
"src/selfhost/queue-common.ts",
18+
"src/selfhost/sqlite-queue.ts",
19+
"src/selfhost/pg-queue.ts",
20+
"src/selfhost/qdrant-vectorize.ts",
21+
"src/selfhost/orb-collector.ts",
22+
"src/selfhost/monitored-work.ts",
23+
"src/selfhost/ai.ts",
24+
];
25+
const metricSource = METRIC_SOURCE_FILES.map((path) => readFileSync(path, "utf8")).join("\n");
26+
const alertsSource = readFileSync("prometheus/rules/alerts.yml", "utf8");
27+
28+
describe("self-hosting-troubleshooting doc: metric/alert names match source (#1943)", () => {
29+
it("every gittensory_..._total metric name referenced in the doc is actually emitted by the code", () => {
30+
const names = [...new Set([...doc.matchAll(/gittensory_[a-z0-9_]+_total/g)].map((m) => m[0]))];
31+
expect(names.length).toBeGreaterThan(5); // sanity: the extraction found the runbooks' real content
32+
const missing = names.filter((name) => !metricSource.includes(name));
33+
expect(missing).toEqual([]);
34+
});
35+
36+
it("every GittensoryXxx alert name referenced in the doc exists in prometheus/rules/alerts.yml", () => {
37+
const names = [...new Set([...doc.matchAll(/Gittensory[A-Za-z]+/g)].map((m) => m[0]))];
38+
expect(names.length).toBeGreaterThan(2);
39+
const missing = names.filter((name) => !alertsSource.includes(`alert: ${name}`));
40+
expect(missing).toEqual([]);
41+
});
42+
});

0 commit comments

Comments
 (0)