feat: SEO improvements, invite rebate fixes, upstream sync - #6531
feat: SEO improvements, invite rebate fixes, upstream sync#6531seven7763 wants to merge 32 commits into
Conversation
Document merge-friendly design for invitee top-up rebates into aff_quota with ledger, configurable ratio, and user/admin pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step-by-step merge-friendly plan covering ledger, hooks, APIs, and default/classic UIs with TDD-oriented tasks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Options default off; unique topup_id ledger; unit tests for formula, disabled path, no inviter, and idempotent grant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hook Stripe/Creem/Waffo/Pancake/manual complete/epay success paths; add user and admin invite_rebate endpoints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Default and classic themes: stats pages, transfer reuse, settings options for enable flag and basis-point ratio. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skip grant when inviter missing; admin route guard; classic sidebar module mapping; coerce enable switch; transfer loading state; grant log only on first success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UI already sends aff_code in body; API clients and invite links using only ?aff= now also resolve inviter_id on password register. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add zh/zh-TW translations for rebate pages, admin filters, settings, and transfer dialog labels used by the feature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Overflow-safe rebate math with hard cap - Skip grants for disabled inviters/invitees and user_id mismatch - Treat gorm.ErrDuplicatedKey as idempotent - Reject non-positive aff_transfer amounts - Ignore disabled inviters at register; bound aff code length - Mask invitee username/display on inviter invitee list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add scheduled system task to re-grant missing rebates for successful top-ups without ledger rows. Reject aff_transfer when account is disabled at both API and model layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recover from RecordLog/cache panics so rebate crediting cannot be rolled back or aborted by logging/redis issues. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add root-only POST /api/system-task/invite-rebate-backfill, option InviteTopupRebateBackfillMinutes (1-1440), admin UI trigger button, and settings fields on default/classic themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Redesign user/admin pages with SectionPageLayout, stats tiles, tabs, invite link copy, and a masked public leaderboard (by rebate/invitees). Add GET /api/user/invite_rebate/leaderboard and classic leaderboard table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Load summary/logs/invitees independently so a missing leaderboard route does not break the whole page; silence global error toast for that call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avoid wiping board state when leaderboard request fails; show error/empty state with retry. Keep tab panels mounted for smoother switching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only expose raw user_id for the viewer's own row; others remain 0 with masked names to reduce cross-user enumeration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge-friendly SPA SEO plan: configurable meta/OG, robots/sitemap, JSON-LD, minimal upstream hooks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task breakdown for merge-friendly SPA SEO: options, robots/sitemap, default lib/seo, settings UI, classic light parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add configurable SEO.* options, expose them via /api/status, and serve crawler-friendly /robots.txt and /sitemap.xml with merge-friendly files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement merge-friendly SPA SEO: SEO.* options, /robots.txt and /sitemap.xml, default lib/seo DOM helpers, homepage JSON-LD, and light classic parity with settings fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix broken import insertion that prevented the SPA bundle from loading, and fetch /api/status on the homepage so SEO meta and JSON-LD apply reliably. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow SEO.Title (full) or SEO.TitleSuffix appended to SystemName so homepage titles can include long-tail keywords instead of short brand-only names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge origin/main into local main while keeping invite top-up rebate and homepage SEO. - Keep backend rebate/SEO modules and thin hooks - Drop removed classic theme paths (upstream web consolidated to web/src) - Port invite-rebate feature, SEO lib, and routes from web/default to web/src - Preserve robots.txt/sitemap.xml registration with WebAssets router API Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove leftover web/default invite-rebate/SEO paths (live under web/src) - Reapply pending invite rebate and SEO hardening on new layout - Keep OAuth aff binding disabled-inviter guard with upstream affiliateCode API - Sync route-level SEO apply in __root with new auth-session bootstrap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CI workflows Brings in 21 upstream commits including: - relaykit module extraction and import path refactoring - trusted_proxies renamed to middleware package - per-channel HTTP transport controls - OIDC custom login display name - Qwen thinking_budget passthrough fix - CI workflow for build/typecheck/test No conflicts with custom features (invite rebate, SEO, etc.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tus cache refresh - Restructure SEO admin settings into dedicated section with proper grid layout - Add RobotsIndex toggle switch to admin UI - Enhance sitemap.xml with lastmod/changefreq/priority metadata - Add /login and /register to sitemap, more Disallow paths to robots.txt - Fix og:site_name to always use brand name instead of full long-tail title - Remove redundant getStatus() call in useHomeSeo (use localStorage cache) - Add SEO keys to STATUS_RELATED_KEYS for immediate cache refresh on save - Add invite_rebate_backfill to system task type labels - Add missing i18n translations for SEO settings across all locales Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mapping
- Add OptionMapRWMutex.RLock/RUnlock around reads of SEOSiteURL,
SEORobotsIndex, and ServerAddress in seoSiteBase() and RobotsTxt()
- XML-escape the site URL in sitemap <loc> elements using html.EscapeString
- Add proper native translations for SEO keys in fr/ja/ru/vi locales
(previously untranslated English placeholders)
- Add missing description keys (Used for meta, Canonical base, etc.)
to fr/ja/ru/vi locales
- Extract nested ternary for language detection into resolveDocumentLang()
helper in web/src/lib/seo/apply.ts
- Use useRouterState pathname selector in __root.tsx (already done)
- Add .url().optional().or(z.literal('')) validation to SEO.SiteURL and
SEO.OGImage fields in the zod schema
- Extract readCachedStatus() helper to web/src/lib/seo/cached-status.ts
and use it in use-home-seo.ts and __root.tsx
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WalkthroughThis PR adds invite top-up rebate accounting with ledger-backed grants, reporting APIs, scheduled backfill, and frontend pages/settings. It also adds configurable homepage SEO metadata, crawl endpoints, browser-side metadata updates, structured data, and DaoXE branding defaults. ChangesInvite top-up rebate
Homepage SEO
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
controller/topup.go (1)
391-413: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftMake Epay completion and user credit atomic before exposing the order to backfill.
topUp.Update()persists success/completion beforeIncreaseUserQuota. If quota credit fails, backfill later sees an eligible successful order and can reward the inviter even though the invitee received no quota. Commit the status and quota update in one transaction, then grant the rebate post-commit.🤖 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 `@controller/topup.go` around lines 391 - 413, Wrap the topUp.Update and IncreaseUserQuota operations in a single database transaction so the completion status and user quota are committed or rolled back together. Ensure the order is not eligible for backfill until both succeed, then invoke GrantInviteTopupRebate only after the transaction commits; preserve the existing error logging and response behavior for failures.
🟡 Minor comments (15)
docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md-12-13 (1)
12-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the frontend paths in the completion note.
The documented paths use
web/default/...andweb/classic/..., but the supplied PR file inventory places the implementation underweb/src/.... Replace these stale paths so future maintainers can locate the actual code.🤖 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 `@docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md` around lines 12 - 13, Update the UI paths in the completion note to reference the implementation under web/src/... instead of the stale web/default/... and web/classic/... locations, while leaving the model and controller paths unchanged.controller/seo.go-88-99 (1)
88-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not report sitemap generation time as each page’s modification time.
This marks every URL as changed every day, even when its content did not change. Omit
lastmoduntil a real per-page update time is available.🤖 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 `@controller/seo.go` around lines 88 - 99, Remove the shared lastmod generation and omit the lastmod XML element from the sitemap entries built in the SEO handler. Keep the remaining URL fields unchanged, and only reintroduce lastmod when a real per-page modification timestamp is available.web/src/features/system-settings/general/system-info-section.tsx-115-121 (1)
115-121: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRestore URL validation in the active resolver.
systemInfoSchemaWithI18nis the schema passed tozodResolver, butSEO.SiteURLandSEO.OGImageare onlyz.string().optional()here while_systemInfoSchemarequiresz.string().url().optional().or(z.literal('')). Use the same URL-or-empty constraints; otherwise invalid strings can be submitted and stored.🤖 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 `@web/src/features/system-settings/general/system-info-section.tsx` around lines 115 - 121, Update SEO.SiteURL and SEO.OGImage in systemInfoSchemaWithI18n to use the same optional URL-or-empty validation as _systemInfoSchema, preserving acceptance of empty strings while rejecting invalid URLs. Ensure the active zodResolver schema enforces these constraints.docs/superpowers/notes/2026-07-12-seo-homepage-done.md-20-24 (1)
20-24: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd a blank line before the table.
Markdownlint reports MD058 because the table directly follows its heading.
🤖 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 `@docs/superpowers/notes/2026-07-12-seo-homepage-done.md` around lines 20 - 24, Insert a blank line between the preceding heading and the SEO configuration table in the documentation, leaving the table contents unchanged.Source: Linters/SAST tools
docs/superpowers/plans/2026-07-12-seo-homepage.md-7-8 (1)
7-8: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAlign SEO documentation with the active frontend layout. Both documents refer to
web/default/src/..., while the reviewed implementation is underweb/src/....
docs/superpowers/plans/2026-07-12-seo-homepage.md#L7-L8: replace the obsoleteweb/default/srcpaths throughout the plan.docs/superpowers/notes/2026-07-12-seo-homepage-done.md#L14-L17: record the deliveredweb/src/lib/seo/*path.🤖 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 `@docs/superpowers/plans/2026-07-12-seo-homepage.md` around lines 7 - 8, Update docs/superpowers/plans/2026-07-12-seo-homepage.md throughout to replace obsolete web/default/src paths with web/src paths, including the SEO helper location. Update docs/superpowers/notes/2026-07-12-seo-homepage-done.md at lines 14-17 to record the delivered web/src/lib/seo/* path.docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md-41-42 (1)
41-42: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSynchronize the rebate design and implementation plan with the final implementation.
docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L41-L42: remove backfill from non-goals.docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L96-L97: documentgrantedandskippedstatuses.docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L116-L119: document permanent skip rows.docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md#L207-L207: update tiny-top-up behavior to record a skip.docs/superpowers/plans/2026-07-12-invite-topup-rebate.md#L148-L153: change the expected 1% result from50to5000.docs/superpowers/plans/2026-07-12-invite-topup-rebate.md#L502-L503: document post-commit best-effort granting.docs/superpowers/plans/2026-07-12-invite-topup-rebate.md#L1021-L1021: remove the claim that transactional paths grant inside the transaction.🤖 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 `@docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md` around lines 41 - 42, Synchronize the documentation with the final implementation: in docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md at lines 41-42 remove backfill from the non-goals, at lines 96-97 document granted and skipped statuses, at lines 116-119 document permanent skip rows, and at line 207 state that tiny top-ups record a skip; in docs/superpowers/plans/2026-07-12-invite-topup-rebate.md at lines 148-153 change the expected 1% result from 50 to 5000, at lines 502-503 document post-commit best-effort granting, and at line 1021 remove the claim that transactional paths grant inside the transaction.model/invite_rebate.go-412-416 (1)
412-416: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPropagate reporting query failures instead of returning fabricated zero values.
These ignored errors make invitee totals appear as zero and can report the viewer as rank 1 when the database query fails. Return the error so controllers can produce an accurate failure response.
Also applies to: 687-723
🤖 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 `@model/invite_rebate.go` around lines 412 - 416, Propagate database errors from the reporting queries that scan invitee rebate totals, including the InviteRebate aggregate query and the related queries in the additional referenced section. Replace ignored Scan(...).Error results with returned errors through the enclosing reporting methods so controllers receive failures instead of fabricated zero totals or incorrect rank data.web/src/i18n/locales/fr.json-5231-5283 (1)
5231-5283: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winInvite-rebate strings left untranslated in fr.json.
Most of the new invite-rebate keys in this range are copied verbatim from English (e.g.
"Your invite program","Invitee","Rebate","Pending rewards","Copy invite link","Top inviters","Failed to load leaderboard", and ~30 more), while the SEO keys added in the same hunk (lines 5285-5294) were properly translated. French-locale users will see the entire new Invite Rebate UI in English.🤖 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 `@web/src/i18n/locales/fr.json` around lines 5231 - 5283, Translate all newly added invite-rebate values in the French locale, including labels, messages, actions, statuses, leaderboard text, and admin settings, while preserving the existing interpolation placeholders such as {{rate}}. Keep the already translated SEO entries unchanged and ensure no invite-rebate value in this section remains identical to its English source unless it is intentionally language-neutral.web/src/i18n/locales/vi.json-5231-5246 (1)
5231-5246: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winTranslate the new invite-rebate strings into Vietnamese.
These newly added user-facing values remain in English, so Vietnamese users will see untranslated rebate, leaderboard, and backfill UI text. Preserve all interpolation placeholders while providing Vietnamese translations.
Also applies to: 5248-5259, 5261-5283
🤖 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 `@web/src/i18n/locales/vi.json` around lines 5231 - 5246, Translate all newly added invite-rebate, leaderboard, and backfill values in vi.json into natural Vietnamese, including the ranges referenced by the comment. Preserve every interpolation placeholder exactly, such as {{rate}}, while leaving the translation keys unchanged.web/src/i18n/locales/vi.json-5294-5294 (1)
5294-5294: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRemove the duplicate
Legalkey.
Legalis already defined at Line 2478. Duplicate JSON keys have parser-dependent overwrite behavior and should not be added here.Proposed fix
- "Legal": "Pháp lý"🤖 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 `@web/src/i18n/locales/vi.json` at line 5294, Remove the duplicate "Legal" entry near the later locale section, keeping the existing "Legal" definition already present earlier in vi.json unchanged.web/src/i18n/locales/zh-TW.json-5309-5309 (1)
5309-5309: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAvoid redefining the
Legalkey.
Legalis already defined at Line 2478. This duplicate relies on last-write-wins parser behavior and changes every existingLegaltranslation from法律to法律條款. Remove this entry, update the original intentionally, or use a distinct key for the SEO label.🤖 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 `@web/src/i18n/locales/zh-TW.json` at line 5309, Remove the duplicate “Legal” entry from the locale object, or rename it to a distinct SEO-specific key and update its callers; if the intended translation is global, modify the existing “Legal” definition near the earlier declaration instead. Ensure the locale contains only one “Legal” key and preserves the intended translations.web/src/i18n/locales/zh.json-5310-5310 (1)
5310-5310: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRemove the duplicate
Legalkey.
"Legal"already exists at Line 2478. Keeping both keys makes the parsed locale depend on duplicate-key behavior and causes all existingLegalusages to resolve to"法律条款"instead of"法律". Reuse the existing key or introduce a distinct source key for legal terms.🤖 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 `@web/src/i18n/locales/zh.json` at line 5310, Remove the later duplicate "Legal" entry from the Chinese locale and retain the existing "Legal" key so lookups continue resolving to its original translation.Source: Learnings
web/src/features/system-settings/general/quota-settings-section.tsx-59-60 (1)
59-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject fractional basis points and backfill intervals.
Both settings represent integers, but the schemas and inputs accept decimals. Add
.int()andstep={1}so invalid values cannot reach the option API.Proposed fix
- InviteTopupRebateRatioBp: z.coerce.number().min(0).max(10000), - InviteTopupRebateBackfillMinutes: z.coerce.number().min(1).max(1440), + InviteTopupRebateRatioBp: z.coerce.number().int().min(0).max(10000), + InviteTopupRebateBackfillMinutes: z.coerce.number().int().min(1).max(1440),Add to both number inputs:
type='number' + step={1}Also applies to: 289-335
🤖 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 `@web/src/features/system-settings/general/quota-settings-section.tsx` around lines 59 - 60, Update the Zod schemas for InviteTopupRebateRatioBp and InviteTopupRebateBackfillMinutes to require integers with .int(), and set step={1} on both corresponding number inputs in the quota settings section so fractional values are rejected before reaching the option API.web/src/features/wallet/components/affiliate-rewards-card.tsx-77-84 (1)
77-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep the new details link visible.
The link follows a long sentence inside
line-clamp-1, so it will commonly be truncated and unclickable. Remove the clamp or render the link separately.- <p className='text-muted-foreground line-clamp-1 text-xs'> + <p className='text-muted-foreground text-xs'>🤖 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 `@web/src/features/wallet/components/affiliate-rewards-card.tsx` around lines 77 - 84, Update the paragraph rendering in the affiliate rewards card around the details Link so the “View top-up rebate details” link remains visible and clickable; remove the line-clamp-1 class or render the link outside the clamped sentence while preserving the existing text and styling.web/src/features/invite-rebate/index.tsx-758-758 (1)
758-758: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winTranslate the admin table’s
IDheading.- <TableHead>ID</TableHead> + <TableHead>{t('ID')}</TableHead>As per coding guidelines, “All user-facing React UI text must support i18n.” <coding_guidelines>
🤖 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 `@web/src/features/invite-rebate/index.tsx` at line 758, Update the admin table header near the TableHead containing “ID” to use the existing i18n translation mechanism instead of a hardcoded string, adding the required translation key if one does not already exist while preserving the displayed meaning.Source: Coding guidelines
🧹 Nitpick comments (4)
web/src/features/home/hooks/use-home-seo.ts (1)
12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the hook’s explicit return type.
Use
export function useHomeSeo(): voidto meet the frontend typing rule.🤖 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 `@web/src/features/home/hooks/use-home-seo.ts` at line 12, Add the explicit void return type to the exported useHomeSeo function declaration, changing its signature to return void while preserving the hook’s existing implementation.Source: Coding guidelines
web/src/lib/seo/dom.ts (1)
1-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeclare the exported helper return types.
All changed helpers have typed parameters but omit their
voidreturn type. Add: voidconsistently.🤖 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 `@web/src/lib/seo/dom.ts` around lines 1 - 55, Declare an explicit void return type on each exported helper: upsertMetaByName, upsertMetaByProperty, upsertLinkRel, upsertJsonLd, and removeJsonLd, without changing their existing behavior.Source: Coding guidelines
web/src/routes/__root.tsx (1)
49-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an explicit component return type.
RootComponentis changed TSX code but has no return annotation. Add a type-onlyReactElementimport and annotate the function, then verify with the project’s Bun lint/type-check commands.🤖 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 `@web/src/routes/__root.tsx` around lines 49 - 55, Add a type-only ReactElement import and annotate RootComponent with ReactElement as its explicit return type. Preserve the existing component implementation, then run the project’s Bun lint and type-check commands to validate the change.Source: Coding guidelines
web/src/i18n/locales/ja.json (1)
5230-5294: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winInvite-rebate strings left untranslated in ja.json and ru.json, while SEO strings in the same block were translated. Both locale files show the same gap: the SEO configuration strings added in this PR (SEO Description, SEO Title Suffix, SEO Keywords, SEO Settings, SEO Site URL, SEO OG Image URL, etc.) are properly localized, but most invite-rebate strings (
Your invite program,Rebate sum,Invite Top-up Rebate,Pending rewards,Top inviters,Backfill queued,Copy invite link, etc.) keep their English text as the translated value.
web/src/i18n/locales/ja.json#L5230-L5294: translate the invite-rebate strings into Japanese to match the SEO strings' translation quality in this same block.web/src/i18n/locales/ru.json#L5230-L5294: translate the invite-rebate strings into Russian to match the SEO strings' translation quality in this same block.🤖 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 `@web/src/i18n/locales/ja.json` around lines 5230 - 5294, Translate all invite-rebate entries in web/src/i18n/locales/ja.json lines 5230-5294 into natural Japanese, replacing English values while preserving keys and interpolation placeholders. Apply the same change to web/src/i18n/locales/ru.json lines 5230-5294 with natural Russian translations; leave the already localized SEO strings unchanged.
🤖 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 `@model/invite_rebate.go`:
- Around line 288-340: Update the rebate transaction around the quota updates
and ledger handling to use the checked quota helpers, propagate their clamp
results, and calculate the amount actually credited after saturation. Persist
that credited amount in InviteRebate rather than the full rebate, including zero
when fully capped, and set granted/log only according to the actual credited
result so RecordLog reports the credited amount and clamp outcome accurately.
- Line 105: Update both skip and grant insert paths in the invite rebate flow to
use GORM’s clause.OnConflict with the topup_id column and DoNothing enabled,
instead of detecting duplicate-key errors after Create. Use
Create(...).RowsAffected to distinguish whether each row was inserted or
skipped, and remove the post-insert unique-violation handling.
- Around line 309-312: Update the zero RowsAffected branch in the invite-rebate
transaction to check the error returned by inner.Delete for the newly inserted
InviteRebate row and return that failure instead of returning nil. Preserve the
existing successful cleanup behavior while ensuring any cleanup failure causes
the transaction to roll back rather than committing an uncredited granted
ledger.
In `@model/option.go`:
- Around line 333-349: The InviteTopupRebateEnabled transition must refresh and
persist the cutoff on every off→on enablement. In the InviteTopupRebateEnabled
case, generate a new timestamp and save it successfully before publishing the
enabled state, update the in-memory value only after persistence succeeds, and
return any DB.Save failure instead of ignoring it.
In `@web/src/features/invite-rebate/index.tsx`:
- Around line 645-674: Separate the invitee and inviter text input state from
the applied filter values used by reload. Keep reload and its useEffect
dependent only on applied filters, and have the Filter button copy the current
draft inputs into those applied values before triggering the request, preventing
requests on every keystroke and stale partial-ID results.
- Around line 125-138: Update the allSettled response handling in the
invite-rebate loading flow to explicitly handle fulfilled business-error
responses where success is false. Ensure failures for summary, logs, invitees,
and affiliate code set appropriate error state or user feedback instead of
silently leaving zero or disabled UI values, while preserving successful
response handling and the existing leaderboard error behavior.
In `@web/src/lib/seo/apply.ts`:
- Around line 154-155: Update the SEO options in applyDocumentSeo so the path
field always preserves the actual path, including for non-marketing pages,
instead of defaulting to '/'. Leave robotsIndex as the sole control for indexing
behavior and preserve the existing publicPath handling.
---
Outside diff comments:
In `@controller/topup.go`:
- Around line 391-413: Wrap the topUp.Update and IncreaseUserQuota operations in
a single database transaction so the completion status and user quota are
committed or rolled back together. Ensure the order is not eligible for backfill
until both succeed, then invoke GrantInviteTopupRebate only after the
transaction commits; preserve the existing error logging and response behavior
for failures.
---
Minor comments:
In `@controller/seo.go`:
- Around line 88-99: Remove the shared lastmod generation and omit the lastmod
XML element from the sitemap entries built in the SEO handler. Keep the
remaining URL fields unchanged, and only reintroduce lastmod when a real
per-page modification timestamp is available.
In `@docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md`:
- Around line 12-13: Update the UI paths in the completion note to reference the
implementation under web/src/... instead of the stale web/default/... and
web/classic/... locations, while leaving the model and controller paths
unchanged.
In `@docs/superpowers/notes/2026-07-12-seo-homepage-done.md`:
- Around line 20-24: Insert a blank line between the preceding heading and the
SEO configuration table in the documentation, leaving the table contents
unchanged.
In `@docs/superpowers/plans/2026-07-12-seo-homepage.md`:
- Around line 7-8: Update docs/superpowers/plans/2026-07-12-seo-homepage.md
throughout to replace obsolete web/default/src paths with web/src paths,
including the SEO helper location. Update
docs/superpowers/notes/2026-07-12-seo-homepage-done.md at lines 14-17 to record
the delivered web/src/lib/seo/* path.
In `@docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md`:
- Around line 41-42: Synchronize the documentation with the final
implementation: in
docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md at lines 41-42
remove backfill from the non-goals, at lines 96-97 document granted and skipped
statuses, at lines 116-119 document permanent skip rows, and at line 207 state
that tiny top-ups record a skip; in
docs/superpowers/plans/2026-07-12-invite-topup-rebate.md at lines 148-153 change
the expected 1% result from 50 to 5000, at lines 502-503 document post-commit
best-effort granting, and at line 1021 remove the claim that transactional paths
grant inside the transaction.
In `@model/invite_rebate.go`:
- Around line 412-416: Propagate database errors from the reporting queries that
scan invitee rebate totals, including the InviteRebate aggregate query and the
related queries in the additional referenced section. Replace ignored
Scan(...).Error results with returned errors through the enclosing reporting
methods so controllers receive failures instead of fabricated zero totals or
incorrect rank data.
In `@web/src/features/invite-rebate/index.tsx`:
- Line 758: Update the admin table header near the TableHead containing “ID” to
use the existing i18n translation mechanism instead of a hardcoded string,
adding the required translation key if one does not already exist while
preserving the displayed meaning.
In `@web/src/features/system-settings/general/quota-settings-section.tsx`:
- Around line 59-60: Update the Zod schemas for InviteTopupRebateRatioBp and
InviteTopupRebateBackfillMinutes to require integers with .int(), and set
step={1} on both corresponding number inputs in the quota settings section so
fractional values are rejected before reaching the option API.
In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 115-121: Update SEO.SiteURL and SEO.OGImage in
systemInfoSchemaWithI18n to use the same optional URL-or-empty validation as
_systemInfoSchema, preserving acceptance of empty strings while rejecting
invalid URLs. Ensure the active zodResolver schema enforces these constraints.
In `@web/src/features/wallet/components/affiliate-rewards-card.tsx`:
- Around line 77-84: Update the paragraph rendering in the affiliate rewards
card around the details Link so the “View top-up rebate details” link remains
visible and clickable; remove the line-clamp-1 class or render the link outside
the clamped sentence while preserving the existing text and styling.
In `@web/src/i18n/locales/fr.json`:
- Around line 5231-5283: Translate all newly added invite-rebate values in the
French locale, including labels, messages, actions, statuses, leaderboard text,
and admin settings, while preserving the existing interpolation placeholders
such as {{rate}}. Keep the already translated SEO entries unchanged and ensure
no invite-rebate value in this section remains identical to its English source
unless it is intentionally language-neutral.
In `@web/src/i18n/locales/vi.json`:
- Around line 5231-5246: Translate all newly added invite-rebate, leaderboard,
and backfill values in vi.json into natural Vietnamese, including the ranges
referenced by the comment. Preserve every interpolation placeholder exactly,
such as {{rate}}, while leaving the translation keys unchanged.
- Line 5294: Remove the duplicate "Legal" entry near the later locale section,
keeping the existing "Legal" definition already present earlier in vi.json
unchanged.
In `@web/src/i18n/locales/zh-TW.json`:
- Line 5309: Remove the duplicate “Legal” entry from the locale object, or
rename it to a distinct SEO-specific key and update its callers; if the intended
translation is global, modify the existing “Legal” definition near the earlier
declaration instead. Ensure the locale contains only one “Legal” key and
preserves the intended translations.
In `@web/src/i18n/locales/zh.json`:
- Line 5310: Remove the later duplicate "Legal" entry from the Chinese locale
and retain the existing "Legal" key so lookups continue resolving to its
original translation.
---
Nitpick comments:
In `@web/src/features/home/hooks/use-home-seo.ts`:
- Line 12: Add the explicit void return type to the exported useHomeSeo function
declaration, changing its signature to return void while preserving the hook’s
existing implementation.
In `@web/src/i18n/locales/ja.json`:
- Around line 5230-5294: Translate all invite-rebate entries in
web/src/i18n/locales/ja.json lines 5230-5294 into natural Japanese, replacing
English values while preserving keys and interpolation placeholders. Apply the
same change to web/src/i18n/locales/ru.json lines 5230-5294 with natural Russian
translations; leave the already localized SEO strings unchanged.
In `@web/src/lib/seo/dom.ts`:
- Around line 1-55: Declare an explicit void return type on each exported
helper: upsertMetaByName, upsertMetaByProperty, upsertLinkRel, upsertJsonLd, and
removeJsonLd, without changing their existing behavior.
In `@web/src/routes/__root.tsx`:
- Around line 49-55: Add a type-only ReactElement import and annotate
RootComponent with ReactElement as its explicit return type. Preserve the
existing component implementation, then run the project’s Bun lint and
type-check commands to validate the change.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b501ac9a-bdc3-46f9-ba39-fd35eb2f7fe3
📒 Files selected for processing (64)
common/constants.gocommon/seo.gocontroller/invite_rebate.gocontroller/invite_rebate_task.gocontroller/misc.gocontroller/oauth.gocontroller/seo.gocontroller/system_task.gocontroller/system_task_handlers.gocontroller/topup.gocontroller/user.godocs/superpowers/notes/2026-07-12-invite-topup-rebate-done.mddocs/superpowers/notes/2026-07-12-seo-homepage-done.mddocs/superpowers/plans/2026-07-12-invite-topup-rebate.mddocs/superpowers/plans/2026-07-12-seo-homepage.mddocs/superpowers/specs/2026-07-12-invite-topup-rebate-design.mddocs/superpowers/specs/2026-07-12-seo-homepage-design.mdmodel/invite_rebate.gomodel/invite_rebate_test.gomodel/main.gomodel/option.gomodel/system_task.gomodel/task_cas_test.gomodel/topup.gomodel/user.gorouter/api-router.gorouter/main.goweb/index.htmlweb/src/features/home/hooks/use-home-seo.tsweb/src/features/home/index.tsxweb/src/features/invite-rebate/api.tsweb/src/features/invite-rebate/index.tsxweb/src/features/invite-rebate/types.tsweb/src/features/system-info/components/system-tasks-panel.tsxweb/src/features/system-settings/billing/index.tsxweb/src/features/system-settings/billing/section-registry.tsxweb/src/features/system-settings/general/quota-settings-section.tsxweb/src/features/system-settings/general/system-info-section.tsxweb/src/features/system-settings/hooks/use-update-option.tsweb/src/features/system-settings/site/index.tsxweb/src/features/system-settings/site/section-registry.tsxweb/src/features/system-settings/types.tsweb/src/features/wallet/components/affiliate-rewards-card.tsxweb/src/hooks/use-sidebar-config.tsweb/src/hooks/use-sidebar-data.tsweb/src/i18n/locales/en.jsonweb/src/i18n/locales/fr.jsonweb/src/i18n/locales/ja.jsonweb/src/i18n/locales/ru.jsonweb/src/i18n/locales/vi.jsonweb/src/i18n/locales/zh-TW.jsonweb/src/i18n/locales/zh.jsonweb/src/i18n/static-keys.tsweb/src/lib/seo/apply.tsweb/src/lib/seo/cached-status.tsweb/src/lib/seo/defaults.tsweb/src/lib/seo/dom.tsweb/src/lib/seo/index.tsweb/src/lib/seo/types.tsweb/src/main.tsxweb/src/routeTree.gen.tsweb/src/routes/__root.tsxweb/src/routes/_authenticated/invite-rebate/admin.tsxweb/src/routes/_authenticated/invite-rebate/index.tsx
| Status: InviteRebateStatusSkipped, | ||
| CreatedAt: common.GetTimestamp(), | ||
| } | ||
| if err := db.Create(row).Error; err != nil && !isDuplicateKeyError(err) { |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
rg -n -C3 'TranslateError|ErrDuplicatedKey|OnConflict|isDuplicateKeyError' \
--glob '*.go' .
rg -n 'gorm.io/gorm' "$(fd -a '^go\.mod$' . | head -n1)"Repository: QuantumNous/new-api
Length of output: 5241
🏁 Script executed:
#!/bin/bash
set -eu
echo "== invite_rebate.go relevant sections =="
sed -n '1,125p' model/invite_rebate.go | cat -n
sed -n '145,175p' model/invite_rebate.go | cat -n
sed -n '250,295p' model/invite_rebate.go | cat -n
echo "== invite_rebate.go schema/index definitions =="
sed -n '1,80p' model/invite_rebate.go | cat -n
echo "== imports =="
sed -n '1,40p' model/invite_rebate.go | cat -nRepository: QuantumNous/new-api
Length of output: 13248
Use conflict-ignore insertion instead of handling unique violations after insert.
On PostgreSQL, a unique-constraint violation aborts the transaction at the database level, so recognizing gorm.ErrDuplicatedKey after db.Create(...) won’t make the transaction usable. Use clause.OnConflict{Columns: ... Columns: {{Name: "topup_id"}}, DoNothing: true} for both the skip and grant inserts, then rely on Create(...).RowsAffected to distinguish inserted versus skipped rows.
🤖 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 `@model/invite_rebate.go` at line 105, Update both skip and grant insert paths
in the invite rebate flow to use GORM’s clause.OnConflict with the topup_id
column and DoNothing enabled, instead of detecting duplicate-key errors after
Create. Use Create(...).RowsAffected to distinguish whether each row was
inserted or skipped, and remove the post-insert unique-violation handling.
Source: Coding guidelines
| // Only credit enabled inviter (re-check status under same tx when possible). | ||
| // Saturate to MaxQuota without dialect-specific LEAST (SQLite has no LEAST by default). | ||
| capHeadroom := common.MaxQuota - rebate | ||
| if capHeadroom < 0 { | ||
| capHeadroom = 0 | ||
| } | ||
| res := inner.Model(&User{}). | ||
| Where("id = ? AND status = ?", invitee.InviterId, common.UserStatusEnabled). | ||
| Updates(map[string]interface{}{ | ||
| "aff_quota": gorm.Expr( | ||
| "CASE WHEN aff_quota > ? THEN ? ELSE aff_quota + ? END", | ||
| capHeadroom, common.MaxQuota, rebate, | ||
| ), | ||
| "aff_history": gorm.Expr( | ||
| "CASE WHEN aff_history > ? THEN ? ELSE aff_history + ? END", | ||
| capHeadroom, common.MaxQuota, rebate, | ||
| ), | ||
| }) | ||
| if res.Error != nil { | ||
| return res.Error | ||
| } | ||
| if res.RowsAffected == 0 { | ||
| // Inviter became disabled between read and update: remove ledger so retry is possible. | ||
| _ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error | ||
| return nil | ||
| } | ||
| granted = true | ||
| return nil | ||
| } | ||
|
|
||
| var err error | ||
| if tx != nil { | ||
| err = run(tx) | ||
| } else { | ||
| err = DB.Transaction(run) | ||
| } | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if granted { | ||
| // Log only on first successful grant (webhook retries must not spam). | ||
| // RecordLog may touch cache/redis; never let logging fail the grant path. | ||
| func() { | ||
| defer func() { | ||
| if rec := recover(); rec != nil { | ||
| common.SysError(fmt.Sprintf("invite rebate RecordLog panic: %v", rec)) | ||
| } | ||
| }() | ||
| RecordLog(invitee.InviterId, LogTypeSystem, fmt.Sprintf( | ||
| "邀请充值返佣 %s(被邀请用户 #%d,订单 %s,基数 %s,比例 %d bp)", | ||
| logger.LogQuota(rebate), inviteeId, topUp.TradeNo, logger.LogQuota(topupQuota), ratioBp, | ||
| )) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Record and log the amount actually credited after saturation.
The ledger stores the full rebate, even when aff_quota or aff_history is capped and receives less—or nothing. That permanently marks the top-up granted while overstating accounting totals. Use the checked quota helpers, propagate clamp details, and persist the actual credited amount before logging.
As per coding guidelines, billing paths must use checked quota helpers, propagate clamp information, and make saturation auditable before logging. <coding_guidelines>
🤖 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 `@model/invite_rebate.go` around lines 288 - 340, Update the rebate transaction
around the quota updates and ledger handling to use the checked quota helpers,
propagate their clamp results, and calculate the amount actually credited after
saturation. Persist that credited amount in InviteRebate rather than the full
rebate, including zero when fully capped, and set granted/log only according to
the actual credited result so RecordLog reports the credited amount and clamp
outcome accurately.
Source: Coding guidelines
| if res.RowsAffected == 0 { | ||
| // Inviter became disabled between read and update: remove ledger so retry is possible. | ||
| _ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error | ||
| return nil |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Return a failed ledger cleanup instead of committing it.
If deleting the newly inserted row fails after the inviter update affects zero rows, returning nil commits a granted ledger without crediting the inviter and prevents future retries.
Proposed fix
if res.RowsAffected == 0 {
// Inviter became disabled between read and update: remove ledger so retry is possible.
- _ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error
+ if err := inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error; err != nil {
+ return err
+ }
return nil
}📝 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.
| if res.RowsAffected == 0 { | |
| // Inviter became disabled between read and update: remove ledger so retry is possible. | |
| _ = inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error | |
| return nil | |
| if res.RowsAffected == 0 { | |
| // Inviter became disabled between read and update: remove ledger so retry is possible. | |
| if err := inner.Delete(&InviteRebate{}, "id = ?", row.Id).Error; err != nil { | |
| return err | |
| } | |
| return nil |
🤖 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 `@model/invite_rebate.go` around lines 309 - 312, Update the zero RowsAffected
branch in the invite-rebate transaction to check the error returned by
inner.Delete for the newly inserted InviteRebate row and return that failure
instead of returning nil. Preserve the existing successful cleanup behavior
while ensuring any cleanup failure causes the transaction to roll back rather
than committing an uncredited granted ledger.
| case "InviteTopupRebateEnabled": | ||
| was := common.InviteTopupRebateEnabled | ||
| common.InviteTopupRebateEnabled = boolValue | ||
| // First/last turn-ON stamps the cutoff: only later top-ups earn rebate. | ||
| // Turning OFF does not clear the stamp (re-enable keeps original start). | ||
| // If admin never had a stamp and enables, set now. | ||
| if boolValue && !was { | ||
| if common.InviteTopupRebateEnabledAt <= 0 { | ||
| common.InviteTopupRebateEnabledAt = common.GetTimestamp() | ||
| common.OptionMap["InviteTopupRebateEnabledAt"] = strconv.FormatInt(common.InviteTopupRebateEnabledAt, 10) | ||
| // Persist cutoff so restarts / other nodes share the same boundary. | ||
| _ = DB.Save(&Option{ | ||
| Key: "InviteTopupRebateEnabledAt", | ||
| Value: common.OptionMap["InviteTopupRebateEnabledAt"], | ||
| }).Error | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Reset and atomically persist the cutoff on every off→on transition.
Keeping the original timestamp means re-enabling later allows backfill to grant top-ups completed while the feature was disabled. Additionally, ignoring DB.Save failures can leave nodes using different eligibility boundaries. Persist a fresh timestamp successfully before publishing the enabled state, and return persistence failures.
🤖 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 `@model/option.go` around lines 333 - 349, The InviteTopupRebateEnabled
transition must refresh and persist the cutoff on every off→on enablement. In
the InviteTopupRebateEnabled case, generate a new timestamp and save it
successfully before publishing the enabled state, update the in-memory value
only after persistence succeeds, and return any DB.Save failure instead of
ignoring it.
| if (s?.success && s.data) setSummary(s.data) | ||
| else if (!s) toast.error(t('Failed to load invite rebate data')) | ||
|
|
||
| if (l?.success && l.data) setLogs(l.data.items || []) | ||
| if (i?.success && i.data) setInvitees(i.data.items || []) | ||
| if (b?.success && b.data) { | ||
| setBoard(b.data.items || []) | ||
| setMyRank(b.data.my_rank || 0) | ||
| setLeaderboardError(false) | ||
| } else if (settled[3].status === 'rejected' || (b && !b.success)) { | ||
| // Keep previous board data if any; only flag error for empty UI retry | ||
| setLeaderboardError(true) | ||
| } | ||
| if (a?.success && a.data) setAffCode(a.data) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle fulfilled business-error responses instead of rendering zero data.
Promise.allSettled treats { success: false } as fulfilled, but summary, logs, invitees, and affiliate-code failures are silently ignored. On initial summary failure, loading ends and the page misleadingly renders zero balances and “Rebate disabled.”
Proposed summary handling
- if (s?.success && s.data) setSummary(s.data)
- else if (!s) toast.error(t('Failed to load invite rebate data'))
+ if (s?.success && s.data) {
+ setSummary(s.data)
+ } else {
+ toast.error(s?.message || t('Failed to load invite rebate data'))
+ }📝 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.
| if (s?.success && s.data) setSummary(s.data) | |
| else if (!s) toast.error(t('Failed to load invite rebate data')) | |
| if (l?.success && l.data) setLogs(l.data.items || []) | |
| if (i?.success && i.data) setInvitees(i.data.items || []) | |
| if (b?.success && b.data) { | |
| setBoard(b.data.items || []) | |
| setMyRank(b.data.my_rank || 0) | |
| setLeaderboardError(false) | |
| } else if (settled[3].status === 'rejected' || (b && !b.success)) { | |
| // Keep previous board data if any; only flag error for empty UI retry | |
| setLeaderboardError(true) | |
| } | |
| if (a?.success && a.data) setAffCode(a.data) | |
| if (s?.success && s.data) { | |
| setSummary(s.data) | |
| } else { | |
| toast.error(s?.message || t('Failed to load invite rebate data')) | |
| } | |
| if (l?.success && l.data) setLogs(l.data.items || []) | |
| if (i?.success && i.data) setInvitees(i.data.items || []) | |
| if (b?.success && b.data) { | |
| setBoard(b.data.items || []) | |
| setMyRank(b.data.my_rank || 0) | |
| setLeaderboardError(false) | |
| } else if (settled[3].status === 'rejected' || (b && !b.success)) { | |
| // Keep previous board data if any; only flag error for empty UI retry | |
| setLeaderboardError(true) | |
| } | |
| if (a?.success && a.data) setAffCode(a.data) |
🤖 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 `@web/src/features/invite-rebate/index.tsx` around lines 125 - 138, Update the
allSettled response handling in the invite-rebate loading flow to explicitly
handle fulfilled business-error responses where success is false. Ensure
failures for summary, logs, invitees, and affiliate code set appropriate error
state or user feedback instead of silently leaving zero or disabled UI values,
while preserving successful response handling and the existing leaderboard error
behavior.
| const reload = useCallback(async () => { | ||
| setLoading(true) | ||
| try { | ||
| const params: { | ||
| p: number | ||
| page_size: number | ||
| inviter_id?: number | ||
| invitee_id?: number | ||
| } = { p: 1, page_size: 50 } | ||
| if (inviterId) params.inviter_id = Number(inviterId) | ||
| if (inviteeId) params.invitee_id = Number(inviteeId) | ||
| const [s, list] = await Promise.all([ | ||
| fetchAdminInviteRebateSummary( | ||
| inviterId ? Number(inviterId) : undefined | ||
| ), | ||
| fetchAdminInviteRebates(params), | ||
| ]) | ||
| if (s.success && s.data) setSummary(s.data) | ||
| if (list.success && list.data) setRows(list.data.items || []) | ||
| } catch (e) { | ||
| console.error(e) | ||
| toast.error(t('Failed to load admin rebate data')) | ||
| } finally { | ||
| setLoading(false) | ||
| } | ||
| }, [inviteeId, inviterId, t]) | ||
|
|
||
| useEffect(() => { | ||
| void reload() | ||
| }, [reload]) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not apply filters automatically on every keystroke.
Because reload depends on inviterId and inviteeId, the effect sends requests after every input change. Concurrent responses can arrive out of order and replace the table with results for an older partial ID. Keep draft inputs separate from applied filters, and update the latter only from the Filter button.
🤖 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 `@web/src/features/invite-rebate/index.tsx` around lines 645 - 674, Separate
the invitee and inviter text input state from the applied filter values used by
reload. Keep reload and its useEffect dependent only on applied filters, and
have the Filter button copy the current draft inputs into those applied values
before triggering the request, preventing requests on every keystroke and stale
partial-ID results.
| robotsIndex: publicPath ? baseRobots : false, | ||
| path: publicPath ? path : '/', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve the actual route path for non-marketing pages.
Rewriting every non-marketing path to / makes applyDocumentSeo treat console/auth pages as homepages: it appends the homepage long-tail title and emits the homepage canonical/OG URL. Keep path unchanged; use robotsIndex alone to control indexing.
Proposed fix
- path: publicPath ? path : '/',
+ path,📝 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.
| robotsIndex: publicPath ? baseRobots : false, | |
| path: publicPath ? path : '/', | |
| robotsIndex: publicPath ? baseRobots : false, | |
| path, |
🤖 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 `@web/src/lib/seo/apply.ts` around lines 154 - 155, Update the SEO options in
applyDocumentSeo so the path field always preserves the actual path, including
for non-marketing pages, instead of defaulting to '/'. Leave robotsIndex as the
sole control for indexing behavior and preserve the existing publicPath
handling.
…a validation, docs paths - Wrap epay topUp.Update() and quota increase in a single DB transaction so order is not eligible for backfill until both succeed; rebate grant happens only after transaction commits - Remove meaningless lastmod (today's date) from sitemap entries - Add z.string().url() validation to SEO.SiteURL and SEO.OGImage fields - Update web/default/src and web/classic/src paths to web/src in docs - Add blank line before table in SEO done note (MD058) - Sync invite-rebate spec: remove backfill from non-goals, document granted/skipped statuses and permanent skip rows - Fix expected 1% result from 50 to 5000 in rebate plan test Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
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)
web/src/features/system-settings/general/system-info-section.tsx (1)
97-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPreserve the default-enabled robots setting.
When the option is absent, this expression normalizes it to
false, while the browser SEO layer treats a missingseo_robots_indexas enabled. The form therefore displays indexing as disabled for the default configuration.Normalize only an explicit
false/'false'as disabled; treat missing values astrue.🤖 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 `@web/src/features/system-settings/general/system-info-section.tsx` around lines 97 - 99, Update the 'SEO.RobotsIndex' normalization in the system information form to return false only when defaultValues explicitly contains false or 'false'; treat missing or other values as true so the default-enabled robots setting is preserved.
🧹 Nitpick comments (1)
web/src/features/system-settings/general/system-info-section.tsx (1)
91-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the
anycasts from SEO fields.These casts bypass type checking for both default access and every SEO
FormFieldname. Define a typed field-path contract instead, so renames and schema mismatches are caught at compile time.Also applies to: 298-298, 324-324, 350-350, 375-375, 398-398, 422-422, 446-446
🤖 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 `@web/src/features/system-settings/general/system-info-section.tsx` around lines 91 - 96, Replace the any casts used for SEO default-value access and all SEO FormField names in the system info section with a typed field-path contract derived from the form schema. Update the affected SEO fields, including SEO.Title, SEO.TitleSuffix, SEO.Description, SEO.Keywords, SEO.SiteURL, and SEO.OGImage, so both default access and field names are compile-time validated.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 `@controller/topup.go`:
- Around line 402-405: Replace the raw quota Update in the top-up transaction
with a transaction-compatible checked quota helper that enforces the quota/clamp
contract and returns clamp information. Ensure quota-cache synchronization or
invalidation occurs only after the transaction commits, and propagate the
saturation result to call attachQuotaSaturation before writing any consume or
task logs.
- Around line 398-407: The transaction around saving topUp must atomically
update the order only when its current status is pending, rather than using an
unconditional tx.Save. Check the conditional update’s RowsAffected and treat
zero rows as an already-processed callback, ensuring quota crediting and the
log/rebate side effects execute only for the transaction that successfully
transitions the order from pending to success.
In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 119-120: Update the SEO.OGImage schema entry in the system-info
validation definition to accept empty values and paths beginning with “/”,
instead of requiring a full URL. Leave the SEO.SiteURL validation unchanged and
preserve rejection of other invalid OG image values.
---
Outside diff comments:
In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 97-99: Update the 'SEO.RobotsIndex' normalization in the system
information form to return false only when defaultValues explicitly contains
false or 'false'; treat missing or other values as true so the default-enabled
robots setting is preserved.
---
Nitpick comments:
In `@web/src/features/system-settings/general/system-info-section.tsx`:
- Around line 91-96: Replace the any casts used for SEO default-value access and
all SEO FormField names in the system info section with a typed field-path
contract derived from the form schema. Update the affected SEO fields, including
SEO.Title, SEO.TitleSuffix, SEO.Description, SEO.Keywords, SEO.SiteURL, and
SEO.OGImage, so both default access and field names are compile-time validated.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bb31e385-393e-461c-a55d-f0bbfe8c91e5
📒 Files selected for processing (8)
controller/seo.gocontroller/topup.godocs/superpowers/notes/2026-07-12-invite-topup-rebate-done.mddocs/superpowers/notes/2026-07-12-seo-homepage-done.mddocs/superpowers/plans/2026-07-12-invite-topup-rebate.mddocs/superpowers/plans/2026-07-12-seo-homepage.mddocs/superpowers/specs/2026-07-12-invite-topup-rebate-design.mdweb/src/features/system-settings/general/system-info-section.tsx
💤 Files with no reviewable changes (1)
- controller/seo.go
🚧 Files skipped from review as they are similar to previous changes (4)
- docs/superpowers/notes/2026-07-12-invite-topup-rebate-done.md
- docs/superpowers/plans/2026-07-12-invite-topup-rebate.md
- docs/superpowers/plans/2026-07-12-seo-homepage.md
- docs/superpowers/specs/2026-07-12-invite-topup-rebate-design.md
| err := model.DB.Transaction(func(tx *gorm.DB) error { | ||
| if txErr := tx.Save(topUp).Error; txErr != nil { | ||
| return txErr | ||
| } | ||
| if txErr := tx.Model(&model.User{}).Where("id = ?", topUp.UserId). | ||
| Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; txErr != nil { | ||
| return txErr | ||
| } | ||
| return nil | ||
| }) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
git ls-files | rg '(^|/)controller/topup\.go$|LockOrder|rebate|topup' || true
echo
echo "== controller/topup.go outline/stat =="
wc -l controller/topup.go 2>/dev/null || true
ast-grep outline controller/topup.go 2>/dev/null || true
echo
echo "== relevant topup.go sections =="
sed -n '320,450p' controller/topup.go 2>/dev/null || true
echo
echo "== LockOrder references =="
rg -n "LockOrder|ReleaseOrder|order status|Update\\(\"status\"|TopUp|topUp|top_up|quota" controller -S || trueRepository: QuantumNous/new-api
Length of output: 28836
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== LockOrder implementation =="
sed -n '270,310p' controller/topup.go
echo
echo "== payment webhook lock handlers =="
for f in controller/topup_stripe.go controller/topup_creem.go controller/topup_waffo.go controller/topup_waffo_pancake.go controller/subscription_payment_epay.go controller/subscription_payment_creem.go controller/subscription_payment_stripe.go controller/subscription_payment_waffo_pancake.go; do
if [ -f "$f" ]; then
echo "--- $f"
rg -n -A 35 -B 10 "LockOrder\\(|UnlockOrder\\(|Status != common\\.TopUpStatusPending|UpdatePendingTopUpStatus|quota\\s*\\+|RecordTopupLog|GrantInviteTopupRebate" "$f"
fi
done
echo
echo "== TopUp model methods =="
cat -n model/topup.go | sed -n '1,220p'
echo
echo "== rebate idempotency references =="
rg -n -A 30 -B 10 "func GrantInviteTopupRebate|GrantInviteTopupRebate\\(|Insert|top_up_rebate|topup.*rebate|topup_quota|rebate_quota|quota|UNIQUE|unique|duplicate" model/invite_rebate.go controller/invite_rebate.goRepository: QuantumNous/new-api
Length of output: 18163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== TopUp model methods =="
cat -n model/topup.go | sed -n '1,260p'
echo
echo "== rebate idempotency references =="
rg -n -A 40 -B 10 "func GrantInviteTopupRebate|GrantInviteTopupRebate\\(|Insert|top_up_rebate|topup.*rebate|topup_quota|rebate_quota|quota|UNIQUE|unique|duplicate|NOT NULL" model/invite_rebate.go controller/invite_rebate.go model -S || true
echo
echo "== deterministic transaction probe =="
python3 - <<'PY'
from dataclasses import dataclass
`@dataclass`
class Store:
topup_status: str
user_quota: int
class TxAlreadyCommitted(Exception):
pass
def save(store, new_status):
if store.topup_status != new_status:
raise TxAlreadyCommitted()
def update_add_quota(store, amount):
store.user_quota += amount
def handle(store, pending_status, target_status, amount):
try:
save(store, target_status)
except TxAlreadyCommitted:
return False
update_add_quota(store, amount)
return True
# Concurrent in-memory read before both attempt transaction.
s = Store(topup_status='pending', user_quota=100)
s2 = Store(**vars(s))
a = handle(s, 'pending', 'pending', 50)
b = handle(s2, 'pending', 'pending', 50)
print("r1_processed:", True if a else False)
print("r2_processed:", True if not b else False)
print("r1_quota_after:", s.user_quota)
print("r2_quota_after:", s2.user_quota)
# In a real DB, Save(status=pending->success) leaves status success; second Save fails.
PYRepository: QuantumNous/new-api
Length of output: 50376
Make the pending-to-success transition database-idempotent.
LockOrder only serializes requests inside one process. Concurrent or duplicate epay callbacks can both pass the initial Status == Pending check, then both commit quota credits. Update the order only when its current status is pending, check RowsAffected, and run the log/rebate side effects only for the transaction that acquired the pending row.
🤖 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 `@controller/topup.go` around lines 398 - 407, The transaction around saving
topUp must atomically update the order only when its current status is pending,
rather than using an unconditional tx.Save. Check the conditional update’s
RowsAffected and treat zero rows as an already-processed callback, ensuring
quota crediting and the log/rebate side effects execute only for the transaction
that successfully transitions the order from pending to success.
| if txErr := tx.Model(&model.User{}).Where("id = ?", topUp.UserId). | ||
| Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; txErr != nil { | ||
| return txErr | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Use the checked quota mutation path instead of a raw update.
This bypasses the quota-cache synchronization performed by model.IncreaseUserQuota, so cache-backed balance checks can retain the pre-top-up quota after commit. It also bypasses the required checked quota/clamp contract. Add a transaction-compatible checked helper, then synchronize or invalidate cache only after commit.
As per coding guidelines, “Billing paths must use checked quota helpers, propagate clamp information, and call attachQuotaSaturation before writing consume or task logs so saturation is auditable.”
🤖 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 `@controller/topup.go` around lines 402 - 405, Replace the raw quota Update in
the top-up transaction with a transaction-compatible checked quota helper that
enforces the quota/clamp contract and returns clamp information. Ensure
quota-cache synchronization or invalidation occurs only after the transaction
commits, and propagate the saturation result to call attachQuotaSaturation
before writing any consume or task logs.
Source: Coding guidelines
| 'SEO.SiteURL': z.string().url().optional().or(z.literal('')), | ||
| 'SEO.OGImage': z.string().url().optional().or(z.literal('')), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'system-info-section\.tsx$' . || true
echo "== outline =="
file="$(fd 'system-info-section\.tsx$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
wc -l "$file"
ast-grep outline "$file" || true
echo "== relevant schema lines =="
sed -n '100,135p' "$file"
echo "== relevant line 428 =="
sed -n '420,435p' "$file"
fi
echo "== search OG/SiteURL related schemas/usages/renderers =="
rg -n "SEO\.OGImage|SEO\.SiteURL|OgImage|og:image|/logo\.png|OGImage|SiteURL|relative" web/src || trueRepository: QuantumNous/new-api
Length of output: 43117
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SEO apply outline =="
ast-grep outline web/src/lib/seo/apply.ts || true
echo "== SEO apply relevant lines =="
sed -n '1,120p' web/src/lib/seo/apply.ts
echo "== SEO apply full =="
wc -l web/src/bin/seo/apply.ts || true
sed -n '120,190p' web/src/lib/seo/apply.ts
echo "== Zod package availability/version =="
if [ -f web/package.json ]; then sed -n '1,140p' web/package.json; fi
if command -v bun >/dev/null 2>&1; then bun --version || true; fi
echo "== Standalone Zod URL behavior probe =="
node - <<'JS'
async function main() {
let z
try {
const pkg = await import('zod')
z = pkg.z
} catch (e) {
try {
z = require('zod')
} catch (e2) {
console.log('zod runtime unavailable:', e2 && e2.message)
return
}
}
const schema = z.string().url().optional().or(z.literal(''))
const values = ['', '/logo.png', 'https://example.com/logo.png', 'data:text/plain,x']
for (const value of values) {
try {
console.log(JSON.stringify({value, result: schema.parse(value)}))
} catch (e) {
console.log(JSON.stringify({value, error: e.message || String(e)}))
}
}
}
main().catch(err => { console.error(err); process.exit(1) })
JSRepository: QuantumNous/new-api
Length of output: 10309
🌐 Web query:
Zod 4 string url validation accepts relative URL /logo.png URL string
💡 Result:
No, Zod 4 string URL validation does not accept relative URLs like /logo.png by default [1][2]. The z.url() validator in Zod 4 internally utilizes the WHATWG new URL() constructor to validate inputs [1][3]. According to the WHATWG URL standard, a relative path alone cannot be instantiated as a valid absolute URL without a base URL, which causes new URL('/logo.png') to throw an error [1][4]. Consequently, z.url() will fail validation for such strings [1][2]. If you need to support relative URLs, you must use a custom validation method, such as .refine() [4][3]: const relativeUrlSchema = z.string.refine((val) => { // Check if it is a relative path (e.g., starts with /) if (val.startsWith("/")) return true; // Otherwise validate as an absolute URL try { new URL(val); return true; } catch { return false; } }); relativeUrlSchema.parse("/logo.png"); // ✅ relativeUrlSchema.parse("https://example.com/logo.png"); // ✅
Citations:
- 1: https://zod.dev/api
- 2: Improve
z.url(): supporthostnameandprotocolregexes colinhacks/zod#4367 - 3: z.string().url() seems to accept any string colinhacks/zod#2236
- 4: v4: URL parser does not accept
localhostas a valid hostname colinhacks/zod#4103
Allow relative OG image paths in the SEO schema.
SEO.OGImage is required to be a URL, but the form placeholder and SEO renderer support relative values like /logo.png; replace the URL validator there with one that accepts /-prefixed paths or empty values.
🤖 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 `@web/src/features/system-settings/general/system-info-section.tsx` around
lines 119 - 120, Update the SEO.OGImage schema entry in the system-info
validation definition to accept empty values and paths beginning with “/”,
instead of requiring a full URL. Leave the SEO.SiteURL validation unchanged and
preserve rejection of other invalid OG image values.
📝 变更描述 / Description
本次合并包含以下内容:
Upstream 同步
SEO 功能完善
邀请返利维护
文档站优化
i18n
🚀 变更类型 / Type of change
✅ 提交前检查项 / Checklist
此 PR 由 AI 辅助生成代码。
🤖 Generated with Claude Code
Summary by CodeRabbit