From 8c0748f82fec9e28a85c4736d3d80e31f8cd8510 Mon Sep 17 00:00:00 2001 From: FrostByte0x <271714451+FrostByte0x@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:46:03 +0000 Subject: [PATCH 1/2] CHORE: Bump dependancies CHORE: run go fix to modernize Go idioms Bumping kin-openapi added a verification that $ref fields are alone, which is not the case for OVH API, so we allow it. Logout.go: will move more error handling to errors.AsType later. Signed-off-by: FrostByte0x <271714451+FrostByte0x@users.noreply.github.com> --- .cds/workflows/build.yaml | 2 +- .github/workflows/build.yml | 2 +- go.mod | 70 +- go.sum | 183 +- internal/cmd/completion_test.go | 2 +- internal/openapi/openapi.go | 3 +- internal/services/baremetal/baremetal.go | 2 +- internal/services/browser/api.go | 582 +++--- internal/services/browser/backup_api.go | 48 +- internal/services/browser/backup_wizard.go | 5 +- internal/services/browser/file_api.go | 39 +- .../services/browser/floating_ip_wizard.go | 10 +- internal/services/browser/gateway_wizard.go | 15 +- .../browser/lb_health_monitor_wizard.go | 70 +- .../services/browser/lb_l7policy_wizard.go | 19 +- internal/services/browser/lb_l7rule_wizard.go | 6 +- .../services/browser/lb_listener_wizard.go | 18 +- internal/services/browser/lb_pool_wizard.go | 8 +- internal/services/browser/lb_wizard.go | 33 +- .../browser/managed_analytics_wizard.go | 47 +- .../services/browser/managed_db_wizard.go | 170 +- internal/services/browser/manager.go | 1784 ++++++++--------- internal/services/browser/object_api.go | 62 +- internal/services/browser/object_wizard.go | 11 +- .../browser/private_network_wizard.go | 56 +- .../views/block_storage/backup_detail.go | 19 +- .../browser/views/block_storage/detail.go | 20 +- .../views/block_storage/snapshot_detail.go | 8 +- .../browser/views/block_storage/table.go | 21 +- .../browser/views/file_storage/detail.go | 22 +- .../browser/views/instances/detail.go | 12 +- .../services/browser/views/instances/table.go | 25 +- .../browser/views/kubernetes/detail.go | 12 +- .../browser/views/kubernetes/nodepools.go | 58 +- .../browser/views/kubernetes/table.go | 21 +- .../browser/views/object_storage/detail.go | 42 +- .../views/object_storage/user_detail.go | 6 +- internal/services/browser/views/styles.go | 4 +- internal/services/browser/workflow_wizard.go | 15 +- internal/services/login/logout.go | 5 +- internal/services/login/logout_test.go | 2 +- internal/services/webhosting/webhosting.go | 5 +- 42 files changed, 1699 insertions(+), 1845 deletions(-) diff --git a/.cds/workflows/build.yaml b/.cds/workflows/build.yaml index 00bd3b3e9..3f0682332 100644 --- a/.cds/workflows/build.yaml +++ b/.cds/workflows/build.yaml @@ -4,7 +4,7 @@ on: [push] jobs: build: - runs-on: library/go-1-26 + runs-on: library/go-1-27 steps: - uses: actions/checkout - run: |- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aad326c7d..f4988e8f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.24.x', '1.25.x', '1.26.x' ] + go-version: [ '1.25.x', '1.26.x', '1.27.x' ] steps: - uses: actions/checkout@v5 diff --git a/go.mod b/go.mod index d76a0bfca..0d06071fc 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/ovh/ovhcloud-cli -go 1.26.0 +go 1.27.1 require ( - code.cloudfoundry.org/bytefmt v0.64.0 + code.cloudfoundry.org/bytefmt v0.90.0 dario.cat/mergo v1.0.2 github.com/NimbleMarkets/ntcharts v0.5.1 github.com/PaesslerAG/gval v1.2.4 @@ -11,71 +11,67 @@ require ( github.com/amstuta/fx v0.0.0-20250411141758-99b16761e78b github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/glamour v1.0.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/charmbracelet/x/ansi v0.11.6 + github.com/charmbracelet/x/ansi v0.11.8 github.com/charmbracelet/x/term v0.2.2 - github.com/getkin/kin-openapi v0.133.0 + github.com/getkin/kin-openapi v0.149.0 github.com/ghodss/yaml v1.0.0 github.com/google/uuid v1.6.0 - github.com/jarcoal/httpmock v1.4.1 - github.com/mattn/go-shellwords v1.0.12 - github.com/maxatome/go-testdeep v1.15.0 + github.com/jarcoal/httpmock v1.4.2 + github.com/mattn/go-shellwords v1.0.15 + github.com/maxatome/go-testdeep v1.16.0 github.com/maxatome/tdhttpmock v1.0.0 github.com/ovh/go-ovh v1.9.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa - golang.org/x/sync v0.20.0 - golang.org/x/text v0.37.0 - gopkg.in/ini.v1 v1.67.1 + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba + golang.org/x/sync v0.23.0 + golang.org/x/text v0.42.0 + gopkg.in/ini.v1 v1.67.3 ) require ( - github.com/alecthomas/chroma/v2 v2.23.1 // indirect + github.com/alecthomas/chroma/v2 v2.27.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20260223110321-bb4be0bca7ac // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20260920004010-53e2afe73ae5 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dlclark/regexp2/v2 v2.8.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/jsonpointer v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mailru/easyjson v0.9.1 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/lrstanley/bubblezone v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.30 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/oasdiff/yaml v0.1.1 // indirect + github.com/oasdiff/yaml3 v0.0.14 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect + github.com/sahilm/fuzzy v0.1.3 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/woodsbury/decimal128 v1.4.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.16 // indirect + github.com/xo/terminfo v1.2.0 // indirect + github.com/yuin/goldmark v1.8.6 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/oauth2 v0.37.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/term v0.46.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e59dbbf66..c7d5a91bd 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -code.cloudfoundry.org/bytefmt v0.64.0 h1:g5LF+pAbjI3fstZoFswBhGwcBk9tAcJ1fu5Ea8+icZ4= -code.cloudfoundry.org/bytefmt v0.64.0/go.mod h1:uMNYWUfigXArv2r8WzUpqYRcT30NTWlrDA1kjGrPxDU= +code.cloudfoundry.org/bytefmt v0.90.0 h1:0YI6Rgw22aMMUXAnInBy73WvrUWlvWqZLzkJgoGfvgo= +code.cloudfoundry.org/bytefmt v0.90.0/go.mod h1:RDBla+jDS0YPzUXaVOF2yhINP0kV2HCJGYdSYNPuaTw= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/NimbleMarkets/ntcharts v0.5.1 h1:HWtekubEXfESwi24pyFynwGo2Hulbb9fPh7INMUc1dg= github.com/NimbleMarkets/ntcharts v0.5.1/go.mod h1:zVeRqYkh2n59YPe1bflaSL4O2aD2ZemNmrbdEqZ70hk= github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= @@ -14,8 +14,8 @@ github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEs github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= +github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/amstuta/fx v0.0.0-20250411141758-99b16761e78b h1:hbaPXWcWIAJ8vsKU8tWwOuZbi/LcgNFmga7qbuf1PIE= @@ -32,20 +32,20 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= -github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= -github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= -github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ= +github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/slice v0.0.0-20260223110321-bb4be0bca7ac h1:868LzE25Z+KuaVr+2jh2gyw/XwahjapwjtvSUnmfVo8= -github.com/charmbracelet/x/exp/slice v0.0.0-20260223110321-bb4be0bca7ac/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= +github.com/charmbracelet/x/exp/slice v0.0.0-20260920004010-53e2afe73ae5 h1:VE9hnLa26cT2N+5m7Rj5dLkiFcBU4p6If0IH5Kug1bA= +github.com/charmbracelet/x/exp/slice v0.0.0-20260920004010-53e2afe73ae5/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= @@ -55,33 +55,32 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw= +github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= -github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA= +github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg= +github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY= +github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8= +github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe h1:QAinXoAFJdGQYztXn3VpFey7KCwpedbZ/EkzbplQ0cY= +github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -90,35 +89,36 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= -github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jarcoal/httpmock v1.4.2 h1:dKwiP/9zITCPfBLsDn3kchbSOu16JrnxtVEmL0fPRcI= +github.com/jarcoal/httpmock v1.4.2/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e h1:OLwZ8xVaeVrru0xyeuOX+fne0gQTFEGlzfNjipCbxlU= -github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e/go.mod h1:NQ34EGeu8FAYGBMDzwhfNJL8YQYoWZP5xYJPRDAwN3E= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/lrstanley/bubblezone v1.0.0 h1:bIpUaBilD42rAQwlg/4u5aTqVAt6DSRKYZuSdmkr8UA= +github.com/lrstanley/bubblezone v1.0.0/go.mod h1:kcTekA8HE/0Ll2bWzqHlhA2c513KDNLW7uDfDP4Mly8= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/maxatome/go-testdeep v1.15.0 h1:o3ghyKh6Hmy5EnyfQh+8lbH++4gpknmgc8jo7h/AVp8= -github.com/maxatome/go-testdeep v1.15.0/go.mod h1:BEC221DXFjTrG2VLzAYYi3xz8aK1QYa391djuaR2jkA= +github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= +github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mattn/go-shellwords v1.0.15 h1:rx0n8+ZdM9JWZMlr2BMPAjtLU0rfluLNtwMC2FJOTtY= +github.com/mattn/go-shellwords v1.0.15/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/maxatome/go-testdeep v1.16.0 h1:Btr/xvvx7bfQ63vGZ3zgOhM1kbTMusw+q0uVJtMp3gs= +github.com/maxatome/go-testdeep v1.16.0/go.mod h1:BEC221DXFjTrG2VLzAYYi3xz8aK1QYa391djuaR2jkA= github.com/maxatome/tdhttpmock v1.0.0 h1:yExbhieb7XayhnxlfumXcvFjQGKHElJZkXTYKtQeZC4= github.com/maxatome/tdhttpmock v1.0.0/go.mod h1:WrKuKZ2l6bqOD5z6PHiafRrrgqDH8ERxZgmaQwkjjYI= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -127,28 +127,30 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= -github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY= +github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU= +github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw= +github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg= +github.com/onsi/ginkgo/v2 v2.32.2 h1:2o6vyFvR6snrJWgRVztC+OwuqqPEMI1UzYl2s2iU7Cg= +github.com/onsi/ginkgo/v2 v2.32.2/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.43.0 h1:VlG/1FxqNxhSO+lq/OHBNaaqwiBK/mO8JbVkX9Y+FeU= +github.com/onsi/gomega v1.43.0/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU= +github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= @@ -166,42 +168,41 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= -github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4= +github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/completion_test.go b/internal/cmd/completion_test.go index fe97f0832..66f72b3c8 100644 --- a/internal/cmd/completion_test.go +++ b/internal/cmd/completion_test.go @@ -61,7 +61,7 @@ func TestCompletionInstall_Idempotent(t *testing.T) { home := setupHome(t) t.Setenv("SHELL", "/bin/bash") - for i := 0; i < 2; i++ { + for i := range 2 { if err := runCompletionInstall(nil, nil); err != nil { t.Fatalf("call %d: unexpected error: %v", i, err) } diff --git a/internal/openapi/openapi.go b/internal/openapi/openapi.go index acdea578d..c89e512af 100644 --- a/internal/openapi/openapi.go +++ b/internal/openapi/openapi.go @@ -77,7 +77,8 @@ func getRequestBodyFromSpec(spec []byte, path, method string) (*openapi3.MediaTy if err != nil { return nil, fmt.Errorf("failed to load spec: %w", err) } - if err = doc.Validate(context.Background()); err != nil { + // Remove AllowExtraSiblings when all OVH APIs are > 3.0 to allow extra fields with $ref + if err = doc.Validate(context.Background(), openapi3.AllowExtraSiblingFields("description")); err != nil { return nil, fmt.Errorf("failed to validate spec: %w", err) } diff --git a/internal/services/baremetal/baremetal.go b/internal/services/baremetal/baremetal.go index 3d12cf8a1..1f9a40d8f 100644 --- a/internal/services/baremetal/baremetal.go +++ b/internal/services/baremetal/baremetal.go @@ -244,7 +244,7 @@ func waitForDedicatedServerTask(serviceName string, taskID any) error { lastDescription = fmt.Sprintf("%v", taskID) ) - for retry := 0; retry < taskPollAttempts; retry++ { + for range taskPollAttempts { var task map[string]any if err := httpLib.Client.Get(endpoint, &task); err != nil { diff --git a/internal/services/browser/api.go b/internal/services/browser/api.go index 9a9015377..45d092e9a 100644 --- a/internal/services/browser/api.go +++ b/internal/services/browser/api.go @@ -171,9 +171,9 @@ func (m Model) fetchProjectsData() projectsLoadedMsg { } // Now fetch details for each project - var projects []map[string]interface{} + var projects []map[string]any for _, id := range projectIDs { - var project map[string]interface{} + var project map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s", id) if err := httpLib.Client.Get(endpoint, &project); err == nil { projects = append(projects, project) @@ -194,7 +194,7 @@ func (m Model) fetchInstancesData() instancesLoadedMsg { } } - var instances []map[string]interface{} + var instances []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/instance", m.cloudProject) err := httpLib.Client.Get(endpoint, &instances) if err != nil { @@ -211,11 +211,11 @@ func (m Model) fetchInstancesData() instancesLoadedMsg { } // fetchInstancesEnrichedData fetches images and floating IPs in parallel -func (m Model) fetchInstancesEnrichedData(instances []map[string]interface{}) tea.Cmd { +func (m Model) fetchInstancesEnrichedData(instances []map[string]any) tea.Cmd { return func() tea.Msg { // Fetch images to build imageId -> imageName map imageMap := make(map[string]string) - var images []map[string]interface{} + var images []map[string]any imagesEndpoint := fmt.Sprintf("/v1/cloud/project/%s/image", m.cloudProject) if imgErr := httpLib.Client.Get(imagesEndpoint, &images); imgErr == nil { for _, img := range images { @@ -238,12 +238,12 @@ func (m Model) fetchInstancesEnrichedData(instances []map[string]interface{}) te } // Fetch floating IPs for each region for region := range regionSet { - var floatingIPs []map[string]interface{} + var floatingIPs []map[string]any fipEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/floatingip", m.cloudProject, region) if fipErr := httpLib.Client.Get(fipEndpoint, &floatingIPs); fipErr == nil { for _, fip := range floatingIPs { // Check if floating IP is associated to an instance - if associatedEntity, ok := fip["associatedEntity"].(map[string]interface{}); ok { + if associatedEntity, ok := fip["associatedEntity"].(map[string]any); ok { if instanceId, ok := associatedEntity["id"].(string); ok && instanceId != "" { if ip, ok := fip["ip"].(string); ok { floatingIPMap[instanceId] = ip @@ -281,9 +281,9 @@ func (m Model) fetchKubernetesData() dataLoadedMsg { } // Now fetch details for each cluster - var clusters []map[string]interface{} + var clusters []map[string]any for _, id := range clusterIDs { - var cluster map[string]interface{} + var cluster map[string]any detailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s", m.cloudProject, id) if err := httpLib.Client.Get(detailEndpoint, &cluster); err == nil { clusters = append(clusters, cluster) @@ -307,14 +307,14 @@ func (m Model) fetchKubeNodePools(kubeId string) tea.Cmd { } // Fetch node pools for the cluster - var nodePools []map[string]interface{} + var nodePools []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s/nodepool", m.cloudProject, kubeId) err := httpLib.Client.Get(endpoint, &nodePools) if err != nil { // If node pools fail to load, don't treat it as a fatal error return kubeNodePoolsLoadedMsg{ kubeId: kubeId, - nodePools: []map[string]interface{}{}, + nodePools: []map[string]any{}, err: nil, } } @@ -365,7 +365,7 @@ func (m Model) fetchKubeRegions() tea.Cmd { func compareVersions(a, b string) int { parsePart := func(s string) []int { var parts []int - for _, seg := range strings.Split(s, ".") { + for seg := range strings.SplitSeq(s, ".") { n := 0 fmt.Sscanf(seg, "%d", &n) parts = append(parts, n) @@ -440,7 +440,7 @@ func (m Model) fetchKubeNetworks() tea.Cmd { } } - var networks []map[string]interface{} + var networks []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private", m.cloudProject) err := httpLib.Client.Get(endpoint, &networks) if err != nil { @@ -472,7 +472,7 @@ func (m Model) fetchKubeSubnets(networkID string) tea.Cmd { } } - var subnets []map[string]interface{} + var subnets []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private/%s/subnet", m.cloudProject, networkID) err := httpLib.Client.Get(endpoint, &subnets) if err != nil { @@ -496,7 +496,7 @@ func (m Model) fetchKubeSubnets(networkID string) tea.Cmd { } // createKubeCluster creates a new Kubernetes cluster -func (m Model) createKubeCluster(config map[string]interface{}) tea.Cmd { +func (m Model) createKubeCluster(config map[string]any) tea.Cmd { return func() tea.Msg { if m.cloudProject == "" { return kubeClusterCreatedMsg{ @@ -504,7 +504,7 @@ func (m Model) createKubeCluster(config map[string]interface{}) tea.Cmd { } } - var cluster map[string]interface{} + var cluster map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube", m.cloudProject) err := httpLib.Client.Post(endpoint, config, &cluster) if err != nil { @@ -614,7 +614,7 @@ func (m Model) handleKubeNodePoolsLoaded(msg kubeNodePoolsLoadedMsg) (tea.Model, // Store in cache if m.kubeNodePools == nil { - m.kubeNodePools = make(map[string][]map[string]interface{}) + m.kubeNodePools = make(map[string][]map[string]any) } m.kubeNodePools[msg.kubeId] = msg.nodePools @@ -686,9 +686,9 @@ func (m Model) fetchDatabasesData(category string) dataLoadedMsg { } // Now fetch details for each database service - var databases []map[string]interface{} + var databases []map[string]any for _, id := range serviceIDs { - var db map[string]interface{} + var db map[string]any detailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/database/service/%s", m.cloudProject, id) if err := httpLib.Client.Get(detailEndpoint, &db); err == nil { databases = append(databases, db) @@ -721,17 +721,17 @@ func (m Model) fetchS3StorageData() dataLoadedMsg { } // Fetch details for each region to check if it has S3 storage feature - var allContainers []map[string]interface{} + var allContainers []map[string]any for _, regionName := range regionNames { // Get region details to check for S3 feature - var region map[string]interface{} + var region map[string]any regionDetailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s", m.cloudProject, regionName) if err := httpLib.Client.Get(regionDetailEndpoint, ®ion); err != nil { continue } // Check if region has S3 storage feature - services, ok := region["services"].([]interface{}) + services, ok := region["services"].([]any) if !ok { continue } @@ -739,7 +739,7 @@ func (m Model) fetchS3StorageData() dataLoadedMsg { hasS3 := false s3Offer := "Standard" for _, svc := range services { - if svcMap, ok := svc.(map[string]interface{}); ok { + if svcMap, ok := svc.(map[string]any); ok { if name, ok := svcMap["name"].(string); ok { if name == "storage-s3-high-perf" { hasS3 = true @@ -762,13 +762,13 @@ func (m Model) fetchS3StorageData() dataLoadedMsg { deployMode = "Local Zone" } - var rawResponse []interface{} + var rawResponse []any storageEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/storage", m.cloudProject, regionName) if err := httpLib.Client.Get(storageEndpoint, &rawResponse); err == nil { for _, item := range rawResponse { if containerName, ok := item.(string); ok { // It's a container name, fetch details - var container map[string]interface{} + var container map[string]any detailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/storage/%s", m.cloudProject, regionName, containerName) if err := httpLib.Client.Get(detailEndpoint, &container); err == nil { container["_offer"] = s3Offer @@ -776,7 +776,7 @@ func (m Model) fetchS3StorageData() dataLoadedMsg { container["_type"] = "S3" allContainers = append(allContainers, container) } - } else if containerObj, ok := item.(map[string]interface{}); ok { + } else if containerObj, ok := item.(map[string]any); ok { containerObj["_offer"] = s3Offer containerObj["_deployMode"] = deployMode containerObj["_type"] = "S3" @@ -787,12 +787,12 @@ func (m Model) fetchS3StorageData() dataLoadedMsg { } // Fetch cloud users with their S3 credentials - var s3Users []map[string]interface{} - var cloudUsers []map[string]interface{} + var s3Users []map[string]any + var cloudUsers []map[string]any userEndpoint := fmt.Sprintf("/v1/cloud/project/%s/user", m.cloudProject) if err := httpLib.Client.Get(userEndpoint, &cloudUsers); err == nil { for _, user := range cloudUsers { - userEntry := make(map[string]interface{}) + userEntry := make(map[string]any) userEntry["_username"] = user["username"] userEntry["_userDescription"] = user["description"] userEntry["_userId"] = user["id"] @@ -817,7 +817,7 @@ func (m Model) fetchS3StorageData() dataLoadedMsg { continue } - var s3Creds []map[string]interface{} + var s3Creds []map[string]any s3Endpoint := fmt.Sprintf("/v1/cloud/project/%s/user/%d/s3Credentials", m.cloudProject, userId) if err := httpLib.Client.Get(s3Endpoint, &s3Creds); err == nil && len(s3Creds) > 0 { cred := s3Creds[0] @@ -850,7 +850,7 @@ func (m Model) fetchSwiftStorageData() dataLoadedMsg { } // includeType=true makes the API return the containerType field (private/public/static) - var rawResponse []interface{} + var rawResponse []any endpoint := fmt.Sprintf("/v1/cloud/project/%s/storage?includeType=true", m.cloudProject) err := httpLib.Client.Get(endpoint, &rawResponse) if err != nil { @@ -860,12 +860,12 @@ func (m Model) fetchSwiftStorageData() dataLoadedMsg { } } - var containers []map[string]interface{} + var containers []map[string]any if len(rawResponse) > 0 { if _, ok := rawResponse[0].(string); ok { for _, item := range rawResponse { if containerID, ok := item.(string); ok { - var container map[string]interface{} + var container map[string]any detailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/storage/%s?includeType=true", m.cloudProject, containerID) if err := httpLib.Client.Get(detailEndpoint, &container); err == nil { container["_type"] = "Swift" @@ -876,7 +876,7 @@ func (m Model) fetchSwiftStorageData() dataLoadedMsg { } else { // Response contains full objects for _, item := range rawResponse { - if obj, ok := item.(map[string]interface{}); ok { + if obj, ok := item.(map[string]any); ok { obj["_type"] = "Swift" containers = append(containers, obj) } @@ -899,7 +899,7 @@ func (m Model) fetchBlockStorageData() dataLoadedMsg { } // Try to fetch as array of interfaces first (could be strings or objects) - var rawResponse []interface{} + var rawResponse []any endpoint := fmt.Sprintf("/v1/cloud/project/%s/volume", m.cloudProject) err := httpLib.Client.Get(endpoint, &rawResponse) if err != nil { @@ -910,13 +910,13 @@ func (m Model) fetchBlockStorageData() dataLoadedMsg { } // Check if response contains strings (IDs) or objects - var volumes []map[string]interface{} + var volumes []map[string]any if len(rawResponse) > 0 { if _, ok := rawResponse[0].(string); ok { // Response contains string IDs, fetch details for each for _, item := range rawResponse { if volumeID, ok := item.(string); ok { - var volume map[string]interface{} + var volume map[string]any detailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s", m.cloudProject, volumeID) if err := httpLib.Client.Get(detailEndpoint, &volume); err == nil { volumes = append(volumes, volume) @@ -926,14 +926,14 @@ func (m Model) fetchBlockStorageData() dataLoadedMsg { } else { // Response contains full objects for _, item := range rawResponse { - if obj, ok := item.(map[string]interface{}); ok { + if obj, ok := item.(map[string]any); ok { volumes = append(volumes, obj) } } } } - var filtered []map[string]interface{} + var filtered []map[string]any for _, v := range volumes { status := getString(v, "status") if status != "deleting" && status != "deleted" { @@ -971,7 +971,7 @@ func (m Model) fetchVolumeRegions() tea.Cmd { go func(regionName string) { typesEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volumeType", m.cloudProject, url.PathEscape(regionName)) - var rawTypes []map[string]interface{} + var rawTypes []map[string]any if err := httpLib.Client.Get(typesEndpoint, &rawTypes); err != nil { ch <- probeResult{region: regionName} return @@ -984,7 +984,7 @@ func (m Model) fetchVolumeRegions() tea.Cmd { continue } types = append(types, n) - if azRaw, ok := t["availabilityZones"].([]interface{}); ok { + if azRaw, ok := t["availabilityZones"].([]any); ok { var azs []string for _, az := range azRaw { if s, ok := az.(string); ok && s != "" { @@ -1030,7 +1030,7 @@ func (m Model) fetchVolumeTypes(region string) tea.Cmd { return volumeTypesLoadedMsg{err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volumeType", m.cloudProject, url.PathEscape(region)) - var rawTypes []map[string]interface{} + var rawTypes []map[string]any if err := httpLib.Client.Get(endpoint, &rawTypes); err != nil { return volumeTypesLoadedMsg{err: fmt.Errorf("failed to fetch volume types: %w", err)} } @@ -1042,7 +1042,7 @@ func (m Model) fetchVolumeTypes(region string) tea.Cmd { continue } types = append(types, n) - if azRaw, ok := t["availabilityZones"].([]interface{}); ok { + if azRaw, ok := t["availabilityZones"].([]any); ok { var azs []string for _, az := range azRaw { if s, ok := az.(string); ok && s != "" { @@ -1064,12 +1064,12 @@ func (m Model) fetchVolumeAvailabilityZones(region string) tea.Cmd { return volumeAZLoadedMsg{err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s", m.cloudProject, url.PathEscape(region)) - var regionDetail map[string]interface{} + var regionDetail map[string]any if err := httpLib.Client.Get(endpoint, ®ionDetail); err != nil { return volumeAZLoadedMsg{err: fmt.Errorf("failed to fetch region details: %w", err)} } var azs []string - if raw, ok := regionDetail["availabilityZones"].([]interface{}); ok { + if raw, ok := regionDetail["availabilityZones"].([]any); ok { for _, az := range raw { if s, ok := az.(string); ok && s != "" && s != "nova" { azs = append(azs, s) @@ -1090,7 +1090,7 @@ func (m Model) createVolume() tea.Cmd { if m.wizard.volumeEncryptionIdx == 1 && !strings.HasSuffix(effectiveType, "-luks") { effectiveType += "-luks" } - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.volumeName, "size": m.wizard.volumeSize, "type": effectiveType, @@ -1099,7 +1099,7 @@ func (m Model) createVolume() tea.Cmd { body["availabilityZone"] = m.wizard.volumeAvailabilityZone } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volume", m.cloudProject, url.PathEscape(m.wizard.selectedRegion)) - var volume map[string]interface{} + var volume map[string]any err := httpLib.Client.Post(endpoint, body, &volume) return volumeCreatedMsg{volume: volume, err: err} } @@ -1109,7 +1109,7 @@ func (m Model) deleteVolume(volumeId string) tea.Cmd { return func() tea.Msg { // Check for snapshots first — the API rejects delete if any exist snapshotsEndpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/snapshot", m.cloudProject) - var snapshots []map[string]interface{} + var snapshots []map[string]any if err := httpLib.Client.Get(snapshotsEndpoint, &snapshots); err == nil { var blocking []string for _, s := range snapshots { @@ -1133,7 +1133,7 @@ func (m Model) deleteVolume(volumeId string) tea.Cmd { func (m Model) renameVolume(volumeId, newName string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s", m.cloudProject, url.PathEscape(volumeId)) - body := map[string]interface{}{"name": newName} + body := map[string]any{"name": newName} err := httpLib.Client.Put(endpoint, body, nil) return volumeActionDoneMsg{action: 1, err: err} } @@ -1142,7 +1142,7 @@ func (m Model) renameVolume(volumeId, newName string) tea.Cmd { func (m Model) extendVolume(volumeId string, newSizeGB int) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s/upsize", m.cloudProject, url.PathEscape(volumeId)) - body := map[string]interface{}{"size": newSizeGB} + body := map[string]any{"size": newSizeGB} err := httpLib.Client.Post(endpoint, body, nil) return volumeActionDoneMsg{action: 2, err: err} } @@ -1159,7 +1159,7 @@ func (m Model) handleVolumeRegionsLoaded(msg volumeRegionsLoadedMsg) (tea.Model, } m.wizard.regions = nil for _, name := range msg.regionNames { - m.wizard.regions = append(m.wizard.regions, map[string]interface{}{"name": name}) + m.wizard.regions = append(m.wizard.regions, map[string]any{"name": name}) } m.wizard.volumeRegionTypeMap = msg.regionTypeMap m.wizard.volumeRegionTypeAZMap = msg.regionTypeAZMap @@ -1318,7 +1318,7 @@ func (m Model) fetchLBPools(lbID, region string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool?loadbalancerId=%s", m.cloudProject, url.PathEscape(region), url.QueryEscape(lbID)) - var pools []map[string]interface{} + var pools []map[string]any if err := httpLib.Client.Get(endpoint, &pools); err != nil { return lbPoolsLoadedMsg{lbID: lbID, err: err} } @@ -1334,12 +1334,12 @@ func (m Model) fetchLBListeners(lbID, region string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/listener", m.cloudProject, url.PathEscape(region)) - var all []map[string]interface{} + var all []map[string]any if err := httpLib.Client.Get(endpoint, &all); err != nil { return lbListenersLoadedMsg{lbID: lbID, err: err} } // Filter client-side: handle both singular "loadbalancerId" and list "loadBalancerIds" - var listeners []map[string]interface{} + var listeners []map[string]any for _, l := range all { // Case 1: singular string field "loadbalancerId" if v, ok := l["loadbalancerId"].(string); ok && v == lbID { @@ -1347,7 +1347,7 @@ func (m Model) fetchLBListeners(lbID, region string) tea.Cmd { continue } // Case 2: list field "loadBalancerIds" - if ids, ok := l["loadBalancerIds"].([]interface{}); ok { + if ids, ok := l["loadBalancerIds"].([]any); ok { for _, id := range ids { if fmt.Sprintf("%v", id) == lbID { listeners = append(listeners, l) @@ -1400,17 +1400,17 @@ func (m Model) updateLBPool() tea.Cmd { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool/%s", m.cloudProject, url.PathEscape(m.wizard.lbPoolLBRegion), url.PathEscape(m.wizard.lbPoolEditPoolId)) - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.lbPoolName, "algorithm": m.wizard.lbPoolAlgo, } if m.wizard.lbPoolSession != "" && m.wizard.lbPoolSession != "disabled" { - body["sessionPersistence"] = map[string]interface{}{ + body["sessionPersistence"] = map[string]any{ "type": m.wizard.lbPoolSession, } } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Put(endpoint, body, &result); err != nil { return lbPoolUpdatedMsg{poolName: m.wizard.lbPoolName, err: fmt.Errorf("update failed: %w", err)} } @@ -1454,14 +1454,14 @@ func (m Model) updateLBListener() tea.Cmd { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/listener/%s", m.cloudProject, url.PathEscape(m.wizard.lbListenerLBRegion), url.PathEscape(m.wizard.lbListenerEditId)) - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.lbListenerName, } if m.wizard.lbListenerPoolId != "" { body["defaultPoolId"] = m.wizard.lbListenerPoolId } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Put(endpoint, body, &result); err != nil { return lbListenerUpdatedMsg{listenerName: m.wizard.lbListenerName, err: fmt.Errorf("update failed: %w", err)} } @@ -1552,14 +1552,14 @@ func (m Model) executePrivNetworkDelete() tea.Cmd { // The region-based API uses openstackId, not the vRack pn-XXXXX_N id. // Each network has regions[].{region, openstackId} — delete from all regions. - regions, ok := m.detailData["regions"].([]interface{}) + regions, ok := m.detailData["regions"].([]any) if !ok || len(regions) == 0 { return privNetDeletedMsg{networkName: networkName, err: fmt.Errorf("no region found for this network")} } var lastErr error for _, r := range regions { - rm, ok := r.(map[string]interface{}) + rm, ok := r.(map[string]any) if !ok { continue } @@ -1598,7 +1598,7 @@ func (m Model) fetchPrivateNetworksData() dataLoadedMsg { } } - var networks []map[string]interface{} + var networks []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private", m.cloudProject) if err := httpLib.Client.Get(endpoint, &networks); err != nil { return dataLoadedMsg{err: err} @@ -1622,8 +1622,8 @@ func (m Model) fetchPrivateNetworksData() dataLoadedMsg { } // Embed _regionType on each network based on its first region for i, n := range networks { - if regions, ok := n["regions"].([]interface{}); ok && len(regions) > 0 { - if rm, ok := regions[0].(map[string]interface{}); ok { + if regions, ok := n["regions"].([]any); ok && len(regions) > 0 { + if rm, ok := regions[0].(map[string]any); ok { if reg := getString(rm, "region"); reg != "" { networks[i]["_regionType"] = regionTypeMap[reg] } @@ -1649,9 +1649,9 @@ func (m Model) fetchPrivateNetworksData() dataLoadedMsg { // Collect all unique region names used by these networks. regionSet := map[string]bool{} for _, n := range networks { - if regions, ok := n["regions"].([]interface{}); ok { + if regions, ok := n["regions"].([]any); ok { for _, rv := range regions { - if rm, ok := rv.(map[string]interface{}); ok { + if rm, ok := rv.(map[string]any); ok { if r := getString(rm, "region"); r != "" { regionSet[r] = true } @@ -1671,9 +1671,9 @@ func (m Model) fetchPrivateNetworksData() dataLoadedMsg { for _, regionGWs := range allRegionGateways { for _, gw := range regionGWs { gwName := getString(gw, "name") - if interfaces, ok := gw["interfaces"].([]interface{}); ok { + if interfaces, ok := gw["interfaces"].([]any); ok { for _, iface := range interfaces { - if ifaceMap, ok := iface.(map[string]interface{}); ok { + if ifaceMap, ok := iface.(map[string]any); ok { if netID := getString(ifaceMap, "networkId"); netID != "" { openstackToGateway[netID] = gwName } @@ -1684,9 +1684,9 @@ func (m Model) fetchPrivateNetworksData() dataLoadedMsg { } // Match each network's openstackId against the map for i, n := range networks { - if regions, ok := n["regions"].([]interface{}); ok { + if regions, ok := n["regions"].([]any); ok { for _, rv := range regions { - if rm, ok := rv.(map[string]interface{}); ok { + if rm, ok := rv.(map[string]any); ok { opID := getString(rm, "openstackId") if gwName, found := openstackToGateway[opID]; found && gwName != "" { networks[i]["_gatewayName"] = gwName @@ -1705,7 +1705,7 @@ func (m Model) fetchPrivateNetworksData() dataLoadedMsg { } // fetchPrivateNetRegions returns regions suitable for private network creation with their type. -func (m Model) fetchPrivateNetRegions() ([]map[string]interface{}, error) { +func (m Model) fetchPrivateNetRegions() ([]map[string]any, error) { regionEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region", m.cloudProject) var allNames []string if err := httpLib.Client.Get(regionEndpoint, &allNames); err != nil { @@ -1716,7 +1716,7 @@ func (m Model) fetchPrivateNetRegions() ([]map[string]interface{}, error) { ids[i] = n } details, _ := httpLib.FetchObjectsParallel[map[string]any](regionEndpoint+"/%s", ids, true) - var result []map[string]interface{} + var result []map[string]any for i, d := range details { if d == nil { continue @@ -1725,9 +1725,9 @@ func (m Model) fetchPrivateNetRegions() ([]map[string]interface{}, error) { rtype, _ := d["type"].(string) // Only include regions that support vrack / network hasNetwork := false - if services, ok := d["services"].([]interface{}); ok { + if services, ok := d["services"].([]any); ok { for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if sm["name"] == "network" && sm["status"] == "UP" { hasNetwork = true break @@ -1736,7 +1736,7 @@ func (m Model) fetchPrivateNetRegions() ([]map[string]interface{}, error) { } } if hasNetwork { - result = append(result, map[string]interface{}{ + result = append(result, map[string]any{ "name": name, "type": rtype, }) @@ -1753,7 +1753,7 @@ func (m Model) fetchPublicNetworksData() dataLoadedMsg { } } - var networks []map[string]interface{} + var networks []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/public", m.cloudProject) err := httpLib.Client.Get(endpoint, &networks) @@ -1786,9 +1786,9 @@ func (m Model) fetchNetworkRegions() ([]string, error) { continue } name := allNames[i] - if services, ok := r["services"].([]interface{}); ok { + if services, ok := r["services"].([]any); ok { for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if sm["name"] == "network" && sm["status"] == "UP" { result = append(result, name) break @@ -1822,7 +1822,7 @@ func (m Model) fetchFloatingIPsData() dataLoadedMsg { allRegionIPs, _ := httpLib.FetchObjectsParallel[[]map[string]any](regionEndpoint+"/%s/floatingip", regions, true) - var floatingIPs []map[string]interface{} + var floatingIPs []map[string]any for i, ips := range allRegionIPs { for _, ip := range ips { if r, _ := ip["region"].(string); r == "" { @@ -1833,7 +1833,7 @@ func (m Model) fetchFloatingIPsData() dataLoadedMsg { } // Also fetch failover IPs (Additional IPs) - var failoverIPs []map[string]interface{} + var failoverIPs []map[string]any failEndpoint := fmt.Sprintf("/v1/cloud/project/%s/ip/failover", m.cloudProject) httpLib.Client.Get(failEndpoint, &failoverIPs) // ignore error — not all projects have failover IPs @@ -1858,7 +1858,7 @@ func (m Model) fetchFIPInstances() tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/instance?region=%s", m.cloudProject, url.QueryEscape(region)) - var instances []map[string]interface{} + var instances []map[string]any if err := httpLib.Client.Get(endpoint, &instances); err != nil { return fipInstancesLoadedMsg{err: err} } @@ -1880,9 +1880,9 @@ func (m Model) createStandaloneFloatingIP() tea.Cmd { var privateIP string for _, inst := range m.wizard.fipInstances { if id, _ := inst["id"].(string); id == m.wizard.fipInstanceId { - if addrs, ok := inst["ipAddresses"].([]interface{}); ok { + if addrs, ok := inst["ipAddresses"].([]any); ok { for _, a := range addrs { - addrMap, ok := a.(map[string]interface{}) + addrMap, ok := a.(map[string]any) if !ok { continue } @@ -1906,13 +1906,13 @@ func (m Model) createStandaloneFloatingIP() tea.Cmd { m.cloudProject, url.PathEscape(m.wizard.fipRegion), url.PathEscape(m.wizard.fipInstanceId)) // First attempt: without gateway (subnet already has one) - var result map[string]interface{} - err := httpLib.Client.Post(endpoint, map[string]interface{}{"ip": privateIP}, &result) + var result map[string]any + err := httpLib.Client.Post(endpoint, map[string]any{"ip": privateIP}, &result) if err != nil && strings.Contains(err.Error(), "subnet require to have router") { // Subnet has no gateway yet — auto-create one and retry - bodyWithGW := map[string]interface{}{ + bodyWithGW := map[string]any{ "ip": privateIP, - "gateway": map[string]interface{}{ + "gateway": map[string]any{ "model": "s", "name": "gw-" + m.wizard.fipInstanceName, }, @@ -1940,11 +1940,11 @@ func (m Model) fetchNetworkSubnets(networkID string) tea.Cmd { func (m Model) fetchPrivateNetworkDetail(networkID string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private/%s", m.cloudProject, url.PathEscape(networkID)) - var netData map[string]interface{} + var netData map[string]any if err := httpLib.Client.Get(endpoint, &netData); err != nil { return privNetDetailLoadedMsg{networkID: networkID} } - regions, _ := netData["regions"].([]interface{}) + regions, _ := netData["regions"].([]any) return privNetDetailLoadedMsg{networkID: networkID, regions: regions} } } @@ -1955,11 +1955,11 @@ func (m Model) executeRegionDelete() tea.Cmd { if netID == "" { return regionDeletedMsg{err: fmt.Errorf("network ID missing")} } - regions, _ := m.detailData["regions"].([]interface{}) + regions, _ := m.detailData["regions"].([]any) if m.privNetSelectedRegion >= len(regions) { return regionDeletedMsg{networkID: netID, err: fmt.Errorf("no region selected")} } - rm, _ := regions[m.privNetSelectedRegion].(map[string]interface{}) + rm, _ := regions[m.privNetSelectedRegion].(map[string]any) regionName := getString(rm, "region") if regionName == "" { return regionDeletedMsg{networkID: netID, err: fmt.Errorf("region name missing")} @@ -1985,21 +1985,21 @@ func (m Model) executeGatewayDetachFromNetwork() tea.Cmd { if netID == "" { return gatewayDetachedMsg{err: fmt.Errorf("network ID missing")} } - regions, _ := m.detailData["regions"].([]interface{}) + regions, _ := m.detailData["regions"].([]any) if len(regions) == 0 { return gatewayDetachedMsg{networkID: netID, err: fmt.Errorf("no regions found on this network")} } detached := 0 for _, rv := range regions { - rm, _ := rv.(map[string]interface{}) + rm, _ := rv.(map[string]any) regionName := getString(rm, "region") openstackID := getString(rm, "openstackId") if regionName == "" || openstackID == "" { continue } gwEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/gateway", m.cloudProject, url.PathEscape(regionName)) - var gateways []map[string]interface{} + var gateways []map[string]any if err := httpLib.Client.Get(gwEndpoint, &gateways); err != nil { continue } @@ -2008,9 +2008,9 @@ func (m Model) executeGatewayDetachFromNetwork() tea.Cmd { if gwID == "" { continue } - interfaces, _ := gw["interfaces"].([]interface{}) + interfaces, _ := gw["interfaces"].([]any) for _, iface := range interfaces { - ifaceMap, _ := iface.(map[string]interface{}) + ifaceMap, _ := iface.(map[string]any) if getString(ifaceMap, "networkId") != openstackID { continue } @@ -2051,9 +2051,9 @@ func (m Model) executeSubnetDelete() tea.Cmd { // Fast path: subnet has networkId (returned by new regional endpoint). // Fallback: query each region's subnet list to find where this subnet lives. region := "" - if regions, ok := m.detailData["regions"].([]interface{}); ok { + if regions, ok := m.detailData["regions"].([]any); ok { for _, rv := range regions { - if rm, ok := rv.(map[string]interface{}); ok { + if rm, ok := rv.(map[string]any); ok { if getString(rm, "openstackId") == openstackNetID { region = getString(rm, "region") break @@ -2062,7 +2062,7 @@ func (m Model) executeSubnetDelete() tea.Cmd { } if region == "" { for _, rv := range regions { - if rm, ok := rv.(map[string]interface{}); ok { + if rm, ok := rv.(map[string]any); ok { rName := getString(rm, "region") opID := getString(rm, "openstackId") if rName == "" || opID == "" { @@ -2070,7 +2070,7 @@ func (m Model) executeSubnetDelete() tea.Cmd { } searchEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network/%s/subnet", m.cloudProject, url.PathEscape(rName), url.PathEscape(opID)) - var regionSubnets []map[string]interface{} + var regionSubnets []map[string]any if err := httpLib.Client.Get(searchEndpoint, ®ionSubnets); err == nil { for _, rs := range regionSubnets { if getString(rs, "id") == subID { @@ -2184,9 +2184,9 @@ func (m Model) fetchLoadBalancersData() dataLoadedMsg { if r == nil { continue } - if services, ok := r["services"].([]interface{}); ok { + if services, ok := r["services"].([]any); ok { for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if sm["name"] == "octavialoadbalancer" && sm["status"] == "UP" { regions = append(regions, allNames[i]) regionNames = append(regionNames, allNames[i]) @@ -2216,7 +2216,7 @@ func (m Model) fetchLoadBalancersData() dataLoadedMsg { } } - var lbs []map[string]interface{} + var lbs []map[string]any for i, regionLBs := range allRegionLBs { for _, lb := range regionLBs { if r, _ := lb["region"].(string); r == "" { @@ -2257,7 +2257,7 @@ func (m Model) fetchLoadBalancersData() dataLoadedMsg { lbFIPMap := make(map[string]string) // lbID -> floatingIP address for _, fips := range allRegionFIPs { for _, fip := range fips { - if entity, ok := fip["associatedEntity"].(map[string]interface{}); ok { + if entity, ok := fip["associatedEntity"].(map[string]any); ok { if getStringValue(entity, "type", "") == "loadbalancer" { lbID := getStringValue(entity, "id", "") if lbID != "" { @@ -2272,7 +2272,7 @@ func (m Model) fetchLoadBalancersData() dataLoadedMsg { for _, lb := range lbs { if lbID, ok := lb["id"].(string); ok { if ip, found := lbFIPMap[lbID]; found { - lb["floatingIp"] = map[string]interface{}{"ip": ip} + lb["floatingIp"] = map[string]any{"ip": ip} } } } @@ -2302,7 +2302,7 @@ func (m Model) fetchGatewaysData() dataLoadedMsg { allRegionGateways, _ := httpLib.FetchObjectsParallel[[]map[string]any](regionEndpoint+"/%s/gateway", regions, true) - var gateways []map[string]interface{} + var gateways []map[string]any for i, regionGateways := range allRegionGateways { for _, gw := range regionGateways { if r, _ := gw["region"].(string); r == "" { @@ -2567,7 +2567,7 @@ func (m Model) handleDataLoaded(msg dataLoadedMsg) (tea.Model, tea.Cmd) { } // createProjectsTable creates a table for displaying projects -func createProjectsTable(projects []map[string]interface{}, height int) table.Model { +func createProjectsTable(projects []map[string]any, height int) table.Model { columns := []table.Column{ {Title: "Project ID", Width: 40}, {Title: "Name", Width: 40}, @@ -2585,10 +2585,7 @@ func createProjectsTable(projects []map[string]interface{}, height int) table.Mo } // Calculate table height: leave room for header(2) + nav(3) + title(3) + footer(3) + borders(4) - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -2616,7 +2613,7 @@ func createProjectsTable(projects []map[string]interface{}, height int) table.Mo } // createInstancesTable creates a table for displaying instances (like OVHcloud web UI) -func createInstancesTable(instances []map[string]interface{}, imageMap map[string]string, floatingIPMap map[string]string, width, height int) table.Model { +func createInstancesTable(instances []map[string]any, imageMap map[string]string, floatingIPMap map[string]string, width, height int) table.Model { // Sort instances by name for stable ordering sort.Slice(instances, func(i, j int) bool { nameI := getString(instances[i], "name") @@ -2637,9 +2634,9 @@ func createInstancesTable(instances []map[string]interface{}, imageMap map[strin for _, instance := range instances { // Extract public IP from ipAddresses array publicIP := "" - if addresses, ok := instance["ipAddresses"].([]interface{}); ok { + if addresses, ok := instance["ipAddresses"].([]any); ok { for _, addr := range addresses { - if addrMap, ok := addr.(map[string]interface{}); ok { + if addrMap, ok := addr.(map[string]any); ok { ipType := getString(addrMap, "type") if ipType == "public" { version := getNumericValue(addrMap, "version") @@ -2681,7 +2678,7 @@ func createInstancesTable(instances []map[string]interface{}, imageMap map[strin // Extract flavor name from planCode (e.g., "b2-7.consumption" -> "b2-7") flavorName := "" - if flavor, ok := instance["flavor"].(map[string]interface{}); ok { + if flavor, ok := instance["flavor"].(map[string]any); ok { flavorName = getString(flavor, "name") } if flavorName == "" { @@ -2721,10 +2718,7 @@ func createInstancesTable(instances []map[string]interface{}, imageMap map[strin } // Calculate table height: leave room for header(2) + nav(3) + title(3) + footer(3) + borders(4) - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -2753,7 +2747,7 @@ func createInstancesTable(instances []map[string]interface{}, imageMap map[strin // createInstanceBackupsTable creates a table for instance backups (snapshots). // Columns: Name, Region, Model (flavorType), Size, Status, Created -func createInstanceBackupsTable(data []map[string]interface{}, width, height int) table.Model { +func createInstanceBackupsTable(data []map[string]any, width, height int) table.Model { sort.Slice(data, func(i, j int) bool { return getString(data[i], "name") < getString(data[j], "name") }) @@ -2778,7 +2772,7 @@ func createInstanceBackupsTable(data []map[string]interface{}, width, height int // Location: snapshots expose "regions" as []interface{} or "region" as string location := getString(s, "region") if location == "" { - if regions, ok := s["regions"].([]interface{}); ok && len(regions) > 0 { + if regions, ok := s["regions"].([]any); ok && len(regions) > 0 { var regionNames []string for _, r := range regions { if rs, ok := r.(string); ok { @@ -2817,10 +2811,7 @@ func createInstanceBackupsTable(data []map[string]interface{}, width, height int rows = append(rows, table.Row{name, id, location, sizeStr, created, status}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -2847,7 +2838,7 @@ func createInstanceBackupsTable(data []map[string]interface{}, width, height int // createWorkflowsTable creates a table for backup workflows. // Columns: Name, ID, Backup, Location, Workflow (cron), Targeted Resource, Rotation, Last Execution, Last State -func createWorkflowsTable(data []map[string]interface{}, width, height int) table.Model { +func createWorkflowsTable(data []map[string]any, width, height int) table.Model { sort.Slice(data, func(i, j int) bool { return getString(data[i], "name") < getString(data[j], "name") }) @@ -2897,10 +2888,7 @@ func createWorkflowsTable(data []map[string]interface{}, width, height int) tabl rows = append(rows, table.Row{name, id, backup, location, workflowStr, instanceId, Scheduling}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -2926,7 +2914,7 @@ func createWorkflowsTable(data []map[string]interface{}, width, height int) tabl } // createKubernetesTable creates a table for Kubernetes clusters -func createKubernetesTable(clusters []map[string]interface{}, width, height int) table.Model { +func createKubernetesTable(clusters []map[string]any, width, height int) table.Model { // Sort clusters by name for stable ordering sort.Slice(clusters, func(i, j int) bool { nameI := getString(clusters[i], "name") @@ -2970,10 +2958,7 @@ func createKubernetesTable(clusters []map[string]interface{}, width, height int) } // Calculate table height: leave room for header(2) + nav(3) + title(3) + footer(3) + borders(4) - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3001,7 +2986,7 @@ func createKubernetesTable(clusters []map[string]interface{}, width, height int) } // createGenericTable creates a generic table for any data -func createGenericTable(data []map[string]interface{}, width, height int) table.Model { +func createGenericTable(data []map[string]any, width, height int) table.Model { if len(data) == 0 { return table.Model{} } @@ -3013,10 +2998,7 @@ func createGenericTable(data []map[string]interface{}, width, height int) table. } columns := make([]table.Column, 0, len(keys)) - colWidth := width / len(keys) - if colWidth < 15 { - colWidth = 15 - } + colWidth := max(width/len(keys), 15) if colWidth > 40 { colWidth = 40 } @@ -3038,10 +3020,7 @@ func createGenericTable(data []map[string]interface{}, width, height int) table. } // Calculate table height: leave room for header(2) + nav(3) + title(3) + footer(3) + borders(4) - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3069,7 +3048,7 @@ func createGenericTable(data []map[string]interface{}, width, height int) table. } // createPrivateNetworksTable creates a table for private networks (one tab's worth of data). -func createPrivateNetworksTable(data []map[string]interface{}, width, height int) table.Model { +func createPrivateNetworksTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "VLAN ID", Width: 8}, {Title: "Name", Width: 22}, @@ -3096,9 +3075,9 @@ func createPrivateNetworksTable(data []map[string]interface{}, width, height int } name := getString(net, "name") var locationParts []string - if regions, ok := net["regions"].([]interface{}); ok { + if regions, ok := net["regions"].([]any); ok { for _, r := range regions { - if rm, ok := r.(map[string]interface{}); ok { + if rm, ok := r.(map[string]any); ok { if reg := getString(rm, "region"); reg != "" { locationParts = append(locationParts, reg) } @@ -3113,7 +3092,7 @@ func createPrivateNetworksTable(data []map[string]interface{}, width, height int gateway := "-" dhcp := "-" allocPool := "-" - if subnets, ok := net["_subnets"].([]map[string]interface{}); ok && len(subnets) > 0 { + if subnets, ok := net["_subnets"].([]map[string]any); ok && len(subnets) > 0 { sub := subnets[0] if v := getString(sub, "cidr"); v != "" { cidr = v @@ -3128,8 +3107,8 @@ func createPrivateNetworksTable(data []map[string]interface{}, width, height int dhcp = "✗" } } - if pools, ok := sub["ipPools"].([]interface{}); ok && len(pools) > 0 { - if pool, ok := pools[0].(map[string]interface{}); ok { + if pools, ok := sub["ipPools"].([]any); ok && len(pools) > 0 { + if pool, ok := pools[0].(map[string]any); ok { start := getString(pool, "start") end := getString(pool, "end") if start != "" && end != "" { @@ -3145,10 +3124,7 @@ func createPrivateNetworksTable(data []map[string]interface{}, width, height int rows = append(rows, table.Row{vlanId, name, location, cidr, gateway, dhcp, allocPool, gwName}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 25 { tableHeight = 25 } @@ -3174,7 +3150,7 @@ func createPrivateNetworksTable(data []map[string]interface{}, width, height int } // createLoadBalancersTable creates a table for load balancers. -func createLoadBalancersTable(data []map[string]interface{}, width, height int) table.Model { +func createLoadBalancersTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Name", Width: 22}, {Title: "Region", Width: 16}, @@ -3203,7 +3179,7 @@ func createLoadBalancersTable(data []map[string]interface{}, width, height int) status := getString(lb, "operatingStatus") publicIP := "-" - if fi, ok := lb["floatingIp"].(map[string]interface{}); ok { + if fi, ok := lb["floatingIp"].(map[string]any); ok { if v := getString(fi, "ip"); v != "" { publicIP = v } @@ -3218,10 +3194,7 @@ func createLoadBalancersTable(data []map[string]interface{}, width, height int) rows = append(rows, table.Row{name, region, size, privateNetwork, publicIP, privateIP, provisioning, status}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3249,7 +3222,7 @@ func createLoadBalancersTable(data []map[string]interface{}, width, height int) } // createManagedDatabasesTable creates a table for managed database services. -func createManagedDatabasesTable(data []map[string]interface{}, width, height int) table.Model { +func createManagedDatabasesTable(data []map[string]any, width, height int) table.Model { sort.Slice(data, func(i, j int) bool { return getString(data[i], "description") < getString(data[j], "description") }) @@ -3281,8 +3254,8 @@ func createManagedDatabasesTable(data []map[string]interface{}, width, height in flavor := getString(db, "flavor") storageStr := "-" - if storage, ok := db["storage"].(map[string]interface{}); ok { - if size, ok := storage["size"].(map[string]interface{}); ok { + if storage, ok := db["storage"].(map[string]any); ok { + if size, ok := storage["size"].(map[string]any); ok { val := getString(size, "value") unit := getString(size, "unit") if val != "" { @@ -3293,10 +3266,10 @@ func createManagedDatabasesTable(data []map[string]interface{}, width, height in location := "-" nodesCount := 0 - if nodes, ok := db["nodes"].([]interface{}); ok { + if nodes, ok := db["nodes"].([]any); ok { nodesCount = len(nodes) if len(nodes) > 0 { - if node, ok := nodes[0].(map[string]interface{}); ok { + if node, ok := nodes[0].(map[string]any); ok { if r, ok := node["region"].(string); ok && r != "" { location = r } @@ -3324,10 +3297,7 @@ func createManagedDatabasesTable(data []map[string]interface{}, width, height in }) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 25 { tableHeight = 25 } @@ -3353,7 +3323,7 @@ func createManagedDatabasesTable(data []map[string]interface{}, width, height in } // createGatewaysTable creates a table for gateways. -func createGatewaysTable(data []map[string]interface{}, width, height int) table.Model { +func createGatewaysTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Name", Width: 22}, {Title: "Region", Width: 16}, @@ -3372,9 +3342,9 @@ func createGatewaysTable(data []map[string]interface{}, width, height int) table status := getString(gw, "status") publicIP := "-" - if ei, ok := gw["externalInformation"].(map[string]interface{}); ok { - if ips, ok := ei["ips"].([]interface{}); ok && len(ips) > 0 { - if ipm, ok := ips[0].(map[string]interface{}); ok { + if ei, ok := gw["externalInformation"].(map[string]any); ok { + if ips, ok := ei["ips"].([]any); ok && len(ips) > 0 { + if ipm, ok := ips[0].(map[string]any); ok { if v := getString(ipm, "ip"); v != "" { publicIP = v } @@ -3384,9 +3354,9 @@ func createGatewaysTable(data []map[string]interface{}, width, height int) table privateNetwork := "-" var privateIPs []string - if ifaces, ok := gw["interfaces"].([]interface{}); ok && len(ifaces) > 0 { + if ifaces, ok := gw["interfaces"].([]any); ok && len(ifaces) > 0 { for _, iface := range ifaces { - if ifm, ok := iface.(map[string]interface{}); ok { + if ifm, ok := iface.(map[string]any); ok { if v := getString(ifm, "ip"); v != "" { privateIPs = append(privateIPs, v) } @@ -3406,10 +3376,7 @@ func createGatewaysTable(data []map[string]interface{}, width, height int) table rows = append(rows, table.Row{name, region, size, privateNetwork, publicIP, privateIP, status}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3437,7 +3404,7 @@ func createGatewaysTable(data []map[string]interface{}, width, height int) table } // createFloatingIPsTable creates a table for floating/public IPs. -func createFloatingIPsTable(data []map[string]interface{}, width, height int) table.Model { +func createFloatingIPsTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "IP Address", Width: 18}, {Title: "Region", Width: 18}, @@ -3456,7 +3423,7 @@ func createFloatingIPsTable(data []map[string]interface{}, width, height int) ta } endpoint := "-" - if ae, ok := fip["associatedEntity"].(map[string]interface{}); ok { + if ae, ok := fip["associatedEntity"].(map[string]any); ok { atype := getString(ae, "type") aip := getString(ae, "ip") switch { @@ -3472,10 +3439,7 @@ func createFloatingIPsTable(data []map[string]interface{}, width, height int) ta rows = append(rows, table.Row{ip, region, endpoint}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3503,7 +3467,7 @@ func createFloatingIPsTable(data []map[string]interface{}, width, height int) ta } // createAdditionalIPsTable creates a table for failover/additional IPs. -func createAdditionalIPsTable(data []map[string]interface{}, width, height int) table.Model { +func createAdditionalIPsTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "IP Address", Width: 20}, {Title: "Block", Width: 20}, @@ -3538,10 +3502,7 @@ func createAdditionalIPsTable(data []map[string]interface{}, width, height int) } rows = append(rows, table.Row{addr, block, routedTo, geoloc, status}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3559,7 +3520,7 @@ func createAdditionalIPsTable(data []map[string]interface{}, width, height int) } // createBlockStorageTable creates a nicely formatted table for block storage volumes. -func createBlockStorageTable(data []map[string]interface{}, width, height int) table.Model { +func createBlockStorageTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Name", Width: 24}, {Title: "ID", Width: 36}, @@ -3589,7 +3550,7 @@ func createBlockStorageTable(data []map[string]interface{}, width, height int) t } } instance := "-" - if raw, ok := vol["attachedTo"].([]interface{}); ok && len(raw) > 0 { + if raw, ok := vol["attachedTo"].([]any); ok && len(raw) > 0 { if id, ok := raw[0].(string); ok { if len(id) > 18 { instance = id[:18] + "…" @@ -3606,10 +3567,7 @@ func createBlockStorageTable(data []map[string]interface{}, width, height int) t rows = append(rows, table.Row{name, id, region, vType, size, instance, encryption, status}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -3637,7 +3595,7 @@ func createBlockStorageTable(data []map[string]interface{}, width, height int) t } // getString safely extracts a string value from a map -func getString(m map[string]interface{}, key string) string { +func getString(m map[string]any, key string) string { if val, ok := m[key]; ok { return fmt.Sprintf("%v", val) } @@ -3701,7 +3659,7 @@ func (m Model) fetchRegions() tea.Cmd { } // Fetch all images to determine available regions - var allImages []map[string]interface{} + var allImages []map[string]any imageEndpoint := fmt.Sprintf("/v1/cloud/project/%s/image", m.cloudProject) err := httpLib.Client.Get(imageEndpoint, &allImages) if err != nil { @@ -3710,12 +3668,12 @@ func (m Model) fetchRegions() tea.Cmd { // Extract unique regions from images regionMap := make(map[string]bool) - var instanceRegions []map[string]interface{} + var instanceRegions []map[string]any for _, image := range allImages { regionStr := getString(image, "region") if regionStr != "" && !regionMap[regionStr] { regionMap[regionStr] = true - region := map[string]interface{}{ + region := map[string]any{ "name": regionStr, "id": regionStr, } @@ -3738,13 +3696,13 @@ func (m Model) fetchFlavors(region string) tea.Cmd { return flavorsLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var flavors []map[string]interface{} + var flavors []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/flavor?region=%s", m.cloudProject, region) err := httpLib.Client.Get(endpoint, &flavors) // Filter out unavailable flavors if err == nil { - var availableFlavors []map[string]interface{} + var availableFlavors []map[string]any for _, flavor := range flavors { available, _ := flavor["available"].(bool) if available { @@ -3768,7 +3726,7 @@ func (m Model) fetchImages(region string) tea.Cmd { return imagesLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var images []map[string]interface{} + var images []map[string]any // Check if we have cached images from region fetch if len(m.wizard.images) > 0 { @@ -3793,7 +3751,7 @@ func (m Model) fetchImages(region string) tea.Cmd { } // Filter for common/usable images (exclude snapshots, etc.) - var publicImages []map[string]interface{} + var publicImages []map[string]any for _, image := range images { visibility, _ := image["visibility"].(string) status, _ := image["status"].(string) @@ -3838,7 +3796,7 @@ func (m Model) createInstanceWithNetworking() tea.Cmd { } // Build request body for v1 API - requestBody := map[string]interface{}{ + requestBody := map[string]any{ "flavorId": m.wizard.selectedFlavor, "imageId": m.wizard.selectedImage, "name": m.wizard.instanceName, @@ -3858,7 +3816,7 @@ func (m Model) createInstanceWithNetworking() tea.Cmd { if m.wizard.selectedPrivateNetwork != "" { // Private network selected - add it to networks array - networks := []map[string]interface{}{ + networks := []map[string]any{ {"networkId": m.wizard.selectedPrivateNetwork}, } requestBody["networks"] = networks @@ -3866,7 +3824,7 @@ func (m Model) createInstanceWithNetworking() tea.Cmd { // If only public network (no private network selected), don't add "networks" key // The API will use public network by default - var instance map[string]interface{} + var instance map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/instance", m.cloudProject) err := httpLib.Client.Post(endpoint, requestBody, &instance) @@ -3937,7 +3895,7 @@ func (m Model) fetchSSHKeys() tea.Cmd { return sshKeysLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var sshKeys []map[string]interface{} + var sshKeys []map[string]any // Query SSH keys filtered by the selected region endpoint := fmt.Sprintf("/v1/cloud/project/%s/sshkey?region=%s", m.cloudProject, m.wizard.selectedRegion) err := httpLib.Client.Get(endpoint, &sshKeys) @@ -3972,12 +3930,12 @@ func (m Model) createSSHKey() tea.Cmd { return sshKeyCreatedMsg{err: fmt.Errorf("no cloud project selected")} } - requestBody := map[string]interface{}{ + requestBody := map[string]any{ "name": m.wizard.newSSHKeyName, "publicKey": m.wizard.newSSHKeyPublicKey, } - var sshKey map[string]interface{} + var sshKey map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/sshkey", m.cloudProject) err := httpLib.Client.Post(endpoint, requestBody, &sshKey) @@ -4037,7 +3995,7 @@ func (m Model) fetchPrivateNetworks() tea.Cmd { return privateNetworksLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var networks []map[string]interface{} + var networks []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network", m.cloudProject, m.wizard.selectedRegion) err := httpLib.Client.Get(endpoint, &networks) @@ -4049,7 +4007,7 @@ func (m Model) fetchPrivateNetworks() tea.Cmd { } // Filter to private networks only - var privateNetworks []map[string]interface{} + var privateNetworks []map[string]any for _, network := range networks { if v, ok := network["visibility"]; ok && v == "private" { privateNetworks = append(privateNetworks, network) @@ -4077,15 +4035,15 @@ func (m Model) handlePrivateNetworksLoaded(msg privateNetworksLoadedMsg) (tea.Mo availableNetworks := msg.networks // Build the list: No Network, Create New, then existing networks - noNetworkOption := map[string]interface{}{ + noNetworkOption := map[string]any{ "id": "", "name": "(No Private Network)", } - createNetworkOption := map[string]interface{}{ + createNetworkOption := map[string]any{ "id": "__create_new__", "name": "+ Create new private network", } - m.wizard.privateNetworks = []map[string]interface{}{noNetworkOption, createNetworkOption} + m.wizard.privateNetworks = []map[string]any{noNetworkOption, createNetworkOption} m.wizard.privateNetworks = append(m.wizard.privateNetworks, availableNetworks...) m.wizard.selectedIndex = 0 m.wizard.usePublicNetwork = true // Default to public network enabled @@ -4179,8 +4137,8 @@ func (m Model) waitForInstanceIP(instanceId, instanceName string) tea.Cmd { return func() tea.Msg { // Poll for up to 60 seconds maxAttempts := 12 - for attempt := 0; attempt < maxAttempts; attempt++ { - var instance map[string]interface{} + for range maxAttempts { + var instance map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/instance/%s", m.cloudProject, instanceId) err := httpLib.Client.Get(endpoint, &instance) if err != nil { @@ -4192,9 +4150,9 @@ func (m Model) waitForInstanceIP(instanceId, instanceName string) tea.Cmd { } // Look for private IP in ipAddresses array - if ipAddresses, ok := instance["ipAddresses"].([]interface{}); ok { + if ipAddresses, ok := instance["ipAddresses"].([]any); ok { for _, ipAddr := range ipAddresses { - if ipMap, ok := ipAddr.(map[string]interface{}); ok { + if ipMap, ok := ipAddr.(map[string]any); ok { ipType := getString(ipMap, "type") ip := getString(ipMap, "ip") if ipType == "private" && ip != "" { @@ -4255,17 +4213,17 @@ func (m Model) handleInstanceIPReady(msg instanceIPReadyMsg) (tea.Model, tea.Cmd func (m Model) attachFloatingIP(instanceId, instanceName, privateIP string) tea.Cmd { return func() tea.Msg { // Build request body - requestBody := map[string]interface{}{ + requestBody := map[string]any{ "ip": privateIP, } // Add gateway creation if needed (small gateway) - requestBody["gateway"] = map[string]interface{}{ + requestBody["gateway"] = map[string]any{ "model": "s", "name": fmt.Sprintf("gw-%s", instanceName), } - var result map[string]interface{} + var result map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/instance/%s/floatingIp", m.cloudProject, m.wizard.selectedRegion, instanceId) err := httpLib.Client.Post(endpoint, requestBody, &result) @@ -4425,7 +4383,7 @@ func (m Model) fetchFloatingIPs() tea.Cmd { return floatingIPsLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var floatingIPs []map[string]interface{} + var floatingIPs []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/floatingip", m.cloudProject, m.wizard.selectedRegion) err := httpLib.Client.Get(endpoint, &floatingIPs) @@ -4443,7 +4401,7 @@ func (m Model) handleFloatingIPsLoaded(msg floatingIPsLoadedMsg) (tea.Model, tea if msg.err != nil { // If region doesn't support floating IPs, just show create option - m.wizard.floatingIPs = []map[string]interface{}{ + m.wizard.floatingIPs = []map[string]any{ {"id": "__none__", "name": "(No Floating IP - no external access)"}, {"id": "__create_new__", "name": "+ Create new Floating IP"}, } @@ -4452,7 +4410,7 @@ func (m Model) handleFloatingIPsLoaded(msg floatingIPsLoadedMsg) (tea.Model, tea } // Filter floating IPs that are not associated (available) - var availableFloatingIPs []map[string]interface{} + var availableFloatingIPs []map[string]any for _, fip := range msg.floatingIPs { // Check if floating IP is not associated to an instance associatedEntity := getString(fip, "associatedEntity") @@ -4462,15 +4420,15 @@ func (m Model) handleFloatingIPsLoaded(msg floatingIPsLoadedMsg) (tea.Model, tea } // Build the list: No floating IP, Create New, then available IPs - noFIPOption := map[string]interface{}{ + noFIPOption := map[string]any{ "id": "__none__", "name": "(No Floating IP - no external access)", } - createFIPOption := map[string]interface{}{ + createFIPOption := map[string]any{ "id": "__create_new__", "name": "+ Create new Floating IP", } - m.wizard.floatingIPs = []map[string]interface{}{noFIPOption, createFIPOption} + m.wizard.floatingIPs = []map[string]any{noFIPOption, createFIPOption} m.wizard.floatingIPs = append(m.wizard.floatingIPs, availableFloatingIPs...) m.wizard.selectedIndex = 0 return m, nil @@ -4484,15 +4442,15 @@ func (m Model) createGatewayIfNeeded() tea.Cmd { } // First, check if there's already a gateway in the region - var gateways []map[string]interface{} + var gateways []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/gateway", m.cloudProject, m.wizard.selectedRegion) err := httpLib.Client.Get(endpoint, &gateways) if err == nil && len(gateways) > 0 { // Gateway exists, check if it's attached to our network for _, gw := range gateways { - if interfaces, ok := gw["interfaces"].([]interface{}); ok { + if interfaces, ok := gw["interfaces"].([]any); ok { for _, iface := range interfaces { - if ifaceMap, ok := iface.(map[string]interface{}); ok { + if ifaceMap, ok := iface.(map[string]any); ok { networkId := getString(ifaceMap, "networkId") if networkId == m.wizard.selectedPrivateNetwork { // Gateway already exists for this network @@ -4505,16 +4463,16 @@ func (m Model) createGatewayIfNeeded() tea.Cmd { } // Need to create a gateway - create S size by default - gatewayBody := map[string]interface{}{ + gatewayBody := map[string]any{ "name": fmt.Sprintf("gw-%s", m.wizard.instanceName), "model": "s", - "network": map[string]interface{}{ + "network": map[string]any{ "id": m.wizard.selectedPrivateNetwork, }, } bodyBytes, _ := json.Marshal(gatewayBody) - var gateway map[string]interface{} + var gateway map[string]any err = httpLib.Client.Post(endpoint, string(bodyBytes), &gateway) return gatewayCreatedMsg{ @@ -4544,12 +4502,12 @@ func (m Model) createFloatingIP(instanceId string) tea.Cmd { } // Create the floating IP - fipBody := map[string]interface{}{ + fipBody := map[string]any{ "description": fmt.Sprintf("FIP for %s", m.wizard.instanceName), } bodyBytes, _ := json.Marshal(fipBody) - var floatingIP map[string]interface{} + var floatingIP map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/floatingip", m.cloudProject, m.wizard.selectedRegion) err := httpLib.Client.Post(endpoint, string(bodyBytes), &floatingIP) @@ -4560,7 +4518,7 @@ func (m Model) createFloatingIP(instanceId string) tea.Cmd { // Now associate it with the instance fipId := getString(floatingIP, "id") if fipId != "" && instanceId != "" { - associateBody := map[string]interface{}{ + associateBody := map[string]any{ "instanceId": instanceId, } bodyBytes, _ = json.Marshal(associateBody) @@ -4620,21 +4578,21 @@ func (m Model) ensureGatewayAndCreateFloatingIP(instanceId string) tea.Cmd { // Step 1: Check if gateway exists for this subnet gwCheckEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/gateway?subnetId=%s", m.cloudProject, m.wizard.selectedRegion, m.wizard.selectedSubnetId) - var gateways []map[string]interface{} + var gateways []map[string]any err := httpLib.Client.Get(gwCheckEndpoint, &gateways) hasGateway := err == nil && len(gateways) > 0 // Step 2: Wait for instance to have a private IP var privateIP string - for retry := 0; retry < 20; retry++ { - var instance map[string]interface{} + for range 20 { + var instance map[string]any instanceEndpoint := fmt.Sprintf("/v1/cloud/project/%s/instance/%s", m.cloudProject, instanceId) if err := httpLib.Client.Get(instanceEndpoint, &instance); err == nil { // Look for private IP in ipAddresses - if ipAddresses, ok := instance["ipAddresses"].([]interface{}); ok { + if ipAddresses, ok := instance["ipAddresses"].([]any); ok { for _, ipAddr := range ipAddresses { - if ipMap, ok := ipAddr.(map[string]interface{}); ok { + if ipMap, ok := ipAddr.(map[string]any); ok { ipType := getString(ipMap, "type") if ipType == "private" { privateIP = getString(ipMap, "ip") @@ -4655,20 +4613,20 @@ func (m Model) ensureGatewayAndCreateFloatingIP(instanceId string) tea.Cmd { } // Step 3: Create floating IP with gateway info if needed - fipBody := map[string]interface{}{ + fipBody := map[string]any{ "ip": privateIP, } // If no gateway exists, include gateway creation parameters if !hasGateway { - fipBody["gateway"] = map[string]interface{}{ + fipBody["gateway"] = map[string]any{ "model": "s", "name": fmt.Sprintf("gw-%s", m.wizard.instanceName), } } bodyBytes, _ := json.Marshal(fipBody) - var floatingIPResult map[string]interface{} + var floatingIPResult map[string]any fipEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/instance/%s/floatingIp", m.cloudProject, m.wizard.selectedRegion, instanceId) err = httpLib.Client.Post(fipEndpoint, string(bodyBytes), &floatingIPResult) @@ -4690,7 +4648,7 @@ func waitForOperation(projectID, operationID string, timeout time.Duration) erro deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { - var operation map[string]interface{} + var operation map[string]any if err := httpLib.Client.Get(endpoint, &operation); err != nil { return fmt.Errorf("error fetching operation: %w", err) } @@ -4737,9 +4695,9 @@ func (m Model) executeInstanceAction(actionIndex int) tea.Cmd { case "ssh": // Get public IP from instance publicIP := "" - if addresses, ok := m.detailData["ipAddresses"].([]interface{}); ok { + if addresses, ok := m.detailData["ipAddresses"].([]any); ok { for _, addr := range addresses { - if addrMap, ok := addr.(map[string]interface{}); ok { + if addrMap, ok := addr.(map[string]any); ok { ipType := getString(addrMap, "type") if ipType == "public" { version := getNumericValue(addrMap, "version") @@ -4832,7 +4790,7 @@ func (m Model) executeInstanceAction(actionIndex int) tea.Cmd { case "vnc": // POST /cloud/project/{serviceName}/instance/{instanceId}/vnc endpoint := fmt.Sprintf("/v1/cloud/project/%s/instance/%s/vnc", m.cloudProject, instanceId) - var result map[string]interface{} + var result map[string]any err = httpLib.Client.Post(endpoint, nil, &result) if err == nil { // Get the VNC URL from response @@ -4963,12 +4921,12 @@ func (m Model) createPrivateNetwork() tea.Cmd { } // Step 1: Create the private network using the regional API - networkBody := map[string]interface{}{ + networkBody := map[string]any{ "name": m.wizard.newNetworkName, "vlanId": m.wizard.newNetworkVlanId, } - var operation map[string]interface{} + var operation map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network", m.cloudProject, m.wizard.selectedRegion) err := httpLib.Client.Post(endpoint, networkBody, &operation) if err != nil { @@ -4985,7 +4943,7 @@ func (m Model) createPrivateNetwork() tea.Cmd { return networkCreatedMsg{err: fmt.Errorf("network created but ID not returned")} } - network := map[string]interface{}{ + network := map[string]any{ "id": resourceId, "name": m.wizard.newNetworkName, } @@ -5024,7 +4982,7 @@ func (m Model) handleNetworkStep(msg networkStepMsg) (tea.Model, tea.Cmd) { } // createSubnet creates a subnet for the network (Step 2) -func (m Model) createSubnet(networkId string, network map[string]interface{}) tea.Cmd { +func (m Model) createSubnet(networkId string, network map[string]any) tea.Cmd { return func() tea.Msg { cidr := m.wizard.newNetworkCIDR if cidr == "" { @@ -5047,7 +5005,7 @@ func (m Model) createSubnet(networkId string, network map[string]interface{}) te dhcpStart := baseIP + ".2" dhcpEnd := baseIP + ".254" - subnetBody := map[string]interface{}{ + subnetBody := map[string]any{ "name": m.wizard.newNetworkName + "-subnet", "cidr": cidr, "ipVersion": 4, @@ -5067,10 +5025,10 @@ func (m Model) createSubnet(networkId string, network map[string]interface{}) te m.cloudProject, m.wizard.selectedRegion, networkId) // Retry creating subnet with exponential backoff (network needs to activate) - var subnet map[string]interface{} + var subnet map[string]any var subnetErr error maxRetries := 10 - for retry := 0; retry < maxRetries; retry++ { + for retry := range maxRetries { subnetErr = httpLib.Client.Post(subnetEndpoint, subnetBody, &subnet) if subnetErr == nil { break @@ -5100,7 +5058,7 @@ func (m Model) createSubnet(networkId string, network map[string]interface{}) te network["subnet"] = cidr // Store subnet ID in a subnets array (to match the structure from fetchPrivateNetworks) if subnet != nil { - network["subnets"] = []map[string]interface{}{subnet} + network["subnets"] = []map[string]any{subnet} } return networkStepMsg{ @@ -5134,16 +5092,16 @@ func (m Model) handleNetworkCreated(msg networkCreatedMsg) (tea.Model, tea.Cmd) // Extract subnet ID if available m.wizard.selectedSubnetId = "" - if subnets, ok := msg.network["subnets"].([]map[string]interface{}); ok && len(subnets) > 0 { + if subnets, ok := msg.network["subnets"].([]map[string]any); ok && len(subnets) > 0 { m.wizard.selectedSubnetId = getString(subnets[0], "id") - } else if subnets, ok := msg.network["subnets"].([]interface{}); ok && len(subnets) > 0 { - if subnet, ok := subnets[0].(map[string]interface{}); ok { + } else if subnets, ok := msg.network["subnets"].([]any); ok && len(subnets) > 0 { + if subnet, ok := subnets[0].(map[string]any); ok { m.wizard.selectedSubnetId = getString(subnet, "id") } } // Add the new network to the list (after "No Network" and "Create New") - newNetworkEntry := map[string]interface{}{ + newNetworkEntry := map[string]any{ "id": networkId, "name": networkName + " (new)", "subnets": msg.network["subnets"], @@ -5153,7 +5111,7 @@ func (m Model) handleNetworkCreated(msg networkCreatedMsg) (tea.Model, tea.Cmd) if len(m.wizard.privateNetworks) >= 2 { m.wizard.privateNetworks = append( m.wizard.privateNetworks[:2], - append([]map[string]interface{}{newNetworkEntry}, m.wizard.privateNetworks[2:]...)..., + append([]map[string]any{newNetworkEntry}, m.wizard.privateNetworks[2:]...)..., ) m.wizard.selectedIndex = 2 // Select the newly created network } else { @@ -5316,7 +5274,6 @@ func (m Model) executeKubeAction(actionIndex int) tea.Cmd { } } - // kubeconfigReadyForK9sMsg signals that the kubeconfig has been downloaded and k9s can be launched. type kubeconfigReadyForK9sMsg struct { kubeconfigPath string @@ -5386,7 +5343,7 @@ func (m Model) handleStartNodePoolWizard(msg startNodePoolWizardMsg) (tea.Model, func (m Model) loadNodePoolFlavors(clusterId, region string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s/flavors", m.cloudProject, clusterId) - var flavors []map[string]interface{} + var flavors []map[string]any if err := httpLib.Client.Get(endpoint, &flavors); err != nil { return nodePoolFlavorsLoadedMsg{err: err} } @@ -5396,7 +5353,7 @@ func (m Model) loadNodePoolFlavors(clusterId, region string) tea.Cmd { // nodePoolFlavorsLoadedMsg is sent after flavors are loaded type nodePoolFlavorsLoadedMsg struct { - flavors []map[string]interface{} + flavors []map[string]any err error } @@ -5409,7 +5366,7 @@ func (m Model) handleNodePoolFlavorsLoaded(msg nodePoolFlavorsLoadedMsg) (tea.Mo } // Filter flavors to only show available ones - var availableFlavors []map[string]interface{} + var availableFlavors []map[string]any for _, flavor := range msg.flavors { // Check if flavor is available (default to true if field doesn't exist) available := true @@ -5439,7 +5396,7 @@ func (m Model) handleNodePoolFlavorsLoaded(msg nodePoolFlavorsLoadedMsg) (tea.Mo // createNodePool creates a new node pool func (m Model) createNodePool() tea.Cmd { return func() tea.Msg { - payload := map[string]interface{}{ + payload := map[string]any{ "name": m.wizard.nodePoolName, "flavorName": m.wizard.nodePoolFlavorName, "desiredNodes": m.wizard.nodePoolDesiredNodes, @@ -5451,7 +5408,7 @@ func (m Model) createNodePool() tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s/nodepool", m.cloudProject, m.wizard.nodePoolClusterId) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, payload, &result); err != nil { return nodePoolCreatedMsg{err: err} } @@ -5504,7 +5461,7 @@ func (m Model) fetchKubeUpgradeVersions(clusterId string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s", m.cloudProject, clusterId) - var cluster map[string]interface{} + var cluster map[string]any err := httpLib.Client.Get(endpoint, &cluster) if err != nil { return kubeUpgradeVersionsLoadedMsg{err: fmt.Errorf("failed to fetch cluster: %w", err)} @@ -5512,7 +5469,7 @@ func (m Model) fetchKubeUpgradeVersions(clusterId string) tea.Cmd { // Get available versions from the cluster info var versions []string - if nextVersions, ok := cluster["nextUpgradeVersions"].([]interface{}); ok { + if nextVersions, ok := cluster["nextUpgradeVersions"].([]any); ok { for _, v := range nextVersions { if vs, ok := v.(string); ok { versions = append(versions, vs) @@ -5532,7 +5489,7 @@ func (m Model) upgradeKubeCluster(clusterId, targetVersion string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s/update", m.cloudProject, clusterId) - body := map[string]interface{}{ + body := map[string]any{ "strategy": "LATEST_PATCH", "version": targetVersion, } @@ -5620,7 +5577,7 @@ func (m Model) updateKubePolicy(clusterId, policy string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s", m.cloudProject, clusterId) - body := map[string]interface{}{ + body := map[string]any{ "updatePolicy": policy, } @@ -5840,7 +5797,7 @@ func (m Model) scaleNodePool(clusterId, nodePoolId string, desiredNodes, minNode } endpoint := fmt.Sprintf("/v1/cloud/project/%s/kube/%s/nodepool/%s", m.cloudProject, clusterId, nodePoolId) - body := map[string]interface{}{ + body := map[string]any{ "desiredNodes": desiredNodes, "minNodes": minNodes, "maxNodes": maxNodes, @@ -5917,13 +5874,13 @@ func (m Model) fetchInstanceBackupsData() dataLoadedMsg { return dataLoadedMsg{err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/snapshot", m.cloudProject) - var raw []interface{} + var raw []any if err := httpLib.Client.Get(endpoint, &raw); err != nil { return dataLoadedMsg{err: err} } - var snapshots []map[string]interface{} + var snapshots []map[string]any for _, item := range raw { - if obj, ok := item.(map[string]interface{}); ok { + if obj, ok := item.(map[string]any); ok { snapshots = append(snapshots, obj) } } @@ -5941,10 +5898,10 @@ func (m Model) fetchWorkflowsData() dataLoadedMsg { if err := httpLib.Client.Get(regionsEndpoint, ®ionNames); err != nil { return dataLoadedMsg{err: fmt.Errorf("failed to fetch regions: %w", err)} } - var workflows []map[string]interface{} + var workflows []map[string]any for _, region := range regionNames { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/workflow/backup", m.cloudProject, url.PathEscape(region)) - var raw []map[string]interface{} + var raw []map[string]any if err := httpLib.Client.Get(endpoint, &raw); err != nil { continue } @@ -5956,7 +5913,7 @@ func (m Model) fetchWorkflowsData() dataLoadedMsg { if wfID, ok := obj["id"].(string); ok && wfID != "" { detailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/workflow/backup/%s", m.cloudProject, url.PathEscape(region), url.PathEscape(wfID)) - var detail map[string]interface{} + var detail map[string]any if err := httpLib.Client.Get(detailEndpoint, &detail); err == nil { if v, ok := detail["lastExecution"]; ok { obj["lastExecution"] = v @@ -5978,13 +5935,13 @@ func (m Model) fetchVolumeSnapshotsData() dataLoadedMsg { return dataLoadedMsg{err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/snapshot", m.cloudProject) - var raw []interface{} + var raw []any if err := httpLib.Client.Get(endpoint, &raw); err != nil { return dataLoadedMsg{err: err} } - var snapshots []map[string]interface{} + var snapshots []map[string]any for _, item := range raw { - if obj, ok := item.(map[string]interface{}); ok { + if obj, ok := item.(map[string]any); ok { snapshots = append(snapshots, obj) } } @@ -6001,16 +5958,16 @@ func (m Model) fetchVolumeBackupsData() dataLoadedMsg { if err := httpLib.Client.Get(regEndpoint, ®ionNames); err != nil { return dataLoadedMsg{err: err} } - var backups []map[string]interface{} + var backups []map[string]any for _, region := range regionNames { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volumeBackup", m.cloudProject, url.PathEscape(region)) - var raw []interface{} + var raw []any if err := httpLib.Client.Get(endpoint, &raw); err != nil { continue // skip regions without backup support } for _, item := range raw { - if obj, ok := item.(map[string]interface{}); ok { + if obj, ok := item.(map[string]any); ok { backups = append(backups, obj) } } @@ -6018,7 +5975,7 @@ func (m Model) fetchVolumeBackupsData() dataLoadedMsg { return dataLoadedMsg{data: backups} } -func createVolumeSnapshotsTable(data []map[string]interface{}, width, height int) table.Model { +func createVolumeSnapshotsTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Nom", Width: 24}, {Title: "ID", Width: 36}, @@ -6050,10 +6007,7 @@ func createVolumeSnapshotsTable(data []map[string]interface{}, width, height int } rows = append(rows, table.Row{name, id, region, volumeId, size, status, created}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -6070,7 +6024,7 @@ func createVolumeSnapshotsTable(data []map[string]interface{}, width, height int return t } -func createVolumeBackupsTable(data []map[string]interface{}, width, height int) table.Model { +func createVolumeBackupsTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Nom", Width: 24}, {Title: "ID", Width: 36}, @@ -6102,10 +6056,7 @@ func createVolumeBackupsTable(data []map[string]interface{}, width, height int) } rows = append(rows, table.Row{name, id, region, volumeId, size, status, created}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -6122,7 +6073,6 @@ func createVolumeBackupsTable(data []map[string]interface{}, width, height int) return t } - // fetchLBMembers fetches the list of members for a given pool. func (m Model) fetchLBMembers(poolID, region string) tea.Cmd { return func() tea.Msg { @@ -6130,8 +6080,8 @@ func (m Model) fetchLBMembers(poolID, region string) tea.Cmd { return lbPoolMembersLoadedMsg{poolID: poolID, err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool/%s/member", -m.cloudProject, url.PathEscape(region), url.PathEscape(poolID)) - var members []map[string]interface{} + m.cloudProject, url.PathEscape(region), url.PathEscape(poolID)) + var members []map[string]any if err := httpLib.Client.Get(endpoint, &members); err != nil { return lbPoolMembersLoadedMsg{poolID: poolID, err: err} } @@ -6149,7 +6099,7 @@ func (m Model) executeDeleteLBMember(poolID, memberID, region string) tea.Cmd { return lbPoolMemberDeletedMsg{poolID: poolID, err: fmt.Errorf("pool ID, member ID or region missing")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool/%s/member/%s", -m.cloudProject, url.PathEscape(region), url.PathEscape(poolID), url.PathEscape(memberID)) + m.cloudProject, url.PathEscape(region), url.PathEscape(poolID), url.PathEscape(memberID)) if err := httpLib.Client.Delete(endpoint, nil); err != nil { return lbPoolMemberDeletedMsg{poolID: poolID, err: fmt.Errorf("deletion failed: %w", err)} } @@ -6170,20 +6120,20 @@ func (m Model) saveLBMember() tea.Cmd { } if m.wizard.lbMemberEditId != "" { // Edit: PUT — only name and weight are mutable; address and protocolPort are immutable - putBody := map[string]interface{}{ + putBody := map[string]any{ "name": m.wizard.lbMemberName, "weight": m.wizard.lbMemberWeight, } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool/%s/member/%s", m.cloudProject, url.PathEscape(region), url.PathEscape(poolID), url.PathEscape(m.wizard.lbMemberEditId)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Put(endpoint, putBody, &result); err != nil { return lbPoolMemberSavedMsg{poolID: poolID, err: fmt.Errorf("update failed: %w", err)} } } else { // Create: POST — body is {"members": [...]} - postBody := map[string]interface{}{ - "members": []map[string]interface{}{ + postBody := map[string]any{ + "members": []map[string]any{ { "name": m.wizard.lbMemberName, "address": m.wizard.lbMemberIP, @@ -6194,7 +6144,7 @@ func (m Model) saveLBMember() tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool/%s/member", m.cloudProject, url.PathEscape(region), url.PathEscape(poolID)) - var result []map[string]interface{} + var result []map[string]any if err := httpLib.Client.Post(endpoint, postBody, &result); err != nil { return lbPoolMemberSavedMsg{poolID: poolID, err: fmt.Errorf("creation failed: %w", err)} } @@ -6210,8 +6160,8 @@ func (m Model) fetchLBHealthMonitor(poolID, region string) tea.Cmd { return lbHMLoadedMsg{poolID: poolID, err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/healthMonitor?poolId=%s", -m.cloudProject, url.PathEscape(region), url.QueryEscape(poolID)) - var monitors []map[string]interface{} + m.cloudProject, url.PathEscape(region), url.QueryEscape(poolID)) + var monitors []map[string]any if err := httpLib.Client.Get(endpoint, &monitors); err != nil { return lbHMLoadedMsg{poolID: poolID, err: err} } @@ -6235,7 +6185,7 @@ func (m Model) saveHealthMonitor() tea.Cmd { } if m.wizard.lbHMEditId != "" { // Update: PUT — monitorType and poolId are immutable - putBody := map[string]interface{}{ + putBody := map[string]any{ "name": m.wizard.lbHMName, "delay": m.wizard.lbHMDelay, "maxRetries": m.wizard.lbHMMaxRetries, @@ -6243,21 +6193,21 @@ func (m Model) saveHealthMonitor() tea.Cmd { "timeout": m.wizard.lbHMTimeout, } if lbHMTypeNeedsHttpConfig(m.wizard.lbHMType) { - putBody["httpConfiguration"] = map[string]interface{}{ + putBody["httpConfiguration"] = map[string]any{ "httpMethod": m.wizard.lbHMHttpMethod, "urlPath": m.wizard.lbHMUrlPath, "expectedCodes": m.wizard.lbHMExpectedCodes, } } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/healthMonitor/%s", -m.cloudProject, url.PathEscape(region), url.PathEscape(m.wizard.lbHMEditId)) - var result map[string]interface{} + m.cloudProject, url.PathEscape(region), url.PathEscape(m.wizard.lbHMEditId)) + var result map[string]any if err := httpLib.Client.Put(endpoint, putBody, &result); err != nil { return lbHMSavedMsg{poolID: poolID, err: fmt.Errorf("update failed: %w", err)} } } else { // Create: POST - postBody := map[string]interface{}{ + postBody := map[string]any{ "name": m.wizard.lbHMName, "monitorType": m.wizard.lbHMType, "poolId": poolID, @@ -6267,15 +6217,15 @@ m.cloudProject, url.PathEscape(region), url.PathEscape(m.wizard.lbHMEditId)) "timeout": m.wizard.lbHMTimeout, } if lbHMTypeNeedsHttpConfig(m.wizard.lbHMType) { - postBody["httpConfiguration"] = map[string]interface{}{ + postBody["httpConfiguration"] = map[string]any{ "httpMethod": m.wizard.lbHMHttpMethod, "urlPath": m.wizard.lbHMUrlPath, "expectedCodes": m.wizard.lbHMExpectedCodes, } } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/healthMonitor", -m.cloudProject, url.PathEscape(region)) - var result map[string]interface{} + m.cloudProject, url.PathEscape(region)) + var result map[string]any if err := httpLib.Client.Post(endpoint, postBody, &result); err != nil { return lbHMSavedMsg{poolID: poolID, err: fmt.Errorf("creation failed: %w", err)} } @@ -6294,7 +6244,7 @@ func (m Model) deleteHealthMonitor(hmID, poolID, region string) tea.Cmd { return lbHMDeletedMsg{poolID: poolID, err: fmt.Errorf("health monitor ID or region missing")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/healthMonitor/%s", -m.cloudProject, url.PathEscape(region), url.PathEscape(hmID)) + m.cloudProject, url.PathEscape(region), url.PathEscape(hmID)) if err := httpLib.Client.Delete(endpoint, nil); err != nil { return lbHMDeletedMsg{poolID: poolID, err: fmt.Errorf("deletion failed: %w", err)} } diff --git a/internal/services/browser/backup_api.go b/internal/services/browser/backup_api.go index 0536a2d90..ec84e0cf6 100644 --- a/internal/services/browser/backup_api.go +++ b/internal/services/browser/backup_api.go @@ -19,7 +19,7 @@ import ( type volumeBackupCreatedMsg struct { name string - backupType string + backupType string err error } @@ -38,8 +38,8 @@ func (m Model) createVolumeSnapshot(volumeID, name string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s/snapshot", m.cloudProject, url.PathEscape(volumeID)) - body := map[string]interface{}{"name": name} - var result map[string]interface{} + body := map[string]any{"name": name} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return volumeBackupCreatedMsg{name: name, backupType: "snapshot", err: fmt.Errorf("failed to create snapshot: %w", err)} } @@ -54,11 +54,11 @@ func (m Model) createVolumeBackup(volumeID, region, name string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volumeBackup", m.cloudProject, url.PathEscape(region)) - body := map[string]interface{}{ + body := map[string]any{ "name": name, "volumeId": volumeID, } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return volumeBackupCreatedMsg{name: name, backupType: "backup", err: fmt.Errorf("failed to create backup: %w", err)} } @@ -75,7 +75,7 @@ func (m Model) fetchBackupVolumes() tea.Cmd { } type backupVolumesLoadedMsg struct { - volumes []map[string]interface{} + volumes []map[string]any err error } @@ -97,7 +97,7 @@ func (m Model) executeSnapshotAction(msg block_storage.ExecuteSnapshotActionMsg) return nil } -func (m Model) deleteSnapshot(snapshot map[string]interface{}) tea.Cmd { +func (m Model) deleteSnapshot(snapshot map[string]any) tea.Cmd { return func() tea.Msg { if m.cloudProject == "" { return snapshotActionDoneMsg{action: block_storage.SnapshotActionDelete, err: fmt.Errorf("no cloud project selected")} @@ -112,7 +112,7 @@ func (m Model) deleteSnapshot(snapshot map[string]interface{}) tea.Cmd { } } -func (m Model) createVolumeFromSnapshot(snapshot map[string]interface{}, volName, volSize string) tea.Cmd { +func (m Model) createVolumeFromSnapshot(snapshot map[string]any, volName, volSize string) tea.Cmd { return func() tea.Msg { if m.cloudProject == "" { return snapshotActionDoneMsg{action: block_storage.SnapshotActionCreateVolume, err: fmt.Errorf("no cloud project selected")} @@ -127,7 +127,7 @@ func (m Model) createVolumeFromSnapshot(snapshot map[string]interface{}, volName // Fetch source volume type so we use the correct type volType := "classic" if sourceVolumeID != "" && sourceVolumeID != "" { - var sourceVol map[string]interface{} + var sourceVol map[string]any srcEndpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s", m.cloudProject, url.PathEscape(sourceVolumeID)) if err := httpLib.Client.Get(srcEndpoint, &sourceVol); err == nil { if t, ok := sourceVol["type"].(string); ok && t != "" { @@ -135,14 +135,14 @@ func (m Model) createVolumeFromSnapshot(snapshot map[string]interface{}, volName } } } - body := map[string]interface{}{ + body := map[string]any{ "name": volName, "region": region, "size": sizeInt, "type": volType, "snapshotId": snapshotID, } - var result map[string]interface{} + var result map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/volume", m.cloudProject) if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return snapshotActionDoneMsg{action: block_storage.SnapshotActionCreateVolume, err: err} @@ -171,7 +171,7 @@ func (m Model) executeBackupAction(msg block_storage.ExecuteBackupActionMsg) tea return nil } -func (m Model) deleteBackup(backup map[string]interface{}) tea.Cmd { +func (m Model) deleteBackup(backup map[string]any) tea.Cmd { return func() tea.Msg { if m.cloudProject == "" { return backupActionDoneMsg{action: block_storage.BackupActionDelete, err: fmt.Errorf("no cloud project selected")} @@ -188,7 +188,7 @@ func (m Model) deleteBackup(backup map[string]interface{}) tea.Cmd { } } -func (m Model) restoreBackup(backup map[string]interface{}, volumeID string) tea.Cmd { +func (m Model) restoreBackup(backup map[string]any, volumeID string) tea.Cmd { return func() tea.Msg { if m.cloudProject == "" { return backupActionDoneMsg{action: block_storage.BackupActionRestore, err: fmt.Errorf("no cloud project selected")} @@ -197,8 +197,8 @@ func (m Model) restoreBackup(backup map[string]interface{}, volumeID string) tea region := fmt.Sprintf("%v", backup["region"]) endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volumeBackup/%s/restore", m.cloudProject, url.PathEscape(region), url.PathEscape(id)) - body := map[string]interface{}{"volumeId": volumeID} - var result map[string]interface{} + body := map[string]any{"volumeId": volumeID} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return backupActionDoneMsg{action: block_storage.BackupActionRestore, err: err} } @@ -207,7 +207,7 @@ func (m Model) restoreBackup(backup map[string]interface{}, volumeID string) tea } } -func (m Model) createVolumeFromBackup(backup map[string]interface{}, volName, volSize string) tea.Cmd { +func (m Model) createVolumeFromBackup(backup map[string]any, volName, volSize string) tea.Cmd { return func() tea.Msg { if m.cloudProject == "" { return backupActionDoneMsg{action: block_storage.BackupActionCreateVolume, err: fmt.Errorf("no cloud project selected")} @@ -222,7 +222,7 @@ func (m Model) createVolumeFromBackup(backup map[string]interface{}, volName, vo // Fetch source volume type to preserve it volType := "classic" if sourceVolumeID != "" && sourceVolumeID != "" { - var sourceVol map[string]interface{} + var sourceVol map[string]any srcEndpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s", m.cloudProject, url.PathEscape(sourceVolumeID)) if err := httpLib.Client.Get(srcEndpoint, &sourceVol); err == nil { if t, ok := sourceVol["type"].(string); ok && t != "" { @@ -231,13 +231,13 @@ func (m Model) createVolumeFromBackup(backup map[string]interface{}, volName, vo } } // Step 1: create a new empty volume - volBody := map[string]interface{}{ + volBody := map[string]any{ "name": volName, "region": region, "size": sizeInt, "type": volType, } - var newVol map[string]interface{} + var newVol map[string]any volEndpoint := fmt.Sprintf("/v1/cloud/project/%s/volume", m.cloudProject) if err := httpLib.Client.Post(volEndpoint, volBody, &newVol); err != nil { return backupActionDoneMsg{action: block_storage.BackupActionCreateVolume, err: fmt.Errorf("failed to create volume: %w", err)} @@ -245,9 +245,9 @@ func (m Model) createVolumeFromBackup(backup map[string]interface{}, volName, vo newVolID := fmt.Sprintf("%v", newVol["id"]) // Wait for the new volume to become available before restoring volDetailEndpoint := fmt.Sprintf("/v1/cloud/project/%s/volume/%s", m.cloudProject, url.PathEscape(newVolID)) - for i := 0; i < 60; i++ { + for range 60 { time.Sleep(3 * time.Second) - var volStatus map[string]interface{} + var volStatus map[string]any if err := httpLib.Client.Get(volDetailEndpoint, &volStatus); err == nil { if status, _ := volStatus["status"].(string); status == "available" { break @@ -257,8 +257,8 @@ func (m Model) createVolumeFromBackup(backup map[string]interface{}, volName, vo // Step 2: restore backup onto the new volume restoreEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/volumeBackup/%s/restore", m.cloudProject, url.PathEscape(region), url.PathEscape(id)) - restoreBody := map[string]interface{}{"volumeId": newVolID} - var restoreResult map[string]interface{} + restoreBody := map[string]any{"volumeId": newVolID} + var restoreResult map[string]any if err := httpLib.Client.Post(restoreEndpoint, restoreBody, &restoreResult); err != nil { return backupActionDoneMsg{action: block_storage.BackupActionCreateVolume, err: fmt.Errorf("volume created but restore failed: %w", err)} } @@ -274,7 +274,7 @@ func (m Model) fetchVolumesForRegion(region string) tea.Cmd { return block_storage.BackupVolumesLoadedMsg{Volumes: nil} } // Filter to same region - var filtered []map[string]interface{} + var filtered []map[string]any for _, v := range msg.data { if fmt.Sprintf("%v", v["region"]) == region { filtered = append(filtered, v) diff --git a/internal/services/browser/backup_wizard.go b/internal/services/browser/backup_wizard.go index 5387129c8..7e1526723 100644 --- a/internal/services/browser/backup_wizard.go +++ b/internal/services/browser/backup_wizard.go @@ -57,10 +57,7 @@ func (m Model) renderBackupWizardVolumeStep(width int) string { if m.wizard.backupVolumeIdx >= maxVisible { startIdx = m.wizard.backupVolumeIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(m.wizard.backupVolumes) { - endIdx = len(m.wizard.backupVolumes) - } + endIdx := min(startIdx+maxVisible, len(m.wizard.backupVolumes)) if startIdx > 0 { content.WriteString(dimStyle.Render(fmt.Sprintf(" (...%d above)", startIdx)) + "\n") } diff --git a/internal/services/browser/file_api.go b/internal/services/browser/file_api.go index 8d7a437f9..56bda38c2 100644 --- a/internal/services/browser/file_api.go +++ b/internal/services/browser/file_api.go @@ -44,7 +44,7 @@ func (m Model) fetchFileShareRegions() tea.Cmd { go func(regionName string) { probe := fmt.Sprintf("/v1/cloud/project/%s/region/%s/share", m.cloudProject, url.PathEscape(regionName)) - var result []map[string]interface{} + var result []map[string]any err := httpLib.Client.Get(probe, &result) ch <- probeResult{region: regionName, supported: err == nil} }(name) @@ -74,16 +74,16 @@ func (m Model) fetchFileShareNetworks() tea.Cmd { if m.cloudProject == "" { return fileShareNetworksLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var networks []map[string]interface{} + var networks []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private", m.cloudProject) if err := httpLib.Client.Get(endpoint, &networks); err != nil { return fileShareNetworksLoadedMsg{err: fmt.Errorf("failed to fetch networks: %w", err)} } for i, net := range networks { openstackId := "" - if regions, ok := net["regions"].([]interface{}); ok { + if regions, ok := net["regions"].([]any); ok { for _, r := range regions { - if rm, ok := r.(map[string]interface{}); ok { + if rm, ok := r.(map[string]any); ok { if rm["region"] == region { openstackId, _ = rm["openstackId"].(string) break @@ -95,7 +95,7 @@ func (m Model) fetchFileShareNetworks() tea.Cmd { networks[i]["_openstackId"] = openstackId } } - var filtered []map[string]interface{} + var filtered []map[string]any for _, net := range networks { if _, ok := net["_openstackId"]; ok { filtered = append(filtered, net) @@ -116,7 +116,7 @@ func (m Model) fetchFileShareSubnets(networkID string) tea.Cmd { if m.cloudProject == "" { return fileShareSubnetsLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var subnets []map[string]interface{} + var subnets []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private/%s/subnet", m.cloudProject, networkID) if err := httpLib.Client.Get(endpoint, &subnets); err != nil { return fileShareSubnetsLoadedMsg{err: fmt.Errorf("failed to fetch subnets: %w", err)} @@ -136,14 +136,14 @@ func (m Model) createFileShare() tea.Cmd { if m.cloudProject == "" { return fileShareCreatedMsg{err: fmt.Errorf("no cloud project selected")} } - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.fileShareName, "type": m.wizard.fileShareType, "size": m.wizard.fileShareSize, "networkId": m.wizard.fileShareNetworkId, "subnetId": m.wizard.fileShareSubnetId, } - var share map[string]interface{} + var share map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/share", m.cloudProject, url.PathEscape(m.wizard.selectedRegion)) if err := httpLib.Client.Post(endpoint, body, &share); err != nil { @@ -174,9 +174,9 @@ func (m Model) fetchShareRegions() ([]string, error) { continue } name := allNames[i] - if services, ok := r["services"].([]interface{}); ok { + if services, ok := r["services"].([]any); ok { for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if sm["name"] == "share" && sm["status"] == "UP" { result = append(result, name) break @@ -202,13 +202,13 @@ func (m Model) fetchFileStorageData() dataLoadedMsg { return dataLoadedMsg{data: nil} } - type regionResult struct{ shares []map[string]interface{} } + type regionResult struct{ shares []map[string]any } ch := make(chan regionResult, len(regionNames)) for _, name := range regionNames { go func(regionName string) { probe := fmt.Sprintf("/v1/cloud/project/%s/region/%s/share", m.cloudProject, url.PathEscape(regionName)) - var shares []map[string]interface{} + var shares []map[string]any if err := httpLib.Client.Get(probe, &shares); err != nil { ch <- regionResult{} return @@ -220,7 +220,7 @@ func (m Model) fetchFileStorageData() dataLoadedMsg { }(name) } - var allShares []map[string]interface{} + var allShares []map[string]any for range regionNames { r := <-ch allShares = append(allShares, r.shares...) @@ -234,7 +234,7 @@ func (m Model) fetchFileStorageData() dataLoadedMsg { } // createFileStorageTable builds the table model for file shares. -func createFileStorageTable(data []map[string]interface{}, width, height int) table.Model { +func createFileStorageTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Name", Width: 25}, {Title: "ID", Width: 20}, @@ -268,10 +268,7 @@ func createFileStorageTable(data []map[string]interface{}, width, height int) ta rows = append(rows, table.Row{name, id, region, shareType, sizeStr, status}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -381,7 +378,7 @@ func (m Model) renameFileShare(shareId, region, newName string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/share/%s", m.cloudProject, url.PathEscape(region), url.PathEscape(shareId)) - body := map[string]interface{}{"name": newName} + body := map[string]any{"name": newName} err := httpLib.Client.Put(endpoint, body, nil) return fileShareActionDoneMsg{action: file_storage.FileShareActionRename, err: err} } @@ -391,7 +388,7 @@ func (m Model) extendFileShare(shareId, region string, newSizeGB int) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/share/%s", m.cloudProject, url.PathEscape(region), url.PathEscape(shareId)) - body := map[string]interface{}{"newSize": newSizeGB} + body := map[string]any{"newSize": newSizeGB} err := httpLib.Client.Put(endpoint, body, nil) return fileShareActionDoneMsg{action: file_storage.FileShareActionExtend, err: err} } @@ -414,7 +411,7 @@ func (m Model) handleExecuteFileShareAction(msg file_storage.ExecuteFileShareAct case file_storage.FileShareActionExtend: newSize, err := strconv.Atoi(msg.Param) if err != nil || newSize < 1 { - m.notification = "❌ Invalid size" + m.notification = "❌ Invalid size" m.notificationExpiry = time.Now().Add(5 * time.Second) return m, nil } diff --git a/internal/services/browser/floating_ip_wizard.go b/internal/services/browser/floating_ip_wizard.go index f5d245416..b20a26416 100644 --- a/internal/services/browser/floating_ip_wizard.go +++ b/internal/services/browser/floating_ip_wizard.go @@ -42,10 +42,7 @@ func (m Model) renderFIPWizardRegionStep(width int) string { if m.wizard.fipRegionIdx >= maxVisible { startIdx = m.wizard.fipRegionIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(m.wizard.fipAvailableRegions) { - endIdx = len(m.wizard.fipAvailableRegions) - } + endIdx := min(startIdx+maxVisible, len(m.wizard.fipAvailableRegions)) for i := startIdx; i < endIdx; i++ { r := m.wizard.fipAvailableRegions[i] if i == m.wizard.fipRegionIdx { @@ -96,10 +93,7 @@ func (m Model) renderFIPWizardInstanceStep(width int) string { if m.wizard.fipInstanceIdx >= maxVisible { startIdx = m.wizard.fipInstanceIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(entries) { - endIdx = len(entries) - } + endIdx := min(startIdx+maxVisible, len(entries)) for i := startIdx; i < endIdx; i++ { if i == m.wizard.fipInstanceIdx { content.WriteString(selectedStyle.Render("▶ "+entries[i]) + "\n") diff --git a/internal/services/browser/gateway_wizard.go b/internal/services/browser/gateway_wizard.go index 92cf210fb..b13b300e7 100644 --- a/internal/services/browser/gateway_wizard.go +++ b/internal/services/browser/gateway_wizard.go @@ -45,7 +45,7 @@ func (m Model) fetchGwSubnet(networkID string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network/%s/subnet", m.cloudProject, url.PathEscape(region), url.PathEscape(networkID)) - var subnets []map[string]interface{} + var subnets []map[string]any if err := httpLib.Client.Get(endpoint, &subnets); err != nil || len(subnets) == 0 { // No subnet exists yet — network selected but not ready for gateway return gwSubnetLoadedMsg{subnetID: "", err: fmt.Errorf("this network has no compatible subnet (noGateway=true). First create a subnet using the 'OVH Gateway' option.")} @@ -59,12 +59,12 @@ func (m Model) fetchGwNetworks() tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network", m.cloudProject, url.PathEscape(region)) - var nets []map[string]interface{} + var nets []map[string]any if err := httpLib.Client.Get(endpoint, &nets); err != nil { return gwNetworksLoadedMsg{err: err} } // Exclude the external/public network - var filtered []map[string]interface{} + var filtered []map[string]any for _, n := range nets { name := getStringValue(n, "name", "") if name != "" && name != "Ext-Net" { @@ -91,7 +91,7 @@ func (m Model) createGatewayFromWizard() tea.Cmd { } return "s" }() - body := map[string]interface{}{ + body := map[string]any{ "model": model, "name": m.wizard.gwName, } @@ -112,7 +112,7 @@ func (m Model) createGatewayFromWizard() tea.Cmd { m.cloudProject, url.PathEscape(m.wizard.gwRegion)) } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { errMsg := err.Error() if strings.Contains(errMsg, "gateway IP must not be used by a port") || @@ -414,11 +414,11 @@ func (m Model) handleGwWizardNetworkKeys(key string) (tea.Model, tea.Cmd) { m.wizard.gwNetworkName = getStringValue(net, "name", getStringValue(net, "id", "unknown")) // Regional network API returns subnets as a list of ID strings — extract the first one subnetID := "" - if subnets, ok := net["subnets"].([]interface{}); ok && len(subnets) > 0 { + if subnets, ok := net["subnets"].([]any); ok && len(subnets) > 0 { switch v := subnets[0].(type) { case string: subnetID = v - case map[string]interface{}: + case map[string]any: subnetID = getStringValue(v, "id", "") } } @@ -460,4 +460,3 @@ func (m Model) handleGwWizardConfirmKeys(key string) (tea.Model, tea.Cmd) { } return m, nil } - diff --git a/internal/services/browser/lb_health_monitor_wizard.go b/internal/services/browser/lb_health_monitor_wizard.go index 1e5d8f03e..fb512df78 100644 --- a/internal/services/browser/lb_health_monitor_wizard.go +++ b/internal/services/browser/lb_health_monitor_wizard.go @@ -63,40 +63,40 @@ func (m Model) renderLBHealthMonitorView(width int) string { labelSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Width(22) valueSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) -// Actions bar — navigate with ←/→, execute with Enter - var actionsContent string - if hm != nil { - actions := []string{"✏ Edit", "🗑 Delete"} - actionIdx := m.lbHMActionIdx - if actionIdx >= len(actions) { - actionIdx = 0 - } - var actionParts []string - for i, act := range actions { - if i == actionIdx { - bg := lipgloss.Color("#7B68EE") - if i == 1 { - bg = lipgloss.Color("#FF4444") - } - actionParts = append(actionParts, lipgloss.NewStyle(). - Background(bg).Foreground(lipgloss.Color("#FFFFFF")).Bold(true).Padding(0, 1).Render(act)) - } else { - actionParts = append(actionParts, lipgloss.NewStyle(). - Foreground(lipgloss.Color("#888888")).Padding(0, 1).Render("["+act+"]")) - } - } - actionsContent = strings.Join(actionParts, " ") - if m.lbHMConfirm { - actionsContent += "\n\n" + lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFD700")).Bold(true). - Render("⚠️ Press Enter to confirm delete, Esc to cancel") - } - } else { - // No HM yet — only Create - createSt := lipgloss.NewStyle().Background(lipgloss.Color("#00AA55")).Foreground(lipgloss.Color("#FFFFFF")).Bold(true).Padding(0, 1) - actionsContent = createSt.Render("+ Create") - } - actionsBox := renderBox("Actions (←/→ to navigate, Enter to execute)", actionsContent, width-4) + // Actions bar — navigate with ←/→, execute with Enter + var actionsContent string + if hm != nil { + actions := []string{"✏ Edit", "🗑 Delete"} + actionIdx := m.lbHMActionIdx + if actionIdx >= len(actions) { + actionIdx = 0 + } + var actionParts []string + for i, act := range actions { + if i == actionIdx { + bg := lipgloss.Color("#7B68EE") + if i == 1 { + bg = lipgloss.Color("#FF4444") + } + actionParts = append(actionParts, lipgloss.NewStyle(). + Background(bg).Foreground(lipgloss.Color("#FFFFFF")).Bold(true).Padding(0, 1).Render(act)) + } else { + actionParts = append(actionParts, lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")).Padding(0, 1).Render("["+act+"]")) + } + } + actionsContent = strings.Join(actionParts, " ") + if m.lbHMConfirm { + actionsContent += "\n\n" + lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFD700")).Bold(true). + Render("⚠️ Press Enter to confirm delete, Esc to cancel") + } + } else { + // No HM yet — only Create + createSt := lipgloss.NewStyle().Background(lipgloss.Color("#00AA55")).Foreground(lipgloss.Color("#FFFFFF")).Bold(true).Padding(0, 1) + actionsContent = createSt.Render("+ Create") + } + actionsBox := renderBox("Actions (←/→ to navigate, Enter to execute)", actionsContent, width-4) content.WriteString(actionsBox + "\n\n") titleLine := fmt.Sprintf("Health Monitor — Pool: %s", poolName) @@ -120,7 +120,7 @@ func (m Model) renderLBHealthMonitorView(width int) string { hmHttpMethod := "" hmUrlPath := "" hmExpectedCodes := "" - if httpCfg, ok := hm["httpConfiguration"].(map[string]interface{}); ok { + if httpCfg, ok := hm["httpConfiguration"].(map[string]any); ok { hmHttpMethod = getStringValue(httpCfg, "httpMethod", "") hmUrlPath = getStringValue(httpCfg, "urlPath", "") hmExpectedCodes = getStringValue(httpCfg, "expectedCodes", "") diff --git a/internal/services/browser/lb_l7policy_wizard.go b/internal/services/browser/lb_l7policy_wizard.go index e716091f0..d31aab3f9 100644 --- a/internal/services/browser/lb_l7policy_wizard.go +++ b/internal/services/browser/lb_l7policy_wizard.go @@ -275,10 +275,7 @@ func (m Model) handleLBL7PolicyWizardActionKeys(key string) (tea.Model, tea.Cmd) func (m Model) handleLBL7PolicyWizardRedirectPoolKeys(key string) (tea.Model, tea.Cmd) { pools := m.lbPools[m.wizard.l7PolicyLBId] - maxIdx := len(pools) - 1 - if maxIdx < 0 { - maxIdx = 0 - } + maxIdx := max(len(pools)-1, 0) switch key { case "up", "k": if m.wizard.l7PolicyRedirectPoolIdx > 0 { @@ -364,7 +361,7 @@ func (m Model) createLBL7Policy() tea.Cmd { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/l7Policy", m.cloudProject, url.PathEscape(m.wizard.l7PolicyLBRegion)) - body := map[string]interface{}{ + body := map[string]any{ "listenerId": m.wizard.l7PolicyListenerId, "name": m.wizard.l7PolicyName, "position": m.wizard.l7PolicyPosition, @@ -380,7 +377,7 @@ func (m Model) createLBL7Policy() tea.Cmd { body["redirectPrefix"] = m.wizard.l7PolicyRedirectUrl } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return lbL7PolicyCreatedMsg{policyName: m.wizard.l7PolicyName, err: fmt.Errorf("creation failed: %w", err)} } @@ -395,12 +392,12 @@ func (m Model) fetchLBL7Policies(listenerID, region string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/l7Policy", m.cloudProject, url.PathEscape(region)) - var all []map[string]interface{} + var all []map[string]any if err := httpLib.Client.Get(endpoint, &all); err != nil { return lbL7PoliciesLoadedMsg{listenerID: listenerID, err: err} } // Filter by listenerId - var policies []map[string]interface{} + var policies []map[string]any for _, p := range all { if getStringValue(p, "listenerId", "") == listenerID { policies = append(policies, p) @@ -443,7 +440,7 @@ func (m Model) updateLBL7Policy() tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/l7Policy/%s", m.cloudProject, url.PathEscape(m.wizard.l7PolicyLBRegion), url.PathEscape(m.wizard.l7PolicyEditId)) - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.l7PolicyName, "position": m.wizard.l7PolicyPosition, "action": m.wizard.l7PolicyAction, @@ -457,7 +454,7 @@ func (m Model) updateLBL7Policy() tea.Cmd { if m.wizard.l7PolicyAction == "redirectPrefix" { body["redirectPrefix"] = m.wizard.l7PolicyRedirectUrl } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Put(endpoint, body, &result); err != nil { return lbL7PolicyUpdatedMsg{policyName: m.wizard.l7PolicyName, err: fmt.Errorf("update failed: %w", err)} } @@ -472,7 +469,7 @@ func (m Model) fetchLBL7Rules(policyID, region string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/l7Policy/%s/l7Rule", m.cloudProject, url.PathEscape(region), url.PathEscape(policyID)) - var rules []map[string]interface{} + var rules []map[string]any if err := httpLib.Client.Get(endpoint, &rules); err != nil { return lbL7RulesLoadedMsg{policyID: policyID, err: err} } diff --git a/internal/services/browser/lb_l7rule_wizard.go b/internal/services/browser/lb_l7rule_wizard.go index 0f955f851..d720a231b 100644 --- a/internal/services/browser/lb_l7rule_wizard.go +++ b/internal/services/browser/lb_l7rule_wizard.go @@ -410,7 +410,7 @@ func (m Model) createLBL7Rule() tea.Cmd { break } } - body := map[string]interface{}{ + body := map[string]any{ "ruleType": m.wizard.l7RuleType, "compareType": m.wizard.l7RuleCompare, "value": m.wizard.l7RuleValue, @@ -427,7 +427,7 @@ func (m Model) createLBL7Rule() tea.Cmd { // Edit mode: PUT endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/l7Policy/%s/l7Rule/%s", m.cloudProject, url.PathEscape(region), url.PathEscape(policyID), url.PathEscape(m.wizard.l7RuleEditId)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Put(endpoint, body, &result); err != nil { return lbL7RuleCreatedMsg{policyID: policyID, err: fmt.Errorf("update failed: %w", err)} } @@ -437,7 +437,7 @@ func (m Model) createLBL7Rule() tea.Cmd { // Create mode: POST endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/l7Policy/%s/l7Rule", m.cloudProject, url.PathEscape(region), url.PathEscape(policyID)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return lbL7RuleCreatedMsg{policyID: policyID, err: fmt.Errorf("creation failed: %w", err)} } diff --git a/internal/services/browser/lb_listener_wizard.go b/internal/services/browser/lb_listener_wizard.go index 8cd60ddd7..cbddf1e28 100644 --- a/internal/services/browser/lb_listener_wizard.go +++ b/internal/services/browser/lb_listener_wizard.go @@ -9,6 +9,7 @@ package browser import ( "fmt" "net/url" + "slices" "strconv" "strings" @@ -116,12 +117,7 @@ func lbPoolCompatible(poolProto, listenerProto string) bool { if compatible == nil { return true } - for _, p := range compatible { - if p == poolProto { - return true - } - } - return false + return slices.Contains(compatible, poolProto) } func (m Model) renderLBListenerWizardPoolStep(width int) string { @@ -136,7 +132,7 @@ func (m Model) renderLBListenerWizardPoolStep(width int) string { compatProtos := lbCompatiblePoolProtos(m.wizard.lbListenerProto) compatNote := "" if len(compatProtos) > 0 { - compatNote = "Compatible protocols with "+m.wizard.lbListenerProto+": "+strings.Join(compatProtos, ", ") + compatNote = "Compatible protocols with " + m.wizard.lbListenerProto + ": " + strings.Join(compatProtos, ", ") } b.WriteString(descStyle.Render("Attach an existing pool to this listener, or skip this step.") + "\n") if compatNote != "" { @@ -146,7 +142,7 @@ func (m Model) renderLBListenerWizardPoolStep(width int) string { allPools := m.lbPools[m.wizard.lbListenerLBId] // Filter to compatible pools only - var pools []map[string]interface{} + var pools []map[string]any for _, p := range allPools { if lbPoolCompatible(getStringValue(p, "protocol", ""), m.wizard.lbListenerProto) { pools = append(pools, p) @@ -327,7 +323,7 @@ func (m Model) handleLBListenerWizardPortKeys(key string) (tea.Model, tea.Cmd) { func (m Model) handleLBListenerWizardPoolKeys(key string) (tea.Model, tea.Cmd) { allPools := m.lbPools[m.wizard.lbListenerLBId] - var pools []map[string]interface{} + var pools []map[string]any for _, p := range allPools { if lbPoolCompatible(getStringValue(p, "protocol", ""), m.wizard.lbListenerProto) { pools = append(pools, p) @@ -400,7 +396,7 @@ func (m Model) createLBListener() tea.Cmd { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/listener", m.cloudProject, url.PathEscape(m.wizard.lbListenerLBRegion)) - body := map[string]interface{}{ + body := map[string]any{ "loadbalancerId": m.wizard.lbListenerLBId, "name": m.wizard.lbListenerName, "protocol": m.wizard.lbListenerProto, @@ -410,7 +406,7 @@ func (m Model) createLBListener() tea.Cmd { body["defaultPoolId"] = m.wizard.lbListenerPoolId } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return lbListenerCreatedMsg{listenerName: m.wizard.lbListenerName, err: fmt.Errorf("creation failed: %w", err)} } diff --git a/internal/services/browser/lb_pool_wizard.go b/internal/services/browser/lb_pool_wizard.go index c4d29304d..9a6122163 100644 --- a/internal/services/browser/lb_pool_wizard.go +++ b/internal/services/browser/lb_pool_wizard.go @@ -167,7 +167,7 @@ func (m Model) renderLBPoolWizardConfirmStep(width int) string { b.WriteString(labelStyle.Render(" Session:") + valStyle.Render(sessionLabel) + "\n\n") if m.wizard.isLoading { - b.WriteString(loadingStyle.Render("⏳ "+m.wizard.loadingMessage)) + b.WriteString(loadingStyle.Render("⏳ " + m.wizard.loadingMessage)) return b.String() } if m.wizard.errorMsg != "" { @@ -347,7 +347,7 @@ func (m Model) createLBPool() tea.Cmd { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/pool", m.cloudProject, url.PathEscape(m.wizard.lbPoolLBRegion)) - body := map[string]interface{}{ + body := map[string]any{ "loadbalancerId": m.wizard.lbPoolLBId, "name": m.wizard.lbPoolName, "algorithm": m.wizard.lbPoolAlgo, @@ -355,12 +355,12 @@ func (m Model) createLBPool() tea.Cmd { } if m.wizard.lbPoolSession != "" && m.wizard.lbPoolSession != "disabled" { - body["sessionPersistence"] = map[string]interface{}{ + body["sessionPersistence"] = map[string]any{ "type": m.wizard.lbPoolSession, } } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return lbPoolCreatedMsg{poolName: m.wizard.lbPoolName, err: fmt.Errorf("creation failed: %w", err)} } diff --git a/internal/services/browser/lb_wizard.go b/internal/services/browser/lb_wizard.go index c74d88b80..a149f79fb 100644 --- a/internal/services/browser/lb_wizard.go +++ b/internal/services/browser/lb_wizard.go @@ -37,9 +37,9 @@ func (m Model) fetchLBRegions() tea.Cmd { if r == nil { continue } - if services, ok := r["services"].([]interface{}); ok { + if services, ok := r["services"].([]any); ok { for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if sm["name"] == "octavialoadbalancer" && sm["status"] == "UP" { regions = append(regions, allNames[i]) break @@ -59,7 +59,7 @@ func (m Model) fetchLBFlavors() tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/flavor", m.cloudProject, url.PathEscape(region)) - var flavors []map[string]interface{} + var flavors []map[string]any if err := httpLib.Client.Get(endpoint, &flavors); err != nil { return lbFlavorsLoadedMsg{err: err} } @@ -73,11 +73,11 @@ func (m Model) fetchLBNetworks() tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network", m.cloudProject, url.PathEscape(region)) - var nets []map[string]interface{} + var nets []map[string]any if err := httpLib.Client.Get(endpoint, &nets); err != nil { return lbNetworksLoadedMsg{err: err} } - var filtered []map[string]interface{} + var filtered []map[string]any for _, n := range nets { if getStringValue(n, "visibility", "") == "private" { filtered = append(filtered, n) @@ -93,7 +93,7 @@ func (m Model) fetchLBSubnet(networkID string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network/%s/subnet", m.cloudProject, url.PathEscape(region), url.PathEscape(networkID)) - var subnets []map[string]interface{} + var subnets []map[string]any if err := httpLib.Client.Get(endpoint, &subnets); err != nil || len(subnets) == 0 { return lbSubnetLoadedMsg{subnetID: ""} } @@ -111,12 +111,12 @@ func (m Model) createLBFromWizard() tea.Cmd { if m.wizard.lbNetworkId == "" || m.wizard.lbSubnetId == "" { return lbCreatedMsg{err: fmt.Errorf("a private network with a subnet is required to create a load balancer")} } - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.lbName, "flavorId": m.wizard.lbFlavorId, - "network": map[string]interface{}{ - "private": map[string]interface{}{ - "network": map[string]interface{}{ + "network": map[string]any{ + "private": map[string]any{ + "network": map[string]any{ "id": m.wizard.lbNetworkId, "subnetId": m.wizard.lbSubnetId, }, @@ -125,7 +125,7 @@ func (m Model) createLBFromWizard() tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/loadbalancing/loadbalancer", m.cloudProject, url.PathEscape(m.wizard.lbRegion)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return lbCreatedMsg{err: fmt.Errorf("failed to create load balancer: %w", err)} } @@ -186,10 +186,7 @@ func (m Model) renderLBWizardRegionStep(width int) string { if m.wizard.lbRegionIdx >= maxVisible { startIdx = m.wizard.lbRegionIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(m.wizard.lbAvailableRegions) { - endIdx = len(m.wizard.lbAvailableRegions) - } + endIdx := min(startIdx+maxVisible, len(m.wizard.lbAvailableRegions)) for i := startIdx; i < endIdx; i++ { r := m.wizard.lbAvailableRegions[i] if i == m.wizard.lbRegionIdx { @@ -200,7 +197,7 @@ func (m Model) renderLBWizardRegionStep(width int) string { } if len(m.wizard.lbAvailableRegions) > maxVisible { content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")). - Render(fmt.Sprintf("\n %d / %d regions", m.wizard.lbRegionIdx+1, len(m.wizard.lbAvailableRegions)))) + Render(fmt.Sprintf("\n %d / %d regions", m.wizard.lbRegionIdx+1, len(m.wizard.lbAvailableRegions)))) } } @@ -442,11 +439,11 @@ func (m Model) handleLBWizardNetworkKeys(key string) (tea.Model, tea.Cmd) { m.wizard.lbNetworkName = getStringValue(net, "name", getStringValue(net, "id", "unknown")) // Try to find subnet ID embedded in network data subnetID := "" - if subnets, ok := net["subnets"].([]interface{}); ok && len(subnets) > 0 { + if subnets, ok := net["subnets"].([]any); ok && len(subnets) > 0 { switch v := subnets[0].(type) { case string: subnetID = v - case map[string]interface{}: + case map[string]any: subnetID = getStringValue(v, "id", "") } } diff --git a/internal/services/browser/managed_analytics_wizard.go b/internal/services/browser/managed_analytics_wizard.go index fcc7c7e4a..9eb1abce9 100644 --- a/internal/services/browser/managed_analytics_wizard.go +++ b/internal/services/browser/managed_analytics_wizard.go @@ -28,17 +28,17 @@ func (m Model) fetchAnalyticsCapabilities() tea.Cmd { capEndpoint := fmt.Sprintf("/v1/cloud/project/%s/database/capabilities", m.cloudProject) availEndpoint := fmt.Sprintf("/v1/cloud/project/%s/database/availability", m.cloudProject) - var caps map[string]interface{} + var caps map[string]any if err := httpLib.Client.Get(capEndpoint, &caps); err != nil { return dbCapabilitiesLoadedMsg{err: err} } - var availItems []map[string]interface{} + var availItems []map[string]any _ = httpLib.Client.Get(availEndpoint, &availItems) // Fallback regions from capabilities top-level list var capsRegions []string - if raws, ok := caps["regions"].([]interface{}); ok { + if raws, ok := caps["regions"].([]any); ok { for _, r := range raws { if s, ok := r.(string); ok { capsRegions = append(capsRegions, s) @@ -48,10 +48,10 @@ func (m Model) fetchAnalyticsCapabilities() tea.Cmd { } // Only keep analytics engines (category == "analysis") - engines, _ := caps["engines"].([]interface{}) - var engMaps []map[string]interface{} + engines, _ := caps["engines"].([]any) + var engMaps []map[string]any for _, e := range engines { - if em, ok := e.(map[string]interface{}); ok { + if em, ok := e.(map[string]any); ok { if strings.ToLower(getStringValue(em, "category", "")) == "analysis" { engMaps = append(engMaps, em) } @@ -61,10 +61,10 @@ func (m Model) fetchAnalyticsCapabilities() tea.Cmd { return getStringValue(engMaps[i], "name", "") < getStringValue(engMaps[j], "name", "") }) - flavors, _ := caps["flavors"].([]interface{}) - var flavMaps []map[string]interface{} + flavors, _ := caps["flavors"].([]any) + var flavMaps []map[string]any for _, f := range flavors { - if fm, ok := f.(map[string]interface{}); ok { + if fm, ok := f.(map[string]any); ok { flavMaps = append(flavMaps, fm) } } @@ -77,10 +77,10 @@ func (m Model) fetchAnalyticsCapabilities() tea.Cmd { return getStringValue(flavMaps[i], "name", "") < getStringValue(flavMaps[j], "name", "") }) - plans, _ := caps["plans"].([]interface{}) - var planMaps []map[string]interface{} + plans, _ := caps["plans"].([]any) + var planMaps []map[string]any for _, p := range plans { - if pm, ok := p.(map[string]interface{}); ok { + if pm, ok := p.(map[string]any); ok { planMaps = append(planMaps, pm) } } @@ -107,7 +107,7 @@ func (m Model) fetchAnalyticsEngineAvail(engineName string) tea.Cmd { return func() tea.Msg { endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/availability", m.cloudProject, url.PathEscape(engineName)) - var items []map[string]interface{} + var items []map[string]any if err := httpLib.Client.Get(endpoint, &items); err != nil { return analyticsEngineAvailLoadedMsg{engine: engineName, err: err} } @@ -128,11 +128,11 @@ func (m Model) createAnalyticsFromWizard() tea.Cmd { if engine == "" { return analyticsCreatedMsg{err: fmt.Errorf("no engine selected")} } - body := map[string]interface{}{ + body := map[string]any{ "description": m.wizard.dbName, "version": m.wizard.dbVersion, "plan": m.wizard.dbPlan, - "nodesPattern": map[string]interface{}{ + "nodesPattern": map[string]any{ "flavor": m.wizard.dbFlavor, "region": m.wizard.dbRegion, "number": m.wizard.dbNodes, @@ -142,12 +142,12 @@ func (m Model) createAnalyticsFromWizard() tea.Cmd { body["networkId"] = m.wizard.dbNetworkId } if m.wizard.dbDiskSize > 0 { - body["disk"] = map[string]interface{}{"size": m.wizard.dbDiskSize} + body["disk"] = map[string]any{"size": m.wizard.dbDiskSize} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s", m.cloudProject, url.PathEscape(engine)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return analyticsCreatedMsg{err: fmt.Errorf("failed to create analytics service: %w", err)} } @@ -264,10 +264,7 @@ func (m Model) renderAnalyticsWizardRegionStep(_ int) string { if m.wizard.dbRegionIdx >= maxVisible { startIdx = m.wizard.dbRegionIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(regions) { - endIdx = len(regions) - } + endIdx := min(startIdx+maxVisible, len(regions)) for i := startIdx; i < endIdx; i++ { r := regions[i] if i == m.wizard.dbRegionIdx { @@ -653,16 +650,16 @@ func (m Model) handleAnalyticsWizardFlavorKeys(key string) (tea.Model, tea.Cmd) // We do NOT use min or step since deprecated fields can be stale/wrong. diskSize := 0 if avail := m.dbActiveAvail(); avail != nil { - if specs, ok := avail["specifications"].(map[string]interface{}); ok { - if storage, ok := specs["storage"].(map[string]interface{}); ok { - if maxS, ok := storage["maximum"].(map[string]interface{}); ok { + if specs, ok := avail["specifications"].(map[string]any); ok { + if storage, ok := specs["storage"].(map[string]any); ok { + if maxS, ok := storage["maximum"].(map[string]any); ok { if v, ok := toFloat64(maxS["value"]); ok && v > 0 { diskSize = int(v) } } // If maximum is absent or zero, fall back to minimum if diskSize == 0 { - if minS, ok := storage["minimum"].(map[string]interface{}); ok { + if minS, ok := storage["minimum"].(map[string]any); ok { if v, ok := toFloat64(minS["value"]); ok && v > 0 { diskSize = int(v) } diff --git a/internal/services/browser/managed_db_wizard.go b/internal/services/browser/managed_db_wizard.go index 26e689dc7..6530fa0ce 100644 --- a/internal/services/browser/managed_db_wizard.go +++ b/internal/services/browser/managed_db_wizard.go @@ -28,18 +28,17 @@ func (m Model) fetchDBCapabilities() tea.Cmd { capEndpoint := fmt.Sprintf("/v1/cloud/project/%s/database/capabilities", m.cloudProject) availEndpoint := fmt.Sprintf("/v1/cloud/project/%s/database/availability", m.cloudProject) - - var caps map[string]interface{} + var caps map[string]any if err := httpLib.Client.Get(capEndpoint, &caps); err != nil { return dbCapabilitiesLoadedMsg{err: err} } - var availItems []map[string]interface{} + var availItems []map[string]any _ = httpLib.Client.Get(availEndpoint, &availItems) // Fallback regions from capabilities top-level list var capsRegions []string - if raws, ok := caps["regions"].([]interface{}); ok { + if raws, ok := caps["regions"].([]any); ok { for _, r := range raws { if s, ok := r.(string); ok { capsRegions = append(capsRegions, s) @@ -48,10 +47,10 @@ func (m Model) fetchDBCapabilities() tea.Cmd { sort.Strings(capsRegions) } - engines, _ := caps["engines"].([]interface{}) - var engMaps []map[string]interface{} + engines, _ := caps["engines"].([]any) + var engMaps []map[string]any for _, e := range engines { - if em, ok := e.(map[string]interface{}); ok { + if em, ok := e.(map[string]any); ok { // Exclude analytics engines — those belong to the Managed Analytics wizard if strings.ToLower(getStringValue(em, "category", "")) != "analysis" { engMaps = append(engMaps, em) @@ -62,10 +61,10 @@ func (m Model) fetchDBCapabilities() tea.Cmd { return getStringValue(engMaps[i], "name", "") < getStringValue(engMaps[j], "name", "") }) - flavors, _ := caps["flavors"].([]interface{}) - var flavMaps []map[string]interface{} + flavors, _ := caps["flavors"].([]any) + var flavMaps []map[string]any for _, f := range flavors { - if fm, ok := f.(map[string]interface{}); ok { + if fm, ok := f.(map[string]any); ok { flavMaps = append(flavMaps, fm) } } @@ -78,10 +77,10 @@ func (m Model) fetchDBCapabilities() tea.Cmd { return getStringValue(flavMaps[i], "name", "") < getStringValue(flavMaps[j], "name", "") }) - plans, _ := caps["plans"].([]interface{}) - var planMaps []map[string]interface{} + plans, _ := caps["plans"].([]any) + var planMaps []map[string]any for _, p := range plans { - if pm, ok := p.(map[string]interface{}); ok { + if pm, ok := p.(map[string]any); ok { planMaps = append(planMaps, pm) } } @@ -107,14 +106,14 @@ func (m Model) fetchDBDetailSubresources(engine, serviceId string) tea.Cmd { base := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s", m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - fetchList := func(suffix string) []map[string]interface{} { + fetchList := func(suffix string) []map[string]any { var ids []string if err := httpLib.Client.Get(base+suffix, &ids); err != nil { return nil } - var items []map[string]interface{} + var items []map[string]any for _, id := range ids { - var item map[string]interface{} + var item map[string]any if err := httpLib.Client.Get(base+suffix+"/"+url.PathEscape(id), &item); err == nil { items = append(items, item) } @@ -123,7 +122,7 @@ func (m Model) fetchDBDetailSubresources(engine, serviceId string) tea.Cmd { } // Connection pools are only supported by PostgreSQL - var pools []map[string]interface{} + var pools []map[string]any if strings.EqualFold(engine, "postgresql") { pools = fetchList("/connectionPool") } @@ -152,19 +151,19 @@ type dbPoolCreatedMsg struct { // dbLogsMsg is sent after fetching service log entries. type dbLogsMsg struct { - logs []map[string]interface{} + logs []map[string]any err error } // dbACLMsg is sent after fetching ACL entries for an analytics service. type dbACLMsg struct { - acl []map[string]interface{} + acl []map[string]any err error } // dbTopicsMsg is sent after fetching Kafka topic list. type dbTopicsMsg struct { - topics []map[string]interface{} + topics []map[string]any err error } @@ -182,9 +181,9 @@ func (m Model) fetchDBTopics() tea.Cmd { if err := httpLib.Client.Get(base, &ids); err != nil { return dbTopicsMsg{err: err} } - var topics []map[string]interface{} + var topics []map[string]any for _, id := range ids { - var entry map[string]interface{} + var entry map[string]any if err := httpLib.Client.Get(base+"/"+url.PathEscape(id), &entry); err == nil { topics = append(topics, entry) } @@ -208,12 +207,12 @@ func (m Model) createDBACL(username, topic, permission string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/acl", m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - body := map[string]interface{}{ + body := map[string]any{ "username": username, "topic": topic, "permission": permission, } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return dbACLCreatedMsg{err: err} } @@ -236,7 +235,7 @@ func (m Model) createDBTopic(name string, minInsyncReplicas, partitions, replica } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/topic", m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - body := map[string]interface{}{ + body := map[string]any{ "name": name, "minInsyncReplicas": minInsyncReplicas, "partitions": partitions, @@ -244,7 +243,7 @@ func (m Model) createDBTopic(name string, minInsyncReplicas, partitions, replica "retentionBytes": retentionBytes, "retentionHours": retentionHours, } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return dbTopicCreatedMsg{err: err} } @@ -299,40 +298,40 @@ func (m Model) fetchDBMetricNames() tea.Cmd { // dbNodeNamesMsg carries the ordered list of node names for the current service. type dbNodeNamesMsg struct { - names []string - err error + names []string + err error } // fetchDBNodeNames fetches the node list and returns names sorted alphabetically. func (m Model) fetchDBNodeNames() tea.Cmd { - return func() tea.Msg { - engine := getStringValue(m.detailData, "engine", "") - serviceId := getStringValue(m.detailData, "id", "") - if engine == "" || serviceId == "" { - return dbNodeNamesMsg{err: fmt.Errorf("missing engine or service ID")} - } - endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/node", - m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - var ids []string - if err := httpLib.Client.Get(endpoint, &ids); err != nil { - return dbNodeNamesMsg{err: err} - } - type nodeDetail struct { - Name string `json:"name"` - } - var names []string - for _, id := range ids { - var nd nodeDetail - detailEp := fmt.Sprintf("%s/%s", endpoint, url.PathEscape(id)) - if err := httpLib.Client.Get(detailEp, &nd); err == nil && nd.Name != "" { - names = append(names, nd.Name) - } else { - names = append(names, id) - } - } - sort.Strings(names) - return dbNodeNamesMsg{names: names} - } + return func() tea.Msg { + engine := getStringValue(m.detailData, "engine", "") + serviceId := getStringValue(m.detailData, "id", "") + if engine == "" || serviceId == "" { + return dbNodeNamesMsg{err: fmt.Errorf("missing engine or service ID")} + } + endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/node", + m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) + var ids []string + if err := httpLib.Client.Get(endpoint, &ids); err != nil { + return dbNodeNamesMsg{err: err} + } + type nodeDetail struct { + Name string `json:"name"` + } + var names []string + for _, id := range ids { + var nd nodeDetail + detailEp := fmt.Sprintf("%s/%s", endpoint, url.PathEscape(id)) + if err := httpLib.Client.Get(detailEp, &nd); err == nil && nd.Name != "" { + names = append(names, nd.Name) + } else { + names = append(names, id) + } + } + sort.Strings(names) + return dbNodeNamesMsg{names: names} + } } // fetchDBMetric fetches metric data for the given metric name and period. @@ -427,9 +426,9 @@ func (m Model) fetchDBACL() tea.Cmd { if err := httpLib.Client.Get(base, &ids); err != nil { return dbACLMsg{err: err} } - var acl []map[string]interface{} + var acl []map[string]any for _, id := range ids { - var entry map[string]interface{} + var entry map[string]any if err := httpLib.Client.Get(base+"/"+url.PathEscape(id), &entry); err == nil { acl = append(acl, entry) } @@ -447,7 +446,7 @@ func (m Model) fetchDBLogs() tea.Cmd { if serviceId == "" { return dbLogsMsg{err: fmt.Errorf("missing service ID")} } - var logs []map[string]interface{} + var logs []map[string]any // Try engine-specific endpoint first (works for postgresql, mysql, etc.) if engine != "" { @@ -488,8 +487,8 @@ func (m Model) createDBDatabase(name string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/database", m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - body := map[string]interface{}{"name": name} - var result map[string]interface{} + body := map[string]any{"name": name} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return dbDatabaseCreatedMsg{name: name, err: err} } @@ -507,13 +506,13 @@ func (m Model) createDBPool(name, databaseId, mode string, size int) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/connectionPool", m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - body := map[string]interface{}{ + body := map[string]any{ "name": name, "databaseId": databaseId, "mode": mode, "size": size, } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return dbPoolCreatedMsg{name: name, err: err} } @@ -523,7 +522,7 @@ func (m Model) createDBPool(name, databaseId, mode string, size int) tea.Cmd { // dbUserCreatedMsg is sent after a DB user creation attempt. type dbUserCreatedMsg struct { - user map[string]interface{} + user map[string]any err error } @@ -543,8 +542,8 @@ func (m Model) createDBUser(username string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/user", m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId)) - body := map[string]interface{}{"name": username} - var result map[string]interface{} + body := map[string]any{"name": username} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return dbUserCreatedMsg{err: err} } @@ -593,11 +592,11 @@ func (m Model) createManagedDBFromWizard() tea.Cmd { if engine == "" { return dbCreatedMsg{err: fmt.Errorf("no engine selected")} } - body := map[string]interface{}{ + body := map[string]any{ "description": m.wizard.dbName, "version": m.wizard.dbVersion, "plan": m.wizard.dbPlan, - "nodesPattern": map[string]interface{}{ + "nodesPattern": map[string]any{ "flavor": m.wizard.dbFlavor, "region": m.wizard.dbRegion, "number": m.wizard.dbNodes, @@ -607,12 +606,12 @@ func (m Model) createManagedDBFromWizard() tea.Cmd { body["networkId"] = m.wizard.dbNetworkId } if m.wizard.dbDiskSize > 0 { - body["disk"] = map[string]interface{}{"size": m.wizard.dbDiskSize} + body["disk"] = map[string]any{"size": m.wizard.dbDiskSize} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s", m.cloudProject, url.PathEscape(engine)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return dbCreatedMsg{err: fmt.Errorf("failed to create database: %w", err)} } @@ -628,11 +627,11 @@ func (m Model) createManagedDBFromWizard() tea.Cmd { // getAvailFlavor returns the flavor name from an availability item, // checking the deprecated top-level field first, then specifications.flavor (new format). -func getAvailFlavor(a map[string]interface{}) string { +func getAvailFlavor(a map[string]any) string { if f := getStringValue(a, "flavor", ""); f != "" { return f } - if specs, ok := a["specifications"].(map[string]interface{}); ok { + if specs, ok := a["specifications"].(map[string]any); ok { return getStringValue(specs, "flavor", "") } return "" @@ -640,7 +639,7 @@ func getAvailFlavor(a map[string]interface{}) string { // toFloat64 converts a JSON value to float64, handling both float64 and json.Number // (go-ovh uses UseNumber() so all numbers come as json.Number). -func toFloat64(v interface{}) (float64, bool) { +func toFloat64(v any) (float64, bool) { switch n := v.(type) { case float64: return n, true @@ -660,7 +659,7 @@ func (m Model) dbFilteredVersions() []string { engLower := strings.ToLower(m.wizard.dbEngine) for _, e := range m.wizard.dbEngines { if strings.ToLower(getStringValue(e, "name", "")) == engLower { - if versions, ok := e["versions"].([]interface{}); ok { + if versions, ok := e["versions"].([]any); ok { var vs []string for _, v := range versions { if s, ok := v.(string); ok { @@ -794,7 +793,7 @@ func (m Model) dbFilteredFlavors() []string { // dbActiveAvail returns the availability entry matching current engine+version+region+plan+flavor. // It tries progressively relaxed matches to handle analytics per-engine items // where some top-level deprecated fields may be absent or formatted differently. -func (m Model) dbActiveAvail() map[string]interface{} { +func (m Model) dbActiveAvail() map[string]any { engLower := strings.ToLower(m.wizard.dbEngine) // Pass 1: strict — all 5 fields @@ -851,7 +850,7 @@ func (m Model) dbActiveAvail() map[string]interface{} { } // dbFlavorInfo returns the capabilities.flavor entry for the specified flavor name. -func (m Model) dbFlavorInfo(flavorName string) map[string]interface{} { +func (m Model) dbFlavorInfo(flavorName string) map[string]any { for _, f := range m.wizard.dbFlavors { if strings.EqualFold(getStringValue(f, "name", ""), flavorName) { return f @@ -879,8 +878,8 @@ func (m Model) dbNodesConstraints() (int, int) { min := 0 max := 0 // Prefer specifications.nodes (new format) over deprecated top-level fields - if specs, ok := avail["specifications"].(map[string]interface{}); ok { - if nodes, ok := specs["nodes"].(map[string]interface{}); ok { + if specs, ok := avail["specifications"].(map[string]any); ok { + if nodes, ok := specs["nodes"].(map[string]any); ok { if v, ok := toFloat64(nodes["minimum"]); ok && v > 0 { min = int(v) } @@ -916,19 +915,19 @@ func (m Model) dbStorageConstraints() (int, int, int) { min, max, step := 0, 0, 0 if avail != nil { // Prefer specifications.storage (new format) over deprecated top-level fields - if specs, ok := avail["specifications"].(map[string]interface{}); ok { - if storage, ok := specs["storage"].(map[string]interface{}); ok { - if minS, ok := storage["minimum"].(map[string]interface{}); ok { + if specs, ok := avail["specifications"].(map[string]any); ok { + if storage, ok := specs["storage"].(map[string]any); ok { + if minS, ok := storage["minimum"].(map[string]any); ok { if v, ok := toFloat64(minS["value"]); ok && v > 0 { min = int(v) } } - if maxS, ok := storage["maximum"].(map[string]interface{}); ok { + if maxS, ok := storage["maximum"].(map[string]any); ok { if v, ok := toFloat64(maxS["value"]); ok && v > 0 { max = int(v) } } - if stepS, ok := storage["step"].(map[string]interface{}); ok { + if stepS, ok := storage["step"].(map[string]any); ok { if v, ok := toFloat64(stepS["value"]); ok && v > 0 { step = int(v) } @@ -1100,10 +1099,7 @@ func (m Model) renderDBWizardRegionStep(_ int) string { if m.wizard.dbRegionIdx >= maxVisible { startIdx = m.wizard.dbRegionIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(regions) { - endIdx = len(regions) - } + endIdx := min(startIdx+maxVisible, len(regions)) for i := startIdx; i < endIdx; i++ { r := regions[i] if i == m.wizard.dbRegionIdx { diff --git a/internal/services/browser/manager.go b/internal/services/browser/manager.go index cedea0576..a77c25748 100644 --- a/internal/services/browser/manager.go +++ b/internal/services/browser/manager.go @@ -9,6 +9,7 @@ package browser import ( "encoding/json" "fmt" + "math" "math/rand" "os" "os/exec" @@ -17,19 +18,18 @@ import ( "strconv" "strings" "time" - "math" + "github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart" "github.com/ovh/ovhcloud-cli/internal/config" "github.com/ovh/ovhcloud-cli/internal/flags" httpLib "github.com/ovh/ovhcloud-cli/internal/http" + "github.com/ovh/ovhcloud-cli/internal/services/browser/views" block_storage "github.com/ovh/ovhcloud-cli/internal/services/browser/views/block_storage" file_storage "github.com/ovh/ovhcloud-cli/internal/services/browser/views/file_storage" object_storage "github.com/ovh/ovhcloud-cli/internal/services/browser/views/object_storage" - "github.com/ovh/ovhcloud-cli/internal/services/browser/views" "github.com/spf13/cobra" ) @@ -113,14 +113,14 @@ const ( const ( // File Storage wizard steps (offset by 400) - FileWizardStepName WizardStep = iota + 400 + FileWizardStepName WizardStep = iota + 400 FileWizardStepRegion FileWizardStepType FileWizardStepSize FileWizardStepNetwork FileWizardStepConfirm - ObjectWizardStepName WizardStep = iota + 500 + ObjectWizardStepName WizardStep = iota + 500 ObjectWizardStepType ObjectWizardStepRegion ObjectWizardStepReplication @@ -149,14 +149,14 @@ const ( const ( // Private Network wizard steps (offset by 800) - PrivNetWizardStepRegion WizardStep = iota + 800 // choose location - PrivNetWizardStepName // network name - PrivNetWizardStepVlanID // VLAN ID (layer 2 option) - PrivNetWizardStepSubnet // configure subnet CIDR - PrivNetWizardStepDHCP // DHCP distribution options - PrivNetWizardStepAllocPool // IP allocation pool (start/end) - PrivNetWizardStepGateway // gateway options - PrivNetWizardStepConfirm // confirm + PrivNetWizardStepRegion WizardStep = iota + 800 // choose location + PrivNetWizardStepName // network name + PrivNetWizardStepVlanID // VLAN ID (layer 2 option) + PrivNetWizardStepSubnet // configure subnet CIDR + PrivNetWizardStepDHCP // DHCP distribution options + PrivNetWizardStepAllocPool // IP allocation pool (start/end) + PrivNetWizardStepGateway // gateway options + PrivNetWizardStepConfirm // confirm ) const ( @@ -169,11 +169,11 @@ const ( const ( // Load Balancer wizard steps (offset by 1000) - LBWizardStepName WizardStep = iota + 1000 // enter name - LBWizardStepRegion // select region - LBWizardStepFlavor // select size/flavor - LBWizardStepNetwork // select private network (optional) - LBWizardStepConfirm // confirm + create + LBWizardStepName WizardStep = iota + 1000 // enter name + LBWizardStepRegion // select region + LBWizardStepFlavor // select size/flavor + LBWizardStepNetwork // select private network (optional) + LBWizardStepConfirm // confirm + create ) const ( @@ -222,35 +222,35 @@ const ( const ( // L7 Rule wizard steps (offset by 1600) - LBL7RuleWizardStepType WizardStep = iota + 1600 // select rule type - LBL7RuleWizardStepCompare // select comparison type - LBL7RuleWizardStepKey // enter key (optional) - LBL7RuleWizardStepValue // enter value - LBL7RuleWizardStepInvert // toggle invert - LBL7RuleWizardStepConfirm // confirm + create + LBL7RuleWizardStepType WizardStep = iota + 1600 // select rule type + LBL7RuleWizardStepCompare // select comparison type + LBL7RuleWizardStepKey // enter key (optional) + LBL7RuleWizardStepValue // enter value + LBL7RuleWizardStepInvert // toggle invert + LBL7RuleWizardStepConfirm // confirm + create ) const ( // LB Member wizard steps (offset by 1700) LBMemberWizardStepName WizardStep = iota + 1700 // enter member name - LBMemberWizardStepIP // enter IP address - LBMemberWizardStepPort // enter protocol port - LBMemberWizardStepWeight // enter weight - LBMemberWizardStepConfirm // confirm + save + LBMemberWizardStepIP // enter IP address + LBMemberWizardStepPort // enter protocol port + LBMemberWizardStepWeight // enter weight + LBMemberWizardStepConfirm // confirm + save ) const ( // LB Health Monitor wizard steps (offset by 1800) - LBHMWizardStepName WizardStep = iota + 1800 // enter name - LBHMWizardStepType // select monitor type - LBHMWizardStepHttpMethod // select HTTP method (http/https only) - LBHMWizardStepUrlPath // enter URL path (http/https only) - LBHMWizardStepExpectedCodes // enter expected HTTP codes (http/https only) - LBHMWizardStepDelay // enter delay (seconds) - LBHMWizardStepMaxRetries // enter max retries - LBHMWizardStepMaxRetriesDown // enter max retries down - LBHMWizardStepTimeout // enter timeout - LBHMWizardStepConfirm // confirm + save + LBHMWizardStepName WizardStep = iota + 1800 // enter name + LBHMWizardStepType // select monitor type + LBHMWizardStepHttpMethod // select HTTP method (http/https only) + LBHMWizardStepUrlPath // enter URL path (http/https only) + LBHMWizardStepExpectedCodes // enter expected HTTP codes (http/https only) + LBHMWizardStepDelay // enter delay (seconds) + LBHMWizardStepMaxRetries // enter max retries + LBHMWizardStepMaxRetriesDown // enter max retries down + LBHMWizardStepTimeout // enter timeout + LBHMWizardStepConfirm // confirm + save ) const ( @@ -302,19 +302,19 @@ const ( ProductNetworkGateway // Gateways (sub-nav) ProductNetworkLB // Load Balancers (sub-nav) ProductProjects - ProductCompute // Compute top-level nav - ProductInstanceBackup // Instance Backup (compute sub-nav) - ProductWorkflow // Workflow (compute sub-nav) + ProductCompute // Compute top-level nav + ProductInstanceBackup // Instance Backup (compute sub-nav) + ProductWorkflow // Workflow (compute sub-nav) ) // WizardData holds the state for the creation wizard type WizardData struct { step WizardStep - regions []map[string]interface{} - flavors []map[string]interface{} - images []map[string]interface{} - sshKeys []map[string]interface{} - privateNetworks []map[string]interface{} + regions []map[string]any + flavors []map[string]any + images []map[string]any + sshKeys []map[string]any + privateNetworks []map[string]any selectedIndex int // Current selection index in the list selectedRegion string // Selected region code selectedFlavor string // Selected flavor ID @@ -348,17 +348,17 @@ type WizardData struct { newNetworkDHCP bool // Enable DHCP for the subnet networkCreateField int // 0 = name, 1 = VLAN ID, 2 = CIDR, 3 = DHCP, 4 = Create/Cancel // Floating IP fields (for private network without public network) - floatingIPs []map[string]interface{} // Available floating IPs - selectedFloatingIP string // Selected floating IP ID (empty = none, "__create_new__" = create) - selectedFloatingIPAddress string // Selected floating IP address for display - createdInstanceId string // ID of the created instance (for floating IP attachment) - createdInstanceName string // Name of the created instance (for display) + floatingIPs []map[string]any // Available floating IPs + selectedFloatingIP string // Selected floating IP ID (empty = none, "__create_new__" = create) + selectedFloatingIPAddress string // Selected floating IP address for display + createdInstanceId string // ID of the created instance (for floating IP attachment) + createdInstanceName string // Name of the created instance (for display) // Filter for wizard lists filterMode bool // Whether filter input mode is active in wizard filterInput string // Current filter input text for wizard lists // Cleanup tracking - IDs of resources created during wizard - createdSSHKeyId string // ID of SSH key created during wizard - + createdSSHKeyId string // ID of SSH key created during wizard + createdNetworkId string // ID of network created during wizard createdSubnetId string // ID of subnet created during wizard createdGatewayId string // ID of gateway created during wizard @@ -367,45 +367,45 @@ type WizardData struct { cleanupPending bool // Whether we're waiting for cleanup confirmation cleanupError string // Error message that triggered cleanup prompt // Kubernetes wizard fields - kubeRegions []string // Available regions for K8s - kubeVersions []string // Available K8s versions - kubeNetworks []map[string]interface{} // Private networks - kubeSubnets []map[string]interface{} // Subnets for selected network - kubeLBSubnets []map[string]interface{} // Subnets for load balancers - selectedKubeRegion string // Selected region - selectedKubeVersion string // Selected K8s version - selectedKubeNetwork string // Selected private network ID - selectedKubeNetworkName string // Selected private network name - selectedNodesSubnet string // Selected nodes subnet ID - selectedNodesSubnetCIDR string // Selected nodes subnet CIDR - selectedLBSubnet string // Selected LB subnet ID (empty = same as nodes) - selectedLBSubnetCIDR string // Selected LB subnet CIDR - kubeName string // Cluster name - kubeNameInput string // Current input buffer for name - kubePlan string // "free" or "standard" - kubeUpdatePolicy string // Update policy - kubeProxyMode string // "iptables" or "ipvs" - kubePrivateRouting bool // Use private routing as default - kubeGatewayIP string // vRack gateway IP - kubeGatewayIPInput string // Current input for gateway IP - kubeOptionsFieldIndex int // Current field in options step (0-3: plan, policy, proxy, routing flag, 4: gateway IP, 5: buttons) - kubeConfirmButtonIndex int // 0 = Cancel, 1 = Create - kubeSubnetMenuIndex int // 0 = nodes subnet, 1 = LB subnet selection + kubeRegions []string // Available regions for K8s + kubeVersions []string // Available K8s versions + kubeNetworks []map[string]any // Private networks + kubeSubnets []map[string]any // Subnets for selected network + kubeLBSubnets []map[string]any // Subnets for load balancers + selectedKubeRegion string // Selected region + selectedKubeVersion string // Selected K8s version + selectedKubeNetwork string // Selected private network ID + selectedKubeNetworkName string // Selected private network name + selectedNodesSubnet string // Selected nodes subnet ID + selectedNodesSubnetCIDR string // Selected nodes subnet CIDR + selectedLBSubnet string // Selected LB subnet ID (empty = same as nodes) + selectedLBSubnetCIDR string // Selected LB subnet CIDR + kubeName string // Cluster name + kubeNameInput string // Current input buffer for name + kubePlan string // "free" or "standard" + kubeUpdatePolicy string // Update policy + kubeProxyMode string // "iptables" or "ipvs" + kubePrivateRouting bool // Use private routing as default + kubeGatewayIP string // vRack gateway IP + kubeGatewayIPInput string // Current input for gateway IP + kubeOptionsFieldIndex int // Current field in options step (0-3: plan, policy, proxy, routing flag, 4: gateway IP, 5: buttons) + kubeConfirmButtonIndex int // 0 = Cancel, 1 = Create + kubeSubnetMenuIndex int // 0 = nodes subnet, 1 = LB subnet selection // Node pool wizard fields - nodePoolClusterId string // Cluster ID to add node pool to - nodePoolFlavors []map[string]interface{} // Available flavors for node pool - nodePoolName string // Node pool name - nodePoolNameInput string // Input buffer for name - nodePoolFlavorName string // Selected flavor name - nodePoolDesiredNodes int // Desired number of nodes - nodePoolMinNodes int // Minimum nodes (for autoscale) - nodePoolMaxNodes int // Maximum nodes (for autoscale) - nodePoolAutoscale bool // Enable autoscaling - nodePoolAntiAffinity bool // Enable anti-affinity - nodePoolMonthlyBilled bool // Monthly billing - nodePoolSizeFieldIndex int // 0 = desired, 1 = min, 2 = max - nodePoolOptionsFieldIdx int // 0 = autoscale, 1 = anti-affinity, 2 = monthly - nodePoolConfirmBtnIdx int // 0 = Cancel, 1 = Create + nodePoolClusterId string // Cluster ID to add node pool to + nodePoolFlavors []map[string]any // Available flavors for node pool + nodePoolName string // Node pool name + nodePoolNameInput string // Input buffer for name + nodePoolFlavorName string // Selected flavor name + nodePoolDesiredNodes int // Desired number of nodes + nodePoolMinNodes int // Minimum nodes (for autoscale) + nodePoolMaxNodes int // Maximum nodes (for autoscale) + nodePoolAutoscale bool // Enable autoscaling + nodePoolAntiAffinity bool // Enable anti-affinity + nodePoolMonthlyBilled bool // Monthly billing + nodePoolSizeFieldIndex int // 0 = desired, 1 = min, 2 = max + nodePoolOptionsFieldIdx int // 0 = autoscale, 1 = anti-affinity, 2 = monthly + nodePoolConfirmBtnIdx int // 0 = Cancel, 1 = Create // Kube upgrade wizard fields kubeUpgradeClusterId string // Cluster ID for upgrade kubeUpgradeVersions []string // Available upgrade versions @@ -437,151 +437,151 @@ type WizardData struct { kubeKubeconfigEntries []string // Subdirectory names in current dir kubeKubeconfigSelectedIdx int // 0="..", 1="[Save here]", 2+= entries // Volume (Block Storage) wizard fields - volumeTypes []string // Available volume types for the selected region - volumeRegionTypeMap map[string][]string // region name -> []type names (pre-loaded) - volumeAvailabilityZones []string // Available availability zones for the region - volumeTypeAZMap map[string][]string // type name -> available AZs (for selected region) + volumeTypes []string // Available volume types for the selected region + volumeRegionTypeMap map[string][]string // region name -> []type names (pre-loaded) + volumeAvailabilityZones []string // Available availability zones for the region + volumeTypeAZMap map[string][]string // type name -> available AZs (for selected region) volumeRegionTypeAZMap map[string]map[string][]string // region -> type -> AZs (pre-loaded) - volumeName string // Volume name input - volumeNameInput string // Input buffer for volume name - volumeSize int // Volume size in GB - volumeSizeInput string // Input buffer for volume size - volumeType string // Selected volume type - volumeAvailabilityZone string // Selected availability zone - volumeEncryptionIdx int // 0=none, 1=OVHcloud Managed Key - volumeConfirmBtnIdx int // 0 = Create, 1 = Cancel + volumeName string // Volume name input + volumeNameInput string // Input buffer for volume name + volumeSize int // Volume size in GB + volumeSizeInput string // Input buffer for volume size + volumeType string // Selected volume type + volumeAvailabilityZone string // Selected availability zone + volumeEncryptionIdx int // 0=none, 1=OVHcloud Managed Key + volumeConfirmBtnIdx int // 0 = Create, 1 = Cancel // File Storage wizard fields - fileShareName string - fileShareNameInput string - fileShareSize int - fileShareSizeInput string - fileShareType string // selected type (e.g., "standard-1az") - fileShareTypeIdx int - fileShareRegions []string - fileShareNetworks []map[string]interface{} - fileShareSubnets []map[string]interface{} - fileShareNetworkId string - fileShareNetworkName string - fileShareSubnetId string - fileShareSubnetCIDR string - fileShareNetworkMenuIdx int // 0=network list, 1=subnet list - fileShareConfirmBtnIdx int // 0=Create, 1=Cancel + fileShareName string + fileShareNameInput string + fileShareSize int + fileShareSizeInput string + fileShareType string // selected type (e.g., "standard-1az") + fileShareTypeIdx int + fileShareRegions []string + fileShareNetworks []map[string]any + fileShareSubnets []map[string]any + fileShareNetworkId string + fileShareNetworkName string + fileShareSubnetId string + fileShareSubnetCIDR string + fileShareNetworkMenuIdx int // 0=network list, 1=subnet list + fileShareConfirmBtnIdx int // 0=Create, 1=Cancel // Object Storage wizard fields - objectName string // Container name + objectName string // Container name objectNameInput string - objectTypeIdx int // 0=Standard, 1=High Performance - objectRegions []string // Regions supporting S3 - objectUsers []map[string]interface{} // Cloud users + objectTypeIdx int // 0=Standard, 1=High Performance + objectRegions []string // Regions supporting S3 + objectUsers []map[string]any // Cloud users objectUserIdx int - objectReplication bool // Offsite replication enabled - objectVersioning bool // Versioning enabled - objectLock bool // Object Lock enabled - objectEncryption bool // Encryption enabled (AES256) - objectConfirmBtnIdx int // 0=Create, 1=Cancel - objectSwiftTypeIdx int // 0=Static, 1=Private, 2=Public + objectReplication bool // Offsite replication enabled + objectVersioning bool // Versioning enabled + objectLock bool // Object Lock enabled + objectEncryption bool // Encryption enabled (AES256) + objectConfirmBtnIdx int // 0=Create, 1=Cancel + objectSwiftTypeIdx int // 0=Static, 1=Private, 2=Public objectSwiftRegions []string // Available regions for Swift - objectSwiftRegion string // Selected Swift region + objectSwiftRegion string // Selected Swift region // S3 User wizard fields s3UserDescInput string // Description input buffer s3UserDesc string // Confirmed description s3UserConfirmBtnIdx int // 0=Create, 1=Cancel // Volume Backup / Snapshot wizard fields - backupVolumes []map[string]interface{} // loaded block storage volumes - backupVolumeIdx int // selected volume index - backupTypeIdx int // 0=Snapshot, 1=Backup - backupName string // confirmed name - backupNameInput string // input buffer for name - backupConfirmBtnIdx int // 0=Create, 1=Cancel + backupVolumes []map[string]any // loaded block storage volumes + backupVolumeIdx int // selected volume index + backupTypeIdx int // 0=Snapshot, 1=Backup + backupName string // confirmed name + backupNameInput string // input buffer for name + backupConfirmBtnIdx int // 0=Create, 1=Cancel // Private Network wizard fields - privNetRegions []map[string]interface{} // [{name, type}] - privNetRegionIdx int // selected region index - privNetNameInput string // network name input - privNetName string // confirmed name - privNetDefineVlan bool // whether user wants to set a VLAN ID - privNetVlanInput string // VLAN ID input ("" = auto) - privNetVlanID int // confirmed VLAN ID (0 = auto) - privNetEnableSubnet bool // whether to configure a subnet - privNetCIDRInput string // subnet CIDR input - privNetCIDR string // confirmed CIDR - privNetEnableDHCP bool // DHCP distribution enabled - privNetDHCPFieldIdx int // 0=toggle, 1=Next/Back - privNetAllocStart string // allocation pool start IP - privNetAllocEnd string // allocation pool end IP - privNetAllocField int // 0=start, 1=end - privNetGatewayMode int // 0=announce first CIDR IP, 1=assign explicit IP - privNetGatewayInput string // gateway IP input (mode 1) - privNetGateway string // confirmed gateway IP (mode 1) - privNetConfirmBtnIdx int // 0=Create, 1=Cancel - privNetIsLocalZone bool // true when selected region is a local zone - privNetUsedVlanIDs map[int]bool // VLAN IDs already in use (to validate before API call) - privNetAddSubnetMode bool // true when adding subnet to an existing network - privNetTargetNetworkID string // network ID to add subnet to (add-subnet mode) - privNetSubnettedRegions map[string]bool // regions that already have a subnet (add-subnet mode) + privNetRegions []map[string]any // [{name, type}] + privNetRegionIdx int // selected region index + privNetNameInput string // network name input + privNetName string // confirmed name + privNetDefineVlan bool // whether user wants to set a VLAN ID + privNetVlanInput string // VLAN ID input ("" = auto) + privNetVlanID int // confirmed VLAN ID (0 = auto) + privNetEnableSubnet bool // whether to configure a subnet + privNetCIDRInput string // subnet CIDR input + privNetCIDR string // confirmed CIDR + privNetEnableDHCP bool // DHCP distribution enabled + privNetDHCPFieldIdx int // 0=toggle, 1=Next/Back + privNetAllocStart string // allocation pool start IP + privNetAllocEnd string // allocation pool end IP + privNetAllocField int // 0=start, 1=end + privNetGatewayMode int // 0=announce first CIDR IP, 1=assign explicit IP + privNetGatewayInput string // gateway IP input (mode 1) + privNetGateway string // confirmed gateway IP (mode 1) + privNetConfirmBtnIdx int // 0=Create, 1=Cancel + privNetIsLocalZone bool // true when selected region is a local zone + privNetUsedVlanIDs map[int]bool // VLAN IDs already in use (to validate before API call) + privNetAddSubnetMode bool // true when adding subnet to an existing network + privNetTargetNetworkID string // network ID to add subnet to (add-subnet mode) + privNetSubnettedRegions map[string]bool // regions that already have a subnet (add-subnet mode) // Gateway wizard fields - gwNetworkID string - gwNetworkName string - gwRegion string - gwSubnetID string - gwModelIdx int // index into gatewayModels slice - gwNameInput string - gwName string - gwConfirmBtnIdx int // 0=Create, 1=Cancel - gwAvailableRegions []string // regions fetched from API - gwRegionIdx int // selected region index - gwAvailableNetworks []map[string]interface{} // networks in selected region - gwNetworkIdx int // selected network index + gwNetworkID string + gwNetworkName string + gwRegion string + gwSubnetID string + gwModelIdx int // index into gatewayModels slice + gwNameInput string + gwName string + gwConfirmBtnIdx int // 0=Create, 1=Cancel + gwAvailableRegions []string // regions fetched from API + gwRegionIdx int // selected region index + gwAvailableNetworks []map[string]any // networks in selected region + gwNetworkIdx int // selected network index // Attach mode: populated when launched from private network detail // maps region name -> {"openstackId": "...", "subnetId": "..."} - gwNetworkRegionMap map[string]map[string]string - gwAttachMode bool // true when wizard was launched from private network detail view + gwNetworkRegionMap map[string]map[string]string + gwAttachMode bool // true when wizard was launched from private network detail view // Load Balancer wizard fields - lbName string - lbNameInput string - lbRegion string - lbRegionIdx int - lbAvailableRegions []string - lbFlavors []map[string]interface{} - lbFlavorIdx int - lbFlavorId string - lbFlavorName string - lbNetworks []map[string]interface{} - lbNetworkIdx int // 0 = Aucun réseau, 1+ = index into lbNetworks - lbNetworkId string - lbNetworkName string - lbSubnetId string - lbConfirmBtnIdx int + lbName string + lbNameInput string + lbRegion string + lbRegionIdx int + lbAvailableRegions []string + lbFlavors []map[string]any + lbFlavorIdx int + lbFlavorId string + lbFlavorName string + lbNetworks []map[string]any + lbNetworkIdx int // 0 = Aucun réseau, 1+ = index into lbNetworks + lbNetworkId string + lbNetworkName string + lbSubnetId string + lbConfirmBtnIdx int // LB Pool wizard fields - lbPoolLBId string - lbPoolLBName string - lbPoolLBRegion string - lbPoolNameInput string - lbPoolName string - lbPoolAlgoIdx int - lbPoolAlgo string - lbPoolProtoIdx int - lbPoolProto string - lbPoolSessionIdx int // 0=None, 1=Source IP - lbPoolSession string // "" or "SOURCE_IP" - lbPoolConfirmIdx int - lbPoolEditPoolId string // non-empty = edit mode (pool ID being edited) + lbPoolLBId string + lbPoolLBName string + lbPoolLBRegion string + lbPoolNameInput string + lbPoolName string + lbPoolAlgoIdx int + lbPoolAlgo string + lbPoolProtoIdx int + lbPoolProto string + lbPoolSessionIdx int // 0=None, 1=Source IP + lbPoolSession string // "" or "SOURCE_IP" + lbPoolConfirmIdx int + lbPoolEditPoolId string // non-empty = edit mode (pool ID being edited) // LB Listener wizard fields - lbListenerLBId string - lbListenerLBName string - lbListenerLBRegion string - lbListenerNameInput string - lbListenerName string - lbListenerProtoIdx int - lbListenerProto string - lbListenerPortInput string - lbListenerPort int - lbListenerPoolIdx int // 0 = no pool, 1+ index into lbPools for this LB - lbListenerPoolId string - lbListenerConfirmIdx int - lbListenerEditId string // non-empty = edit mode (listener ID being edited) + lbListenerLBId string + lbListenerLBName string + lbListenerLBRegion string + lbListenerNameInput string + lbListenerName string + lbListenerProtoIdx int + lbListenerProto string + lbListenerPortInput string + lbListenerPort int + lbListenerPoolIdx int // 0 = no pool, 1+ index into lbPools for this LB + lbListenerPoolId string + lbListenerConfirmIdx int + lbListenerEditId string // non-empty = edit mode (listener ID being edited) // L7 Policy wizard fields l7PolicyListenerId string @@ -602,20 +602,20 @@ type WizardData struct { l7PolicyEditId string // non-empty = edit mode (policy ID being edited) // L7 Rule wizard fields - l7RulePolicyId string // policy ID the rule belongs to - l7RulePolicyName string // policy name (display) - l7RuleLBRegion string // region - l7RuleTypeIdx int // selected index in lbL7RuleTypeOptions - l7RuleType string // e.g. "HEADER", "PATH", "HOST_NAME", ... - l7RuleCompareIdx int // selected index in compare options for this type - l7RuleCompare string // e.g. "EQUAL_TO", "STARTS_WITH", "REGEX", ... - l7RuleKeyInput string // raw input for key field - l7RuleKey string // key (for HEADER / COOKIE type) - l7RuleValueInput string // raw input for value - l7RuleValue string // value to compare against - l7RuleInvert bool // whether to invert the rule - l7RuleConfirmIdx int // 0=Confirm, 1=Cancel - l7RuleEditId string // non-empty = edit mode (rule ID being edited) + l7RulePolicyId string // policy ID the rule belongs to + l7RulePolicyName string // policy name (display) + l7RuleLBRegion string // region + l7RuleTypeIdx int // selected index in lbL7RuleTypeOptions + l7RuleType string // e.g. "HEADER", "PATH", "HOST_NAME", ... + l7RuleCompareIdx int // selected index in compare options for this type + l7RuleCompare string // e.g. "EQUAL_TO", "STARTS_WITH", "REGEX", ... + l7RuleKeyInput string // raw input for key field + l7RuleKey string // key (for HEADER / COOKIE type) + l7RuleValueInput string // raw input for value + l7RuleValue string // value to compare against + l7RuleInvert bool // whether to invert the rule + l7RuleConfirmIdx int // 0=Confirm, 1=Cancel + l7RuleEditId string // non-empty = edit mode (rule ID being edited) // LB Member wizard fields lbMemberPoolId string // pool ID the member belongs to @@ -632,19 +632,19 @@ type WizardData struct { lbMemberConfirmIdx int // 0=Save, 1=Cancel // LB Health Monitor wizard fields - lbHMPoolId string // pool ID the monitor belongs to - lbHMPoolRegion string // region - lbHMEditId string // non-empty = edit mode (HM ID being edited) - lbHMNameInput string // raw input for name - lbHMName string // confirmed name - lbHMTypeIdx int // selected index in lbHMTypeOptions - lbHMType string // e.g. "http", "tcp", ... - lbHMDelayInput string // raw input for delay - lbHMDelay int // confirmed delay (seconds) - lbHMMaxRetriesInput string // raw input for max retries - lbHMMaxRetries int // confirmed max retries + lbHMPoolId string // pool ID the monitor belongs to + lbHMPoolRegion string // region + lbHMEditId string // non-empty = edit mode (HM ID being edited) + lbHMNameInput string // raw input for name + lbHMName string // confirmed name + lbHMTypeIdx int // selected index in lbHMTypeOptions + lbHMType string // e.g. "http", "tcp", ... + lbHMDelayInput string // raw input for delay + lbHMDelay int // confirmed delay (seconds) + lbHMMaxRetriesInput string // raw input for max retries + lbHMMaxRetries int // confirmed max retries lbHMMaxRetriesDownInput string // raw input for max retries down - lbHMMaxRetriesDown int // confirmed max retries down + lbHMMaxRetriesDown int // confirmed max retries down lbHMTimeoutInput string // raw input for timeout lbHMTimeout int // confirmed timeout (seconds) lbHMHttpMethodIdx int // selected index in lbHMHttpMethodOptions @@ -656,55 +656,55 @@ type WizardData struct { lbHMConfirmIdx int // 0=Save, 1=Cancel // Floating IP wizard fields - fipRegion string - fipRegionIdx int - fipAvailableRegions []string - fipInstances []map[string]interface{} - fipInstanceIdx int // 0 = standalone (no instance), 1+ = index into fipInstances - fipInstanceId string - fipInstanceName string - fipConfirmBtnIdx int + fipRegion string + fipRegionIdx int + fipAvailableRegions []string + fipInstances []map[string]any + fipInstanceIdx int // 0 = standalone (no instance), 1+ = index into fipInstances + fipInstanceId string + fipInstanceName string + fipConfirmBtnIdx int // Workflow wizard fields - wfInstances []map[string]interface{} - wfInstanceIdx int - wfInstanceId string - wfInstanceName string - wfRegion string - wfName string - wfNameInput string - wfScheduleIdx int // 0=rotation7, 1=rotation14, 2=custom - wfCron string - wfCronInput string - wfRotation int - wfConfirmBtnIdx int + wfInstances []map[string]any + wfInstanceIdx int + wfInstanceId string + wfInstanceName string + wfRegion string + wfName string + wfNameInput string + wfScheduleIdx int // 0=rotation7, 1=rotation14, 2=custom + wfCron string + wfCronInput string + wfRotation int + wfConfirmBtnIdx int // Managed Database wizard fields - dbNameInput string - dbName string - dbEngines []map[string]interface{} // from capabilities - dbEngineIdx int - dbEngine string - dbEngineCategory string // "operational" or "analysis" - dbVersionIdx int - dbVersion string - dbRegionIdx int - dbRegion string - dbPlanIdx int - dbPlan string - dbFlavors []map[string]interface{} // from capabilities - dbFlavorIdx int - dbFlavor string - dbCapPlans []string // fallback plan names from capabilities - dbNodesInput string - dbNodes int - dbStorageInput string - dbDiskSize int - dbNetworkIdx int // 0=public 1=private - dbNetworkId string - dbAvailItems []map[string]interface{} // from /database/availability - dbCapsRegions []string // fallback regions from capabilities - dbConfirmIdx int // 0=Create 1=Cancel + dbNameInput string + dbName string + dbEngines []map[string]any // from capabilities + dbEngineIdx int + dbEngine string + dbEngineCategory string // "operational" or "analysis" + dbVersionIdx int + dbVersion string + dbRegionIdx int + dbRegion string + dbPlanIdx int + dbPlan string + dbFlavors []map[string]any // from capabilities + dbFlavorIdx int + dbFlavor string + dbCapPlans []string // fallback plan names from capabilities + dbNodesInput string + dbNodes int + dbStorageInput string + dbDiskSize int + dbNetworkIdx int // 0=public 1=private + dbNetworkId string + dbAvailItems []map[string]any // from /database/availability + dbCapsRegions []string // fallback regions from capabilities + dbConfirmIdx int // 0=Create 1=Cancel } // Model represents the TUI application state @@ -723,69 +723,69 @@ type Model struct { inComputeSubNav bool // Whether the keyboard focus is in the compute sub-nav bar inTableFocus bool // Whether the keyboard focus is in the table content (third navigation level) table table.Model - detailData map[string]interface{} - currentData []map[string]interface{} + detailData map[string]any + currentData []map[string]any errorMsg string cloudProject string - cloudProjectName string // Display name of the selected project - currentItemName string // Name of the currently viewed item - notification string // Temporary notification message - notificationExpiry time.Time // When the notification should disappear - projectsList []map[string]interface{} // Cache of projects for selection - wizard WizardData // Wizard state for resource creation - selectedAction int // Selected action index in detail view (0-5) - actionConfirm bool // Whether we're in confirmation mode for an action + cloudProjectName string // Display name of the selected project + currentItemName string // Name of the currently viewed item + notification string // Temporary notification message + notificationExpiry time.Time // When the notification should disappear + projectsList []map[string]any // Cache of projects for selection + wizard WizardData // Wizard state for resource creation + selectedAction int // Selected action index in detail view (0-5) + actionConfirm bool // Whether we're in confirmation mode for an action // Filter mode filterMode bool // Whether filter input mode is active filterInput string // Current filter input text // Object Storage tab rendering renderObjectStorageTabs bool // Delete confirmation - deleteTarget map[string]interface{} // Item to be deleted - deleteConfirmInput string // User input for delete confirmation + deleteTarget map[string]any // Item to be deleted + deleteConfirmInput string // User input for delete confirmation // Debug view debugScrollOffset int // Scroll offset for debug log view // Instance data cache imageMap map[string]string // imageId -> imageName (for instances) floatingIPMap map[string]string // instanceId -> floatingIP address // Kubernetes data cache - kubeNodePools map[string][]map[string]interface{} // kubeId -> list of node pools - nodePoolsSelectedIdx int // Selected index in node pools view - selectedNodePool map[string]interface{} // Currently selected node pool for detail view - nodePoolDetailActionIdx int // Selected action index in node pool detail view - nodePoolDetailConfirm bool // Whether we're in confirmation mode + kubeNodePools map[string][]map[string]any // kubeId -> list of node pools + nodePoolsSelectedIdx int // Selected index in node pools view + selectedNodePool map[string]any // Currently selected node pool for detail view + nodePoolDetailActionIdx int // Selected action index in node pool detail view + nodePoolDetailConfirm bool // Whether we're in confirmation mode // LB pools cache (lbId -> pools) - lbPools map[string][]map[string]interface{} - selectedLBPool map[string]interface{} // Currently selected pool for detail view - lbPoolDetailActionIdx int // Selected action in pool detail view (0=Edit, 1=Delete, 2=Members) - lbPoolDetailConfirm bool // Whether we're in confirm mode in pool detail - lbPoolListIdx int // Highlighted pool row in LB detail (-1 = none) + lbPools map[string][]map[string]any + selectedLBPool map[string]any // Currently selected pool for detail view + lbPoolDetailActionIdx int // Selected action in pool detail view (0=Edit, 1=Delete, 2=Members) + lbPoolDetailConfirm bool // Whether we're in confirm mode in pool detail + lbPoolListIdx int // Highlighted pool row in LB detail (-1 = none) // LB pool members cache (poolId -> members) - lbPoolMembers map[string][]map[string]interface{} // poolID → members - lbPoolMemberDetailIdx int // Currently displayed member index - lbPoolMemberConfirm bool // Confirm mode for member deletion - lbMembersSection int // 0=actions bar, 1=member pagination - lbMembersActionIdx int // Selected action: 0=Create,1=Edit,2=Delete,3=HealthMonitor + lbPoolMembers map[string][]map[string]any // poolID → members + lbPoolMemberDetailIdx int // Currently displayed member index + lbPoolMemberConfirm bool // Confirm mode for member deletion + lbMembersSection int // 0=actions bar, 1=member pagination + lbMembersActionIdx int // Selected action: 0=Create,1=Edit,2=Delete,3=HealthMonitor // LB health monitors cache (poolId -> health monitor) - lbHealthMonitors map[string]map[string]interface{} // poolID → health monitor (one per pool) - lbHMConfirm bool // Confirm mode for HM deletion - lbHMActionIdx int // Selected action button in HM view (0=Create or Edit, 1=Delete) + lbHealthMonitors map[string]map[string]any // poolID → health monitor (one per pool) + lbHMConfirm bool // Confirm mode for HM deletion + lbHMActionIdx int // Selected action button in HM view (0=Create or Edit, 1=Delete) // LB listeners cache (lbId -> listeners) - lbListeners map[string][]map[string]interface{} - lbListenerListIdx int // Highlighted listener row in LB detail (-1 = none) - selectedLBListener map[string]interface{} // Currently selected listener for detail view - lbListenerDetailActionIdx int // Selected action in listener detail view (0=Edit, 1=Delete) - lbListenerDetailConfirm bool // Confirm mode in listener detail - lbDetailSection int // 0=Listeners block focused, 1=Pools block focused - lbL7Policies map[string][]map[string]interface{} // key = listenerId - lbL7Rules map[string][]map[string]interface{} // key = policyId - lbL7PolicyListIdx int // Highlighted policy row in listener detail (-1 = none) - selectedLBL7Policy map[string]interface{} // Currently selected policy for detail view - lbL7PolicyDetailActionIdx int // Selected action in policy detail (0=Edit, 1=Delete) - lbL7PolicyDetailConfirm bool // Confirm mode in policy detail - lbL7RuleDetailIdx int // Currently displayed rule index in L7 Rules view - lbL7RuleActionIdx int // Selected action button in L7 Rules view (0=Create,1=Edit,2=Delete) - lbL7RuleConfirm bool // Confirm mode for rule deletion + lbListeners map[string][]map[string]any + lbListenerListIdx int // Highlighted listener row in LB detail (-1 = none) + selectedLBListener map[string]any // Currently selected listener for detail view + lbListenerDetailActionIdx int // Selected action in listener detail view (0=Edit, 1=Delete) + lbListenerDetailConfirm bool // Confirm mode in listener detail + lbDetailSection int // 0=Listeners block focused, 1=Pools block focused + lbL7Policies map[string][]map[string]any // key = listenerId + lbL7Rules map[string][]map[string]any // key = policyId + lbL7PolicyListIdx int // Highlighted policy row in listener detail (-1 = none) + selectedLBL7Policy map[string]any // Currently selected policy for detail view + lbL7PolicyDetailActionIdx int // Selected action in policy detail (0=Edit, 1=Delete) + lbL7PolicyDetailConfirm bool // Confirm mode in policy detail + lbL7RuleDetailIdx int // Currently displayed rule index in L7 Rules view + lbL7RuleActionIdx int // Selected action button in L7 Rules view (0=Create,1=Edit,2=Delete) + lbL7RuleConfirm bool // Confirm mode for rule deletion // Background detail-view refresh (set by auto-refresh timer, cleared by data handlers) detailRefreshId string detailRefreshName string @@ -803,29 +803,29 @@ type Model struct { objectUserDetailView *object_storage.UserDetailView // Object Storage tabs (0=Containers, 1=Users) objectStorageTabIdx int - objectStorageUsers []map[string]interface{} + objectStorageUsers []map[string]any // Managed DB/Analytics detail sub-resources - dbDetailUsers []map[string]interface{} - dbDetailBackups []map[string]interface{} - dbDetailDatabases []map[string]interface{} - dbDetailPools []map[string]interface{} - dbDetailLoaded bool // true once fetchDBDetailSubresources has returned - dbDetailTab int // 0=Service, 1=Users, 2=Backups, 3=Databases, 4=Pools, 5=Logs, 6=ACL - dbDetailLogs []map[string]interface{} // last fetched log entries - dbLogsLoaded bool // true once fetchDBLogs has returned - dbLogsUnsupported bool // true when the engine does not expose a /logs endpoint - dbLogsScrollOffset int // scroll offset for logs tab (0 = bottom/newest) - dbDetailACL []map[string]interface{} // ACL entries (kafka only) - dbACLLoaded bool // true once fetchDBACL has returned + dbDetailUsers []map[string]any + dbDetailBackups []map[string]any + dbDetailDatabases []map[string]any + dbDetailPools []map[string]any + dbDetailLoaded bool // true once fetchDBDetailSubresources has returned + dbDetailTab int // 0=Service, 1=Users, 2=Backups, 3=Databases, 4=Pools, 5=Logs, 6=ACL + dbDetailLogs []map[string]any // last fetched log entries + dbLogsLoaded bool // true once fetchDBLogs has returned + dbLogsUnsupported bool // true when the engine does not expose a /logs endpoint + dbLogsScrollOffset int // scroll offset for logs tab (0 = bottom/newest) + dbDetailACL []map[string]any // ACL entries (kafka only) + dbACLLoaded bool // true once fetchDBACL has returned // ACL creation state (ACL tab) — multi-step inline wizard - dbACLCreateStep int // -1=inactive, 0=username, 1=topic, 2=permission + dbACLCreateStep int // -1=inactive, 0=username, 1=topic, 2=permission dbACLCreateUser string dbACLCreateTopic string - dbACLCreatePerm int // 0=read 1=write 2=admin - dbDetailTopics []map[string]interface{} // Topics (kafka only) - dbTopicsLoaded bool // true once fetchDBTopics has returned + dbACLCreatePerm int // 0=read 1=write 2=admin + dbDetailTopics []map[string]any // Topics (kafka only) + dbTopicsLoaded bool // true once fetchDBTopics has returned // Topic creation state (Topics tab) — multi-step inline wizard - dbTopicCreateStep int // -1=inactive, 0=name..5=retentionHours + dbTopicCreateStep int // -1=inactive, 0=name..5=retentionHours dbTopicCreateName string dbTopicCreateMinSync string // minInsyncReplicas dbTopicCreatePartitions string @@ -833,45 +833,45 @@ type Model struct { dbTopicCreateRetentionByt string // bytes (-1 = unlimited) dbTopicCreateRetentionHrs string // hours (-1 = unlimited) // Metrics tab (tab 8) - dbMetricNames []string // list of available metric names - dbMetricNamesLoaded bool - dbMetricSelectedIdx int // selected metric in the list - dbMetricPeriodIdx int // 0=lastHour 1=lastDay - dbNodeNames []string // ordered node names for legend labels - dbMetricSeries []dbMetricSeries // current chart data (one per API series) + dbMetricNames []string // list of available metric names + dbMetricNamesLoaded bool + dbMetricSelectedIdx int // selected metric in the list + dbMetricPeriodIdx int // 0=lastHour 1=lastDay + dbNodeNames []string // ordered node names for legend labels + dbMetricSeries []dbMetricSeries // current chart data (one per API series) dbMetricLoaded bool - dbMetricLoading bool // true while a fetch is in flight - dbMetricName string // currently displayed metric name - dbMetricSeriesHidden map[int]bool // set of series indices the user has toggled off - dbUserCreateMode bool // true when typing a new username - dbUserCreateInput string // username being typed - dbUserCreatedData map[string]interface{} // creation result (has password + endpoints) - dbUserSelectedIdx int // currently highlighted user row (-1 = none) - dbUserDeleteConfirm bool // waiting for second Enter to confirm delete + dbMetricLoading bool // true while a fetch is in flight + dbMetricName string // currently displayed metric name + dbMetricSeriesHidden map[int]bool // set of series indices the user has toggled off + dbUserCreateMode bool // true when typing a new username + dbUserCreateInput string // username being typed + dbUserCreatedData map[string]any // creation result (has password + endpoints) + dbUserSelectedIdx int // currently highlighted user row (-1 = none) + dbUserDeleteConfirm bool // waiting for second Enter to confirm delete // DB database state (Databases tab) dbDBCreateMode bool // true when typing a new database name dbDBCreateInput string // database name being typed // DB connection pool creation state (Pools tab) — multi-step inline wizard - dbPoolCreateStep int // 0=name, 1=database, 2=mode, 3=size (-1 = not active) + dbPoolCreateStep int // 0=name, 1=database, 2=mode, 3=size (-1 = not active) dbPoolCreateName string dbPoolCreateDBIdx int // index into dbDetailDatabases dbPoolCreateMode int // 0=session 1=statement 2=transaction dbPoolCreateSize string // numeric input // Private Networks tabs (0=Régions vRack, 1=Local Zones) - privNetTabIdx int - privNetLocalZones []map[string]interface{} - privNetSelectedSubnet int // index of subnet selected for deletion in detail view - privNetSelectedRegion int // index of region selected for deletion in detail view + privNetTabIdx int + privNetLocalZones []map[string]any + privNetSelectedSubnet int // index of subnet selected for deletion in detail view + privNetSelectedRegion int // index of region selected for deletion in detail view // Public IPs tabs (0=Floating IPs, 1=Additional IPs) - publicIPTabIdx int - additionalIPsData []map[string]interface{} + publicIPTabIdx int + additionalIPsData []map[string]any // S3 user creation result (for credentials display) - s3CreatedUser map[string]interface{} - s3CreatedCredentials map[string]interface{} + s3CreatedUser map[string]any + s3CreatedCredentials map[string]any s3CredentialsSavedPath string s3CredentialsSaveError string - s3PendingEnableUser map[string]interface{} // user being enabled (for credentials display) - s3CredentialsFromEnable bool // true if S3CredentialsView opened from enable action + s3PendingEnableUser map[string]any // user being enabled (for credentials display) + s3CredentialsFromEnable bool // true if S3CredentialsView opened from enable action } // Navigation items for the top bar @@ -952,13 +952,13 @@ var ( // Messages for async operations type projectsLoadedMsg struct { - projects []map[string]interface{} + projects []map[string]any err error forProduct ProductType // The product that requested this data } type instancesLoadedMsg struct { - instances []map[string]interface{} + instances []map[string]any imageMap map[string]string // imageId -> imageName floatingIPMap map[string]string // instanceId -> floatingIP address err error @@ -972,11 +972,11 @@ type instancesEnrichedMsg struct { } type dataLoadedMsg struct { - data []map[string]interface{} + data []map[string]any err error - forProduct ProductType // The product that requested this data - s3Users []map[string]interface{} // S3 users (for Object Storage) - additionalIPs []map[string]interface{} // Failover IPs (for ProductNetworkPublic tab 1) + forProduct ProductType // The product that requested this data + s3Users []map[string]any // S3 users (for Object Storage) + additionalIPs []map[string]any // Failover IPs (for ProductNetworkPublic tab 1) } // setDefaultProjectMsg is returned after setting the default project @@ -997,48 +997,48 @@ type refreshTickMsg struct{} // Wizard-related messages type regionsLoadedMsg struct { - regions []map[string]interface{} - images []map[string]interface{} + regions []map[string]any + images []map[string]any err error } type flavorsLoadedMsg struct { - flavors []map[string]interface{} + flavors []map[string]any err error } type imagesLoadedMsg struct { - images []map[string]interface{} + images []map[string]any err error } type sshKeysLoadedMsg struct { - sshKeys []map[string]interface{} + sshKeys []map[string]any err error } type sshKeyCreatedMsg struct { - sshKey map[string]interface{} + sshKey map[string]any err error } type privateNetworksLoadedMsg struct { - networks []map[string]interface{} + networks []map[string]any err error } type floatingIPsLoadedMsg struct { - floatingIPs []map[string]interface{} + floatingIPs []map[string]any err error } type gatewayCreatedMsg struct { - gateway map[string]interface{} + gateway map[string]any err error } type floatingIPCreatedMsg struct { - floatingIP map[string]interface{} + floatingIP map[string]any err error } @@ -1056,19 +1056,19 @@ type instanceIPReadyMsg struct { // Network creation step messages type networkStepMsg struct { - step string // "network_created", "creating_subnet", "subnet_created" - networkId string // Network ID for subsequent steps - network map[string]interface{} // Network data + step string // "network_created", "creating_subnet", "subnet_created" + networkId string // Network ID for subsequent steps + network map[string]any // Network data err error } type networkCreatedMsg struct { - network map[string]interface{} + network map[string]any err error } type instanceCreatedMsg struct { - instance map[string]interface{} + instance map[string]any err error } @@ -1114,31 +1114,31 @@ type kubeVersionsLoadedMsg struct { } type kubeNetworksLoadedMsg struct { - networks []map[string]interface{} + networks []map[string]any err error } type kubeSubnetsLoadedMsg struct { - subnets []map[string]interface{} + subnets []map[string]any err error } type kubeClusterCreatedMsg struct { - cluster map[string]interface{} + cluster map[string]any err error } type kubeNodePoolsLoadedMsg struct { kubeId string - nodePools []map[string]interface{} + nodePools []map[string]any err error } type volumeRegionsLoadedMsg struct { - regionNames []string - regionTypeMap map[string][]string - regionTypeAZMap map[string]map[string][]string // region -> type -> []AZs - err error + regionNames []string + regionTypeMap map[string][]string + regionTypeAZMap map[string]map[string][]string // region -> type -> []AZs + err error } type volumeTypesLoadedMsg struct { @@ -1153,7 +1153,7 @@ type volumeAZLoadedMsg struct { } type volumeCreatedMsg struct { - volume map[string]interface{} + volume map[string]any err error } @@ -1170,29 +1170,29 @@ type fileShareRegionsLoadedMsg struct { } type fileShareNetworksLoadedMsg struct { - networks []map[string]interface{} + networks []map[string]any err error } type fileShareSubnetsLoadedMsg struct { - subnets []map[string]interface{} + subnets []map[string]any err error } type fileShareCreatedMsg struct { - share map[string]interface{} + share map[string]any err error } type objectStorageInitDataLoadedMsg struct { - regions []string - users []map[string]interface{} - err error + regions []string + users []map[string]any + err error } type objectContainerCreatedMsg struct { - container map[string]interface{} - err error + container map[string]any + err error } type objectContainerActionDoneMsg struct { @@ -1219,13 +1219,13 @@ type s3SecretLoadedMsg struct { type s3UserActionDoneMsg struct { action int - newCredential map[string]interface{} + newCredential map[string]any err error } type s3UserCreatedMsg struct { - user map[string]interface{} - credentials map[string]interface{} + user map[string]any + credentials map[string]any err error } @@ -1240,12 +1240,12 @@ type swiftRegionsLoadedMsg struct { } type privNetRegionsLoadedMsg struct { - regions []map[string]interface{} + regions []map[string]any err error } type privNetCreatedMsg struct { - network map[string]interface{} + network map[string]any err error } @@ -1276,7 +1276,7 @@ type privNetDeletedMsg struct { } type gwCreatedMsg struct { - gateway map[string]interface{} + gateway map[string]any err error } @@ -1286,7 +1286,7 @@ type gwRegionsLoadedMsg struct { } type gwNetworksLoadedMsg struct { - networks []map[string]interface{} + networks []map[string]any err error } @@ -1301,7 +1301,7 @@ type gwDeletedMsg struct { } type lbCreatedMsg struct { - lb map[string]interface{} + lb map[string]any err error } @@ -1311,10 +1311,10 @@ type lbRegionsLoadedMsg struct { } type dbCapabilitiesLoadedMsg struct { - engines []map[string]interface{} - flavors []map[string]interface{} - plans []map[string]interface{} - availItems []map[string]interface{} + engines []map[string]any + flavors []map[string]any + plans []map[string]any + availItems []map[string]any capsRegions []string err error } @@ -1331,10 +1331,10 @@ type analyticsCreatedMsg struct { type dbDetailSubresourcesMsg struct { serviceId string - users []map[string]interface{} - backups []map[string]interface{} - databases []map[string]interface{} - pools []map[string]interface{} + users []map[string]any + backups []map[string]any + databases []map[string]any + pools []map[string]any err error } @@ -1344,18 +1344,18 @@ type dbServiceDeletedMsg struct { } type analyticsEngineAvailLoadedMsg struct { - engine string - availItems []map[string]interface{} - err error + engine string + availItems []map[string]any + err error } type lbFlavorsLoadedMsg struct { - flavors []map[string]interface{} + flavors []map[string]any err error } type lbNetworksLoadedMsg struct { - networks []map[string]interface{} + networks []map[string]any err error } @@ -1376,7 +1376,7 @@ type lbPoolCreatedMsg struct { type lbPoolsLoadedMsg struct { lbID string - pools []map[string]interface{} + pools []map[string]any err error } @@ -1407,7 +1407,7 @@ type lbListenerUpdatedMsg struct { type lbListenersLoadedMsg struct { lbID string - listeners []map[string]interface{} + listeners []map[string]any err error } @@ -1418,13 +1418,13 @@ type lbL7PolicyCreatedMsg struct { type lbL7PoliciesLoadedMsg struct { listenerID string - policies []map[string]interface{} + policies []map[string]any err error } type lbL7RulesLoadedMsg struct { policyID string - rules []map[string]interface{} + rules []map[string]any err error } @@ -1450,7 +1450,7 @@ type lbL7PolicyUpdatedMsg struct { type lbPoolMembersLoadedMsg struct { poolID string - members []map[string]interface{} + members []map[string]any err error } @@ -1466,7 +1466,7 @@ type lbPoolMemberSavedMsg struct { type lbHMLoadedMsg struct { poolID string - hm map[string]interface{} // nil if not found + hm map[string]any // nil if not found err error } @@ -1486,12 +1486,12 @@ type fipRegionsLoadedMsg struct { } type fipInstancesLoadedMsg struct { - instances []map[string]interface{} + instances []map[string]any err error } type fipCreatedMsg struct { - floatingIP map[string]interface{} + floatingIP map[string]any err error } @@ -1522,7 +1522,7 @@ type subnetsLoadedMsg struct { type privNetDetailLoadedMsg struct { networkID string - regions []interface{} + regions []any } func getNavItems() []NavItem { @@ -1750,14 +1750,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } m.wizard = WizardData{ - step: PrivNetWizardStepRegion, - privNetEnableDHCP: true, + step: PrivNetWizardStepRegion, + privNetEnableDHCP: true, privNetEnableSubnet: true, - privNetGatewayMode: 0, - privNetCIDRInput: "10.0.0.0/16", - privNetUsedVlanIDs: usedVlans, - isLoading: true, - loadingMessage: "Loading regions...", + privNetGatewayMode: 0, + privNetCIDRInput: "10.0.0.0/16", + privNetUsedVlanIDs: usedVlans, + isLoading: true, + loadingMessage: "Loading regions...", } return m, m.fetchPrivateNetRegionsCmd() } else if msg.product == ProductNetworkGateway { @@ -1977,7 +1977,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // In add-subnet mode, filter out regions that already have a subnet if m.wizard.privNetAddSubnetMode && len(m.wizard.privNetSubnettedRegions) > 0 { - var filtered []map[string]interface{} + var filtered []map[string]any for _, r := range msg.regions { name, _ := r["name"].(string) if !m.wizard.privNetSubnettedRegions[name] { @@ -2286,7 +2286,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case lbPoolsLoadedMsg: if msg.err == nil { if m.lbPools == nil { - m.lbPools = make(map[string][]map[string]interface{}) + m.lbPools = make(map[string][]map[string]any) } m.lbPools[msg.lbID] = msg.pools } @@ -2365,7 +2365,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case lbListenersLoadedMsg: if msg.err == nil { if m.lbListeners == nil { - m.lbListeners = make(map[string][]map[string]interface{}) + m.lbListeners = make(map[string][]map[string]any) } m.lbListeners[msg.lbID] = msg.listeners } @@ -2444,7 +2444,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case lbL7PoliciesLoadedMsg: if msg.err == nil { if m.lbL7Policies == nil { - m.lbL7Policies = make(map[string][]map[string]interface{}) + m.lbL7Policies = make(map[string][]map[string]any) } m.lbL7Policies[msg.listenerID] = msg.policies } @@ -2453,7 +2453,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case lbL7RulesLoadedMsg: if msg.err == nil { if m.lbL7Rules == nil { - m.lbL7Rules = make(map[string][]map[string]interface{}) + m.lbL7Rules = make(map[string][]map[string]any) } m.lbL7Rules[msg.policyID] = msg.rules } @@ -2552,7 +2552,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case lbPoolMembersLoadedMsg: if msg.err == nil { if m.lbPoolMembers == nil { - m.lbPoolMembers = make(map[string][]map[string]interface{}) + m.lbPoolMembers = make(map[string][]map[string]any) } m.lbPoolMembers[msg.poolID] = msg.members } @@ -2602,7 +2602,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case lbHMLoadedMsg: if msg.err == nil { if m.lbHealthMonitors == nil { - m.lbHealthMonitors = make(map[string]map[string]interface{}) + m.lbHealthMonitors = make(map[string]map[string]any) } m.lbHealthMonitors[msg.poolID] = msg.hm // can be nil (no HM) } @@ -2930,18 +2930,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case dbMetricDataMsg: - m.dbMetricLoading = false - // Discard stale responses from a previous metric/period selection - if msg.name != m.dbMetricName { - return m, nil - } - m.dbMetricLoaded = true - if msg.err != nil { - m.notification = fmt.Sprintf("❌ Metric error: %s", msg.err.Error()) - m.notificationExpiry = time.Now().Add(6 * time.Second) - return m, tea.Tick(6*time.Second, func(t time.Time) tea.Msg { return clearNotificationMsg{} }) - } - m.dbMetricSeries = msg.series + m.dbMetricLoading = false + // Discard stale responses from a previous metric/period selection + if msg.name != m.dbMetricName { + return m, nil + } + m.dbMetricLoaded = true + if msg.err != nil { + m.notification = fmt.Sprintf("❌ Metric error: %s", msg.err.Error()) + m.notificationExpiry = time.Now().Add(6 * time.Second) + return m, tea.Tick(6*time.Second, func(t time.Time) tea.Msg { return clearNotificationMsg{} }) + } + m.dbMetricSeries = msg.series case dbCreatedMsg: m.wizard.isLoading = false @@ -3009,7 +3009,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { "small": 1, "s": 1, "medium": 2, "m": 2, "large": 3, "l": 3, - "xl": 4, + "xl": 4, "2xl": 5, "xxl": 5, "3xl": 6, } @@ -3518,7 +3518,7 @@ func (m Model) handleS3UserActionDone(msg s3UserActionDoneMsg) (tea.Model, tea.C username = u } } - m.s3CreatedUser = map[string]interface{}{"username": username} + m.s3CreatedUser = map[string]any{"username": username} m.s3CreatedCredentials = msg.newCredential m.s3CredentialsSavedPath = "" m.s3CredentialsSaveError = "" @@ -3616,10 +3616,7 @@ func (m Model) View() string { var content strings.Builder // Calculate available width - width := m.width - if width < 80 { - width = 80 - } + width := max(m.width, 80) // Header with logo content.WriteString(m.renderHeader()) @@ -4173,14 +4170,8 @@ func (m Model) renderDebugView(width int) string { // Calculate visible entries based on scroll offset maxVisible := 15 // Show last 15 entries by default - startIdx := len(entries) - maxVisible - m.debugScrollOffset - if startIdx < 0 { - startIdx = 0 - } - endIdx := startIdx + maxVisible - if endIdx > len(entries) { - endIdx = len(entries) - } + startIdx := max(len(entries)-maxVisible-m.debugScrollOffset, 0) + endIdx := min(startIdx+maxVisible, len(entries)) // Show entries in reverse order (newest first) for i := endIdx - 1; i >= startIdx; i-- { @@ -4194,10 +4185,7 @@ func (m Model) renderDebugView(width int) string { // Format URL (truncate if too long) url := entry.URL - maxUrlLen := width - 60 - if maxUrlLen < 20 { - maxUrlLen = 20 - } + maxUrlLen := max(width-60, 20) if len(url) > maxUrlLen { url = url[:maxUrlLen-3] + "..." } @@ -5137,23 +5125,23 @@ func (m Model) renderWizardView(width int) string { case FileWizardStepConfirm: content.WriteString(m.renderFileWizardConfirmStep(width)) case ObjectWizardStepName: - content.WriteString(m.renderObjectWizardNameStep(width)) + content.WriteString(m.renderObjectWizardNameStep(width)) case ObjectWizardStepType: - content.WriteString(m.renderObjectWizardTypeStep(width)) + content.WriteString(m.renderObjectWizardTypeStep(width)) case ObjectWizardStepRegion: - content.WriteString(m.renderObjectWizardRegionStep(width)) + content.WriteString(m.renderObjectWizardRegionStep(width)) case ObjectWizardStepReplication: - content.WriteString(m.renderObjectWizardReplicationStep(width)) + content.WriteString(m.renderObjectWizardReplicationStep(width)) case ObjectWizardStepVersioning: - content.WriteString(m.renderObjectWizardVersioningStep(width)) + content.WriteString(m.renderObjectWizardVersioningStep(width)) case ObjectWizardStepObjectLock: - content.WriteString(m.renderObjectWizardObjectLockStep(width)) + content.WriteString(m.renderObjectWizardObjectLockStep(width)) case ObjectWizardStepUser: - content.WriteString(m.renderObjectWizardUserStep(width)) + content.WriteString(m.renderObjectWizardUserStep(width)) case ObjectWizardStepEncryption: - content.WriteString(m.renderObjectWizardEncryptionStep(width)) + content.WriteString(m.renderObjectWizardEncryptionStep(width)) case ObjectWizardStepConfirm: - content.WriteString(m.renderObjectWizardConfirmStep(width)) + content.WriteString(m.renderObjectWizardConfirmStep(width)) case ObjectWizardStepSwiftType: content.WriteString(m.renderObjectWizardSwiftTypeStep(width)) case ObjectWizardStepSwiftRegion: @@ -5377,10 +5365,7 @@ func (m Model) renderWizardRegionStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(filtered) { - endIdx = len(filtered) - } + endIdx := min(startIdx+maxVisible, len(filtered)) for i := startIdx; i < endIdx; i++ { region := filtered[i] @@ -5434,10 +5419,7 @@ func (m Model) renderWizardFlavorStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(filtered) { - endIdx = len(filtered) - } + endIdx := min(startIdx+maxVisible, len(filtered)) for i := startIdx; i < endIdx; i++ { flavor := filtered[i] @@ -5507,10 +5489,7 @@ func (m Model) renderWizardImageStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(filtered) { - endIdx = len(filtered) - } + endIdx := min(startIdx+maxVisible, len(filtered)) for i := startIdx; i < endIdx; i++ { image := filtered[i] @@ -5652,10 +5631,7 @@ func (m Model) renderWizardSSHKeyStep(width int) string { if startIdx < 0 { startIdx = 0 } - endIdx := startIdx + maxVisible - if endIdx > len(filtered) { - endIdx = len(filtered) - } + endIdx := min(startIdx+maxVisible, len(filtered)) for i := startIdx; i < endIdx; i++ { sshKey := filtered[i] @@ -5791,10 +5767,7 @@ func (m Model) renderWizardNetworkStep(width int) string { if m.wizard.networkMenuIndex == 1 && m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(filtered) { - endIdx = len(filtered) - } + endIdx := min(startIdx+maxVisible, len(filtered)) for i := startIdx; i < endIdx; i++ { network := filtered[i] @@ -5869,10 +5842,7 @@ func (m Model) renderWizardFloatingIPStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(filtered) { - endIdx = len(filtered) - } + endIdx := min(startIdx+maxVisible, len(filtered)) for i := startIdx; i < endIdx; i++ { fip := filtered[i] @@ -6151,7 +6121,7 @@ func (m Model) renderKubeWizardSubnetStep(width int) string { selectedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#00FF7F")).Padding(0, 1) listStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#CCCCCC")) - var subnets []map[string]interface{} + var subnets []map[string]any var selectedSubnet string if m.wizard.kubeSubnetMenuIndex == 0 { @@ -6367,7 +6337,7 @@ func (m Model) renderNodePoolWizardFlavorStep(width int) string { // Apply filter to flavors flavors := m.wizard.nodePoolFlavors if m.wizard.filterInput != "" { - var filtered []map[string]interface{} + var filtered []map[string]any for _, flavor := range flavors { name := getString(flavor, "name") if strings.Contains(strings.ToLower(name), strings.ToLower(m.wizard.filterInput)) { @@ -6395,10 +6365,7 @@ func (m Model) renderNodePoolWizardFlavorStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(flavors) { - endIdx = len(flavors) - } + endIdx := min(startIdx+maxVisible, len(flavors)) for i := startIdx; i < endIdx; i++ { flavor := flavors[i] @@ -7083,11 +7050,11 @@ func (m Model) renderDetailView(width int) string { return m.backupDetailView.Render(width, 0) } return m.renderGenericDetail(width) - case ProductStorageFile: - if m.fileShareDetailView != nil { - return m.fileShareDetailView.Render(width, 0) - } - return m.renderGenericDetail(width) + case ProductStorageFile: + if m.fileShareDetailView != nil { + return m.fileShareDetailView.Render(width, 0) + } + return m.renderGenericDetail(width) case ProductNetworkPrivate: return m.renderPrivateNetworkDetail(width) case ProductManagedDatabases: @@ -7100,21 +7067,21 @@ func (m Model) renderDetailView(width int) string { return m.renderLBDetail(width) case ProductNetworkPublic: return m.renderFIPDetail(width) - case ProductStorageObject: - if m.objectUserDetailView != nil { - return m.objectUserDetailView.Render(width, 0) - } - if m.objectDetailView != nil { - return m.objectDetailView.Render(width, 0) - } - return m.renderGenericDetail(width) + case ProductStorageObject: + if m.objectUserDetailView != nil { + return m.objectUserDetailView.Render(width, 0) + } + if m.objectDetailView != nil { + return m.objectDetailView.Render(width, 0) + } + return m.renderGenericDetail(width) case ProductWorkflow: return m.renderWorkflowDetail(width) case ProductInstanceBackup: return m.renderInstanceBackupDetail(width) - default: - return m.renderGenericDetail(width) - } + default: + return m.renderGenericDetail(width) + } } func (m Model) renderWorkflowDetail(width int) string { @@ -7198,7 +7165,7 @@ func (m Model) renderInstanceBackupDetail(width int) string { } location := getStringValue(m.detailData, "region", "") if location == "" { - if regions, ok := m.detailData["regions"].([]interface{}); ok && len(regions) > 0 { + if regions, ok := m.detailData["regions"].([]any); ok && len(regions) > 0 { var rnames []string for _, r := range regions { if rs, ok := r.(string); ok { @@ -7273,7 +7240,7 @@ func (m Model) renderInstanceDetail(width int) string { // Get flavor name from nested object or fallback to flavorId flavorName := "N/A" - if flavor, ok := m.detailData["flavor"].(map[string]interface{}); ok { + if flavor, ok := m.detailData["flavor"].(map[string]any); ok { flavorName = getStringValue(flavor, "name", "N/A") } if flavorName == "N/A" { @@ -7297,9 +7264,9 @@ func (m Model) renderInstanceDetail(width int) string { ipv4Private := "" ipv6Public := "" floatingIP := "" - if addresses, ok := m.detailData["ipAddresses"].([]interface{}); ok { + if addresses, ok := m.detailData["ipAddresses"].([]any); ok { for _, addr := range addresses { - if addrMap, ok := addr.(map[string]interface{}); ok { + if addrMap, ok := addr.(map[string]any); ok { ip := getStringValue(addrMap, "ip", "") version := int(getFloatValue(addrMap, "version", 0)) ipType := getStringValue(addrMap, "type", "") @@ -7332,10 +7299,7 @@ func (m Model) renderInstanceDetail(width int) string { } // Build the detail view with boxes - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) // Left column - Information box infoContent := strings.Builder{} @@ -7494,8 +7458,8 @@ func (m Model) renderManagedDatabaseDetail(width int) string { } storageStr := "-" - if storage, ok := m.detailData["storage"].(map[string]interface{}); ok { - if size, ok := storage["size"].(map[string]interface{}); ok { + if storage, ok := m.detailData["storage"].(map[string]any); ok { + if size, ok := storage["size"].(map[string]any); ok { val := getStringValue(size, "value", "") unit := getStringValue(size, "unit", "") if val != "" { @@ -7506,10 +7470,10 @@ func (m Model) renderManagedDatabaseDetail(width int) string { location := "-" nodesCount := 0 - if nodes, ok := m.detailData["nodes"].([]interface{}); ok { + if nodes, ok := m.detailData["nodes"].([]any); ok { nodesCount = len(nodes) if len(nodes) > 0 { - if node, ok := nodes[0].(map[string]interface{}); ok { + if node, ok := nodes[0].(map[string]any); ok { if r := getStringValue(node, "region", ""); r != "" { location = r } @@ -7528,10 +7492,7 @@ func (m Model) renderManagedDatabaseDetail(width int) string { statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF4444")) } - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) var infoContent strings.Builder infoContent.WriteString(fmt.Sprintf("%s %s\n", labelSt.Render("Status"), statusStyle.Render(statusIcon+" "+status))) @@ -7580,9 +7541,9 @@ func (m Model) renderManagedDatabaseDetail(width int) string { // Build URIs from the service's endpoints host, port, dbname, scheme, sslMode := "", "", "defaultdb", engineRaw, "require" - if endpoints, ok := m.detailData["endpoints"].([]interface{}); ok { + if endpoints, ok := m.detailData["endpoints"].([]any); ok { for _, ep := range endpoints { - epMap, ok := ep.(map[string]interface{}) + epMap, ok := ep.(map[string]any) if !ok { continue } @@ -7681,7 +7642,7 @@ func (m Model) renderManagedDatabaseDetail(width int) string { } userStatus := getStringValue(u, "status", "—") var roles []string - if rawRoles, ok := u["roles"].([]interface{}); ok { + if rawRoles, ok := u["roles"].([]any); ok { for _, r := range rawRoles { if s, ok := r.(string); ok { roles = append(roles, s) @@ -7919,27 +7880,18 @@ func (m Model) renderManagedDatabaseDetail(width int) string { } else { timeSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")) hostSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#7B68EE")) - msgSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) + msgSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) indentSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) // Derive maxVisible from terminal height. // Each entry uses 2 lines (header + message), so halve available lines. - maxVisible := (m.height - 27) / 2 - if maxVisible < 3 { - maxVisible = 3 - } + maxVisible := max((m.height-27)/2, 3) // Show entries most-recent first, with scroll window total := len(m.dbDetailLogs) - startIdx := m.dbLogsScrollOffset - if startIdx > total-1 { - startIdx = total - 1 - } + startIdx := min(m.dbLogsScrollOffset, total-1) if startIdx < 0 { startIdx = 0 } - endIdx := startIdx + maxVisible - if endIdx > total { - endIdx = total - } + endIdx := min(startIdx+maxVisible, total) scrollHint := lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")).Render( fmt.Sprintf(" ↑/↓ scroll (%d-%d / %d)", startIdx+1, endIdx, total)) logsContent.WriteString(scrollHint + "\n") @@ -8045,12 +7997,18 @@ func (m Model) renderManagedDatabaseDetail(width int) string { wiz.WriteString(headSt.Width(0).Render(fmt.Sprintf("Step %d/6 — %s", m.dbTopicCreateStep+1, stepLabels[m.dbTopicCreateStep])) + "\n\n") var curVal string switch m.dbTopicCreateStep { - case 0: curVal = m.dbTopicCreateName - case 1: curVal = m.dbTopicCreateMinSync - case 2: curVal = m.dbTopicCreatePartitions - case 3: curVal = m.dbTopicCreateReplication - case 4: curVal = m.dbTopicCreateRetentionByt - case 5: curVal = m.dbTopicCreateRetentionHrs + case 0: + curVal = m.dbTopicCreateName + case 1: + curVal = m.dbTopicCreateMinSync + case 2: + curVal = m.dbTopicCreatePartitions + case 3: + curVal = m.dbTopicCreateReplication + case 4: + curVal = m.dbTopicCreateRetentionByt + case 5: + curVal = m.dbTopicCreateRetentionHrs } wiz.WriteString(inputSt.Render(curVal+"▌") + "\n\n") wiz.WriteString(dimSt.Render("Enter → next Esc → cancel")) @@ -8147,20 +8105,11 @@ func (m Model) renderManagedDatabaseDetail(width int) string { } else { // Metric list (left column) + chart (right column) listWidth := 28 - chartWidth := fullWidth - listWidth - 3 - if chartWidth < 20 { - chartWidth = 20 - } + chartWidth := max(fullWidth-listWidth-3, 20) // The chartBox has border (2 chars) + padding (2 chars) = 4 chars overhead. // The chart must be created 4 chars narrower so its lines don't wrap inside the box. - chartInnerWidth := chartWidth - 4 - if chartInnerWidth < 16 { - chartInnerWidth = 16 - } - chartHeight := m.height - 26 - if chartHeight < 10 { - chartHeight = 10 - } + chartInnerWidth := max(chartWidth-4, 16) + chartHeight := max(m.height-26, 10) // Build metric list var listBuf strings.Builder @@ -8175,9 +8124,9 @@ func (m Model) renderManagedDatabaseDetail(width int) string { name = name[:listWidth-2] } if i == m.dbMetricSelectedIdx { - listBuf.WriteString(selMetricSt.Render("▶ " + name) + "\n") + listBuf.WriteString(selMetricSt.Render("▶ "+name) + "\n") } else { - listBuf.WriteString(unselMetricSt.Render(" " + name) + "\n") + listBuf.WriteString(unselMetricSt.Render(" "+name) + "\n") } } @@ -8187,148 +8136,145 @@ func (m Model) renderManagedDatabaseDetail(width int) string { chartBuf.WriteString(dimSt.Render("Loading...") + "\n") } else if !m.dbMetricLoaded { chartBuf.WriteString(dimSt.Render("Press Enter to load metric") + "\n") - } else if len(m.dbMetricSeries) == 0 { - chartBuf.WriteString(dimSt.Render("No data for this metric/period") + "\n") - } else { - // Palette for multiple series - seriesColors := []string{"#00FFD0", "#FF6B6B", "#FFD93D", "#6BCB77", "#4D96FF", "#C77DFF"} - - // Compute global time and value ranges across visible series only - var minT, maxT time.Time - var minV, maxV float64 - first := true - for si, s := range m.dbMetricSeries { - if m.dbMetricSeriesHidden[si] { - continue - } - for _, p := range s.Points { - if first || p.T.Before(minT) { - minT = p.T - } - if first || p.T.After(maxT) { - maxT = p.T - } - if first || p.V < minV { - minV = p.V - } - if first || p.V > maxV { - maxV = p.V - } - first = false - } - } - if first { - // All series hidden - chartBuf.WriteString(dimSt.Render("All series hidden — press 1-9 to show") + "\n") - goto renderColumns - } - if maxT.Equal(minT) { - maxT = minT.Add(time.Second) - } - yPad := (maxV - minV) * 0.1 - if yPad == 0 { - yPad = 0.1 - } - minV = math.Max(0, minV-yPad) - maxV = maxV + yPad - - xFmt := timeserieslinechart.HourTimeLabelFormatter() - - chart := timeserieslinechart.New(chartInnerWidth, chartHeight, - timeserieslinechart.WithTimeRange(minT, maxT), - timeserieslinechart.WithYRange(minV, maxV), - timeserieslinechart.WithXYSteps(4, 4), - timeserieslinechart.WithXLabelFormatter(xFmt), - timeserieslinechart.WithYLabelFormatter(func(_ int, v float64) string { - if v >= 1000 { - return fmt.Sprintf("%.0f", v) - } else if v >= 10 { - return fmt.Sprintf("%.1f", v) - } - return fmt.Sprintf("%.2f", v) - }), - ) - chart.AxisStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")) - chart.LabelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) - - // Disable auto-range so Push() never alters the scale mid-loop. - chart.AutoMinX = false - chart.AutoMaxX = false - chart.AutoMinY = false - chart.AutoMaxY = false - chart.SetViewTimeAndYRange(minT, maxT, minV, maxV) - - graphW := chart.GraphWidth() - if graphW < 1 { - graphW = chartInnerWidth - } - - var dsNames []string - for si, s := range m.dbMetricSeries { - if m.dbMetricSeriesHidden[si] { - continue - } - color := seriesColors[si%len(seriesColors)] - dsName := s.Name - chart.SetDataSetStyle(dsName, lipgloss.NewStyle().Foreground(lipgloss.Color(color))) - dsNames = append(dsNames, dsName) - - // Downsample per series so every column has a point - pts := s.Points - if len(pts) > graphW { - bucket := len(pts) / graphW - sampled := make([]dbMetricPoint, 0, graphW) - for i := 0; i < len(pts); i += bucket { - end := i + bucket - if end > len(pts) { - end = len(pts) - } - var sum float64 - for _, p := range pts[i:end] { - sum += p.V - } - sampled = append(sampled, dbMetricPoint{T: pts[i].T, V: sum / float64(end-i)}) - } - pts = sampled - } - for _, pt := range pts { - chart.PushDataSet(dsName, timeserieslinechart.TimePoint{Time: pt.T, Value: pt.V}) - } - } - chart.DrawDataSets(dsNames) - - chartTitle := m.dbMetricName + " [" + periods[m.dbMetricPeriodIdx] + "]" - chartBuf.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")).Render(chartTitle) + "\n") - chartBuf.WriteString(chart.View()) - - // Legend — use real node names when available, always shown - // Press number key (1-9) to toggle a series on/off - var legend strings.Builder - for si, s := range m.dbMetricSeries { - color := seriesColors[si%len(seriesColors)] - label := s.Name - if si < len(m.dbNodeNames) && m.dbNodeNames[si] != "" { - label = strings.TrimSuffix(m.dbNodeNames[si], ".cloud.ovh.net") - } - numKey := fmt.Sprintf("[%d]", si+1) - if m.dbMetricSeriesHidden[si] { - // Dimmed, struck-through style for hidden series - hiddenSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")) - legend.WriteString(hiddenSt.Render(numKey+" ━ "+label) + " ") - } else { - dot := lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render("━") - numSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render(numKey) - legend.WriteString(numSt + " " + dot + " " + dimSt.Render(label) + " ") - } - } - if legend.Len() > 0 { - chartBuf.WriteString("\n" + legend.String() + "\n") - } - } - - renderColumns: - // Join list and chart side by side - listBox := lipgloss.NewStyle(). + } else if len(m.dbMetricSeries) == 0 { + chartBuf.WriteString(dimSt.Render("No data for this metric/period") + "\n") + } else { + // Palette for multiple series + seriesColors := []string{"#00FFD0", "#FF6B6B", "#FFD93D", "#6BCB77", "#4D96FF", "#C77DFF"} + + // Compute global time and value ranges across visible series only + var minT, maxT time.Time + var minV, maxV float64 + first := true + for si, s := range m.dbMetricSeries { + if m.dbMetricSeriesHidden[si] { + continue + } + for _, p := range s.Points { + if first || p.T.Before(minT) { + minT = p.T + } + if first || p.T.After(maxT) { + maxT = p.T + } + if first || p.V < minV { + minV = p.V + } + if first || p.V > maxV { + maxV = p.V + } + first = false + } + } + if first { + // All series hidden + chartBuf.WriteString(dimSt.Render("All series hidden — press 1-9 to show") + "\n") + goto renderColumns + } + if maxT.Equal(minT) { + maxT = minT.Add(time.Second) + } + yPad := (maxV - minV) * 0.1 + if yPad == 0 { + yPad = 0.1 + } + minV = math.Max(0, minV-yPad) + maxV = maxV + yPad + + xFmt := timeserieslinechart.HourTimeLabelFormatter() + + chart := timeserieslinechart.New(chartInnerWidth, chartHeight, + timeserieslinechart.WithTimeRange(minT, maxT), + timeserieslinechart.WithYRange(minV, maxV), + timeserieslinechart.WithXYSteps(4, 4), + timeserieslinechart.WithXLabelFormatter(xFmt), + timeserieslinechart.WithYLabelFormatter(func(_ int, v float64) string { + if v >= 1000 { + return fmt.Sprintf("%.0f", v) + } else if v >= 10 { + return fmt.Sprintf("%.1f", v) + } + return fmt.Sprintf("%.2f", v) + }), + ) + chart.AxisStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")) + chart.LabelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) + + // Disable auto-range so Push() never alters the scale mid-loop. + chart.AutoMinX = false + chart.AutoMaxX = false + chart.AutoMinY = false + chart.AutoMaxY = false + chart.SetViewTimeAndYRange(minT, maxT, minV, maxV) + + graphW := chart.GraphWidth() + if graphW < 1 { + graphW = chartInnerWidth + } + + var dsNames []string + for si, s := range m.dbMetricSeries { + if m.dbMetricSeriesHidden[si] { + continue + } + color := seriesColors[si%len(seriesColors)] + dsName := s.Name + chart.SetDataSetStyle(dsName, lipgloss.NewStyle().Foreground(lipgloss.Color(color))) + dsNames = append(dsNames, dsName) + + // Downsample per series so every column has a point + pts := s.Points + if len(pts) > graphW { + bucket := len(pts) / graphW + sampled := make([]dbMetricPoint, 0, graphW) + for i := 0; i < len(pts); i += bucket { + end := min(i+bucket, len(pts)) + var sum float64 + for _, p := range pts[i:end] { + sum += p.V + } + sampled = append(sampled, dbMetricPoint{T: pts[i].T, V: sum / float64(end-i)}) + } + pts = sampled + } + for _, pt := range pts { + chart.PushDataSet(dsName, timeserieslinechart.TimePoint{Time: pt.T, Value: pt.V}) + } + } + chart.DrawDataSets(dsNames) + + chartTitle := m.dbMetricName + " [" + periods[m.dbMetricPeriodIdx] + "]" + chartBuf.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")).Render(chartTitle) + "\n") + chartBuf.WriteString(chart.View()) + + // Legend — use real node names when available, always shown + // Press number key (1-9) to toggle a series on/off + var legend strings.Builder + for si, s := range m.dbMetricSeries { + color := seriesColors[si%len(seriesColors)] + label := s.Name + if si < len(m.dbNodeNames) && m.dbNodeNames[si] != "" { + label = strings.TrimSuffix(m.dbNodeNames[si], ".cloud.ovh.net") + } + numKey := fmt.Sprintf("[%d]", si+1) + if m.dbMetricSeriesHidden[si] { + // Dimmed, struck-through style for hidden series + hiddenSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")) + legend.WriteString(hiddenSt.Render(numKey+" ━ "+label) + " ") + } else { + dot := lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render("━") + numSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render(numKey) + legend.WriteString(numSt + " " + dot + " " + dimSt.Render(label) + " ") + } + } + if legend.Len() > 0 { + chartBuf.WriteString("\n" + legend.String() + "\n") + } + } + + renderColumns: + // Join list and chart side by side + listBox := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#333333")). Width(listWidth).Padding(0, 1). @@ -8347,7 +8293,6 @@ func (m Model) renderManagedDatabaseDetail(width int) string { func (m Model) renderGatewayDetail_PLACEHOLDER() string { return "" } - func (m Model) renderGatewayDetail(width int) string { var content strings.Builder @@ -8358,9 +8303,9 @@ func (m Model) renderGatewayDetail(width int) string { status := getStringValue(m.detailData, "status", "N/A") publicIP := "N/A" - if ei, ok := m.detailData["externalInformation"].(map[string]interface{}); ok { - if ips, ok := ei["ips"].([]interface{}); ok && len(ips) > 0 { - if ipm, ok := ips[0].(map[string]interface{}); ok { + if ei, ok := m.detailData["externalInformation"].(map[string]any); ok { + if ips, ok := ei["ips"].([]any); ok && len(ips) > 0 { + if ipm, ok := ips[0].(map[string]any); ok { if v := getStringValue(ipm, "ip", ""); v != "" { publicIP = v } @@ -8370,9 +8315,9 @@ func (m Model) renderGatewayDetail(width int) string { var privateIPs []string privateNetwork := "N/A" - if ifaces, ok := m.detailData["interfaces"].([]interface{}); ok { + if ifaces, ok := m.detailData["interfaces"].([]any); ok { for _, iface := range ifaces { - if ifm, ok := iface.(map[string]interface{}); ok { + if ifm, ok := iface.(map[string]any); ok { if v := getStringValue(ifm, "ip", ""); v != "" { privateIPs = append(privateIPs, v) } @@ -8398,10 +8343,7 @@ func (m Model) renderGatewayDetail(width int) string { labelSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Width(18) valueSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) // Info box var infoContent strings.Builder @@ -8467,7 +8409,7 @@ func (m Model) renderLBDetail(width int) string { } publicIP := "-" - if fi, ok := m.detailData["floatingIp"].(map[string]interface{}); ok { + if fi, ok := m.detailData["floatingIp"].(map[string]any); ok { if v := getStringValue(fi, "ip", ""); v != "" { publicIP = v } @@ -8482,10 +8424,7 @@ func (m Model) renderLBDetail(width int) string { labelSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Width(20) valueSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) // Info box var infoContent strings.Builder @@ -8659,7 +8598,7 @@ func (m Model) renderLBPoolDetailView(width int) string { region := getStringValue(m.detailData, "region", "N/A") sessionType := "-" - if sp, ok := m.selectedLBPool["sessionPersistence"].(map[string]interface{}); ok { + if sp, ok := m.selectedLBPool["sessionPersistence"].(map[string]any); ok { sessionType = getStringValue(sp, "type", "-") } @@ -9009,10 +8948,7 @@ func (m Model) renderLBL7RulesView(width int) string { } // Clamp index - idx := m.lbL7RuleDetailIdx - if idx < 0 { - idx = 0 - } + idx := max(m.lbL7RuleDetailIdx, 0) if idx >= len(rules) { idx = len(rules) - 1 } @@ -9134,10 +9070,7 @@ func (m Model) renderLBPoolMembersView(width int) string { } // Clamp member index - idx := m.lbPoolMemberDetailIdx - if idx < 0 { - idx = 0 - } + idx := max(m.lbPoolMemberDetailIdx, 0) if idx >= len(members) { idx = len(members) - 1 } @@ -9189,7 +9122,7 @@ func (m Model) renderFIPDetail(width int) string { // Associated entity (instance or LB) associatedTo := "-" - if entity, ok := m.detailData["associatedEntity"].(map[string]interface{}); ok { + if entity, ok := m.detailData["associatedEntity"].(map[string]any); ok { entityType := getStringValue(entity, "type", "") entityID := getStringValue(entity, "id", "") if entityID != "" { @@ -9199,10 +9132,7 @@ func (m Model) renderFIPDetail(width int) string { labelSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Width(20) valueSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) // Status styling statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF7F")) @@ -9264,27 +9194,35 @@ func (m Model) renderPrivateNetworkDetail(width int) string { fullWidth := width - 4 // Info box (top-left) - rawRegions, _ := m.detailData["regions"].([]interface{}) + rawRegions, _ := m.detailData["regions"].([]any) var infoContent strings.Builder infoContent.WriteString(fmt.Sprintf("%s %s\n", labelStyle.Render("ID"), valueStyle.Render(truncate(netID, 36)))) vlanStr := "automatic" - if vlanID > 0 { vlanStr = fmt.Sprintf("%d", vlanID) } + if vlanID > 0 { + vlanStr = fmt.Sprintf("%d", vlanID) + } infoContent.WriteString(fmt.Sprintf("%s %s\n", labelStyle.Render("VLAN ID"), valueStyle.Render(vlanStr))) rTypeLabel := "Region (vRack)" - if regionType == "localzone" { rTypeLabel = "Local Zone" } + if regionType == "localzone" { + rTypeLabel = "Local Zone" + } infoContent.WriteString(fmt.Sprintf("%s %s\n", labelStyle.Render("Type"), valueStyle.Render(rTypeLabel))) // Regions list if len(rawRegions) == 0 { infoContent.WriteString(fmt.Sprintf("%s %s", labelStyle.Render("Regions"), lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")).Render("none"))) } else { for idx, rv := range rawRegions { - rm, _ := rv.(map[string]interface{}) + rm, _ := rv.(map[string]any) rName := getStringValue(rm, "region", "N/A") rStatus := getStringValue(rm, "status", "") label := "Regions" - if idx > 0 { label = "" } + if idx > 0 { + label = "" + } line := rName - if rStatus != "" { line += " (" + rStatus + ")" } + if rStatus != "" { + line += " (" + rStatus + ")" + } // Highlight selected region when Delete Region action is active if m.selectedAction == 4 && idx == m.privNetSelectedRegion { arrow := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF6B6B")).Bold(true).Render(" ◄") @@ -9296,7 +9234,9 @@ func (m Model) renderPrivateNetworkDetail(width int) string { line = valueStyle.Render(line) } sep := "\n" - if idx == len(rawRegions)-1 { sep = "" } + if idx == len(rawRegions)-1 { + sep = "" + } infoContent.WriteString(fmt.Sprintf("%s %s%s", labelStyle.Render(label), line, sep)) } } @@ -9315,11 +9255,15 @@ func (m Model) renderPrivateNetworkDetail(width int) string { gatewayIP := getStringValue(sub, "gatewayIp", "N/A") dhcpStr := "N/A" if dhcp, ok := sub["dhcpEnabled"].(bool); ok { - if dhcp { dhcpStr = "enabled" } else { dhcpStr = "disabled" } + if dhcp { + dhcpStr = "enabled" + } else { + dhcpStr = "disabled" + } } allocPool := "-" - if pools, ok := sub["ipPools"].([]interface{}); ok && len(pools) > 0 { - if pool, ok := pools[0].(map[string]interface{}); ok { + if pools, ok := sub["ipPools"].([]any); ok && len(pools) > 0 { + if pool, ok := pools[0].(map[string]any); ok { start := getString(pool, "start") end := getString(pool, "end") if start != "" && end != "" { @@ -9403,10 +9347,7 @@ func (m Model) renderKubernetesDetail(width int) string { statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")) } - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) // Cluster info infoContent := strings.Builder{} @@ -9527,10 +9468,7 @@ func (m Model) renderProjectDetail(width int) string { statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")) } - boxWidth := (width - 6) / 2 - if boxWidth < 35 { - boxWidth = 35 - } + boxWidth := max((width-6)/2, 35) // Project info infoContent := strings.Builder{} @@ -9596,10 +9534,7 @@ func renderBox(title string, content string, width int) string { var contentLines []string for _, line := range lines { lineWidth := lipgloss.Width(line) - padding := width - 4 - lineWidth - if padding < 0 { - padding = 0 - } + padding := max(width-4-lineWidth, 0) contentLines = append(contentLines, "│ "+line+strings.Repeat(" ", padding)+" │") } @@ -9907,23 +9842,23 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, cmd } - // Delegate to file storage detail view when in DetailView for ProductStorageFile - if m.mode == DetailView && m.currentProduct == ProductStorageFile && m.fileShareDetailView != nil { - cmd := m.fileShareDetailView.HandleKey(msg) - return m, cmd - } + // Delegate to file storage detail view when in DetailView for ProductStorageFile + if m.mode == DetailView && m.currentProduct == ProductStorageFile && m.fileShareDetailView != nil { + cmd := m.fileShareDetailView.HandleKey(msg) + return m, cmd + } - // Delegate to object storage detail view when in DetailView for ProductStorageObject - if m.mode == DetailView && m.currentProduct == ProductStorageObject && m.objectDetailView != nil { - cmd := m.objectDetailView.HandleKey(msg) - return m, cmd - } + // Delegate to object storage detail view when in DetailView for ProductStorageObject + if m.mode == DetailView && m.currentProduct == ProductStorageObject && m.objectDetailView != nil { + cmd := m.objectDetailView.HandleKey(msg) + return m, cmd + } - // Delegate to object storage user detail view - if m.mode == DetailView && m.currentProduct == ProductStorageObject && m.objectUserDetailView != nil { - cmd := m.objectUserDetailView.HandleKey(msg) - return m, cmd - } + // Delegate to object storage user detail view + if m.mode == DetailView && m.currentProduct == ProductStorageObject && m.objectUserDetailView != nil { + cmd := m.objectUserDetailView.HandleKey(msg) + return m, cmd + } // Intercept all keys when DB user creation text input is active if m.dbUserCreateMode && m.mode == DetailView && @@ -10333,8 +10268,8 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } - switch msg.String() { - case "left": + switch msg.String() { + case "left": // In NodePoolDetailView, navigate actions if m.mode == NodePoolDetailView { if m.nodePoolDetailActionIdx > 0 { @@ -10747,7 +10682,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.mode == DetailView && m.currentProduct == ProductNetworkPublic { fipAttached := false if m.detailData != nil { - if entity, ok := m.detailData["associatedEntity"].(map[string]interface{}); ok { + if entity, ok := m.detailData["associatedEntity"].(map[string]any); ok { fipAttached = getStringValue(entity, "id", "") != "" } } @@ -11012,10 +10947,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { policyName := getStringValue(m.selectedLBL7Policy, "name", "") region := getStringValue(m.detailData, "region", "") rules := m.lbL7Rules[policyID] - idx := m.lbL7RuleDetailIdx - if idx < 0 { - idx = 0 - } + idx := max(m.lbL7RuleDetailIdx, 0) if len(rules) > 0 && idx < len(rules) { r := rules[idx] ruleID := getStringValue(r, "id", "") @@ -11129,7 +11061,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { hmHttpMethodIdx := 2 // GET is index 2 in lbHMHttpMethodOptions hmUrlPath := "/" hmExpectedCodes := "200" - if httpCfg, ok := hm["httpConfiguration"].(map[string]interface{}); ok { + if httpCfg, ok := hm["httpConfiguration"].(map[string]any); ok { if method, ok := httpCfg["httpMethod"].(string); ok && method != "" { hmHttpMethod = method for i, opt := range lbHMHttpMethodOptions { @@ -11260,7 +11192,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } sessionVal := "" - if sp, ok := m.selectedLBPool["sessionPersistence"].(map[string]interface{}); ok { + if sp, ok := m.selectedLBPool["sessionPersistence"].(map[string]any); ok { sessionVal = getStringValue(sp, "type", "") } sessionIdx := 0 @@ -11308,10 +11240,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { poolID := getStringValue(m.selectedLBPool, "id", "") region := getStringValue(m.detailData, "region", "") members := m.lbPoolMembers[poolID] - idx := m.lbPoolMemberDetailIdx - if idx < 0 { - idx = 0 - } + idx := max(m.lbPoolMemberDetailIdx, 0) // Resolve actual action index (when no members, actions are 0=Create, 1=HM) actionIdx := m.lbMembersActionIdx if len(members) == 0 { @@ -11418,27 +11347,67 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { hmType := getStringValue(hm, "monitorType", "http") hmTypeIdx := 0 for i, opt := range lbHMTypeOptions { - if opt.value == hmType { hmTypeIdx = i; break } + if opt.value == hmType { + hmTypeIdx = i + break + } } hmDelay := 5 - if v, ok := hm["delay"]; ok { switch d := v.(type) { case float64: hmDelay = int(d); case int: hmDelay = d } } + if v, ok := hm["delay"]; ok { + switch d := v.(type) { + case float64: + hmDelay = int(d) + case int: + hmDelay = d + } + } hmTimeout := 5 - if v, ok := hm["timeout"]; ok { switch d := v.(type) { case float64: hmTimeout = int(d); case int: hmTimeout = d } } + if v, ok := hm["timeout"]; ok { + switch d := v.(type) { + case float64: + hmTimeout = int(d) + case int: + hmTimeout = d + } + } hmMaxRetries := 3 - if v, ok := hm["maxRetries"]; ok { switch d := v.(type) { case float64: hmMaxRetries = int(d); case int: hmMaxRetries = d } } + if v, ok := hm["maxRetries"]; ok { + switch d := v.(type) { + case float64: + hmMaxRetries = int(d) + case int: + hmMaxRetries = d + } + } hmMaxRetriesDown := 3 - if v, ok := hm["maxRetriesDown"]; ok { switch d := v.(type) { case float64: hmMaxRetriesDown = int(d); case int: hmMaxRetriesDown = d } } + if v, ok := hm["maxRetriesDown"]; ok { + switch d := v.(type) { + case float64: + hmMaxRetriesDown = int(d) + case int: + hmMaxRetriesDown = d + } + } hmHttpMethod := "GET" hmHttpMethodIdx := 2 hmUrlPath := "/" hmExpectedCodes := "200" - if httpCfg, ok := hm["httpConfiguration"].(map[string]interface{}); ok { + if httpCfg, ok := hm["httpConfiguration"].(map[string]any); ok { if method, ok := httpCfg["httpMethod"].(string); ok && method != "" { hmHttpMethod = method - for i, opt := range lbHMHttpMethodOptions { if opt == hmHttpMethod { hmHttpMethodIdx = i; break } } + for i, opt := range lbHMHttpMethodOptions { + if opt == hmHttpMethod { + hmHttpMethodIdx = i + break + } + } + } + if up, ok := httpCfg["urlPath"].(string); ok && up != "" { + hmUrlPath = up + } + if ec, ok := httpCfg["expectedCodes"].(string); ok && ec != "" { + hmExpectedCodes = ec } - if up, ok := httpCfg["urlPath"].(string); ok && up != "" { hmUrlPath = up } - if ec, ok := httpCfg["expectedCodes"].(string); ok && ec != "" { hmExpectedCodes = ec } } m.mode = WizardView m.wizard = WizardData{ @@ -11478,8 +11447,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { l7RuleLBRegion: region, } case 1: // Edit - idx := m.lbL7RuleDetailIdx - if idx < 0 { idx = 0 } + idx := max(m.lbL7RuleDetailIdx, 0) if len(rules) > 0 && idx < len(rules) { r := rules[idx] ruleID := getStringValue(r, "id", "") @@ -11488,19 +11456,30 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { ruleKey := getStringValue(r, "key", "") ruleValue := getStringValue(r, "value", "") ruleInvert := false - if inv, ok := r["invert"].(bool); ok { ruleInvert = inv } + if inv, ok := r["invert"].(bool); ok { + ruleInvert = inv + } typeIdx := 0 for i, opt := range lbL7RuleTypeOptions { - if opt.value == ruleType { typeIdx = i; break } + if opt.value == ruleType { + typeIdx = i + break + } } compareIdx := 0 validOpts := validCompareOptionsForType(ruleType) for i, opt := range validOpts { - if opt.value == compareType { compareIdx = i; break } + if opt.value == compareType { + compareIdx = i + break + } } prefillKey := "" for _, t := range lbL7RuleTypeOptions { - if t.value == ruleType && t.needsKey { prefillKey = ruleKey; break } + if t.value == ruleType && t.needsKey { + prefillKey = ruleKey + break + } } m.mode = WizardView m.wizard = WizardData{ @@ -11519,8 +11498,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } case 2: // Delete - idx := m.lbL7RuleDetailIdx - if idx < 0 { idx = 0 } + idx := max(m.lbL7RuleDetailIdx, 0) if len(rules) > 0 && idx < len(rules) { if m.lbL7RuleConfirm { m.lbL7RuleConfirm = false @@ -11569,22 +11547,22 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.mode = WizardView m.wizard = WizardData{ - step: LBL7PolicyWizardStepName, - l7PolicyEditId: getStringValue(m.selectedLBL7Policy, "id", ""), - l7PolicyListenerId: getStringValue(m.selectedLBListener, "id", ""), - l7PolicyListenerName: getStringValue(m.selectedLBListener, "name", ""), - l7PolicyLBRegion: getStringValue(m.detailData, "region", ""), - l7PolicyLBId: getStringValue(m.detailData, "id", ""), - l7PolicyNameInput: policyName, - l7PolicyName: policyName, - l7PolicyPositionInput: fmt.Sprintf("%d", policyPosition), - l7PolicyPosition: policyPosition, - l7PolicyActionIdx: actionIdx, - l7PolicyAction: policyAction, - l7PolicyRedirectPoolIdx: redirectPoolIdx, - l7PolicyRedirectPoolId: redirectPoolId, + step: LBL7PolicyWizardStepName, + l7PolicyEditId: getStringValue(m.selectedLBL7Policy, "id", ""), + l7PolicyListenerId: getStringValue(m.selectedLBListener, "id", ""), + l7PolicyListenerName: getStringValue(m.selectedLBListener, "name", ""), + l7PolicyLBRegion: getStringValue(m.detailData, "region", ""), + l7PolicyLBId: getStringValue(m.detailData, "id", ""), + l7PolicyNameInput: policyName, + l7PolicyName: policyName, + l7PolicyPositionInput: fmt.Sprintf("%d", policyPosition), + l7PolicyPosition: policyPosition, + l7PolicyActionIdx: actionIdx, + l7PolicyAction: policyAction, + l7PolicyRedirectPoolIdx: redirectPoolIdx, + l7PolicyRedirectPoolId: redirectPoolId, l7PolicyRedirectUrlInput: redirectUrl, - l7PolicyRedirectUrl: redirectUrl, + l7PolicyRedirectUrl: redirectUrl, } return m, nil case 1: // Delete @@ -11640,11 +11618,11 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.mode = WizardView m.wizard = WizardData{ - step: LBListenerWizardStepName, - lbListenerEditId: getStringValue(m.selectedLBListener, "id", ""), - lbListenerLBId: getStringValue(m.detailData, "id", ""), - lbListenerLBName: getStringValue(m.detailData, "name", ""), - lbListenerLBRegion: getStringValue(m.detailData, "region", ""), + step: LBListenerWizardStepName, + lbListenerEditId: getStringValue(m.selectedLBListener, "id", ""), + lbListenerLBId: getStringValue(m.detailData, "id", ""), + lbListenerLBName: getStringValue(m.detailData, "name", ""), + lbListenerLBRegion: getStringValue(m.detailData, "region", ""), lbListenerNameInput: listenerName, lbListenerName: listenerName, lbListenerProtoIdx: protoIdx, @@ -11662,12 +11640,12 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case 2: // L7 Policies — launch L7 policy creation wizard m.mode = WizardView m.wizard = WizardData{ - step: LBL7PolicyWizardStepName, - l7PolicyListenerId: getStringValue(m.selectedLBListener, "id", ""), - l7PolicyListenerName: getStringValue(m.selectedLBListener, "name", ""), - l7PolicyLBRegion: getStringValue(m.detailData, "region", ""), - l7PolicyLBId: getStringValue(m.detailData, "id", ""), - l7PolicyPosition: 1, + step: LBL7PolicyWizardStepName, + l7PolicyListenerId: getStringValue(m.selectedLBListener, "id", ""), + l7PolicyListenerName: getStringValue(m.selectedLBListener, "name", ""), + l7PolicyLBRegion: getStringValue(m.detailData, "region", ""), + l7PolicyLBId: getStringValue(m.detailData, "id", ""), + l7PolicyPosition: 1, l7PolicyPositionInput: "1", } return m, nil @@ -11805,9 +11783,9 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.mode = WizardView pools := m.lbPools[getStringValue(m.detailData, "id", "")] m.wizard = WizardData{ - step: LBListenerWizardStepName, - lbListenerLBId: getStringValue(m.detailData, "id", ""), - lbListenerLBName: getStringValue(m.detailData, "name", ""), + step: LBListenerWizardStepName, + lbListenerLBId: getStringValue(m.detailData, "id", ""), + lbListenerLBName: getStringValue(m.detailData, "name", ""), lbListenerLBRegion: getStringValue(m.detailData, "region", ""), } // pre-load pools list into wizard for pool selection step @@ -11840,7 +11818,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.actionConfirm = true } return m, nil - } else if m.mode == DetailView && m.currentProduct == ProductWorkflow { + } else if m.mode == DetailView && m.currentProduct == ProductWorkflow { switch m.selectedAction { case 0: // Supprimer if m.actionConfirm { @@ -11874,9 +11852,9 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // Build region list from the network's available regions var regionNames []string regionMap := make(map[string]map[string]string) - if regions, ok := m.detailData["regions"].([]interface{}); ok { + if regions, ok := m.detailData["regions"].([]any); ok { for _, rv := range regions { - rm, ok := rv.(map[string]interface{}) + rm, ok := rv.(map[string]any) if !ok { continue } @@ -11988,7 +11966,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.actionConfirm = true case 4: // Delete Region - regions, _ := m.detailData["regions"].([]interface{}) + regions, _ := m.detailData["regions"].([]any) if len(regions) == 0 { return m, nil } @@ -12040,7 +12018,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // Open detail view from table (replaces former 'v' key) if m.mode == TableView { - isSubNavProd := (m.currentProduct >= ProductStorageBlock && m.currentProduct <= ProductNetworkLB) + isSubNavProd := (m.currentProduct >= ProductStorageBlock && m.currentProduct <= ProductNetworkLB) if !isSubNavProd || m.inTableFocus { selectedRow := m.table.Cursor() if selectedRow >= 0 && selectedRow < len(m.currentData) { @@ -12165,10 +12143,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.dbDetailTab == 5 && m.dbLogsLoaded { total := len(m.dbDetailLogs) maxVisible := 20 - maxOffset := total - maxVisible - if maxOffset < 0 { - maxOffset = 0 - } + maxOffset := max(total-maxVisible, 0) if key == "up" || key == "k" { if m.dbLogsScrollOffset < maxOffset { m.dbLogsScrollOffset++ @@ -12375,7 +12350,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // Private network detail: ↑/↓ to select region when Delete Region action is active if m.mode == DetailView && m.currentProduct == ProductNetworkPrivate && m.selectedAction == 4 { - regions, _ := m.detailData["regions"].([]interface{}) + regions, _ := m.detailData["regions"].([]any) if len(regions) > 0 { if key == "down" || key == "j" { if m.privNetSelectedRegion < len(regions)-1 { @@ -12410,7 +12385,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.mode = LoadingView // Store a flag to return to detail view after loading - m.detailData = map[string]interface{}{ + m.detailData = map[string]any{ "_refreshItemId": itemId, "_refreshItemName": itemName, } @@ -12455,10 +12430,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { policyID := getStringValue(m.selectedLBL7Policy, "id", "") rules := m.lbL7Rules[policyID] if len(rules) > 0 { - idx := m.lbL7RuleDetailIdx - if idx < 0 { - idx = 0 - } + idx := max(m.lbL7RuleDetailIdx, 0) if idx < len(rules) { if m.lbL7RuleConfirm { // Second press: execute delete @@ -12490,7 +12462,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // In Projects selection view: set selected project as default if m.mode == ProjectSelectView || m.currentProduct == ProductProjects { - var project map[string]interface{} + var project map[string]any if m.mode == ProjectSelectView { // Use the selected row in project selection view @@ -12565,10 +12537,7 @@ func (m Model) handleDebugKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // Scroll up (show older entries) entries := httpLib.BrowserDebugLogger.GetEntries() maxVisible := 15 - maxOffset := len(entries) - maxVisible - if maxOffset < 0 { - maxOffset = 0 - } + maxOffset := max(len(entries)-maxVisible, 0) if m.debugScrollOffset < maxOffset { m.debugScrollOffset++ } @@ -12699,7 +12668,7 @@ func (m *Model) applyTableFilter() { switch m.currentProduct { case ProductInstances: - var filtered []map[string]interface{} + var filtered []map[string]any for _, item := range m.currentData { name := strings.ToLower(getStringValue(item, "name", "")) status := strings.ToLower(getStringValue(item, "status", "")) @@ -12710,7 +12679,7 @@ func (m *Model) applyTableFilter() { } m.table = createInstancesTable(filtered, m.imageMap, m.floatingIPMap, m.width, m.height) case ProductKubernetes: - var filtered []map[string]interface{} + var filtered []map[string]any for _, item := range m.currentData { name := strings.ToLower(getStringValue(item, "name", "")) status := strings.ToLower(getStringValue(item, "status", "")) @@ -12722,7 +12691,7 @@ func (m *Model) applyTableFilter() { } m.table = createKubernetesTable(filtered, m.width, m.height) case ProductStorageObject: - var filtered []map[string]interface{} + var filtered []map[string]any for _, item := range m.currentData { name := strings.ToLower(getStringValue(item, "name", "")) region := strings.ToLower(getStringValue(item, "region", "")) @@ -12987,11 +12956,11 @@ func (m Model) handleWizardKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleFileWizardConfirmKeys(key) // Object Storage wizard steps case ObjectWizardStepName: - return m.handleObjectWizardNameKeys(msg) + return m.handleObjectWizardNameKeys(msg) case ObjectWizardStepType: - return m.handleObjectWizardTypeKeys(key) + return m.handleObjectWizardTypeKeys(key) case ObjectWizardStepRegion: - return m.handleObjectWizardRegionKeys(key) + return m.handleObjectWizardRegionKeys(key) case ObjectWizardStepReplication: switch key { case "left", "h", "y": @@ -13029,7 +12998,7 @@ func (m Model) handleWizardKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } return m, nil case ObjectWizardStepUser: - return m.handleObjectWizardUserKeys(key) + return m.handleObjectWizardUserKeys(key) case ObjectWizardStepEncryption: switch key { case "up", "k": @@ -13280,12 +13249,12 @@ func (m Model) handleCleanupConfirmKeys(key string) (tea.Model, tea.Cmd) { } // getFilteredWizardRegions returns filtered regions based on wizard filter input -func (m Model) getFilteredWizardRegions() []map[string]interface{} { +func (m Model) getFilteredWizardRegions() []map[string]any { if m.wizard.filterInput == "" { return m.wizard.regions } filter := strings.ToLower(m.wizard.filterInput) - var filtered []map[string]interface{} + var filtered []map[string]any for _, region := range m.wizard.regions { name := strings.ToLower(getString(region, "name")) location := strings.ToLower(getString(region, "datacenterLocation")) @@ -13298,12 +13267,12 @@ func (m Model) getFilteredWizardRegions() []map[string]interface{} { } // getFilteredWizardFlavors returns filtered flavors based on wizard filter input -func (m Model) getFilteredWizardFlavors() []map[string]interface{} { +func (m Model) getFilteredWizardFlavors() []map[string]any { if m.wizard.filterInput == "" { return m.wizard.flavors } filter := strings.ToLower(m.wizard.filterInput) - var filtered []map[string]interface{} + var filtered []map[string]any for _, flavor := range m.wizard.flavors { name := strings.ToLower(getString(flavor, "name")) flavorType := strings.ToLower(getString(flavor, "type")) @@ -13315,12 +13284,12 @@ func (m Model) getFilteredWizardFlavors() []map[string]interface{} { } // getFilteredWizardImages returns filtered images based on wizard filter input -func (m Model) getFilteredWizardImages() []map[string]interface{} { +func (m Model) getFilteredWizardImages() []map[string]any { if m.wizard.filterInput == "" { return m.wizard.images } filter := strings.ToLower(m.wizard.filterInput) - var filtered []map[string]interface{} + var filtered []map[string]any for _, image := range m.wizard.images { name := strings.ToLower(getString(image, "name")) if strings.Contains(name, filter) { @@ -13331,12 +13300,12 @@ func (m Model) getFilteredWizardImages() []map[string]interface{} { } // getFilteredWizardSSHKeys returns filtered SSH keys based on wizard filter input -func (m Model) getFilteredWizardSSHKeys() []map[string]interface{} { +func (m Model) getFilteredWizardSSHKeys() []map[string]any { if m.wizard.filterInput == "" { return m.wizard.sshKeys } filter := strings.ToLower(m.wizard.filterInput) - var filtered []map[string]interface{} + var filtered []map[string]any for _, key := range m.wizard.sshKeys { name := strings.ToLower(getString(key, "name")) // Always include "(No SSH Key)" option @@ -13348,12 +13317,12 @@ func (m Model) getFilteredWizardSSHKeys() []map[string]interface{} { } // getFilteredWizardNetworks returns filtered private networks based on wizard filter input -func (m Model) getFilteredWizardNetworks() []map[string]interface{} { +func (m Model) getFilteredWizardNetworks() []map[string]any { if m.wizard.filterInput == "" { return m.wizard.privateNetworks } filter := strings.ToLower(m.wizard.filterInput) - var filtered []map[string]interface{} + var filtered []map[string]any for _, network := range m.wizard.privateNetworks { name := strings.ToLower(getString(network, "name")) id := getString(network, "id") @@ -13366,12 +13335,12 @@ func (m Model) getFilteredWizardNetworks() []map[string]interface{} { } // getFilteredWizardFloatingIPs returns filtered floating IPs based on wizard filter input -func (m Model) getFilteredWizardFloatingIPs() []map[string]interface{} { +func (m Model) getFilteredWizardFloatingIPs() []map[string]any { if m.wizard.filterInput == "" { return m.wizard.floatingIPs } filter := strings.ToLower(m.wizard.filterInput) - var filtered []map[string]interface{} + var filtered []map[string]any for _, fip := range m.wizard.floatingIPs { name := strings.ToLower(getString(fip, "name")) ip := strings.ToLower(getString(fip, "ip")) @@ -13880,10 +13849,10 @@ func (m Model) handleWizardNetworkKeys(key string, msg tea.KeyMsg) (tea.Model, t m.wizard.selectedPrivateNetworkName = getString(network, "name") // Store subnet ID if available - handle both []interface{} and []map[string]interface{} m.wizard.selectedSubnetId = "" - if subnets, ok := network["subnets"].([]map[string]interface{}); ok && len(subnets) > 0 { + if subnets, ok := network["subnets"].([]map[string]any); ok && len(subnets) > 0 { m.wizard.selectedSubnetId = getString(subnets[0], "id") - } else if subnets, ok := network["subnets"].([]interface{}); ok && len(subnets) > 0 { - if subnet, ok := subnets[0].(map[string]interface{}); ok { + } else if subnets, ok := network["subnets"].([]any); ok && len(subnets) > 0 { + if subnet, ok := subnets[0].(map[string]any); ok { m.wizard.selectedSubnetId = getString(subnet, "id") } } @@ -14504,7 +14473,7 @@ func (m Model) handleKubeWizardConfirmKeys(key string) (tea.Model, tea.Cmd) { // createKubeClusterWrapper wraps the cluster creation with proper data formatting func (m Model) createKubeClusterWrapper() tea.Cmd { // Build the creation payload - payload := map[string]interface{}{ + payload := map[string]any{ "name": m.wizard.kubeName, "region": m.wizard.selectedKubeRegion, "version": m.wizard.selectedKubeVersion, @@ -14662,7 +14631,7 @@ func (m Model) loadComputeSubProduct() (Model, tea.Cmd) { } // Helper functions -func getStringValue(data map[string]interface{}, key string, defaultVal string) string { +func getStringValue(data map[string]any, key string, defaultVal string) string { if val, ok := data[key]; ok { if str, ok := val.(string); ok { return str @@ -14672,7 +14641,7 @@ func getStringValue(data map[string]interface{}, key string, defaultVal string) return defaultVal } -func getFloatValue(data map[string]interface{}, key string, defaultVal float64) float64 { +func getFloatValue(data map[string]any, key string, defaultVal float64) float64 { if val, ok := data[key]; ok { if f, ok := val.(float64); ok { return f @@ -14681,7 +14650,7 @@ func getFloatValue(data map[string]interface{}, key string, defaultVal float64) return defaultVal } -func getBoolValue(data map[string]interface{}, key string, defaultVal bool) bool { +func getBoolValue(data map[string]any, key string, defaultVal bool) bool { if val, ok := data[key]; ok { if b, ok := val.(bool); ok { return b @@ -14691,7 +14660,7 @@ func getBoolValue(data map[string]interface{}, key string, defaultVal bool) bool } // getIntOrFloatValue extracts a numeric value that could be int or float64 in JSON -func getIntOrFloatValue(data map[string]interface{}, key string, defaultVal float64) float64 { +func getIntOrFloatValue(data map[string]any, key string, defaultVal float64) float64 { if val, ok := data[key]; ok { switch v := val.(type) { case float64: @@ -15084,7 +15053,7 @@ func (m Model) handleVolumeWizardConfirmKeys(key string) (tea.Model, tea.Cmd) { } // getNumericValue extracts a numeric value from a map, handling json.Number type -func getNumericValue(data map[string]interface{}, key string) float64 { +func getNumericValue(data map[string]any, key string) float64 { if val, ok := data[key]; ok { switch v := val.(type) { case json.Number: @@ -15406,4 +15375,3 @@ func (m Model) handleNodePoolDeleteConfirmKeyPress(msg tea.KeyMsg) (tea.Model, t return m, nil } } - diff --git a/internal/services/browser/object_api.go b/internal/services/browser/object_api.go index 8314a6557..e6213bf6d 100644 --- a/internal/services/browser/object_api.go +++ b/internal/services/browser/object_api.go @@ -46,15 +46,15 @@ func (m Model) fetchObjectStorageInitData() tea.Cmd { ch := make(chan probeResult, len(regionNames)) for _, name := range regionNames { go func(r string) { - var region map[string]interface{} + var region map[string]any ep := fmt.Sprintf("/v1/cloud/project/%s/region/%s", m.cloudProject, url.PathEscape(r)) if err := httpLib.Client.Get(ep, ®ion); err != nil { ch <- probeResult{region: r, supported: false} return } - services, _ := region["services"].([]interface{}) + services, _ := region["services"].([]any) for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if n, _ := sm["name"].(string); n == "storage-s3-high-perf" || n == "storage-s3-standard" { ch <- probeResult{region: r, supported: true} return @@ -75,7 +75,7 @@ func (m Model) fetchObjectStorageInitData() tea.Cmd { sort.Strings(supportedRegions) // Fetch cloud users - var users []map[string]interface{} + var users []map[string]any userEndpoint := fmt.Sprintf("/v1/cloud/project/%s/user", m.cloudProject) if err := httpLib.Client.Get(userEndpoint, &users); err != nil { // Non-fatal: continue without users @@ -112,15 +112,15 @@ func (m Model) fetchSwiftRegions() tea.Cmd { ch := make(chan probeResult, len(regionNames)) for _, name := range regionNames { go func(r string) { - var region map[string]interface{} + var region map[string]any ep := fmt.Sprintf("/v1/cloud/project/%s/region/%s", m.cloudProject, url.PathEscape(r)) if err := httpLib.Client.Get(ep, ®ion); err != nil { ch <- probeResult{region: r, supported: false} return } - services, _ := region["services"].([]interface{}) + services, _ := region["services"].([]any) for _, svc := range services { - if sm, ok := svc.(map[string]interface{}); ok { + if sm, ok := svc.(map[string]any); ok { if n, _ := sm["name"].(string); n == "storage" || n == "storage-object" { ch <- probeResult{region: r, supported: true} return @@ -150,25 +150,25 @@ func (m Model) createObjectContainer() tea.Cmd { } if m.wizard.objectTypeIdx == 1 { - body := map[string]interface{}{ + body := map[string]any{ "containerName": m.wizard.objectName, "region": m.wizard.objectSwiftRegion, } body["archive"] = (m.wizard.objectSwiftTypeIdx == 1) endpoint := fmt.Sprintf("/v1/cloud/project/%s/storage", m.cloudProject) - var container map[string]interface{} + var container map[string]any if err := httpLib.Client.Post(endpoint, body, &container); err != nil { return objectContainerCreatedMsg{err: fmt.Errorf("failed to create Swift container: %w", err)} } return objectContainerCreatedMsg{container: container} } - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.objectName, } // Encryption if m.wizard.objectEncryption { - body["encryption"] = map[string]interface{}{ + body["encryption"] = map[string]any{ "sseAlgorithm": "AES256", } } @@ -178,14 +178,14 @@ func (m Model) createObjectContainer() tea.Cmd { // Versioning if enableVersioning { - body["versioning"] = map[string]interface{}{ + body["versioning"] = map[string]any{ "status": "enabled", } } // Object Lock (requires versioning) if m.wizard.objectLock { - body["objectLock"] = map[string]interface{}{ + body["objectLock"] = map[string]any{ "status": "enabled", } } @@ -207,7 +207,7 @@ func (m Model) createObjectContainer() tea.Cmd { endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/storage", m.cloudProject, url.PathEscape(region)) - var container map[string]interface{} + var container map[string]any if err := httpLib.Client.Post(endpoint, body, &container); err != nil { return objectContainerCreatedMsg{err: fmt.Errorf("failed to create container: %w", err)} } @@ -226,7 +226,7 @@ func (m Model) updateSwiftContainerType(containerID, newType string) tea.Cmd { // Enable static website hosting endpoint := fmt.Sprintf("/v1/cloud/project/%s/storage/%s/static", m.cloudProject, url.PathEscape(containerID)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, nil, &result); err != nil { return swiftContainerUpdatedMsg{containerName: containerID, err: fmt.Errorf("failed to enable static: %w", err)} } @@ -237,7 +237,7 @@ func (m Model) updateSwiftContainerType(containerID, newType string) tea.Cmd { _ = httpLib.Client.Delete(staticEp, nil) // ignore error if wasn't static // Update container ACL via containerType field - body := map[string]interface{}{"containerType": newType} + body := map[string]any{"containerType": newType} endpoint := fmt.Sprintf("/v1/cloud/project/%s/storage/%s", m.cloudProject, url.PathEscape(containerID)) if err := httpLib.Client.Put(endpoint, body, nil); err != nil { @@ -268,7 +268,7 @@ func (m Model) addS3ContainerPolicy(containerName, region string, userID int64, } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/storage/%s/policy/%d", m.cloudProject, url.PathEscape(region), url.PathEscape(containerName), userID) - body := map[string]interface{}{"roleName": roleName} + body := map[string]any{"roleName": roleName} if err := httpLib.Client.Post(endpoint, body, nil); err != nil { return containerPolicyAddedMsg{containerName: containerName, err: fmt.Errorf("failed to add policy: %w", err)} } @@ -283,7 +283,7 @@ func (m Model) getS3Secret(userID int64, access string) tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/user/%d/s3Credentials/%s/secret", m.cloudProject, userID, url.PathEscape(access)) - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, nil, &result); err != nil { return s3SecretLoadedMsg{err: fmt.Errorf("failed to retrieve secret: %w", err)} } @@ -298,7 +298,7 @@ func (m Model) enableS3User(userID int64) tea.Cmd { return s3UserActionDoneMsg{action: object_storage.UserActionEnable, err: fmt.Errorf("no cloud project selected")} } endpoint := fmt.Sprintf("/v1/cloud/project/%s/user/%d/s3Credentials", m.cloudProject, userID) - var cred map[string]interface{} + var cred map[string]any if err := httpLib.Client.Post(endpoint, nil, &cred); err != nil { return s3UserActionDoneMsg{action: object_storage.UserActionEnable, err: fmt.Errorf("failed to create credentials: %w", err)} } @@ -343,11 +343,11 @@ func (m Model) createS3User() tea.Cmd { return s3UserCreatedMsg{err: fmt.Errorf("no cloud project selected")} } - body := map[string]interface{}{ + body := map[string]any{ "description": m.wizard.s3UserDesc, "role": "objectstore_operator", } - var user map[string]interface{} + var user map[string]any userEndpoint := fmt.Sprintf("/v1/cloud/project/%s/user", m.cloudProject) if err := httpLib.Client.Post(userEndpoint, body, &user); err != nil { return s3UserCreatedMsg{err: fmt.Errorf("failed to create user: %w", err)} @@ -371,8 +371,8 @@ func (m Model) createS3User() tea.Cmd { // Poll until user status is "ok" (max ~30s) userGetEndpoint := fmt.Sprintf("/v1/cloud/project/%s/user/%d", m.cloudProject, userId) - for i := 0; i < 30; i++ { - var u map[string]interface{} + for range 30 { + var u map[string]any if err := httpLib.Client.Get(userGetEndpoint, &u); err == nil { if status, _ := u["status"].(string); status == "ok" { user = u @@ -383,7 +383,7 @@ func (m Model) createS3User() tea.Cmd { } // Create S3 credentials for the user - var credentials map[string]interface{} + var credentials map[string]any credsEndpoint := fmt.Sprintf("/v1/cloud/project/%s/user/%d/s3Credentials", m.cloudProject, userId) if err := httpLib.Client.Post(credsEndpoint, nil, &credentials); err != nil { return s3UserCreatedMsg{user: user, err: fmt.Errorf("user created but S3 credentials failed: %w", err)} @@ -534,7 +534,7 @@ func (m Model) handleS3CredentialsSaved(msg s3CredentialsSavedMsg) (tea.Model, t } // createObjectStorageTable builds the table for S3 containers. -func createObjectStorageTable(data []map[string]interface{}, width, height int) table.Model { +func createObjectStorageTable(data []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Name", Width: 28}, {Title: "Location", Width: 12}, @@ -617,10 +617,7 @@ func createObjectStorageTable(data []map[string]interface{}, width, height int) rows = append(rows, table.Row{name, region, deployMode, offer, objectsCount, sizeStr, containerType}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } @@ -646,7 +643,7 @@ func createObjectStorageTable(data []map[string]interface{}, width, height int) } // createObjectStorageUsersTable creates a table to display S3 users/credentials. -func createObjectStorageUsersTable(users []map[string]interface{}, width, height int) table.Model { +func createObjectStorageUsersTable(users []map[string]any, width, height int) table.Model { columns := []table.Column{ {Title: "Name", Width: 30}, {Title: "Description", Width: 40}, @@ -683,10 +680,7 @@ func createObjectStorageUsersTable(users []map[string]interface{}, width, height rows = append(rows, table.Row{name, description, accessKey, s3User}) } - tableHeight := height - 15 - if tableHeight < 5 { - tableHeight = 5 - } + tableHeight := max(height-15, 5) if tableHeight > 20 { tableHeight = 20 } diff --git a/internal/services/browser/object_wizard.go b/internal/services/browser/object_wizard.go index ba9df88e1..7d394891e 100644 --- a/internal/services/browser/object_wizard.go +++ b/internal/services/browser/object_wizard.go @@ -97,10 +97,7 @@ func (m Model) renderObjectWizardRegionStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(m.wizard.objectRegions) { - endIdx = len(m.wizard.objectRegions) - } + endIdx := min(startIdx+maxVisible, len(m.wizard.objectRegions)) if startIdx > 0 { content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")).Render(" ↑ more") + "\n") } @@ -702,10 +699,7 @@ func (m Model) renderObjectWizardSwiftRegionStep(width int) string { if m.wizard.selectedIndex >= maxVisible { startIdx = m.wizard.selectedIndex - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(m.wizard.objectSwiftRegions) { - endIdx = len(m.wizard.objectSwiftRegions) - } + endIdx := min(startIdx+maxVisible, len(m.wizard.objectSwiftRegions)) if startIdx > 0 { content.WriteString(itemStyle.Render(fmt.Sprintf(" (...%d more above)", startIdx)) + "\n") @@ -800,4 +794,3 @@ func (m Model) handleS3CredentialsViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) } return m, nil } - diff --git a/internal/services/browser/private_network_wizard.go b/internal/services/browser/private_network_wizard.go index 842d1b861..8729d1573 100644 --- a/internal/services/browser/private_network_wizard.go +++ b/internal/services/browser/private_network_wizard.go @@ -39,11 +39,11 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { // Build region list from selected region region := m.wizard.selectedRegion - var network map[string]interface{} + var network map[string]any if m.wizard.privNetIsLocalZone { // Local zones use the regional network API (isolated, not vRack-based) - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.privNetName, } if m.wizard.privNetVlanID > 0 { @@ -51,7 +51,7 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network", m.cloudProject, url.PathEscape(region)) - var op map[string]interface{} + var op map[string]any if err := httpLib.Client.Post(endpoint, body, &op); err != nil { return privNetCreatedMsg{err: fmt.Errorf("failed to create network: %w", err)} } @@ -63,7 +63,7 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { if netID == "" { return privNetCreatedMsg{err: fmt.Errorf("network created but ID missing in response")} } - network = map[string]interface{}{"id": netID, "name": m.wizard.privNetName} + network = map[string]any{"id": netID, "name": m.wizard.privNetName} // Optionally create a subnet using the regional subnet API if m.wizard.privNetEnableSubnet && m.wizard.privNetCIDR != "" { @@ -73,7 +73,7 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { if len(ipParts) == 4 { gatewayIP = ipParts[0] + "." + ipParts[1] + "." + ipParts[2] + ".1" } - subnetBody := map[string]interface{}{ + subnetBody := map[string]any{ "name": m.wizard.privNetName + "-subnet", "cidr": m.wizard.privNetCIDR, "ipVersion": 4, @@ -85,10 +85,10 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { } subnetEndpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network/%s/subnet", m.cloudProject, url.PathEscape(region), url.PathEscape(netID)) - var subnet map[string]interface{} + var subnet map[string]any // Retry briefly to let the network activate var subnetErr error - for i := 0; i < 10; i++ { + for range 10 { subnetErr = httpLib.Client.Post(subnetEndpoint, subnetBody, &subnet) if subnetErr == nil { break @@ -104,7 +104,7 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { } } else { // Standard vRack regions use the legacy global private network API - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.privNetName, "regions": []string{region}, } @@ -128,12 +128,12 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { m.cloudProject, url.PathEscape(netID)) const maxAttempts = 15 regionActive := false - for i := 0; i < maxAttempts; i++ { - var netData map[string]interface{} + for range maxAttempts { + var netData map[string]any if err := httpLib.Client.Get(networkEndpoint, &netData); err == nil { - if regions, ok := netData["regions"].([]interface{}); ok { + if regions, ok := netData["regions"].([]any); ok { for _, r := range regions { - if rMap, ok := r.(map[string]interface{}); ok { + if rMap, ok := r.(map[string]any); ok { if rMap["region"] == region { if rMap["status"] == "ACTIVE" { regionActive = true @@ -157,7 +157,7 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { noGateway := m.wizard.privNetGatewayMode == 1 // mode 1 = will attach OVH Gateway service - subnetBody := map[string]interface{}{ + subnetBody := map[string]any{ "dhcp": m.wizard.privNetEnableDHCP, "network": m.wizard.privNetCIDR, "noGateway": noGateway, @@ -165,7 +165,7 @@ func (m Model) createPrivateNetworkFromWizard() tea.Cmd { "start": m.wizard.privNetAllocStart, "end": m.wizard.privNetAllocEnd, } - var subnet map[string]interface{} + var subnet map[string]any subnetEndpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private/%s/subnet", m.cloudProject, url.PathEscape(netID)) if err := httpLib.Client.Post(subnetEndpoint, subnetBody, &subnet); err != nil { @@ -228,9 +228,9 @@ func (m Model) renderPrivNetWizardRegionStep(width int) string { } label := e.name if i == m.wizard.privNetRegionIdx { - content.WriteString(selectedStyle.Render("▶ " + label) + "\n") + content.WriteString(selectedStyle.Render("▶ "+label) + "\n") } else { - content.WriteString(dimStyle.Render(" " + label) + "\n") + content.WriteString(dimStyle.Render(" "+label) + "\n") } } @@ -869,13 +869,13 @@ func (m Model) createSubnetForNetwork() tea.Cmd { // Check if region is already activated on the network; if not, activate it first networkEndpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private/%s", m.cloudProject, url.PathEscape(netID)) - var netData map[string]interface{} + var netData map[string]any regionActive := false openstackID := "" if err := httpLib.Client.Get(networkEndpoint, &netData); err == nil { - if regions, ok := netData["regions"].([]interface{}); ok { + if regions, ok := netData["regions"].([]any); ok { for _, rv := range regions { - if rm, ok := rv.(map[string]interface{}); ok { + if rm, ok := rv.(map[string]any); ok { if rm["region"] == region { regionActive = true openstackID, _ = rm["openstackId"].(string) @@ -887,18 +887,18 @@ func (m Model) createSubnetForNetwork() tea.Cmd { if !regionActive { // Activate the region on the network first activateEndpoint := fmt.Sprintf("/v1/cloud/project/%s/network/private/%s/region", m.cloudProject, url.PathEscape(netID)) - var op map[string]interface{} - if err := httpLib.Client.Post(activateEndpoint, map[string]interface{}{"region": region}, &op); err != nil { + var op map[string]any + if err := httpLib.Client.Post(activateEndpoint, map[string]any{"region": region}, &op); err != nil { return subnetAddedMsg{networkID: netID, err: fmt.Errorf("failed to activate region %s on network: %w", region, err)} } // Poll until ACTIVE and capture the openstackId - for i := 0; i < 20; i++ { + for range 20 { time.Sleep(3 * time.Second) - var nd map[string]interface{} + var nd map[string]any if err := httpLib.Client.Get(networkEndpoint, &nd); err == nil { - if regs, ok := nd["regions"].([]interface{}); ok { + if regs, ok := nd["regions"].([]any); ok { for _, rv := range regs { - if rm, ok := rv.(map[string]interface{}); ok { + if rm, ok := rv.(map[string]any); ok { if rm["region"] == region && rm["status"] == "ACTIVE" { regionActive = true openstackID, _ = rm["openstackId"].(string) @@ -928,18 +928,18 @@ func (m Model) createSubnetForNetwork() tea.Cmd { enableGateway := m.wizard.privNetGatewayMode != 1 // mode 1 = noGateway - subnetBody := map[string]interface{}{ + subnetBody := map[string]any{ "cidr": m.wizard.privNetCIDR, "enableDhcp": m.wizard.privNetEnableDHCP, "enableGatewayIp": enableGateway, "ipVersion": ipVersion, "name": fmt.Sprintf("%s-%s", m.wizard.privNetName, region), - "allocationPools": []map[string]interface{}{ + "allocationPools": []map[string]any{ {"start": m.wizard.privNetAllocStart, "end": m.wizard.privNetAllocEnd}, }, } - var subnet map[string]interface{} + var subnet map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/network/%s/subnet", m.cloudProject, url.PathEscape(region), url.PathEscape(openstackID)) if err := httpLib.Client.Post(endpoint, subnetBody, &subnet); err != nil { diff --git a/internal/services/browser/views/block_storage/backup_detail.go b/internal/services/browser/views/block_storage/backup_detail.go index 5cdad0e92..af870d498 100644 --- a/internal/services/browser/views/block_storage/backup_detail.go +++ b/internal/services/browser/views/block_storage/backup_detail.go @@ -32,7 +32,7 @@ const ( // ExecuteBackupActionMsg is dispatched when a backup action is confirmed. type ExecuteBackupActionMsg struct { - Backup map[string]interface{} + Backup map[string]any Action int VolumeID string // for Restore action VolumeName string // for CreateVolume action @@ -41,23 +41,23 @@ type ExecuteBackupActionMsg struct { // BackupVolumesLoadedMsg is sent by the manager after loading volumes for restore picker. type BackupVolumesLoadedMsg struct { - Volumes []map[string]interface{} + Volumes []map[string]any } // BackupDetailView displays a volume backup with Delete, Restore and Create Volume actions. type BackupDetailView struct { views.BaseView - backup map[string]interface{} + backup map[string]any selectedAction int confirmMode bool subMenu int - restoreVolumes []map[string]interface{} + restoreVolumes []map[string]any restoreIdx int nameInput string sizeInput string } -func NewBackupDetailView(ctx *views.Context, backup map[string]interface{}) *BackupDetailView { +func NewBackupDetailView(ctx *views.Context, backup map[string]any) *BackupDetailView { return &BackupDetailView{ BaseView: views.NewBaseView(ctx), backup: backup, @@ -65,7 +65,7 @@ func NewBackupDetailView(ctx *views.Context, backup map[string]interface{}) *Bac } // SetRestoreVolumes is called by the manager after volumes are loaded. -func (v *BackupDetailView) SetRestoreVolumes(volumes []map[string]interface{}) { +func (v *BackupDetailView) SetRestoreVolumes(volumes []map[string]any) { v.restoreVolumes = volumes v.restoreIdx = 0 } @@ -134,10 +134,7 @@ func (v *BackupDetailView) renderActions() string { if v.restoreIdx >= maxVisible { startIdx = v.restoreIdx - maxVisible + 1 } - endIdx := startIdx + maxVisible - if endIdx > len(v.restoreVolumes) { - endIdx = len(v.restoreVolumes) - } + endIdx := min(startIdx+maxVisible, len(v.restoreVolumes)) if startIdx > 0 { sb.WriteString(dimStyle.Render(fmt.Sprintf(" (...%d above)", startIdx)) + "\n") } @@ -320,7 +317,7 @@ func (v *BackupDetailView) HandleKey(msg tea.KeyMsg) tea.Cmd { // LoadBackupRestoreVolumesMsg asks the manager to fetch volumes for the restore picker. type LoadBackupRestoreVolumesMsg struct { - Backup map[string]interface{} + Backup map[string]any } func (v *BackupDetailView) Title() string { diff --git a/internal/services/browser/views/block_storage/detail.go b/internal/services/browser/views/block_storage/detail.go index a2c869a5c..4c201d0e4 100644 --- a/internal/services/browser/views/block_storage/detail.go +++ b/internal/services/browser/views/block_storage/detail.go @@ -27,16 +27,16 @@ var volumeActionLabels = []string{"Delete", "Rename", "Extend"} // DetailView displays block storage volume details with actions. type DetailView struct { views.BaseView - volume map[string]interface{} + volume map[string]any selectedAction int confirmMode bool - renameMode bool - renameInput string - extendMode bool - extendInput string + renameMode bool + renameInput string + extendMode bool + extendInput string } -func NewDetailView(ctx *views.Context, volume map[string]interface{}) *DetailView { +func NewDetailView(ctx *views.Context, volume map[string]any) *DetailView { return &DetailView{ BaseView: views.NewBaseView(ctx), volume: volume, @@ -284,12 +284,12 @@ func (v *DetailView) HelpText() string { } type ExecuteVolumeActionMsg struct { - Volume map[string]interface{} + Volume map[string]any Action int Param string } -func getBootable(volume map[string]interface{}) string { +func getBootable(volume map[string]any) string { if b, ok := volume["bootable"].(bool); ok { if b { return "Yes" @@ -299,9 +299,9 @@ func getBootable(volume map[string]interface{}) string { return "-" } -func getAttachedTo(volume map[string]interface{}) []string { +func getAttachedTo(volume map[string]any) []string { var result []string - if raw, ok := volume["attachedTo"].([]interface{}); ok { + if raw, ok := volume["attachedTo"].([]any); ok { for _, item := range raw { if id, ok := item.(string); ok { result = append(result, id) diff --git a/internal/services/browser/views/block_storage/snapshot_detail.go b/internal/services/browser/views/block_storage/snapshot_detail.go index 48f70e36f..1444617fc 100644 --- a/internal/services/browser/views/block_storage/snapshot_detail.go +++ b/internal/services/browser/views/block_storage/snapshot_detail.go @@ -16,7 +16,7 @@ import ( ) const ( - SnapshotActionDelete = iota + SnapshotActionDelete = iota SnapshotActionCreateVolume ) @@ -30,7 +30,7 @@ const ( // ExecuteSnapshotActionMsg is dispatched when snapshot action is confirmed. type ExecuteSnapshotActionMsg struct { - Snapshot map[string]interface{} + Snapshot map[string]any Action int VolumeName string VolumeSize string // GB as string @@ -39,7 +39,7 @@ type ExecuteSnapshotActionMsg struct { // SnapshotDetailView displays a volume snapshot with Delete and Create Volume actions. type SnapshotDetailView struct { views.BaseView - snapshot map[string]interface{} + snapshot map[string]any selectedAction int confirmMode bool subMenu int @@ -47,7 +47,7 @@ type SnapshotDetailView struct { sizeInput string } -func NewSnapshotDetailView(ctx *views.Context, snapshot map[string]interface{}) *SnapshotDetailView { +func NewSnapshotDetailView(ctx *views.Context, snapshot map[string]any) *SnapshotDetailView { return &SnapshotDetailView{ BaseView: views.NewBaseView(ctx), snapshot: snapshot, diff --git a/internal/services/browser/views/block_storage/table.go b/internal/services/browser/views/block_storage/table.go index d0177d5d0..f88d1073e 100644 --- a/internal/services/browser/views/block_storage/table.go +++ b/internal/services/browser/views/block_storage/table.go @@ -20,13 +20,13 @@ import ( type TableView struct { views.BaseView table table.Model - data []map[string]interface{} + data []map[string]any filterMode bool filterInput string - filteredData []map[string]interface{} + filteredData []map[string]any } -func NewTableView(ctx *views.Context, data []map[string]interface{}) *TableView { +func NewTableView(ctx *views.Context, data []map[string]any) *TableView { v := &TableView{ BaseView: views.NewBaseView(ctx), data: data, @@ -57,10 +57,7 @@ func (v *TableView) createTable() table.Model { } ctx := v.Context() - height := ctx.Height - 15 - if height < 5 { - height = 5 - } + height := max(ctx.Height-15, 5) if height > 20 { height = 20 } @@ -174,7 +171,7 @@ func (v *TableView) HelpText() string { return "↑↓: Navigate • /: Filter • v: Details • d: Debug • p: Projects • q: Quit" } -func (v *TableView) GetSelectedVolume() map[string]interface{} { +func (v *TableView) GetSelectedVolume() map[string]any { idx := v.table.Cursor() if idx >= 0 && idx < len(v.filteredData) { return v.filteredData[idx] @@ -183,7 +180,7 @@ func (v *TableView) GetSelectedVolume() map[string]interface{} { } // UpdateData updates the table with new data. -func (v *TableView) UpdateData(data []map[string]interface{}) { +func (v *TableView) UpdateData(data []map[string]any) { cursor := v.table.Cursor() v.data = data v.applyFilter() @@ -193,17 +190,17 @@ func (v *TableView) UpdateData(data []map[string]interface{}) { } type ShowVolumeDetailMsg struct { - Volume map[string]interface{} + Volume map[string]any } -func getString(m map[string]interface{}, key string) string { +func getString(m map[string]any, key string) string { if v, ok := m[key].(string); ok { return v } return "" } -func getSizeStr(volume map[string]interface{}) string { +func getSizeStr(volume map[string]any) string { switch v := volume["size"].(type) { case float64: return fmt.Sprintf("%d", int(v)) diff --git a/internal/services/browser/views/file_storage/detail.go b/internal/services/browser/views/file_storage/detail.go index f9d074ce7..d675e8e19 100644 --- a/internal/services/browser/views/file_storage/detail.go +++ b/internal/services/browser/views/file_storage/detail.go @@ -28,7 +28,7 @@ var fileShareActionLabels = []string{"Delete", "Rename", "Extend"} // DetailView displays file storage share details with actions. type DetailView struct { views.BaseView - share map[string]interface{} + share map[string]any selectedAction int confirmMode bool renameMode bool @@ -38,7 +38,7 @@ type DetailView struct { } // NewDetailView creates a detail view for a file share. -func NewDetailView(ctx *views.Context, share map[string]interface{}) *DetailView { +func NewDetailView(ctx *views.Context, share map[string]any) *DetailView { return &DetailView{ BaseView: views.NewBaseView(ctx), share: share, @@ -95,7 +95,7 @@ func (v *DetailView) renderActions() string { Width(40) return views.StyleStatusWarning.Render("New name:") + "\n" + inputStyle.Render(v.renameInput+"▌") + "\n\n" + - views.StyleFooter.Render("Enter: Confirm • Esc: Cancel") + views.StyleFooter.Render("Enter: Confirm • Esc: Cancel") } if v.extendMode { @@ -106,10 +106,10 @@ func (v *DetailView) renderActions() string { Padding(0, 1). Width(20) return views.StyleStatusWarning.Render( - fmt.Sprintf("New size in GB (current: %s GB, minimum: %s GB, must be larger):", currentSize, currentSize), - ) + "\n" + - inputStyle.Render(v.extendInput+"▌") + "\n\n" + - views.StyleFooter.Render("Enter: Confirm • Esc: Cancel") + fmt.Sprintf("New size in GB (current: %s GB, minimum: %s GB, must be larger):", currentSize, currentSize), + ) + "\n" + + inputStyle.Render(v.extendInput+"▌") + "\n\n" + + views.StyleFooter.Render("Enter: Confirm • Esc: Cancel") } var parts []string @@ -263,7 +263,7 @@ func (v *DetailView) Title() string { // HelpText returns the footer help text. func (v *DetailView) HelpText() string { if v.renameMode || v.extendMode { - return "Type value • Enter: Confirm • Esc: Cancel" + return "Type value • Enter: Confirm • Esc: Cancel" } if v.confirmMode { return "Enter: Confirm action • Esc: Cancel" @@ -273,19 +273,19 @@ func (v *DetailView) HelpText() string { // ExecuteFileShareActionMsg is dispatched when the user confirms an action. type ExecuteFileShareActionMsg struct { - Share map[string]interface{} + Share map[string]any Action int Param string } -func getString(m map[string]interface{}, key string) string { +func getString(m map[string]any, key string) string { if v, ok := m[key].(string); ok { return v } return "" } -func getSizeStr(share map[string]interface{}) string { +func getSizeStr(share map[string]any) string { switch v := share["size"].(type) { case float64: return fmt.Sprintf("%d", int(v)) diff --git a/internal/services/browser/views/instances/detail.go b/internal/services/browser/views/instances/detail.go index dba14e541..5b155b2ce 100644 --- a/internal/services/browser/views/instances/detail.go +++ b/internal/services/browser/views/instances/detail.go @@ -30,7 +30,7 @@ var actionLabels = []string{"Start", "Stop", "Soft Reboot", "Reboot", "SSH", "De // DetailView displays instance details with actions. type DetailView struct { views.BaseView - instance map[string]interface{} + instance map[string]any imageName string floatingIP string selectedAction int @@ -38,7 +38,7 @@ type DetailView struct { } // NewDetailView creates a new instance detail view. -func NewDetailView(ctx *views.Context, instance map[string]interface{}, imageName, floatingIP string) *DetailView { +func NewDetailView(ctx *views.Context, instance map[string]any, imageName, floatingIP string) *DetailView { return &DetailView{ BaseView: views.NewBaseView(ctx), instance: instance, @@ -69,9 +69,9 @@ func (v *DetailView) Render(width, height int) string { // Get IP addresses var publicIPs, privateIPs []string - if addresses, ok := v.instance["ipAddresses"].([]interface{}); ok { + if addresses, ok := v.instance["ipAddresses"].([]any); ok { for _, addr := range addresses { - if addrMap, ok := addr.(map[string]interface{}); ok { + if addrMap, ok := addr.(map[string]any); ok { ip := getString(addrMap, "ip") version := getString(addrMap, "version") ipType := getString(addrMap, "type") @@ -201,7 +201,7 @@ func (v *DetailView) HelpText() string { } func (v *DetailView) getFlavorName() string { - if flavor, ok := v.instance["flavor"].(map[string]interface{}); ok { + if flavor, ok := v.instance["flavor"].(map[string]any); ok { if name, ok := flavor["name"].(string); ok { return name } @@ -211,6 +211,6 @@ func (v *DetailView) getFlavorName() string { // ExecuteInstanceActionMsg signals to execute an action on an instance. type ExecuteInstanceActionMsg struct { - Instance map[string]interface{} + Instance map[string]any Action int } diff --git a/internal/services/browser/views/instances/table.go b/internal/services/browser/views/instances/table.go index 53314b75d..5dd3bd4e8 100644 --- a/internal/services/browser/views/instances/table.go +++ b/internal/services/browser/views/instances/table.go @@ -20,16 +20,16 @@ import ( type TableView struct { views.BaseView table table.Model - data []map[string]interface{} + data []map[string]any imageMap map[string]string // imageId -> imageName floatingIPMap map[string]string // instanceId -> floatingIP filterMode bool filterInput string - filteredData []map[string]interface{} + filteredData []map[string]any } // NewTableView creates a new instance table view. -func NewTableView(ctx *views.Context, data []map[string]interface{}, imageMap, floatingIPMap map[string]string) *TableView { +func NewTableView(ctx *views.Context, data []map[string]any, imageMap, floatingIPMap map[string]string) *TableView { v := &TableView{ BaseView: views.NewBaseView(ctx), data: data, @@ -81,10 +81,7 @@ func (v *TableView) createTable() table.Model { } ctx := v.Context() - height := ctx.Height - 15 - if height < 5 { - height = 5 - } + height := max(ctx.Height-15, 5) if height > 20 { height = 20 } @@ -209,7 +206,7 @@ func (v *TableView) HelpText() string { } // GetSelectedInstance returns the currently selected instance. -func (v *TableView) GetSelectedInstance() map[string]interface{} { +func (v *TableView) GetSelectedInstance() map[string]any { idx := v.table.Cursor() if idx >= 0 && idx < len(v.filteredData) { return v.filteredData[idx] @@ -218,7 +215,7 @@ func (v *TableView) GetSelectedInstance() map[string]interface{} { } // UpdateData updates the table with new data. -func (v *TableView) UpdateData(data []map[string]interface{}, imageMap, floatingIPMap map[string]string) { +func (v *TableView) UpdateData(data []map[string]any, imageMap, floatingIPMap map[string]string) { cursor := v.table.Cursor() v.data = data v.imageMap = imageMap @@ -231,21 +228,21 @@ func (v *TableView) UpdateData(data []map[string]interface{}, imageMap, floating // ShowInstanceDetailMsg signals to show instance detail. type ShowInstanceDetailMsg struct { - Instance map[string]interface{} + Instance map[string]any } // Helper functions -func getString(m map[string]interface{}, key string) string { +func getString(m map[string]any, key string) string { if v, ok := m[key].(string); ok { return v } return "" } -func getFirstIP(instance map[string]interface{}) string { - if addresses, ok := instance["ipAddresses"].([]interface{}); ok { +func getFirstIP(instance map[string]any) string { + if addresses, ok := instance["ipAddresses"].([]any); ok { for _, addr := range addresses { - if addrMap, ok := addr.(map[string]interface{}); ok { + if addrMap, ok := addr.(map[string]any); ok { if ip, ok := addrMap["ip"].(string); ok { return ip } diff --git a/internal/services/browser/views/kubernetes/detail.go b/internal/services/browser/views/kubernetes/detail.go index 23632b416..232660c26 100644 --- a/internal/services/browser/views/kubernetes/detail.go +++ b/internal/services/browser/views/kubernetes/detail.go @@ -31,14 +31,14 @@ var clusterActionLabels = []string{"Kubeconfig", "K9s", "Pools", "Upgrade", "Pol // DetailView displays Kubernetes cluster details with actions. type DetailView struct { views.BaseView - cluster map[string]interface{} - nodePools []map[string]interface{} + cluster map[string]any + nodePools []map[string]any selectedAction int confirmMode bool } // NewDetailView creates a new cluster detail view. -func NewDetailView(ctx *views.Context, cluster map[string]interface{}, nodePools []map[string]interface{}) *DetailView { +func NewDetailView(ctx *views.Context, cluster map[string]any, nodePools []map[string]any) *DetailView { return &DetailView{ BaseView: views.NewBaseView(ctx), cluster: cluster, @@ -193,17 +193,17 @@ func (v *DetailView) HelpText() string { } // UpdateNodePools updates the node pools list. -func (v *DetailView) UpdateNodePools(nodePools []map[string]interface{}) { +func (v *DetailView) UpdateNodePools(nodePools []map[string]any) { v.nodePools = nodePools } // ExecuteClusterActionMsg signals to execute an action on a cluster. type ExecuteClusterActionMsg struct { - Cluster map[string]interface{} + Cluster map[string]any Action int } -func getIntValue(m map[string]interface{}, key string) int { +func getIntValue(m map[string]any, key string) int { if v, ok := m[key]; ok { switch val := v.(type) { case int: diff --git a/internal/services/browser/views/kubernetes/nodepools.go b/internal/services/browser/views/kubernetes/nodepools.go index 74d5acfcf..d7a0eb9de 100644 --- a/internal/services/browser/views/kubernetes/nodepools.go +++ b/internal/services/browser/views/kubernetes/nodepools.go @@ -19,14 +19,14 @@ import ( type NodePoolsView struct { views.BaseView table table.Model - cluster map[string]interface{} - nodePools []map[string]interface{} + cluster map[string]any + nodePools []map[string]any filterInput string filtering bool } // NewNodePoolsView creates a new node pools list view. -func NewNodePoolsView(ctx *views.Context, cluster map[string]interface{}, nodePools []map[string]interface{}) *NodePoolsView { +func NewNodePoolsView(ctx *views.Context, cluster map[string]any, nodePools []map[string]any) *NodePoolsView { v := &NodePoolsView{ BaseView: views.NewBaseView(ctx), cluster: cluster, @@ -176,7 +176,7 @@ func (v *NodePoolsView) applyFilter() { } filter := strings.ToLower(v.filterInput) - filtered := make([]map[string]interface{}, 0) + filtered := make([]map[string]any, 0) for _, pool := range v.nodePools { name := strings.ToLower(getString(pool, "name")) flavor := strings.ToLower(getString(pool, "flavor")) @@ -206,25 +206,25 @@ func (v *NodePoolsView) HelpText() string { // ShowNodePoolDetailMsg signals to show node pool details. type ShowNodePoolDetailMsg struct { - Cluster map[string]interface{} + Cluster map[string]any PoolName string } // CreateNodePoolMsg signals to create a new node pool. type CreateNodePoolMsg struct { - Cluster map[string]interface{} + Cluster map[string]any } // RefreshNodePoolsMsg signals to refresh the node pools list. type RefreshNodePoolsMsg struct { - Cluster map[string]interface{} + Cluster map[string]any } // NodePoolDetailView displays details for a single node pool. type NodePoolDetailView struct { views.BaseView - cluster map[string]interface{} - nodePool map[string]interface{} + cluster map[string]any + nodePool map[string]any selectedAction int confirmMode bool } @@ -238,7 +238,7 @@ const ( var nodePoolActionLabels = []string{"Scale", "Delete"} // NewNodePoolDetailView creates a new node pool detail view. -func NewNodePoolDetailView(ctx *views.Context, cluster, nodePool map[string]interface{}) *NodePoolDetailView { +func NewNodePoolDetailView(ctx *views.Context, cluster, nodePool map[string]any) *NodePoolDetailView { return &NodePoolDetailView{ BaseView: views.NewBaseView(ctx), cluster: cluster, @@ -379,12 +379,12 @@ func (v *NodePoolDetailView) HelpText() string { // ExecuteNodePoolActionMsg signals to execute an action on a node pool. type ExecuteNodePoolActionMsg struct { - Cluster map[string]interface{} - NodePool map[string]interface{} + Cluster map[string]any + NodePool map[string]any Action int } -func getBool(m map[string]interface{}, key string) bool { +func getBool(m map[string]any, key string) bool { if v, ok := m[key]; ok { if b, ok := v.(bool); ok { return b @@ -403,8 +403,8 @@ func boolToStr(b bool) string { // NodePoolScaleView displays the scale form for a node pool. type NodePoolScaleView struct { views.BaseView - cluster map[string]interface{} - nodePool map[string]interface{} + cluster map[string]any + nodePool map[string]any fields []ScaleField selectedField int errorMsg string @@ -419,7 +419,7 @@ type ScaleField struct { } // NewNodePoolScaleView creates a new node pool scale view. -func NewNodePoolScaleView(ctx *views.Context, cluster, nodePool map[string]interface{}) *NodePoolScaleView { +func NewNodePoolScaleView(ctx *views.Context, cluster, nodePool map[string]any) *NodePoolScaleView { desired := getIntValue(nodePool, "desiredNodes") minNodes := getIntValue(nodePool, "minNodes") maxNodes := getIntValue(nodePool, "maxNodes") @@ -547,8 +547,8 @@ func (v *NodePoolScaleView) HelpText() string { // SubmitNodePoolScaleMsg signals to submit the node pool scale. type SubmitNodePoolScaleMsg struct { - Cluster map[string]interface{} - NodePool map[string]interface{} + Cluster map[string]any + NodePool map[string]any DesiredNodes int MinNodes int MaxNodes int @@ -557,7 +557,7 @@ type SubmitNodePoolScaleMsg struct { // UpdatePolicyView displays the update policy selection. type UpdatePolicyView struct { views.BaseView - cluster map[string]interface{} + cluster map[string]any policies []string selectedPolicy int } @@ -565,7 +565,7 @@ type UpdatePolicyView struct { var updatePolicies = []string{"ALWAYS_UPDATE", "MINIMAL_DOWNTIME", "NEVER_UPDATE"} // NewUpdatePolicyView creates a new update policy view. -func NewUpdatePolicyView(ctx *views.Context, cluster map[string]interface{}) *UpdatePolicyView { +func NewUpdatePolicyView(ctx *views.Context, cluster map[string]any) *UpdatePolicyView { currentPolicy := getString(cluster, "updatePolicy") selected := 0 for i, p := range updatePolicies { @@ -658,20 +658,20 @@ func (v *UpdatePolicyView) HelpText() string { // SubmitUpdatePolicyMsg signals to submit the update policy change. type SubmitUpdatePolicyMsg struct { - Cluster map[string]interface{} + Cluster map[string]any Policy string } // UpgradeView displays the version upgrade selection. type UpgradeView struct { views.BaseView - cluster map[string]interface{} + cluster map[string]any versions []string selectedVersion int } // NewUpgradeView creates a new upgrade view. -func NewUpgradeView(ctx *views.Context, cluster map[string]interface{}, versions []string) *UpgradeView { +func NewUpgradeView(ctx *views.Context, cluster map[string]any, versions []string) *UpgradeView { return &UpgradeView{ BaseView: views.NewBaseView(ctx), cluster: cluster, @@ -765,20 +765,20 @@ func (v *UpgradeView) HelpText() string { // SubmitUpgradeMsg signals to submit the cluster upgrade. type SubmitUpgradeMsg struct { - Cluster map[string]interface{} + Cluster map[string]any Version string } // DeleteConfirmView displays a delete confirmation dialog. type DeleteConfirmView struct { views.BaseView - cluster map[string]interface{} - nodePool map[string]interface{} // nil for cluster delete + cluster map[string]any + nodePool map[string]any // nil for cluster delete confirmed bool } // NewDeleteConfirmView creates a new delete confirmation view. -func NewDeleteConfirmView(ctx *views.Context, cluster, nodePool map[string]interface{}) *DeleteConfirmView { +func NewDeleteConfirmView(ctx *views.Context, cluster, nodePool map[string]any) *DeleteConfirmView { return &DeleteConfirmView{ BaseView: views.NewBaseView(ctx), cluster: cluster, @@ -860,6 +860,6 @@ func (v *DeleteConfirmView) HelpText() string { // ConfirmDeleteMsg signals deletion was confirmed. type ConfirmDeleteMsg struct { - Cluster map[string]interface{} - NodePool map[string]interface{} + Cluster map[string]any + NodePool map[string]any } diff --git a/internal/services/browser/views/kubernetes/table.go b/internal/services/browser/views/kubernetes/table.go index 1757286ff..c2f90a994 100644 --- a/internal/services/browser/views/kubernetes/table.go +++ b/internal/services/browser/views/kubernetes/table.go @@ -20,14 +20,14 @@ import ( type TableView struct { views.BaseView table table.Model - data []map[string]interface{} + data []map[string]any filterMode bool filterInput string - filteredData []map[string]interface{} + filteredData []map[string]any } // NewTableView creates a new Kubernetes table view. -func NewTableView(ctx *views.Context, data []map[string]interface{}) *TableView { +func NewTableView(ctx *views.Context, data []map[string]any) *TableView { v := &TableView{ BaseView: views.NewBaseView(ctx), data: data, @@ -58,10 +58,7 @@ func (v *TableView) createTable() table.Model { } ctx := v.Context() - height := ctx.Height - 15 - if height < 5 { - height = 5 - } + height := max(ctx.Height-15, 5) if height > 20 { height = 20 } @@ -176,7 +173,7 @@ func (v *TableView) HelpText() string { } // GetSelectedCluster returns the currently selected cluster. -func (v *TableView) GetSelectedCluster() map[string]interface{} { +func (v *TableView) GetSelectedCluster() map[string]any { idx := v.table.Cursor() if idx >= 0 && idx < len(v.filteredData) { return v.filteredData[idx] @@ -185,7 +182,7 @@ func (v *TableView) GetSelectedCluster() map[string]interface{} { } // UpdateData updates the table with new data. -func (v *TableView) UpdateData(data []map[string]interface{}) { +func (v *TableView) UpdateData(data []map[string]any) { cursor := v.table.Cursor() v.data = data v.applyFilter() @@ -196,18 +193,18 @@ func (v *TableView) UpdateData(data []map[string]interface{}) { // ShowClusterDetailMsg signals to show cluster detail. type ShowClusterDetailMsg struct { - Cluster map[string]interface{} + Cluster map[string]any } // Helper functions -func getString(m map[string]interface{}, key string) string { +func getString(m map[string]any, key string) string { if v, ok := m[key].(string); ok { return v } return "" } -func getNodeCount(cluster map[string]interface{}) string { +func getNodeCount(cluster map[string]any) string { // Try different possible fields if nodes, ok := cluster["nodesCount"]; ok { return fmt.Sprintf("%v", nodes) diff --git a/internal/services/browser/views/object_storage/detail.go b/internal/services/browser/views/object_storage/detail.go index 57b520b05..cd9ba39cf 100644 --- a/internal/services/browser/views/object_storage/detail.go +++ b/internal/services/browser/views/object_storage/detail.go @@ -17,16 +17,16 @@ import ( // Action identifiers for object storage container detail view. const ( - ContainerActionDelete = iota - ContainerActionChangeType - ContainerActionAddPolicy + ContainerActionDelete = iota + ContainerActionChangeType + ContainerActionAddPolicy ) const ( subMenuNone = 0 subMenuChangeType = 1 - subMenuAddPolicy = 2 - subMenuPickRole = 3 + subMenuAddPolicy = 2 + subMenuPickRole = 3 ) var swiftTypeOptions = []string{"Private", "Public", "Static"} @@ -41,8 +41,8 @@ type containerAction struct { // DetailView displays object storage container details with actions. type DetailView struct { views.BaseView - container map[string]interface{} - users []map[string]interface{} // cloud users + container map[string]any + users []map[string]any // cloud users selectedAction int confirmMode bool subMenu int @@ -51,7 +51,7 @@ type DetailView struct { } // NewDetailView creates a detail view for a container. -func NewDetailView(ctx *views.Context, container map[string]interface{}, users []map[string]interface{}) *DetailView { +func NewDetailView(ctx *views.Context, container map[string]any, users []map[string]any) *DetailView { return &DetailView{ BaseView: views.NewBaseView(ctx), container: container, @@ -124,7 +124,7 @@ func (v *DetailView) renderInfo() string { } else { // Versioning versioningStatus := "-" - if vers, ok := v.container["versioning"].(map[string]interface{}); ok { + if vers, ok := v.container["versioning"].(map[string]any); ok { if s, ok := vers["status"].(string); ok { versioningStatus = s } @@ -133,7 +133,7 @@ func (v *DetailView) renderInfo() string { // Encryption encryptionStatus := "No encryption" encryptionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")) - if enc, ok := v.container["encryption"].(map[string]interface{}); ok { + if enc, ok := v.container["encryption"].(map[string]any); ok { if alg, _ := enc["sseAlgorithm"].(string); alg != "" { encryptionStatus = "SSE-OMK (OVHcloud-managed keys)" encryptionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF7F")) @@ -142,7 +142,7 @@ func (v *DetailView) renderInfo() string { // Object lock objectLockStatus := "-" - if ol, ok := v.container["objectLock"].(map[string]interface{}); ok { + if ol, ok := v.container["objectLock"].(map[string]any); ok { if s, _ := ol["status"].(string); s != "" { objectLockStatus = s } @@ -221,9 +221,9 @@ func (v *DetailView) renderChangeTypeMenu(width int) string { return views.RenderBox("Change container type", content.String(), width-4) } -func (v *DetailView) policyUserCandidates() []map[string]interface{} { +func (v *DetailView) policyUserCandidates() []map[string]any { seen := map[string]bool{} - var result []map[string]interface{} + var result []map[string]any for _, u := range v.users { uid := fmt.Sprintf("%v", u["_userId"]) if uid == "" || uid == "0" || uid == "" { @@ -392,7 +392,7 @@ func (v *DetailView) handlePickRoleKey(key string) tea.Cmd { return ExecuteContainerActionMsg{ Container: container, Action: ContainerActionAddPolicy, - ExtraData: map[string]interface{}{ + ExtraData: map[string]any{ "userId": selectedUser["_userId"], "roleName": roleName, }, @@ -423,7 +423,7 @@ func (v *DetailView) handleSubMenuKey(key string) tea.Cmd { return ExecuteContainerActionMsg{ Container: container, Action: ContainerActionChangeType, - ExtraData: map[string]interface{}{"containerType": newType}, + ExtraData: map[string]any{"containerType": newType}, } } case "esc": @@ -432,7 +432,6 @@ func (v *DetailView) handleSubMenuKey(key string) tea.Cmd { return nil } - // ─── Metadata ──────────────────────────────────────────────────────────────── func (v *DetailView) Title() string { @@ -457,21 +456,21 @@ func (v *DetailView) HelpText() string { // ExecuteContainerActionMsg is dispatched when the user confirms an action. type ExecuteContainerActionMsg struct { - Container map[string]interface{} + Container map[string]any Action int - ExtraData map[string]interface{} + ExtraData map[string]any } // ─── Helpers ────────────────────────────────────────────────────────────────── -func getString(m map[string]interface{}, key string) string { +func getString(m map[string]any, key string) string { if v, ok := m[key].(string); ok { return v } return "" } -func getCountStr(m map[string]interface{}, fields ...string) string { +func getCountStr(m map[string]any, fields ...string) string { for _, f := range fields { if v, ok := m[f]; ok { if n, ok := v.(float64); ok { @@ -482,7 +481,7 @@ func getCountStr(m map[string]interface{}, fields ...string) string { return "-" } -func getSizeStr(m map[string]interface{}, fields ...string) string { +func getSizeStr(m map[string]any, fields ...string) string { for _, f := range fields { if v, ok := m[f]; ok { if n, ok := v.(float64); ok { @@ -499,4 +498,3 @@ func getSizeStr(m map[string]interface{}, fields ...string) string { } return "-" } - diff --git a/internal/services/browser/views/object_storage/user_detail.go b/internal/services/browser/views/object_storage/user_detail.go index bfea46a23..41c5be870 100644 --- a/internal/services/browser/views/object_storage/user_detail.go +++ b/internal/services/browser/views/object_storage/user_detail.go @@ -26,7 +26,7 @@ const ( // UserDetailView displays S3 user details with activate/deactivate and secret key actions. type UserDetailView struct { views.BaseView - user map[string]interface{} + user map[string]any selectedAction int confirmMode bool secretKey string // populated after Show Secret action @@ -34,7 +34,7 @@ type UserDetailView struct { } // NewUserDetailView creates a detail view for an S3 user. -func NewUserDetailView(ctx *views.Context, user map[string]interface{}) *UserDetailView { +func NewUserDetailView(ctx *views.Context, user map[string]any) *UserDetailView { return &UserDetailView{ BaseView: views.NewBaseView(ctx), user: user, @@ -230,6 +230,6 @@ func (v *UserDetailView) HelpText() string { // ExecuteUserActionMsg is dispatched when the user confirms an action. type ExecuteUserActionMsg struct { - User map[string]interface{} + User map[string]any Action int } diff --git a/internal/services/browser/views/styles.go b/internal/services/browser/views/styles.go index ce0fbf253..447b02652 100644 --- a/internal/services/browser/views/styles.go +++ b/internal/services/browser/views/styles.go @@ -110,8 +110,8 @@ var ( Padding(0, 1) StyleButtonDisabled = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#444444")). - Padding(0, 1) + Foreground(lipgloss.Color("#444444")). + Padding(0, 1) StyleButtonDanger = lipgloss.NewStyle(). Foreground(ColorDanger). diff --git a/internal/services/browser/workflow_wizard.go b/internal/services/browser/workflow_wizard.go index d0bd70c6f..a98b8634a 100644 --- a/internal/services/browser/workflow_wizard.go +++ b/internal/services/browser/workflow_wizard.go @@ -66,10 +66,7 @@ func (m Model) renderWorkflowWizardInstanceStep(width int) string { if m.wizard.wfInstanceIdx >= maxVisible { start = m.wizard.wfInstanceIdx - maxVisible + 1 } - end := start + maxVisible - if end > len(m.wizard.wfInstances) { - end = len(m.wizard.wfInstances) - } + end := min(start+maxVisible, len(m.wizard.wfInstances)) for i := start; i < end; i++ { inst := m.wizard.wfInstances[i] label := getStringValue(inst, "name", getStringValue(inst, "id", "unknown")) @@ -160,7 +157,7 @@ func (m Model) renderWorkflowWizardConfirmStep(width int) string { b.WriteString(labelStyle.Render(" Rotation :") + valStyle.Render(fmt.Sprintf("%d backups", m.wizard.wfRotation)) + "\n\n") if m.wizard.isLoading { - b.WriteString(loadingStyle.Render("⏳ Creating...")) + b.WriteString(loadingStyle.Render("⏳ Creating...")) return b.String() } if m.wizard.errorMsg != "" { @@ -310,7 +307,7 @@ func (m Model) handleWorkflowWizardConfirmKeys(key string) (tea.Model, tea.Cmd) // ─── API ────────────────────────────────────────────────────────────────────── type workflowInstancesLoadedMsg struct { - instances []map[string]interface{} + instances []map[string]any err error } @@ -324,7 +321,7 @@ func (m Model) fetchWorkflowInstances() tea.Cmd { if m.cloudProject == "" { return workflowInstancesLoadedMsg{err: fmt.Errorf("no cloud project selected")} } - var instances []map[string]interface{} + var instances []map[string]any endpoint := fmt.Sprintf("/v1/cloud/project/%s/instance", m.cloudProject) if err := httpLib.Client.Get(endpoint, &instances); err != nil { return workflowInstancesLoadedMsg{err: err} @@ -340,13 +337,13 @@ func (m Model) createWorkflow() tea.Cmd { } endpoint := fmt.Sprintf("/v1/cloud/project/%s/region/%s/workflow/backup", m.cloudProject, url.PathEscape(m.wizard.wfRegion)) - body := map[string]interface{}{ + body := map[string]any{ "name": m.wizard.wfName, "instanceId": m.wizard.wfInstanceId, "cron": m.wizard.wfCron, "rotation": m.wizard.wfRotation, } - var result map[string]interface{} + var result map[string]any if err := httpLib.Client.Post(endpoint, body, &result); err != nil { return workflowCreatedMsg{err: err} } diff --git a/internal/services/login/logout.go b/internal/services/login/logout.go index 639cbadd7..3ad71737b 100644 --- a/internal/services/login/logout.go +++ b/internal/services/login/logout.go @@ -95,7 +95,6 @@ func confirmLogout(section string) bool { // isInvalidCredentialError reports whether err is an OVHcloud API error caused // by credentials that are already invalid or revoked (HTTP 401/403). func isInvalidCredentialError(err error) bool { - var apiErr *ovh.APIError - return errors.As(err, &apiErr) && - (apiErr.Code == http.StatusUnauthorized || apiErr.Code == http.StatusForbidden) + apiErr, ok := errors.AsType[*ovh.APIError](err) + return ok && (apiErr.Code == http.StatusForbidden || apiErr.Code == http.StatusUnauthorized) } diff --git a/internal/services/login/logout_test.go b/internal/services/login/logout_test.go index 884adbc28..69c85e7a6 100644 --- a/internal/services/login/logout_test.go +++ b/internal/services/login/logout_test.go @@ -25,7 +25,7 @@ func TestIsInvalidCredentialError(t *testing.T) { {"unauthorized (401)", &ovh.APIError{Code: http.StatusUnauthorized}, true}, {"other API error (500)", &ovh.APIError{Code: http.StatusInternalServerError}, false}, {"not found (404)", &ovh.APIError{Code: http.StatusNotFound}, false}, - {"wrapped forbidden", fmt.Errorf("revoke failed: %w", &ovh.APIError{Code: http.StatusForbidden}), true}, + {"wrapped forbidden", fmt.Errorf("revoke failed: %w", error(&ovh.APIError{Code: http.StatusForbidden})), true}, // go vet reads static type {"plain error", errors.New("network unreachable"), false}, } diff --git a/internal/services/webhosting/webhosting.go b/internal/services/webhosting/webhosting.go index b24ec8b4b..f4198f501 100644 --- a/internal/services/webhosting/webhosting.go +++ b/internal/services/webhosting/webhosting.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "log" + "maps" "net/url" "os" "sort" @@ -3518,9 +3519,7 @@ func updateResource(cmd *cobra.Command, pathSpec, endpoint string, params map[st if err := httpLib.Client.Get(endpoint, ¤t); err != nil { return fmt.Errorf("error fetching %s: %w", endpoint, err) } - for k, v := range cleaned { - current[k] = v - } + maps.Copy(current, cleaned) source = current } From cd8e8a3f2a486167ed8a1ce5cd1232f9da900611 Mon Sep 17 00:00:00 2001 From: FrostByte0x <271714451+FrostByte0x@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:11:00 +0200 Subject: [PATCH 2/2] allow more siblings in openapi spec 3.0 for existing APIs Signed-off-by: FrostByte0x <271714451+FrostByte0x@users.noreply.github.com> --- internal/openapi/openapi.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/openapi/openapi.go b/internal/openapi/openapi.go index c89e512af..7f8102bb7 100644 --- a/internal/openapi/openapi.go +++ b/internal/openapi/openapi.go @@ -77,8 +77,8 @@ func getRequestBodyFromSpec(spec []byte, path, method string) (*openapi3.MediaTy if err != nil { return nil, fmt.Errorf("failed to load spec: %w", err) } - // Remove AllowExtraSiblings when all OVH APIs are > 3.0 to allow extra fields with $ref - if err = doc.Validate(context.Background(), openapi3.AllowExtraSiblingFields("description")); err != nil { + // Remove AllowExtraSiblings when all OVH APIs are > 3.0 to allow extra fields with $ref, unlike 3.0. + if err = doc.Validate(context.Background(), openapi3.AllowExtraSiblingFields("description", "nullable", "readOnly")); err != nil { return nil, fmt.Errorf("failed to validate spec: %w", err) }