Skip to content

feat(hosts): add one-click Categraf install dialog and templated install docs - #2202

Merged
jsers merged 1 commit into
mainfrom
optimize-collect
Jul 22, 2026
Merged

feat(hosts): add one-click Categraf install dialog and templated install docs#2202
jsers merged 1 commit into
mainfrom
optimize-collect

Conversation

@710leo

@710leo 710leo commented Jul 22, 2026

Copy link
Copy Markdown
Member
image

Summary by CodeRabbit

  • New Features

    • Added one-click Categraf installation from the Hosts page, with optional Basic Authentication.
    • Added manual installation instructions and copyable commands through an advanced setup option.
    • Added installation progress tracking, automatic host detection, retry handling, and completion feedback.
    • Added dynamic placeholder replacement for localized Markdown documentation.
  • Documentation

    • Updated Categraf deployment guides in English, Russian, Simplified Chinese, and Traditional Chinese with current installation and configuration steps.
    • Added localized text for the new installation workflow.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Categraf one-click installation flow with generated commands, localized modal UI, installation metadata discovery, target-arrival polling, runtime documentation variables, and revised multilingual deployment instructions.

Changes

Categraf installation

Layer / File(s) Summary
Installation metadata and target probing
src/pages/hosts/services.ts
Adds cached installation metadata retrieval and silent target probing helpers.
Installer command generation
src/pages/hosts/pages/List/InstallCategraf/buildCommand.ts
Builds validated, shell-quoted one-click and manual installation commands with optional Basic Auth.
Runtime documentation variables
src/components/DocumentDrawer/*, public/n9e-docs/categraf/*
Substitutes runtime server and version placeholders in Markdown and updates Categraf deployment instructions across supported languages.
Installation modal and localized interface
src/pages/hosts/pages/List/InstallCategraf/index.tsx, src/pages/hosts/locale/*
Adds the installation modal, command copying, authentication fields, advanced instructions, status displays, and localized strings.
Arrival detection and hosts-list integration
src/pages/hosts/pages/List/InstallCategraf/useTargetArrival.ts, src/pages/hosts/pages/List/List.tsx
Polls for newly reported ungrouped machines and connects installation metadata, modal actions, documentation variables, and list refreshes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HostsList
  participant InstallCategraf
  participant CategrafServices
  HostsList->>CategrafServices: fetch installation metadata
  HostsList->>InstallCategraf: open installation modal
  InstallCategraf->>CategrafServices: poll target arrival
  CategrafServices-->>InstallCategraf: return target totals and ungrouped idents
  InstallCategraf-->>HostsList: report detection result
  HostsList->>HostsList: refresh target list when detected
Loading

Possibly related PRs

  • n9e/fe#2138: Updates related hosts-list empty-state and Categraf documentation-opening behavior.

Suggested reviewers: jsers

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a one-click Categraf install dialog plus templated installation docs for hosts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimize-collect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/DocumentDrawer/Document.tsx (1)

45-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep runtime documentation variables synchronized with the fetch effect. Both components now consume variables but do not rerun their loading effect when it changes.

  • src/components/DocumentDrawer/Document.tsx#L45-L56: add stable variables dependencies or memoize the variables map.
  • src/components/DocumentDrawer/index.tsx#L45-L67: replace the empty dependency list with stable dependencies including variables.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DocumentDrawer/Document.tsx` around lines 45 - 56, The
document-loading effects must rerun when their rendered variables change. In
src/components/DocumentDrawer/Document.tsx lines 45-56, add a stable variables
dependency or memoize the variables map before including it in the effect
dependencies; in src/components/DocumentDrawer/index.tsx lines 45-67, replace
the empty dependency list with stable dependencies that include variables,
preserving the existing fetch and fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/DocumentDrawer/Document.tsx`:
- Around line 47-53: Handle rejection from the en_US fallback fetch by adding
terminal error handling after the fallback promise chain in Document.tsx (lines
47-53) and index.tsx (lines 58-64). Apply the existing request-error pattern or
set an explicit error state in both fallback paths so every asynchronous request
is handled.

In `@src/pages/hosts/pages/List/InstallCategraf/useTargetArrival.ts`:
- Around line 24-65: Prevent overlapping baseline probes in captureBaseline by
adding an in-flight guard or shared promise, so start and the useInterval
recovery path reuse the same capture attempt rather than launching concurrent
calls. Ensure the guard is cleared when the attempt settles, while
baselineRef.current is updated only from the active capture result and existing
success/failure status behavior is preserved.

In `@src/pages/hosts/services.ts`:
- Around line 72-110: Update getCategrafInstallMeta’s response handling so only
a deterministic missing-route response such as 404 remains cached as null; for
other non-2xx responses, clear categrafMetaPromise before returning null,
matching the retry behavior in the catch block. Preserve the existing successful
JSON validation and network-error handling.

---

Outside diff comments:
In `@src/components/DocumentDrawer/Document.tsx`:
- Around line 45-56: The document-loading effects must rerun when their rendered
variables change. In src/components/DocumentDrawer/Document.tsx lines 45-56, add
a stable variables dependency or memoize the variables map before including it
in the effect dependencies; in src/components/DocumentDrawer/index.tsx lines
45-67, replace the empty dependency list with stable dependencies that include
variables, preserving the existing fetch and fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7286e4f7-3438-48be-9457-abca5a4d438f

📥 Commits

Reviewing files that changed from the base of the PR and between 4cdaaa7 and 644c70d.

📒 Files selected for processing (17)
  • public/n9e-docs/categraf/en_US.md
  • public/n9e-docs/categraf/ru_RU.md
  • public/n9e-docs/categraf/zh_CN.md
  • public/n9e-docs/categraf/zh_HK.md
  • src/components/DocumentDrawer/Document.tsx
  • src/components/DocumentDrawer/index.tsx
  • src/components/DocumentDrawer/renderVariables.ts
  • src/pages/hosts/locale/en_US.ts
  • src/pages/hosts/locale/ja_JP.ts
  • src/pages/hosts/locale/ru_RU.ts
  • src/pages/hosts/locale/zh_CN.ts
  • src/pages/hosts/locale/zh_HK.ts
  • src/pages/hosts/pages/List/InstallCategraf/buildCommand.ts
  • src/pages/hosts/pages/List/InstallCategraf/index.tsx
  • src/pages/hosts/pages/List/InstallCategraf/useTargetArrival.ts
  • src/pages/hosts/pages/List/List.tsx
  • src/pages/hosts/services.ts

Comment on lines 47 to 53
.catch(() => {
// 如果获取文档失败,使用 en_US 作为默认语言
return fetch(`${documentPath}/en_US.md`)
.then((res) => res.text())
.then((res) => {
setDocument(res);
setDocument(renderVariables(res, variables));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle rejection of every fallback document request. In both components, the fallback fetch is returned from a catch handler without a final rejection handler.

  • src/components/DocumentDrawer/Document.tsx#L47-L53: add a terminal catch or explicit error state after the en_US fallback request.
  • src/components/DocumentDrawer/index.tsx#L58-L64: add a terminal catch or explicit error state after the en_US fallback request.

As per coding guidelines, all asynchronous requests must include error handling consistent with existing patterns.

📍 Affects 2 files
  • src/components/DocumentDrawer/Document.tsx#L47-L53 (this comment)
  • src/components/DocumentDrawer/index.tsx#L58-L64
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DocumentDrawer/Document.tsx` around lines 47 - 53, Handle
rejection from the en_US fallback fetch by adding terminal error handling after
the fallback promise chain in Document.tsx (lines 47-53) and index.tsx (lines
58-64). Apply the existing request-error pattern or set an explicit error state
in both fallback paths so every asynchronous request is handled.

Source: Coding guidelines

Comment on lines +24 to +65
/**
* 建立基线。探测失败返回 false 且不写入基线 —— 失败绝不能当成「0 台」,
* 否则下一轮会把存量机器全部误判成新上报,给用户一个假的成功信号。
*/
const captureBaseline = useCallback(async () => {
const [all, ungrouped] = await Promise.all([probeTargets(), probeTargets({ gids: '0', limit: 100 })]);
if (!all) return false;
baselineRef.current = {
total: all.total,
ungrouped: new Set(_.map(ungrouped?.list ?? [], 'ident')),
};
return true;
}, []);

const start = useCallback(() => {
baselineRef.current = null;
setNewIdents([]);
setStatus('baselining');
startedAtRef.current = Date.now();
captureBaseline().then((ok) => {
if (ok) setStatus('waiting');
});
}, [captureBaseline]);

useEffect(() => {
start();
}, [start]);

useInterval(
() => {
if (Date.now() - startedAtRef.current > POLL_TIMEOUT) {
setStatus('timeout');
return;
}
const baseline = baselineRef.current;
if (!baseline) {
// 首次探测失败,先把基线补建起来,期间不做任何「新机器」判断
captureBaseline().then((ok) => {
if (ok) setStatus('waiting');
});
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Concurrent captureBaseline() calls can clobber the baseline with stale data.

captureBaseline has no guard against re-entrancy. While status is 'baselining' (or whenever baselineRef.current is still null), the useInterval tick (Line 59-64) fires every POLL_INTERVAL and calls captureBaseline() again — even if the original call from start() (Line 43) is merely slow, not failed. If the first (now-stale) call resolves after the second, it overwrites baselineRef.current with older data, which can produce a spurious "new host detected" or mask a real one on the next tick. This will reproduce deterministically whenever probeTargets takes ≥5s to resolve.

🛠️ Suggested fix — guard against overlapping captures
+  const capturingRef = useRef(false);
+
   const captureBaseline = useCallback(async () => {
-    const [all, ungrouped] = await Promise.all([probeTargets(), probeTargets({ gids: '0', limit: 100 })]);
-    if (!all) return false;
-    baselineRef.current = {
-      total: all.total,
-      ungrouped: new Set(_.map(ungrouped?.list ?? [], 'ident')),
-    };
-    return true;
+    if (capturingRef.current) return false;
+    capturingRef.current = true;
+    try {
+      const [all, ungrouped] = await Promise.all([probeTargets(), probeTargets({ gids: '0', limit: 100 })]);
+      if (!all) return false;
+      baselineRef.current = {
+        total: all.total,
+        ungrouped: new Set(_.map(ungrouped?.list ?? [], 'ident')),
+      };
+      return true;
+    } finally {
+      capturingRef.current = false;
+    }
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 建立基线。探测失败返回 false 且不写入基线 —— 失败绝不能当成「0 台」,
* 否则下一轮会把存量机器全部误判成新上报,给用户一个假的成功信号。
*/
const captureBaseline = useCallback(async () => {
const [all, ungrouped] = await Promise.all([probeTargets(), probeTargets({ gids: '0', limit: 100 })]);
if (!all) return false;
baselineRef.current = {
total: all.total,
ungrouped: new Set(_.map(ungrouped?.list ?? [], 'ident')),
};
return true;
}, []);
const start = useCallback(() => {
baselineRef.current = null;
setNewIdents([]);
setStatus('baselining');
startedAtRef.current = Date.now();
captureBaseline().then((ok) => {
if (ok) setStatus('waiting');
});
}, [captureBaseline]);
useEffect(() => {
start();
}, [start]);
useInterval(
() => {
if (Date.now() - startedAtRef.current > POLL_TIMEOUT) {
setStatus('timeout');
return;
}
const baseline = baselineRef.current;
if (!baseline) {
// 首次探测失败,先把基线补建起来,期间不做任何「新机器」判断
captureBaseline().then((ok) => {
if (ok) setStatus('waiting');
});
return;
}
const capturingRef = useRef(false);
/**
* 建立基线。探测失败返回 false 且不写入基线 —— 失败绝不能当成「0 台」,
* 否则下一轮会把存量机器全部误判成新上报,给用户一个假的成功信号。
*/
const captureBaseline = useCallback(async () => {
if (capturingRef.current) return false;
capturingRef.current = true;
try {
const [all, ungrouped] = await Promise.all([probeTargets(), probeTargets({ gids: '0', limit: 100 })]);
if (!all) return false;
baselineRef.current = {
total: all.total,
ungrouped: new Set(_.map(ungrouped?.list ?? [], 'ident')),
};
return true;
} finally {
capturingRef.current = false;
}
}, []);
const start = useCallback(() => {
baselineRef.current = null;
setNewIdents([]);
setStatus('baselining');
startedAtRef.current = Date.now();
captureBaseline().then((ok) => {
if (ok) setStatus('waiting');
});
}, [captureBaseline]);
useEffect(() => {
start();
}, [start]);
useInterval(
() => {
if (Date.now() - startedAtRef.current > POLL_TIMEOUT) {
setStatus('timeout');
return;
}
const baseline = baselineRef.current;
if (!baseline) {
// 首次探测失败,先把基线补建起来,期间不做任何「新机器」判断
captureBaseline().then((ok) => {
if (ok) setStatus('waiting');
});
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/hosts/pages/List/InstallCategraf/useTargetArrival.ts` around lines
24 - 65, Prevent overlapping baseline probes in captureBaseline by adding an
in-flight guard or shared promise, so start and the useInterval recovery path
reuse the same capture attempt rather than launching concurrent calls. Ensure
the guard is cleared when the attempt settles, while baselineRef.current is
updated only from the active capture result and existing success/failure status
behavior is preserved.

Comment on lines +72 to +110
export function getCategrafInstallMeta(): Promise<CategrafInstallMeta | null> {
if (IS_PLUS) return Promise.resolve(null); // 企业版有自己的采集器接入体系
if (!categrafMetaPromise) {
categrafMetaPromise = fetch(`${basePrefix}/api/n9e/agents/categraf/meta`, {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${localStorage.getItem(AccessTokenKey) || ''}`,
'X-Language': i18next.language,
},
})
.then(async (res) => {
if (!res.ok) return null;
// SPA 兜底返回的是 HTML,content-type 与首字符两道判断
if (!(res.headers.get('content-type') || '').includes('json')) return null;
const text = await res.text();
if (/^\s*</.test(text)) return null;
const dat = JSON.parse(text)?.dat;
if (!dat || typeof dat.version !== 'string') return null;
return {
bundled: !!dat.bundled,
version: dat.version,
arches: Array.isArray(dat.arches) ? dat.arches : [],
basic_auth: !!dat.basic_auth,
base_url: dat.base_url || '',
script_url: dat.script_url || '',
} as CategrafInstallMeta;
})
.catch((err) => {
// 按「不支持」降级,但必须留痕:这个探测决定入口是否出现,
// 静默失败会让一键安装按钮无声消失且毫无排查线索
console.error('categraf install meta probe failed', err);
categrafMetaPromise = undefined;
return null;
}); // 这个 promise 永不 reject
}
return categrafMetaPromise;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Non-2xx responses are cached as "unsupported" forever, defeating the stated retry intent.

The docstring says network-layer failures aren't cached so a "next mount" retry is possible, but that guarantee only covers fetch rejecting. If the response resolves with !res.ok (Line 85) — e.g. a transient 401 because the access token was mid-refresh at initial page load, or a 500categrafMetaPromise is never reset, so null is memoized for the rest of the SPA session and the install entry silently disappears until a full page reload, exactly the failure mode the comment says it's trying to avoid.

Consider only treating clearly deterministic "route doesn't exist" signals (e.g. 404) as permanently cacheable, and resetting the cache (like the catch block already does) for other non-2xx statuses so a later mount can retry.

🛠️ Suggested fix
       .then(async (res) => {
-        if (!res.ok) return null;
+        if (!res.ok) {
+          // 404 之外的状态码(如登录态刷新期间的 401、临时的 5xx)不代表「不支持」,
+          // 不能永久缓存,否则一次瞬断会让入口整个会话消失
+          if (res.status !== 404) categrafMetaPromise = undefined;
+          return null;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getCategrafInstallMeta(): Promise<CategrafInstallMeta | null> {
if (IS_PLUS) return Promise.resolve(null); // 企业版有自己的采集器接入体系
if (!categrafMetaPromise) {
categrafMetaPromise = fetch(`${basePrefix}/api/n9e/agents/categraf/meta`, {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${localStorage.getItem(AccessTokenKey) || ''}`,
'X-Language': i18next.language,
},
})
.then(async (res) => {
if (!res.ok) return null;
// SPA 兜底返回的是 HTML,content-type 与首字符两道判断
if (!(res.headers.get('content-type') || '').includes('json')) return null;
const text = await res.text();
if (/^\s*</.test(text)) return null;
const dat = JSON.parse(text)?.dat;
if (!dat || typeof dat.version !== 'string') return null;
return {
bundled: !!dat.bundled,
version: dat.version,
arches: Array.isArray(dat.arches) ? dat.arches : [],
basic_auth: !!dat.basic_auth,
base_url: dat.base_url || '',
script_url: dat.script_url || '',
} as CategrafInstallMeta;
})
.catch((err) => {
// 按「不支持」降级,但必须留痕:这个探测决定入口是否出现,
// 静默失败会让一键安装按钮无声消失且毫无排查线索
console.error('categraf install meta probe failed', err);
categrafMetaPromise = undefined;
return null;
}); // 这个 promise 永不 reject
}
return categrafMetaPromise;
}
.then(async (res) => {
if (!res.ok) {
// 404 之外的状态码(如登录态刷新期间的 401、临时的 5xx)不代表「不支持」,
// 不能永久缓存,否则一次瞬断会让入口整个会话消失
if (res.status !== 404) categrafMetaPromise = undefined;
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/hosts/services.ts` around lines 72 - 110, Update
getCategrafInstallMeta’s response handling so only a deterministic missing-route
response such as 404 remains cached as null; for other non-2xx responses, clear
categrafMetaPromise before returning null, matching the retry behavior in the
catch block. Preserve the existing successful JSON validation and network-error
handling.

@jsers
jsers merged commit ba6551f into main Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants