diff --git a/docs/architecture.md b/docs/architecture.md index efdfa3311..98292fc4f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -369,8 +369,8 @@ All registered in `web.go` → `startTask()`. Each is a struct with a `Run()` me | `@every 5m` | `outbound_subscription_job` | Refresh outbound provider configs | | `@every 10m` | `clear_logs_job` (`PruneXrayLogsJob`) | Truncate Xray access/error logs once either exceeds 64 MiB | | `@hourly` | `warp_ip_job`, `periodic_traffic_reset_job("hourly")` | WARP IP rotation; traffic resets | -| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")` | IP-limit and Xray access/error log cleanup; traffic resets | -| `@weekly` / `@monthly` | `periodic_traffic_reset_job(...)` | Weekly/monthly traffic resets | +| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")`, `periodic_traffic_reset_job("monthly")` | IP-limit and Xray access/error log cleanup; daily resets and due monthly resets | +| `@weekly` | `periodic_traffic_reset_job("weekly")` | Weekly traffic resets | | default `@every 1m` | `ldap_sync_job` | Only if LDAP enabled; schedule configurable | | default `@daily` | `stats_notify_job` | Only if TG bot enabled; schedule configurable | | `@every 2m` | `check_hash_storage` | Only if TG bot enabled; expires bot callback hashes | diff --git a/docs/content/docs/en/config/inbounds.mdx b/docs/content/docs/en/config/inbounds.mdx index a533763e2..e982e41f6 100644 --- a/docs/content/docs/en/config/inbounds.mdx +++ b/docs/content/docs/en/config/inbounds.mdx @@ -40,6 +40,9 @@ See [Clients](/docs/config/clients). Optionally cap total traffic and set an expiry date for the inbound, and choose a periodic **traffic reset** schedule: `never` (default), `hourly`, `daily`, `weekly`, or `monthly`. + +For `monthly` resets, select a day from 1 to 31. If the selected day does not +exist in a shorter month, the reset runs on that month's last day. diff --git a/docs/content/docs/fa/config/inbounds.mdx b/docs/content/docs/fa/config/inbounds.mdx index aca1d6576..99e326584 100644 --- a/docs/content/docs/fa/config/inbounds.mdx +++ b/docs/content/docs/fa/config/inbounds.mdx @@ -40,6 +40,9 @@ TLS یا REALITY) را انتخاب کنید. به [انتقال‌ها](/docs/c به‌صورت اختیاری می‌توانید کل ترافیک را محدود کنید و یک تاریخ انقضا برای ورودی تعیین کنید، و یک زمان‌بندی **بازنشانی ترافیک** دوره‌ای انتخاب کنید: `never` (پیش‌فرض)، `hourly`، `daily`، `weekly` یا `monthly`. + +برای بازنشانی `monthly`، روزی از ۱ تا ۳۱ انتخاب کنید. اگر آن روز در ماهی کوتاه‌تر +وجود نداشته باشد، بازنشانی در آخرین روز همان ماه انجام می‌شود. diff --git a/docs/content/docs/ru/config/inbounds.mdx b/docs/content/docs/ru/config/inbounds.mdx index 2954c99ce..92c5250a5 100644 --- a/docs/content/docs/ru/config/inbounds.mdx +++ b/docs/content/docs/ru/config/inbounds.mdx @@ -41,6 +41,9 @@ icon: ArrowDownToLine При необходимости ограничьте общий объём трафика и установите дату истечения для входящего подключения, а также выберите расписание периодического **сброса трафика**: `never` (по умолчанию), `hourly`, `daily`, `weekly` или `monthly`. + +Для сброса `monthly` выберите день от 1 до 31. Если выбранного дня нет в более +коротком месяце, сброс выполняется в последний день этого месяца. diff --git a/docs/content/docs/zh/config/inbounds.mdx b/docs/content/docs/zh/config/inbounds.mdx index 3cc510e9e..58dc13900 100644 --- a/docs/content/docs/zh/config/inbounds.mdx +++ b/docs/content/docs/zh/config/inbounds.mdx @@ -37,6 +37,9 @@ icon: ArrowDownToLine 可选地为入站设置总流量上限和到期日期,并选择一个周期性的**流量重置**计划: `never`(默认)、`hourly`、`daily`、`weekly` 或 `monthly`。 + +选择 `monthly` 时,可以指定每月 1 至 31 日重置。如果当月没有指定日期, +则在该月最后一天重置。 diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 1e280f13e..6e27e0192 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -1868,6 +1868,13 @@ ], "type": "string" }, + "trafficResetDay": { + "description": "Day of month for monthly traffic resets", + "example": 1, + "maximum": 31, + "minimum": 1, + "type": "integer" + }, "up": { "description": "Upload traffic in bytes", "format": "int64", @@ -1894,6 +1901,7 @@ "tag", "total", "trafficReset", + "trafficResetDay", "up" ], "type": "object" @@ -3159,6 +3167,7 @@ "tag": "in-443-tcp", "total": 0, "trafficReset": "never", + "trafficResetDay": 1, "up": 0 } ] diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 0af3b7007..0a7d34cf3 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -450,6 +450,7 @@ export const EXAMPLES: Record = { "tag": "in-443-tcp", "total": 0, "trafficReset": "never", + "trafficResetDay": 1, "up": 0 }, "InboundClientIps": { diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 087a6c75a..651eafbae 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1842,6 +1842,13 @@ export const SCHEMAS: Record = { ], "type": "string" }, + "trafficResetDay": { + "description": "Day of month for monthly traffic resets", + "example": 1, + "maximum": 31, + "minimum": 1, + "type": "integer" + }, "up": { "description": "Upload traffic in bytes", "format": "int64", @@ -1868,6 +1875,7 @@ export const SCHEMAS: Record = { "tag", "total", "trafficReset", + "trafficResetDay", "up" ], "type": "object" diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index e62b9b3ea..acfdcad7c 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -428,6 +428,7 @@ export interface Inbound { tag: string; total: number; trafficReset: string; + trafficResetDay: number; up: number; } diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 20ccd393a..d4466c239 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -453,6 +453,7 @@ export const InboundSchema = z.object({ tag: z.string(), total: z.number().int(), trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']), + trafficResetDay: z.number().int().min(1).max(31), up: z.number().int(), }); export type Inbound = z.infer; diff --git a/frontend/src/lib/xray/inbound-form-adapter.ts b/frontend/src/lib/xray/inbound-form-adapter.ts index 4362655c3..cebc15087 100644 --- a/frontend/src/lib/xray/inbound-form-adapter.ts +++ b/frontend/src/lib/xray/inbound-form-adapter.ts @@ -41,6 +41,7 @@ export interface RawInboundRow { enable?: boolean; expiryTime?: number; trafficReset?: string; + trafficResetDay?: number; lastTrafficResetTime?: number; nodeId?: number | null; shareAddrStrategy?: string; @@ -60,6 +61,7 @@ export interface WireInboundPayload { enable: boolean; expiryTime: number; trafficReset: TrafficReset; + trafficResetDay: number; lastTrafficResetTime: number; listen: string; port: number; @@ -202,6 +204,7 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues { down: row.down ?? 0, total: row.total ?? 0, trafficReset: coerceTrafficReset(row.trafficReset), + trafficResetDay: Math.min(31, Math.max(1, row.trafficResetDay ?? 1)), lastTrafficResetTime: row.lastTrafficResetTime ?? 0, nodeId: row.nodeId ?? null, shareAddrStrategy: coerceShareAddrStrategy(row.shareAddrStrategy), @@ -344,6 +347,7 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP enable: values.enable, expiryTime: values.expiryTime, trafficReset: values.trafficReset, + trafficResetDay: values.trafficResetDay, lastTrafficResetTime: values.lastTrafficResetTime, listen: values.listen, port: values.port, diff --git a/frontend/src/models/dbinbound.ts b/frontend/src/models/dbinbound.ts index 81eecbdb5..57a6b9bc1 100644 --- a/frontend/src/models/dbinbound.ts +++ b/frontend/src/models/dbinbound.ts @@ -30,6 +30,7 @@ export type DBInboundInit = Partial<{ enable: boolean; expiryTime: number; trafficReset: string; + trafficResetDay: number; lastTrafficResetTime: number; listen: string; port: number; @@ -76,6 +77,7 @@ export class DBInbound { enable: boolean; expiryTime: number; trafficReset: string; + trafficResetDay: number; lastTrafficResetTime: number; listen: string; @@ -105,6 +107,7 @@ export class DBInbound { this.enable = true; this.expiryTime = 0; this.trafficReset = "never"; + this.trafficResetDay = 1; this.lastTrafficResetTime = 0; this.listen = ""; diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index a7fecd507..9b0fb6f3e 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -33,6 +33,7 @@ import { isSS2022, } from '@/lib/xray/protocol-capabilities'; import { + InboundDbFieldsSchema, InboundFormBaseSchema, InboundFormSchema, type InboundFormValues, @@ -255,6 +256,7 @@ export default function InboundFormModal({ const wTunnelNetwork = useWatch({ control, name: 'settings.allowedNetwork' }); const wTotal = (useWatch({ control, name: 'total' }) as number | undefined) ?? 0; const wExpiry = (useWatch({ control, name: 'expiryTime' }) as number | undefined) ?? 0; + const trafficReset = useWatch({ control, name: 'trafficReset' }) ?? 'never'; const autoTagRef = useRef(true); const lastWrittenTagRef = useRef(''); const currentTagInput = (): InboundTagInput => ({ @@ -619,6 +621,16 @@ export default function InboundFormModal({ /> + {trafficReset === 'monthly' && ( + + + + )} + diff --git a/frontend/src/schemas/forms/inbound-form.ts b/frontend/src/schemas/forms/inbound-form.ts index a729c2fbc..d9c51ee05 100644 --- a/frontend/src/schemas/forms/inbound-form.ts +++ b/frontend/src/schemas/forms/inbound-form.ts @@ -22,6 +22,7 @@ export const InboundDbFieldsSchema = z.object({ down: z.number().int().min(0).default(0), total: z.number().int().min(0).default(0), trafficReset: TrafficResetSchema.default('never'), + trafficResetDay: z.number().int().min(1).max(31).default(1), lastTrafficResetTime: z.number().int().default(0), nodeId: z.number().int().nullable().optional(), shareAddrStrategy: ShareAddrStrategySchema.default('node'), diff --git a/frontend/src/test/inbound-form-adapter.test.ts b/frontend/src/test/inbound-form-adapter.test.ts index 234f13e2c..c949d095f 100644 --- a/frontend/src/test/inbound-form-adapter.test.ts +++ b/frontend/src/test/inbound-form-adapter.test.ts @@ -32,6 +32,7 @@ const vlessRow: RawInboundRow = { total: 1_000_000_000, expiryTime: 0, trafficReset: 'monthly', + trafficResetDay: 15, lastTrafficResetTime: 0, tag: 'inbound-1', nodeId: null, @@ -267,6 +268,7 @@ describe('formValuesToWirePayload', () => { enable: payload.enable, expiryTime: payload.expiryTime, trafficReset: payload.trafficReset, + trafficResetDay: payload.trafficResetDay, lastTrafficResetTime: payload.lastTrafficResetTime, nodeId: payload.nodeId ?? null, }); @@ -276,8 +278,13 @@ describe('formValuesToWirePayload', () => { expect(replay.listen).toBe(original.listen); expect(replay.up).toBe(original.up); expect(replay.down).toBe(original.down); + expect(replay.trafficResetDay).toBe(original.trafficResetDay); expect(replay.streamSettings).toEqual(original.streamSettings); }); + + it('defaults a missing monthly reset day to the first', () => { + expect(rawInboundToFormValues({ ...vlessRow, trafficResetDay: undefined }).trafficResetDay).toBe(1); + }); }); describe('subSortIndex', () => { diff --git a/internal/database/model/model.go b/internal/database/model/model.go index 6c650fbfa..c2036d900 100644 --- a/internal/database/model/model.go +++ b/internal/database/model/model.go @@ -54,6 +54,7 @@ type Inbound struct { Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // Whether the inbound is enabled ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule + TrafficResetDay int `json:"trafficResetDay" form:"trafficResetDay" gorm:"default:1" validate:"omitempty,gte=1,lte=31" example:"1"` // Day of month for monthly traffic resets LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics diff --git a/internal/web/job/periodic_traffic_reset_job.go b/internal/web/job/periodic_traffic_reset_job.go index 6153fcac6..b0564df4e 100644 --- a/internal/web/job/periodic_traffic_reset_job.go +++ b/internal/web/job/periodic_traffic_reset_job.go @@ -1,6 +1,8 @@ package job import ( + "time" + "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) @@ -13,15 +15,25 @@ type PeriodicTrafficResetJob struct { inboundService service.InboundService clientService service.ClientService period Period + location *time.Location } // NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period. -func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob { +func NewPeriodicTrafficResetJob(period Period, location *time.Location) *PeriodicTrafficResetJob { return &PeriodicTrafficResetJob{ - period: period, + period: period, + location: location, } } +func monthlyResetDue(resetDay int, now time.Time) bool { + if resetDay < 1 { + resetDay = 1 + } + lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day() + return now.Day() == min(resetDay, lastDay) +} + // Run resets traffic statistics for all inbounds that match the configured reset period. func (j *PeriodicTrafficResetJob) Run() { inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period)) @@ -30,13 +42,22 @@ func (j *PeriodicTrafficResetJob) Run() { return } + if j.period == "monthly" { + now := time.Now().In(j.location) + due := inbounds[:0] + for _, inbound := range inbounds { + if monthlyResetDue(inbound.TrafficResetDay, now) { + due = append(due, inbound) + } + } + inbounds = due + } if len(inbounds) == 0 { return } logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds)) resetCount := 0 - for _, inbound := range inbounds { resetInboundErr := j.inboundService.ResetInboundTraffic(inbound.Id) if resetInboundErr != nil { diff --git a/internal/web/job/periodic_traffic_reset_job_test.go b/internal/web/job/periodic_traffic_reset_job_test.go new file mode 100644 index 000000000..2c7c19880 --- /dev/null +++ b/internal/web/job/periodic_traffic_reset_job_test.go @@ -0,0 +1,31 @@ +package job + +import ( + "testing" + "time" +) + +func TestMonthlyResetDue(t *testing.T) { + cases := []struct { + name string + resetDay int + now time.Time + want bool + }{ + {"legacy default on first", 0, time.Date(2026, time.July, 1, 0, 0, 0, 0, time.UTC), true}, + {"configured day", 15, time.Date(2026, time.July, 15, 0, 0, 0, 0, time.UTC), true}, + {"before configured day", 15, time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC), false}, + {"month end", 31, time.Date(2026, time.January, 31, 0, 0, 0, 0, time.UTC), true}, + {"short month fallback", 31, time.Date(2026, time.February, 28, 0, 0, 0, 0, time.UTC), true}, + {"leap year fallback", 31, time.Date(2028, time.February, 29, 0, 0, 0, 0, time.UTC), true}, + {"not before short month end", 31, time.Date(2028, time.February, 28, 0, 0, 0, 0, time.UTC), false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := monthlyResetDue(tc.resetDay, tc.now); got != tc.want { + t.Fatalf("monthlyResetDue(%d, %s) = %v, want %v", tc.resetDay, tc.now.Format(time.DateOnly), got, tc.want) + } + }) + } +} diff --git a/internal/web/runtime/remote.go b/internal/web/runtime/remote.go index 20af5453a..17828788f 100644 --- a/internal/web/runtime/remote.go +++ b/internal/web/runtime/remote.go @@ -770,6 +770,9 @@ func wireInbound(ib *model.Inbound, remoteNodeID int) url.Values { if ib.TrafficReset != "" { v.Set("trafficReset", ib.TrafficReset) } + if ib.TrafficResetDay > 0 { + v.Set("trafficResetDay", strconv.Itoa(ib.TrafficResetDay)) + } return v } diff --git a/internal/web/runtime/remote_test.go b/internal/web/runtime/remote_test.go index 09f80ddc5..2701ca738 100644 --- a/internal/web/runtime/remote_test.go +++ b/internal/web/runtime/remote_test.go @@ -252,9 +252,12 @@ func TestIsNonEmptySlice(t *testing.T) { } func TestWireInboundTrafficReset(t *testing.T) { - with := wireInbound(&model.Inbound{TrafficReset: "daily"}, 0) - if got := with.Get("trafficReset"); got != "daily" { - t.Fatalf("trafficReset = %q, want daily", got) + with := wireInbound(&model.Inbound{TrafficReset: "monthly", TrafficResetDay: 15}, 0) + if got := with.Get("trafficReset"); got != "monthly" { + t.Fatalf("trafficReset = %q, want monthly", got) + } + if got := with.Get("trafficResetDay"); got != "15" { + t.Fatalf("trafficResetDay = %q, want 15", got) } // Empty TrafficReset must be omitted entirely, not sent as an empty field. without := wireInbound(&model.Inbound{}, 0) diff --git a/internal/web/service/inbound.go b/internal/web/service/inbound.go index fe6edbcc7..336c19d26 100644 --- a/internal/web/service/inbound.go +++ b/internal/web/service/inbound.go @@ -34,6 +34,13 @@ type InboundService struct { fallbackService FallbackService } +func normalizeTrafficResetDay(day int) int { + if day < 1 { + return 1 + } + return min(day, 31) +} + func normalizeInboundShareAddrStrategy(strategy string) string { strategy = strings.TrimSpace(strategy) switch strategy { @@ -905,6 +912,7 @@ func (s *InboundService) normalizeMtprotoXrayPort(inbound *model.Inbound, oldSet // Returns the created inbound, whether Xray needs restart, and any error. func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { inbound.Id = 0 + inbound.TrafficResetDay = normalizeTrafficResetDay(inbound.TrafficResetDay) // Normalize streamSettings based on protocol s.normalizeStreamSettings(inbound) if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil { @@ -1331,6 +1339,7 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) { } func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { + inbound.TrafficResetDay = normalizeTrafficResetDay(inbound.TrafficResetDay) // Normalize streamSettings based on protocol s.normalizeStreamSettings(inbound) if err := validateFinalMaskRealityCombo(inbound.StreamSettings); err != nil { @@ -1460,6 +1469,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, oldInbound.Enable = inbound.Enable oldInbound.ExpiryTime = inbound.ExpiryTime oldInbound.TrafficReset = inbound.TrafficReset + oldInbound.TrafficResetDay = inbound.TrafficResetDay oldInbound.Listen = inbound.Listen oldInbound.Port = inbound.Port oldInbound.Protocol = inbound.Protocol diff --git a/internal/web/service/inbound_node.go b/internal/web/service/inbound_node.go index eb9b646bb..73fe5fdfd 100644 --- a/internal/web/service/inbound_node.go +++ b/internal/web/service/inbound_node.go @@ -311,7 +311,8 @@ func adoptedWireChanged(c, snapIb *model.Inbound, adoptedSettings string) bool { c.ExpiryTime != snapIb.ExpiryTime || c.StreamSettings != snapIb.StreamSettings || c.Sniffing != snapIb.Sniffing || - c.TrafficReset != snapIb.TrafficReset + c.TrafficReset != snapIb.TrafficReset || + c.TrafficResetDay != normalizeTrafficResetDay(snapIb.TrafficResetDay) } // adoptedWireInbound is the central inbound as it reads after adopting the @@ -330,6 +331,7 @@ func adoptedWireInbound(c, snapIb *model.Inbound, adoptedSettings string) *model a.StreamSettings = snapIb.StreamSettings a.Sniffing = snapIb.Sniffing a.TrafficReset = snapIb.TrafficReset + a.TrafficResetDay = normalizeTrafficResetDay(snapIb.TrafficResetDay) return &a } @@ -561,6 +563,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi StreamSettings: snapIb.StreamSettings, Sniffing: snapIb.Sniffing, TrafficReset: snapIb.TrafficReset, + TrafficResetDay: normalizeTrafficResetDay(snapIb.TrafficResetDay), LastTrafficResetTime: snapIb.LastTrafficResetTime, Enable: snapIb.Enable, Remark: snapIb.Remark, @@ -616,6 +619,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi updates["stream_settings"] = snapIb.StreamSettings updates["sniffing"] = snapIb.Sniffing updates["traffic_reset"] = snapIb.TrafficReset + updates["traffic_reset_day"] = normalizeTrafficResetDay(snapIb.TrafficResetDay) updates["last_traffic_reset_time"] = snapIb.LastTrafficResetTime if adoptedWireChanged(c, snapIb, adoptedSettings) { adoptedInbounds = append(adoptedInbounds, adoptedWireInbound(c, snapIb, adoptedSettings)) diff --git a/internal/web/service/inbound_traffic_reset_test.go b/internal/web/service/inbound_traffic_reset_test.go new file mode 100644 index 000000000..d8ab07060 --- /dev/null +++ b/internal/web/service/inbound_traffic_reset_test.go @@ -0,0 +1,18 @@ +package service + +import "testing" + +func TestNormalizeTrafficResetDay(t *testing.T) { + tests := map[int]int{ + 0: 1, + 1: 1, + 15: 15, + 31: 31, + 32: 31, + } + for input, want := range tests { + if got := normalizeTrafficResetDay(input); got != want { + t.Errorf("normalizeTrafficResetDay(%d) = %d, want %d", input, got, want) + } + } +} diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 1b3a64c9c..85915ce5f 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -451,6 +451,7 @@ "importInbound": "استيراد إدخال", "periodicTrafficResetTitle": "إعادة تعيين حركة المرور", "periodicTrafficResetDesc": "إعادة تعيين عداد حركة المرور تلقائيًا في فترات محددة", + "periodicTrafficResetDay": "يوم إعادة التعيين الشهري", "lastReset": "آخر إعادة تعيين", "periodicTrafficReset": { "never": "أبداً", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index a4f8d0d33..d6e6bb974 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -451,6 +451,7 @@ "importInbound": "Import an Inbound", "periodicTrafficResetTitle": "Traffic Reset", "periodicTrafficResetDesc": "Automatically reset traffic counter at specified intervals", + "periodicTrafficResetDay": "Monthly reset day", "lastReset": "Last Reset", "periodicTrafficReset": { "never": "Never", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index a0608f227..9f505ff0c 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -451,6 +451,7 @@ "importInbound": "Importar un entrante", "periodicTrafficResetTitle": "Reset de Tráfico", "periodicTrafficResetDesc": "Reiniciar automáticamente el contador de tráfico en intervalos especificados", + "periodicTrafficResetDay": "Día de reinicio mensual", "lastReset": "Último reinicio", "periodicTrafficReset": { "never": "Nunca", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index dd196e30f..816ff481e 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -451,6 +451,7 @@ "importInbound": "افزودن یک ورودی", "periodicTrafficResetTitle": "بازنشانی ترافیک", "periodicTrafficResetDesc": "بازنشانی خودکار شمارنده ترافیک در فواصل زمانی مشخص", + "periodicTrafficResetDay": "روز بازنشانی ماهانه", "lastReset": "آخرین بازنشانی", "periodicTrafficReset": { "never": "هرگز", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 1b37be9c3..a44010547 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -451,6 +451,7 @@ "importInbound": "Impor Masuk", "periodicTrafficResetTitle": "Reset Trafik Berkala", "periodicTrafficResetDesc": "Reset otomatis penghitung trafik pada interval tertentu", + "periodicTrafficResetDay": "Hari reset bulanan", "lastReset": "Reset Terakhir", "periodicTrafficReset": { "never": "Tidak Pernah", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 28eaec2b8..55aeea470 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -451,6 +451,7 @@ "importInbound": "インバウンドルールをインポート", "periodicTrafficResetTitle": "トラフィックリセット", "periodicTrafficResetDesc": "指定された間隔でトラフィックカウンタを自動的にリセット", + "periodicTrafficResetDay": "毎月のリセット日", "lastReset": "最後のリセット", "periodicTrafficReset": { "never": "なし", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index bf4e36c27..3cb5e764d 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -451,6 +451,7 @@ "importInbound": "Importar um Inbound", "periodicTrafficResetTitle": "Reset de Tráfego", "periodicTrafficResetDesc": "Reinicia automaticamente o contador de tráfego em intervalos especificados", + "periodicTrafficResetDay": "Dia da redefinição mensal", "lastReset": "Último Reset", "periodicTrafficReset": { "never": "Nunca", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 02d99281f..14c401278 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -451,6 +451,7 @@ "importInbound": "Импорт подключений", "periodicTrafficResetTitle": "Сброс трафика", "periodicTrafficResetDesc": "Автоматический сброс счетчика трафика через указанные интервалы", + "periodicTrafficResetDay": "День ежемесячного сброса", "lastReset": "Последний сброс", "periodicTrafficReset": { "never": "Никогда", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 8fac0af4d..6309d7e0c 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -451,6 +451,7 @@ "importInbound": "Gelen Bağlantı İçe Aktar", "periodicTrafficResetTitle": "Trafik Sıfırlama", "periodicTrafficResetDesc": "Belirtilen aralıklarla trafik sayacını otomatik olarak sıfırla", + "periodicTrafficResetDay": "Aylık sıfırlama günü", "lastReset": "Son Sıfırlama", "periodicTrafficReset": { "never": "Asla", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 53e810440..8b8cee385 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -451,6 +451,7 @@ "importInbound": "Імпортувати вхідний", "periodicTrafficResetTitle": "Скидання трафіку", "periodicTrafficResetDesc": "Автоматично скидати лічильник трафіку через певні проміжки часу", + "periodicTrafficResetDay": "День щомісячного скидання", "lastReset": "Останнє скидання", "periodicTrafficReset": { "never": "Ніколи", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index af252b9e3..8cbb40c12 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -451,6 +451,7 @@ "importInbound": "Nhập inbound", "periodicTrafficResetTitle": "Đặt lại lưu lượng", "periodicTrafficResetDesc": "Tự động đặt lại bộ đếm lưu lượng theo khoảng thời gian xác định", + "periodicTrafficResetDay": "Ngày đặt lại hàng tháng", "lastReset": "Đặt lại lần cuối", "periodicTrafficReset": { "never": "Không bao giờ", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index b39641117..a6800b386 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -451,6 +451,7 @@ "importInbound": "导入入站规则", "periodicTrafficResetTitle": "流量重置", "periodicTrafficResetDesc": "按指定间隔自动重置流量计数器", + "periodicTrafficResetDay": "每月重置日", "lastReset": "上次重置", "periodicTrafficReset": { "never": "从不", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index f79fb3809..41f365419 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -451,6 +451,7 @@ "importInbound": "匯入入站規則", "periodicTrafficResetTitle": "流量重置", "periodicTrafficResetDesc": "按指定間隔自動重置流量計數器", + "periodicTrafficResetDay": "每月重置日", "lastReset": "上次重置", "periodicTrafficReset": { "never": "從不", diff --git a/internal/web/web.go b/internal/web/web.go index 76f8927ec..a2cb0657e 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -302,7 +302,7 @@ const ( // startTask schedules background jobs (Xray checks, traffic jobs, cron // jobs) which the panel relies on for periodic maintenance and monitoring. -func (s *Server) startTask(restartXray bool) { +func (s *Server) startTask(restartXray bool, loc *time.Location) { if restartXray { err := s.xrayService.RestartXray(true) if err != nil { @@ -344,13 +344,13 @@ func (s *Server) startTask(restartXray bool) { // Inbound traffic reset jobs // Run every hour - _, _ = s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly")) + _, _ = s.cron.AddJob("@hourly", job.NewPeriodicTrafficResetJob("hourly", loc)) // Run once a day, midnight - _, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily")) + _, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily", loc)) // Run once a week, midnight between Sat/Sun - _, _ = s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly")) - // Run once a month, midnight, first of month - _, _ = s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly")) + _, _ = s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly", loc)) + // Check monthly reset days at midnight + _, _ = s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("monthly", loc)) // LDAP sync scheduling if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled { @@ -651,7 +651,7 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) { } }) - s.startTask(restartXray) + s.startTask(restartXray, loc) if startTgBot { isTgbotenabled, err := s.settingService.GetTgbotEnabled()