package ui import ( "fmt" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) func newMenuList() list.Model { items := []list.Item{ simpleItem{id: "users", title: "Пользователи", desc: "Создать, заблокировать, удалить учётные записи"}, simpleItem{id: "catalog", title: "Хосты и сервисы", desc: "Общий каталог: адреса и порты, один раз на всех, группы просто выбирают из него"}, simpleItem{id: "groups", title: "Группы", desc: "Какие сервисы из каталога входят в каждую группу"}, simpleItem{id: "export", title: "Экспорт архивов", desc: "Сгенерировать ключи и .7z архивы для доставки пользователям"}, simpleItem{id: "configupdate", title: "Обновить конфиги", desc: "Перевыпустить только config (без ключа) для тех, у кого изменился доступ"}, simpleItem{id: "audit", title: "Аудит / кто в сети", desc: "Активные сессии и журнал подключений"}, simpleItem{id: "quit", title: "Выход", desc: ""}, } return newList(items, "ssh-access-tui", 0, 0) } func (m Model) updateMenu(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "enter": sel, ok := m.menu.SelectedItem().(simpleItem) if !ok { return m, nil } m.err, m.status = "", "" switch sel.id { case "users": m.screen = scrUsers m.refreshUsersList() case "catalog": m.screen = scrCatalogHosts m.refreshCatalogHostsList() case "groups": m.screen = scrGroups m.refreshGroupsList() case "export": m.screen = scrExportSelect m.initExportSelect() case "configupdate": m.screen = scrConfigUpdateSelect m.initConfigUpdateSelect() case "audit": m.screen = scrAudit m.refreshAudit() case "quit": return m.attemptQuit() } return m, nil case "q": return m.attemptQuit() } } var cmd tea.Cmd m.menu, cmd = m.menu.Update(msg) return m, cmd } // viewMenu appends a live summary of pending follow-up work (users with no // key yet, users whose config no longer matches their real access) below // the menu, recomputed on every render — so it can never go stale itself. func (m Model) viewMenu() string { body := m.menu.View() neverExported, stale := m.quitWarnings() if len(neverExported) == 0 && len(stale) == 0 { return borderStyle.Render(body) } notice := "" if len(neverExported) > 0 { notice += warnStyle.Render(fmt.Sprintf("Без ключа: %d (см. «Экспорт архивов»)", len(neverExported))) + "\n" } if len(stale) > 0 { notice += warnStyle.Render(fmt.Sprintf("Устаревший конфиг: %d (см. «Обновить конфиги»)", len(stale))) + "\n" } return borderStyle.Render(body + "\n" + notice) } // quitWarnings splits active users into the two categories worth warning // about before quitting: those who never got a key at all, and those whose // access changed since their last config (full export or config-only // update) — the latter are running with an out-of-date client config that // won't reach everything they're now allowed to. func (m *Model) quitWarnings() (neverExported, stale []string) { st := m.loadState() return st.NeverExportedUsernames(), st.StaleConfigUsernames() } // attemptQuit is the only path that actually quits from the menu ("q" or // selecting "Выход"): if there are users created but never exported, or // users whose config fell out of date, it warns instead of quitting // straight away — both are easy to lose track of mid-session. ctrl+c // bypasses this deliberately, as an always-available hard exit. func (m Model) attemptQuit() (tea.Model, tea.Cmd) { neverExported, stale := m.quitWarnings() if len(neverExported) == 0 && len(stale) == 0 { return m, tea.Quit } m.quitPending = neverExported m.quitStale = stale m.quitReturnTo = scrMenu m.screen = scrQuitConfirm return m, nil } func (m Model) updateQuitConfirm(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "enter", "y": return m, tea.Quit case "esc", "n": m.screen = m.quitReturnTo return m, nil } } return m, nil } func (m Model) viewQuitConfirm() string { body := "" if len(m.quitPending) > 0 { body += warnStyle.Render("Без выданного ключа (не были в «Экспорт архивов»):") + "\n" for _, u := range m.quitPending { body += " - " + u + "\n" } body += "\n" } if len(m.quitStale) > 0 { body += warnStyle.Render("Доступ изменился, но конфиг у пользователя старый (см. «Обновить конфиги»):") + "\n" for _, u := range m.quitStale { body += " - " + u + "\n" } body += "\n" } body += "Без этого у части пользователей либо нет доступа вообще, либо их текущий\n" body += "config не соответствует реальным правам (не хватает новых сервисов, или\n" body += "остались лишние, которых уже нет).\n" body += "\n[enter/y] выйти всё равно [esc/n] вернуться" return borderStyle.Render(body) }