History

fix(tui): update with review

lhpqaq committed 9 months ago

Commit 2c882181 parent 0a2555b6 files · +197 −163Browse files

Unified · Split

6 files
  1. cmd/server/main.go +8 −8
  2. go.mod +1 −1
  3. internal/api/server.go +3 −2
  4. internal/tui/app.go +37 −32
  5. internal/tui/auth_tab.go +131 −119
  6. internal/tui/dashboard.go +17 −1
cmd/server/main.go+8−8
@@ -511,22 +511,22 @@ func main() { · 510 unmodified lines
511 password = localMgmtPassword511 password = localMgmtPassword
512 }512 }
513 513
514 // Ensure management routes are registered (secret-key must be set)
515 if cfg.RemoteManagement.SecretKey == "" {
516 cfg.RemoteManagement.SecretKey = "$tui-placeholder$"
517 }
518
519 // Start server in background514 // Start server in background
520 cancel, done := cmd.StartServiceBackground(cfg, configFilePath, password)515 cancel, done := cmd.StartServiceBackground(cfg, configFilePath, password)
521 516
522 // Wait for server to be ready by polling management API517 // Wait for server to be ready by polling management API with exponential backoff
523 {518 {
524 client := tui.NewClient(cfg.Port, password)519 client := tui.NewClient(cfg.Port, password)
525 for i := 0; i < 50; i++ {520 backoff := 100 * time.Millisecond
526 time.Sleep(100 * time.Millisecond)521 // Try for up to ~10-15 seconds
522 for i := 0; i < 30; i++ {
527 if _, err := client.GetConfig(); err == nil {523 if _, err := client.GetConfig(); err == nil {
528 break524 break
529 }525 }
526 time.Sleep(backoff)
527 if backoff < 1*time.Second {
528 backoff = time.Duration(float64(backoff) * 1.5)
529 }
530 }530 }
531 }531 }
532 532
go.mod+1−1
@@ -1,6 +1,6 @@
1module github.com/router-for-me/CLIProxyAPI/v61module github.com/router-for-me/CLIProxyAPI/v6
2 2
3go 1.24.23go 1.26.0
4 4
5require (5require (
6 github.com/andybalholm/brotli v1.0.66 github.com/andybalholm/brotli v1.0.6
internal/api/server.go+3−2
@@ -284,8 +284,9 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk · 283 unmodified lines
284 optionState.routerConfigurator(engine, s.handlers, cfg)284 optionState.routerConfigurator(engine, s.handlers, cfg)
285 }285 }
286 286
287 // Register management routes when configuration or environment secrets are available.287 // Register management routes when configuration or environment secrets are available,
288 hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret288 // or when a local management password is provided (e.g. TUI mode).
289 hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != ""
289 s.managementRoutesEnabled.Store(hasManagementSecret)290 s.managementRoutesEnabled.Store(hasManagementSecret)
290 if hasManagementSecret {291 if hasManagementSecret {
291 s.registerManagementRoutes()292 s.registerManagementRoutes()
internal/tui/app.go+37−32
@@ -103,38 +103,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { · 102 unmodified lines
103 case "L":103 case "L":
104 ToggleLocale()104 ToggleLocale()
105 a.tabs = TabNames()105 a.tabs = TabNames()
106 // Broadcast locale change to ALL tabs so each re-renders106 return a.broadcastToAllTabs(localeChangedMsg{})
107 var cmds []tea.Cmd
108 var cmd tea.Cmd
109 a.dashboard, cmd = a.dashboard.Update(localeChangedMsg{})
110 if cmd != nil {
111 cmds = append(cmds, cmd)
112 }
113 a.config, cmd = a.config.Update(localeChangedMsg{})
114 if cmd != nil {
115 cmds = append(cmds, cmd)
116 }
117 a.auth, cmd = a.auth.Update(localeChangedMsg{})
118 if cmd != nil {
119 cmds = append(cmds, cmd)
120 }
121 a.keys, cmd = a.keys.Update(localeChangedMsg{})
122 if cmd != nil {
123 cmds = append(cmds, cmd)
124 }
125 a.oauth, cmd = a.oauth.Update(localeChangedMsg{})
126 if cmd != nil {
127 cmds = append(cmds, cmd)
128 }
129 a.usage, cmd = a.usage.Update(localeChangedMsg{})
130 if cmd != nil {
131 cmds = append(cmds, cmd)
132 }
133 a.logs, cmd = a.logs.Update(localeChangedMsg{})
134 if cmd != nil {
135 cmds = append(cmds, cmd)
136 }
137 return a, tea.Batch(cmds...)
138 case "tab":107 case "tab":
139 prevTab := a.activeTab108 prevTab := a.activeTab
140 a.activeTab = (a.activeTab + 1) % len(a.tabs)109 a.activeTab = (a.activeTab + 1) % len(a.tabs)
@@ -278,3 +247,39 @@ func Run(port int, secretKey string, hook *LogHook, output io.Writer) error { · 137 unmodified lines
278 _, err := p.Run()247 _, err := p.Run()
279 return err248 return err
280}249}
250
251func (a App) broadcastToAllTabs(msg tea.Msg) (tea.Model, tea.Cmd) {
252 var cmds []tea.Cmd
253 var cmd tea.Cmd
254
255 a.dashboard, cmd = a.dashboard.Update(msg)
256 if cmd != nil {
257 cmds = append(cmds, cmd)
258 }
259 a.config, cmd = a.config.Update(msg)
260 if cmd != nil {
261 cmds = append(cmds, cmd)
262 }
263 a.auth, cmd = a.auth.Update(msg)
264 if cmd != nil {
265 cmds = append(cmds, cmd)
266 }
267 a.keys, cmd = a.keys.Update(msg)
268 if cmd != nil {
269 cmds = append(cmds, cmd)
270 }
271 a.oauth, cmd = a.oauth.Update(msg)
272 if cmd != nil {
273 cmds = append(cmds, cmd)
274 }
275 a.usage, cmd = a.usage.Update(msg)
276 if cmd != nil {
277 cmds = append(cmds, cmd)
278 }
279 a.logs, cmd = a.logs.Update(msg)
280 if cmd != nil {
281 cmds = append(cmds, cmd)
282 }
283
284 return a, tea.Batch(cmds...)
285}
internal/tui/auth_tab.go+131−119
@@ -106,132 +106,16 @@ func (m authTabModel) Update(msg tea.Msg) (authTabModel, tea.Cmd) { · 105 unmodified lines
106 case tea.KeyMsg:106 case tea.KeyMsg:
107 // ---- Editing mode ----107 // ---- Editing mode ----
108 if m.editing {108 if m.editing {
109 switch msg.String() {109 return m.handleEditInput(msg)
110 case "enter":
111 value := m.editInput.Value()
112 fieldKey := authEditableFields[m.editField].key
113 fileName := m.editFileName
114 m.editing = false
115 m.editInput.Blur()
116 fields := map[string]any{}
117 if fieldKey == "priority" {
118p, err := strconv.Atoi(value)
119if err != nil {
120 return m, func() tea.Msg {
121 return authActionMsg{err: fmt.Errorf("invalid priority: must be a number")}
122 }
123}
124 fields[fieldKey] = p
125 } else {
126 fields[fieldKey] = value
127 }
128 return m, func() tea.Msg {
129 err := m.client.PatchAuthFileFields(fileName, fields)
130 if err != nil {
131 return authActionMsg{err: err}
132 }
133 return authActionMsg{action: fmt.Sprintf(T("updated_field"), fieldKey, fileName)}
134 }
135 case "esc":
136 m.editing = false
137 m.editInput.Blur()
138 m.viewport.SetContent(m.renderContent())
139 return m, nil
140 default:
141 var cmd tea.Cmd
142 m.editInput, cmd = m.editInput.Update(msg)
143 m.viewport.SetContent(m.renderContent())
144 return m, cmd
145 }
146 }110 }
147 111
148 // ---- Delete confirmation mode ----112 // ---- Delete confirmation mode ----
149 if m.confirm >= 0 {113 if m.confirm >= 0 {
150 switch msg.String() {114 return m.handleConfirmInput(msg)
151 case "y", "Y":
152 idx := m.confirm
153 m.confirm = -1
154 if idx < len(m.files) {
155 name := getString(m.files[idx], "name")
156 return m, func() tea.Msg {
157 err := m.client.DeleteAuthFile(name)
158 if err != nil {
159 return authActionMsg{err: err}
160 }
161 return authActionMsg{action: fmt.Sprintf(T("deleted"), name)}
162 }
163 }
164 m.viewport.SetContent(m.renderContent())
165 return m, nil
166 case "n", "N", "esc":
167 m.confirm = -1
168 m.viewport.SetContent(m.renderContent())
169 return m, nil
170 }
171 return m, nil
172 }115 }
173 116
174 // ---- Normal mode ----117 // ---- Normal mode ----
175 switch msg.String() {118 return m.handleNormalInput(msg)
176 case "j", "down":
177 if len(m.files) > 0 {
178 m.cursor = (m.cursor + 1) % len(m.files)
179 m.viewport.SetContent(m.renderContent())
180 }
181 return m, nil
182 case "k", "up":
183 if len(m.files) > 0 {
184 m.cursor = (m.cursor - 1 + len(m.files)) % len(m.files)
185 m.viewport.SetContent(m.renderContent())
186 }
187 return m, nil
188 case "enter", " ":
189 if m.expanded == m.cursor {
190 m.expanded = -1
191 } else {
192 m.expanded = m.cursor
193 }
194 m.viewport.SetContent(m.renderContent())
195 return m, nil
196 case "d", "D":
197 if m.cursor < len(m.files) {
198 m.confirm = m.cursor
199 m.viewport.SetContent(m.renderContent())
200 }
201 return m, nil
202 case "e", "E":
203 if m.cursor < len(m.files) {
204 f := m.files[m.cursor]
205 name := getString(f, "name")
206 disabled := getBool(f, "disabled")
207 newDisabled := !disabled
208 return m, func() tea.Msg {
209 err := m.client.ToggleAuthFile(name, newDisabled)
210 if err != nil {
211 return authActionMsg{err: err}
212 }
213 action := T("enabled")
214 if newDisabled {
215 action = T("disabled")
216 }
217 return authActionMsg{action: fmt.Sprintf("%s %s", action, name)}
218 }
219 }
220 return m, nil
221 case "1":
222 return m, m.startEdit(0) // prefix
223 case "2":
224 return m, m.startEdit(1) // proxy_url
225 case "3":
226 return m, m.startEdit(2) // priority
227 case "r":
228 m.status = ""
229 return m, m.fetchFiles
230 default:
231 var cmd tea.Cmd
232 m.viewport, cmd = m.viewport.Update(msg)
233 return m, cmd
234 }
235 }119 }
236 120
237 var cmd tea.Cmd121 var cmd tea.Cmd
@@ -442,3 +326,131 @@ func max(a, b int) int { · 204 unmodified lines
442 }326 }
443 return b327 return b
444}328}
329
330func (m authTabModel) handleEditInput(msg tea.KeyMsg) (authTabModel, tea.Cmd) {
331 switch msg.String() {
332 case "enter":
333 value := m.editInput.Value()
334 fieldKey := authEditableFields[m.editField].key
335 fileName := m.editFileName
336 m.editing = false
337 m.editInput.Blur()
338 fields := map[string]any{}
339 if fieldKey == "priority" {
340 p, err := strconv.Atoi(value)
341 if err != nil {
342 return m, func() tea.Msg {
343 return authActionMsg{err: fmt.Errorf("%s: %s", T("invalid_int"), value)}
344 }
345 }
346 fields[fieldKey] = p
347 } else {
348 fields[fieldKey] = value
349 }
350 return m, func() tea.Msg {
351 err := m.client.PatchAuthFileFields(fileName, fields)
352 if err != nil {
353 return authActionMsg{err: err}
354 }
355 return authActionMsg{action: fmt.Sprintf(T("updated_field"), fieldKey, fileName)}
356 }
357 case "esc":
358 m.editing = false
359 m.editInput.Blur()
360 m.viewport.SetContent(m.renderContent())
361 return m, nil
362 default:
363 var cmd tea.Cmd
364 m.editInput, cmd = m.editInput.Update(msg)
365 m.viewport.SetContent(m.renderContent())
366 return m, cmd
367 }
368}
369
370func (m authTabModel) handleConfirmInput(msg tea.KeyMsg) (authTabModel, tea.Cmd) {
371 switch msg.String() {
372 case "y", "Y":
373 idx := m.confirm
374 m.confirm = -1
375 if idx < len(m.files) {
376 name := getString(m.files[idx], "name")
377 return m, func() tea.Msg {
378 err := m.client.DeleteAuthFile(name)
379 if err != nil {
380 return authActionMsg{err: err}
381 }
382 return authActionMsg{action: fmt.Sprintf(T("deleted"), name)}
383 }
384 }
385 m.viewport.SetContent(m.renderContent())
386 return m, nil
387 case "n", "N", "esc":
388 m.confirm = -1
389 m.viewport.SetContent(m.renderContent())
390 return m, nil
391 }
392 return m, nil
393}
394
395func (m authTabModel) handleNormalInput(msg tea.KeyMsg) (authTabModel, tea.Cmd) {
396 switch msg.String() {
397 case "j", "down":
398 if len(m.files) > 0 {
399 m.cursor = (m.cursor + 1) % len(m.files)
400 m.viewport.SetContent(m.renderContent())
401 }
402 return m, nil
403 case "k", "up":
404 if len(m.files) > 0 {
405 m.cursor = (m.cursor - 1 + len(m.files)) % len(m.files)
406 m.viewport.SetContent(m.renderContent())
407 }
408 return m, nil
409 case "enter", " ":
410 if m.expanded == m.cursor {
411 m.expanded = -1
412 } else {
413 m.expanded = m.cursor
414 }
415 m.viewport.SetContent(m.renderContent())
416 return m, nil
417 case "d", "D":
418 if m.cursor < len(m.files) {
419 m.confirm = m.cursor
420 m.viewport.SetContent(m.renderContent())
421 }
422 return m, nil
423 case "e", "E":
424 if m.cursor < len(m.files) {
425 f := m.files[m.cursor]
426 name := getString(f, "name")
427 disabled := getBool(f, "disabled")
428 newDisabled := !disabled
429 return m, func() tea.Msg {
430 err := m.client.ToggleAuthFile(name, newDisabled)
431 if err != nil {
432 return authActionMsg{err: err}
433 }
434 action := T("enabled")
435 if newDisabled {
436 action = T("disabled")
437 }
438 return authActionMsg{action: fmt.Sprintf("%s %s", action, name)}
439 }
440 }
441 return m, nil
442 case "1":
443 return m, m.startEdit(0) // prefix
444 case "2":
445 return m, m.startEdit(1) // proxy_url
446 case "3":
447 return m, m.startEdit(2) // priority
448 case "r":
449 m.status = ""
450 return m, m.fetchFiles
451 default:
452 var cmd tea.Cmd
453 m.viewport, cmd = m.viewport.Update(msg)
454 return m, cmd
455 }
456}
internal/tui/dashboard.go+17−1
@@ -19,6 +19,12 @@ type dashboardModel struct { · 18 unmodified lines
19 width int19 width int
20 height int20 height int
21 ready bool21 ready bool
22
23 // Cached data for re-rendering on locale change
24 lastConfig map[string]any
25 lastUsage map[string]any
26 lastAuthFiles []map[string]any
27 lastAPIKeys []string
22}28}
23 29
24type dashboardDataMsg struct {30type dashboardDataMsg struct {
@@ -58,14 +64,24 @@ func (m dashboardModel) fetchData() tea.Msg { · 33 unmodified lines
58func (m dashboardModel) Update(msg tea.Msg) (dashboardModel, tea.Cmd) {64func (m dashboardModel) Update(msg tea.Msg) (dashboardModel, tea.Cmd) {
59 switch msg := msg.(type) {65 switch msg := msg.(type) {
60 case localeChangedMsg:66 case localeChangedMsg:
61 // Re-fetch data to re-render with new locale67 // Re-render immediately with cached data using new locale
68 m.content = m.renderDashboard(m.lastConfig, m.lastUsage, m.lastAuthFiles, m.lastAPIKeys)
69 m.viewport.SetContent(m.content)
70 // Also fetch fresh data in background
62 return m, m.fetchData71 return m, m.fetchData
72
63 case dashboardDataMsg:73 case dashboardDataMsg:
64 if msg.err != nil {74 if msg.err != nil {
65 m.err = msg.err75 m.err = msg.err
66 m.content = errorStyle.Render("⚠ Error: " + msg.err.Error())76 m.content = errorStyle.Render("⚠ Error: " + msg.err.Error())
67 } else {77 } else {
68 m.err = nil78 m.err = nil
79 // Cache data for locale switching
80 m.lastConfig = msg.config
81 m.lastUsage = msg.usage
82 m.lastAuthFiles = msg.authFiles
83 m.lastAPIKeys = msg.apiKeys
84
69 m.content = m.renderDashboard(msg.config, msg.usage, msg.authFiles, msg.apiKeys)85 m.content = m.renderDashboard(msg.config, msg.usage, msg.authFiles, msg.apiKeys)
70 }86 }
71 m.viewport.SetContent(m.content)87 m.viewport.SetContent(m.content)

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t