Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/internal/app/actions_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,27 @@ func (m Model) openImageDeactivateConfirm() (Model, tea.Cmd) {
func (m Model) doDeactivateImage(id, name string) tea.Cmd {
imgClient := m.client.Image
return func() tea.Msg {
shared.Debugf("[action] deactivating image %s", name)
err := image.DeactivateImage(context.Background(), imgClient, id)
if err != nil {
shared.Debugf("[action] deactivate image %s failed: %s", name, err)
return shared.ResourceActionErrMsg{Action: "Deactivate image", Name: name, Err: err}
}
shared.Debugf("[action] deactivated image %s", name)
return shared.ResourceActionMsg{Action: "Deactivated image", Name: name}
}
}

func (m Model) doReactivateImage(id, name string) tea.Cmd {
imgClient := m.client.Image
return func() tea.Msg {
shared.Debugf("[action] reactivating image %s", name)
err := image.ReactivateImage(context.Background(), imgClient, id)
if err != nil {
shared.Debugf("[action] reactivate image %s failed: %s", name, err)
return shared.ResourceActionErrMsg{Action: "Reactivate image", Name: name, Err: err}
}
shared.Debugf("[action] reactivated image %s", name)
return shared.ResourceActionMsg{Action: "Reactivated image", Name: name}
}
}
5 changes: 5 additions & 0 deletions src/internal/app/actions_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,22 @@ func (m Model) doAllocateFIP() (Model, tea.Cmd) {
m.statusBar.Hint = "Allocating floating IP..."
networkClient := m.client.Network
return m, func() tea.Msg {
shared.Debugf("[action] allocating floating IP")
nets, err := network.ListExternalNetworks(context.Background(), networkClient)
if err != nil {
shared.Debugf("[action] allocate floating IP failed: %s", err)
return shared.ResourceActionErrMsg{Action: "Allocate", Name: "floating IP", Err: err}
}
if len(nets) == 0 {
shared.Debugf("[action] allocate floating IP failed: no external networks available")
return shared.ResourceActionErrMsg{Action: "Allocate", Name: "floating IP", Err: fmt.Errorf("no external networks available")}
}
fip, err := network.AllocateFloatingIP(context.Background(), networkClient, nets[0].ID)
if err != nil {
shared.Debugf("[action] allocate floating IP failed: %s", err)
return shared.ResourceActionErrMsg{Action: "Allocate", Name: "floating IP", Err: err}
}
shared.Debugf("[action] allocated floating IP %s", fip.FloatingIP)
return shared.ResourceActionMsg{Action: "Allocated", Name: fip.FloatingIP}
}
}
Expand Down
86 changes: 86 additions & 0 deletions src/internal/app/actions_server.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.idlePaused {
m.idlePaused = false
m.statusBar.Hint = ""
shared.Debugf("[app] resuming from idle, restarting tick")
return m, m.refreshTickCmd()
}

Expand Down Expand Up @@ -1127,11 +1128,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Idle timeout: swallow ticks when paused, or pause if idle too long
if _, ok := msg.(shared.TickMsg); ok {
if m.idlePaused {
shared.Debugf("[app] TickMsg: swallowed (idle paused)")
return m, nil
}
if m.idleTimeout > 0 && !m.lastActivity.IsZero() && time.Since(m.lastActivity) > m.idleTimeout {
m.idlePaused = true
m.statusBar.Hint = "⏸ Paused — press any key to resume"
shared.Debugf("[app] TickMsg: pausing due to idle timeout")
return m, nil
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/internal/ui/fippicker/fippicker.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,18 @@ func (m Model) associateFIP(fip network.FloatingIP) tea.Cmd {
fipID := fip.ID
fipAddr := fip.FloatingIP
return func() tea.Msg {
shared.Debugf("[fippicker] associating FIP %s (%s) to server %s", fipID, fipAddr, serverName)
portID, err := network.FindServerPortID(context.Background(), client, serverID)
if err != nil {
shared.Debugf("[fippicker] error finding port for server %s: %v", serverID, err)
return associateErrMsg{err: err}
}
err = network.AssociateFloatingIP(context.Background(), client, fipID, portID)
if err != nil {
shared.Debugf("[fippicker] error associating FIP %s: %v", fipID, err)
return associateErrMsg{err: err}
}
shared.Debugf("[fippicker] associated FIP %s to server %s", fipAddr, serverName)
return associateDoneMsg{fipAddr: fipAddr, serverName: serverName}
}
}
Expand All @@ -266,25 +270,32 @@ func (m Model) allocateAndAssociate() tea.Cmd {
serverID := m.serverID
serverName := m.serverName
return func() tea.Msg {
shared.Debugf("[fippicker] allocating and associating FIP for server %s", serverName)
nets, err := network.ListExternalNetworks(context.Background(), client)
if err != nil {
shared.Debugf("[fippicker] error listing external networks: %v", err)
return allocateErrMsg{err: err}
}
if len(nets) == 0 {
shared.Debugf("[fippicker] no external networks available")
return allocateErrMsg{err: fmt.Errorf("no external networks available")}
}
fip, err := network.AllocateFloatingIP(context.Background(), client, nets[0].ID)
if err != nil {
shared.Debugf("[fippicker] error allocating FIP: %v", err)
return allocateErrMsg{err: err}
}
portID, err := network.FindServerPortID(context.Background(), client, serverID)
if err != nil {
shared.Debugf("[fippicker] error finding port for server %s: %v", serverID, err)
return allocateErrMsg{err: err}
}
err = network.AssociateFloatingIP(context.Background(), client, fip.ID, portID)
if err != nil {
shared.Debugf("[fippicker] error associating FIP %s: %v", fip.ID, err)
return allocateErrMsg{err: err}
}
shared.Debugf("[fippicker] allocated and associated FIP %s to server %s", fip.FloatingIP, serverName)
return allocateDoneMsg{fipAddr: fip.FloatingIP, serverName: serverName}
}
}
9 changes: 9 additions & 0 deletions src/internal/ui/floatingiplist/floatingiplist.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func New(client *gophercloud.ServiceClient, refreshInterval time.Duration) Model

// Init starts the initial fetch.
func (m Model) Init() tea.Cmd {
shared.Debugf("[floatingiplist] Init()")
return tea.Batch(m.spinner.Tick, m.fetchFIPs())
}

Expand All @@ -71,6 +72,7 @@ func (m Model) SelectedFIP() *network.FloatingIP {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case fipsLoadedMsg:
shared.Debugf("[floatingiplist] loaded %d floating IPs", len(msg.fips))
var cursorID string
if m.cursor >= 0 && m.cursor < len(m.fips) {
cursorID = m.fips[m.cursor].ID
Expand All @@ -93,14 +95,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, nil

case fipsErrMsg:
shared.Debugf("[floatingiplist] error: %v", msg.err)
m.loading = false
m.err = msg.err.Error()
return m, nil

case shared.TickMsg:
if m.loading {
shared.Debugf("[floatingiplist] tick skipped (loading)")
return m, nil
}
shared.Debugf("[floatingiplist] tick fetching")
return m, m.fetchFIPs()

case spinner.TickMsg:
Expand Down Expand Up @@ -347,16 +352,20 @@ func fipStatusStyle(status string) lipgloss.Style {
func (m Model) fetchFIPs() tea.Cmd {
client := m.client
return func() tea.Msg {
shared.Debugf("[floatingiplist] fetch start")
fips, err := network.ListFloatingIPs(context.Background(), client)
if err != nil {
shared.Debugf("[floatingiplist] fetch error: %v", err)
return fipsErrMsg{err: err}
}
shared.Debugf("[floatingiplist] fetch done, count=%d", len(fips))
return fipsLoadedMsg{fips: fips}
}
}

// ForceRefresh triggers a manual reload of the floating IP list.
func (m *Model) ForceRefresh() tea.Cmd {
shared.Debugf("[floatingiplist] ForceRefresh()")
m.loading = true
return tea.Batch(m.spinner.Tick, m.fetchFIPs())
}
Expand Down
9 changes: 9 additions & 0 deletions src/internal/ui/imagedetail/imagedetail.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func New(client *gophercloud.ServiceClient, imageID string) Model {

// Init fetches the image details.
func (m Model) Init() tea.Cmd {
shared.Debugf("[imagedetail] Init()")
return tea.Batch(m.spinner.Tick, m.fetchImage())
}

Expand Down Expand Up @@ -78,20 +79,24 @@ func (m Model) ImageStatus() string {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case imageDetailLoadedMsg:
shared.Debugf("[imagedetail] imageDetailLoadedMsg")
m.loading = false
m.image = msg.image
m.err = ""
return m, nil

case imageDetailErrMsg:
shared.Debugf("[imagedetail] imageDetailErrMsg: %v", msg.err)
m.loading = false
m.err = msg.err.Error()
return m, nil

case shared.TickMsg:
if m.loading {
shared.Debugf("[imagedetail] tick skipped (loading)")
return m, nil
}
shared.Debugf("[imagedetail] tick fetching")
return m, m.fetchImage()

case spinner.TickMsg:
Expand Down Expand Up @@ -238,16 +243,20 @@ func (m Model) fetchImage() tea.Cmd {
}
id := m.imageID
return func() tea.Msg {
shared.Debugf("[imagedetail] fetchImage start")
img, err := image.GetImage(context.Background(), client, id)
if err != nil {
shared.Debugf("[imagedetail] fetchImage error: %v", err)
return imageDetailErrMsg{err: err}
}
shared.Debugf("[imagedetail] fetchImage done")
return imageDetailLoadedMsg{image: img}
}
}

// ForceRefresh triggers a manual reload of the image detail.
func (m *Model) ForceRefresh() tea.Cmd {
shared.Debugf("[imagedetail] ForceRefresh()")
m.loading = true
return tea.Batch(m.spinner.Tick, m.fetchImage())
}
Expand Down
9 changes: 9 additions & 0 deletions src/internal/ui/imagelist/imagelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func New(client *gophercloud.ServiceClient, refreshInterval time.Duration) Model

// Init starts the initial fetch.
func (m Model) Init() tea.Cmd {
shared.Debugf("[imagelist] Init()")
return tea.Batch(m.spinner.Tick, m.fetchImages())
}

Expand All @@ -169,6 +170,7 @@ func (m Model) SelectedImage() *image.Image {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case imagesLoadedMsg:
shared.Debugf("[imagelist] loaded %d images", len(msg.images))
var cursorID string
if m.cursor >= 0 && m.cursor < len(m.images) {
cursorID = m.images[m.cursor].ID
Expand All @@ -191,14 +193,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, nil

case imagesErrMsg:
shared.Debugf("[imagelist] error: %v", msg.err)
m.loading = false
m.err = msg.err.Error()
return m, nil

case shared.TickMsg:
if m.loading {
shared.Debugf("[imagelist] tick skipped (loading)")
return m, nil
}
shared.Debugf("[imagelist] tick fetching")
return m, m.fetchImages()

case spinner.TickMsg:
Expand Down Expand Up @@ -545,16 +550,20 @@ func (m Model) fetchImages() tea.Cmd {
}
}
return func() tea.Msg {
shared.Debugf("[imagelist] fetch start")
imgs, err := image.ListImages(context.Background(), client)
if err != nil {
shared.Debugf("[imagelist] fetch error: %v", err)
return imagesErrMsg{err: err}
}
shared.Debugf("[imagelist] fetch done, count=%d", len(imgs))
return imagesLoadedMsg{images: imgs}
}
}

// ForceRefresh triggers a manual reload of the image list.
func (m *Model) ForceRefresh() tea.Cmd {
shared.Debugf("[imagelist] ForceRefresh()")
m.loading = true
return tea.Batch(m.spinner.Tick, m.fetchImages())
}
Expand Down
6 changes: 6 additions & 0 deletions src/internal/ui/keypaircreate/keypaircreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,21 +467,27 @@ func (m Model) submit() (Model, tea.Cmd) {

if publicKey != "" {
return m, tea.Batch(m.spinner.Tick, func() tea.Msg {
shared.Debugf("[keypaircreate] importing keypair %q", name)
kp, err := compute.ImportKeyPair(context.Background(), client, name, publicKey)
if err != nil {
shared.Debugf("[keypaircreate] error importing keypair %q: %v", name, err)
return keypairCreateErrMsg{err: err}
}
shared.Debugf("[keypaircreate] imported keypair %q", name)
return keypairCreatedMsg{kp: kp}
})
}

algo := kt.algorithm
keySize := kt.keySize
return m, tea.Batch(m.spinner.Tick, func() tea.Msg {
shared.Debugf("[keypaircreate] generating keypair %q (algo=%s)", name, algo)
kp, err := compute.GenerateAndImportKeyPair(context.Background(), client, name, algo, keySize)
if err != nil {
shared.Debugf("[keypaircreate] error generating keypair %q: %v", name, err)
return keypairCreateErrMsg{err: err}
}
shared.Debugf("[keypaircreate] generated keypair %q", name)
return keypairCreatedMsg{kp: kp}
})
}
Expand Down
9 changes: 9 additions & 0 deletions src/internal/ui/keypairlist/keypairlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ func New(client *gophercloud.ServiceClient, refreshInterval time.Duration) Model

// Init starts the initial fetch.
func (m Model) Init() tea.Cmd {
shared.Debugf("[keypairlist] Init()")
return tea.Batch(m.spinner.Tick, m.fetchKeypairs())
}

// Update handles messages.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case keypairsLoadedMsg:
shared.Debugf("[keypairlist] loaded %d keypairs", len(msg.keypairs))
var cursorName string
if m.cursor >= 0 && m.cursor < len(m.pairs) {
cursorName = m.pairs[m.cursor].Name
Expand All @@ -83,14 +85,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, nil

case keypairsErrMsg:
shared.Debugf("[keypairlist] error: %v", msg.err)
m.loading = false
m.err = msg.err.Error()
return m, nil

case shared.TickMsg:
if m.loading {
shared.Debugf("[keypairlist] tick skipped (loading)")
return m, nil
}
shared.Debugf("[keypairlist] tick fetching")
return m, m.fetchKeypairs()

case spinner.TickMsg:
Expand Down Expand Up @@ -281,16 +286,20 @@ func (m Model) SelectedKeyPair() *compute.KeyPair {
func (m Model) fetchKeypairs() tea.Cmd {
client := m.client
return func() tea.Msg {
shared.Debugf("[keypairlist] fetch start")
kps, err := compute.ListKeyPairs(context.Background(), client)
if err != nil {
shared.Debugf("[keypairlist] fetch error: %v", err)
return keypairsErrMsg{err: err}
}
shared.Debugf("[keypairlist] fetch done, count=%d", len(kps))
return keypairsLoadedMsg{keypairs: kps}
}
}

// ForceRefresh triggers a manual reload of the keypair list.
func (m *Model) ForceRefresh() tea.Cmd {
shared.Debugf("[keypairlist] ForceRefresh()")
m.loading = true
return tea.Batch(m.spinner.Tick, m.fetchKeypairs())
}
Expand Down
Loading
Loading