Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
30bf59a
chore: add guarded usage privacy patch
lien0219 Aug 1, 2026
72555ce
ci: apply and verify usage privacy feature
lien0219 Aug 1, 2026
721abfc
ci: persist usage privacy diagnostics
lien0219 Aug 1, 2026
2fb6fad
ci: run usage privacy patch on pull request
lien0219 Aug 1, 2026
35008cc
ci: record usage privacy failure diagnostics
github-actions[bot] Aug 1, 2026
18f54cc
ci: record usage privacy failure diagnostics
github-actions[bot] Aug 1, 2026
76b9541
fix: make usage privacy patch spacing tolerant
lien0219 Aug 1, 2026
ea96a4a
ci: apply usage privacy patch fixer
lien0219 Aug 1, 2026
c2dfac8
ci: serialize usage privacy verification on pull request
lien0219 Aug 1, 2026
2ca0836
ci: record usage privacy failure diagnostics
github-actions[bot] Aug 1, 2026
0845676
fix: preserve existing usage log type contracts
lien0219 Aug 1, 2026
7000542
ci: record usage privacy failure diagnostics
github-actions[bot] Aug 1, 2026
3faa89b
test: cover user billing detail visibility switches
lien0219 Aug 1, 2026
95218c8
ci: run focused usage privacy frontend tests
lien0219 Aug 1, 2026
d92a732
feat: control and redact user usage billing details
github-actions[bot] Aug 1, 2026
0b1bd0c
ci: restore audit checker removed by feature verification
lien0219 Aug 1, 2026
71243fc
fix: restore audit exception checker
github-actions[bot] Aug 1, 2026
b344d5c
test: document usage privacy visibility contract
lien0219 Aug 1, 2026
30db837
chore: add guarded privacy hardening patch
lien0219 Aug 1, 2026
faab156
ci: verify privacy hardening and security upgrade
lien0219 Aug 1, 2026
7b92737
ci: run privacy hardening verification on pull request
lien0219 Aug 1, 2026
dc1889d
fix: harden usage billing privacy controls
github-actions[bot] Aug 1, 2026
071b7e9
ci: restore tracked audit snapshot after verification
lien0219 Aug 1, 2026
d88758c
fix: restore tracked frontend audit snapshot
github-actions[bot] Aug 1, 2026
7baaeb1
test: cover nested multiplier redaction
lien0219 Aug 1, 2026
bee7e97
chore: add guarded API contract update
lien0219 Aug 1, 2026
2998537
ci: verify updated usage privacy API contracts
lien0219 Aug 1, 2026
bb955db
ci: run usage privacy contract update on pull request
lien0219 Aug 1, 2026
ea4e6ef
test: update admin settings API contracts
github-actions[bot] Aug 1, 2026
1cd0240
test: cover independent usage privacy switches
lien0219 Aug 1, 2026
ce07ce2
feat: control and redact user usage billing details (#9)
lien0219 Aug 1, 2026
56a2b22
style: hide homepage GitHub entry
lien0219 Aug 1, 2026
4ea43cf
style: load homepage footer overrides
lien0219 Aug 1, 2026
7222ebd
feat(home): add interactive 3D service hero scene
lien0219 Aug 1, 2026
e611bcb
feat(home): redesign official landing page with dual-theme interactions
lien0219 Aug 1, 2026
0d0d1b2
fix(home): align dark mode with project theme
lien0219 Aug 1, 2026
21dc6dd
fix(home): remove dark mode haze and improve contrast
lien0219 Aug 1, 2026
765eb0b
fix(home): remove dark full-page decorative layer
lien0219 Aug 1, 2026
c04ad25
fix(home): restore dark contrast and scroll-linked navigation
lien0219 Aug 1, 2026
0fea882
fix(home): enforce dark content hierarchy and preserve scroll effects
lien0219 Aug 1, 2026
8204921
fix(home): prevent authenticated modal backdrops on public pages
lien0219 Aug 1, 2026
f8ca73b
fix(announcement): clean up popup backdrop state on close
lien0219 Aug 1, 2026
25ee1d5
fix(home): guard public homepage from orphan overlays
lien0219 Aug 1, 2026
a5f78d1
fix(home): force dark homepage above stale backdrops
lien0219 Aug 1, 2026
4ab01a1
fix(home): activate route-level overlay guard
lien0219 Aug 1, 2026
b815ad8
feat(home): add pointer-driven 3D card motion
lien0219 Aug 1, 2026
0eee51e
feat(home): enrich 3D scene and page animations
lien0219 Aug 1, 2026
9122964
feat(home): activate enhanced homepage motion
lien0219 Aug 1, 2026
9b27eed
feat(home): add resilient Three.js runtime loader
lien0219 Aug 1, 2026
a4bba47
feat(home): style Three.js ambient background
lien0219 Aug 1, 2026
f541b88
feat(home): add scroll-linked Three.js point cloud background
lien0219 Aug 1, 2026
767763e
feat(home): mount Three.js background with route lifecycle
lien0219 Aug 1, 2026
1440c82
fix(home): always open homepage at the top
lien0219 Aug 1, 2026
06a0a7b
feat(home): rebuild hero visual with Three.js holographic scene
lien0219 Aug 1, 2026
c28ec30
fix(home): clean hero scene and add branded cube faces
lien0219 Aug 1, 2026
6e346de
feat(home): add scroll guidance and looping hero typography
lien0219 Aug 1, 2026
214e35e
feat(home): load hero guidance styles
lien0219 Aug 1, 2026
fb320ee
feat(home): add interactive Three.js advertising scene
lien0219 Aug 1, 2026
437ad62
style(home): redesign advertising panel for 3D scene
lien0219 Aug 1, 2026
c292c25
feat(home): mount advertising Three.js experience
lien0219 Aug 1, 2026
1cb3e50
fix(home): restore dark mode contrast and highlights
lien0219 Aug 1, 2026
8f9dd6f
Merge remote-tracking branch 'origin/develop' into feature/usage-deta…
lien0219 Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/internal/handler/admin/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,10 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {

AffiliateEnabled: settings.AffiliateEnabled,

AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests,
AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests,
UsageDetailShowUnitPrices: settings.UsageDetailShowUnitPrices,
UsageDetailShowRateMultiplier: settings.UsageDetailShowRateMultiplier,
UsageDetailShowOriginalCost: settings.UsageDetailShowOriginalCost,
}

// OpenAI fast policy (stored under a dedicated setting key)
Expand Down
23 changes: 22 additions & 1 deletion backend/internal/handler/admin/setting_handler_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,10 @@ type UpdateSettingsRequest struct {
AuthSourceGooglePlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_google_platform_quotas"`
AuthSourceDingTalkPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_dingtalk_platform_quotas"`

AllowUserViewErrorRequests *bool `json:"allow_user_view_error_requests"`
AllowUserViewErrorRequests *bool `json:"allow_user_view_error_requests"`
UsageDetailShowUnitPrices *bool `json:"usage_detail_show_unit_prices"`
UsageDetailShowRateMultiplier *bool `json:"usage_detail_show_rate_multiplier"`
UsageDetailShowOriginalCost *bool `json:"usage_detail_show_original_cost"`
}

// UpdateSettings 更新系统设置
Expand Down Expand Up @@ -1385,6 +1388,24 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.AllowUserViewErrorRequests
}(),
UsageDetailShowUnitPrices: func() bool {
if req.UsageDetailShowUnitPrices != nil {
return *req.UsageDetailShowUnitPrices
}
return previousSettings.UsageDetailShowUnitPrices
}(),
UsageDetailShowRateMultiplier: func() bool {
if req.UsageDetailShowRateMultiplier != nil {
return *req.UsageDetailShowRateMultiplier
}
return previousSettings.UsageDetailShowRateMultiplier
}(),
UsageDetailShowOriginalCost: func() bool {
if req.UsageDetailShowOriginalCost != nil {
return *req.UsageDetailShowOriginalCost
}
return previousSettings.UsageDetailShowOriginalCost
}(),
OpsMonitoringEnabled: func() bool {
if req.OpsMonitoringEnabled != nil {
return *req.OpsMonitoringEnabled
Expand Down
10 changes: 8 additions & 2 deletions backend/internal/handler/dto/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,10 @@ type SystemSettings struct {
DefaultPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"default_platform_quotas,omitempty"`

// 允许终端用户在用量页查看自己的失败请求
AllowUserViewErrorRequests bool `json:"allow_user_view_error_requests"`
AllowUserViewErrorRequests bool `json:"allow_user_view_error_requests"`
UsageDetailShowUnitPrices bool `json:"usage_detail_show_unit_prices"`
UsageDetailShowRateMultiplier bool `json:"usage_detail_show_rate_multiplier"`
UsageDetailShowOriginalCost bool `json:"usage_detail_show_original_cost"`
}

type DefaultSubscriptionSetting struct {
Expand Down Expand Up @@ -373,7 +376,10 @@ type PublicSettings struct {

RiskControlEnabled bool `json:"risk_control_enabled"`

AllowUserViewErrorRequests bool `json:"allow_user_view_error_requests"`
AllowUserViewErrorRequests bool `json:"allow_user_view_error_requests"`
UsageDetailShowUnitPrices bool `json:"usage_detail_show_unit_prices"`
UsageDetailShowRateMultiplier bool `json:"usage_detail_show_rate_multiplier"`
UsageDetailShowOriginalCost bool `json:"usage_detail_show_original_cost"`
}

type LoginAgreementDocument struct {
Expand Down
62 changes: 62 additions & 0 deletions backend/internal/handler/dto/usage_visibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package dto

import (
"encoding/json"

"github.com/Wei-Shaw/sub2api/internal/service"
)

var userUsageUnitPriceFields = []string{
"input_cost",
"output_cost",
"cache_creation_cost",
"cache_read_cost",
"image_input_cost",
"image_output_cost",
}

// UsageLogFromServiceWithVisibility converts a service usage record and removes
// disabled billing-calculation fields before the payload reaches JSON encoding.
// Admin endpoints continue using UsageLogFromServiceAdmin and remain unaffected.
func UsageLogFromServiceWithVisibility(log *service.UsageLog, visibility service.UsageDetailVisibility) map[string]any {
return RedactUsageLogForUser(UsageLogFromService(log), visibility)
}

// RedactUsageLogForUser performs response-level redaction rather than UI-only
// hiding. The omitted keys therefore cannot be recovered through DevTools.
func RedactUsageLogForUser(log *UsageLog, visibility service.UsageDetailVisibility) map[string]any {
if log == nil {
return nil
}
raw, err := json.Marshal(log)
if err != nil {
return map[string]any{}
}
payload := make(map[string]any)
if err := json.Unmarshal(raw, &payload); err != nil {
return map[string]any{}
}

if !visibility.ShowUnitPrices {
for _, field := range userUsageUnitPriceFields {
delete(payload, field)
}
}
if !visibility.ShowRateMultiplier {
delete(payload, "rate_multiplier")
redactNestedGroupRate(payload["group"])
if apiKey, ok := payload["api_key"].(map[string]any); ok {
redactNestedGroupRate(apiKey["group"])
}
}
if !visibility.ShowOriginalCost {
delete(payload, "total_cost")
}
return payload
}

func redactNestedGroupRate(value any) {
if group, ok := value.(map[string]any); ok {
delete(group, "rate_multiplier")
}
}
83 changes: 83 additions & 0 deletions backend/internal/handler/dto/usage_visibility_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package dto

import (
"encoding/json"
"testing"

"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
)

func TestRedactUsageLogForUserOmitsDisabledBillingFields(t *testing.T) {
log := &UsageLog{
ID: 1,
InputCost: 1.25,
OutputCost: 2.5,
CacheCreationCost: 0.5,
CacheReadCost: 0.25,
ImageInputCost: 0.75,
ImageOutputCost: 1.75,
TotalCost: 7,
ActualCost: 0.42,
RateMultiplier: 0.06,
Group: &Group{ID: 1, RateMultiplier: 0.06},
APIKey: &APIKey{
ID: 2,
Group: &Group{ID: 1, RateMultiplier: 0.06},
},
}

payload := RedactUsageLogForUser(log, service.UsageDetailVisibility{})
raw, err := json.Marshal(payload)
require.NoError(t, err)
encoded := string(raw)

for _, key := range append(append([]string{}, userUsageUnitPriceFields...), "rate_multiplier", "total_cost") {
require.NotContains(t, encoded, `"`+key+`"`)
}
require.Contains(t, encoded, `"actual_cost":0.42`)
}

func TestRedactUsageLogForUserKeepsEnabledBillingFields(t *testing.T) {
log := &UsageLog{
ID: 1,
InputCost: 1.25,
OutputCost: 2.5,
TotalCost: 3.75,
ActualCost: 0.225,
RateMultiplier: 0.06,
}

payload := RedactUsageLogForUser(log, service.UsageDetailVisibility{
ShowUnitPrices: true,
ShowRateMultiplier: true,
ShowOriginalCost: true,
})
require.Equal(t, 1.25, payload["input_cost"])
require.Equal(t, 2.5, payload["output_cost"])
require.Equal(t, 3.75, payload["total_cost"])
require.Equal(t, 0.06, payload["rate_multiplier"])
}

func TestRedactUsageLogForUserSupportsIndependentSwitches(t *testing.T) {
log := &UsageLog{
ID: 1,
InputCost: 1.25,
OutputCost: 2.5,
TotalCost: 3.75,
ActualCost: 0.225,
RateMultiplier: 0.06,
}

payload := RedactUsageLogForUser(log, service.UsageDetailVisibility{
ShowUnitPrices: true,
ShowRateMultiplier: false,
ShowOriginalCost: true,
})

require.Equal(t, 1.25, payload["input_cost"])
require.Equal(t, 2.5, payload["output_cost"])
require.Equal(t, 3.75, payload["total_cost"])
require.NotContains(t, payload, "rate_multiplier")
require.Equal(t, 0.225, payload["actual_cost"])
}
5 changes: 4 additions & 1 deletion backend/internal/handler/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {

RiskControlEnabled: settings.RiskControlEnabled,

AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests,
AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests,
UsageDetailShowUnitPrices: settings.UsageDetailShowUnitPrices,
UsageDetailShowRateMultiplier: settings.UsageDetailShowRateMultiplier,
UsageDetailShowOriginalCost: settings.UsageDetailShowOriginalCost,
})
}

Expand Down
22 changes: 19 additions & 3 deletions backend/internal/handler/usage_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,17 @@ func (h *UsageHandler) List(c *gin.Context) {
return
}

out := make([]dto.UsageLog, 0, len(records))
visibility := service.UsageDetailVisibility{
ShowUnitPrices: true,
ShowRateMultiplier: true,
ShowOriginalCost: true,
}
if h.settingService != nil {
visibility = h.settingService.GetUsageDetailVisibility(c.Request.Context())
}
out := make([]map[string]any, 0, len(records))
for i := range records {
out = append(out, *dto.UsageLogFromService(&records[i]))
out = append(out, dto.UsageLogFromServiceWithVisibility(&records[i], visibility))
}
response.Paginated(c, out, result.Total, page, pageSize)
}
Expand Down Expand Up @@ -389,7 +397,15 @@ func (h *UsageHandler) GetByID(c *gin.Context) {
return
}

response.Success(c, dto.UsageLogFromService(record))
visibility := service.UsageDetailVisibility{
ShowUnitPrices: true,
ShowRateMultiplier: true,
ShowOriginalCost: true,
}
if h.settingService != nil {
visibility = h.settingService.GetUsageDetailVisibility(c.Request.Context())
}
response.Success(c, dto.UsageLogFromServiceWithVisibility(record, visibility))
}

// Stats handles getting usage statistics
Expand Down
10 changes: 8 additions & 2 deletions backend/internal/server/api_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,10 @@ func TestAPIContracts(t *testing.T) {
"wechat_connect_redirect_url": "",
"wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
"wechat_connect_scopes": "snsapi_login",
"allow_user_view_error_requests": false
"allow_user_view_error_requests": false,
"usage_detail_show_unit_prices": true,
"usage_detail_show_rate_multiplier": true,
"usage_detail_show_original_cost": true
}
}`,
},
Expand Down Expand Up @@ -1289,7 +1292,10 @@ func TestAPIContracts(t *testing.T) {
"auth_source_default_dingtalk_grant_on_signup": false,
"auth_source_default_dingtalk_grant_on_first_bind": false,
"force_email_on_third_party_signup": false,
"allow_user_view_error_requests": false
"allow_user_view_error_requests": false,
"usage_detail_show_unit_prices": true,
"usage_detail_show_rate_multiplier": true,
"usage_detail_show_original_cost": true
}
}`,
},
Expand Down
8 changes: 8 additions & 0 deletions backend/internal/service/domain_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,11 @@ const AdminAPIKeyPrefix = "admin-"
// SettingKeyAllowUserViewErrorRequests controls whether end users can view
// their own failed requests on the usage page. Default false (opt-in).
const SettingKeyAllowUserViewErrorRequests = "allow_user_view_error_requests"

// User usage cost-detail visibility. These are opt-out settings so existing
// installations preserve their current UI and API behavior after upgrading.
const (
SettingKeyUsageDetailShowUnitPrices = "usage_detail_show_unit_prices"
SettingKeyUsageDetailShowRateMultiplier = "usage_detail_show_rate_multiplier"
SettingKeyUsageDetailShowOriginalCost = "usage_detail_show_original_cost"
)
9 changes: 9 additions & 0 deletions backend/internal/service/setting_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,12 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "",

SettingKeyAllowUserViewErrorRequests: "false",

SettingKeyUsageDetailShowUnitPrices: "true",

SettingKeyUsageDetailShowRateMultiplier: "true",

SettingKeyUsageDetailShowOriginalCost: "true",
}

return s.settingRepo.SetMultiple(ctx, defaults)
Expand Down Expand Up @@ -892,6 +898,9 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}

result.AllowUserViewErrorRequests = settings[SettingKeyAllowUserViewErrorRequests] == "true" // default false
result.UsageDetailShowUnitPrices = !isFalseSettingValue(settings[SettingKeyUsageDetailShowUnitPrices])
result.UsageDetailShowRateMultiplier = !isFalseSettingValue(settings[SettingKeyUsageDetailShowRateMultiplier])
result.UsageDetailShowOriginalCost = !isFalseSettingValue(settings[SettingKeyUsageDetailShowOriginalCost])

return result
}
Expand Down
Loading