diff --git a/CLAUDE.md b/CLAUDE.md index af3cf19..e8aa0c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,9 +72,22 @@ Makefile Multi-arch build + S3 publish. (writes `envs`) → `StepDownloadWireguard` → `StepRunWireguard`. 4. Config forms: `StepAppConfig` → `StepServerConfig` → `StepDatabaseConfig` → `StepCertConfig`. -5. `StepGenerateFile` — writes `config.toml` (validates numeric fields first). -6. `StepRunDocker` — runs `app-dono-cliente` container. -7. `StepDone`. +5. `StepReview` — shows all collected config; Enter confirms. +6. `StepGenerateFile` — writes `config.toml` (validates numeric fields first). +7. `StepRunDocker` — runs `app-dono-cliente` container. +8. `StepDone`. + +### Navigation & error handling + +- **Back navigation:** `Esc` returns to the previous *input* step. The Model keeps a + `history []step` stack; `advance(next)` pushes the current input step, `goBack()` + pops. `isInputStep` ([steps.go](internal/tui/steps.go)) gates which steps participate + — action/wait steps (downloads, file gen, container runs) are excluded so back never + re-enters a side-effecting step. `Esc` is handled globally in `Update`; forms don't + consume it. Form values survive going back because the `FormStep`s live on the Model. +- **Retry vs. fix:** transient failures (image pull, container run) offer `r` to re-run + just that command (`q`/other quits). Validation failures (file generation) route back + to the relevant form to correct the value instead of quitting. ## Key constants diff --git a/README.md b/README.md index 7f45d6b..a02a3f1 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,18 @@ os arquivos de configuração (`config.toml` e `envs`) e sobe os containers nece ## Sumário -- [O que ele faz](#o-que-ele-faz) -- [Pré-requisitos](#pré-requisitos) -- [Como usar](#como-usar) -- [Fluxo de instalação](#fluxo-de-instalação) -- [Conectividade: IP público vs. vproxy](#conectividade-ip-público-vs-vproxy) -- [Arquivos gerados](#arquivos-gerados) -- [Containers e rede Docker](#containers-e-rede-docker) -- [Build a partir do código](#build-a-partir-do-código) -- [Documentação adicional](#documentação-adicional) +- [App do Dono — Instalador Cliente (TUI)](#app-do-dono--instalador-cliente-tui) + - [Sumário](#sumário) + - [O que ele faz](#o-que-ele-faz) + - [Pré-requisitos](#pré-requisitos) + - [Como usar](#como-usar) + - [Navegação na interface](#navegação-na-interface) + - [Fluxo de instalação](#fluxo-de-instalação) + - [Conectividade: IP público vs. vproxy](#conectividade-ip-público-vs-vproxy) + - [Arquivos gerados](#arquivos-gerados) + - [Containers e rede Docker](#containers-e-rede-docker) + - [Build a partir do código](#build-a-partir-do-código) + - [Documentação adicional](#documentação-adicional) --- @@ -80,6 +83,8 @@ go run ./cmd | `Shift+Tab` / `↑` | Campo anterior | | `←` / `→` | Alternar opção (campos de seleção) | | `Enter` | Confirmar campo / avançar etapa | +| `Esc` | Voltar à etapa anterior | +| `r` | Tentar novamente (em telas de erro) | | Qualquer tecla | Avançar em telas de status | | `Ctrl+C` | Sair a qualquer momento | @@ -111,11 +116,15 @@ go run ./cmd │ │ ▼ ▼ ┌─────────────────────────────────────────┐ -│ Config. Aplicação → Servidor → │ -│ Banco de Dados → Certificados │ +│ Config. Aplicação → Servidor → │ +│ Banco de Dados → Certificados │ └────────────────────┬────────────────────┘ ▼ ┌──────────────────┐ + │ Revisão (Review) │ ← esc volta para editar + └────────┬─────────┘ + ▼ + ┌──────────────────┐ │ Gera config.toml │ └────────┬─────────┘ ▼ diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..a3708d6 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,48 @@ +# TODO — UX / UI improvements + +Backlog of usability and interface improvements for the installer TUI. Ordered by +value-to-effort. Status: ☐ pending · ☑ done. + +## High value + +- ☑ **1. Back navigation between steps.** Maintain a history stack of *input* steps + (forms + IP question + review) and bind `Esc` to go back. Action steps (download, + generate, run) are not part of the stack and are not re-enterable via back. Form + values are preserved because the `FormStep`s live on the `Model`. + +- ☑ **2. Review/summary step before running containers.** Show all collected config + (`StepReview`) for confirmation before `StepGenerateFile`/`StepRunDocker`. Natural + anchor for "go back and edit a section" via `Esc`. + +- ☑ **3. Retry instead of quit on transient errors.** Image pull / container run + failures offer `r: tentar novamente` (re-runs just that command) instead of throwing + away the whole session. Validation errors (generate file) route back to the relevant + form to fix the value. + +## Medium value + +- ☐ **4. Center the layout (vertically + horizontally).** Use + `lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, body)` — the + Model already tracks `width`/`height`. Caveat: every `viewXxx`/`form.View` currently + hardcodes a manual left pad (`strings.Repeat(" ", padding)`); to center cleanly, + strip that and wrap the body in one padded/bordered box, then `Place` it. Consider + pinning the help footer to the bottom rather than centering it with the body. + +- ☐ **5. Inline field validation.** Numeric validation currently happens only at file + write time (`WriteConfigFile`), far from input. Wire `textinput.Validate` per field + and render a red hint under invalid fields in `form.go`. + +- ☐ **6. Step indicator.** Show "Etapa N / Total" or a breadcrumb in the header. Total + is dynamic because the vproxy branch adds steps. + +## Low value / cosmetic + +- ☐ **7. Replace the fake progress bar.** `viewCheckDocker` animates a random-increment + bar for an instant `LookPath` check, then waits for a keypress. Use a spinner that + resolves immediately (or auto-advance on success). + +- ☐ **8. Bordered card + `bubbles/help` footer.** Wrap the body in a rounded + `lipgloss` border and render the footer via `bubbles/help` with a proper key map and + a `?` toggle. Pairs with #4. + +- ☐ **9. Password reveal toggle** (`ctrl+r`) on the login password field. diff --git a/docs/fluxo.md b/docs/fluxo.md index 7e22e5f..33a8250 100644 --- a/docs/fluxo.md +++ b/docs/fluxo.md @@ -21,9 +21,23 @@ executadas em segundo plano. As etapas são definidas em | 11 | `StepServerConfig` | Formulário do servidor (porta, timeout, ambiente, compatibilidade). | | 12 | `StepDatabaseConfig` | Formulário do banco de dados. | | 13 | `StepCertConfig` | Formulário do diretório de certificados. | -| 14 | `StepGenerateFile` | Gera o `config.toml`. | -| 15 | `StepRunDocker` | Sobe o container `app-dono-cliente`. | -| 16 | `StepDone` | Mensagem de sucesso. | +| 14 | `StepReview` | Revisão de todas as configurações antes de instalar. | +| 15 | `StepGenerateFile` | Gera o `config.toml`. | +| 16 | `StepRunDocker` | Sobe o container `app-dono-cliente`. | +| 17 | `StepDone` | Mensagem de sucesso. | + +## Navegação e tratamento de erros + +- **Voltar (`Esc`):** retorna à etapa de entrada anterior (formulários, pergunta de IP + e revisão). O `Model` mantém uma pilha `history` de etapas; etapas de ação (downloads, + geração de arquivo, subida de container) ficam de fora e não são reexecutadas ao + voltar. Os valores já digitados nos formulários são preservados. +- **Revisão (`StepReview`):** antes de gravar qualquer arquivo, todas as configurações + coletadas são exibidas para confirmação. `Enter` confirma e gera o `config.toml`; + `Esc` volta ao formulário anterior para editar. +- **Tentar novamente:** falhas transitórias (download de imagem, subida de container) + oferecem `r` para reexecutar apenas aquele passo (`q` sai). Erros de validação + (geração de arquivo) levam de volta ao formulário correspondente para correção. ## Detalhamento diff --git a/internal/tui/model.go b/internal/tui/model.go index ad7ccc5..d7b67c4 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -12,6 +12,7 @@ import ( type Model struct { currentStep step + history []step // stack of input steps visited, for back navigation cursor int width int height int @@ -263,3 +264,31 @@ func InitialModel() Model { func (m Model) Init() tea.Cmd { return tea.Batch(CheckDockerCmd(), TickCmd(), m.spinner.Tick) } + +// advance moves to the next step, recording the current step on the history stack when +// it is an input step so that Esc can return to it. Consecutive duplicates are skipped. +func (m *Model) advance(next step) { + if isInputStep(m.currentStep) { + if n := len(m.history); n == 0 || m.history[n-1] != m.currentStep { + m.history = append(m.history, m.currentStep) + } + } + m.currentStep = next +} + +// goBack returns to the previous input step on the history stack, if any. It reports +// whether the step changed. +func (m *Model) goBack() bool { + if len(m.history) == 0 { + return false + } + last := m.history[len(m.history)-1] + m.history = m.history[:len(m.history)-1] + m.currentStep = last + return true +} + +// canGoBack reports whether Esc would return to a previous step from the current one. +func (m Model) canGoBack() bool { + return isInputStep(m.currentStep) && len(m.history) > 0 +} diff --git a/internal/tui/nav_test.go b/internal/tui/nav_test.go new file mode 100644 index 0000000..941d52f --- /dev/null +++ b/internal/tui/nav_test.go @@ -0,0 +1,105 @@ +package tui + +import "testing" + +func TestIsInputStep(t *testing.T) { + input := []step{ + StepDockerLogin, StepIPQuestion, StepWireguardConfig, + StepAppConfig, StepServerConfig, StepDatabaseConfig, + StepCertConfig, StepReview, + } + for _, s := range input { + if !isInputStep(s) { + t.Errorf("isInputStep(%d) = false, want true", s) + } + } + + action := []step{ + StepCheckDocker, StepDockerInstall, StepDownloadImage, + StepGenerateWireguardFile, StepDownloadWireguard, StepRunWireguard, + StepGenerateFile, StepRunDocker, StepDone, + } + for _, s := range action { + if isInputStep(s) { + t.Errorf("isInputStep(%d) = true, want false", s) + } + } +} + +func TestAdvancePushesInputSteps(t *testing.T) { + m := Model{currentStep: StepAppConfig} + + m.advance(StepServerConfig) + if m.currentStep != StepServerConfig { + t.Fatalf("currentStep = %d, want StepServerConfig", m.currentStep) + } + if len(m.history) != 1 || m.history[0] != StepAppConfig { + t.Fatalf("history = %v, want [StepAppConfig]", m.history) + } +} + +func TestAdvanceSkipsActionSteps(t *testing.T) { + // Advancing away from an action step must not record it on the stack. + m := Model{currentStep: StepDownloadImage} + + m.advance(StepIPQuestion) + if len(m.history) != 0 { + t.Fatalf("history = %v, want empty (action step not recorded)", m.history) + } +} + +func TestAdvanceDedupesConsecutive(t *testing.T) { + m := Model{currentStep: StepIPQuestion} + + m.advance(StepIPQuestion) // self-advance (e.g. re-entering) must not duplicate + m.currentStep = StepIPQuestion + m.advance(StepIPQuestion) + + if len(m.history) > 1 { + t.Fatalf("history = %v, want at most one entry", m.history) + } +} + +func TestGoBack(t *testing.T) { + m := Model{currentStep: StepCertConfig, history: []step{StepAppConfig, StepServerConfig, StepDatabaseConfig}} + + if !m.goBack() { + t.Fatal("goBack() = false, want true") + } + if m.currentStep != StepDatabaseConfig { + t.Errorf("currentStep = %d, want StepDatabaseConfig", m.currentStep) + } + if len(m.history) != 2 { + t.Errorf("history len = %d, want 2", len(m.history)) + } +} + +func TestGoBackEmptyHistory(t *testing.T) { + m := Model{currentStep: StepDockerLogin} + if m.goBack() { + t.Error("goBack() = true on empty history, want false") + } + if m.currentStep != StepDockerLogin { + t.Error("currentStep changed on empty goBack") + } +} + +func TestCanGoBack(t *testing.T) { + // Input step with history -> can go back. + m := Model{currentStep: StepServerConfig, history: []step{StepAppConfig}} + if !m.canGoBack() { + t.Error("canGoBack() = false, want true for input step with history") + } + + // Input step, empty history -> cannot. + m = Model{currentStep: StepDockerLogin} + if m.canGoBack() { + t.Error("canGoBack() = true, want false with empty history") + } + + // Action step with history -> cannot (esc handled by the step, not global back). + m = Model{currentStep: StepDownloadImage, history: []step{StepDockerLogin}} + if m.canGoBack() { + t.Error("canGoBack() = true on action step, want false") + } +} diff --git a/internal/tui/steps.go b/internal/tui/steps.go index 5680ad4..8e1730a 100644 --- a/internal/tui/steps.go +++ b/internal/tui/steps.go @@ -24,8 +24,24 @@ const ( StepDatabaseConfig StepCertConfig + // Review + StepReview + // Finalizing StepGenerateFile StepRunDocker StepDone ) + +// isInputStep reports whether a step collects user input and therefore participates +// in back navigation. Action/wait steps (downloads, file generation, container runs) +// are excluded so that "back" never re-enters a side-effecting step. +func isInputStep(s step) bool { + switch s { + case StepDockerLogin, StepIPQuestion, StepWireguardConfig, + StepAppConfig, StepServerConfig, StepDatabaseConfig, + StepCertConfig, StepReview: + return true + } + return false +} diff --git a/internal/tui/update.go b/internal/tui/update.go index 1c71191..6d0b474 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -20,6 +20,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch key.String() { case "ctrl+c": return m, tea.Quit + case "esc": + // Global "back" on input steps. Action/wait steps handle esc themselves. + if m.canGoBack() { + m.goBack() + return m, nil + } } } @@ -43,7 +49,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if done { m.configValues.Login = m.loginForm.Values() - m.currentStep = StepDownloadImage + m.advance(StepDownloadImage) return m, DownloadImageCmd(imageName, m.configValues.Login["user"], m.configValues.Login["password"]) } @@ -64,7 +70,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.downloadDone = false m.downloadMessage = "" m.downloadError = nil - m.currentStep = StepGenerateWireguardFile + m.advance(StepGenerateWireguardFile) return m, GenerateWireguardConfigFile(m.configValues, wireguardConfigPath) } @@ -82,7 +88,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if done { m.configValues.Application = m.appForm.Values() - m.currentStep = StepServerConfig + m.advance(StepServerConfig) } return m, cmd @@ -91,7 +97,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if done { m.configValues.Server = m.serverForm.Values() - m.currentStep = StepDatabaseConfig + m.advance(StepDatabaseConfig) } return m, cmd @@ -100,7 +106,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if done { m.configValues.Database = m.dbForm.Values() - m.currentStep = StepCertConfig + m.advance(StepCertConfig) } return m, cmd @@ -109,15 +115,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if done { m.configValues.Cert = m.certForm.Values() - m.currentStep = StepGenerateFile - - m.finishedFile = false - m.configFileError = nil - - return m, GenerateConfigFile(m.configValues, configPath) + m.advance(StepReview) } return m, cmd + case StepReview: + return m.updateReview(msg) case StepGenerateFile: return m.updateGenerateFile(msg) case StepRunDocker: @@ -146,9 +149,9 @@ func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) { if m.checkDockerDone && m.checkProgress == 1 { if m.dockerInstalled { m.loading = true - m.currentStep = StepDockerLogin + m.advance(StepDockerLogin) } else { - m.currentStep = StepDockerInstall + m.advance(StepDockerInstall) } } } @@ -175,9 +178,17 @@ func (m Model) updateDownloadImage(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: if m.downloadDone && m.downloadError == nil { - m.currentStep = StepIPQuestion + m.advance(StepIPQuestion) } else if m.downloadDone { - return m, tea.Quit + switch msg.String() { + case "r": + m.downloadDone = false + m.downloadMessage = "" + m.downloadError = nil + return m, DownloadImageCmd(imageName, m.configValues.Login["user"], m.configValues.Login["password"]) + default: + return m, tea.Quit + } } } @@ -201,11 +212,11 @@ func (m Model) updateIPQuestion(msg tea.Msg) (tea.Model, tea.Cmd) { case "enter": // Yes if m.cursor == 0 { - m.currentStep = StepAppConfig + m.advance(StepAppConfig) return m, nil } - m.currentStep = StepWireguardConfig + m.advance(StepWireguardConfig) } } @@ -229,7 +240,15 @@ func (m Model) updateDownloadWireguard(msg tea.Msg) (tea.Model, tea.Cmd) { if m.downloadDone && m.downloadError == nil { m.currentStep = StepRunWireguard } else if m.downloadDone { - return m, tea.Quit + switch msg.String() { + case "r": + m.downloadDone = false + m.downloadMessage = "" + m.downloadError = nil + return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"]) + default: + return m, tea.Quit + } } } @@ -250,7 +269,12 @@ func (m Model) updateGenerateWireguardFile(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: if m.finishedFile && m.configFileError != nil { - return m, tea.Quit + // Validation error (e.g. MTU): go back to the form to correct it. The form + // is the top of the history stack, so goBack lands on it without leaving a + // duplicate entry; fall back to a direct set if history is unexpectedly empty. + if !m.goBack() { + m.currentStep = StepWireguardConfig + } } else if m.finishedFile && m.configFileError == nil { m.currentStep = StepDownloadWireguard @@ -269,9 +293,36 @@ func (m Model) updateRunWireguardDocker(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: if m.finishedDockerRun && m.dockerRunError != nil { - return m, tea.Quit + switch msg.String() { + case "r": + m.finishedDockerRun = false + m.dockerRunError = nil + return m, RunWireguardContainer(wireguardConfigPath, m.configValues) + default: + return m, tea.Quit + } } else if m.finishedDockerRun && m.dockerRunError == nil { - m.currentStep = StepAppConfig + m.advance(StepAppConfig) + } + } + + return m, nil +} + +// updateReview shows the collected configuration for confirmation before any files are +// written. Enter confirms and generates config.toml; Esc (handled globally) goes back +// to the previous form to edit. +func (m Model) updateReview(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "enter": + m.advance(StepGenerateFile) + + m.finishedFile = false + m.configFileError = nil + + return m, GenerateConfigFile(m.configValues, configPath) } } @@ -286,7 +337,11 @@ func (m Model) updateGenerateFile(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: if m.finishedFile && m.configFileError != nil { - return m, tea.Quit + // Validation error: return to the review (top of the stack) so the user can + // navigate back to the forms; direct set as a fallback. + if !m.goBack() { + m.currentStep = StepReview + } } else if m.finishedFile && m.configFileError == nil { m.currentStep = StepRunDocker @@ -314,7 +369,20 @@ func (m Model) updateRunDocker(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: if m.finishedDockerRun && m.dockerRunError != nil { - return m, tea.Quit + switch msg.String() { + case "r": + m.finishedDockerRun = false + m.dockerRunError = nil + return m, RunAppContainer( + imageName, + "app-dono-cliente", + configPath, + fmt.Sprintf("/app/%s", configPath), + m.configValues, + ) + default: + return m, tea.Quit + } } else if m.finishedDockerRun && m.dockerRunError == nil { m.currentStep = StepDone } diff --git a/internal/tui/view.go b/internal/tui/view.go index 831f4b6..8782c31 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -10,15 +10,33 @@ import ( const ( header = "App do Dono - Instalador Cliente" - defaultMsg = "ctrl+c: sair" anyKeyOutMsg = "qualquer tecla: sair" - formMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" + retryMsg = "r: tentar novamente • q: sair" + fixMsg = "qualquer tecla: voltar e corrigir" + + formBase = "tab: próximo campo • enter: confirmar" + ipBase = "↑/↓: navegar • enter: selecionar" + reviewBase = "enter: confirmar e instalar" ) +// helpFor builds the footer for an input step, appending "esc: voltar" when back +// navigation is available and always ending with "ctrl+c: sair". +func (m Model) helpFor(base string) string { + parts := []string{} + if base != "" { + parts = append(parts, base) + } + if m.canGoBack() { + parts = append(parts, "esc: voltar") + } + parts = append(parts, "ctrl+c: sair") + return strings.Join(parts, " • ") +} + func (m Model) View() tea.View { pad := strings.Repeat(" ", padding) var body string - helpMsg := defaultMsg + helpMsg := m.helpFor("") switch m.currentStep { // Docker stuff @@ -29,53 +47,65 @@ func (m Model) View() tea.View { helpMsg = anyKeyOutMsg case StepDockerLogin: body = m.loginForm.View() - helpMsg = formMsg + helpMsg = m.helpFor(formBase) case StepDownloadImage: body = m.viewDownloadImage() + if m.downloadDone && m.downloadError != nil { + helpMsg = retryMsg + } // IP Stuff case StepIPQuestion: body = m.viewIPQuestion() + helpMsg = m.helpFor(ipBase) case StepWireguardConfig: body = m.wireguardForm.View() - helpMsg = formMsg + helpMsg = m.helpFor(formBase) case StepGenerateWireguardFile: body = m.viewGenerateFile() if m.finishedFile && m.configFileError != nil { - helpMsg = anyKeyOutMsg + helpMsg = fixMsg } case StepDownloadWireguard: body = m.viewDownloadImage() + if m.downloadDone && m.downloadError != nil { + helpMsg = retryMsg + } case StepRunWireguard: body = m.viewDockerRun() if m.finishedDockerRun && m.dockerRunError != nil { - helpMsg = anyKeyOutMsg + helpMsg = retryMsg } // App Config Stuff case StepAppConfig: body = m.appForm.View() - helpMsg = formMsg + helpMsg = m.helpFor(formBase) case StepServerConfig: body = m.serverForm.View() - helpMsg = formMsg + helpMsg = m.helpFor(formBase) case StepDatabaseConfig: body = m.dbForm.View() - helpMsg = formMsg + helpMsg = m.helpFor(formBase) case StepCertConfig: body = m.certForm.View() - helpMsg = formMsg + helpMsg = m.helpFor(formBase) + + // Review + case StepReview: + body = m.viewReview() + helpMsg = m.helpFor(reviewBase) // Finalize case StepGenerateFile: body = m.viewGenerateFile() if m.finishedFile && m.configFileError != nil { - helpMsg = anyKeyOutMsg + helpMsg = fixMsg } case StepRunDocker: body = m.viewDockerRun() if m.finishedDockerRun && m.dockerRunError != nil { - helpMsg = anyKeyOutMsg + helpMsg = retryMsg } case StepDone: body = m.viewDoneMessage() @@ -155,7 +185,7 @@ func (m Model) viewDownloadImage() string { errText := dualPad + m.downloadMessage + "\n" + dualPad + m.downloadError.Error() sb.WriteString(ErrorStyle.Width(m.width - (padding * 2)).Align(lipgloss.Left).Render(errText)) - sb.WriteString("\n\n" + pad + "Pressione qualquer tecla para sair.") + sb.WriteString("\n\n" + pad + "Pressione 'r' para tentar novamente ou 'q' para sair.") } return sb.String() @@ -187,6 +217,51 @@ func (m Model) viewIPQuestion() string { return sb.String() } +func (m Model) viewReview() string { + pad := strings.Repeat(" ", padding) + cv := m.configValues + + var sb strings.Builder + sb.WriteString(pad + "Revise as configurações antes de instalar:\n") + + section := func(title string, rows [][2]string) { + sb.WriteString("\n" + pad + TitleStyle.Render(title) + "\n") + for _, r := range rows { + sb.WriteString(pad + HelpStyle.Render(r[0]+": ") + r[1] + "\n") + } + } + + if len(cv.Wireguard) > 0 { + section("vproxy", [][2]string{ + {"IP Virtual", cv.Wireguard["vip"]}, + {"Proxy EDPS", cv.Wireguard["proxy_edps"]}, + {"MTU", cv.Wireguard["mtu"]}, + {"Protocolo", cv.Wireguard["proto"]}, + }) + } + + section("Aplicação", [][2]string{ + {"Servidor Central", cv.Application["central_server_url"]}, + {"Token de Inscrição", cv.Application["enrollment_token"]}, + }) + section("Servidor", [][2]string{ + {"Porta (host)", cv.Server["port"]}, + {"Timeout", cv.Server["timeout"]}, + {"Ambiente", cv.Server["environment"]}, + {"Modo Compatibilidade", cv.Server["seccomp_unconfined"]}, + }) + section("Banco de Dados", [][2]string{ + {"Tipo", cv.Database["database_type"]}, + {"URL", cv.Database["database_url"]}, + {"Conexões (máx/mín)", cv.Database["max_conns"] + "/" + cv.Database["min_conns"]}, + }) + section("Certificado", [][2]string{ + {"Diretório", cv.Cert["cert_dir_path"]}, + }) + + return sb.String() +} + func (m Model) viewGenerateFile() string { pad := strings.Repeat(" ", padding) var sb strings.Builder @@ -205,7 +280,7 @@ func (m Model) viewGenerateFile() string { errText := dualPad + m.configFileError.Error() sb.WriteString(ErrorStyle.Width(m.width - (padding * 2)).Align(lipgloss.Left).Render(errText)) - sb.WriteString("\n\n" + pad + "Tente novamente.") + sb.WriteString("\n\n" + pad + "Pressione qualquer tecla para voltar e corrigir os dados.") } } @@ -230,7 +305,7 @@ func (m Model) viewDockerRun() string { errText := dualPad + m.dockerRunError.Error() sb.WriteString(ErrorStyle.Width(m.width - (padding * 2)).Align(lipgloss.Left).Render(errText)) - sb.WriteString("\n\n" + pad + "Tente novamente.") + sb.WriteString("\n\n" + pad + "Pressione 'r' para tentar novamente ou 'q' para sair.") } }