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
511511 password = localMgmtPassword
512512 }
513513
514− // Ensure management routes are registered (secret-key must be set)
515− if cfg.RemoteManagement.SecretKey == "" {
516− cfg.RemoteManagement.SecretKey = "$tui-placeholder$"
517− }
518−
519514 // Start server in background
520515 cancel, done := cmd.StartServiceBackground(cfg, configFilePath, password)
521516
522− // Wait for server to be ready by polling management API
517+ // Wait for server to be ready by polling management API with exponential backoff
523518 {
524519 client := tui.NewClient(cfg.Port, password)
525− for i := 0; i < 50; i++ {
526− time.Sleep(100 * time.Millisecond)
520+ backoff := 100 * time.Millisecond
521+ // Try for up to ~10-15 seconds
522+ for i := 0; i < 30; i++ {
527523 if _, err := client.GetConfig(); err == nil {
528524 break
529525 }
526+ time.Sleep(backoff)
527+ if backoff < 1*time.Second {
528+ backoff = time.Duration(float64(backoff) * 1.5)
529+ }
530530 }
531531 }
532532
go.mod+1−1
@@ -1,6 +1,6 @@
11 module github.com/router-for-me/CLIProxyAPI/v6
22
3−go 1.24.2
3+go 1.26.0
44
55 require (
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
284284 optionState.routerConfigurator(engine, s.handlers, cfg)
285285 }
286286
287− // Register management routes when configuration or environment secrets are available.
288− hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret
287+ // Register management routes when configuration or environment secrets are available,
288+ // or when a local management password is provided (e.g. TUI mode).
289+ hasManagementSecret := cfg.RemoteManagement.SecretKey != "" || envManagementSecret || s.localPassword != ""
289290 s.managementRoutesEnabled.Store(hasManagementSecret)
290291 if hasManagementSecret {
291292 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
103103 case "L":
104104 ToggleLocale()
105105 a.tabs = TabNames()
106− // Broadcast locale change to ALL tabs so each re-renders
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...)
106+ return a.broadcastToAllTabs(localeChangedMsg{})
138107 case "tab":
139108 prevTab := a.activeTab
140109 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
278247 _, err := p.Run()
279248 return err
280249 }
250+
251+func (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
106106 case tea.KeyMsg:
107107 // ---- Editing mode ----
108108 if m.editing {
109− switch msg.String() {
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" {
118−p, err := strconv.Atoi(value)
119−if 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− }
109+ return m.handleEditInput(msg)
146110 }
147111
148112 // ---- Delete confirmation mode ----
149113 if m.confirm >= 0 {
150− switch msg.String() {
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
114+ return m.handleConfirmInput(msg)
172115 }
173116
174117 // ---- Normal mode ----
175− switch msg.String() {
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− }
118+ return m.handleNormalInput(msg)
235119 }
236120
237121 var cmd tea.Cmd
@@ -442,3 +326,131 @@ func max(a, b int) int { · 204 unmodified lines
442326 }
443327 return b
444328 }
329+
330+func (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+
370+func (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+
395+func (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
1919 width int
2020 height int
2121 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
2228 }
2329
2430 type dashboardDataMsg struct {
@@ -58,14 +64,24 @@ func (m dashboardModel) fetchData() tea.Msg { · 33 unmodified lines
5864 func (m dashboardModel) Update(msg tea.Msg) (dashboardModel, tea.Cmd) {
5965 switch msg := msg.(type) {
6066 case localeChangedMsg:
61− // Re-fetch data to re-render with new locale
67+ // 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
6271 return m, m.fetchData
72+
6373 case dashboardDataMsg:
6474 if msg.err != nil {
6575 m.err = msg.err
6676 m.content = errorStyle.Render("⚠ Error: " + msg.err.Error())
6777 } else {
6878 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+
6985 m.content = m.renderDashboard(msg.config, msg.usage, msg.authFiles, msg.apiKeys)
7086 }
7187 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