- Fix struct field alignment in dto/types.go, settings_view.go,
sora_generation_service.go for gofmt compliance
- Remove embedded field "Account" from selector in account_handler.go (QF1008)
- Remove unused settingService field from SoraGDriveOAuthService
- Replace deprecated google.CredentialsFromJSON with CredentialsFromJSONWithParams (SA1019)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add colored circle badge showing affinity client count before the account
status indicator. Badge color reflects count severity (gray=0, green=1-5,
yellow=6-15, red=16+). Tooltip shows full client ID list on hover.
Backend: AccountHandler batch queries reverse affinity index via
GetAccountAffinityClientsBatch, returns affinity_client_count and
affinity_clients in DTO. GatewayCache interface extended with the new
batch method backed by a Lua script (get_affinity_clients.lua).
All test mocks synchronized with updated GatewayCache interface.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove RemoveClientAffinity from GatewayCache interface, implementation,
Lua script, and all test mocks (never called in production code)
- Add 26 unit tests for affinity scheduling: filterByMinAffinityCount,
populateAffinityCounts, Layer 1/2 sort chain integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Separate clientAffinityTTL (24h) from stickySessionTTL (1h)
- Atomic Lua dual-write for forward (client→accounts) and reverse
(account→clients) affinity indexes with lazy expiry cleanup
- Extract Lua scripts to standalone .lua files with //go:embed
- Add filterByMinAffinityCount to Layer 1 & 2 scheduling so new
clients are routed to accounts with fewest existing affinities
- Skip Redis query when no affinity-enabled accounts exist (zero
overhead for non-affinity users)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The client affinity setting was only available when editing an account.
Now it's also available during account creation for all Anthropic
platform types (OAuth, Setup Token, API Key).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move storage provider selection (S3/GDrive) from separate modal into
the drawer itself, shown as the first step when creating a new profile
- Provider type is locked once selected (read-only badge in header)
- Remove default_storage_quota_gb field from form and table column
- Remove unused formatStorageQuotaGB function and providerSelectOpen ref
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add quota_daily_limit/used and quota_weekly_limit/used to DTO and
frontend Account type
- AccountCapacityCell shows D (daily), W (weekly), and $ (total) badges
with color-coded status (green/yellow/red)
- AccountActionMenu shows reset button when any quota dimension is set
- Extract quotaBadgeClass/quotaBadgeTooltip as shared functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Split compound "Profile" column into separate Profile ID and Name columns
- Replace "Endpoint" column with "Storage Path" showing bucket/prefix or folder info
- Add GDrive quota display (capacity/used) via new backend API endpoint
- Add video count statistics per storage type via new backend API endpoint
- Add inline test button per profile with 15s timeout and per-profile loading state
- Backend: GetQuotaInfo, CountByStorageType, GetGDriveQuota, GetStorageVideoStats
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace single-period-selector (三选一) with three independently configurable
quota dimensions (日/周/总) that can all be set simultaneously. Account is
paused when ANY dimension is exceeded. Periodic quotas (daily/weekly) use
lazy reset via atomic SQL — no cron job needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The OAuth flow opens a popup window that redirects to /admin/gdrive-oauth-callback,
but this route and page were missing. Add the callback view that extracts the
authorization code from the URL and sends it back to the opener window via
postMessage, completing the OAuth authorization flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Define SoraObjectStorage interface abstracting S3 and GDrive backends
- Implement SoraGDriveStorage with OAuth2 and Service Account auth
- Add SoraStorageRouter to route requests based on active profile provider
- Add GDrive OAuth handler for authorization flow (start + callback)
- Extend profile model with provider, auth_type, and GDrive-specific fields
- Update frontend UI with provider selection, dynamic form fields, and i18n
- Migrate API paths from /sora-s3 to /sora-storage (old paths preserved)
- All existing S3 functionality remains backward compatible
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CTE snapshot in READ COMMITTED can cause concurrent requests during
period reset to lose one increment. Inline CASE in single UPDATE
ensures PostgreSQL re-evaluates all expressions with latest row data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use NOW() AT TIME ZONE 'UTC' to ensure consistent timestamps
regardless of database timezone setting
- Clear quota_period_start when manually resetting quota so a
fresh period starts from next usage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support daily/weekly automatic quota reset using a rolling period
anchored to first usage. Implements lazy reset via atomic CTE SQL
in IncrementQuotaUsed - no cron job needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove frontend badge/tooltip and backend counting/query infrastructure
for client affinity. Core client affinity scheduling logic is preserved.
Removed:
- GatewayCache injection in AccountHandler
- GetAccountAffinityCount/Batch/Clients methods and interface
- AffinityClientInfo struct and ClientAffinityTTL function
- /affinity-clients API endpoint and route
- Reverse index (account_affinity:*) Lua scripts
- Frontend blue badge, tooltip, and lazy-load logic
- affinity_client_count field from types and response
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR #723 changed all OAuth 401 to use SetTempUnschedulable instead of SetError,
and PR #761 added 401 escalation logic in tryTempUnschedulable. Both caused
non-OpenAI accounts to frequently become temporarily unschedulable.
- HandleUpstreamError case 401: limit temp-unschedulable to OpenAI OAuth only,
other platforms (Anthropic/Gemini/Antigravity) revert to original SetError
- tryTempUnschedulable: skip 401 escalation for Antigravity, let its
applyErrorPolicy temp_unschedulable_rules handle 401 naturally
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reverse index account_affinity:{accountID} was using the same 1h
EXPIRE as the forward index. But the forward key EXPIRE refreshes on
every client request, while the reverse key only refreshes when a
request is routed to that specific account. This caused reverse keys
to expire prematurely.
Fix: set reverse key EXPIRE to 24h as safety net, rely on
ZREMRANGEBYSCORE for member-level expiration. Also clean up empty
keys after member cleanup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: expand IsClientAffinityEnabled() to support all Anthropic
accounts (OAuth/SetupToken/APIKey), not just OAuth/SetupToken
- Frontend: show client affinity toggle for all Anthropic accounts
- Frontend: fix save logic to properly chain extra field updates
- Antigravity accounts explicitly excluded from client affinity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: expand IsClientAffinityEnabled() to support all Anthropic
accounts (OAuth/SetupToken/APIKey), not just OAuth/SetupToken
- Frontend: show client affinity toggle for all Anthropic accounts
- Frontend: fix save logic to properly chain extra field updates
- Antigravity accounts explicitly excluded from client affinity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: remove count > 0 filter in GetAccountAffinityCountBatch so
accounts with 0 bound clients are included in the response
- Frontend: change badge condition from affinityClientCount > 0 to
account.client_affinity_enabled so the badge always shows for accounts
with affinity enabled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add reverse Redis index (account_affinity:{accountID}) to track which
clients are bound to each account via affinity scheduling. Display a
blue badge with client count in the admin account list, with a hover
tooltip showing client details (groupID:clientID + TTL).
Backend: reverse index in Lua scripts, batch query methods, new
GET /accounts/:id/affinity-clients endpoint.
Frontend: blue badge + lazy-loaded tooltip in AccountStatusIndicator.