Skip to content

feat: optimize host onboarding - #2230

Merged
jsers merged 10 commits into
mainfrom
optimize-host-op
Aug 3, 2026
Merged

feat: optimize host onboarding#2230
jsers merged 10 commits into
mainfrom
optimize-host-op

Conversation

@710leo

@710leo 710leo commented Jul 31, 2026

Copy link
Copy Markdown
Member
image

Summary by CodeRabbit

  • New Features
    • Added guided host-monitoring onboarding with one-click dashboard and alert-rule setup.
    • Added notification-rule creation and test-alert workflows with per-channel results and troubleshooting links.
    • Added “Next steps” cards, remembered data-source verification, and expanded progress tracking.
    • Added first-use guidance for selecting mute methods.
  • Localization
    • Added onboarding translations in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese.
  • Improvements
    • Added localized troubleshooting links and more responsive host-list actions.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds centralized onboarding actions and modals, expands progress tracking, imports a Linux host-monitoring pack, adds follow-up and test-alert flows, persists verified datasources, and adds localized onboarding and mute-method guidance.

Changes

Onboarding progress and actions

Layer / File(s) Summary
Progress, typed actions, and shared modal host
src/components/OnboardingProgress/*, src/components/OnboardingActions/*, src/App.tsx
Onboarding steps support typed actions, optional steps, expanded completion detection, shared refreshes, and centralized modal rendering.
Host monitoring pack import
src/components/OnboardingActions/HostMonitorPack/*
The modal resolves, previews, transforms, and imports selected dashboards and alert rules. It skips existing resources and reports individual failures.
Next steps and host setup integration
src/components/OnboardingActions/NextStepsCard/*, src/pages/hosts/pages/List/*, src/pages/hosts/constants.ts, src/pages/hosts/locale/*, src/pages/landing/locale/*
Follow-up cards guide collection, dashboard, alert, notification, and delivery steps. Successful datasource verification is persisted and reused. Installation timeouts include localized troubleshooting links.
Test alerts and notification delivery
src/components/OnboardingActions/SendTestAlert/*, src/pages/event/EventNotifyRecords/services.ts, src/pages/notificationRules/services.ts, src/utils/request.tsx
The test-alert modal sends channel-specific mock events and displays results. Notification delivery status and silent request failures are handled locally.
Mute-method hint
src/pages/warning/shield/components/operateForm.tsx, src/pages/warning/shield/locale/*
A dismissible first-use hint explains the available mute methods and persists dismissal in local storage.

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

Sequence Diagram(s)

sequenceDiagram
  participant OnboardingTracks
  participant OnboardingActionsProvider
  participant HostMonitorPackModal
  participant SendTestAlertModal
  participant OnboardingProgress
  OnboardingTracks->>OnboardingActionsProvider: open host pack action
  OnboardingActionsProvider->>HostMonitorPackModal: render import modal
  HostMonitorPackModal->>OnboardingProgress: refresh after effective imports
  HostMonitorPackModal->>SendTestAlertModal: continue to test alert
  SendTestAlertModal->>OnboardingProgress: refresh after successful delivery
Loading

Possibly related PRs

  • n9e/fe#2163: Both changes modify onboarding progress and track handling.
  • n9e/fe#2170: Both changes modify mute-method functionality.
  • n9e/fe#2195: Both changes modify notification test requests.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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.
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.
Title check ✅ Passed The title clearly summarizes the pull request's main change: improving host onboarding workflows and actions.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimize-host-op

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: 9

🧹 Nitpick comments (4)
src/pages/hosts/pages/List/CollectSetup/index.tsx (2)

160-164: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Read the remembered IDs once with a lazy state initializer.

useRef(readVerifiedDatasourceIds()) evaluates its argument on every render. The ref keeps only the first value, so each later render performs a localStorage.getItem plus JSON.parse and discards the result. A lazy useState initializer runs the read once.

♻️ Proposed refactor
-  const rememberedDatasourceIds = useRef(readVerifiedDatasourceIds()).current;
+  const [rememberedDatasourceIds] = useState(readVerifiedDatasourceIds);
🤖 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/CollectSetup/index.tsx` around lines 160 - 164,
Replace the useRef(readVerifiedDatasourceIds()) initialization with a lazy
useState initializer so readVerifiedDatasourceIds executes only once, while
preserving the rememberedDatasourceIds value used by defaultFromRemembered.

196-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Declare the effect dependencies or add the lint exemption.

The effect reads arrivalDatasources, arrival.hitDatasourceNames, and datasourceIds, but the dependency array lists only arrival.status. The verifiedMarkedRef guard makes the single-run behavior intentional, so state the intent explicitly. The same file already uses this pattern at Line 291.

♻️ Proposed change
     writeVerifiedDatasourceIds(hitIds.length > 0 ? hitIds : datasourceIds);
+    // 只在 detected 的那一次写入,其余依赖有意省略
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [arrival.status]);
🤖 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/CollectSetup/index.tsx` around lines 196 - 207,
Update the useEffect handling arrival verification to explicitly document its
intentional single-run behavior, following the existing pattern around the later
effect in the same file. Either add the referenced values to the dependency
array while preserving the verifiedMarkedRef guard, or add the established lint
exemption for intentional omitted dependencies; ensure arrivalDatasources,
arrival.hitDatasourceNames, and datasourceIds are covered by the chosen
approach.
src/components/OnboardingProgress/detect.test.ts (1)

80-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the storage-unavailable test exercise the catch branch.

The test relies on delete globalThis.localStorage in afterEach. Under jsdom, localStorage is defined on window, so the delete may not remove it. The assertions then still pass because the key is absent, and the try/catch in detect.ts is never exercised. Install a throwing stub to test the failure path directly.

🧪 Proposed test change
   it('reports false instead of throwing when storage is unavailable', () => {
     // 隐私模式 / 禁用存储:读写都不能把整轮进度探测带崩
+    (globalThis as { localStorage?: unknown }).localStorage = {
+      getItem: () => {
+        throw new Error('storage disabled');
+      },
+      setItem: () => {
+        throw new Error('storage disabled');
+      },
+    };
     expect(() => writeOnboardingMarker('testDelivered')).not.toThrow();
     expect(readOnboardingMarker('testDelivered')).toBe(false);
   });
🤖 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/OnboardingProgress/detect.test.ts` around lines 80 - 84,
Update the storage-unavailable test around writeOnboardingMarker and
readOnboardingMarker to replace localStorage with a stub whose access methods
throw, ensuring both operations enter the catch path directly. Preserve the
existing no-throw and false-result assertions, and restore the original storage
state through the test cleanup.
src/components/OnboardingActions/SendTestAlert/index.tsx (1)

98-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a native type check instead of _.isString.

Line 99 uses _.isString(res?.dat). Replace it with typeof res?.dat === 'string'.

As per coding guidelines: "For type narrowing, prefer native checks such as Array.isArray, typeof x === 'number', and x == null over lodash type-guard functions."

♻️ Proposed fix
-          (res) => ({ index, label, ok: true, detail: _.isString(res?.dat) ? res.dat : undefined } as SendResult),
+          (res) => ({ index, label, ok: true, detail: typeof res?.dat === 'string' ? res.dat : undefined } as SendResult),
🤖 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/OnboardingActions/SendTestAlert/index.tsx` around lines 98 -
101, In the notifyRuleTest success handler within the SendResult mapping,
replace the lodash _.isString(res?.dat) check with the native typeof res?.dat
=== 'string' type guard, preserving the existing detail fallback and error
handling.

Source: Coding guidelines

🤖 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/OnboardingActions/HostMonitorPack/index.tsx`:
- Around line 306-310: Update the board-name rendering in the selectedBoards
display to use a locale-appropriate separator instead of the hardcoded Chinese
“、”. Reuse the component’s existing translation or locale mechanism so en_US,
ja_JP, and ru_RU display names naturally, while preserving the current
empty-list handling and ordering.
- Around line 161-176: Update the rule processing around selectedRules and
ruleBodies so buildAlertRuleImportBody failures are preserved as failed
ImportItemResult entries using the existing bad-template translation, rather
than returning null and being removed by _.compact. Keep successfully parsed
rules in the import request, then merge parse-failure results with the import
responses so results.rules contains one result for every selected rule.

In `@src/components/OnboardingActions/HostMonitorPack/transform.ts`:
- Line 30: Replace the Lodash array checks in buildBoardImportBody and
buildAlertRuleImportBody with the native Array.isArray(parsed) check, preserving
the existing parsed[0] versus parsed selection behavior.

In `@src/components/OnboardingActions/NextStepsCard/index.tsx`:
- Around line 109-141: Replace the clickable anchors in the pending-row map and
dismiss control with native button elements using type="button". Preserve their
existing onClick behavior, labels, styling, and Tooltip wrappers, following the
button pattern used by OnboardingTracks.tsx.

In `@src/components/OnboardingActions/SendTestAlert/index.tsx`:
- Around line 83-115: Update handleSend to ignore stale async results by
tracking component mount state and the rule/request identity captured when
sending begins. Before setResults, setSending, and related onboarding updates,
verify the component remains mounted and the captured rule is still current;
ensure rule selection cannot change while sending is active, following the
existing cancelled guard pattern in the component effect.

In `@src/components/OnboardingProgress/detect.ts`:
- Around line 46-52: Update isHostBoard to match HOST_PACK_TAG as a complete tag
token rather than using substring matching on board.tags. Since appendPackTag
joins tags with spaces, split or otherwise test the space-delimited tags for
exact equality, while preserving the existing name-hint fallback behavior.

In `@src/components/OnboardingProgress/useOnboardingProgress.ts`:
- Around line 192-223: Update refreshOnboardingProgress and the shared probe
flow so a refresh arriving while pendingProbe is active schedules a follow-up
probe after the current one completes, rather than reusing only the in-flight
result. Ensure the follow-up runs with the latest lastDetect state and publishes
its result, while preserving the direct local-marker path and existing request
deduplication for concurrent initial probes.

In `@src/pages/hosts/pages/List/List.tsx`:
- Around line 384-397: Add an explicit aria-label to both install and collect
action Buttons in the List component, using their existing translated entry
labels so the controls remain accessible when ACTION_LABEL_CLASS hides the
visible text. Keep the current Tooltip and click behavior unchanged.
- Line 235: Update the history replacement in the host list onboarding flow to
remove only the consumed onboarding query parameter while preserving all other
search parameters. Use the existing location search and query-string handling
around history.replace rather than clearing search entirely.

---

Nitpick comments:
In `@src/components/OnboardingActions/SendTestAlert/index.tsx`:
- Around line 98-101: In the notifyRuleTest success handler within the
SendResult mapping, replace the lodash _.isString(res?.dat) check with the
native typeof res?.dat === 'string' type guard, preserving the existing detail
fallback and error handling.

In `@src/components/OnboardingProgress/detect.test.ts`:
- Around line 80-84: Update the storage-unavailable test around
writeOnboardingMarker and readOnboardingMarker to replace localStorage with a
stub whose access methods throw, ensuring both operations enter the catch path
directly. Preserve the existing no-throw and false-result assertions, and
restore the original storage state through the test cleanup.

In `@src/pages/hosts/pages/List/CollectSetup/index.tsx`:
- Around line 160-164: Replace the useRef(readVerifiedDatasourceIds())
initialization with a lazy useState initializer so readVerifiedDatasourceIds
executes only once, while preserving the rememberedDatasourceIds value used by
defaultFromRemembered.
- Around line 196-207: Update the useEffect handling arrival verification to
explicitly document its intentional single-run behavior, following the existing
pattern around the later effect in the same file. Either add the referenced
values to the dependency array while preserving the verifiedMarkedRef guard, or
add the established lint exemption for intentional omitted dependencies; ensure
arrivalDatasources, arrival.hitDatasourceNames, and datasourceIds are covered by
the chosen approach.
🪄 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 Plus

Run ID: ad36069f-5e74-4aab-adab-5e912b6c2828

📥 Commits

Reviewing files that changed from the base of the PR and between 7b2a152 and 808d3b6.

📒 Files selected for processing (52)
  • src/App.tsx
  • src/components/OnboardingActions/HostMonitorPack/index.tsx
  • src/components/OnboardingActions/HostMonitorPack/resolvePack.test.ts
  • src/components/OnboardingActions/HostMonitorPack/resolvePack.ts
  • src/components/OnboardingActions/HostMonitorPack/services.ts
  • src/components/OnboardingActions/HostMonitorPack/transform.test.ts
  • src/components/OnboardingActions/HostMonitorPack/transform.ts
  • src/components/OnboardingActions/NextStepsCard/index.tsx
  • src/components/OnboardingActions/NextStepsCard/visibility.test.ts
  • src/components/OnboardingActions/NextStepsCard/visibility.ts
  • src/components/OnboardingActions/SendTestAlert/index.tsx
  • src/components/OnboardingActions/constants.ts
  • src/components/OnboardingActions/index.tsx
  • src/components/OnboardingActions/locale/en_US.ts
  • src/components/OnboardingActions/locale/index.ts
  • src/components/OnboardingActions/locale/ja_JP.ts
  • src/components/OnboardingActions/locale/ru_RU.ts
  • src/components/OnboardingActions/locale/zh_CN.ts
  • src/components/OnboardingActions/locale/zh_HK.ts
  • src/components/OnboardingActions/types.ts
  • src/components/OnboardingActions/useOnboardingStepClick.ts
  • src/components/OnboardingProgress/OnboardingTracks.tsx
  • src/components/OnboardingProgress/PopoverContent.tsx
  • src/components/OnboardingProgress/detect.test.ts
  • src/components/OnboardingProgress/detect.ts
  • src/components/OnboardingProgress/index.tsx
  • src/components/OnboardingProgress/tracks.ts
  • src/components/OnboardingProgress/useOnboardingProgress.ts
  • src/pages/event/EventNotifyRecords/services.ts
  • src/pages/hosts/constants.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/CollectSetup/index.tsx
  • src/pages/hosts/pages/List/InstallCategraf/index.tsx
  • src/pages/hosts/pages/List/List.tsx
  • src/pages/landing/OnboardingChecklist.tsx
  • src/pages/landing/locale/en_US.ts
  • src/pages/landing/locale/ja_JP.ts
  • src/pages/landing/locale/ru_RU.ts
  • src/pages/landing/locale/zh_CN.ts
  • src/pages/landing/locale/zh_HK.ts
  • src/pages/notificationRules/services.ts
  • src/pages/warning/shield/components/operateForm.tsx
  • src/pages/warning/shield/locale/en_US.ts
  • src/pages/warning/shield/locale/ja_JP.ts
  • src/pages/warning/shield/locale/ru_RU.ts
  • src/pages/warning/shield/locale/zh_CN.ts
  • src/pages/warning/shield/locale/zh_HK.ts
  • src/utils/request.tsx

Comment thread src/components/OnboardingActions/HostMonitorPack/index.tsx
Comment on lines +306 to +310
<a onClick={() => setPreviewOpen(!previewOpen)}>{previewOpen ? t('common:btn.collapse') : t('pack.preview')}</a>
</div>
{/* 名字由后端按语言翻译过,直接展示实际选中的,不写死在文案里 */}
{!previewOpen && !_.isEmpty(selectedBoards) && <div className='text-soft'>{_.join(_.map(selectedBoards, 'name'), '、')}</div>}
{previewOpen && renderPreviewList(resolved?.boards ?? [], boardIds, setBoardIds)}

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 | 🟡 Minor | ⚡ Quick win

Board name list uses a hardcoded Chinese separator.

Line 309 joins board names with '、', the Chinese enumeration comma, regardless of the active locale. This component ships en_US, ja_JP, and ru_RU translations, so this list looks out of place for non-Chinese users.

🌐 Proposed fix
-  const { t } = useTranslation(NS);
+  const { t, i18n } = useTranslation(NS);
-              {!previewOpen && !_.isEmpty(selectedBoards) && <div className='text-soft'>{_.join(_.map(selectedBoards, 'name'), '、')}</div>}
+              {!previewOpen && !_.isEmpty(selectedBoards) && (
+                <div className='text-soft'>{new Intl.ListFormat(i18n.language).format(_.map(selectedBoards, 'name'))}</div>
+              )}
🤖 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/OnboardingActions/HostMonitorPack/index.tsx` around lines 306
- 310, Update the board-name rendering in the selectedBoards display to use a
locale-appropriate separator instead of the hardcoded Chinese “、”. Reuse the
component’s existing translation or locale mechanism so en_US, ja_JP, and ru_RU
display names naturally, while preserving the current empty-list handling and
ordering.

*/
export function buildBoardImportBody(content: string): BoardImportBody {
const parsed = JSON.parse(content);
const board = _.isArray(parsed) ? parsed[0] : parsed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use native Array.isArray instead of _.isArray.

buildBoardImportBody (line 30) and buildAlertRuleImportBody (line 57) both call _.isArray(parsed) to narrow the parsed payload. Use the native check instead.

As per coding guidelines, "prefer native checks such as Array.isArray, typeof x === 'number', and x == null over equivalent Lodash predicates such as _.isArray, _.isNumber, and _.isNil."

♻️ Proposed fix
-  const board = _.isArray(parsed) ? parsed[0] : parsed;
+  const board = Array.isArray(parsed) ? parsed[0] : parsed;
-  const rule = _.isArray(parsed) ? parsed[0] : parsed;
+  const rule = Array.isArray(parsed) ? parsed[0] : parsed;

Also applies to: 57-57

🤖 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/OnboardingActions/HostMonitorPack/transform.ts` at line 30,
Replace the Lodash array checks in buildBoardImportBody and
buildAlertRuleImportBody with the native Array.isArray(parsed) check, preserving
the existing parsed[0] versus parsed selection behavior.

Source: Coding guidelines

Comment on lines +109 to +141
return (
<div className='mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 border-0 border-b border-dashed border-[var(--fc-border-color)] pb-3'>
<span className='font-bold'>{t('card.title')}</span>
<span className='text-soft'>{`${doneCount}/${required.length}`}</span>
{_.map(pending, (row) => (
// 描述进 Tooltip:撑住原先「一项一行」的正是这段文案,摘掉它几项才能并排
<Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
<a onClick={() => runAction(row.onClick)}>
{t(`card.rows.${row.key}.title`)}
{row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
</a>
</Tooltip>
))}
{primary && (
// ml-auto 把主按钮和关闭推到最右:中间的步骤链接再多也不会把它们挤成不对齐的一坨
<Button size='small' type='primary' className='ml-auto' onClick={() => runAction(primary.onClick)}>
{t(`card.rows.${primary.key}.action`)}
</Button>
)}
<Tooltip title={t('card.dismiss')}>
<a
className='text-soft'
onClick={() => {
setDismissed(true);
persistDismissed();
}}
>
<CloseOutlined />
</a>
</Tooltip>
</div>
);
}

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

Make the inline row link and dismiss control keyboard-operable.

Line 116 renders <a onClick={() => runAction(row.onClick)}> without an href. Line 129 renders the dismiss control the same way. An anchor without href is not part of the keyboard tab order and is not exposed as a link to assistive technology. A keyboard user cannot reach or activate either control.

Use <button type='button'> for both controls, matching the pattern already used in OnboardingTracks.tsx for step buttons.

♿ Proposed fix using native buttons
-          <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
-            <a onClick={() => runAction(row.onClick)}>
-              {t(`card.rows.${row.key}.title`)}
-              {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
-            </a>
-          </Tooltip>
+          <Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
+            <button type='button' className='border-0 bg-transparent p-0 text-inherit hover:underline' onClick={() => runAction(row.onClick)}>
+              {t(`card.rows.${row.key}.title`)}
+              {row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
+            </button>
+          </Tooltip>
         <Tooltip title={t('card.dismiss')}>
-          <a
+          <button
+            type='button'
             className='text-soft'
             onClick={() => {
               setDismissed(true);
               persistDismissed();
             }}
           >
             <CloseOutlined />
-          </a>
+          </button>
         </Tooltip>
📝 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
return (
<div className='mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 border-0 border-b border-dashed border-[var(--fc-border-color)] pb-3'>
<span className='font-bold'>{t('card.title')}</span>
<span className='text-soft'>{`${doneCount}/${required.length}`}</span>
{_.map(pending, (row) => (
// 描述进 Tooltip:撑住原先「一项一行」的正是这段文案,摘掉它几项才能并排
<Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
<a onClick={() => runAction(row.onClick)}>
{t(`card.rows.${row.key}.title`)}
{row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
</a>
</Tooltip>
))}
{primary && (
// ml-auto 把主按钮和关闭推到最右:中间的步骤链接再多也不会把它们挤成不对齐的一坨
<Button size='small' type='primary' className='ml-auto' onClick={() => runAction(primary.onClick)}>
{t(`card.rows.${primary.key}.action`)}
</Button>
)}
<Tooltip title={t('card.dismiss')}>
<a
className='text-soft'
onClick={() => {
setDismissed(true);
persistDismissed();
}}
>
<CloseOutlined />
</a>
</Tooltip>
</div>
);
}
return (
<div className='mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 border-0 border-b border-dashed border-[var(--fc-border-color)] pb-3'>
<span className='font-bold'>{t('card.title')}</span>
<span className='text-soft'>{`${doneCount}/${required.length}`}</span>
{_.map(pending, (row) => (
// 描述进 Tooltip:撑住原先「一项一行」的正是这段文案,摘掉它几项才能并排
<Tooltip key={row.key} title={t(`card.rows.${row.key}.desc`)}>
<button type='button' className='border-0 bg-transparent p-0 text-inherit hover:underline' onClick={() => runAction(row.onClick)}>
{t(`card.rows.${row.key}.title`)}
{row.optional && <span className='ml-1 text-[10px] text-soft'>{t('card.optional')}</span>}
</button>
</Tooltip>
))}
{primary && (
// ml-auto 把主按钮和关闭推到最右:中间的步骤链接再多也不会把它们挤成不对齐的一坨
<Button size='small' type='primary' className='ml-auto' onClick={() => runAction(primary.onClick)}>
{t(`card.rows.${primary.key}.action`)}
</Button>
)}
<Tooltip title={t('card.dismiss')}>
<button
type='button'
className='text-soft'
onClick={() => {
setDismissed(true);
persistDismissed();
}}
>
<CloseOutlined />
</button>
</Tooltip>
</div>
);
}
🤖 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/OnboardingActions/NextStepsCard/index.tsx` around lines 109 -
141, Replace the clickable anchors in the pending-row map and dismiss control
with native button elements using type="button". Preserve their existing onClick
behavior, labels, styling, and Tooltip wrappers, following the button pattern
used by OnboardingTracks.tsx.

Comment on lines +83 to +115
const handleSend = () => {
if (_.isEmpty(configs)) return;
setSending(true);
setResults(undefined);

// 逐个 notify_config 发一次,这样用户能看到「钉钉成功、邮件失败」而不是一个笼统的结果。
// time_ranges 直接用接口返回的原始值:列表接口给的就是后端要的 'HH:mm' 字符串,
// 只有表单态才会被 normalizeInitialValues 转成 moment。
Promise.all(
_.map(configs, (config, index) => {
const label = getConfigLabel(config, index);
if (!config?.channel_id || config.channel_id <= 0) {
// 后端对 channel_id <= 0 直接 400,先在前端说清楚是"没选通知媒介"
return Promise.resolve<SendResult>({ index, label, ok: false, detail: t('test.no_channel') });
}
return notifyRuleTest({ use_mock_event: true, notify_config: config }, { silence: true }).then(
(res) => ({ index, label, ok: true, detail: _.isString(res?.dat) ? res.dat : undefined } as SendResult),
(err) => ({ index, label, ok: false, detail: err?.message || t('test.unknown_error') } as SendResult),
);
}),
)
.then((list) => {
setResults(list);
if (_.some(list, { ok: true })) {
// 本地标记让这一步立刻点亮;换浏览器/其他用户由服务端送达探测兜住
writeOnboardingMarker('testDelivered');
refreshOnboardingProgress(['testDelivered']);
}
})
.finally(() => {
setSending(false);
});
};

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

Guard handleSend against updates after unmount or a rule change.

handleSend does not track mount state. If the user closes the modal (triggers onCancel, unmounting this component) while Promise.all is still pending, setResults and setSending in .then/.finally run after unmount. The effect at lines 60-78 in this same file already guards setRules/setRuleId/setLoading with a cancelled flag for this exact failure mode; handleSend needs the same guard.

The rule Select is also not disabled while sending is true. If the user switches ruleId before the pending send resolves, the stale result overwrites state for a rule that is no longer selected.

🔒 Proposed fix using a mount ref and a request-identity check
+  const mountedRef = React.useRef(true);
+  React.useEffect(
+    () => () => {
+      mountedRef.current = false;
+    },
+    [],
+  );
+
   const handleSend = () => {
     if (_.isEmpty(configs)) return;
     setSending(true);
     setResults(undefined);
+    const requestedRuleId = ruleId;
 
     Promise.all(
       _.map(configs, (config, index) => {
         ...
       }),
     )
       .then((list) => {
+        if (!mountedRef.current || requestedRuleId !== ruleId) return;
         setResults(list);
         if (_.some(list, { ok: true })) {
           writeOnboardingMarker('testDelivered');
           refreshOnboardingProgress(['testDelivered']);
         }
       })
       .finally(() => {
-        setSending(false);
+        if (mountedRef.current) setSending(false);
       });
   };
🤖 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/OnboardingActions/SendTestAlert/index.tsx` around lines 83 -
115, Update handleSend to ignore stale async results by tracking component mount
state and the rule/request identity captured when sending begins. Before
setResults, setSending, and related onboarding updates, verify the component
remains mounted and the captured rule is still current; ensure rule selection
cannot change while sending is active, following the existing cancelled guard
pattern in the component effect.

Comment on lines +46 to +52
export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
if (!board) return false;
if (board.tags && board.tags.includes(HOST_PACK_TAG)) return true;
if (!board.name) return false;
const lower = board.name.toLowerCase();
return HOST_DASHBOARD_NAME_HINTS.some((hint) => lower.includes(hint));
}

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 | 🟡 Minor | ⚡ Quick win

Match the pack tag as a whole token.

board.tags.includes(HOST_PACK_TAG) is a substring test. A user tag such as n9e-host-pack-old then counts as a pack board. appendPackTag in src/components/OnboardingActions/HostMonitorPack/transform.ts joins tags with spaces, so an exact token test is available and equally cheap.

🔍 Proposed fix
-export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
-  if (!board) return false;
-  if (board.tags && board.tags.includes(HOST_PACK_TAG)) return true;
+export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
+  if (!board) return false;
+  // tags 是空白分隔的自由字符串,按 token 比对,避免 n9e-host-pack-xxx 这类前缀误命中
+  if (board.tags && board.tags.split(/\s+/).includes(HOST_PACK_TAG)) return true;
   if (!board.name) return 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
export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
if (!board) return false;
if (board.tags && board.tags.includes(HOST_PACK_TAG)) return true;
if (!board.name) return false;
const lower = board.name.toLowerCase();
return HOST_DASHBOARD_NAME_HINTS.some((hint) => lower.includes(hint));
}
export function isHostBoard(board?: { name?: string; tags?: string } | null): boolean {
if (!board) return false;
// tags 是空白分隔的自由字符串,按 token 比对,避免 n9e-host-pack-xxx 这类前缀误命中
if (board.tags && board.tags.split(/\s+/).includes(HOST_PACK_TAG)) return true;
if (!board.name) return false;
const lower = board.name.toLowerCase();
return HOST_DASHBOARD_NAME_HINTS.some((hint) => lower.includes(hint));
}
🤖 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/OnboardingProgress/detect.ts` around lines 46 - 52, Update
isHostBoard to match HOST_PACK_TAG as a complete tag token rather than using
substring matching on board.tags. Since appendPackTag joins tags with spaces,
split or otherwise test the space-delimited tags for exact equality, while
preserving the existing name-hint fallback behavior.

Comment thread src/components/OnboardingProgress/useOnboardingProgress.ts
} else if (installMeta.collect) {
setCollectVisible(true);
}
history.replace({ pathname: location.pathname, search: '' });

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 | 🟡 Minor | ⚡ Quick win

Remove only the onboarding parameter instead of the whole query string.

history.replace({ pathname: location.pathname, search: '' }) discards every other query parameter on /targets. Any co-existing parameter, for example a deep link that also carries a filter, is lost after the wizard opens. Delete the single consumed key.

🔧 Proposed fix
-    const onboarding = new URLSearchParams(location.search).get('onboarding');
+    const searchParams = new URLSearchParams(location.search);
+    const onboarding = searchParams.get('onboarding');
     if (onboarding !== 'install' && onboarding !== 'collect') return;
@@
-    history.replace({ pathname: location.pathname, search: '' });
+    searchParams.delete('onboarding');
+    const nextSearch = searchParams.toString();
+    history.replace({ pathname: location.pathname, search: nextSearch ? `?${nextSearch}` : '' });
📝 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
history.replace({ pathname: location.pathname, search: '' });
searchParams.delete('onboarding');
const nextSearch = searchParams.toString();
history.replace({ pathname: location.pathname, search: nextSearch ? `?${nextSearch}` : '' });
🤖 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/List.tsx` at line 235, Update the history
replacement in the host list onboarding flow to remove only the consumed
onboarding query parameter while preserving all other search parameters. Use the
existing location search and query-string handling around history.replace rather
than clearing search entirely.

Comment on lines +384 to +397
{!aiTaskMode && installMeta && (
<Tooltip title={t('install.entry')}>
<Button type='primary' ghost icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span>
</Button>
</Tooltip>
)}
{!aiTaskMode && installMeta?.collect && (
<Tooltip title={t('collect.entry')}>
<Button type='primary' ghost icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span>
</Button>
</Tooltip>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Give the collapsed action buttons an accessible name.

ACTION_LABEL_CLASS is hidden 2xl:inline-block. Below the 2xl breakpoint the label leaves the accessibility tree, so each button exposes only an icon. The antd Tooltip sets aria-describedby only while the tooltip is open, so screen readers announce an unnamed button. Add aria-label to both buttons.

♿ Proposed fix
             {!aiTaskMode && installMeta && (
               <Tooltip title={t('install.entry')}>
-                <Button type='primary' ghost icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
+                <Button type='primary' ghost aria-label={t('install.entry')} icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
                   <span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span>
                 </Button>
               </Tooltip>
             )}
             {!aiTaskMode && installMeta?.collect && (
               <Tooltip title={t('collect.entry')}>
-                <Button type='primary' ghost icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
+                <Button type='primary' ghost aria-label={t('collect.entry')} icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
                   <span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span>
                 </Button>
               </Tooltip>
             )}
📝 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
{!aiTaskMode && installMeta && (
<Tooltip title={t('install.entry')}>
<Button type='primary' ghost icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span>
</Button>
</Tooltip>
)}
{!aiTaskMode && installMeta?.collect && (
<Tooltip title={t('collect.entry')}>
<Button type='primary' ghost icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span>
</Button>
</Tooltip>
)}
{!aiTaskMode && installMeta && (
<Tooltip title={t('install.entry')}>
<Button type='primary' ghost aria-label={t('install.entry')} icon={<DownloadOutlined />} onClick={() => setInstallVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('install.entry')}</span>
</Button>
</Tooltip>
)}
{!aiTaskMode && installMeta?.collect && (
<Tooltip title={t('collect.entry')}>
<Button type='primary' ghost aria-label={t('collect.entry')} icon={<AppstoreAddOutlined />} onClick={() => setCollectVisible(true)}>
<span className={ACTION_LABEL_CLASS}>{t('collect.entry')}</span>
</Button>
</Tooltip>
)}
🤖 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/List.tsx` around lines 384 - 397, Add an explicit
aria-label to both install and collect action Buttons in the List component,
using their existing translated entry labels so the controls remain accessible
when ACTION_LABEL_CLASS hides the visible text. Keep the current Tooltip and
click behavior unchanged.

- import alert rules without force: pre-check same-name rules in the
  target busi group and skip them (same policy as dashboards) instead of
  letting the backend upsert silently overwrite user-tuned rules, or
  duplicate them after a language switch changes template names
- gate the delivered probe on this round's notification result instead
  of the previous round's cache, so the first probe of a session can
  light up "send a test alert" for deployments that already delivered
- drop the one-shot ref guarding the ?onboarding deep link: clearing the
  query already prevents re-triggering, and the ref swallowed a second
  click on the same onboarding step within one mount
- track bgid + loading for the existing-name pre-check and keep submit
  disabled until the pre-check matches the selected busi group

@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.

♻️ Duplicate comments (1)
src/components/OnboardingActions/HostMonitorPack/index.tsx (1)

178-199: 🎯 Functional Correctness | 🟠 Major

Rule parse failures still disappear from results (regression).

buildAlertRuleImportBody failures return null in the catch block. _.compact drops that entry from ruleBodies before it reaches existedRules, rulesToImport, or skippedResults. The failing rule never appears in results.rules, unlike the board path, which keeps a 'failed' entry with t('pack.bad_template') (Line 170).

This is the same defect flagged in a prior review round and marked addressed. The current refactor, which adds the bodyName partition logic, dropped the failure-tracking fix. Reintroduce it: collect parse failures separately and merge them back into the final ruleTask result.

🐛 Proposed fix
+      const ruleParseFailures: ImportItemResult[] = [];
       const ruleBodies = _.compact(
         _.map(selectedRules, (payload) => {
           try {
             const body = buildAlertRuleImportBody(payload.content, { datasourceQueries: values.datasource_queries, notifyRuleIds });
             // bodyName 才是后端落库与响应 map 的 key(payload.name 只是列表展示名,两者通常一致)
             return { name: payload.name, bodyName: body.name ?? payload.name, body };
           } catch (e) {
-            return null;
+            ruleParseFailures.push({ name: payload.name, status: 'failed', detail: t('pack.bad_template') });
+            return null;
           }
         }),
       );
       // 与大盘同一口径:重名跳过、不覆盖。不能走后端 force 导入 —— 那是按 (group_id, name)
       // 的整行覆盖,会把用户改过阈值 / 主动停用 / 换过通知绑定的同名规则静默还原(见 services.ts)
       const [existedRules, rulesToImport] = _.partition(ruleBodies, (item) => existing.rules[item.bodyName]);
       const skippedResults = _.map(existedRules, (item) => ({ name: item.name, status: 'skipped' } as ImportItemResult));
       const ruleTask: Promise<ImportItemResult[]> = _.isEmpty(rulesToImport)
-        ? Promise.resolve(skippedResults)
+        ? Promise.resolve<ImportItemResult[]>(skippedResults)
         : importAlertRules(targetBgid, _.map(rulesToImport, 'body')).then(
             (res) => [..._.map(rulesToImport, (item) => ({ name: item.name, status: res?.[item.bodyName] ? 'failed' : 'ok', detail: res?.[item.bodyName] } as ImportItemResult)), ...skippedResults],
             (err) => [..._.map(rulesToImport, (item) => ({ name: item.name, status: 'failed', detail: err?.message || t('pack.unknown_error') } as ImportItemResult)), ...skippedResults],
-          );
+          );
+      const ruleTaskWithParseFailures = ruleTask.then((items) => [...ruleParseFailures, ...items]);

Replace the later ruleTask reference in Promise.all([Promise.all(boardTasks), ruleTask]) with ruleTaskWithParseFailures.

🤖 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/OnboardingActions/HostMonitorPack/index.tsx` around lines 178
- 199, Update the rule-processing flow around buildAlertRuleImportBody to
preserve parse failures as failed results instead of dropping them via
_.compact. Collect failed payloads with the existing bad-template result shape
and merge them into the final rule task, then use ruleTaskWithParseFailures in
the Promise.all result assembly so each failed rule appears in results.rules
alongside imported and skipped rules.
🤖 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.

Duplicate comments:
In `@src/components/OnboardingActions/HostMonitorPack/index.tsx`:
- Around line 178-199: Update the rule-processing flow around
buildAlertRuleImportBody to preserve parse failures as failed results instead of
dropping them via _.compact. Collect failed payloads with the existing
bad-template result shape and merge them into the final rule task, then use
ruleTaskWithParseFailures in the Promise.all result assembly so each failed rule
appears in results.rules alongside imported and skipped rules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02f79649-465b-42e4-adc2-decb0c4fb31c

📥 Commits

Reviewing files that changed from the base of the PR and between 808d3b6 and e593e94.

📒 Files selected for processing (10)
  • src/components/OnboardingActions/HostMonitorPack/index.tsx
  • src/components/OnboardingActions/HostMonitorPack/services.ts
  • src/components/OnboardingActions/HostMonitorPack/transform.ts
  • src/components/OnboardingActions/locale/en_US.ts
  • src/components/OnboardingActions/locale/ja_JP.ts
  • src/components/OnboardingActions/locale/ru_RU.ts
  • src/components/OnboardingActions/locale/zh_CN.ts
  • src/components/OnboardingActions/locale/zh_HK.ts
  • src/components/OnboardingProgress/useOnboardingProgress.ts
  • src/pages/hosts/pages/List/List.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/components/OnboardingActions/locale/zh_HK.ts
  • src/components/OnboardingActions/locale/ja_JP.ts
  • src/components/OnboardingActions/locale/en_US.ts
  • src/components/OnboardingActions/HostMonitorPack/transform.ts
  • src/components/OnboardingActions/locale/ru_RU.ts
  • src/components/OnboardingProgress/useOnboardingProgress.ts

710leo added 2 commits July 31, 2026 21:23
Backend relaxed the check from delivered-only to any notification record
(and renamed the endpoint), so the delivered-only query no longer exists.
Rename getNotifyDelivered -> getNotifyUsed, detect field delivered ->
notifyUsed. Local test-alert markers (testDelivered*) keep their names:
they track the send-test-alert UX, not the server probe.
- the "bind notifications" step no longer lights up on rule existence
  alone: once enabled host-cate alert rules exist, at least one of them
  must be bound to a notification rule (the pack allows importing with
  notify_rule_ids empty, so all four steps could turn green while real
  alerts reached nobody); legacy-notify rules (notify_version != 1)
  count as bound since their notify config is embedded in the rule.
  detection reuses the same alert-rules request as hostAlert
- after quick-creating a notification rule from the guided flow, show a
  pointer to batch-bind it when enabled host rules are still unbound --
  the new rule is not attached to imported rules automatically, and
  without the hint the step would stay unlit with no explanation
- refresh onboarding progress as soon as InstallCategraf detects the
  first reporting machine: the machine flag was only re-probed on route
  change, so the inline next-steps strip on the host list would not
  appear until the user navigated away and back
710leo added 2 commits August 1, 2026 00:58
… errors

- the backend treats any HTTP 200 as a successful send without parsing
  the body (http_common.go), while dingtalk/wecom bots report invalid
  tokens and unmatched security keywords exactly as 200 + non-zero
  errcode; such sends showed "Delivered" and stamped the testDelivered
  onboarding marker
- add a conservative response interpreter that only recognizes the
  de-facto errcode convention (0 = ok): fields like `code` vary by
  channel (custom webhooks may use code:200 for success), so anything
  unrecognized still counts as sent instead of guessing
- success rows now say "request sent" instead of "delivered", render
  the raw provider response alongside (errcode/errmsg is the user's
  only troubleshooting clue), and the hint stresses that only actual
  receipt proves the notification path works
- OnboardingActionModals was mounted unconditionally in the App-level
  catch-all, so its progress probe (five authenticated requests) also
  fired on anonymous routes and in plus builds; on shared dashboard /
  chart pages the 401 handler then kicked anonymous visitors to the
  login page, breaking sharing. The mount gate now lives inside the
  component: it renders nothing on anonymous paths (checked via
  useLocation so SPA navigations re-evaluate -- the module-level
  `anonymous` const in App.tsx does not) and when the layer is
  disabled (plus edition)
- the host list no longer holds its own probe hook: the "has machines"
  gate moved into NextStepsCard's inline variant, and the strip is only
  mounted for !aiTaskMode && !IS_PLUS, since returning null cannot stop
  an already-mounted hook from probing
- actions are now permission-gated in line with backend rt.perm
  (pack = /dashboards/add + /alert-rules/add, notify =
  /notification-rules/add, test = /notification-rules): openAction
  refuses unpermitted actions, checklist steps fall back to their `to`
  navigation, NextStepsCard hides unpermitted rows, and the pack modal
  hides its quick-create / send-test entries -- previously a read-only
  user got a modal that could only 403 on submit

@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: 2

🧹 Nitpick comments (2)
src/components/OnboardingActions/SendTestAlert/interpretResponse.test.ts (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the provider-response fixtures with as const.

Apply as const to these literal fixtures. This preserves their narrow literal types.

Proposed change
-    const dat = 'status_code:200, response:{"errcode":310000,"errmsg":"keywords not in content"}';
+    const dat = 'status_code:200, response:{"errcode":310000,"errmsg":"keywords not in content"}' as const;

As per coding guidelines, “Prefer as const for test data literals to preserve narrow literal types.”

Also applies to: 17-17, 22-22, 28-28

🤖 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/OnboardingActions/SendTestAlert/interpretResponse.test.ts` at
line 12, Update the provider-response fixture literals in
interpretResponse.test.ts, including dat and the additional fixtures at the
referenced locations, by applying as const so their narrow literal types are
preserved. Do not change the fixture values or test behavior.

Source: Coding guidelines

src/components/OnboardingActions/SendTestAlert/interpretResponse.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use native checks for type narrowing.

Replace _.isString(dat) with typeof dat !== 'string'. Replace _.isNumber(errcode) with typeof errcode === 'number'. Then remove the Lodash import.

Proposed change
-import _ from 'lodash';
-
 export interface TestSendOutcome {
@@
-  if (!_.isString(dat) || dat === '' || dat === 'success') {
+  if (typeof dat !== 'string' || dat === '' || dat === 'success') {
@@
-    if (_.isNumber(errcode) && errcode !== 0) {
+    if (typeof errcode === 'number' && errcode !== 0) {

As per coding guidelines, “For type narrowing in JavaScript and TypeScript code, prefer native checks … over equivalent Lodash predicates.”

Also applies to: 23-31

🤖 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/OnboardingActions/SendTestAlert/interpretResponse.ts` at line
1, In interpretResponse, replace the Lodash predicates used for dat and errcode
type narrowing with native typeof checks, using typeof dat !== 'string' and
typeof errcode === 'number' semantics. Remove the now-unused Lodash import while
preserving the surrounding response handling.

Source: Coding guidelines

🤖 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/OnboardingActions/NextStepsCard/index.tsx`:
- Around line 77-83: Update the onboarding progress state used by the
notification row in NextStepsCard so it distinguishes “no host alerts” from a
failed or unknown host-alert probe. Expose and consume a separate
successful-probe or known-state flag from useOnboardingProgress, and require
that flag before allowing doneMap.notification to mark the row complete;
preserve the existing hostNotifyBound requirement when host alerts are present.

In `@src/pages/hosts/pages/List/InstallCategraf/index.tsx`:
- Around line 40-47: Update refreshOnboardingProgress and its
probeOnboardingShared flow so a machine detection refresh waits for any existing
in-flight probe to complete, then queues a follow-up probe instead of reusing
only the current promise. Preserve the one-time trigger guarded by
machineRefreshedRef and ensure the post-install probe publishes the latest
machine state.

---

Nitpick comments:
In `@src/components/OnboardingActions/SendTestAlert/interpretResponse.test.ts`:
- Line 12: Update the provider-response fixture literals in
interpretResponse.test.ts, including dat and the additional fixtures at the
referenced locations, by applying as const so their narrow literal types are
preserved. Do not change the fixture values or test behavior.

In `@src/components/OnboardingActions/SendTestAlert/interpretResponse.ts`:
- Line 1: In interpretResponse, replace the Lodash predicates used for dat and
errcode type narrowing with native typeof checks, using typeof dat !== 'string'
and typeof errcode === 'number' semantics. Remove the now-unused Lodash import
while preserving the surrounding response handling.
🪄 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 Plus

Run ID: 05ecfb53-3ab5-494d-b2af-62b697c45a70

📥 Commits

Reviewing files that changed from the base of the PR and between 611426c and 49e4aac.

📒 Files selected for processing (19)
  • src/App.tsx
  • src/components/OnboardingActions/HostMonitorPack/index.tsx
  • src/components/OnboardingActions/NextStepsCard/index.tsx
  • src/components/OnboardingActions/SendTestAlert/index.tsx
  • src/components/OnboardingActions/SendTestAlert/interpretResponse.test.ts
  • src/components/OnboardingActions/SendTestAlert/interpretResponse.ts
  • src/components/OnboardingActions/constants.ts
  • src/components/OnboardingActions/index.tsx
  • src/components/OnboardingActions/locale/en_US.ts
  • src/components/OnboardingActions/locale/ja_JP.ts
  • src/components/OnboardingActions/locale/ru_RU.ts
  • src/components/OnboardingActions/locale/zh_CN.ts
  • src/components/OnboardingActions/locale/zh_HK.ts
  • src/components/OnboardingActions/useOnboardingStepClick.ts
  • src/components/OnboardingProgress/detect.test.ts
  • src/components/OnboardingProgress/detect.ts
  • src/components/OnboardingProgress/useOnboardingProgress.ts
  • src/pages/hosts/pages/List/InstallCategraf/index.tsx
  • src/pages/hosts/pages/List/List.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/components/OnboardingActions/locale/zh_HK.ts
  • src/components/OnboardingActions/useOnboardingStepClick.ts
  • src/pages/hosts/pages/List/List.tsx
  • src/components/OnboardingActions/locale/en_US.ts
  • src/components/OnboardingProgress/useOnboardingProgress.ts
  • src/components/OnboardingActions/locale/zh_CN.ts
  • src/components/OnboardingActions/locale/ja_JP.ts
  • src/components/OnboardingActions/locale/ru_RU.ts
  • src/components/OnboardingActions/SendTestAlert/index.tsx
  • src/components/OnboardingActions/HostMonitorPack/index.tsx

Comment on lines +77 to +83
permittedActions.notify && {
key: 'notify' as RowKey,
// 只看「存在通知规则」不够:基础包允许 notify_rule_ids 留空导入,主机告警可能一条都没绑
// 通知,真告警仍无人收到。已有启用中的主机告警时,额外要求至少一条真的绑定了通知;
// 主机告警还没导入时「绑定」无从谈起,维持「有通知规则即完成」的原口径。
done: doneMap.notification && (!doneMap.hostAlert || doneMap.hostNotifyBound),
onClick: () => openAction('notify'),

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 | 🏗️ Heavy lift

Do not mark notification setup complete when host-alert detection fails.

!doneMap.hostAlert treats “no host alerts exist” and “the host-alert request failed” as the same state. The progress hook preserves false on a failed alert probe, so a successful notification-rule probe can mark this row complete and hide the binding action. (raw.githubusercontent.com)

Expose a separate successful-probe or known-state flag. Keep this row incomplete while host-alert state is unknown.

🤖 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/OnboardingActions/NextStepsCard/index.tsx` around lines 77 -
83, Update the onboarding progress state used by the notification row in
NextStepsCard so it distinguishes “no host alerts” from a failed or unknown
host-alert probe. Expose and consume a separate successful-probe or known-state
flag from useOnboardingProgress, and require that flag before allowing
doneMap.notification to mark the row complete; preserve the existing
hostNotifyBound requirement when host alerts are present.

Comment on lines +40 to +47
// 检测到机器上报就立刻刷新引导进度:machine 只随路由变化重探,首台机器装完若不主动刷一次,
// 列表页工具栏的 inline 引导条要等用户切一次页面才会出现。ref 保证整个弹窗生命周期只触发一次
const machineRefreshedRef = useRef(false);
useEffect(() => {
if (!detected || machineRefreshedRef.current) return;
machineRefreshedRef.current = true;
refreshOnboardingProgress(['machine']);
}, [detected]);

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A45 -B8 'pendingProbe|probeOnboardingShared|refreshOnboardingProgress' \
  src/components/OnboardingProgress/useOnboardingProgress.ts

Repository: n9e/fe

Length of output: 6062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/pages/hosts/pages/List/InstallCategraf/index.tsx"

printf '--- outline ---\n'
ast-grep outline "$file" --view condensed || true

printf '\n--- relevant imports and effect/hook area ---\n'
rg -n -A120 -B80 'useOnboardingProgress|refreshOnboardingProgress|machineRefreshedRef|onboarding|detect|detected' "$file"

printf '\n--- all installed categraf onboarding callsites ---\n'
rg -n -A30 -B15 'refreshOnboardingProgress\(\[.machine.\]\)|onboardingProgress|doneMap.machine|useOnboardingProgress' src

Repository: n9e/fe

Length of output: 235


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/pages/hosts/pages/List/InstallCategraf/index.tsx"

printf '%s\n' '--- outline ---'
ast-grep outline "$file" --view condensed || true

printf '%s\n' ''
printf '%s\n' '--- relevant imports and effect/hook area ---'
rg -n -A120 -B80 'useOnboardingProgress|refreshOnboardingProgress|machineRefreshedRef|onboarding|detect|detected' "$file"

printf '%s\n' ''
printf '%s\n' '--- all installed categraf onboarding callsites ---'
rg -n -A30 -B15 'refreshOnboardingProgress\(\[.machine.\]\)|onboardingProgress|doneMap.machine|useOnboardingProgress' src

Repository: n9e/fe

Length of output: 50362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('src/components/OnboardingProgress/useOnboardingProgress.ts')
s = p.read_text()

checks = {
    "lastDetect initializes machine false": "INIT_DETECT = {\nl    machine: false," in s,
    "pendingProbe is module-level null/empty": "let pendingProbe: Promise<DetectState> | null = null;" in s,
    "probeOnboardingShared returns immediately if existing": "function probeOnboardingShared(): Promise<DetectState> {\\n  if (!pendingProbe) {\\n    pendingProbe = probeOnboarding()\\n      .then((next) => {\\n        publish(next);\\n        return next;\\n      })\\n      .finally(() => {\\n        pendingProbe = null;\\n      });\\n  }\\n  return pendingProbe;\\n}" in s,
    "refreshOnboardingProgress uses shared probe when keys include non-marker keys": "  probeOnboardingShared().catch(() => undefined);" in s,
}
for name, ok in checks.items():
    print(f"{name}: {ok}")

start = s.index("function probeOnboarding(): Promise<DetectState> {")
end = s.index("/** 共享同一轮探测:并发挂载只发一组请求,结果广播给所有实例 */")
probe = s[start:end]
print("probeOnboarding reads known.machine via local 'known':", "const known = lastDetect;" in probe and "known.machine" in probe)
PY

Repository: n9e/fe

Length of output: 439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '114,145p' src/components/OnboardingProgress/useOnboardingProgress.ts
sed -n '214,244p' src/components/OnboardingProgress/useOnboardingProgress.ts
sed -n '1,45p' src/components/OnboardingProgress/useOnboardingProgress.ts

Repository: n9e/fe

Length of output: 3717


Schedule a post-install refresh when a probe is already running.

refreshOnboardingProgress(['machine']) only calls probeOnboardingShared(), and probeOnboardingShared() returns the existing in-flight promise instead of starting a new probe. If that earlier probe started with lastDetect.machine === false, the post-install detection can publish machine: false, so the inline onboarding card stays hidden until route refresh. Make refreshOnboardingProgress(['machine']) wait for the current probe, then queue a follow-up probe.

🤖 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/index.tsx` around lines 40 - 47,
Update refreshOnboardingProgress and its probeOnboardingShared flow so a machine
detection refresh waits for any existing in-flight probe to complete, then
queues a follow-up probe instead of reusing only the current promise. Preserve
the one-time trigger guarded by machineRefreshedRef and ensure the post-install
probe publishes the latest machine state.

710leo added 3 commits August 1, 2026 12:27
- 12b2f0d's errcode interpreter only covers the dingtalk/wecom dialect.
  Feishu/Lark report business failures as HTTP 200 + non-zero StatusCode
  (see alert/sender/provider/feishucard_provider_test.go), so those sends
  still rendered a green check; worse, index.tsx then stamped the
  testDelivered marker, which readMarkers ORs into the done state forever
  without ever re-validating
- rather than maintaining a per-channel error-code dialect table that new
  channels will always outrun, drop the guess entirely:
  interpretTestSendResponse becomes formatTestSendResponse, which only
  pretty-prints the provider body so errcode / StatusCode / code are
  readable whatever they happen to be called
- SendResult.ok becomes SendResult.called, narrowed to "the request went
  out"; the green check becomes a neutral SendOutlined and the raw
  response renders in a <pre>. Hard failures (request never sent, no
  channel picked) keep the red icon
- troubleshooting links used to appear only on !ok, which hid them for
  exactly the HTTP-200 business failures that need them most; they now
  sit under hard-failure rows and beside the "only actual receipt proves
  the notification path works" hint
- the localStorage marker keeps its key and keeps being written:
  /notify-rule/test does not write notification_record, so the server
  side used probe can never light this step. Its meaning is now "you ran
  the send action", matching the step name, with no implied delivery claim
- the host list's business group tree offers preset filters that are not
  business groups: -2 (all machines) and 0 (ungrouped), plus the
  dashboard page's -1 (public). Clicking one sets businessGroup.id to
  that value and persists it through the businessGroupKey localStorage
  entry, since getDefaultBusiness deliberately skips the existence check
  for preset values
- defaultBgid piped businessGroup.id through `??`, which only falls
  through on null/undefined, so those values became the import target:
  0 left existing.bgid undefined and the "pre-check done and belongs to
  the current group" submit gate never closed (button permanently
  disabled), while -2/-1 actually posted the dashboards and alert rules
  to /busi-group/-2/..., failing the whole batch. Either way the business
  group select rendered a bare number, since -2/0 match no option
- guard all three sources with the positivity check curBusiId already
  had: preset values are exactly the non-positive ones, and a positive id
  is always a real group (tree nodes are built from busiGroups, and
  getDefaultBusiness already validates and clears stale localStorage
  values), so no membership lookup is needed
探测的跳过条件收紧成 known.alert && known.hostAlert && known.hostNotifyBound
之后,全量告警规则列表从「每次页面加载最多拉一次」退化成「每次路由切换都拉一次」:
hostAlert 要求存在启用中的 cate=host 规则,而内置库里的 host 规则一律以 disabled=1
出厂,这个合取式对多数部署永远闭合不了,doneCount === total 的会话短路同样永远不写入。

已完成项交给缓存、恒为假的项交给时间窗:

- 新增 detectCache.ts:探测结论按 profile.id 隔离落 localStorage。localStorage 是
  origin 级的,而这些结论探的是「当前用户可见业务组内是否存在」,不按用户隔离会让
  换账号登录的人直接继承上一个人的完成态。
- 只写 true、绝不写 false:一次网络抖动探到的 false 若被固化,用户就再也点不亮那一步。
  collectVerified / testDeliveredLocal 本身就是 localStorage 标记,notifyUsed 会随
  服务端记录按保留期清理而回退,三者都不进缓存。
- 缓存记两个时间戳:at 每轮探测前移、只用于节流;establishedAt 只在完成项集合真的变化时
  前移、只用于 TTL。合用一个会让活跃用户的有效期被无限顺延,TTL 等于永不生效,删光规则的
  用户永久停在「已完成」。
- PROBE_MIN_INTERVAL 取 60s:九步里有八步在完成处显式调 refreshOnboardingProgress
  (不经过节流,点完即时变绿),只有 llm 与从常规页面创建的大盘/规则靠路由探测兜底。
- 同一次页面加载内换登录用户时归零并广播,不继承前一个人的完成态。
@jsers
jsers merged commit b7a4f8b into main Aug 3, 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