From b930ec71f9918acebb778c7ed9b380b4f50f806f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Apr 2026 10:08:45 +0800 Subject: [PATCH] docs: add geofile scheduled update implementation plan --- .../2026-04-28-geofile-scheduled-update.md | 520 ++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-28-geofile-scheduled-update.md diff --git a/docs/superpowers/plans/2026-04-28-geofile-scheduled-update.md b/docs/superpowers/plans/2026-04-28-geofile-scheduled-update.md new file mode 100644 index 00000000..0cc899f9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-geofile-scheduled-update.md @@ -0,0 +1,520 @@ +# Geofiles Scheduled Update Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add configurable scheduled geofile updates, settable from both the web panel and x-ui.sh. + +**Architecture:** Follow the existing BackupJob pattern — a cron job registered as `@every 1m` that checks a `shouldRun()` guard based on settings stored in `x-ui.json`. Only the master node executes actual updates; workers sync via shared DB version bump. + +**Tech Stack:** Go (robfig/cron/v3), Vue.js (Ant Design Vue), Bash (x-ui.sh), JSON config + +--- + +### Task 1: Add entity fields and settings infrastructure + +**Files:** +- Modify: `web/entity/entity.go` (add fields after BackupMaxCount) +- Modify: `web/service/setting.go` (add defaults, group mapping, getters, GetDefaultSettings) + +- [ ] **Step 1: Add fields to AllSetting struct** + +In `web/entity/entity.go`, after `BackupMaxCount` (line 129), add: + +```go +// Geofile update schedule settings +GeofileUpdateEnabled bool `json:"geofileUpdateEnabled" form:"geofileUpdateEnabled"` +GeofileUpdateFrequency string `json:"geofileUpdateFrequency" form:"geofileUpdateFrequency"` +GeofileUpdateHour int `json:"geofileUpdateHour" form:"geofileUpdateHour"` +``` + +- [ ] **Step 2: Add defaultValueMap entries** + +In `web/service/setting.go`, after line 135 (`"backupMaxCount": "10",`), add: + +```go +// Geofile update schedule settings +"geofileUpdateEnabled": "false", +"geofileUpdateFrequency": "daily", +"geofileUpdateHour": "4", +``` + +- [ ] **Step 3: Add settingGroups mapping** + +In `web/service/setting.go`, after the `"backup"` group (lines 256-261), add: + +```go +"geofileUpdate": { + "enabled": "geofileUpdateEnabled", + "frequency": "geofileUpdateFrequency", + "hour": "geofileUpdateHour", +}, +``` + +- [ ] **Step 4: Add getter methods** + +In `web/service/setting.go`, after `GetBackupMaxCount()` (line 1167), add: + +```go +func (s *SettingService) GetGeofileUpdateEnabled() (bool, error) { + return s.getBool("geofileUpdateEnabled") +} + +func (s *SettingService) GetGeofileUpdateFrequency() (string, error) { + return s.getString("geofileUpdateFrequency") +} + +func (s *SettingService) GetGeofileUpdateHour() (int, error) { + return s.getInt("geofileUpdateHour") +} +``` + +- [ ] **Step 5: Add to GetDefaultSettings** + +In `web/service/setting.go`, inside the `settings` map in `GetDefaultSettings()` (after line 1251), add: + +```go +"geofileUpdateEnabled": func() (any, error) { return s.GetGeofileUpdateEnabled() }, +"geofileUpdateFrequency": func() (any, error) { return s.GetGeofileUpdateFrequency() }, +"geofileUpdateHour": func() (any, error) { return s.GetGeofileUpdateHour() }, +``` + +- [ ] **Step 6: Verify compilation** + +```bash +cd /usr/x-ui/3x-ui && go build ./... +``` + +Expected: no errors + +- [ ] **Step 7: Commit** + +```bash +git add web/entity/entity.go web/service/setting.go +git commit -m "feat: add geofile update schedule settings infrastructure" +``` + +--- + +### Task 2: Create GeofileUpdateJob + +**Files:** +- Create: `web/job/geofile_update_job.go` +- Modify: `web/web.go` (register job) + +- [ ] **Step 1: Create the job file** + +Create `web/job/geofile_update_job.go`: + +```go +package job + +import ( + "time" + + "github.com/mhsanaei/3x-ui/v2/database" + "github.com/mhsanaei/3x-ui/v2/logger" + "github.com/mhsanaei/3x-ui/v2/web/service" +) + +// GeofileUpdateJob handles scheduled geofile updates. +type GeofileUpdateJob struct { + settingService service.SettingService + serverService service.ServerService +} + +// NewGeofileUpdateJob creates a new GeofileUpdateJob instance. +func NewGeofileUpdateJob() *GeofileUpdateJob { + return &GeofileUpdateJob{} +} + +// Run executes the scheduled geofile update if enabled and time matches the configured frequency. +func (j *GeofileUpdateJob) Run() { + enabled, err := j.settingService.GetGeofileUpdateEnabled() + if err != nil || !enabled { + return + } + + frequency, err := j.settingService.GetGeofileUpdateFrequency() + if err != nil { + return + } + + if !j.shouldRun(frequency) { + return + } + + if err := j.serverService.UpdateGeofile(""); err != nil { + logger.Warning("scheduled geofile update failed:", err) + return + } + + if err := database.BumpSharedGeoVersion(database.GetDB()); err != nil { + logger.Warning("bump shared geo version failed:", err) + } +} + +// shouldRun checks if the geofile update should run based on the configured frequency. +func (j *GeofileUpdateJob) shouldRun(frequency string) bool { + now := time.Now() + + switch frequency { + case "hourly": + return now.Minute() == 0 + case "every12h": + return (now.Hour() == 0 || now.Hour() == 12) && now.Minute() == 0 + case "daily": + hour, err := j.settingService.GetGeofileUpdateHour() + if err != nil { + hour = 4 + } + return now.Hour() == hour && now.Minute() == 0 + case "weekly": + if now.Weekday() != time.Sunday { + return false + } + hour, err := j.settingService.GetGeofileUpdateHour() + if err != nil { + hour = 4 + } + return now.Hour() == hour && now.Minute() == 0 + default: + return false + } +} +``` + +- [ ] **Step 2: Register job in web.go** + +In `web/web.go`, after line 415 (`s.cron.AddJob("@every 1m", job.NewBackupJob())`), add: + +```go +s.cron.AddJob("@every 1m", job.NewGeofileUpdateJob()) +``` + +- [ ] **Step 3: Verify compilation** + +```bash +cd /usr/x-ui/3x-ui && go build ./... +``` + +Expected: no errors + +- [ ] **Step 4: Commit** + +```bash +git add web/job/geofile_update_job.go web/web.go +git commit -m "feat: add GeofileUpdateJob for scheduled geofile updates" +``` + +--- + +### Task 3: Add UI controls to the panel + +**Files:** +- Modify: `web/html/index.html` +- Modify: `web/translation/translate.en_US.toml` +- Modify: `web/translation/translate.zh_CN.toml` + +- [ ] **Step 1: Add i18n keys — English** + +In `web/translation/translate.en_US.toml`, after line 168 (`"geofileUpdatePopover"`), add: + +```toml +"geofileScheduleTitle" = "Scheduled Update" +"geofileScheduleEnable" = "Enable" +"geofileScheduleFrequency" = "Frequency" +"geofileScheduleHour" = "Hour" +"geofileScheduleHourly" = "Hourly" +"geofileScheduleEvery12h" = "Every 12 Hours" +"geofileScheduleDaily" = "Daily" +"geofileScheduleWeekly" = "Weekly" +``` + +- [ ] **Step 2: Add i18n keys — Chinese** + +In `web/translation/translate.zh_CN.toml`, after line 168, add: + +```toml +"geofileScheduleTitle" = "定时更新" +"geofileScheduleEnable" = "启用" +"geofileScheduleFrequency" = "更新频率" +"geofileScheduleHour" = "更新小时" +"geofileScheduleHourly" = "每小时" +"geofileScheduleEvery12h" = "每12小时" +"geofileScheduleDaily" = "每天" +"geofileScheduleWeekly" = "每周" +``` + +- [ ] **Step 3: Add Vue data fields** + +In `web/html/index.html`, in the `data:` section, after line 922 (`ipLimitEnable: false,`), add: + +```javascript +geofileUpdateEnabled: false, +geofileUpdateFrequency: 'daily', +geofileUpdateHour: 4, +``` + +- [ ] **Step 4: Load geofile update settings on mount** + +In `web/html/index.html`, in the `mounted()` function, after the line that reads `this.ipLimitEnable = msg.obj.ipLimitEnable;` (line 1184), add: + +```javascript +this.geofileUpdateEnabled = msg.obj.geofileUpdateEnabled; +this.geofileUpdateFrequency = msg.obj.geofileUpdateFrequency; +this.geofileUpdateHour = msg.obj.geofileUpdateHour; +``` + +- [ ] **Step 5: Add save method** + +In `web/html/index.html`, in the `methods:` section, add after the `syncUpdateGeofile` method (after line 1042): + +```javascript +async saveGeofileSchedule() { + try { + await HttpUtil.post('/panel/setting/update', { + geofileUpdateEnabled: this.geofileUpdateEnabled, + geofileUpdateFrequency: this.geofileUpdateFrequency, + geofileUpdateHour: this.geofileUpdateHour, + }); + } catch (e) { + console.error("Failed to save geofile schedule:", e); + } +}, +``` + +- [ ] **Step 6: Add UI controls in the Geofiles panel** + +In `web/html/index.html`, after the existing `syncUpdateGeofile` button row (after line 344, the closing `` of the buttons), add: + +```html +
+

{{ i18n "pages.index.geofileScheduleTitle" }}

+ + + + + + + {{ i18n "pages.index.geofileScheduleHourly" }} + {{ i18n "pages.index.geofileScheduleEvery12h" }} + {{ i18n "pages.index.geofileScheduleDaily" }} + {{ i18n "pages.index.geofileScheduleWeekly" }} + + + + + [[ h-1 ]] + + + +
+``` + +- [ ] **Step 7: Verify compilation** + +```bash +cd /usr/x-ui/3x-ui && go build ./... +``` + +Expected: no errors + +- [ ] **Step 8: Commit** + +```bash +git add web/html/index.html web/translation/translate.en_US.toml web/translation/translate.zh_CN.toml +git commit -m "feat: add geofile scheduled update UI controls to panel" +``` + +--- + +### Task 4: Add geofile-cron command to x-ui.sh + +**Files:** +- Modify: `x-ui.sh` + +- [ ] **Step 1: Add geofile-cron functions** + +In `x-ui.sh`, after the `update_all_geofiles` function (after line 1081), add: + +```bash +# Config file path (must match config.GetSettingPath()) +SETTING_FILE="/etc/x-ui/x-ui.json" + +# Helper: read a value from x-ui.json using python3 or jq +read_geofile_setting() { + local key="$1" + if command -v python3 &>/dev/null; then + python3 -c " +import json, sys +try: + with open('$SETTING_FILE') as f: + data = json.load(f) + print(data.get('geofileUpdate', {}).get('$key', '')) +except: pass +" 2>/dev/null + elif command -v jq &>/dev/null; then + jq -r ".geofileUpdate.$key // empty" "$SETTING_FILE" 2>/dev/null + fi +} + +# Helper: write geofileUpdate section to x-ui.json +write_geofile_setting() { + local enabled="$1" + local frequency="$2" + local hour="$3" + if command -v python3 &>/dev/null; then + python3 -c " +import json +try: + with open('$SETTING_FILE') as f: + data = json.load(f) +except: + data = {} +data['geofileUpdate'] = {'enabled': $enabled, 'frequency': '$frequency', 'hour': $hour} +with open('$SETTING_FILE', 'w') as f: + json.dump(data, f, indent=2) +print('ok') +" 2>/dev/null + else + tmp=$(mktemp) + jq ".geofileUpdate = {\"enabled\": $enabled, \"frequency\": \"$frequency\", \"hour\": $hour}" "$SETTING_FILE" > "$tmp" && mv "$tmp" "$SETTING_FILE" + fi +} + +geofile_cron_status() { + if [ ! -f "$SETTING_FILE" ]; then + echo -e "${red}x-ui.json not found at $SETTING_FILE${plain}" + return 1 + fi + local enabled=$(read_geofile_setting "enabled") + local frequency=$(read_geofile_setting "frequency") + local hour=$(read_geofile_setting "hour") + echo -e "${green}Geofile Scheduled Update:${plain}" + echo -e " Enabled: ${green}${enabled:-false}${plain}" + echo -e " Frequency: ${green}${frequency:-daily}${plain}" + echo -e " Hour: ${green}${hour:-4}${plain}" +} + +geofile_cron_enable() { + local frequency="daily" + local hour="4" + while [ $# -gt 0 ]; do + case "$1" in + --frequency) frequency="$2"; shift 2;; + --hour) hour="$2"; shift 2;; + *) shift;; + esac + done + case "$frequency" in + hourly|every12h|daily|weekly) ;; + *) echo -e "${red}Invalid frequency: $frequency (must be hourly, every12h, daily, or weekly)${plain}"; return 1;; + esac + if ! [ "$hour" -ge 0 ] 2>/dev/null || ! [ "$hour" -le 23 ] 2>/dev/null; then + echo -e "${red}Invalid hour: $hour (must be 0-23)${plain}" + return 1 + fi + write_geofile_setting "true" "$frequency" "$hour" + echo -e "${green}Geofile scheduled update enabled (frequency=$frequency, hour=$hour)${plain}" + echo -e "${yellow}Restarting x-ui to apply changes...${plain}" + systemctl restart x-ui +} + +geofile_cron_disable() { + write_geofile_setting "false" "daily" "4" + echo -e "${green}Geofile scheduled update disabled${plain}" + echo -e "${yellow}Restarting x-ui to apply changes...${plain}" + systemctl restart x-ui +} +``` + +- [ ] **Step 2: Add CLI entry point** + +In `x-ui.sh`, find the `"update-all-geofiles")` case in the main argument parser (line 3921) and add after it: + +```bash +"geofile-cron") + shift + case "${1}" in + "--enable") geofile_cron_enable "${@}";; + "--disable") geofile_cron_disable;; + "--status") geofile_cron_status;; + *) echo -e "${red}Usage: x-ui geofile-cron [--enable --frequency daily --hour 4 | --disable | --status]${plain}";; + esac + ;; +``` + +- [ ] **Step 3: Add help text** + +Find the help display containing `x-ui update-all-geofiles` (line 2309) and add below it: + +```bash +│ ${blue}x-ui geofile-cron --enable${plain} - 启用 Geofile 定时更新 (通过更新 x-ui.json 配置) │ +│ ${blue}x-ui geofile-cron --disable${plain} - 禁用 Geofile 定时更新 │ +│ ${blue}x-ui geofile-cron --status${plain} - 查看当前 Geofile 定时更新状态 │ +``` + +- [ ] **Step 4: Verify syntax** + +```bash +bash -n /usr/x-ui/3x-ui/x-ui.sh +``` + +Expected: no output (no syntax errors) + +- [ ] **Step 5: Commit** + +```bash +git add x-ui.sh +git commit -m "feat: add geofile-cron command to x-ui.sh" +``` + +--- + +### Task 5: Go fmt and final verification + +- [ ] **Step 1: Run gofmt on all Go files** + +```bash +cd /usr/x-ui/3x-ui && gofmt -w web/entity/entity.go web/service/setting.go web/job/geofile_update_job.go web/web.go +``` + +- [ ] **Step 2: Verify build compiles** + +```bash +cd /usr/x-ui/3x-ui && go build ./... +``` + +Expected: no errors + +- [ ] **Step 3: Run existing tests** + +```bash +cd /usr/x-ui/3x-ui && go test ./web/... -count=1 -timeout 60s 2>&1 | tail -20 +``` + +Expected: all pass + +- [ ] **Step 4: Commit** + +```bash +git add -u +git commit -m "chore: gofmt and final polish for geofile scheduled update" +``` + +--- + +### Post-Implementation Checklist + +- [ ] Panel UI: Geofiles modal shows schedule controls with toggle, frequency dropdown, and hour selector +- [ ] Panel UI: Changing dropdown values triggers save via `/panel/setting/update` +- [ ] Panel UI: Hour selector only visible for `daily` and `weekly` frequencies +- [ ] Job: Only executes when enabled and at the correct time +- [ ] Job: Calls `UpdateGeofile("")` then `BumpSharedGeoVersion` on master +- [ ] x-ui.sh: `geofile-cron --status` reads and displays config +- [ ] x-ui.sh: `geofile-cron --enable --frequency daily --hour 4` writes config and restarts +- [ ] x-ui.sh: `geofile-cron --disable` disables and restarts +- [ ] Settings persist across panel restarts via `x-ui.json`