Commit Graph
3736 Commits
Author SHA1 Message Date
erio 3fdb63e2b6 fix: add opportunistic STARTTLS to sendMailPlain for 587 port compatibility
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.
2026-04-14 01:38:42 +08:00
erio 325d8eb2ae chore: bump version to 0.1.110.55 2026-04-14 01:11:13 +08:00
erio 541dd3b467 fix: audit round-3 — proxy safety, intervals persistence, SMTP timeout, sort fix
- 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
2026-04-14 01:10:46 +08:00
erio b1c64dcdda chore: bump version to 0.1.110.54 2026-04-14 00:43:09 +08:00
erio 6d61455232 fix: websearch features_config cleanup and pricing rules validation
- 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)
2026-04-14 00:42:40 +08:00
erio 61f702ddab chore: bump version to 0.1.110.53 2026-04-14 00:26:49 +08:00
erio e5e26d73d6 fix: round-2 audit fixes — security, code quality, and UI improvements
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)
2026-04-14 00:26:20 +08:00
erio b3a2611d67 chore: bump version to 0.1.110.52 2026-04-13 23:36:27 +08:00
erio 931fcb50f8 fix: address audit findings for websearch, email verification, and pricing
- 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
2026-04-13 23:35:59 +08:00
erio c664e67018 refactor: M5 useQuotaNotifyState composable + H14 Vue file splits
M5: New composable frontend/src/composables/useQuotaNotifyState.ts
  - Replaces 9 individual refs in both Create/Edit modals with reactive state
  - Provides loadFromExtra/writeToExtra/reset helpers
  - Eliminates ~120 lines of duplicated code across the two modals

H14: Vue file length violations fixed
  - AdminPaymentPlansView.vue: 325 → 183 lines (extracted PlanEditDialog.vue)
  - QuotaLimitCard.vue: 327 → 268 lines (extracted QuotaDimensionRow.vue)
  - PlanEditDialog.vue: 181 lines (new, plan create/edit form)
  - QuotaDimensionRow.vue: 108 lines (new, single quota dimension row)
2026-04-13 22:35:24 +08:00
erio 7a5ec1a17a refactor: M6 extract applyAccountStatsCost helper, M11 fix postUsageBilling double-queue
M6: New applyAccountStatsCost() helper in account_stats_pricing.go replaces
    identical upstream-model-fallback + resolveAccountStatsCost() call sites
    in both gateway_service.go and openai_gateway_service.go

M11: postUsageBilling legacy path no longer calls finalizePostUsageBilling,
    fixing the double-queue bug (QueueDeductBalance was called twice if the
    fallback path was ever reached). Added NOTE comment documenting the split.
2026-04-13 22:22:05 +08:00
erio 4213688485 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
2026-04-13 22:02:18 +08:00
erio 939902f998 fix: batch 2 audit fixes — diffSettings notify fields, slog migration, frontend constants
H5: diffSettings now tracks 5 balance/quota notify fields in audit log
M15: log.Printf audit log migrated to slog.Info, removed "log" import
M14: New frontend/src/constants/account.ts with shared constants
     QuotaNotifyToggle.vue uses QUOTA_THRESHOLD_TYPE_FIXED/PERCENTAGE
L2: UsageTable.vue uses BILLING_MODE_TOKEN/IMAGE from billingMode.ts
2026-04-13 21:54:01 +08:00
erio 563339dedb 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
2026-04-13 21:41:02 +08:00
erio 66ccd17492 fix(quota-card): balance input widths, inline reset hint, timezone GMT offset
- Limit input and notify threshold now share flex-1 equally (was w-28 fixed)
- Reset schedule hint text moved inline after the selectors (same row)
- Timezone dropdown shows GMT offset, e.g. "Asia/Shanghai (GMT+8)"
2026-04-13 20:45:12 +08:00
erio 69971098af fix(channels): apply platform color to rule group/account chips
In "custom account stats pricing rule" section, group checkbox labels and
selected account chips now use the platform text color (matching the top
"associated groups" header), making it visually consistent across the modal.
2026-04-13 20:39:06 +08:00
erio 1701b32e2c test: add 66 unit tests for balance/quota notify + plan validation
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
2026-04-13 20:35:38 +08:00
erio fea35fbfb2 fix(accounts): unify modal width, add notify props to create, fix quota layout
- 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
2026-04-13 20:01:25 +08:00
erio b0305fef11 fix: audit findings - PUT response rechargeURL, NaN guard, debug logs
- Add BalanceLowNotifyRechargeURL to admin PUT response (fixes save-then-stale)
- Add ?? 1 guard for account_rate_multiplier in UsageTable else branch
- Downgrade high-frequency notify logs from Info to Debug
- Extract "Sub2API" magic string to defaultSiteName constant
2026-04-13 19:45:45 +08:00
erio 6307aff1d3 fix(notify): add recharge URL to admin settings GET response 2026-04-13 19:02:40 +08:00
erio 1621f8ae9e fix(notify): write back auto-filled recharge URL to form on save 2026-04-13 18:52:02 +08:00
erio 1d13c2d2ba fix(notify): auto-fill recharge URL with current origin when empty 2026-04-13 18:46:56 +08:00
erio 6c2c17d17e feat(notify): add platform/ID to quota alert email, add recharge URL to balance alert
- 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
2026-04-13 18:39:45 +08:00
erio 78df126437 fix(ui): widen notify type dropdown to show % fully, align quota input widths 2026-04-13 18:23:20 +08:00
erio d6fe6da831 fix(plans): frontend validation for price > 0 and validity_days >= 1 2026-04-13 18:18:29 +08:00
erio f5e386e330 fix(plans): require price > 0 for subscription plans 2026-04-13 18:12:57 +08:00
erio 5277f33155 fix(plans): subscription plan page validation, sorting, and toFixed error
- Fix toFixed crash when price is undefined
- Remove sort_order field, order by created_at instead
- Fix group selector showing blank when nothing selected
- Add required field markers (*) and backend validation
- Add i18n groupRequired key for both zh/en
2026-04-13 18:08:06 +08:00
erio d96cbe4f21 chore: bump version to 0.1.110.34 2026-04-13 17:38:34 +08:00
erio a03ffc8c99 fix: change quota notify threshold semantics to "remaining quota"
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
2026-04-13 17:38:33 +08:00
erio f6622eaec7 chore: bump version to 0.1.110.33 2026-04-13 17:27:57 +08:00
erio 4a2ef0b538 fix(frontend): quota card layout - title row + input row, fix $ dropdown
- Separate title row (日限额 + 告警阈值) from input row
- Widen limit input to w-32 for better alignment with title
- Remove duplicate label from QuotaNotifyToggle (now in title row)
- Fix dropdown to always show $ or % as selected value
2026-04-13 17:27:57 +08:00
erio 4b097a6428 chore: bump version to 0.1.110.32 2026-04-13 17:20:25 +08:00
erio 879f88afe5 fix(frontend): compact quota card - inline labels, dropdown type selector
- Move dimension labels (日限额/周限额/总限额) inline with input row
- Replace $ / % toggle buttons with a compact dropdown select
- Reduce limit input width (w-28) to give more space for notify toggle
- Overall card height significantly reduced
2026-04-13 17:20:25 +08:00
erio 93241a30b9 chore: bump version to 0.1.110.31 2026-04-13 17:12:20 +08:00
erio 3ef3d6f34e fix(frontend): place quota notify toggle inline with limit input
Move QuotaNotifyToggle to the same row as the limit $ input for all
three dimensions (daily/weekly/total), significantly reducing card height.
2026-04-13 17:12:20 +08:00
erio 2c1bea37f9 chore: bump version to 0.1.110.30 2026-04-13 17:04:23 +08:00
erio 65ef8aceaf fix(frontend): collapsible quota card and compact notify layout
- QuotaLimitCard: add collapse/expand toggle (chevron icon + click header)
- QuotaNotifyToggle: show $ or % suffix in threshold input
- Reduce vertical spacing between reset mode hint and notify toggle
2026-04-13 17:04:23 +08:00
erio 7d17e978ed chore: bump version to 0.1.110.29 2026-04-13 16:57:53 +08:00
erio 20da7a8d07 perf: run balance/quota notification checks async
Move notifyBalanceLow and notifyAccountQuota to goroutines so the
threshold checking logic (DB settings reads, threshold calculation,
crossing detection) no longer blocks the request thread. Both are
fire-and-forget with panic recovery and log-only error handling.
2026-04-13 16:57:53 +08:00
erio 0ab0a01360 chore: bump version to 0.1.110.28 2026-04-13 16:52:02 +08:00
erio 8183ce79a3 fix(frontend): quota notify UI improvements
- QuotaNotifyToggle: add $ or % suffix to threshold input based on type
- QuotaLimitCard: combine reset mode and notify toggle on same row
  to reduce vertical height for daily/weekly sections
- Remove redundant ml-4 indentation from QuotaNotifyToggle
2026-04-13 16:52:02 +08:00
erio 12f923100b chore: bump version to 0.1.110.27 2026-04-13 16:45:10 +08:00
erio 261ea08892 fix: correct account stats pricing priority order
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.
2026-04-13 16:45:10 +08:00
erio c6a9da4ea9 debug: add notification path logging for beta investigation
Add slog.Info/Debug logs to notifyBalanceLow, notifyAccountQuota,
CheckBalanceAfterDeduction, and sendEmails to diagnose why balance
and quota notifications are not being sent in beta environment.
2026-04-13 16:20:07 +08:00
erio 49131efb42 chore: bump version to 0.1.110.25 2026-04-13 16:00:52 +08:00
erio 9c9945071f fix: expose account_stats_cost in usage log API and fix frontend display
Backend:
- Add AccountStatsCost field to AdminUsageLog DTO
- Map AccountStatsCost in UsageLogFromServiceAdmin mapper
- Replace println with slog.Warn in api_key_auth_cache_impl.go

Frontend:
- Add account_stats_cost to AdminUsageLog TypeScript interface
- Usage tooltip and table: prefer account_stats_cost when available,
  fallback to total_cost * multiplier (mirrors SQL COALESCE logic)
- Excel export: same COALESCE fallback
2026-04-13 16:00:52 +08:00
erio 2ec9e345f6 style: fix gofmt alignment in InstanceSelection struct 2026-04-13 15:51:11 +08:00
erio 71e598a5bd chore: bump version to 0.1.110.24 2026-04-13 15:30:06 +08:00
erio 068407d884 fix: add missing AccountQuotaNotifyEnabled to admin settings API
The field was present in SystemSettings response DTO and service layer
but missing from:
- UpdateSettingsRequest (admin handler) - saves were silently ignored
- GET/PUT response mapping in admin handler
- UpdateSettingsRequest (non-admin dto)

This caused the toggle to always revert to off after saving.
2026-04-13 15:30:06 +08:00
erio 76c5043db7 fix(frontend): simplify websearch select labels and reduce width
- "默认(跟随渠道)" → "默认", "Default (follow channel)" → "Default"
- Move "follows channel config" info to description text
- Reduce select width from w-32 to w-24 in both Edit and Create modals
2026-04-13 15:20:00 +08:00