diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..abc1fe8 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,68 @@ +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript +# Special requirements: +# * csharp: Requires the presence of a .sln file in the project folder. +language: typescript + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "cloudly" diff --git a/changelog.md b/changelog.md index 866c58e..6ef5f2a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # Changelog +## 2025-09-07 - 5.2.0 - feat(settings) +Add runtime settings management, node & baremetal managers, and settings UI + +- Introduce CloudlySettingsManager to store runtime settings in an EasyStore (MongoDB) with API handlers for get/update/clear/test. +- Add settings data/interface and typedrequest definitions (ts_interfaces/data/settings.ts, ts_interfaces/requests/settings.ts) and expose via interfaces index. +- Add web UI for managing provider credentials and connections (ts_web/elements/cloudly-view-settings.ts) and integrate the Settings view into the dashboard. +- Replace the previous ServerManager concept with NodeManager and BaremetalManager: new ClusterNode and BareMetal models and managers (auto-provisioning / Hetzner integration), plus curlfresh moved to node manager. +- Update Cluster data shape (servers -> nodes) and adjust related code paths (overview stats, cluster creation and provisioning flows). +- Use settingsManager for provider tokens (cloudflareToken, hetznerToken) instead of reading tokens directly from config/env; connector and manager init code updated accordingly. +- Add numerous implementations and API handlers to support baremetal/node lifecycle and control (getBaremetalServers, controlBaremetal, getNodeConfig, node provisioning helpers). +- Reorder Cloudly startup to initialize MongoDB and settings manager before managers that depend on settings; wire settingsManager into Cloudly class. +- Bump package dependency versions for @git.zone/tsdoc, @design.estate/dees-catalog and @push.rocks/taskbuffer in package.json. + ## 2025-09-05 - 5.1.0 - feat(cluster) Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades diff --git a/package.json b/package.json index 0199385..70631f7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "@git.zone/tsbuild": "^2.6.8", "@git.zone/tsbundle": "^2.5.1", - "@git.zone/tsdoc": "^1.5.1", + "@git.zone/tsdoc": "^1.5.2", "@git.zone/tspublish": "^1.10.3", "@git.zone/tstest": "^2.3.6", "@git.zone/tswatch": "^2.2.1", @@ -39,7 +39,7 @@ "@apiclient.xyz/docker": "^1.3.5", "@apiclient.xyz/hetznercloud": "^1.2.0", "@apiclient.xyz/slack": "^3.0.9", - "@design.estate/dees-catalog": "^1.10.12", + "@design.estate/dees-catalog": "^1.11.2", "@design.estate/dees-domtools": "^2.3.3", "@design.estate/dees-element": "^2.1.2", "@git.zone/tsrun": "^1.3.3", @@ -71,7 +71,7 @@ "@push.rocks/smartstream": "^3.2.5", "@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartunique": "^3.0.9", - "@push.rocks/taskbuffer": "^3.1.10", + "@push.rocks/taskbuffer": "^3.4.0", "@push.rocks/webjwt": "^1.0.9", "@tsclass/tsclass": "^9.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0982da..363d665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^3.0.9 version: 3.0.9 '@design.estate/dees-catalog': - specifier: ^1.10.12 - version: 1.10.12(@tiptap/pm@2.26.1) + specifier: ^1.11.2 + version: 1.11.2(@tiptap/pm@2.26.1) '@design.estate/dees-domtools': specifier: ^2.3.3 version: 2.3.3 @@ -129,8 +129,8 @@ importers: specifier: ^3.0.9 version: 3.0.9 '@push.rocks/taskbuffer': - specifier: ^3.1.10 - version: 3.1.10 + specifier: ^3.4.0 + version: 3.4.0 '@push.rocks/webjwt': specifier: ^1.0.9 version: 1.0.9 @@ -145,8 +145,8 @@ importers: specifier: ^2.5.1 version: 2.5.1 '@git.zone/tsdoc': - specifier: ^1.5.1 - version: 1.5.1(ws@8.18.3)(zod@3.25.76) + specifier: ^1.5.2 + version: 1.5.2(ws@8.18.3)(zod@3.25.76) '@git.zone/tspublish': specifier: ^1.10.3 version: 1.10.3 @@ -476,8 +476,8 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} - '@design.estate/dees-catalog@1.10.12': - resolution: {integrity: sha512-KoP+ftLsMs2Nyqid25NvhIXc1npeju17a2+B/eYnkaVjrQxiyrLKprzH2457in7qtRY+RZaU3Z/Ux4xwhSDOJw==} + '@design.estate/dees-catalog@1.11.2': + resolution: {integrity: sha512-gMK+wDKXDBPzfWmaJySotjjp5A9rwk2PQANQF8V6Q52xUfKKUv7gHj4eju+pN6qkUA5OUzdCDplUeUCrA8i37w==} '@design.estate/dees-comms@1.0.27': resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==} @@ -497,6 +497,9 @@ packages: '@emnapi/runtime@1.4.5': resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} @@ -820,8 +823,8 @@ packages: resolution: {integrity: sha512-esKuSrl1WMOTMDLNt38i16VfLe/gRZt2ZAJ3Yw7slfs7sj583MKqNFqO57zmhknk1Sya6f9Wys89aCzIJkcqlg==} engines: {node: '>=6'} - '@gerrit0/mini-shiki@3.9.2': - resolution: {integrity: sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==} + '@gerrit0/mini-shiki@3.12.2': + resolution: {integrity: sha512-HKZPmO8OSSAAo20H2B3xgJdxZaLTwtlMwxg0967scnrDlPwe6j5+ULGHyIqwgTbFCn9yv/ff8CmfWZLE9YKBzA==} '@git.zone/tsbuild@2.6.8': resolution: {integrity: sha512-g1z7+MxiYD0xMfuqn8NSWitbfK1OaF0Qolmw7WOmUsHmNF60T1AR02Lo4DtNmnjSpchA+xzDFAQzL1xTcQA39w==} @@ -831,8 +834,8 @@ packages: resolution: {integrity: sha512-gBskgM3ECy9FEmhCWnQahDyFCAjjw/7emjx/KYM/FOlPqGV+hmYzt368zwSlkzOGgYF8k9OZ+mp6vexDL/+f2w==} hasBin: true - '@git.zone/tsdoc@1.5.1': - resolution: {integrity: sha512-tfuLL0cg0vwU5DwSHxVlFpvyDENXZfEnewvFFXydeE18jT0+B8lbP807P1fg6FSXtaBd4o4vHMO2ITtqTz9DUw==} + '@git.zone/tsdoc@1.5.2': + resolution: {integrity: sha512-orSwqHjmmbwLcdlOuxlJNrvc0Ga3Bv4jxHvhLaiisnpN+XUgsGfHS7gtlreZtc4CE0eo/iwDF/OxVd19A5nA3A==} hasBin: true '@git.zone/tspublish@1.10.3': @@ -1365,6 +1368,9 @@ packages: '@push.rocks/levelcache@3.1.1': resolution: {integrity: sha512-+JpDNEt+EuvmbtADGH9SkODxBy+slHDDzs43mAbuMbwpVvi6uNuMK0Mkhrfz9UFpxUSp+cJE/jl/OxdpD0xL1A==} + '@push.rocks/levelcache@3.2.0': + resolution: {integrity: sha512-Ch0Oguta2I0SVi704kHghhBcgfyfS92ua1elRu9d8X1/9LMRYuqvvBAnyXyFxQzI3S8q8QC6EkRdd8CAAYSzRg==} + '@push.rocks/lik@6.1.0': resolution: {integrity: sha512-BoSAIRFNryQ8Sd5EP+35ZBj6vAQ1C60/XjZIO2O65XDyLG8xz7xJ+u5Wm8/fjIJ0WX3h8GkkaCz2tJM34nFT3A==} @@ -1578,9 +1584,6 @@ packages: '@push.rocks/smartshell@3.2.3': resolution: {integrity: sha512-BWA/DH1H9lG7Er23d4uYgirfYaya5dX4g/WpWm2la7mOzuL9o2FnPIhel52DQUKIh7ty3Ql305ApV8YaAb4+/w==} - '@push.rocks/smartshell@3.2.4': - resolution: {integrity: sha512-zZEKfRl3qBaII9BJULk4rB/+EelUpgM2/qHCQLui7c4589HTge4o0nWn+olFw/Hge88qUO77OK1sN7hQFZ6zeg==} - '@push.rocks/smartshell@3.3.0': resolution: {integrity: sha512-m0w618H6YBs+vXGz1CgS4nPi5CUAnqRtckcS9/koGwfcIx1IpjqmiP47BoCTbdgcv0IPUxQVBG1IXTHPuZ8Z5g==} @@ -1629,8 +1632,8 @@ packages: '@push.rocks/smartyaml@2.0.5': resolution: {integrity: sha512-tBcf+HaOIfeEsTMwgUZDtZERCxXQyRsWO8Ar5DjBdiSRchbhVGZQEBzXswMS0W5ZoRenjgPK+4tPW3JQGRTfbg==} - '@push.rocks/taskbuffer@3.1.10': - resolution: {integrity: sha512-jT+FxRSk0+IP17q9LD1/Ks8GJBn5TZWgLtfnKRHW/LAZ1bHX/2ARZvAV8fm1T4WMU5s7PyId+y4fkoohG/5Nkg==} + '@push.rocks/taskbuffer@3.4.0': + resolution: {integrity: sha512-Rvwr1CzYztB9PMboojRzVSq3xGp8288kvtvWx4Mg3rvps913znMja1UOjNn52ivOxu3dHUNYE3NDSP+j84cUWQ==} '@push.rocks/webjwt@1.0.9': resolution: {integrity: sha512-IhWAv0hxfXbLbmQHHOGr96Oe3H1kB0OTtDofM8N+9qhJeKxTHfF2pUrdpck6btAQQbaBY2D7xtHvumrIXU5HIg==} @@ -1899,17 +1902,17 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/engine-oniguruma@3.9.2': - resolution: {integrity: sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==} + '@shikijs/engine-oniguruma@3.12.2': + resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==} - '@shikijs/langs@3.9.2': - resolution: {integrity: sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==} + '@shikijs/langs@3.12.2': + resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} - '@shikijs/themes@3.9.2': - resolution: {integrity: sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==} + '@shikijs/themes@3.12.2': + resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} - '@shikijs/types@3.9.2': - resolution: {integrity: sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==} + '@shikijs/types@3.12.2': + resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2724,8 +2727,8 @@ packages: any-base@1.1.0: resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} - apexcharts@5.3.4: - resolution: {integrity: sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==} + apexcharts@5.3.5: + resolution: {integrity: sha512-I04DY/WBZbJgJD2uixeV5EzyiL+J5LgKQXEu8rctqAwyRmKv44aDVeofJoLdTJe3ao4r2KEQfCgtVzXn6pqirg==} argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3646,8 +3649,8 @@ packages: resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==} engines: {node: '>=16'} - gpt-tokenizer@2.9.0: - resolution: {integrity: sha512-YSpexBL/k4bfliAzMrRqn3M6+it02LutVyhVpDeMKrC/O9+pCe/5s8U2hYKa2vFLD5/vHhsKc8sOn/qGqII8Kg==} + gpt-tokenizer@3.0.1: + resolution: {integrity: sha512-5jdaspBq/w4sWw322SvQj1Fku+CN4OAfYZeeEg8U7CWtxBz+zkxZ3h0YOHD43ee+nZYZ5Ud70HRN0ANcdIj4qg==} graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -3899,8 +3902,8 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} - isomorphic-git@1.33.0: - resolution: {integrity: sha512-a90aVhiBFtkUUe8JaqmR0gL7Thk1Ol/30rLS9c7nM20CwSbVqDctnwxX9VFSDLz5iq1wyzV6p4uyU7GStQKkag==} + isomorphic-git@1.33.1: + resolution: {integrity: sha512-Fy5rPAncURJoqL9R+5nJXLl5rQH6YpcjJd7kdCoRJPhrBiLVkLm9b+esRqYQQlT1hKVtKtALbfNtpHjWWJgk6g==} engines: {node: '>=14.17'} hasBin: true @@ -4541,8 +4544,8 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openai@5.12.2: - resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==} + openai@5.19.1: + resolution: {integrity: sha512-zSqnUF7oR9ksmpusKkpUgkNrj8Sl57U+OyzO8jzc7LUjTMg4DRfR3uCm+EIMA6iw06sRPNp4t7ojp3sCpEUZRQ==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -4818,8 +4821,8 @@ packages: prosemirror-transform@1.10.4: resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} - prosemirror-view@1.40.1: - resolution: {integrity: sha512-pbwUjt3G7TlsQQHDiYSupWBhJswpLVB09xXm1YiJPdkjkh9Pe7Y51XdLh5VWIZmROLY8UpUpG03lkdhm9lzIBA==} + prosemirror-view@1.41.0: + resolution: {integrity: sha512-FatMIIl0vRHMcNc3sPy3cMw5MMyWuO1nWQxqvYpJvXAruucGvmQ2tyyjT2/Lbok77T9a/qZqBVCq4sj43V2ihw==} proto-list@1.2.4: resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=} @@ -5407,8 +5410,8 @@ packages: typed-query-selector@2.12.0: resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} - typedoc@0.28.10: - resolution: {integrity: sha512-zYvpjS2bNJ30SoNYfHSRaFpBMZAsL7uwKbWwqoCNFWjcPnI3e/mPLh2SneH9mX7SJxtDpvDgvd9/iZxGbo7daw==} + typedoc@0.28.12: + resolution: {integrity: sha512-H5ODu4f7N+myG4MfuSp2Vh6wV+WLoZaEYxKPt2y8hmmqNEMVrH69DAjjdmYivF4tP/C2jrIZCZhPalZlTU/ipA==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: @@ -5672,8 +5675,8 @@ packages: resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} engines: {node: '>= 4.0.0'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} zod@3.25.76: @@ -5731,7 +5734,7 @@ snapshots: '@push.rocks/smartsitemap': 2.0.3 '@push.rocks/smartstream': 3.2.5 '@push.rocks/smarttime': 4.1.1 - '@push.rocks/taskbuffer': 3.1.10 + '@push.rocks/taskbuffer': 3.4.0 '@push.rocks/webrequest': 3.0.37 '@push.rocks/webstore': 2.0.20 '@tsclass/tsclass': 9.2.0 @@ -6681,7 +6684,7 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@design.estate/dees-catalog@1.10.12(@tiptap/pm@2.26.1)': + '@design.estate/dees-catalog@1.11.2(@tiptap/pm@2.26.1)': dependencies: '@design.estate/dees-domtools': 2.3.3 '@design.estate/dees-element': 2.1.2 @@ -6701,7 +6704,7 @@ snapshots: '@tiptap/starter-kit': 2.26.1 '@tsclass/tsclass': 9.2.0 '@webcontainer/api': 1.2.0 - apexcharts: 5.3.4 + apexcharts: 5.3.5 highlight.js: 11.11.1 ibantools: 4.5.1 lucide: 0.542.0 @@ -6784,6 +6787,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.0.4': dependencies: tslib: 2.8.1 @@ -6957,12 +6965,12 @@ snapshots: dependencies: '@fortawesome/fontawesome-common-types': 7.0.1 - '@gerrit0/mini-shiki@3.9.2': + '@gerrit0/mini-shiki@3.12.2': dependencies: - '@shikijs/engine-oniguruma': 3.9.2 - '@shikijs/langs': 3.9.2 - '@shikijs/themes': 3.9.2 - '@shikijs/types': 3.9.2 + '@shikijs/engine-oniguruma': 3.12.2 + '@shikijs/langs': 3.12.2 + '@shikijs/themes': 3.12.2 + '@shikijs/types': 3.12.2 '@shikijs/vscode-textmate': 10.0.2 '@git.zone/tsbuild@2.6.8': @@ -6977,8 +6985,11 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 typescript: 5.9.2 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt + - react - supports-color + - vue '@git.zone/tsbundle@2.5.1': dependencies: @@ -7001,7 +7012,7 @@ snapshots: - '@swc/helpers' - supports-color - '@git.zone/tsdoc@1.5.1(ws@8.18.3)(zod@3.25.76)': + '@git.zone/tsdoc@1.5.2(ws@8.18.3)(zod@3.25.76)': dependencies: '@git.zone/tspublish': 1.10.3 '@push.rocks/early': 4.0.4 @@ -7016,17 +7027,20 @@ snapshots: '@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartpath': 6.0.0 - '@push.rocks/smartshell': 3.2.4 + '@push.rocks/smartshell': 3.3.0 '@push.rocks/smarttime': 4.1.1 - gpt-tokenizer: 2.9.0 - typedoc: 0.28.10(typescript@5.9.2) + gpt-tokenizer: 3.0.1 + typedoc: 0.28.12(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt - bare-buffer - bufferutil + - react - supports-color - utf-8-validate + - vue - ws - zod @@ -7042,8 +7056,11 @@ snapshots: '@push.rocks/smartrequest': 4.3.1 '@push.rocks/smartshell': 3.3.0 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt + - react - supports-color + - vue '@git.zone/tsrun@1.3.3': dependencies: @@ -7110,7 +7127,7 @@ snapshots: '@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartshell': 3.2.3 - '@push.rocks/taskbuffer': 3.1.10 + '@push.rocks/taskbuffer': 3.4.0 transitivePeerDependencies: - '@nuxt/kit' - '@swc/helpers' @@ -7196,7 +7213,7 @@ snapshots: '@img/sharp-wasm32@0.34.3': dependencies: - '@emnapi/runtime': 1.4.5 + '@emnapi/runtime': 1.5.0 optional: true '@img/sharp-win32-arm64@0.34.3': @@ -7214,7 +7231,7 @@ snapshots: '@inquirer/figures': 1.0.13 '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 '@inquirer/confirm@4.0.1': dependencies: @@ -7234,7 +7251,7 @@ snapshots: signal-exit: 4.1.0 strip-ansi: 6.0.1 wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 '@inquirer/editor@3.0.1': dependencies: @@ -7246,7 +7263,7 @@ snapshots: dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 2.0.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 '@inquirer/figures@1.0.13': {} @@ -7283,14 +7300,14 @@ snapshots: dependencies: '@inquirer/core': 9.2.1 '@inquirer/type': 2.0.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 '@inquirer/search@2.0.1': dependencies: '@inquirer/core': 9.2.1 '@inquirer/figures': 1.0.13 '@inquirer/type': 2.0.0 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 '@inquirer/select@3.0.1': dependencies: @@ -7298,7 +7315,7 @@ snapshots: '@inquirer/figures': 1.0.13 '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 '@inquirer/type@2.0.0': dependencies: @@ -7792,10 +7809,36 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartstring': 4.0.15 '@push.rocks/smartunique': 3.0.9 - '@push.rocks/taskbuffer': 3.1.10 + '@push.rocks/taskbuffer': 3.4.0 '@tsclass/tsclass': 4.4.4 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt + - react + - supports-color + - vue + + '@push.rocks/levelcache@3.2.0': + dependencies: + '@push.rocks/lik': 6.2.2 + '@push.rocks/smartbucket': 3.3.10 + '@push.rocks/smartcache': 1.0.18 + '@push.rocks/smartenv': 5.0.13 + '@push.rocks/smartexit': 1.0.23 + '@push.rocks/smartfile': 11.2.7 + '@push.rocks/smartjson': 5.0.20 + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartpromise': 4.2.3 + '@push.rocks/smartstring': 4.0.15 + '@push.rocks/smartunique': 3.0.9 + '@push.rocks/taskbuffer': 3.4.0 + '@tsclass/tsclass': 9.2.0 + transitivePeerDependencies: + - '@nuxt/kit' + - aws-crt + - react + - supports-color + - vue '@push.rocks/lik@6.1.0': dependencies: @@ -7840,8 +7883,13 @@ snapshots: '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 - '@push.rocks/taskbuffer': 3.1.10 + '@push.rocks/taskbuffer': 3.4.0 '@tsclass/tsclass': 9.2.0 + transitivePeerDependencies: + - '@nuxt/kit' + - react + - supports-color + - vue '@push.rocks/projectinfo@5.0.2': dependencies: @@ -7901,14 +7949,17 @@ snapshots: '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 4.3.1 '@push.rocks/webstream': 1.0.10 - openai: 5.12.2(ws@8.18.3)(zod@3.25.76) + openai: 5.19.1(ws@8.18.3)(zod@3.25.76) transitivePeerDependencies: + - '@nuxt/kit' - aws-crt - bare-buffer - bufferutil + - react - supports-color - typescript - utf-8-validate + - vue - ws - zod @@ -8027,19 +8078,22 @@ snapshots: '@push.rocks/smartstring': 4.0.15 '@push.rocks/smarttime': 4.1.1 '@push.rocks/smartunique': 3.0.9 - '@push.rocks/taskbuffer': 3.1.10 + '@push.rocks/taskbuffer': 3.4.0 '@tsclass/tsclass': 9.2.0 mongodb: 6.18.0(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' + - '@nuxt/kit' - aws-crt - gcp-metadata - kerberos - mongodb-client-encryption + - react - snappy - socks - supports-color + - vue '@push.rocks/smartdelay@3.0.5': dependencies: @@ -8138,12 +8192,12 @@ snapshots: '@push.rocks/smartfile': 11.2.7 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartshell': 3.2.4 + '@push.rocks/smartshell': 3.3.0 '@push.rocks/smartstring': 4.0.15 '@push.rocks/smarttime': 4.1.1 '@types/diff': 8.0.0 diff: 8.0.2 - isomorphic-git: 1.33.0 + isomorphic-git: 1.33.1 '@push.rocks/smartguard@3.1.0': dependencies: @@ -8176,7 +8230,7 @@ snapshots: '@push.rocks/smartjimp@1.2.0': dependencies: - '@push.rocks/levelcache': 3.1.1 + '@push.rocks/levelcache': 3.2.0 '@push.rocks/smartfile': 11.2.7 '@push.rocks/smarthash': 3.2.3 '@push.rocks/smartpath': 6.0.0 @@ -8184,7 +8238,11 @@ snapshots: jimp: 1.6.0 sharp: 0.34.3 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt + - react + - supports-color + - vue '@push.rocks/smartjson@5.0.20': dependencies: @@ -8277,13 +8335,16 @@ snapshots: transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' + - '@nuxt/kit' - aws-crt - gcp-metadata - kerberos - mongodb-client-encryption + - react - snappy - socks - supports-color + - vue '@push.rocks/smartnetwork@4.1.2': dependencies: @@ -8308,8 +8369,11 @@ snapshots: '@push.rocks/smartversion': 3.0.5 package-json: 8.1.1 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt + - react - supports-color + - vue '@push.rocks/smartntml@2.0.8': dependencies: @@ -8375,12 +8439,15 @@ snapshots: pdf-lib: 1.17.1 pdf2json: 3.2.0 transitivePeerDependencies: + - '@nuxt/kit' - aws-crt - bare-buffer - bufferutil + - react - supports-color - typescript - utf-8-validate + - vue '@push.rocks/smartping@1.0.8': dependencies: @@ -8459,15 +8526,6 @@ snapshots: tree-kill: 1.2.2 which: 5.0.0 - '@push.rocks/smartshell@3.2.4': - dependencies: - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartexit': 1.0.23 - '@push.rocks/smartpromise': 4.2.3 - '@types/which': 3.0.4 - tree-kill: 1.2.2 - which: 5.0.0 - '@push.rocks/smartshell@3.3.0': dependencies: '@push.rocks/smartdelay': 3.0.5 @@ -8611,8 +8669,9 @@ snapshots: '@types/js-yaml': 3.12.10 js-yaml: 3.14.1 - '@push.rocks/taskbuffer@3.1.10': + '@push.rocks/taskbuffer@3.4.0': dependencies: + '@design.estate/dees-element': 2.1.2 '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartlog': 3.1.9 @@ -8620,6 +8679,11 @@ snapshots: '@push.rocks/smartrx': 3.0.10 '@push.rocks/smarttime': 4.1.1 '@push.rocks/smartunique': 3.0.9 + transitivePeerDependencies: + - '@nuxt/kit' + - react + - supports-color + - vue '@push.rocks/webjwt@1.0.9': dependencies: @@ -8920,20 +8984,20 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/engine-oniguruma@3.9.2': + '@shikijs/engine-oniguruma@3.12.2': dependencies: - '@shikijs/types': 3.9.2 + '@shikijs/types': 3.12.2 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.9.2': + '@shikijs/langs@3.12.2': dependencies: - '@shikijs/types': 3.9.2 + '@shikijs/types': 3.12.2 - '@shikijs/themes@3.9.2': + '@shikijs/themes@3.12.2': dependencies: - '@shikijs/types': 3.9.2 + '@shikijs/types': 3.12.2 - '@shikijs/types@3.9.2': + '@shikijs/types@3.12.2': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -9672,9 +9736,9 @@ snapshots: prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.3 prosemirror-tables: 1.8.1 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1) + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.0) prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 '@tiptap/starter-kit@2.26.1': dependencies: @@ -10026,7 +10090,7 @@ snapshots: any-base@1.1.0: {} - apexcharts@5.3.4: + apexcharts@5.3.5: dependencies: '@svgdotjs/svg.draggable.js': 3.0.6(@svgdotjs/svg.js@3.2.4) '@svgdotjs/svg.filter.js': 3.0.9 @@ -11047,7 +11111,7 @@ snapshots: p-cancelable: 3.0.0 responselike: 3.0.0 - gpt-tokenizer@2.9.0: {} + gpt-tokenizer@3.0.1: {} graceful-fs@4.2.10: {} @@ -11304,7 +11368,7 @@ snapshots: isexe@3.1.1: {} - isomorphic-git@1.33.0: + isomorphic-git@1.33.1: dependencies: async-lock: 1.4.1 clean-git-ref: 2.0.1 @@ -12151,7 +12215,7 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openai@5.12.2(ws@8.18.3)(zod@3.25.76): + openai@5.19.1(ws@8.18.3)(zod@3.25.76): optionalDependencies: ws: 8.18.3 zod: 3.25.76 @@ -12349,20 +12413,20 @@ snapshots: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 prosemirror-gapcursor@1.3.2: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 prosemirror-history@1.4.1: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 rope-sequence: 1.3.4 prosemirror-inputrules@1.5.0: @@ -12406,7 +12470,7 @@ snapshots: dependencies: prosemirror-model: 1.25.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 prosemirror-tables@1.8.1: dependencies: @@ -12414,21 +12478,21 @@ snapshots: prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1): + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.0): dependencies: '@remirror/core-constants': 3.0.0 escape-string-regexp: 4.0.0 prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 - prosemirror-view: 1.40.1 + prosemirror-view: 1.41.0 prosemirror-transform@1.10.4: dependencies: prosemirror-model: 1.25.3 - prosemirror-view@1.40.1: + prosemirror-view@1.41.0: dependencies: prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 @@ -13197,9 +13261,9 @@ snapshots: typed-query-selector@2.12.0: {} - typedoc@0.28.10(typescript@5.9.2): + typedoc@0.28.12(typescript@5.9.2): dependencies: - '@gerrit0/mini-shiki': 3.9.2 + '@gerrit0/mini-shiki': 3.12.2 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 @@ -13439,7 +13503,7 @@ snapshots: ylru@1.4.0: {} - yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} zod@3.25.76: {} diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index a5b1a43..bcc152b 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/cloudly', - version: '5.1.0', + version: '5.2.0', description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' } diff --git a/ts/classes.cloudly.ts b/ts/classes.cloudly.ts index ecd09ec..003d53e 100644 --- a/ts/classes.cloudly.ts +++ b/ts/classes.cloudly.ts @@ -18,12 +18,14 @@ import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js'; import { ClusterManager } from './manager.cluster/classes.clustermanager.js'; import { CloudlyTaskmanager } from './manager.task/taskmanager.js'; import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'; -import { CloudlyServerManager } from './manager.server/classes.servermanager.js'; +import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js'; +import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js'; import { ExternalApiManager } from './manager.status/statusmanager.js'; import { ExternalRegistryManager } from './manager.externalregistry/index.js'; import { ImageManager } from './manager.image/classes.imagemanager.js'; import { logger } from './logger.js'; import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js'; +import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js'; /** * Cloudly class can be used to instantiate a cloudly server. @@ -52,13 +54,15 @@ export class Cloudly { // managers public authManager: CloudlyAuthManager; public secretManager: CloudlySecretManager; + public settingsManager: CloudlySettingsManager; public clusterManager: ClusterManager; public coreflowManager: CloudlyCoreflowManager; public externalApiManager: ExternalApiManager; public externalRegistryManager: ExternalRegistryManager; public imageManager: ImageManager; public taskManager: CloudlyTaskmanager; - public serverManager: CloudlyServerManager; + public nodeManager: CloudlyNodeManager; + public baremetalManager: CloudlyBaremetalManager; private readyDeferred = new plugins.smartpromise.Deferred(); @@ -79,6 +83,7 @@ export class Cloudly { // managers this.authManager = new CloudlyAuthManager(this); + this.settingsManager = new CloudlySettingsManager(this); this.clusterManager = new ClusterManager(this); this.coreflowManager = new CloudlyCoreflowManager(this); this.externalApiManager = new ExternalApiManager(this); @@ -86,7 +91,8 @@ export class Cloudly { this.imageManager = new ImageManager(this); this.taskManager = new CloudlyTaskmanager(this); this.secretManager = new CloudlySecretManager(this); - this.serverManager = new CloudlyServerManager(this); + this.nodeManager = new CloudlyNodeManager(this); + this.baremetalManager = new CloudlyBaremetalManager(this); } /** @@ -97,13 +103,18 @@ export class Cloudly { // config await this.config.init(this.configOptions); + // database (data comes from config) + await this.mongodbConnector.init(); + + // settings (are stored in db) + await this.settingsManager.init(); + // manageers await this.authManager.start(); await this.secretManager.start(); - await this.serverManager.start(); - - // connectors - await this.mongodbConnector.init(); + await this.nodeManager.start(); + await this.baremetalManager.start(); + await this.cloudflareConnector.init(); await this.letsencryptConnector.init(); await this.clusterManager.init(); diff --git a/ts/classes.config.ts b/ts/classes.config.ts index 2408fb0..96dd073 100644 --- a/ts/classes.config.ts +++ b/ts/classes.config.ts @@ -20,10 +20,8 @@ export class CloudlyConfig { await plugins.npmextra.AppData.createAndInit( { envMapping: { - cfToken: 'CF_TOKEN', environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration', letsEncryptEmail: 'hard:domains@lossless.org', - hetznerToken: 'HETZNER_API_TOKEN', letsEncryptPrivateKey: null, publicUrl: 'SERVEZONE_URL', publicPort: 'SERVEZONE_PORT', @@ -46,8 +44,6 @@ export class CloudlyConfig { servezoneAdminaccount: 'SERVEZONE_ADMINACCOUNT', }, requiredKeys: [ - 'cfToken', - 'hetznerToken', 'letsEncryptEmail', 'publicUrl', 'publicPort', diff --git a/ts/classes.server.ts b/ts/classes.server.ts index e049978..4922d11 100644 --- a/ts/classes.server.ts +++ b/ts/classes.server.ts @@ -95,7 +95,7 @@ export class CloudlyServer { this.typedServer.typedrouter.addTypedRouter(this.typedrouter); this.typedServer.server.addRoute( '/curlfresh/:scriptname', - this.cloudlyRef.serverManager.curlfreshInstance.handler, + this.cloudlyRef.nodeManager.curlfreshInstance.handler, ); await this.typedServer.start(); } diff --git a/ts/connector.cloudflare/connector.ts b/ts/connector.cloudflare/connector.ts index 3279ead..83583c4 100644 --- a/ts/connector.cloudflare/connector.ts +++ b/ts/connector.cloudflare/connector.ts @@ -14,6 +14,13 @@ export class CloudflareConnector { // init the instance public async init() { - this.cloudflare = new plugins.cloudflare.CloudflareAccount(this.cloudlyRef.config.data.cfToken); + const cloudflareToken = await this.cloudlyRef.settingsManager.getSetting('cloudflareToken'); + + if (!cloudflareToken) { + console.log('warn', 'No Cloudflare token configured in settings. Cloudflare features will be disabled.'); + return; + } + + this.cloudflare = new plugins.cloudflare.CloudflareAccount(cloudflareToken); } } diff --git a/ts/manager.baremetal/classes.baremetal.ts b/ts/manager.baremetal/classes.baremetal.ts new file mode 100644 index 0000000..c8b1da5 --- /dev/null +++ b/ts/manager.baremetal/classes.baremetal.ts @@ -0,0 +1,104 @@ +import * as plugins from '../plugins.js'; + +/** + * BareMetal represents an actual physical server + */ +@plugins.smartdata.Manager() +export class BareMetal extends plugins.smartdata.SmartDataDbDoc< + BareMetal, + plugins.servezoneInterfaces.data.IBareMetal +> { + // STATIC + public static async createFromHetznerServer( + hetznerServerArg: plugins.hetznercloud.HetznerServer, + ) { + const newBareMetal = new BareMetal(); + newBareMetal.id = plugins.smartunique.shortId(8); + const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = { + hostname: hetznerServerArg.data.name, + primaryIp: hetznerServerArg.data.public_net.ipv4.ip, + provider: 'hetzner', + location: hetznerServerArg.data.datacenter.name, + specs: { + cpuModel: hetznerServerArg.data.server_type.cpu_type, + cpuCores: hetznerServerArg.data.server_type.cores, + memoryGB: hetznerServerArg.data.server_type.memory, + storageGB: hetznerServerArg.data.server_type.disk, + storageType: 'nvme', + }, + powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off', + osInfo: { + name: 'Debian', + version: '12', + }, + assignedNodeIds: [], + providerMetadata: { + hetznerServerId: hetznerServerArg.data.id, + hetznerServerName: hetznerServerArg.data.name, + }, + }; + Object.assign(newBareMetal, { data }); + await newBareMetal.save(); + return newBareMetal; + } + + // INSTANCE + @plugins.smartdata.unI() + public id: string; + + @plugins.smartdata.svDb() + public data: plugins.servezoneInterfaces.data.IBareMetal['data']; + + constructor() { + super(); + } + + public async assignNode(nodeId: string) { + if (!this.data.assignedNodeIds.includes(nodeId)) { + this.data.assignedNodeIds.push(nodeId); + await this.save(); + } + } + + public async removeNode(nodeId: string) { + this.data.assignedNodeIds = this.data.assignedNodeIds.filter(id => id !== nodeId); + await this.save(); + } + + public async updatePowerState(state: 'on' | 'off' | 'unknown') { + this.data.powerState = state; + await this.save(); + } + + public async powerOn(): Promise { + // TODO: Implement IPMI power on + if (this.data.ipmiAddress && this.data.ipmiCredentials) { + // Implement IPMI power on command + console.log(`Powering on BareMetal ${this.id} via IPMI`); + await this.updatePowerState('on'); + return true; + } + return false; + } + + public async powerOff(): Promise { + // TODO: Implement IPMI power off + if (this.data.ipmiAddress && this.data.ipmiCredentials) { + // Implement IPMI power off command + console.log(`Powering off BareMetal ${this.id} via IPMI`); + await this.updatePowerState('off'); + return true; + } + return false; + } + + public async reset(): Promise { + // TODO: Implement IPMI reset + if (this.data.ipmiAddress && this.data.ipmiCredentials) { + // Implement IPMI reset command + console.log(`Resetting BareMetal ${this.id} via IPMI`); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/ts/manager.baremetal/classes.baremetalmanager.ts b/ts/manager.baremetal/classes.baremetalmanager.ts new file mode 100644 index 0000000..3712700 --- /dev/null +++ b/ts/manager.baremetal/classes.baremetalmanager.ts @@ -0,0 +1,176 @@ +import * as plugins from '../plugins.js'; +import { Cloudly } from '../classes.cloudly.js'; +import { BareMetal } from './classes.baremetal.js'; +import { logger } from '../logger.js'; + +export class CloudlyBaremetalManager { + public cloudlyRef: Cloudly; + public typedRouter = new plugins.typedrequest.TypedRouter(); + + public hetznerAccount: plugins.hetznercloud.HetznerAccount; + + public get db() { + return this.cloudlyRef.mongodbConnector.smartdataDb; + } + public CBareMetal = plugins.smartdata.setDefaultManagerForDoc(this, BareMetal); + + constructor(cloudlyRefArg: Cloudly) { + this.cloudlyRef = cloudlyRefArg; + this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter); + + // API endpoint to get baremetal servers + this.typedRouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getBaremetalServers', + async (requestData) => { + const baremetals = await this.getAllBaremetals(); + return { + baremetals: await Promise.all( + baremetals.map((baremetal) => baremetal.createSavableObject()) + ), + }; + }, + ), + ); + + // API endpoint to control baremetal via IPMI + this.typedRouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'controlBaremetal', + async (requestData) => { + const baremetal = await this.CBareMetal.getInstance({ + id: requestData.baremetalId, + }); + + if (!baremetal) { + return { + success: false, + message: 'BareMetal not found', + }; + } + + let success = false; + switch (requestData.action) { + case 'powerOn': + success = await baremetal.powerOn(); + break; + case 'powerOff': + success = await baremetal.powerOff(); + break; + case 'reset': + success = await baremetal.reset(); + break; + } + + return { + success, + message: success ? `Action ${requestData.action} completed` : `Action ${requestData.action} failed`, + }; + }, + ), + ); + } + + public async start() { + const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken'); + + if (hetznerToken) { + this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken); + } + + logger.log('info', 'BareMetal manager started'); + } + + public async stop() { + logger.log('info', 'BareMetal manager stopped'); + } + + /** + * Get all baremetal servers + */ + public async getAllBaremetals(): Promise { + const baremetals = await this.CBareMetal.getInstances({}); + return baremetals; + } + + /** + * Get baremetal by ID + */ + public async getBaremetalById(id: string): Promise { + const baremetal = await this.CBareMetal.getInstance({ + id, + }); + return baremetal; + } + + /** + * Get baremetals by provider + */ + public async getBaremetalsByProvider(provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise'): Promise { + const baremetals = await this.CBareMetal.getInstances({ + data: { + provider, + }, + }); + return baremetals; + } + + /** + * Create baremetal from Hetzner server + */ + public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise { + // Check if baremetal already exists for this Hetzner server + const existingBaremetals = await this.CBareMetal.getInstances({}); + for (const baremetal of existingBaremetals) { + if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) { + logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`); + return baremetal; + } + } + + // Create new baremetal + const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer); + logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${hetznerServer.data.id}`); + return newBaremetal; + } + + /** + * Sync baremetals with Hetzner + */ + public async syncWithHetzner() { + if (!this.hetznerAccount) { + logger.log('warn', 'Cannot sync with Hetzner - no account configured'); + return; + } + + const hetznerServers = await this.hetznerAccount.getServers(); + + for (const hetznerServer of hetznerServers) { + await this.createBaremetalFromHetznerServer(hetznerServer); + } + + logger.log('success', `Synced ${hetznerServers.length} servers from Hetzner`); + } + + /** + * Provision a new baremetal server + */ + public async provisionBaremetal(options: { + provider: 'hetzner' | 'aws' | 'digitalocean'; + location: any; // TODO: Import proper type from hetznercloud when available + type: any; // TODO: Import proper type from hetznercloud when available + }): Promise { + if (options.provider === 'hetzner' && this.hetznerAccount) { + const hetznerServer = await this.hetznerAccount.createServer({ + name: plugins.smartunique.uniSimple('baremetal'), + location: options.location, + type: options.type, + }); + + const baremetal = await this.createBaremetalFromHetznerServer(hetznerServer); + return baremetal; + } + + throw new Error(`Provider ${options.provider} not supported or not configured`); + } +} \ No newline at end of file diff --git a/ts/manager.cluster/classes.clustermanager.ts b/ts/manager.cluster/classes.clustermanager.ts index 1af029f..9c7cb08 100644 --- a/ts/manager.cluster/classes.clustermanager.ts +++ b/ts/manager.cluster/classes.clustermanager.ts @@ -33,7 +33,7 @@ export class ClusterManager { setupMode: setupMode, acmeInfo: null, cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`, - servers: [], + nodes: [], sshKeys: [], }, }); @@ -41,7 +41,7 @@ export class ClusterManager { // Only auto-provision servers if setupMode is 'hetzner' if (setupMode === 'hetzner') { - this.cloudlyRef.serverManager.ensureServerInfrastructure(); + this.cloudlyRef.nodeManager.ensureNodeInfrastructure(); } return { diff --git a/ts/manager.node/classes.clusternode.ts b/ts/manager.node/classes.clusternode.ts new file mode 100644 index 0000000..1256683 --- /dev/null +++ b/ts/manager.node/classes.clusternode.ts @@ -0,0 +1,61 @@ +import * as plugins from '../plugins.js'; + +/** + * ClusterNode represents a logical node participating in a cluster + */ +@plugins.smartdata.Manager() +export class ClusterNode extends plugins.smartdata.SmartDataDbDoc< + ClusterNode, + plugins.servezoneInterfaces.data.IClusterNode +> { + // STATIC + public static async createFromHetznerServer( + hetznerServerArg: plugins.hetznercloud.HetznerServer, + clusterId: string, + baremetalId: string, + ) { + const newNode = new ClusterNode(); + newNode.id = plugins.smartunique.shortId(8); + const data: plugins.servezoneInterfaces.data.IClusterNode['data'] = { + clusterId: clusterId, + baremetalId: baremetalId, + nodeType: 'baremetal', + status: 'initializing', + role: 'worker', + joinedAt: Date.now(), + lastHealthCheck: Date.now(), + sshKeys: [], + requiredDebianPackages: [], + }; + Object.assign(newNode, { data }); + await newNode.save(); + return newNode; + } + + // INSTANCE + @plugins.smartdata.unI() + public id: string; + + @plugins.smartdata.svDb() + public data: plugins.servezoneInterfaces.data.IClusterNode['data']; + + constructor() { + super(); + } + + public async getDeployments(): Promise { + // TODO: Implement getting deployments for this node + return []; + } + + public async updateMetrics(metrics: plugins.servezoneInterfaces.data.IClusterNodeMetrics) { + this.data.metrics = metrics; + this.data.lastHealthCheck = Date.now(); + await this.save(); + } + + public async updateStatus(status: plugins.servezoneInterfaces.data.IClusterNode['data']['status']) { + this.data.status = status; + await this.save(); + } +} diff --git a/ts/manager.server/classes.curlfresh.ts b/ts/manager.node/classes.curlfresh.ts similarity index 82% rename from ts/manager.server/classes.curlfresh.ts rename to ts/manager.node/classes.curlfresh.ts index 6ca7b3e..0fe7781 100644 --- a/ts/manager.server/classes.curlfresh.ts +++ b/ts/manager.node/classes.curlfresh.ts @@ -1,6 +1,6 @@ import { logger } from '../logger.js'; import * as plugins from '../plugins.js'; -import type { CloudlyServerManager } from './classes.servermanager.js'; +import type { CloudlyNodeManager } from './classes.nodemanager.js'; export class CurlFresh { public optionsArg = { @@ -45,7 +45,7 @@ bash -c "spark installdaemon" `, }; - public serverManagerRef: CloudlyServerManager; + public nodeManagerRef: CloudlyNodeManager; public curlFreshRoute: plugins.typedserver.servertools.Route; public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => { logger.log('info', 'curlfresh handler called. a server might be coming online soon :)'); @@ -62,12 +62,12 @@ bash -c "spark installdaemon" } }); - constructor(serverManagerRefArg: CloudlyServerManager) { - this.serverManagerRef = serverManagerRefArg; + constructor(nodeManagerRefArg: CloudlyNodeManager) { + this.nodeManagerRef = nodeManagerRefArg; } public async getServerUserData(): Promise { const sslMode = - await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode'); + await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode'); let protocol: 'http' | 'https'; if (sslMode === 'none') { protocol = 'http'; @@ -76,9 +76,9 @@ bash -c "spark installdaemon" } const domain = - await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl'); + await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl'); const port = - await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort'); + await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort'); const serverUserData = `#cloud-config runcmd: diff --git a/ts/manager.node/classes.nodemanager.ts b/ts/manager.node/classes.nodemanager.ts new file mode 100644 index 0000000..720992e --- /dev/null +++ b/ts/manager.node/classes.nodemanager.ts @@ -0,0 +1,131 @@ +import * as plugins from '../plugins.js'; +import { Cloudly } from '../classes.cloudly.js'; +import { Cluster } from '../manager.cluster/classes.cluster.js'; +import { ClusterNode } from './classes.clusternode.js'; +import { CurlFresh } from './classes.curlfresh.js'; + +export class CloudlyNodeManager { + public cloudlyRef: Cloudly; + public typedRouter = new plugins.typedrequest.TypedRouter(); + public curlfreshInstance = new CurlFresh(this); + + public hetznerAccount: plugins.hetznercloud.HetznerAccount; + + public get db() { + return this.cloudlyRef.mongodbConnector.smartdataDb; + } + public CClusterNode = plugins.smartdata.setDefaultManagerForDoc(this, ClusterNode); + + constructor(cloudlyRefArg: Cloudly) { + this.cloudlyRef = cloudlyRefArg; + + /** + * is used be serverconfig module on the node to get the actual node config + */ + this.typedRouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getNodeConfig', + async (requestData) => { + const nodeId = requestData.nodeId; + const node = await this.CClusterNode.getInstance({ + id: nodeId, + }); + return { + configData: await node.createSavableObject(), + }; + }, + ), + ); + } + + public async start() { + const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken'); + + if (!hetznerToken) { + console.log('warn', 'No Hetzner token configured in settings. Hetzner features will be disabled.'); + return; + } + + this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken); + } + + public async stop() {} + + /** + * creates the node infrastructure on hetzner + * ensures that there are exactly the resources that are needed + * no more, no less + */ + public async ensureNodeInfrastructure() { + // get all clusters + const allClusters = await this.cloudlyRef.clusterManager.getAllClusters(); + for (const cluster of allClusters) { + // Skip clusters that are not set up for Hetzner auto-provisioning + if (cluster.data.setupMode !== 'hetzner') { + console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`); + continue; + } + + // get existing nodes + const nodes = await this.getNodesByCluster(cluster); + + // if there is no node, create one + if (nodes.length === 0) { + const hetznerServer = await this.hetznerAccount.createServer({ + name: plugins.smartunique.uniSimple('node'), + location: 'nbg1', + type: 'cpx41', + labels: { + clusterId: cluster.id, + priority: '1', + }, + userData: await this.curlfreshInstance.getServerUserData(), + }); + + // First create BareMetal record + const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer); + + const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id); + await baremetal.assignNode(newNode.id); + console.log(`cluster created new node for cluster ${cluster.id}`); + } else { + console.log( + `cluster ${cluster.id} already has nodes. Making sure that they actually exist in the real world...`, + ); + // if there is a node, make sure that it exists + for (const node of nodes) { + const hetznerServers = await this.hetznerAccount.getServersByLabel({ + clusterId: cluster.id, + }); + if (!hetznerServers || hetznerServers.length === 0) { + console.log(`node ${node.id} does not exist in the real world. Creating it now...`); + const hetznerServer = await this.hetznerAccount.createServer({ + name: plugins.smartunique.uniSimple('node'), + location: 'nbg1', + type: 'cpx41', + labels: { + clusterId: cluster.id, + priority: '1', + }, + }); + + // First create BareMetal record + const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer); + + const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id); + await baremetal.assignNode(newNode.id); + } + } + } + } + } + + public async getNodesByCluster(clusterArg: Cluster) { + const results = await this.CClusterNode.getInstances({ + data: { + clusterId: clusterArg.id, + }, + }); + return results; + } +} diff --git a/ts/manager.server/classes.server.ts b/ts/manager.server/classes.server.ts deleted file mode 100644 index 2d5934b..0000000 --- a/ts/manager.server/classes.server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as plugins from '../plugins.js'; - -/* - * cluster defines a swarmkit cluster - */ -@plugins.smartdata.Manager() -export class Server extends plugins.smartdata.SmartDataDbDoc< - Server, - plugins.servezoneInterfaces.data.IServer -> { - // STATIC - public static async createFromHetznerServer( - hetznerServerArg: plugins.hetznercloud.HetznerServer, - ) { - const newServer = new Server(); - newServer.id = plugins.smartunique.shortId(8); - const data: plugins.servezoneInterfaces.data.IServer['data'] = { - assignedClusterId: hetznerServerArg.data.labels.clusterId, - requiredDebianPackages: [], - sshKeys: [], - type: 'hetzner', - }; - Object.assign(newServer, { data }); - await newServer.save(); - return newServer; - } - - // INSTANCE - @plugins.smartdata.unI() - public id: string; - - @plugins.smartdata.svDb() - public data: plugins.servezoneInterfaces.data.IServer['data']; - - constructor() { - super(); - } - - public async getServices(): Promise { - return []; - } -} diff --git a/ts/manager.server/classes.servermanager.ts b/ts/manager.server/classes.servermanager.ts deleted file mode 100644 index 331ae33..0000000 --- a/ts/manager.server/classes.servermanager.ts +++ /dev/null @@ -1,116 +0,0 @@ -import * as plugins from '../plugins.js'; -import { Cloudly } from '../classes.cloudly.js'; -import { Cluster } from '../manager.cluster/classes.cluster.js'; -import { Server } from './classes.server.js'; -import { CurlFresh } from './classes.curlfresh.js'; - -export class CloudlyServerManager { - public cloudlyRef: Cloudly; - public typedRouter = new plugins.typedrequest.TypedRouter(); - public curlfreshInstance = new CurlFresh(this); - - public hetznerAccount: plugins.hetznercloud.HetznerAccount; - - public get db() { - return this.cloudlyRef.mongodbConnector.smartdataDb; - } - public CServer = plugins.smartdata.setDefaultManagerForDoc(this, Server); - - constructor(cloudlyRefArg: Cloudly) { - this.cloudlyRef = cloudlyRefArg; - - /** - * is used be serverconfig module on the server to get the actual server config - */ - this.typedRouter.addTypedHandler( - new plugins.typedrequest.TypedHandler( - 'getServerConfig', - async (requestData) => { - const serverId = requestData.serverId; - const server = await this.CServer.getInstance({ - id: serverId, - }); - return { - configData: await server.createSavableObject(), - }; - }, - ), - ); - } - - public async start() { - this.hetznerAccount = new plugins.hetznercloud.HetznerAccount( - this.cloudlyRef.config.data.hetznerToken, - ); - } - - public async stop() {} - - /** - * creates the server infrastructure on hetzner - * ensures that there are exactly the reources that are needed - * no more, no less - */ - public async ensureServerInfrastructure() { - // get all clusters - const allClusters = await this.cloudlyRef.clusterManager.getAllClusters(); - for (const cluster of allClusters) { - // Skip clusters that are not set up for Hetzner auto-provisioning - if (cluster.data.setupMode !== 'hetzner') { - console.log(`Skipping server provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`); - continue; - } - - // get existing servers - const servers = await this.getServersByCluster(cluster); - - // if there is no server, create one - if (servers.length === 0) { - const server = await this.hetznerAccount.createServer({ - name: plugins.smartunique.uniSimple('server'), - location: 'nbg1', - type: 'cpx41', - labels: { - clusterId: cluster.id, - priority: '1', - }, - userData: await this.curlfreshInstance.getServerUserData(), - }); - const newServer = await Server.createFromHetznerServer(server); - console.log(`cluster created new server for cluster ${cluster.id}`); - } else { - console.log( - `cluster ${cluster.id} already has servers. Making sure that they actually exist in the real world...`, - ); - // if there is a server, make sure that it exists - for (const server of servers) { - const hetznerServer = await this.hetznerAccount.getServersByLabel({ - clusterId: cluster.id, - }); - if (!hetznerServer) { - console.log(`server ${server.id} does not exist in the real world. Creating it now...`); - const hetznerServer = await this.hetznerAccount.createServer({ - name: plugins.smartunique.uniSimple('server'), - location: 'nbg1', - type: 'cpx41', - labels: { - clusterId: cluster.id, - priority: '1', - }, - }); - const newServer = await Server.createFromHetznerServer(hetznerServer); - } - } - } - } - } - - public async getServersByCluster(clusterArg: Cluster) { - const results = await this.CServer.getInstances({ - data: { - assignedClusterId: clusterArg.id, - }, - }); - return results; - } -} diff --git a/ts/manager.settings/classes.settingsmanager.ts b/ts/manager.settings/classes.settingsmanager.ts new file mode 100644 index 0000000..9b5b59d --- /dev/null +++ b/ts/manager.settings/classes.settingsmanager.ts @@ -0,0 +1,255 @@ +import * as plugins from '../plugins.js'; +import type { Cloudly } from '../classes.cloudly.js'; +import * as servezoneInterfaces from '@serve.zone/interfaces'; + +export class CloudlySettingsManager { + public cloudlyRef: Cloudly; + public readyDeferred = plugins.smartpromise.defer(); + public settingsStore: plugins.smartdata.EasyStore; + + constructor(cloudlyRefArg: Cloudly) { + this.cloudlyRef = cloudlyRefArg; + } + + /** + * Initialize the settings manager and create the EasyStore + */ + public async init() { + this.settingsStore = await this.cloudlyRef.mongodbConnector.smartdataDb + .createEasyStore('cloudly-settings') as plugins.smartdata.EasyStore; + + // Setup API route handlers + await this.setupRoutes(); + + this.readyDeferred.resolve(); + } + + /** + * Get all settings + */ + public async getSettings(): Promise { + await this.readyDeferred.promise; + return await this.settingsStore.readAll(); + } + + /** + * Get all settings with masked sensitive values (for API responses) + */ + public async getSettingsMasked(): Promise { + await this.readyDeferred.promise; + const settings = await this.getSettings(); + const masked: servezoneInterfaces.data.ICloudlySettingsMasked = {}; + + for (const [key, value] of Object.entries(settings)) { + if (typeof value === 'string' && value.length > 4) { + // Mask the token, showing only last 4 characters + masked[key] = '****' + value.slice(-4); + } else { + masked[key] = value; + } + } + + return masked; + } + + /** + * Update multiple settings at once + */ + public async updateSettings(updates: Partial): Promise { + await this.readyDeferred.promise; + for (const [key, value] of Object.entries(updates)) { + if (value !== undefined && value !== '') { + await this.settingsStore.writeKey(key as keyof servezoneInterfaces.data.ICloudlySettings, value); + } else if (value === '') { + // Empty string means clear the setting + await this.settingsStore.deleteKey(key as keyof servezoneInterfaces.data.ICloudlySettings); + } + } + } + + /** + * Get a specific setting value + */ + public async getSetting(key: K): Promise { + await this.readyDeferred.promise; + return await this.settingsStore.readKey(key); + } + + /** + * Set a specific setting value + */ + public async setSetting(key: K, value: servezoneInterfaces.data.ICloudlySettings[K]): Promise { + await this.readyDeferred.promise; + if (value !== undefined && value !== '') { + await this.settingsStore.writeKey(key, value); + } + } + + /** + * Clear a specific setting + */ + public async clearSetting(key: keyof servezoneInterfaces.data.ICloudlySettings): Promise { + await this.readyDeferred.promise; + await this.settingsStore.deleteKey(key); + } + + /** + * Clear all settings + */ + public async clearAllSettings(): Promise { + await this.readyDeferred.promise; + await this.settingsStore.wipe(); + } + + /** + * Test connection for a specific provider + */ + public async testProviderConnection(provider: string): Promise<{success: boolean; message: string}> { + await this.readyDeferred.promise; + try { + switch (provider) { + case 'hetzner': + const hetznerToken = await this.getSetting('hetznerToken'); + if (!hetznerToken) { + return { success: false, message: 'No Hetzner token configured' }; + } + // TODO: Implement actual Hetzner API test + return { success: true, message: 'Hetzner connection test successful' }; + + case 'cloudflare': + const cloudflareToken = await this.getSetting('cloudflareToken'); + if (!cloudflareToken) { + return { success: false, message: 'No Cloudflare token configured' }; + } + // TODO: Implement actual Cloudflare API test + return { success: true, message: 'Cloudflare connection test successful' }; + + case 'aws': + const awsKey = await this.getSetting('awsAccessKey'); + const awsSecret = await this.getSetting('awsSecretKey'); + if (!awsKey || !awsSecret) { + return { success: false, message: 'AWS credentials not configured' }; + } + // TODO: Implement actual AWS API test + return { success: true, message: 'AWS connection test successful' }; + + case 'digitalocean': + const doToken = await this.getSetting('digitalOceanToken'); + if (!doToken) { + return { success: false, message: 'No DigitalOcean token configured' }; + } + // TODO: Implement actual DigitalOcean API test + return { success: true, message: 'DigitalOcean connection test successful' }; + + case 'azure': + const azureClientId = await this.getSetting('azureClientId'); + const azureClientSecret = await this.getSetting('azureClientSecret'); + const azureTenantId = await this.getSetting('azureTenantId'); + if (!azureClientId || !azureClientSecret || !azureTenantId) { + return { success: false, message: 'Azure credentials not configured' }; + } + // TODO: Implement actual Azure API test + return { success: true, message: 'Azure connection test successful' }; + + default: + return { success: false, message: `Unknown provider: ${provider}` }; + } + } catch (error) { + return { success: false, message: `Connection test failed: ${error.message}` }; + } + } + + + /** + * Setup API route handlers for settings management + */ + private async setupRoutes() { + // Get Settings Handler + this.cloudlyRef.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getSettings', + async (requestData) => { + // TODO: Add authentication check for admin users + const maskedSettings = await this.getSettingsMasked(); + return { + settings: maskedSettings + }; + } + ) + ); + + // Update Settings Handler + this.cloudlyRef.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'updateSettings', + async (requestData) => { + // TODO: Add authentication check for admin users + try { + await this.updateSettings(requestData.updates); + return { + success: true, + message: 'Settings updated successfully' + }; + } catch (error) { + return { + success: false, + message: `Failed to update settings: ${error.message}` + }; + } + } + ) + ); + + // Clear Setting Handler + this.cloudlyRef.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'clearSetting', + async (requestData) => { + // TODO: Add authentication check for admin users + try { + await this.clearSetting(requestData.key); + return { + success: true, + message: `Setting ${requestData.key} cleared successfully` + }; + } catch (error) { + return { + success: false, + message: `Failed to clear setting: ${error.message}` + }; + } + } + ) + ); + + // Test Provider Connection Handler + this.cloudlyRef.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'testProviderConnection', + async (requestData) => { + // TODO: Add authentication check for admin users + const testResult = await this.testProviderConnection(requestData.provider); + return { + success: testResult.success, + message: testResult.message, + connectionValid: testResult.success + }; + } + ) + ); + + // Get Single Setting Handler (for internal use) + this.cloudlyRef.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler( + 'getSetting', + async (requestData) => { + // TODO: Add authentication check for admin users + const value = await this.getSetting(requestData.key); + return { + value + }; + } + ) + ); + } +} \ No newline at end of file diff --git a/ts/manager.settings/index.ts b/ts/manager.settings/index.ts new file mode 100644 index 0000000..b87d5d6 --- /dev/null +++ b/ts/manager.settings/index.ts @@ -0,0 +1 @@ +export * from './classes.settingsmanager.js'; \ No newline at end of file diff --git a/ts_interfaces/data/baremetal.ts b/ts_interfaces/data/baremetal.ts new file mode 100644 index 0000000..75381e4 --- /dev/null +++ b/ts_interfaces/data/baremetal.ts @@ -0,0 +1,73 @@ +import * as plugins from '../plugins.js'; + +export interface IBareMetal { + id: string; + data: { + hostname: string; + + /** + * IPMI management IP address + */ + ipmiAddress?: string; + + /** + * Encrypted IPMI credentials + */ + ipmiCredentials?: { + username: string; + passwordEncrypted: string; + }; + + /** + * Primary network IP address + */ + primaryIp: string; + + /** + * Provider of the physical server + */ + provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise'; + + /** + * Data center or location + */ + location: string; + + /** + * Hardware specifications + */ + specs: { + cpuModel: string; + cpuCores: number; + memoryGB: number; + storageGB: number; + storageType: 'ssd' | 'hdd' | 'nvme'; + }; + + /** + * Current power state + */ + powerState: 'on' | 'off' | 'unknown'; + + /** + * Operating system information + */ + osInfo: { + name: string; + version: string; + kernel?: string; + }; + + /** + * Array of ClusterNode IDs running on this hardware + */ + assignedNodeIds: string[]; + + /** + * Metadata for provider-specific information + */ + providerMetadata?: { + [key: string]: any; + }; + }; +} \ No newline at end of file diff --git a/ts_interfaces/data/cloudlyconfig.ts b/ts_interfaces/data/cloudlyconfig.ts index c19f376..9ea2d4b 100644 --- a/ts_interfaces/data/cloudlyconfig.ts +++ b/ts_interfaces/data/cloudlyconfig.ts @@ -1,8 +1,6 @@ import * as plugins from '../plugins.js'; export interface ICloudlyConfig { - cfToken?: string; - hetznerToken?: string; environment?: 'production' | 'integration'; letsEncryptEmail?: string; letsEncryptPrivateKey?: string; diff --git a/ts_interfaces/data/cluster.ts b/ts_interfaces/data/cluster.ts index b59035e..8512815 100644 --- a/ts_interfaces/data/cluster.ts +++ b/ts_interfaces/data/cluster.ts @@ -1,7 +1,7 @@ import * as plugins from '../plugins.js'; import { type IDockerRegistryInfo } from '../data/docker.js'; -import type { IServer } from './server.js'; +import type { IClusterNode } from './clusternode.js'; export interface ICluster { id: string; @@ -24,9 +24,9 @@ export interface ICluster { setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; /** - * what servers are expected to be part of the cluster + * Nodes that are part of the cluster */ - servers: IServer[]; + nodes: IClusterNode[]; /** * ACME info. This is used to get SSL certificates. diff --git a/ts_interfaces/data/clusternode.ts b/ts_interfaces/data/clusternode.ts new file mode 100644 index 0000000..821abb5 --- /dev/null +++ b/ts_interfaces/data/clusternode.ts @@ -0,0 +1,71 @@ +import * as plugins from '../plugins.js'; + +export interface IClusterNodeMetrics { + cpuUsagePercent: number; + memoryUsedMB: number; + memoryAvailableMB: number; + diskUsedGB: number; + diskAvailableGB: number; + containerCount: number; + timestamp: number; +} + +export interface IClusterNode { + id: string; + data: { + /** + * Reference to the cluster this node belongs to + */ + clusterId: string; + + /** + * Reference to the physical server (if applicable) + */ + baremetalId?: string; + + /** + * Type of node + */ + nodeType: 'baremetal' | 'vm' | 'container'; + + /** + * Current status of the node + */ + status: 'initializing' | 'online' | 'offline' | 'maintenance'; + + /** + * Role of the node in the cluster + */ + role: 'master' | 'worker'; + + /** + * Timestamp when node joined the cluster + */ + joinedAt: number; + + /** + * Last health check timestamp + */ + lastHealthCheck: number; + + /** + * Current metrics for the node + */ + metrics?: IClusterNodeMetrics; + + /** + * Docker swarm node ID if part of swarm + */ + swarmNodeId?: string; + + /** + * SSH keys deployed to this node + */ + sshKeys: plugins.tsclass.network.ISshKey[]; + + /** + * Debian packages installed on this node + */ + requiredDebianPackages: string[]; + }; +} \ No newline at end of file diff --git a/ts_interfaces/data/deployment.ts b/ts_interfaces/data/deployment.ts index e561170..ada7619 100644 --- a/ts_interfaces/data/deployment.ts +++ b/ts_interfaces/data/deployment.ts @@ -6,8 +6,58 @@ import * as plugins from '../plugins.js'; */ export interface IDeployment { id: string; - affectedServiceIds: string[]; + + /** + * The service being deployed (single service per deployment) + */ + serviceId: string; + + /** + * The node this deployment is running on + */ + nodeId: string; + + /** + * Docker container ID for this deployment + */ + containerId?: string; + + /** + * Image used for this deployment + */ usedImageId: string; + + /** + * Version of the service deployed + */ + version: string; + + /** + * Timestamp when deployed + */ + deployedAt: number; + + /** + * Deployment log entries + */ deploymentLog: string[]; - status: 'scheduled' | 'running' | 'deployed' | 'failed'; + + /** + * Current status of the deployment + */ + status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed'; + + /** + * Health status of the deployment + */ + healthStatus?: 'healthy' | 'unhealthy' | 'unknown'; + + /** + * Resource usage for this deployment + */ + resourceUsage?: { + cpuUsagePercent: number; + memoryUsedMB: number; + lastUpdated: number; + }; } \ No newline at end of file diff --git a/ts_interfaces/data/index.ts b/ts_interfaces/data/index.ts index 0f94f10..0df2068 100644 --- a/ts_interfaces/data/index.ts +++ b/ts_interfaces/data/index.ts @@ -7,8 +7,10 @@ export * from './event.js'; export * from './externalregistry.js'; export * from './image.js'; export * from './secretbundle.js'; -export * from './secretgroup.js' -export * from './server.js'; +export * from './secretgroup.js'; +export * from './baremetal.js'; +export * from './clusternode.js'; +export * from './settings.js'; export * from './service.js'; export * from './status.js'; export * from './traffic.js'; diff --git a/ts_interfaces/data/service.ts b/ts_interfaces/data/service.ts index da3cdb6..557156c 100644 --- a/ts_interfaces/data/service.ts +++ b/ts_interfaces/data/service.ts @@ -17,6 +17,35 @@ export interface IService { * and thus live past the service lifecycle */ additionalSecretBundleIds?: string[]; + + /** + * Service category determines deployment behavior + * - base: Core services that run on every node (coreflow, coretraffic, corelog) + * - distributed: Services that run on limited nodes (cores3, coremongo) + * - workload: User applications + */ + serviceCategory: 'base' | 'distributed' | 'workload'; + + /** + * Deployment strategy for the service + * - all-nodes: Deploy to every node in the cluster + * - limited-replicas: Deploy to a limited number of nodes + * - custom: Custom deployment logic + */ + deploymentStrategy: 'all-nodes' | 'limited-replicas' | 'custom'; + + /** + * Maximum number of replicas for distributed services + * For example, 3 for cores3 or coremongo + */ + maxReplicas?: number; + + /** + * Whether to enforce anti-affinity rules + * When true, tries to spread deployments across different BareMetal servers + */ + antiAffinity?: boolean; + scaleFactor: number; balancingStrategy: 'round-robin' | 'least-connections'; ports: { diff --git a/ts_interfaces/data/settings.ts b/ts_interfaces/data/settings.ts new file mode 100644 index 0000000..f551b9f --- /dev/null +++ b/ts_interfaces/data/settings.ts @@ -0,0 +1,56 @@ +import * as plugins from '../plugins.js'; + +/** + * Interface for Cloudly settings stored in EasyStore + * These are runtime-configurable settings that can be modified via the UI + */ +export interface ICloudlySettings { + // Cloud Provider Tokens + hetznerToken?: string; + cloudflareToken?: string; + + // AWS Credentials + awsAccessKey?: string; + awsSecretKey?: string; + awsRegion?: string; + + // DigitalOcean + digitalOceanToken?: string; + + // Azure Credentials + azureClientId?: string; + azureClientSecret?: string; + azureTenantId?: string; + azureSubscriptionId?: string; + + // Google Cloud + googleCloudKeyJson?: string; + googleCloudProjectId?: string; + + // Vultr + vultrApiKey?: string; + + // Linode + linodeToken?: string; + + // OVH + ovhApplicationKey?: string; + ovhApplicationSecret?: string; + ovhConsumerKey?: string; + + // Scaleway + scalewayAccessKey?: string; + scalewaySecretKey?: string; + scalewayOrganizationId?: string; + + // Other settings that might be added in the future + [key: string]: string | undefined; +} + +/** + * Interface for masked settings (used in API responses) + * Shows only last 4 characters of sensitive tokens + */ +export type ICloudlySettingsMasked = { + [K in keyof ICloudlySettings]: string | undefined; +}; \ No newline at end of file diff --git a/ts_interfaces/requests/baremetal.ts b/ts_interfaces/requests/baremetal.ts new file mode 100644 index 0000000..ba26fda --- /dev/null +++ b/ts_interfaces/requests/baremetal.ts @@ -0,0 +1,22 @@ +import * as plugins from '../plugins.js'; +import type { IBareMetal } from '../data/baremetal.js'; + +export interface IRequest_Any_Cloudly_GetBaremetalServers { + method: 'getBaremetalServers'; + request: {}; + response: { + baremetals: IBareMetal[]; + }; +} + +export interface IRequest_Any_Cloudly_ControlBaremetal { + method: 'controlBaremetal'; + request: { + baremetalId: string; + action: 'powerOn' | 'powerOff' | 'reset'; + }; + response: { + success: boolean; + message: string; + }; +} \ No newline at end of file diff --git a/ts_interfaces/requests/index.ts b/ts_interfaces/requests/index.ts index d303a36..9aa8097 100644 --- a/ts_interfaces/requests/index.ts +++ b/ts_interfaces/requests/index.ts @@ -1,6 +1,7 @@ import * as plugins from '../plugins.js'; import * as adminRequests from './admin.js'; +import * as baremetalRequests from './baremetal.js'; import * as certificateRequests from './certificate.js'; import * as clusterRequests from './cluster.js'; import * as configRequests from './config.js'; @@ -10,16 +11,19 @@ import * as imageRequests from './image.js'; import * as informRequests from './inform.js'; import * as logRequests from './log.js'; import * as networkRequests from './network.js'; +import * as nodeRequests from './node.js'; import * as routingRequests from './routing.js'; import * as secretBundleRequests from './secretbundle.js'; import * as secretGroupRequests from './secretgroup.js'; import * as serverRequests from './server.js'; import * as serviceRequests from './service.js'; +import * as settingsRequests from './settings.js'; import * as statusRequests from './status.js'; import * as versionRequests from './version.js'; export { adminRequests as admin, + baremetalRequests as baremetal, certificateRequests as certificate, clusterRequests as cluster, configRequests as config, @@ -29,11 +33,13 @@ export { informRequests as inform, logRequests as log, networkRequests as network, + nodeRequests as node, routingRequests as routing, secretBundleRequests as secretbundle, secretGroupRequests as secretgroup, serverRequests as server, serviceRequests as service, + settingsRequests as settings, statusRequests as status, versionRequests as version, }; diff --git a/ts_interfaces/requests/node.ts b/ts_interfaces/requests/node.ts new file mode 100644 index 0000000..4d727d6 --- /dev/null +++ b/ts_interfaces/requests/node.ts @@ -0,0 +1,33 @@ +import * as plugins from '../plugins.js'; +import type { IClusterNode } from '../data/clusternode.js'; +import type { IDeployment } from '../data/deployment.js'; + +export interface IRequest_Any_Cloudly_GetNodeConfig { + method: 'getNodeConfig'; + request: { + nodeId: string; + }; + response: { + configData: IClusterNode; + }; +} + +export interface IRequest_Any_Cloudly_GetNodesByCluster { + method: 'getNodesByCluster'; + request: { + clusterId: string; + }; + response: { + nodes: IClusterNode[]; + }; +} + +export interface IRequest_Any_Cloudly_GetNodeDeployments { + method: 'getNodeDeployments'; + request: { + nodeId: string; + }; + response: { + deployments: IDeployment[]; + }; +} \ No newline at end of file diff --git a/ts_interfaces/requests/settings.ts b/ts_interfaces/requests/settings.ts new file mode 100644 index 0000000..b7c3393 --- /dev/null +++ b/ts_interfaces/requests/settings.ts @@ -0,0 +1,59 @@ +import * as plugins from '../plugins.js'; +import type { ICloudlySettings, ICloudlySettingsMasked } from '../data/settings.js'; + +// Get Settings +export interface IRequest_GetSettings extends plugins.typedrequestInterfaces.ITypedRequest { + method: 'getSettings'; + request: {}; + response: { + settings: ICloudlySettingsMasked; + }; +} + +// Update Settings +export interface IRequest_UpdateSettings extends plugins.typedrequestInterfaces.ITypedRequest { + method: 'updateSettings'; + request: { + updates: Partial; + }; + response: { + success: boolean; + message: string; + }; +} + +// Clear Specific Setting +export interface IRequest_ClearSetting extends plugins.typedrequestInterfaces.ITypedRequest { + method: 'clearSetting'; + request: { + key: keyof ICloudlySettings; + }; + response: { + success: boolean; + message: string; + }; +} + +// Test Provider Connection +export interface IRequest_TestProviderConnection extends plugins.typedrequestInterfaces.ITypedRequest { + method: 'testProviderConnection'; + request: { + provider: 'hetzner' | 'cloudflare' | 'aws' | 'digitalocean' | 'azure' | 'google' | 'vultr' | 'linode' | 'ovh' | 'scaleway'; + }; + response: { + success: boolean; + message: string; + connectionValid: boolean; + }; +} + +// Get Single Setting (for internal use, not exposed to frontend) +export interface IRequest_GetSetting extends plugins.typedrequestInterfaces.ITypedRequest { + method: 'getSetting'; + request: { + key: keyof ICloudlySettings; + }; + response: { + value: string | undefined; + }; +} \ No newline at end of file diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index a5b1a43..bcc152b 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/cloudly', - version: '5.1.0', + version: '5.2.0', description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' } diff --git a/ts_web/elements/cloudly-dashboard.ts b/ts_web/elements/cloudly-dashboard.ts index e7007e1..76fcb16 100644 --- a/ts_web/elements/cloudly-dashboard.ts +++ b/ts_web/elements/cloudly-dashboard.ts @@ -25,6 +25,7 @@ import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js'; import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; import { CloudlyViewServices } from './cloudly-view-services.js'; import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; +import { CloudlyViewSettings } from './cloudly-view-settings.js'; declare global { interface HTMLElementTagNameMap { @@ -79,6 +80,11 @@ export class CloudlyDashboard extends DeesElement { iconName: 'lucide:LayoutDashboard', element: CloudlyViewOverview, }, + { + name: 'Settings', + iconName: 'lucide:Settings', + element: CloudlyViewSettings, + }, { name: 'SecretGroups', iconName: 'lucide:ShieldCheck', diff --git a/ts_web/elements/cloudly-view-overview.ts b/ts_web/elements/cloudly-view-overview.ts index e5f2c84..9c03636 100644 --- a/ts_web/elements/cloudly-view-overview.ts +++ b/ts_web/elements/cloudly-view-overview.ts @@ -40,9 +40,9 @@ export class CloudlyViewOverview extends DeesElement { ]; public render() { - // Calculate total servers across all clusters - const totalServers = this.data.clusters?.reduce((sum, cluster) => - sum + (cluster.data.servers?.length || 0), 0) || 0; + // Calculate total nodes across all clusters + const totalNodes = this.data.clusters?.reduce((sum, cluster) => + sum + (cluster.data.nodes?.length || 0), 0) || 0; // Create tiles for the stats grid const statsTiles = [ @@ -55,12 +55,12 @@ export class CloudlyViewOverview extends DeesElement { description: 'Active clusters' }, { - id: 'servers', - title: 'Total Servers', - value: totalServers, + id: 'nodes', + title: 'Total Nodes', + value: totalNodes, type: 'number' as const, iconName: 'lucide:Server', - description: 'Connected servers' + description: 'Connected nodes' }, { id: 'services', diff --git a/ts_web/elements/cloudly-view-settings.ts b/ts_web/elements/cloudly-view-settings.ts new file mode 100644 index 0000000..0b2c161 --- /dev/null +++ b/ts_web/elements/cloudly-view-settings.ts @@ -0,0 +1,478 @@ +import * as plugins from '../plugins.js'; +import * as shared from '../elements/shared/index.js'; + +import { + DeesElement, + customElement, + html, + state, + css, + cssManager, + property, +} from '@design.estate/dees-element'; + +import * as appstate from '../appstate.js'; + +@customElement('cloudly-view-settings') +export class CloudlyViewSettings extends DeesElement { + @state() + private settings: plugins.interfaces.data.ICloudlySettingsMasked = {}; + + @state() + private isLoading = false; + + @state() + private testResults: {[key: string]: {success: boolean; message: string}} = {}; + + constructor() { + super(); + this.loadSettings(); + } + + public static styles = [ + cssManager.defaultStyles, + shared.viewHostCss, + css` + .settings-container { + padding: 24px 0; + display: flex; + flex-direction: column; + gap: 16px; + } + + .provider-icon { + margin-right: 8px; + font-size: 20px; + } + + .test-status { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + } + + .test-status dees-button { + margin-left: auto; + } + + .loading-container { + display: flex; + justify-content: center; + padding: 48px; + } + + .actions-container { + display: flex; + justify-content: center; + margin-top: 24px; + } + + dees-panel { + margin-bottom: 16px; + } + + .form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + + .form-grid.single { + grid-template-columns: 1fr; + } + + @media (max-width: 768px) { + .form-grid { + grid-template-columns: 1fr; + } + } + `, + ]; + + private async loadSettings() { + this.isLoading = true; + try { + const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest< + plugins.interfaces.requests.settings.IRequest_GetSettings + >( + '/typedrequest', + 'getSettings' + ); + const response = await trRequest.fire({}); + this.settings = response.settings; + } catch (error) { + console.error('Failed to load settings:', error); + plugins.deesCatalog.DeesToast.createAndShow({ + message: `Failed to load settings: ${error.message}`, + type: 'error', + }); + } finally { + this.isLoading = false; + } + } + + private async saveSettings(formData: any) { + console.log('saveSettings called with formData:', formData); + this.isLoading = true; + try { + const updates: Partial = {}; + + // Process form data + for (const [key, value] of Object.entries(formData)) { + console.log(`Processing ${key}:`, value); + if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) { + // Only update if value changed (not masked) + updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string; + } + } + console.log('Updates to send:', updates); + + const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest< + plugins.interfaces.requests.settings.IRequest_UpdateSettings + >( + '/typedrequest', + 'updateSettings' + ); + const response = await trRequest.fire({ updates }); + + if (response.success) { + plugins.deesCatalog.DeesToast.createAndShow({ + message: 'Settings saved successfully', + type: 'success', + }); + await this.loadSettings(); // Reload to get masked values + } else { + throw new Error(response.message); + } + } catch (error) { + console.error('Failed to save settings:', error); + plugins.deesCatalog.DeesToast.createAndShow({ + message: `Failed to save settings: ${error.message}`, + type: 'error', + }); + } finally { + this.isLoading = false; + } + } + + private async testConnection(provider: string) { + this.isLoading = true; + try { + const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest< + plugins.interfaces.requests.settings.IRequest_TestProviderConnection + >( + '/typedrequest', + 'testProviderConnection' + ); + const response = await trRequest.fire({ provider: provider as any }); + + this.testResults = { + ...this.testResults, + [provider]: { + success: response.connectionValid, + message: response.message + } + }; + + // Show toast notification + plugins.deesCatalog.DeesToast.createAndShow({ + message: response.message, + type: response.connectionValid ? 'success' : 'error', + }); + } catch (error) { + this.testResults = { + ...this.testResults, + [provider]: { + success: false, + message: `Test failed: ${error.message}` + } + }; + plugins.deesCatalog.DeesToast.createAndShow({ + message: `Connection test failed: ${error.message}`, + type: 'error', + }); + } finally { + this.isLoading = false; + } + } + + private renderProviderStatus(provider: string) { + const result = this.testResults[provider]; + if (!result) return ''; + + return html` + + `; + } + + public render() { + if (this.isLoading && Object.keys(this.settings).length === 0) { + return html` +
+ +
+ `; + } + + return html` + Settings +
+ { + console.log('formData event received:', e); + console.log('Event detail:', e.detail); + console.log('Event detail.data:', e.detail.data); + this.saveSettings(e.detail.data); + }}> + + + +
+ ${this.renderProviderStatus('hetzner')} + { + e.preventDefault(); + e.stopPropagation(); + this.testConnection('hetzner'); + }} + > +
+
+ +
+
+ + + +
+ ${this.renderProviderStatus('cloudflare')} + { + e.preventDefault(); + e.stopPropagation(); + this.testConnection('cloudflare'); + }} + > +
+
+ +
+
+ + + +
+ ${this.renderProviderStatus('aws')} + { + e.preventDefault(); + e.stopPropagation(); + this.testConnection('aws'); + }} + > +
+
+ + +
+
+ +
+
+ + + +
+ ${this.renderProviderStatus('digitalocean')} + { + e.preventDefault(); + e.stopPropagation(); + this.testConnection('digitalocean'); + }} + > +
+
+ +
+
+ + + +
+ ${this.renderProviderStatus('azure')} + { + e.preventDefault(); + e.stopPropagation(); + this.testConnection('azure'); + }} + > +
+
+ + +
+
+ + +
+
+ + + +
+ ${this.renderProviderStatus('google')} + { + e.preventDefault(); + e.stopPropagation(); + this.testConnection('google'); + }} + > +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ `; + } +} \ No newline at end of file