diff --git a/config/version b/config/version
index 53cc14cd..22feccc7 100644
--- a/config/version
+++ b/config/version
@@ -1 +1 @@
-v1.8.1.0
+v1.8.1.1
diff --git a/docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md b/docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md
new file mode 100644
index 00000000..d659bdb3
--- /dev/null
+++ b/docs/Tasktracking/2026-04-27-add-geofile-version-tracking.md
@@ -0,0 +1,39 @@
+# Task Record
+
+Date: 2026-04-27
+Related Module: web/service/server, web/controller/server, web/html/index
+Change Type: Add
+
+## Background
+When downloading geoip.dat and geosite.dat from GitHub releases, the version information (GitHub release tag like `202604262232`) was not captured or displayed. The user wanted to track and show the version of geofiles in the UI.
+
+## Changes
+- `web/service/server.go`:
+ - Changed `downloadFile` closure to return the captured version string alongside the error
+ - Modified `http.Client` to use `CheckRedirect` callback that extracts the release tag from the 302 redirect URL path (format: `/releases/download/{version}/{filename}`)
+ - Added `GeofileVersion` struct and `GeofileVersions` map type for version metadata storage
+ - Added `loadGeofileVersions()` and `saveGeofileVersions()` for reading/writing `geofile_versions.json` in the bin folder
+ - Added `GetGeofileVersions()` public method for API access
+- `web/controller/server.go`:
+ - Added `GET /getGeofileVersions` endpoint returning version metadata
+- `web/html/index.html`:
+ - Added `geofileVersions` to Vue data
+ - Added `loadGeofileVersions()` method, called when the Xray version modal opens
+ - Geofiles panel now displays version string (e.g. `202604262232`) next to each file name
+ - Added CSS classes for version text in light/dark themes
+
+## Impact
+- New file: `bin/geofile_versions.json` stores version metadata per geofile
+- New API: `GET /panel/api/server/getGeofileVersions`
+- No database schema changes
+- Xray binary filename expectations unchanged (files still saved as `geoip.dat`/`geosite.dat`)
+
+## Verification
+- `gofmt -l -w .` passed
+- `go vet ./...` passed
+- Tested redirect URL parsing logic: path `/releases/download/202604262232/geoip.dat` correctly extracts `202604262232`
+- Confirmed `http.Client` with `CheckRedirect` does not interfere with `If-Modified-Since`/`Last-Modified` caching
+
+## Risks And Follow-Up
+- Version extraction depends on GitHub's redirect URL format; if GitHub changes the URL structure, version will be empty (graceful degradation — shows `-` in UI)
+- Worker nodes: version metadata is written locally on each node after their own download via `syncGeoIfNeeded()`, so each worker has its own `geofile_versions.json`
diff --git a/web/controller/server.go b/web/controller/server.go
index 84415ad1..34f6a0d2 100644
--- a/web/controller/server.go
+++ b/web/controller/server.go
@@ -52,6 +52,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.GET("/getNewmldsa65", a.getNewmldsa65)
g.GET("/getNewmlkem768", a.getNewmlkem768)
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
+ g.GET("/getGeofileVersions", a.getGeofileVersions)
g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService)
@@ -177,6 +178,12 @@ func (a *ServerController) syncUpdateGeofile(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), nil)
}
+// getGeofileVersions returns version metadata for all geofiles.
+func (a *ServerController) getGeofileVersions(c *gin.Context) {
+ versions := a.serverService.GetGeofileVersions()
+ jsonObj(c, versions, 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 cecfa3f4..6c5195f2 100644
--- a/web/html/index.html
+++ b/web/html/index.html
@@ -332,6 +332,9 @@
[[ file ]]
+
+ [[ geofileVersions[file].version || '-' ]]
+
@@ -827,6 +830,16 @@
table td, table th {
padding: 2px 15px;
}
+
+ .geofile-version-text {
+ font-size: 11px;
+ color: #999;
+ margin-left: 8px;
+ }
+
+ .dark .geofile-version-text {
+ color: #666;
+ }
@@ -902,6 +915,7 @@
logModal,
xraylogModal,
backupModal,
+ geofileVersions: {},
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
showIp: false,
@@ -974,6 +988,7 @@
return;
}
versionModal.show(msg.obj);
+ this.loadGeofileVersions();
},
switchV2rayVersion(version) {
this.$confirm({
@@ -1026,6 +1041,16 @@
},
});
},
+ async loadGeofileVersions() {
+ try {
+ const msg = await HttpUtil.get('/panel/api/server/getGeofileVersions');
+ if (msg.success && msg.obj) {
+ this.geofileVersions = msg.obj;
+ }
+ } catch (e) {
+ console.error("Failed to load geofile versions:", e);
+ }
+ },
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
diff --git a/web/service/server.go b/web/service/server.go
index dca560af..5188f9d2 100644
--- a/web/service/server.go
+++ b/web/service/server.go
@@ -1080,11 +1080,11 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
}
}
- downloadFile := func(url, destPath string) error {
+ downloadFile := func(url, destPath string) (version string, err error) {
var req *http.Request
- req, err := http.NewRequest("GET", url, nil)
+ req, err = http.NewRequest("GET", url, nil)
if err != nil {
- return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
+ return "", common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
}
var localFileModTime time.Time
@@ -1095,10 +1095,26 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
}
}
- client := &http.Client{}
+ var capturedVersion string
+ client := &http.Client{
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ if len(via) > 5 {
+ return common.NewErrorf("too many redirects for %s", url)
+ }
+ path := req.URL.Path
+ if idx := strings.Index(path, "/releases/download/"); idx != -1 {
+ rest := path[idx+len("/releases/download/"):]
+ slash := strings.Index(rest, "/")
+ if slash > 0 {
+ capturedVersion = rest[:slash]
+ }
+ }
+ return nil
+ },
+ }
resp, err := client.Do(req)
if err != nil {
- return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
+ return "", common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
}
defer resp.Body.Close()
@@ -1126,46 +1142,57 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
// Handle 304 Not Modified
if resp.StatusCode == http.StatusNotModified {
updateFileModTime()
- return nil
+ return capturedVersion, nil
}
if resp.StatusCode != http.StatusOK {
- return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
+ return "", common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
}
file, err := os.Create(destPath)
if err != nil {
- return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
+ return "", common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
- return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
+ return "", common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
}
updateFileModTime()
- return nil
+ return capturedVersion, nil
}
var errorMessages []string
+ versions := loadGeofileVersions(config.GetBinFolderPath())
if fileName == "" {
// Download all geofiles
for _, entry := range geofileAllowlist {
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
- if err := downloadFile(entry.URL, destPath); err != nil {
+ ver, err := downloadFile(entry.URL, destPath)
+ if err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
+ } else {
+ versions.Update(entry.FileName, ver)
}
}
} else {
entry := geofileAllowlist[fileName]
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
- if err := downloadFile(entry.URL, destPath); err != nil {
+ ver, err := downloadFile(entry.URL, destPath)
+ if err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
+ } else {
+ versions.Update(entry.FileName, ver)
}
}
+ if err := saveGeofileVersions(config.GetBinFolderPath(), versions); err != nil {
+ logger.Warningf("Failed to save geofile versions: %v", err)
+ }
+
err := s.RestartXrayService()
if err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
@@ -1178,6 +1205,52 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
return nil
}
+// GeofileVersion holds version metadata for a single geofile.
+type GeofileVersion struct {
+ Version string `json:"version"`
+ UpdatedAt string `json:"updatedAt"`
+}
+
+// GeofileVersions is a map of filename -> version metadata.
+type GeofileVersions map[string]GeofileVersion
+
+// Update sets or updates the version entry for a geofile.
+func (v GeofileVersions) Update(fileName, version string) {
+ entry := v[fileName]
+ if version != "" {
+ entry.Version = version
+ }
+ entry.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
+ v[fileName] = entry
+}
+
+func geofileVersionsPath(binFolder string) string {
+ return filepath.Join(binFolder, "geofile_versions.json")
+}
+
+func loadGeofileVersions(binFolder string) GeofileVersions {
+ versions := make(GeofileVersions)
+ data, err := os.ReadFile(geofileVersionsPath(binFolder))
+ if err != nil {
+ return versions
+ }
+ _ = json.Unmarshal(data, &versions)
+ return versions
+}
+
+func saveGeofileVersions(binFolder string, versions GeofileVersions) error {
+ data, err := json.MarshalIndent(versions, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(geofileVersionsPath(binFolder), data, 0644)
+}
+
+// GetGeofileVersions returns the current geofile version metadata.
+func (s *ServerService) GetGeofileVersions() GeofileVersions {
+ return loadGeofileVersions(config.GetBinFolderPath())
+}
+
func (s *ServerService) GetNewX25519Cert() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")