Admins often need another account with the same provider and routing configuration. Duplicate on the server so credentials never return to the browser, preserve exact group priorities atomically, start the copy paused, and recover the same copy after ambiguous idempotency-store failures.
Constraint: Admin account responses redact credentials, so duplication must remain server-side
Constraint: OAuth and setup-token credentials rotate and must not be shared across account rows
Rejected: Copy raw account JSON to the clipboard | exposes credentials outside the server
Rejected: Duplicate rotating credentials | account-scoped refresh locks can race token rotation
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep copies paused, avoid automatic upstream probes, and exclude rotating credential types unless token ownership is redesigned
Tested: Targeted Go tests, Go vet, server build; frontend lint, typecheck, Vitest suite, production build; integration test compiled
Not-tested: Docker-backed PostgreSQL execution because Docker is unavailable
Related: Wei-Shaw/sub2api#1379
Related: Wei-Shaw/sub2api#2928
Add a toggleable group ID column in /admin/groups column settings.
It shows the group id (e.g. #1), is hidden by default for new and
existing admins via column-settings version migration, and remains
sortable against the existing backend sort_by=id support.
Expose API key ID in the keys table column settings so users can show
or hide it. Hidden by default; bump column-settings version so existing
preferences also keep ID hidden until toggled on.
Expose the active weekly subscription window through /v1/usage, calculate offsets with the same normalized page size used by queries, and keep user-facing date ranges on the browser's local calendar date.
Constraint: Preserve existing response fields and avoid new dependencies
Rejected: Keep duplicate inline date formatters | a shared local-date utility prevents the same UTC regression in both views
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Offset and Limit based on the same normalized page size
Tested: go test ./internal/pkg/pagination ./internal/handler; go vet ./internal/pkg/pagination ./internal/handler; frontend 923 tests; pnpm typecheck; pnpm lint:check; pnpm build
Not-tested: Live API request against a deployed subscription
Related: #4121
Resolve conflict in api_key_repo.go: keep both ListAllByUserID (this PR)
and attachLastUsedIPs/latestUsageLogIPs (main), and have ListAllByUserID
attach last-used IPs so the current_concurrency sort path keeps the
last_used_ip column populated.
Follow-up fixes for the #3775 audit findings:
- Bill Grok video generation per second of output, matching the xAI rate
card: parse the request duration (1-15s, upstream default 8s) and compute
cost as per-second price x duration x count. The built-in rate card values
were already xAI per-second prices but were previously charged per video,
undercharging up to 15x with a user-controlled duration.
- Group video_price_* fields are now documented and surfaced as per-second
rates (USD/s); admin UI labels, placeholders and hints updated accordingly.
- Persist video_count/video_resolution/video_duration_seconds on usage_logs
(migration 172) so video billing is auditable, and exempt any row with
video_count > 0 from the image_size check constraint: a video billed via a
token-mode channel price produces billing_mode='token' with image_count=1
and no image_size, which the previous constraint rejected, dropping the
whole billing transaction.
- Only refetch the group in apiKeyWithFreshGroupMediaPricing when the group
object actually looks like it is missing media pricing fields (both media
multipliers zero and all prices nil, impossible for a normally loaded
group), removing a per-usage DB query for groups without overrides.
- Frontend: drop the unused admin.groups.mediaPricing locale block, map
cleared price inputs to null (create) / -1 (update, cleared via backend
normalizePrice) instead of sending "" that failed *float64 unmarshalling,
and align video price placeholders with the text-to-video default model
(grok-imagine-video 0.05/0.07, 1080p only on 1.5 at 0.25).