fix: merge general improvements from release branch

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
This commit is contained in:
erio
2026-04-14 19:29:37 +08:00
parent c14d739360
commit 63f539b382
13 changed files with 114 additions and 36 deletions

View File

@@ -10,6 +10,8 @@ import (
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
@@ -32,11 +34,12 @@ type PublicSettingsProvider interface {
// FrontendServer serves the embedded frontend with settings injection
type FrontendServer struct {
distFS fs.FS
fileServer http.Handler
baseHTML []byte
cache *HTMLCache
settings PublicSettingsProvider
distFS fs.FS
fileServer http.Handler
baseHTML []byte
cache *HTMLCache
settings PublicSettingsProvider
overrideDir string // local file override directory
}
// NewFrontendServer creates a new frontend server with settings injection
@@ -62,11 +65,12 @@ func NewFrontendServer(settingsProvider PublicSettingsProvider) (*FrontendServer
cache.SetBaseHTML(baseHTML)
return &FrontendServer{
distFS: distFS,
fileServer: http.FileServer(http.FS(distFS)),
baseHTML: baseHTML,
cache: cache,
settings: settingsProvider,
distFS: distFS,
fileServer: http.FileServer(http.FS(distFS)),
baseHTML: baseHTML,
cache: cache,
settings: settingsProvider,
overrideDir: filepath.Join("data", "public"),
}, nil
}
@@ -99,6 +103,11 @@ func (s *FrontendServer) Middleware() gin.HandlerFunc {
return
}
// Try local override first
if s.tryServeOverride(c, cleanPath) {
return
}
// Serve static files normally
s.fileServer.ServeHTTP(c.Writer, c.Request)
c.Abort()
@@ -114,6 +123,22 @@ func (s *FrontendServer) fileExists(path string) bool {
return true
}
// tryServeOverride checks if a local override file exists and serves it.
// Files in overrideDir take precedence over embedded files.
func (s *FrontendServer) tryServeOverride(c *gin.Context, cleanPath string) bool {
if s.overrideDir == "" {
return false
}
filePath := filepath.Join(s.overrideDir, filepath.Clean("/"+cleanPath))
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
return false
}
c.File(filePath)
c.Abort()
return true
}
func (s *FrontendServer) serveIndexHTML(c *gin.Context) {
// Get nonce from context (generated by SecurityHeaders middleware)
nonce := middleware.GetNonceFromContext(c)
@@ -226,6 +251,7 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
panic("failed to get dist subdirectory: " + err.Error())
}
fileServer := http.FileServer(http.FS(distFS))
overrideDir := filepath.Join("data", "public")
return func(c *gin.Context) {
path := c.Request.URL.Path
@@ -242,6 +268,10 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
if file, err := distFS.Open(cleanPath); err == nil {
_ = file.Close()
// Try local override first
if tryServeOverrideFile(c, overrideDir, cleanPath) {
return
}
fileServer.ServeHTTP(c.Writer, c.Request)
c.Abort()
return
@@ -251,6 +281,21 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
}
}
// tryServeOverrideFile is a standalone version of tryServeOverride for legacy usage.
func tryServeOverrideFile(c *gin.Context, overrideDir, cleanPath string) bool {
if overrideDir == "" {
return false
}
filePath := filepath.Join(overrideDir, filepath.Clean("/"+cleanPath))
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
return false
}
c.File(filePath)
c.Abort()
return true
}
func shouldBypassEmbeddedFrontend(path string) bool {
trimmed := strings.TrimSpace(path)
return strings.HasPrefix(trimmed, "/api/") ||