feat(cli): integrate session purging into purge command
- commit
- e24e5cf5bda464684ff3cbae115a54665ee4f657
- author
- brice · · 2025-12-25 10:17 UTC
- parent
- 3630652
- signature
- unsigned
2 files · +276 -74
M internal/cli/cmd/purge.go +34 -5
| @@ -24,11 +24,12 @@ var purgeCmd = &cobra.Command{ | |||
| 24 | 24 | ||
| 25 | 25 | This can remove: | |
| 26 | 26 | - Config directory | |
| 27 | - | - Data directory | |
| 27 | + | - Data directory (includes database) | |
| 28 | 28 | - State directory | |
| 29 | 29 | - Cache directory | |
| 30 | 30 | - Content filter caches | |
| 31 | 31 | - Desktop integration files | |
| 32 | + | - Inactive browser sessions | |
| 32 | 33 | ||
| 33 | 34 | Use --force to remove everything without prompting.`, | |
| 34 | 35 | RunE: runPurge, | |
| @@ -54,20 +55,48 @@ func runPurge(_ *cobra.Command, _ []string) error { | |||
| 54 | 55 | return runPurgeForce(app, purgeUC) | |
| 55 | 56 | } | |
| 56 | 57 | ||
| 57 | - | m := model.NewPurgeModel(app.Theme, purgeUC) | |
| 58 | + | // Build session config from app dependencies | |
| 59 | + | cfg := model.PurgeModelConfig{ | |
| 60 | + | ListSessionsUC: app.ListSessionsUC, | |
| 61 | + | DeleteSessionUC: app.DeleteSessionUC, | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | m := model.NewPurgeModel(app.Ctx(), app.Theme, purgeUC, cfg) | |
| 58 | 65 | p := tea.NewProgram(m, tea.WithAltScreen()) | |
| 59 | 66 | _, err := p.Run() | |
| 60 | 67 | return err | |
| 61 | 68 | } | |
| 62 | 69 | ||
| 63 | 70 | func runPurgeForce(app *cli.App, purgeUC *usecase.PurgeDataUseCase) error { | |
| 64 | - | out, err := purgeUC.PurgeAll(app.Ctx()) | |
| 71 | + | ctx := app.Ctx() | |
| 72 | + | theme := app.Theme | |
| 73 | + | ||
| 74 | + | // First, delete all inactive sessions (before we delete the database) | |
| 75 | + | if app.ListSessionsUC != nil && app.DeleteSessionUC != nil { | |
| 76 | + | output, err := app.ListSessionsUC.GetPurgeableSessions(ctx) | |
| 77 | + | if err == nil && output != nil { | |
| 78 | + | for _, s := range output.Sessions { | |
| 79 | + | sessionID := s.Info.Session.ID | |
| 80 | + | if delErr := app.DeleteSessionUC.Execute(ctx, usecase.DeleteSessionInput{ | |
| 81 | + | SessionID: sessionID, | |
| 82 | + | CurrentSessionID: "", // CLI has no current session | |
| 83 | + | }); delErr != nil { | |
| 84 | + | fmt.Printf("%s Session %s: %v\n", theme.ErrorStyle.Render(styles.IconX), sessionID, delErr) | |
| 85 | + | } else { | |
| 86 | + | fmt.Printf("%s Session %s\n", theme.SuccessStyle.Render(styles.IconCheck), sessionID) | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // Then purge all directories | |
| 93 | + | out, err := purgeUC.PurgeAll(ctx) | |
| 65 | 94 | if out != nil { | |
| 66 | 95 | for _, r := range out.Results { | |
| 67 | 96 | if r.Success { | |
| 68 | - | fmt.Printf("%s %s\n", app.Theme.SuccessStyle.Render(styles.IconCheck), r.Target.Path) | |
| 97 | + | fmt.Printf("%s %s\n", theme.SuccessStyle.Render(styles.IconCheck), r.Target.Path) | |
| 69 | 98 | } else { | |
| 70 | - | fmt.Printf("%s %s: %v\n", app.Theme.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error) | |
| 99 | + | fmt.Printf("%s %s: %v\n", theme.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error) | |
| 71 | 100 | } | |
| 72 | 101 | } | |
| 73 | 102 | } | |
| @@ -24,11 +24,12 @@ var purgeCmd = &cobra.Command{ | |||||
| 24 | 24 | ||||
| 25 | This can remove: | 25 | This can remove: | ||
| 26 | - Config directory | 26 | - Config directory | ||
| 27 | - | - Data directory | 27 | + | - Data directory (includes database) |
| 28 | - State directory | 28 | - State directory | ||
| 29 | - Cache directory | 29 | - Cache directory | ||
| 30 | - Content filter caches | 30 | - Content filter caches | ||
| 31 | - Desktop integration files | 31 | - Desktop integration files | ||
| 32 | + | - Inactive browser sessions | |||
| 32 | 33 | ||||
| 33 | Use --force to remove everything without prompting.`, | 34 | Use --force to remove everything without prompting.`, | ||
| 34 | RunE: runPurge, | 35 | RunE: runPurge, | ||
| @@ -54,20 +55,48 @@ func runPurge(_ *cobra.Command, _ []string) error { | |||||
| 54 | return runPurgeForce(app, purgeUC) | 55 | return runPurgeForce(app, purgeUC) | ||
| 55 | } | 56 | } | ||
| 56 | 57 | ||||
| 57 | - | m := model.NewPurgeModel(app.Theme, purgeUC) | 58 | + | // Build session config from app dependencies |
| 59 | + | cfg := model.PurgeModelConfig{ | |||
| 60 | + | ListSessionsUC: app.ListSessionsUC, | |||
| 61 | + | DeleteSessionUC: app.DeleteSessionUC, | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | m := model.NewPurgeModel(app.Ctx(), app.Theme, purgeUC, cfg) | |||
| 58 | p := tea.NewProgram(m, tea.WithAltScreen()) | 65 | p := tea.NewProgram(m, tea.WithAltScreen()) | ||
| 59 | _, err := p.Run() | 66 | _, err := p.Run() | ||
| 60 | return err | 67 | return err | ||
| 61 | } | 68 | } | ||
| 62 | 69 | ||||
| 63 | func runPurgeForce(app *cli.App, purgeUC *usecase.PurgeDataUseCase) error { | 70 | func runPurgeForce(app *cli.App, purgeUC *usecase.PurgeDataUseCase) error { | ||
| 64 | - | out, err := purgeUC.PurgeAll(app.Ctx()) | 71 | + | ctx := app.Ctx() |
| 72 | + | theme := app.Theme | |||
| 73 | + | ||||
| 74 | + | // First, delete all inactive sessions (before we delete the database) | |||
| 75 | + | if app.ListSessionsUC != nil && app.DeleteSessionUC != nil { | |||
| 76 | + | output, err := app.ListSessionsUC.GetPurgeableSessions(ctx) | |||
| 77 | + | if err == nil && output != nil { | |||
| 78 | + | for _, s := range output.Sessions { | |||
| 79 | + | sessionID := s.Info.Session.ID | |||
| 80 | + | if delErr := app.DeleteSessionUC.Execute(ctx, usecase.DeleteSessionInput{ | |||
| 81 | + | SessionID: sessionID, | |||
| 82 | + | CurrentSessionID: "", // CLI has no current session | |||
| 83 | + | }); delErr != nil { | |||
| 84 | + | fmt.Printf("%s Session %s: %v\n", theme.ErrorStyle.Render(styles.IconX), sessionID, delErr) | |||
| 85 | + | } else { | |||
| 86 | + | fmt.Printf("%s Session %s\n", theme.SuccessStyle.Render(styles.IconCheck), sessionID) | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // Then purge all directories | |||
| 93 | + | out, err := purgeUC.PurgeAll(ctx) | |||
| 65 | if out != nil { | 94 | if out != nil { | ||
| 66 | for _, r := range out.Results { | 95 | for _, r := range out.Results { | ||
| 67 | if r.Success { | 96 | if r.Success { | ||
| 68 | - | fmt.Printf("%s %s\n", app.Theme.SuccessStyle.Render(styles.IconCheck), r.Target.Path) | 97 | + | fmt.Printf("%s %s\n", theme.SuccessStyle.Render(styles.IconCheck), r.Target.Path) |
| 69 | } else { | 98 | } else { | ||
| 70 | - | fmt.Printf("%s %s: %v\n", app.Theme.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error) | 99 | + | fmt.Printf("%s %s: %v\n", theme.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error) |
| 71 | } | 100 | } | ||
| 72 | } | 101 | } | ||
| 73 | } | 102 | } | ||
M internal/cli/model/purge.go +242 -69
| @@ -14,23 +14,57 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | // PurgeModel wraps styles.PurgeModel for standalone CLI use. | |
| 16 | 16 | type PurgeModel struct { | |
| 17 | - | selector styles.PurgeModel | |
| 18 | - | purgeUC *usecase.PurgeDataUseCase | |
| 17 | + | selector styles.PurgeModel | |
| 18 | + | purgeUC *usecase.PurgeDataUseCase | |
| 19 | + | listSessionsUC *usecase.ListSessionsUseCase | |
| 20 | + | deleteSessionUC *usecase.DeleteSessionUseCase | |
| 19 | 21 | ||
| 20 | - | loading bool | |
| 21 | - | purging bool | |
| 22 | - | done bool | |
| 22 | + | loading bool | |
| 23 | + | loadingSessions bool | |
| 24 | + | purging bool | |
| 25 | + | done bool | |
| 23 | 26 | ||
| 24 | - | results *usecase.PurgeOutput | |
| 25 | - | info string | |
| 26 | - | err error | |
| 27 | + | // Track loading state for parallel loading | |
| 28 | + | targetsLoaded bool | |
| 29 | + | sessionsLoaded bool | |
| 30 | + | targets []entity.PurgeTarget | |
| 31 | + | sessions []entity.SessionPurgeItem | |
| 32 | + | sessionsSize int64 | |
| 33 | + | ||
| 34 | + | results *usecase.PurgeOutput | |
| 35 | + | sessionResults []sessionPurgeResult | |
| 36 | + | info string | |
| 37 | + | err error | |
| 27 | 38 | ||
| 28 | 39 | theme *styles.Theme | |
| 40 | + | ctx context.Context | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // sessionPurgeResult tracks the result of deleting a session. | |
| 44 | + | type sessionPurgeResult struct { | |
| 45 | + | SessionID entity.SessionID | |
| 46 | + | Success bool | |
| 47 | + | Error error | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // PurgeModelConfig contains optional dependencies for session purging. | |
| 51 | + | type PurgeModelConfig struct { | |
| 52 | + | ListSessionsUC *usecase.ListSessionsUseCase | |
| 53 | + | DeleteSessionUC *usecase.DeleteSessionUseCase | |
| 29 | 54 | } | |
| 30 | 55 | ||
| 31 | 56 | // NewPurgeModel creates a new purge command model. | |
| 32 | - | func NewPurgeModel(theme *styles.Theme, purgeUC *usecase.PurgeDataUseCase) PurgeModel { | |
| 33 | - | return PurgeModel{theme: theme, purgeUC: purgeUC, loading: true} | |
| 57 | + | func NewPurgeModel(ctx context.Context, theme *styles.Theme, purgeUC *usecase.PurgeDataUseCase, cfg PurgeModelConfig) PurgeModel { | |
| 58 | + | m := PurgeModel{ | |
| 59 | + | theme: theme, | |
| 60 | + | purgeUC: purgeUC, | |
| 61 | + | listSessionsUC: cfg.ListSessionsUC, | |
| 62 | + | deleteSessionUC: cfg.DeleteSessionUC, | |
| 63 | + | loading: true, | |
| 64 | + | loadingSessions: cfg.ListSessionsUC != nil, | |
| 65 | + | ctx: ctx, | |
| 66 | + | } | |
| 67 | + | return m | |
| 34 | 68 | } | |
| 35 | 69 | ||
| 36 | 70 | type purgeTargetsLoadedMsg struct { | |
| @@ -38,14 +72,25 @@ type purgeTargetsLoadedMsg struct { | |||
| 38 | 72 | err error | |
| 39 | 73 | } | |
| 40 | 74 | ||
| 75 | + | type purgeSessionsLoadedMsg struct { | |
| 76 | + | sessions []entity.SessionPurgeItem | |
| 77 | + | size int64 | |
| 78 | + | err error | |
| 79 | + | } | |
| 80 | + | ||
| 41 | 81 | type purgeCompleteMsg struct { | |
| 42 | - | out *usecase.PurgeOutput | |
| 43 | - | err error | |
| 82 | + | targetResults []entity.PurgeResult | |
| 83 | + | sessionResults []sessionPurgeResult | |
| 84 | + | err error | |
| 44 | 85 | } | |
| 45 | 86 | ||
| 46 | 87 | // Init implements tea.Model. | |
| 47 | 88 | func (m PurgeModel) Init() tea.Cmd { | |
| 48 | - | return m.loadTargets() | |
| 89 | + | cmds := []tea.Cmd{m.loadTargets()} | |
| 90 | + | if m.listSessionsUC != nil { | |
| 91 | + | cmds = append(cmds, m.loadSessions()) | |
| 92 | + | } | |
| 93 | + | return tea.Batch(cmds...) | |
| 49 | 94 | } | |
| 50 | 95 | ||
| 51 | 96 | func (m PurgeModel) loadTargets() tea.Cmd { | |
| @@ -55,68 +100,172 @@ func (m PurgeModel) loadTargets() tea.Cmd { | |||
| 55 | 100 | } | |
| 56 | 101 | } | |
| 57 | 102 | ||
| 103 | + | func (m PurgeModel) loadSessions() tea.Cmd { | |
| 104 | + | return func() tea.Msg { | |
| 105 | + | output, err := m.listSessionsUC.GetPurgeableSessions(context.Background()) | |
| 106 | + | if err != nil { | |
| 107 | + | return purgeSessionsLoadedMsg{err: err} | |
| 108 | + | } | |
| 109 | + | return purgeSessionsLoadedMsg{ | |
| 110 | + | sessions: output.Sessions, | |
| 111 | + | size: output.TotalSize, | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 58 | 116 | // Update implements tea.Model. | |
| 59 | 117 | func (m PurgeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| 60 | 118 | switch msg := msg.(type) { | |
| 61 | 119 | case purgeTargetsLoadedMsg: | |
| 62 | - | m.loading = false | |
| 63 | - | if msg.err != nil { | |
| 64 | - | m.err = msg.err | |
| 65 | - | m.done = true | |
| 66 | - | return m, nil | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | m.selector = styles.NewPurge(m.theme, msg.targets) | |
| 70 | - | return m, nil | |
| 71 | - | ||
| 120 | + | return m.handleTargetsLoaded(msg), nil | |
| 121 | + | case purgeSessionsLoadedMsg: | |
| 122 | + | return m.handleSessionsLoaded(msg), nil | |
| 72 | 123 | case purgeCompleteMsg: | |
| 73 | - | m.purging = false | |
| 74 | - | m.done = true | |
| 75 | - | m.results = msg.out | |
| 76 | - | if msg.err != nil { | |
| 77 | - | m.err = msg.err | |
| 78 | - | } | |
| 79 | - | return m, nil | |
| 80 | - | ||
| 124 | + | return m.handlePurgeComplete(msg), nil | |
| 81 | 125 | case tea.KeyMsg: | |
| 82 | 126 | if m.done { | |
| 83 | 127 | return m, tea.Quit | |
| 84 | 128 | } | |
| 85 | - | if m.loading || m.purging { | |
| 129 | + | if m.isBlocked() { | |
| 86 | 130 | return m, nil | |
| 87 | 131 | } | |
| 132 | + | // Fall through to updateSelector | |
| 88 | 133 | } | |
| 89 | 134 | ||
| 90 | - | if m.loading || m.purging { | |
| 135 | + | if m.isBlocked() { | |
| 91 | 136 | return m, nil | |
| 92 | 137 | } | |
| 93 | 138 | ||
| 139 | + | return m.updateSelector(msg) | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func (m PurgeModel) handleTargetsLoaded(msg purgeTargetsLoadedMsg) PurgeModel { | |
| 143 | + | if msg.err != nil { | |
| 144 | + | m.err = msg.err | |
| 145 | + | m.done = true | |
| 146 | + | m.loading = false | |
| 147 | + | return m | |
| 148 | + | } | |
| 149 | + | m.targetsLoaded = true | |
| 150 | + | m.targets = msg.targets | |
| 151 | + | return m.checkLoadingComplete() | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func (m PurgeModel) handleSessionsLoaded(msg purgeSessionsLoadedMsg) PurgeModel { | |
| 155 | + | if msg.err != nil { | |
| 156 | + | // Session loading failure is non-fatal, continue without sessions | |
| 157 | + | m.sessionsLoaded = true | |
| 158 | + | m.loadingSessions = false | |
| 159 | + | return m.checkLoadingComplete() | |
| 160 | + | } | |
| 161 | + | m.sessionsLoaded = true | |
| 162 | + | m.sessions = msg.sessions | |
| 163 | + | m.sessionsSize = msg.size | |
| 164 | + | return m.checkLoadingComplete() | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | func (m PurgeModel) handlePurgeComplete(msg purgeCompleteMsg) PurgeModel { | |
| 168 | + | m.purging = false | |
| 169 | + | m.done = true | |
| 170 | + | if msg.targetResults != nil { | |
| 171 | + | m.results = &usecase.PurgeOutput{Results: msg.targetResults} | |
| 172 | + | for _, r := range msg.targetResults { | |
| 173 | + | if r.Success { | |
| 174 | + | m.results.SuccessCount++ | |
| 175 | + | } else { | |
| 176 | + | m.results.FailureCount++ | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | m.sessionResults = msg.sessionResults | |
| 181 | + | if msg.err != nil { | |
| 182 | + | m.err = msg.err | |
| 183 | + | } | |
| 184 | + | return m | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | func (m PurgeModel) isBlocked() bool { | |
| 188 | + | return m.loading || m.loadingSessions || m.purging | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | func (m PurgeModel) updateSelector(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| 94 | 192 | selector, cmd := m.selector.Update(msg) | |
| 95 | 193 | m.selector = selector | |
| 96 | 194 | ||
| 97 | - | if m.selector.Done() { | |
| 98 | - | if m.selector.Canceled { | |
| 99 | - | return m, tea.Quit | |
| 100 | - | } | |
| 101 | - | if m.selector.Confirmed { | |
| 102 | - | targetTypes := m.selector.SelectedTypes() | |
| 103 | - | if len(targetTypes) == 0 { | |
| 104 | - | m.done = true | |
| 105 | - | m.info = "Nothing selected" | |
| 106 | - | return m, nil | |
| 107 | - | } | |
| 108 | - | m.purging = true | |
| 109 | - | return m, m.performPurge(targetTypes) | |
| 195 | + | if !m.selector.Done() { | |
| 196 | + | return m, cmd | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | if m.selector.Canceled { | |
| 200 | + | return m, tea.Quit | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | if m.selector.Confirmed { | |
| 204 | + | targetTypes := m.selector.SelectedTypes() | |
| 205 | + | sessionIDs := m.selector.SelectedSessionIDs() | |
| 206 | + | if len(targetTypes) == 0 && len(sessionIDs) == 0 { | |
| 207 | + | m.done = true | |
| 208 | + | m.info = "Nothing selected" | |
| 209 | + | return m, nil | |
| 110 | 210 | } | |
| 211 | + | m.purging = true | |
| 212 | + | return m, m.performPurge(targetTypes, sessionIDs) | |
| 111 | 213 | } | |
| 112 | 214 | ||
| 113 | 215 | return m, cmd | |
| 114 | 216 | } | |
| 115 | 217 | ||
| 116 | - | func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType) tea.Cmd { | |
| 218 | + | func (m PurgeModel) checkLoadingComplete() PurgeModel { | |
| 219 | + | // If sessions aren't being loaded, only wait for targets | |
| 220 | + | needSessions := m.listSessionsUC != nil | |
| 221 | + | if m.targetsLoaded && (!needSessions || m.sessionsLoaded) { | |
| 222 | + | m.loading = false | |
| 223 | + | m.loadingSessions = false | |
| 224 | + | m.selector = styles.NewPurgeWithSessions(m.theme, m.targets, m.sessions, m.sessionsSize) | |
| 225 | + | } | |
| 226 | + | return m | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType, sessionIDs []entity.SessionID) tea.Cmd { | |
| 117 | 230 | return func() tea.Msg { | |
| 118 | - | out, err := m.purgeUC.Execute(context.Background(), usecase.PurgeInput{TargetTypes: targetTypes}) | |
| 119 | - | return purgeCompleteMsg{out: out, err: err} | |
| 231 | + | var targetResults []entity.PurgeResult | |
| 232 | + | var sessionResults []sessionPurgeResult | |
| 233 | + | var lastErr error | |
| 234 | + | ||
| 235 | + | // Delete sessions first (if any selected and Data not selected) | |
| 236 | + | if m.deleteSessionUC != nil { | |
| 237 | + | for _, sid := range sessionIDs { | |
| 238 | + | err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{ | |
| 239 | + | SessionID: sid, | |
| 240 | + | CurrentSessionID: "", // CLI has no current session | |
| 241 | + | }) | |
| 242 | + | sessionResults = append(sessionResults, sessionPurgeResult{ | |
| 243 | + | SessionID: sid, | |
| 244 | + | Success: err == nil, | |
| 245 | + | Error: err, | |
| 246 | + | }) | |
| 247 | + | if err != nil { | |
| 248 | + | lastErr = err | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // Then delete target directories | |
| 254 | + | if len(targetTypes) > 0 { | |
| 255 | + | out, err := m.purgeUC.Execute(m.ctx, usecase.PurgeInput{TargetTypes: targetTypes}) | |
| 256 | + | if out != nil { | |
| 257 | + | targetResults = out.Results | |
| 258 | + | } | |
| 259 | + | if err != nil { | |
| 260 | + | lastErr = err | |
| 261 | + | } | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | return purgeCompleteMsg{ | |
| 265 | + | targetResults: targetResults, | |
| 266 | + | sessionResults: sessionResults, | |
| 267 | + | err: lastErr, | |
| 268 | + | } | |
| 120 | 269 | } | |
| 121 | 270 | } | |
| 122 | 271 | ||
| @@ -124,7 +273,7 @@ func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType) tea.Cmd { | |||
| 124 | 273 | func (m PurgeModel) View() string { | |
| 125 | 274 | t := m.theme | |
| 126 | 275 | ||
| 127 | - | if m.loading { | |
| 276 | + | if m.loading || m.loadingSessions { | |
| 128 | 277 | spinner := styles.NewLoading(t, "Scanning purge targets...") | |
| 129 | 278 | return t.Box.Render(spinner.View()) | |
| 130 | 279 | } | |
| @@ -145,7 +294,7 @@ func (m PurgeModel) View() string { | |||
| 145 | 294 | return t.Box.Render(content) | |
| 146 | 295 | } | |
| 147 | 296 | ||
| 148 | - | if m.err != nil && m.results == nil { | |
| 297 | + | if m.err != nil && m.results == nil && len(m.sessionResults) == 0 { | |
| 149 | 298 | content := lipgloss.JoinVertical( | |
| 150 | 299 | lipgloss.Left, | |
| 151 | 300 | t.ErrorStyle.Render("Error: "+m.err.Error()), | |
| @@ -155,28 +304,52 @@ func (m PurgeModel) View() string { | |||
| 155 | 304 | return t.Box.Render(content) | |
| 156 | 305 | } | |
| 157 | 306 | ||
| 158 | - | contentLines := []string{t.Title.Render("Purge complete")} | |
| 159 | - | if m.results != nil { | |
| 160 | - | for _, r := range m.results.Results { | |
| 161 | - | if r.Success { | |
| 162 | - | contentLines = append(contentLines, fmt.Sprintf("%s %s", t.SuccessStyle.Render(styles.IconCheck), r.Target.Path)) | |
| 163 | - | } else { | |
| 164 | - | contentLines = append(contentLines, fmt.Sprintf("%s %s: %v", t.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error)) | |
| 165 | - | } | |
| 307 | + | return m.renderResults() | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | return m.selector.View() | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | func (m PurgeModel) renderResults() string { | |
| 314 | + | t := m.theme | |
| 315 | + | contentLines := []string{t.Title.Render("Purge complete")} | |
| 316 | + | ||
| 317 | + | successCount := 0 | |
| 318 | + | failureCount := 0 | |
| 319 | + | ||
| 320 | + | // Target results | |
| 321 | + | if m.results != nil { | |
| 322 | + | for _, r := range m.results.Results { | |
| 323 | + | if r.Success { | |
| 324 | + | contentLines = append(contentLines, fmt.Sprintf("%s %s", t.SuccessStyle.Render(styles.IconCheck), r.Target.Path)) | |
| 325 | + | successCount++ | |
| 326 | + | } else { | |
| 327 | + | contentLines = append(contentLines, fmt.Sprintf("%s %s: %v", t.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error)) | |
| 328 | + | failureCount++ | |
| 166 | 329 | } | |
| 167 | - | contentLines = append( | |
| 168 | - | contentLines, | |
| 169 | - | "", | |
| 170 | - | t.Subtle.Render( | |
| 171 | - | fmt.Sprintf("%d succeeded, %d failed", m.results.SuccessCount, m.results.FailureCount), | |
| 172 | - | ), | |
| 173 | - | ) | |
| 174 | 330 | } | |
| 175 | - | contentLines = append(contentLines, "", t.Subtle.Render("Press any key to exit")) | |
| 176 | - | return t.Box.Render(lipgloss.JoinVertical(lipgloss.Left, contentLines...)) | |
| 177 | 331 | } | |
| 178 | 332 | ||
| 179 | - | return m.selector.View() | |
| 333 | + | // Session results | |
| 334 | + | for _, r := range m.sessionResults { | |
| 335 | + | if r.Success { | |
| 336 | + | contentLines = append(contentLines, fmt.Sprintf("%s Session %s", t.SuccessStyle.Render(styles.IconCheck), r.SessionID)) | |
| 337 | + | successCount++ | |
| 338 | + | } else { | |
| 339 | + | contentLines = append(contentLines, fmt.Sprintf("%s Session %s: %v", t.ErrorStyle.Render(styles.IconX), r.SessionID, r.Error)) | |
| 340 | + | failureCount++ | |
| 341 | + | } | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | contentLines = append( | |
| 345 | + | contentLines, | |
| 346 | + | "", | |
| 347 | + | t.Subtle.Render(fmt.Sprintf("%d succeeded, %d failed", successCount, failureCount)), | |
| 348 | + | "", | |
| 349 | + | t.Subtle.Render("Press any key to exit"), | |
| 350 | + | ) | |
| 351 | + | ||
| 352 | + | return t.Box.Render(lipgloss.JoinVertical(lipgloss.Left, contentLines...)) | |
| 180 | 353 | } | |
| 181 | 354 | ||
| 182 | 355 | var _ tea.Model = (*PurgeModel)(nil) | |
| @@ -14,23 +14,57 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | // PurgeModel wraps styles.PurgeModel for standalone CLI use. | 15 | // PurgeModel wraps styles.PurgeModel for standalone CLI use. | ||
| 16 | type PurgeModel struct { | 16 | type PurgeModel struct { | ||
| 17 | - | selector styles.PurgeModel | 17 | + | selector styles.PurgeModel |
| 18 | - | purgeUC *usecase.PurgeDataUseCase | 18 | + | purgeUC *usecase.PurgeDataUseCase |
| 19 | + | listSessionsUC *usecase.ListSessionsUseCase | |||
| 20 | + | deleteSessionUC *usecase.DeleteSessionUseCase | |||
| 19 | 21 | ||||
| 20 | - | loading bool | 22 | + | loading bool |
| 21 | - | purging bool | 23 | + | loadingSessions bool |
| 22 | - | done bool | 24 | + | purging bool |
| 25 | + | done bool | |||
| 23 | 26 | ||||
| 24 | - | results *usecase.PurgeOutput | 27 | + | // Track loading state for parallel loading |
| 25 | - | info string | 28 | + | targetsLoaded bool |
| 26 | - | err error | 29 | + | sessionsLoaded bool |
| 30 | + | targets []entity.PurgeTarget | |||
| 31 | + | sessions []entity.SessionPurgeItem | |||
| 32 | + | sessionsSize int64 | |||
| 33 | + | ||||
| 34 | + | results *usecase.PurgeOutput | |||
| 35 | + | sessionResults []sessionPurgeResult | |||
| 36 | + | info string | |||
| 37 | + | err error | |||
| 27 | 38 | ||||
| 28 | theme *styles.Theme | 39 | theme *styles.Theme | ||
| 40 | + | ctx context.Context | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // sessionPurgeResult tracks the result of deleting a session. | |||
| 44 | + | type sessionPurgeResult struct { | |||
| 45 | + | SessionID entity.SessionID | |||
| 46 | + | Success bool | |||
| 47 | + | Error error | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // PurgeModelConfig contains optional dependencies for session purging. | |||
| 51 | + | type PurgeModelConfig struct { | |||
| 52 | + | ListSessionsUC *usecase.ListSessionsUseCase | |||
| 53 | + | DeleteSessionUC *usecase.DeleteSessionUseCase | |||
| 29 | } | 54 | } | ||
| 30 | 55 | ||||
| 31 | // NewPurgeModel creates a new purge command model. | 56 | // NewPurgeModel creates a new purge command model. | ||
| 32 | - | func NewPurgeModel(theme *styles.Theme, purgeUC *usecase.PurgeDataUseCase) PurgeModel { | 57 | + | func NewPurgeModel(ctx context.Context, theme *styles.Theme, purgeUC *usecase.PurgeDataUseCase, cfg PurgeModelConfig) PurgeModel { |
| 33 | - | return PurgeModel{theme: theme, purgeUC: purgeUC, loading: true} | 58 | + | m := PurgeModel{ |
| 59 | + | theme: theme, | |||
| 60 | + | purgeUC: purgeUC, | |||
| 61 | + | listSessionsUC: cfg.ListSessionsUC, | |||
| 62 | + | deleteSessionUC: cfg.DeleteSessionUC, | |||
| 63 | + | loading: true, | |||
| 64 | + | loadingSessions: cfg.ListSessionsUC != nil, | |||
| 65 | + | ctx: ctx, | |||
| 66 | + | } | |||
| 67 | + | return m | |||
| 34 | } | 68 | } | ||
| 35 | 69 | ||||
| 36 | type purgeTargetsLoadedMsg struct { | 70 | type purgeTargetsLoadedMsg struct { | ||
| @@ -38,14 +72,25 @@ type purgeTargetsLoadedMsg struct { | |||||
| 38 | err error | 72 | err error | ||
| 39 | } | 73 | } | ||
| 40 | 74 | ||||
| 75 | + | type purgeSessionsLoadedMsg struct { | |||
| 76 | + | sessions []entity.SessionPurgeItem | |||
| 77 | + | size int64 | |||
| 78 | + | err error | |||
| 79 | + | } | |||
| 80 | + | ||||
| 41 | type purgeCompleteMsg struct { | 81 | type purgeCompleteMsg struct { | ||
| 42 | - | out *usecase.PurgeOutput | 82 | + | targetResults []entity.PurgeResult |
| 43 | - | err error | 83 | + | sessionResults []sessionPurgeResult |
| 84 | + | err error | |||
| 44 | } | 85 | } | ||
| 45 | 86 | ||||
| 46 | // Init implements tea.Model. | 87 | // Init implements tea.Model. | ||
| 47 | func (m PurgeModel) Init() tea.Cmd { | 88 | func (m PurgeModel) Init() tea.Cmd { | ||
| 48 | - | return m.loadTargets() | 89 | + | cmds := []tea.Cmd{m.loadTargets()} |
| 90 | + | if m.listSessionsUC != nil { | |||
| 91 | + | cmds = append(cmds, m.loadSessions()) | |||
| 92 | + | } | |||
| 93 | + | return tea.Batch(cmds...) | |||
| 49 | } | 94 | } | ||
| 50 | 95 | ||||
| 51 | func (m PurgeModel) loadTargets() tea.Cmd { | 96 | func (m PurgeModel) loadTargets() tea.Cmd { | ||
| @@ -55,68 +100,172 @@ func (m PurgeModel) loadTargets() tea.Cmd { | |||||
| 55 | } | 100 | } | ||
| 56 | } | 101 | } | ||
| 57 | 102 | ||||
| 103 | + | func (m PurgeModel) loadSessions() tea.Cmd { | |||
| 104 | + | return func() tea.Msg { | |||
| 105 | + | output, err := m.listSessionsUC.GetPurgeableSessions(context.Background()) | |||
| 106 | + | if err != nil { | |||
| 107 | + | return purgeSessionsLoadedMsg{err: err} | |||
| 108 | + | } | |||
| 109 | + | return purgeSessionsLoadedMsg{ | |||
| 110 | + | sessions: output.Sessions, | |||
| 111 | + | size: output.TotalSize, | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | ||||
| 58 | // Update implements tea.Model. | 116 | // Update implements tea.Model. | ||
| 59 | func (m PurgeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | 117 | func (m PurgeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
| 60 | switch msg := msg.(type) { | 118 | switch msg := msg.(type) { | ||
| 61 | case purgeTargetsLoadedMsg: | 119 | case purgeTargetsLoadedMsg: | ||
| 62 | - | m.loading = false | 120 | + | return m.handleTargetsLoaded(msg), nil |
| 63 | - | if msg.err != nil { | 121 | + | case purgeSessionsLoadedMsg: |
| 64 | - | m.err = msg.err | 122 | + | return m.handleSessionsLoaded(msg), nil |
| 65 | - | m.done = true | |||
| 66 | - | return m, nil | |||
| 67 | - | } | |||
| 68 | - | ||||
| 69 | - | m.selector = styles.NewPurge(m.theme, msg.targets) | |||
| 70 | - | return m, nil | |||
| 71 | - | ||||
| 72 | case purgeCompleteMsg: | 123 | case purgeCompleteMsg: | ||
| 73 | - | m.purging = false | 124 | + | return m.handlePurgeComplete(msg), nil |
| 74 | - | m.done = true | |||
| 75 | - | m.results = msg.out | |||
| 76 | - | if msg.err != nil { | |||
| 77 | - | m.err = msg.err | |||
| 78 | - | } | |||
| 79 | - | return m, nil | |||
| 80 | - | ||||
| 81 | case tea.KeyMsg: | 125 | case tea.KeyMsg: | ||
| 82 | if m.done { | 126 | if m.done { | ||
| 83 | return m, tea.Quit | 127 | return m, tea.Quit | ||
| 84 | } | 128 | } | ||
| 85 | - | if m.loading || m.purging { | 129 | + | if m.isBlocked() { |
| 86 | return m, nil | 130 | return m, nil | ||
| 87 | } | 131 | } | ||
| 132 | + | // Fall through to updateSelector | |||
| 88 | } | 133 | } | ||
| 89 | 134 | ||||
| 90 | - | if m.loading || m.purging { | 135 | + | if m.isBlocked() { |
| 91 | return m, nil | 136 | return m, nil | ||
| 92 | } | 137 | } | ||
| 93 | 138 | ||||
| 139 | + | return m.updateSelector(msg) | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (m PurgeModel) handleTargetsLoaded(msg purgeTargetsLoadedMsg) PurgeModel { | |||
| 143 | + | if msg.err != nil { | |||
| 144 | + | m.err = msg.err | |||
| 145 | + | m.done = true | |||
| 146 | + | m.loading = false | |||
| 147 | + | return m | |||
| 148 | + | } | |||
| 149 | + | m.targetsLoaded = true | |||
| 150 | + | m.targets = msg.targets | |||
| 151 | + | return m.checkLoadingComplete() | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | func (m PurgeModel) handleSessionsLoaded(msg purgeSessionsLoadedMsg) PurgeModel { | |||
| 155 | + | if msg.err != nil { | |||
| 156 | + | // Session loading failure is non-fatal, continue without sessions | |||
| 157 | + | m.sessionsLoaded = true | |||
| 158 | + | m.loadingSessions = false | |||
| 159 | + | return m.checkLoadingComplete() | |||
| 160 | + | } | |||
| 161 | + | m.sessionsLoaded = true | |||
| 162 | + | m.sessions = msg.sessions | |||
| 163 | + | m.sessionsSize = msg.size | |||
| 164 | + | return m.checkLoadingComplete() | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | func (m PurgeModel) handlePurgeComplete(msg purgeCompleteMsg) PurgeModel { | |||
| 168 | + | m.purging = false | |||
| 169 | + | m.done = true | |||
| 170 | + | if msg.targetResults != nil { | |||
| 171 | + | m.results = &usecase.PurgeOutput{Results: msg.targetResults} | |||
| 172 | + | for _, r := range msg.targetResults { | |||
| 173 | + | if r.Success { | |||
| 174 | + | m.results.SuccessCount++ | |||
| 175 | + | } else { | |||
| 176 | + | m.results.FailureCount++ | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | m.sessionResults = msg.sessionResults | |||
| 181 | + | if msg.err != nil { | |||
| 182 | + | m.err = msg.err | |||
| 183 | + | } | |||
| 184 | + | return m | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | func (m PurgeModel) isBlocked() bool { | |||
| 188 | + | return m.loading || m.loadingSessions || m.purging | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | func (m PurgeModel) updateSelector(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 94 | selector, cmd := m.selector.Update(msg) | 192 | selector, cmd := m.selector.Update(msg) | ||
| 95 | m.selector = selector | 193 | m.selector = selector | ||
| 96 | 194 | ||||
| 97 | - | if m.selector.Done() { | 195 | + | if !m.selector.Done() { |
| 98 | - | if m.selector.Canceled { | 196 | + | return m, cmd |
| 99 | - | return m, tea.Quit | 197 | + | } |
| 100 | - | } | 198 | + | |
| 101 | - | if m.selector.Confirmed { | 199 | + | if m.selector.Canceled { |
| 102 | - | targetTypes := m.selector.SelectedTypes() | 200 | + | return m, tea.Quit |
| 103 | - | if len(targetTypes) == 0 { | 201 | + | } |
| 104 | - | m.done = true | 202 | + | |
| 105 | - | m.info = "Nothing selected" | 203 | + | if m.selector.Confirmed { |
| 106 | - | return m, nil | 204 | + | targetTypes := m.selector.SelectedTypes() |
| 107 | - | } | 205 | + | sessionIDs := m.selector.SelectedSessionIDs() |
| 108 | - | m.purging = true | 206 | + | if len(targetTypes) == 0 && len(sessionIDs) == 0 { |
| 109 | - | return m, m.performPurge(targetTypes) | 207 | + | m.done = true |
| 208 | + | m.info = "Nothing selected" | |||
| 209 | + | return m, nil | |||
| 110 | } | 210 | } | ||
| 211 | + | m.purging = true | |||
| 212 | + | return m, m.performPurge(targetTypes, sessionIDs) | |||
| 111 | } | 213 | } | ||
| 112 | 214 | ||||
| 113 | return m, cmd | 215 | return m, cmd | ||
| 114 | } | 216 | } | ||
| 115 | 217 | ||||
| 116 | - | func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType) tea.Cmd { | 218 | + | func (m PurgeModel) checkLoadingComplete() PurgeModel { |
| 219 | + | // If sessions aren't being loaded, only wait for targets | |||
| 220 | + | needSessions := m.listSessionsUC != nil | |||
| 221 | + | if m.targetsLoaded && (!needSessions || m.sessionsLoaded) { | |||
| 222 | + | m.loading = false | |||
| 223 | + | m.loadingSessions = false | |||
| 224 | + | m.selector = styles.NewPurgeWithSessions(m.theme, m.targets, m.sessions, m.sessionsSize) | |||
| 225 | + | } | |||
| 226 | + | return m | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType, sessionIDs []entity.SessionID) tea.Cmd { | |||
| 117 | return func() tea.Msg { | 230 | return func() tea.Msg { | ||
| 118 | - | out, err := m.purgeUC.Execute(context.Background(), usecase.PurgeInput{TargetTypes: targetTypes}) | 231 | + | var targetResults []entity.PurgeResult |
| 119 | - | return purgeCompleteMsg{out: out, err: err} | 232 | + | var sessionResults []sessionPurgeResult |
| 233 | + | var lastErr error | |||
| 234 | + | ||||
| 235 | + | // Delete sessions first (if any selected and Data not selected) | |||
| 236 | + | if m.deleteSessionUC != nil { | |||
| 237 | + | for _, sid := range sessionIDs { | |||
| 238 | + | err := m.deleteSessionUC.Execute(m.ctx, usecase.DeleteSessionInput{ | |||
| 239 | + | SessionID: sid, | |||
| 240 | + | CurrentSessionID: "", // CLI has no current session | |||
| 241 | + | }) | |||
| 242 | + | sessionResults = append(sessionResults, sessionPurgeResult{ | |||
| 243 | + | SessionID: sid, | |||
| 244 | + | Success: err == nil, | |||
| 245 | + | Error: err, | |||
| 246 | + | }) | |||
| 247 | + | if err != nil { | |||
| 248 | + | lastErr = err | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // Then delete target directories | |||
| 254 | + | if len(targetTypes) > 0 { | |||
| 255 | + | out, err := m.purgeUC.Execute(m.ctx, usecase.PurgeInput{TargetTypes: targetTypes}) | |||
| 256 | + | if out != nil { | |||
| 257 | + | targetResults = out.Results | |||
| 258 | + | } | |||
| 259 | + | if err != nil { | |||
| 260 | + | lastErr = err | |||
| 261 | + | } | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | return purgeCompleteMsg{ | |||
| 265 | + | targetResults: targetResults, | |||
| 266 | + | sessionResults: sessionResults, | |||
| 267 | + | err: lastErr, | |||
| 268 | + | } | |||
| 120 | } | 269 | } | ||
| 121 | } | 270 | } | ||
| 122 | 271 | ||||
| @@ -124,7 +273,7 @@ func (m PurgeModel) performPurge(targetTypes []entity.PurgeTargetType) tea.Cmd { | |||||
| 124 | func (m PurgeModel) View() string { | 273 | func (m PurgeModel) View() string { | ||
| 125 | t := m.theme | 274 | t := m.theme | ||
| 126 | 275 | ||||
| 127 | - | if m.loading { | 276 | + | if m.loading || m.loadingSessions { |
| 128 | spinner := styles.NewLoading(t, "Scanning purge targets...") | 277 | spinner := styles.NewLoading(t, "Scanning purge targets...") | ||
| 129 | return t.Box.Render(spinner.View()) | 278 | return t.Box.Render(spinner.View()) | ||
| 130 | } | 279 | } | ||
| @@ -145,7 +294,7 @@ func (m PurgeModel) View() string { | |||||
| 145 | return t.Box.Render(content) | 294 | return t.Box.Render(content) | ||
| 146 | } | 295 | } | ||
| 147 | 296 | ||||
| 148 | - | if m.err != nil && m.results == nil { | 297 | + | if m.err != nil && m.results == nil && len(m.sessionResults) == 0 { |
| 149 | content := lipgloss.JoinVertical( | 298 | content := lipgloss.JoinVertical( | ||
| 150 | lipgloss.Left, | 299 | lipgloss.Left, | ||
| 151 | t.ErrorStyle.Render("Error: "+m.err.Error()), | 300 | t.ErrorStyle.Render("Error: "+m.err.Error()), | ||
| @@ -155,28 +304,52 @@ func (m PurgeModel) View() string { | |||||
| 155 | return t.Box.Render(content) | 304 | return t.Box.Render(content) | ||
| 156 | } | 305 | } | ||
| 157 | 306 | ||||
| 158 | - | contentLines := []string{t.Title.Render("Purge complete")} | 307 | + | return m.renderResults() |
| 159 | - | if m.results != nil { | 308 | + | } |
| 160 | - | for _, r := range m.results.Results { | 309 | + | |
| 161 | - | if r.Success { | 310 | + | return m.selector.View() |
| 162 | - | contentLines = append(contentLines, fmt.Sprintf("%s %s", t.SuccessStyle.Render(styles.IconCheck), r.Target.Path)) | 311 | + | } |
| 163 | - | } else { | 312 | + | |
| 164 | - | contentLines = append(contentLines, fmt.Sprintf("%s %s: %v", t.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error)) | 313 | + | func (m PurgeModel) renderResults() string { |
| 165 | - | } | 314 | + | t := m.theme |
| 315 | + | contentLines := []string{t.Title.Render("Purge complete")} | |||
| 316 | + | ||||
| 317 | + | successCount := 0 | |||
| 318 | + | failureCount := 0 | |||
| 319 | + | ||||
| 320 | + | // Target results | |||
| 321 | + | if m.results != nil { | |||
| 322 | + | for _, r := range m.results.Results { | |||
| 323 | + | if r.Success { | |||
| 324 | + | contentLines = append(contentLines, fmt.Sprintf("%s %s", t.SuccessStyle.Render(styles.IconCheck), r.Target.Path)) | |||
| 325 | + | successCount++ | |||
| 326 | + | } else { | |||
| 327 | + | contentLines = append(contentLines, fmt.Sprintf("%s %s: %v", t.ErrorStyle.Render(styles.IconX), r.Target.Path, r.Error)) | |||
| 328 | + | failureCount++ | |||
| 166 | } | 329 | } | ||
| 167 | - | contentLines = append( | |||
| 168 | - | contentLines, | |||
| 169 | - | "", | |||
| 170 | - | t.Subtle.Render( | |||
| 171 | - | fmt.Sprintf("%d succeeded, %d failed", m.results.SuccessCount, m.results.FailureCount), | |||
| 172 | - | ), | |||
| 173 | - | ) | |||
| 174 | } | 330 | } | ||
| 175 | - | contentLines = append(contentLines, "", t.Subtle.Render("Press any key to exit")) | |||
| 176 | - | return t.Box.Render(lipgloss.JoinVertical(lipgloss.Left, contentLines...)) | |||
| 177 | } | 331 | } | ||
| 178 | 332 | ||||
| 179 | - | return m.selector.View() | 333 | + | // Session results |
| 334 | + | for _, r := range m.sessionResults { | |||
| 335 | + | if r.Success { | |||
| 336 | + | contentLines = append(contentLines, fmt.Sprintf("%s Session %s", t.SuccessStyle.Render(styles.IconCheck), r.SessionID)) | |||
| 337 | + | successCount++ | |||
| 338 | + | } else { | |||
| 339 | + | contentLines = append(contentLines, fmt.Sprintf("%s Session %s: %v", t.ErrorStyle.Render(styles.IconX), r.SessionID, r.Error)) | |||
| 340 | + | failureCount++ | |||
| 341 | + | } | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | contentLines = append( | |||
| 345 | + | contentLines, | |||
| 346 | + | "", | |||
| 347 | + | t.Subtle.Render(fmt.Sprintf("%d succeeded, %d failed", successCount, failureCount)), | |||
| 348 | + | "", | |||
| 349 | + | t.Subtle.Render("Press any key to exit"), | |||
| 350 | + | ) | |||
| 351 | + | ||||
| 352 | + | return t.Box.Render(lipgloss.JoinVertical(lipgloss.Left, contentLines...)) | |||
| 180 | } | 353 | } | ||
| 181 | 354 | ||||
| 182 | var _ tea.Model = (*PurgeModel)(nil) | 355 | var _ tea.Model = (*PurgeModel)(nil) | ||