diff --git a/config/version b/config/version
index f90aee59..f819c546 100644
--- a/config/version
+++ b/config/version
@@ -1 +1 @@
-v1.8.0.8
+v1.8.0.9
diff --git a/database/shared_state.go b/database/shared_state.go
index dac5483f..4bf703e7 100644
--- a/database/shared_state.go
+++ b/database/shared_state.go
@@ -9,6 +9,8 @@ import (
const SharedAccountsVersionKey = "shared_accounts_version"
+const SharedGeoVersionKey = "shared_geo_version"
+
func txOrDB(tx *gorm.DB) *gorm.DB {
if tx != nil {
return tx
@@ -47,6 +49,40 @@ func BumpSharedAccountsVersion(tx *gorm.DB) error {
}).Error
}
+func seedSharedGeoVersion(tx *gorm.DB) error {
+ state := &model.SharedState{
+ Key: SharedGeoVersionKey,
+ }
+ return txOrDB(tx).
+ Attrs(&model.SharedState{
+ Version: 0,
+ UpdatedAt: time.Now().Unix(),
+ }).
+ FirstOrCreate(state).Error
+}
+
+func GetSharedGeoVersion(tx *gorm.DB) (int64, error) {
+ state := &model.SharedState{
+ Key: SharedGeoVersionKey,
+ }
+ if err := txOrDB(tx).First(state).Error; err != nil {
+ return 0, err
+ }
+ return state.Version, nil
+}
+
+func BumpSharedGeoVersion(tx *gorm.DB) error {
+ if err := seedSharedGeoVersion(tx); err != nil {
+ return err
+ }
+ return txOrDB(tx).Model(&model.SharedState{}).
+ Where(&model.SharedState{Key: SharedGeoVersionKey}).
+ Updates(map[string]any{
+ "version": gorm.Expr("version + 1"),
+ "updated_at": time.Now().Unix(),
+ }).Error
+}
+
func UpsertNodeState(tx *gorm.DB, state *model.NodeState) error {
state.UpdatedAt = time.Now().Unix()
return txOrDB(tx).Save(state).Error
diff --git a/docs/Tasktracking/2026-04-27-add-geofile-sync-to-workers.md b/docs/Tasktracking/2026-04-27-add-geofile-sync-to-workers.md
new file mode 100644
index 00000000..4d1784ed
--- /dev/null
+++ b/docs/Tasktracking/2026-04-27-add-geofile-sync-to-workers.md
@@ -0,0 +1,31 @@
+# Task Record
+
+Date: 2026-04-27
+Related Module: database/shared_state, web/controller/server, web/service/node_sync, web/html/index
+Change Type: Add
+
+## Background
+The user needed the ability to trigger "Update Geofiles" on all worker nodes from the master node's UI. Previously, Geofile updates only operated locally on the node where the button was clicked. Workers only synced inbound configurations via `SharedAccountsVersion`; there was no mechanism for the master to broadcast arbitrary actions to workers.
+
+## Changes
+- `database/shared_state.go`: Added `SharedGeoVersionKey` constant and `GetSharedGeoVersion`/`BumpSharedGeoVersion`/`seedSharedGeoVersion` functions, following the same pattern as `SharedAccountsVersion`.
+- `web/controller/server.go`: Added `POST /syncUpdateGeofile` route and `syncUpdateGeofile` handler that updates local Geofiles, then bumps `SharedGeoVersion` in the shared database if shared mode is enabled.
+- `web/service/node_sync.go`: Extended `NodeSyncService` with `lastGeoVersion`, `loadGeoVersion`, `updateGeofiles` fields and a `syncGeoIfNeeded()` method. Workers check for geo version changes on each sync tick and trigger `UpdateGeofile("")` when a new version is detected.
+- `web/html/index.html`: Added "Sync Update" button next to "Update all" in the Geofiles panel, plus the `syncUpdateGeofile()` JavaScript method.
+- Translation files: Added i18n keys `geofileSyncUpdate`, `geofileSyncUpdateDialog`, `geofileSyncUpdateDialogDesc` in both zh_CN and en_US.
+
+## Impact
+- New API endpoint: `POST /panel/api/server/syncUpdateGeofile` (triggers local + broadcast Geofile update)
+- Worker sync loop now includes geo version checking on every tick
+- UI: New "同步更新" button in the Geofiles section of the Xray version modal
+- Database: New `shared_geo_version` key in the `shared_state` table (on first use)
+- No breaking changes to existing API or database schema
+
+## Verification
+- `gofmt -l -w .` passed with no changes
+- `go vet ./...` passed with no errors
+
+## Risks And Follow-Up
+- Geofile sync uses the same polling interval as inbound sync (`SyncIntervalSeconds`); there may be a delay before workers pick up the new version
+- If the shared database is unreachable when bumping the geo version, workers will not be notified but the local update will have already succeeded (handled via warning log)
+- Future: could add a notification to the master's UI showing which workers have/haven't applied the geo update
diff --git a/web/controller/server.go b/web/controller/server.go
index 352226f4..84415ad1 100644
--- a/web/controller/server.go
+++ b/web/controller/server.go
@@ -7,6 +7,8 @@ import (
"strconv"
"time"
+ "github.com/mhsanaei/3x-ui/v2/database"
+ "github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/websocket"
@@ -56,6 +58,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/installXray/:version", a.installXray)
g.POST("/updateGeofile", a.updateGeofile)
g.POST("/updateGeofile/:fileName", a.updateGeofile)
+ g.POST("/syncUpdateGeofile", a.syncUpdateGeofile)
g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/importDB", a.importDB)
@@ -157,6 +160,23 @@ func (a *ServerController) updateGeofile(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
}
+// syncUpdateGeofile updates local Geofiles and notifies worker nodes to do the same.
+func (a *ServerController) syncUpdateGeofile(c *gin.Context) {
+ err := a.serverService.UpdateGeofile("")
+ if err != nil {
+ jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
+ return
+ }
+
+ if service.IsSharedModeEnabled() {
+ if bumpErr := database.BumpSharedGeoVersion(database.GetDB()); bumpErr != nil {
+ logger.Warning("syncUpdateGeofile: local update succeeded but failed to notify workers:", bumpErr)
+ }
+ }
+
+ jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), nil)
+}
+
// stopXrayService stops the Xray service.
func (a *ServerController) stopXrayService(c *gin.Context) {
err := a.serverService.StopXrayService()
diff --git a/web/html/index.html b/web/html/index.html
index 53c97acb..3eebb40a 100644
--- a/web/html/index.html
+++ b/web/html/index.html
@@ -335,8 +335,10 @@