1. 14 Apr, 2026 40 commits
    • erio's avatar
      feat(payment): balance recharge multiplier and refund amount separation · 60a4b931
      erio authored
      - Add balance_recharge_multiplier system setting (e.g. 1.2 = charge 100 get 120)
      - Separate order_amount (credited balance) from pay_amount (actual payment)
      - Refund calculates gateway amount proportionally from pay_amount
      - Frontend shows both amounts in order details, payment status, refund dialog
      - Admin settings UI for configuring recharge multiplier
      60a4b931
    • erio's avatar
      fix: Messages() routing refactor and subscription group test coverage · 8548a130
      erio authored
      - Refactor OpenAI Messages() routing: pre-compute dispatch model using
        resolveOpenAIMessagesDispatchMappedModel + NormalizeOpenAICompatRequestedModel
        instead of try-fail-retry pattern with gin context passing
      - Remove openai_messages_fallback_model context anti-pattern
      - Use effectiveMappedModel directly for forward default mapped model
      - Add 3 subscription group tests covering all branch paths:
        _Blocked (no active subscription → SUBSCRIPTION_REQUIRED),
        _RequiresRepo (nil repo → SUBSCRIPTION_REPOSITORY_UNAVAILABLE),
        _AllowsActiveSubscription (valid subscription → success)
      8548a130
    • erio's avatar
      fix: flaky WebSocket test, usage request queue, and test improvements · 3fa5b8bc
      erio authored
      - Fix flaky WebSocket passthrough test: allow StatusNormalClosure after
        client close instead of requiring NoError (race condition fix)
      - Fix ratelimit 401 test: use PlatformOpenAI instead of PlatformGemini
        for OAuth token cache invalidation scenario (more accurate)
      - Add usageLoadQueue: Anthropic OAuth/setup-token accounts sharing the
        same proxy exit are serialized with 1-2s jitter to prevent upstream 429
      - AccountUsageCell: add module-level usage cache (5min TTL), unmounted
        safety guard, and integrate enqueueUsageRequest for throttled fetching
      3fa5b8bc
    • erio's avatar
      fix: merge general improvements from release branch · 63f539b3
      erio authored
      Backend:
      - gateway_handler: pass subject.UserID instead of int64(0) for user-level routing
      - setting_handler: add missing BalanceLowNotifyRechargeURL to UpdateSettings response
      - openai_gateway_service: use applyAccountStatsCost for account stats pricing integration
      - embed_on: add local file override (data/public/) for embedded frontend assets
      
      Frontend:
      - useTableSelection: add batchUpdate method for batch operations
      - AccountsView: virtual scrolling params, Set-based isSelected, swipe virtualization
      - ProxiesView: add batchUpdate to selection and swipe-select
      - BulkEditAccountModal: fix submit handler to prevent event object as argument
      - SettingsView: move payload construction outside try block
      - i18n: add general translation keys (saved, deleted, view, validation, allowUserRefund)
      - api/client: reorder error fields for consistency
      - stores/payment: clarify pollOrderStatus JSDoc
      63f539b3
    • erio's avatar
      fix: resolve 3 code review issues in allow_user_refund · c14d7393
      erio authored
      1. PrepareRefund: block refund on provider instance lookup failure
         instead of silently skipping permission check (medium severity)
      
      2. UpdateProviderInstance: allow enabling refund_enabled and
         allow_user_refund in the same request by checking req.RefundEnabled
         value before falling back to DB read
      
      3. ExecuteRefund: only revoke subscription on ErrAdjustWouldExpire,
         abort on other errors (DB failure, not found) instead of
         unconditionally revoking
      c14d7393
    • erio's avatar
      fix: merge 30 general improvements from release branch · 6ac8ccde
      erio authored
      Bug fixes:
      - Detached context for GetAccountConcurrencyBatch (prevent all-zero on request cancel)
      - Filter soft-deleted users in GetByGroupID
      - Stripe CSP policy (allow Stripe.js in script-src and frame-src)
      - WebSearch API key validation on save
      - RECHARGING status in payment result success check
      - Windows test fixes (logger Sync deadlock, config path escaping)
      
      Feature enhancements:
      - Webhook multi-instance dispatch (extractOutTradeNo + GetWebhookProvider)
      - EasyPay mobile H5 payment (device param + PayURL2)
      - SSE error propagation in WebSearch emulation
      - AccountStatsCost DTO field for admin usage logs
      - Plans sort by sort_order instead of created_at
      - UsageMapHook for streaming response usage data
      - apicompat Instructions field passthrough
      - EffectiveLoadFactor for ops concurrency/metrics
      - Usage billing RETURNING balance for notify system
      - BulkUpdate mixed channel warning with details
      - println to slog migration in auth cache
      - Wire ProviderSet cleanup
      - CI cache-dependency-path optimization
      
      Frontend:
      - Refund eligibility check per provider (canRequestRefund)
      - Plan sort_order editing
      - Dead code cleanup (simulate_claude_max, client_affinity)
      - GroupsView platform switch guard
      - channels features_config API type
      - UsageView account_stats_cost export
      6ac8ccde
    • erio's avatar
      feat: add per-provider allow_user_refund control and align wildcard matching · f1297a36
      erio authored
      allow_user_refund:
      - Add allow_user_refund field to PaymentProviderInstance ent schema
      - Migration 103: ALTER TABLE payment_provider_instances ADD COLUMN
      - Cascade logic: disabling refund_enabled auto-disables allow_user_refund
      - User refund validation: check provider instance allows user refund
      - Admin refund validation: check provider instance allows admin refund
      - Subscription refund: deduct days on refund, rollback on failure
      - New endpoint: GET /payment/orders/refund-eligible-providers
      - Frontend: ToggleSwitch in ProviderCard/Dialog, cascade in SettingsView
      
      Wildcard matching:
      - Change findPricingForModel from "longest prefix wins" to "config order
        priority (first match wins)", aligning with channel service behavior
      f1297a36
    • erio's avatar
      fix: resolve remaining lint errors for upstream CI · e8ee400a
      erio authored
      - Fix errcheck: brave.go resp.Body.Close, manager_test.go Encode
      - Fix gofmt: payment_config_service.go
      - Fix unused: use shouldFallbackGeminiModel (with modelName param) in handler
      e8ee400a
    • erio's avatar
      fix: resolve upstream CI failures (lint, test, gofmt) · 6a08efee
      erio authored
      - Fix errcheck: handle Write/Encode return values in brave_test.go
      - Fix errcheck: defer resp.Body.Close() with _ assignment in tavily.go
      - Fix gofmt: payment.go, channel.go, payment_config_providers.go
      - Fix unused: remove dead decodeURLValue in easypay.go
      - Restore shouldFallbackGeminiModel function (deleted during cherry-pick)
      - Add missing balanceNotifyService param to NewGatewayService in test
      - Fix platform default test expectation (empty stays empty)
      - Fix wildcard pricing test (longest prefix wins, not config order)
      - Fix subscription group test (SUBSCRIPTION_REPOSITORY_UNAVAILABLE)
      6a08efee
    • erio's avatar
      fix: resolve test compilation errors and restore upstream VERSION · b42f34c3
      erio authored
      - Add missing interface methods to test stubs (RemoveGroupFromUserAllowedGroups,
        GetNotifyCodeUserRate, IncrNotifyCodeUserRate, UpdateGroupIDByUserAndGroup)
      - Fix NewUserService call signatures (add 4th param)
      - Fix GetAccountCount return signature (3 values)
      - Update api_contract_test.go snapshots for balance_notify fields
      - Restore resolveOpenAIMessagesDispatchMappedModel function
      - Reset VERSION to upstream 0.1.112
      b42f34c3
    • erio's avatar
      fix: resolve cherry-pick conflicts and restore compilation · d6965b06
      erio authored
      - Restore gateway_cache.go to upstream (no lua embeds)
      - Restore payment_order.go to upstream (use out_trade_no lookup)
      - Restore payment_fulfillment.go to upstream (same reason)
      - Add FeaturesConfig field and IsWebSearchEmulationEnabled to Channel
      - Add applyAccountStatsCost wrapper function
      - Add SettingKeyWebSearchEmulationConfig constant
      - Add WebSearchEmulationEnabled to SystemSettings
      - Add notify code rate limiting methods to EmailCache interface
      - Remove AllowUserRefund references (ent schema not present)
      - Fix duplicate import in payment_handler.go
      - Fix wire_gen.go argument mismatches
      d6965b06
    • erio's avatar
      test: add unit tests for billing, websearch, and notify systems · 9028d208
      erio authored
      Billing (25 tests):
      - CalculateCostUnified: nil resolver fallback, token/per_request/image modes
      - GetModelPricingWithChannel: nil/partial/full channel overrides
      - resolveAccountStatsCost: four-level priority chain integration tests
      
      WebSearch (18 tests):
      - PopulateWebSearchUsage: nil input, manager states, QuotaLimit nil/*int64
      - ResetWebSearchUsage: nil manager error
      - Manager.ResetUsage: nil Redis
      - shouldEmulateWebSearch: full decision chain (8 scenarios)
      
      Notify (36 tests):
      - ParseNotifyEmails/MarshalNotifyEmails: old/new format, roundtrip
      - crossedDownward: boundary values, threshold semantics
      - checkQuotaDimCrossings: mixed dimensions, disabled/zero skip
      9028d208
    • erio's avatar
      feat: websearch quota enhancements and balance notify hint · 7c729293
      erio authored
      - QuotaLimit changed to *int64 (null=unlimited, >0=limited)
      - Add reset-usage endpoint (POST /admin/settings/web-search-emulation/reset-usage)
      - Show quota usage in header always (collapsed and expanded)
      - Add reset quota button in expanded provider view
      - Quota input: empty=unlimited with ∞ placeholder, must be >0 if set
      - Add email verification hint on balance notify card
      7c729293
    • erio's avatar
      fix: gofmt formatting across all Go source files · 1e6912ea
      erio authored
      1e6912ea
    • erio's avatar
      fix: show websearch API key visibility/copy buttons for saved providers · 9e0d12d3
      erio authored
      The buttons were hidden because v-if only checked provider.api_key,
      which is always empty for saved providers (backend sanitizes it).
      Now also checks api_key_configured. Copy button is disabled when
      no actual key is available (only configured placeholder shown).
      9e0d12d3
    • erio's avatar
      fix: add opportunistic STARTTLS to sendMailPlain for 587 port compatibility · b402c367
      erio authored
      smtp.SendMail automatically upgrades to STARTTLS when the server
      supports it. Our replacement sendMailPlain skipped this, causing
      credentials to be sent in plaintext on port 587. Add STARTTLS
      negotiation before Auth to restore the original security behavior.
      b402c367
    • erio's avatar
      fix: audit round-3 — proxy safety, intervals persistence, SMTP timeout, sort fix · 0a4ece5f
      erio authored
      - Skip websearch provider when ProxyID is set but proxy not found (prevent
        silent direct connection bypass)
      - Fix sortByStableRandomWeight: pair factors with items so sort.Slice swap
        keeps weights aligned
      - Allow empty platform in account_stats_pricing_rules (wildcard matching),
        only force anthropic default for main model_pricing
      - Add channel_account_stats_pricing_intervals table and repo layer support
        for interval-based pricing in account stats rules
      - calculateTokenStatsCost now uses interval pricing when available
      - Replace smtp.SendMail/tls.Dial with net.Dialer timeout (10s dial, 20s IO)
        to prevent goroutine leak on SMTP hang
      - Fix gofmt formatting issues
      - Web Search label: black text with red warning hint
      0a4ece5f
    • erio's avatar
      fix: websearch features_config cleanup and pricing rules validation · 9c09bd19
      erio authored
      - Fix web_search_emulation toggle: explicitly write false for disabled
        platforms instead of leaving stale true from cloned features_config
      - Extract validatePricingEntries from validateChannelConfig for reuse
      - Validate account_stats_pricing_rules[].pricing in both Create and
        Update paths (negative prices, bad intervals, missing per_request price)
      9c09bd19
    • erio's avatar
      fix: round-2 audit fixes — security, code quality, and UI improvements · a9880ee7
      erio authored
      Security (HIGH):
      - Normalize all Redis cache keys to lowercase (verifyCode, passwordReset)
      - Fix verify code TTL renewal on failed attempts: use remaining TTL via
        ExpiresAt field instead of resetting to full 15-minute window
      - Add 3 missing fields to diffSettings audit log (promo_code, invitation_code,
        custom_endpoints)
      
      Code quality (MEDIUM):
      - Extract filterVerifiedEmails shared helper (balance_notify_service.go)
      - Add Pricing array non-empty validation for channel pricing rules
      - Add platform token semantics comment in gateway_service.go
      - Complete validatePlanPatch test coverage (+10 test cases)
      - Replace string types with QuotaThresholdType/QuotaResetMode across frontend
      - Remove duplicate getPlatformTextColor/getRateBadgeClass in ChannelsView
      - Return EMAIL_NOT_FOUND error on RemoveNotifyEmail miss
      
      UI improvements:
      - Reorder cost tooltip: user billing above separator, account billing below
      - Add NaN guard to accountBilled function
      - Move timezone selector inline into reset-mode row (no longer standalone)
      a9880ee7
    • erio's avatar
      fix: address audit findings for websearch, email verification, and pricing · 74f8a30f
      erio authored
      - Fix websearch provider failover: proxy error from provider-specific proxy
        now continues to next provider instead of aborting the entire loop
      - Fix SMTP failure locking users out: send email first, then write cache
        and increment rate counter
      - Fix notify email cache key case sensitivity: normalize to lowercase
      - Add OriginalPrice validation to validatePlanPatch and validatePlanRequired
      - Add empty scope validation for channel pricing rules (group_ids/account_ids)
      - Add platform color to account search dropdown in channel pricing rules
      74f8a30f
    • erio's avatar
      refactor: batch 3 — decompose CheckBalanceAfterDeduction, merge crossing... · 594f0d17
      erio authored
      refactor: batch 3 — decompose CheckBalanceAfterDeduction, merge crossing checks, add QuotaNotifyConfig
      
      M1: CheckBalanceAfterDeduction (63→18 lines) decomposed into:
          canNotifyBalance, resolveUserEffectiveThreshold, crossedDownward, dispatchBalanceLowEmail
      M3: New Account.QuotaNotifyConfig(dim) method replaces 9 hardcoded getters
          (getters kept as thin wrappers for backward compatibility)
      M4: checkQuotaDimCrossings + checkQuotaDimCrossingsFromState merged into one
          function taking pre-built []quotaDim; caller builds dims conditionally
      594f0d17
    • erio's avatar
      fix: batch 1 audit fixes — quota SQL fixed mode, public recharge URL,... · ed8a9d97
      erio authored
      fix: batch 1 audit fixes — quota SQL fixed mode, public recharge URL, WebSearch bool fallback, UpdatePlan validation
      
      H1: incrementUsageBillingAccountQuota now uses shared dailyExpiredExpr/weeklyExpiredExpr
          constants (supporting fixed reset mode) instead of hardcoded '24 hours'/'168 hours'
      H4: public settings endpoint now maps balance_low_notify_recharge_url
      H6: GetWebSearchEmulationMode tolerates legacy bool values (true→enabled)
      H7: UpdatePlan validates non-nil patch fields (rejects negative price, empty name, etc.)
      H8: UsageTable accountBilled() helper with total_cost ?? 0 null guard
      H9: AdminUsageLog TS type adds channel_id + billing_tier
      M2: account.go "fixed" literals replaced with thresholdTypeFixed constant
      M13: SystemSettings TS type adds web_search_emulation_enabled
      UI: QuotaLimitCard title labels now use flex-1 to align with flex-1 input boxes
      ed8a9d97
    • erio's avatar
      test: add 66 unit tests for balance/quota notify + plan validation · ca673f98
      erio authored
      balance_notify_service_test.go (27 tests):
      - resolveBalanceThreshold: fixed/percentage/zero recharged/empty type
      - quotaDim.resolvedThreshold: fixed normal/exceed/equal limit, percentage 0/30/100/>100, zero/negative limit
      - sanitizeEmailHeader: CRLF/CR/LF/clean/empty/multiple newlines
      - buildQuotaDims / buildQuotaDimsFromState: all dimensions, empty extra, state-vs-account precedence
      - collectBalanceNotifyRecipients: empty, filter disabled/unverified, case-insensitive dedup, skip empty, trim
      
      balance_notify_check_test.go (16 tests):
      - CheckBalanceAfterDeduction guard clauses: nil user/disabled/global-off/threshold=0/user-override/no-crossing
      - CheckAccountQuotaAfterIncrement guards: nil account/zero cost/negative cost/global-disabled
      - getBalanceNotifyConfig: all fields, disabled, invalid threshold
      - isAccountQuotaNotifyEnabled: missing/false/true
      - getSiteName: default fallback + configured
      
      balance_notify_email_body_test.go (10 tests):
      - Guards against fmt.Sprintf arg-count mismatches in email templates
      - Verifies HTML escaping of recharge URL
      - Verifies CSS %% escape produces literal % in output
      - Verifies unlimited/percentage/over-quota display branches
      
      payment_config_plans_validation_test.go (13 tests):
      - validatePlanRequired: all 5 validation branches + whitespace handling
      ca673f98
    • erio's avatar
      fix(accounts): unify modal width, add notify props to create, fix quota layout · a43da622
      erio authored
      - EditAccountModal width changed from "normal" to "wide" (match CreateAccountModal)
      - CreateAccountModal now passes all quota notify props to QuotaLimitCard
      - QuotaLimitCard: when global notify disabled, hide title row, input takes full width
      - Quota alert email: show remaining quota + threshold (fixed/$, percentage/%) instead of usage trigger point
      a43da622
    • erio's avatar
      feat(notify): add platform/ID to quota alert email, add recharge URL to balance alert · c1eb79e4
      erio authored
      - Quota alert email now shows account ID and platform
      - Balance low email includes a "Top Up Now" button when recharge URL is configured
      - New setting: balance_low_notify_recharge_url in admin settings
      c1eb79e4
    • erio's avatar
      fix: change quota notify threshold semantics to "remaining quota" · 216bda58
      erio authored
      Threshold now represents remaining quota instead of usage amount:
      - Fixed ($): threshold=400, limit=1000 → alert when remaining drops to $400
        (i.e., usage reaches $600)
      - Percentage (%): threshold=30%, limit=1000 → alert when remaining drops
        to 30% (i.e., usage reaches $700)
      
      Also:
      - Rename 告警阈值 → 提醒阈值 in i18n
      - Widen type dropdown to w-16 for proper $ / % display
      216bda58
    • erio's avatar
      fix: correct account stats pricing priority order · 98c9d517
      erio authored
      Priority was wrong:
      - Before: custom rules → LiteLLM (when ApplyPricingToAccountStats) → nil
      - After:  custom rules → totalCost (when ApplyPricingToAccountStats) → LiteLLM → nil
      
      When ApplyPricingToAccountStats is enabled, use the request's actual
      client billing cost (before multiplier) as account_stats_cost, instead
      of recalculating from LiteLLM per-token prices which produced incorrect
      values for per-request billing mode.
      
      LiteLLM model pricing is now the final fallback (priority 3), used only
      when neither custom rules nor ApplyPricingToAccountStats apply.
      98c9d517
    • erio's avatar
      fix: round 3 audit fixes - SMTP header sanitization and goroutine safety · b1875f0b
      erio authored
      - Move sanitizeEmailHeader to SendEmailWithConfig entry point, covering all
        email senders (verify code, password reset, ops alerts, notifications)
      - Add panic recovery to UpdateBalance goroutine
      - Fix stale comment in getAccountQuotaNotifyEmails (email="" no longer used)
      - Log error instead of silently discarding verifyNotifyCode cache update failure
      b1875f0b
    • erio's avatar
      fix: audit fixes for websearch, notifications, and channel pricing · b7fb2e43
      erio authored
      P0: fix wildcard matching test assertion (config order, not longest prefix)
      P0: add TotalRecharged to auth cache snapshot (v5) for percentage threshold
      P1: move pricing rules into per-platform sections in ChannelsView
      P1: populate account name cache when editing existing channel rules
      P1: sanitize email subject headers to prevent SMTP injection
      P1: make Redis INCR+EXPIRE idempotent for rate limiting
      P1: deep copy FeaturesConfig in Channel.Clone()
      P2: clean up stale email="" placeholder comments
      P2: replace log.Printf with slog in email_service.go
      b7fb2e43
    • erio's avatar
      fix: address audit findings across websearch, notify, and channel pricing · a68df457
      erio authored
      Backend fixes:
      - Fix balance notify ignoring percentage threshold type (was treating
        percentage value as fixed USD amount)
      - Remove dead code parseJSONStringArray
      - Add ImageOutputTokens to tryModelFilePricing calculation
      - Unify zero-value check: cost == 0 → cost <= 0 in calculateTokenStatsCost
      - Use MarshalNotifyEmails instead of json.Marshal for consistency
      - Rename quotaDim.oldUsed → currentUsed for clarity
      - Extract HTML email templates to const variables (function ≤30 lines)
      
      Test fixes:
      - Rewrite account_websearch_test.go for GetWebSearchEmulationMode tri-state
      - Add 6 tryModelFilePricing test cases
      
      Frontend fixes:
      - Replace hardcoded '未命名' with i18n key
      - Extract getBillingModeLabel/getBillingModeBadgeClass to shared utils
      - Replace inline type with imported NotifyEmailEntry
      - Pass platform to AccountStats pricing rules via inferRulePlatform()
      - Add billing mode constants (BILLING_MODE_TOKEN/PER_REQUEST/IMAGE)
      a68df457
    • erio's avatar
      feat: WebSearch tri-state, account stats pricing fix, quota cache fix, usage tooltip · 1262654d
      erio authored
      WebSearch tri-state switch:
      - Account-level web_search_emulation changed from bool to tri-state
        string: "default" (follow channel) / "enabled" / "disabled"
      - shouldEmulateWebSearch checks channel config when account is "default"
      - SQL migration converts old bool values
      - Frontend select replaces toggle in Edit/CreateAccountModal
      
      Account stats pricing:
      - resolveAccountStatsCost uses upstream model (post-mapping) for matching
      - Priority: custom rules → model pricing file (when toggle on) → default
      - Custom rules always configurable, independent of toggle
      - Account ID field changed to searchable selector filtered by platform
      - Description updated to reflect new behavior
      
      Quota notification cache fix:
      - CheckAccountQuotaAfterIncrement fetches real-time account from DB
      - Reconstructs pre-increment usage for accurate threshold crossing detection
      - New AccountQuotaReader interface (minimal: GetByID only)
      
      Usage tooltip:
      - Per-request/image billing shows per-request price instead of $0 token price
      - Token billing continues to show input/output price per million tokens
      1262654d
    • erio's avatar
      fix(channel): use upstream model for account stats pricing and remove channel pricing fallback · 11c46068
      erio authored
      - resolveAccountStatsCost now uses the final upstream model (after
        account-level mapping) to match custom pricing rules, fixing the
        issue where requested model (e.g. claude-sonnet-4-5) didn't match
        rules configured for upstream model (e.g. claude-opus-4-6)
      - Remove tryChannelPricing fallback — only custom rules are applied,
        unmatched requests use default formula (total_cost × rate)
      - Remove unused billingService and serviceTier parameters
      - Update description: "启用后将支持自定义账号统计的模型价格"
      11c46068
    • erio's avatar
      fix(notify): add verification flow for saved unverified emails · 95f9b27e
      erio authored
      - Add "verify" button next to saved unverified emails in
        ProfileBalanceNotifyCard (send code → enter code → verify)
      - Backend: VerifyAndAddNotifyEmail now marks existing unverified
        emails as verified instead of returning "already exists"
      - Inline verification UI with countdown timer and resend button
      95f9b27e
    • erio's avatar
      fix(notify): use real-time balance for crossing detection and simplify email logic · 31550a2c
      erio authored
      - Fix cached balance causing threshold crossing to never trigger:
        read real-time balance from billingCacheService instead of stale
        API key auth snapshot
      - Remove email="" placeholder concept; all emails are user-managed
      - Only send notifications to verified && non-disabled emails
      - Frontend: pre-fill user's email in add input when list is empty
      - Remove FilterEnabledEmails/IsPrimaryDisabled helpers (no longer needed)
      31550a2c
    • erio's avatar
      feat(notify): convert email lists to NotifyEmailEntry struct with toggle support · 915b7a4a
      erio authored
      - Change balance_notify_extra_emails and account_quota_notify_emails
        from []string to []NotifyEmailEntry{email, disabled, verified}
      - Add per-email enable/disable toggle for both user and admin notifications
      - Add PUT /user/notify-email/toggle API endpoint
      - Fix critical bug: API key auth cache snapshot missing balance notify
        fields (Email, Username, BalanceNotifyEnabled, etc.), causing
        notifications to never fire on cached request paths
      - Bump cache snapshot version 3→4 to invalidate stale entries
      - Add SQL migration 104 to convert old format data
      - Backward compatible: parseNotifyEmails auto-detects old/new format
      - User balance notify: max 3 emails (primary + 2 extra)
      - Admin quota notify: unlimited emails, each with toggle
      915b7a4a
    • erio's avatar
      feat(notify): improve balance notify card UX · 81287e96
      erio authored
      - Show system default threshold as placeholder in custom threshold input
      - Display user's primary email with "Primary" badge
      - Support adding multiple pending emails before verification
      - Each pending email has independent send/verify/resend flow
      - Expose balance_low_notify_threshold in PublicSettings API
      - Clean up timers on unmount to prevent leaks
      81287e96
    • erio's avatar
      5df73099
    • erio's avatar
      fix: address audit findings for notify, websearch and security · 4e96a6fa
      erio authored
      - Fix GetByKeyForAuth missing user.FieldEmail and user.FieldUsername (notifications sent to empty address)
      - Guard against empty email in collectBalanceNotifyRecipients
      - Remove non-atomic TotalRecharged read-modify-write in admin balance adjustment
      - HTML-escape userName/siteName/accountName in notification email templates
      - Fix timer leak in ProfileBalanceNotifyCard (add onUnmounted cleanup)
      - Add warning log on websearch proxy URL resolution failure
      4e96a6fa
    • erio's avatar
      feat(notify): add global toggles, percentage threshold, and visibility control · eba289a7
      erio authored
      - Add global toggle for account quota notification in admin settings
      - Add percentage-based threshold type for per-account quota alerts
      - Hide balance notify card on user profile when global toggle is off
      - Expose balance_low_notify_enabled and account_quota_notify_enabled in PublicSettings
      - Add threshold type (fixed/percentage) to QuotaNotifyToggle with $ / % switcher
      eba289a7
    • erio's avatar
      fix(notify): remove percentage threshold from balance notification · cef22c70
      erio authored
      Balance low notification only supports fixed USD amount threshold.
      Percentage threshold is a quota concept, not applicable to balance.
      Reverted threshold_type from admin settings, user profile, and all
      backend/frontend layers. DB fields (balance_notify_threshold_type,
      total_recharged) retained for potential future quota use.
      cef22c70