-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathwebui_state.go
More file actions
187 lines (165 loc) · 4.83 KB
/
webui_state.go
File metadata and controls
187 lines (165 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/*
File: webui_state.go
Description:
Group override state management for the sdproxy web UI.
Extracted from webui.go.
Handles temporary and permanent overrides, expiration ticking,
and disk persistence.
*/
package main
import (
"encoding/json"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// ---------------------------------------------------------------------------
// Group override state
// ---------------------------------------------------------------------------
// OverrideState holds the current runtime mode and optional expiration data.
type OverrideState struct {
Mode string `json:"mode"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
RevertMode string `json:"revert_mode,omitempty"`
}
var (
groupOverride = make(map[string]OverrideState)
groupOverrideMu sync.RWMutex
expirationTickerOnce sync.Once
)
// GetGroupOverride returns the current override for a group ("DEFAULT" when
// none set). Called from CheckParental on every DNS query — must be cheap.
// The background expiration ticker handles reversions to keep this fast.
func GetGroupOverride(name string) string {
groupOverrideMu.RLock()
v, ok := groupOverride[name]
groupOverrideMu.RUnlock()
if !ok || v.Mode == "" {
return "DEFAULT"
}
return v.Mode
}
// SetGroupOverride sets the runtime override mode for a group.
// If durationMinutes > 0, it becomes a temporary override and will revert
// to its previous state upon expiration. Returns the resulting mode.
func SetGroupOverride(name, mode string, durationMinutes int) string {
groupOverrideMu.Lock()
v, ok := groupOverride[name]
if !ok {
v = OverrideState{Mode: "DEFAULT"}
}
if mode == "CANCEL_TIMER" {
if !v.ExpiresAt.IsZero() {
log.Printf("[WEBUI] Timer canceled for group %q. Reverted to %s.", name, v.RevertMode)
v.Mode = v.RevertMode
v.ExpiresAt = time.Time{}
v.RevertMode = ""
}
} else {
if durationMinutes > 0 {
// If not currently in a timer, save the current mode as RevertMode.
if v.ExpiresAt.IsZero() {
v.RevertMode = v.Mode
if v.RevertMode == "" {
v.RevertMode = "DEFAULT"
}
}
v.Mode = mode
v.ExpiresAt = time.Now().Add(time.Duration(durationMinutes) * time.Minute)
log.Printf("[WEBUI] Set temporary override for group %q: %s for %dm", name, mode, durationMinutes)
} else {
// Permanent override
v.Mode = mode
v.ExpiresAt = time.Time{}
v.RevertMode = ""
log.Printf("[WEBUI] Set permanent override for group %q: %s", name, mode)
}
}
groupOverride[name] = v
resultMode := v.Mode
groupOverrideMu.Unlock()
SaveGroupOverrides()
return resultMode
}
// runOverrideExpirationTicker checks for expired temporary overrides every second
// and reverts them to their previous state automatically.
func runOverrideExpirationTicker() {
t := time.NewTicker(time.Second)
defer t.Stop()
for range t.C {
changed := false
now := time.Now()
groupOverrideMu.Lock()
for k, v := range groupOverride {
if !v.ExpiresAt.IsZero() && now.After(v.ExpiresAt) {
log.Printf("[WEBUI] Temporary override for group %q expired. Reverted to %s.", k, v.RevertMode)
v.Mode = v.RevertMode
v.ExpiresAt = time.Time{}
v.RevertMode = ""
groupOverride[k] = v
changed = true
}
}
groupOverrideMu.Unlock()
if changed {
SaveGroupOverrides()
}
}
}
// SaveGroupOverrides persists the current group overrides to disk.
func SaveGroupOverrides() {
dir := snapshotDir()
if dir == "" {
return
}
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("[WEBUI] Failed to create snapshot dir for overrides: %v", err)
return
}
groupOverrideMu.RLock()
b, err := json.Marshal(groupOverride)
groupOverrideMu.RUnlock()
if err == nil {
path := filepath.Join(dir, "parental-overrides.json")
_ = os.WriteFile(path+".tmp", b, 0644)
_ = os.Rename(path+".tmp", path)
}
}
// LoadGroupOverrides restores the persistent group overrides from disk.
func LoadGroupOverrides() {
defer expirationTickerOnce.Do(func() {
go runOverrideExpirationTicker()
})
dir := snapshotDir()
if dir == "" {
return
}
path := filepath.Join(dir, "parental-overrides.json")
b, err := os.ReadFile(path)
if err != nil {
return // no overrides saved yet or file missing
}
// Try the new structured format
var newFmt map[string]OverrideState
if err := json.Unmarshal(b, &newFmt); err == nil && len(newFmt) > 0 {
groupOverrideMu.Lock()
for k, v := range newFmt {
groupOverride[k] = v
}
groupOverrideMu.Unlock()
log.Printf("[WEBUI] Loaded %d group overrides", len(newFmt))
return
}
// Fallback to legacy map[string]string format
var oldFmt map[string]string
if err := json.Unmarshal(b, &oldFmt); err == nil {
groupOverrideMu.Lock()
for k, v := range oldFmt {
groupOverride[k] = OverrideState{Mode: v}
}
groupOverrideMu.Unlock()
log.Printf("[WEBUI] Migrated %d group overrides from legacy format", len(oldFmt))
}
}