feat(settings): Add runtime settings management, node & baremetal managers, and settings UI

This commit is contained in:
2025-09-07 17:21:30 +00:00
parent 83abe37d8c
commit 54ef62e7af
36 changed files with 1914 additions and 301 deletions

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

68
.serena/project.yml Normal file
View File

@@ -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"

View File

@@ -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

View File

@@ -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"
},

264
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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.'
}

View File

@@ -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();

View File

@@ -20,10 +20,8 @@ export class CloudlyConfig {
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
{
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',

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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<boolean> {
// 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<boolean> {
// 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<boolean> {
// 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;
}
}

View File

@@ -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<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_GetBaremetalServers>(
'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<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_ControlBaremetal>(
'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<BareMetal[]> {
const baremetals = await this.CBareMetal.getInstances({});
return baremetals;
}
/**
* Get baremetal by ID
*/
public async getBaremetalById(id: string): Promise<BareMetal | null> {
const baremetal = await this.CBareMetal.getInstance({
id,
});
return baremetal;
}
/**
* Get baremetals by provider
*/
public async getBaremetalsByProvider(provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise'): Promise<BareMetal[]> {
const baremetals = await this.CBareMetal.getInstances({
data: {
provider,
},
});
return baremetals;
}
/**
* Create baremetal from Hetzner server
*/
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
// 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<BareMetal> {
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`);
}
}

View File

@@ -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 {

View File

@@ -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<plugins.servezoneInterfaces.data.IDeployment[]> {
// 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();
}
}

View File

@@ -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<string> {
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:

View File

@@ -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<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetNodeConfig>(
'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;
}
}

View File

@@ -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<plugins.servezoneInterfaces.data.IService[]> {
return [];
}
}

View File

@@ -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<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetServerConfig>(
'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;
}
}

View File

@@ -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<servezoneInterfaces.data.ICloudlySettings>;
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<servezoneInterfaces.data.ICloudlySettings>;
// Setup API route handlers
await this.setupRoutes();
this.readyDeferred.resolve();
}
/**
* Get all settings
*/
public async getSettings(): Promise<servezoneInterfaces.data.ICloudlySettings> {
await this.readyDeferred.promise;
return await this.settingsStore.readAll();
}
/**
* Get all settings with masked sensitive values (for API responses)
*/
public async getSettingsMasked(): Promise<servezoneInterfaces.data.ICloudlySettingsMasked> {
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<servezoneInterfaces.data.ICloudlySettings>): Promise<void> {
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<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K): Promise<servezoneInterfaces.data.ICloudlySettings[K]> {
await this.readyDeferred.promise;
return await this.settingsStore.readKey(key);
}
/**
* Set a specific setting value
*/
public async setSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K, value: servezoneInterfaces.data.ICloudlySettings[K]): Promise<void> {
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<void> {
await this.readyDeferred.promise;
await this.settingsStore.deleteKey(key);
}
/**
* Clear all settings
*/
public async clearAllSettings(): Promise<void> {
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<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
'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<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
'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<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
'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<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
'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<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
'getSetting',
async (requestData) => {
// TODO: Add authentication check for admin users
const value = await this.getSetting(requestData.key);
return {
value
};
}
)
);
}
}

View File

@@ -0,0 +1 @@
export * from './classes.settingsmanager.js';

View File

@@ -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;
};
};
}

View File

@@ -1,8 +1,6 @@
import * as plugins from '../plugins.js';
export interface ICloudlyConfig {
cfToken?: string;
hetznerToken?: string;
environment?: 'production' | 'integration';
letsEncryptEmail?: string;
letsEncryptPrivateKey?: string;

View File

@@ -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.

View File

@@ -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[];
};
}

View File

@@ -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;
};
}

View File

@@ -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';

View File

@@ -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: {

View File

@@ -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;
};

View File

@@ -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;
};
}

View File

@@ -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,
};

View File

@@ -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[];
};
}

View File

@@ -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<ICloudlySettings>;
};
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;
};
}

View File

@@ -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.'
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -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<plugins.interfaces.data.ICloudlySettings> = {};
// 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`
<dees-badge
.type=${result.success ? 'success' : 'error'}
.text=${result.success ? 'Connected' : 'Failed'}
></dees-badge>
`;
}
public render() {
if (this.isLoading && Object.keys(this.settings).length === 0) {
return html`
<div class="loading-container">
<dees-spinner></dees-spinner>
</div>
`;
}
return html`
<cloudly-sectionheading>Settings</cloudly-sectionheading>
<div class="settings-container">
<dees-form @formData=${(e: CustomEvent) => {
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);
}}>
<!-- Hetzner Cloud -->
<dees-panel
.title=${'Hetzner Cloud'}
.subtitle=${'Configure Hetzner Cloud API access'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('hetzner')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('hetzner');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'hetznerToken'}
.label=${'API Token'}
.value=${this.settings.hetznerToken || ''}
.isPasswordBool=${true}
.description=${'Your Hetzner Cloud API token for managing infrastructure'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- Cloudflare -->
<dees-panel
.title=${'Cloudflare'}
.subtitle=${'Configure Cloudflare API access'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('cloudflare')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('cloudflare');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'cloudflareToken'}
.label=${'API Token'}
.value=${this.settings.cloudflareToken || ''}
.isPasswordBool=${true}
.description=${'Cloudflare API token with DNS and Zone permissions'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- AWS -->
<dees-panel
.title=${'Amazon Web Services'}
.subtitle=${'Configure AWS credentials'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('aws')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('aws');
}}
></dees-button>
</div>
<div class="form-grid">
<dees-input-text
.key=${'awsAccessKey'}
.label=${'Access Key ID'}
.value=${this.settings.awsAccessKey || ''}
.isPasswordBool=${true}
.description=${'AWS IAM access key identifier'}
.required=${false}
></dees-input-text>
<dees-input-text
.key=${'awsSecretKey'}
.label=${'Secret Access Key'}
.value=${this.settings.awsSecretKey || ''}
.isPasswordBool=${true}
.description=${'AWS IAM secret access key'}
.required=${false}
></dees-input-text>
</div>
<div class="form-grid single">
<dees-input-dropdown
.key=${'awsRegion'}
.label=${'Default Region'}
.selectedOption=${this.settings.awsRegion || 'us-east-1'}
.options=${[
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
]}
.description=${'Default AWS region for resource provisioning'}
></dees-input-dropdown>
</div>
</dees-panel>
<!-- DigitalOcean -->
<dees-panel
.title=${'DigitalOcean'}
.subtitle=${'Configure DigitalOcean API access'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('digitalocean')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('digitalocean');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'digitalOceanToken'}
.label=${'Personal Access Token'}
.value=${this.settings.digitalOceanToken || ''}
.isPasswordBool=${true}
.description=${'DigitalOcean personal access token with read/write scope'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- Azure -->
<dees-panel
.title=${'Microsoft Azure'}
.subtitle=${'Configure Azure service principal'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('azure')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('azure');
}}
></dees-button>
</div>
<div class="form-grid">
<dees-input-text
.key=${'azureClientId'}
.label=${'Application (Client) ID'}
.value=${this.settings.azureClientId || ''}
.isPasswordBool=${true}
.description=${'Azure AD application client ID'}
.required=${false}
></dees-input-text>
<dees-input-text
.key=${'azureClientSecret'}
.label=${'Client Secret'}
.value=${this.settings.azureClientSecret || ''}
.isPasswordBool=${true}
.description=${'Azure AD application client secret'}
.required=${false}
></dees-input-text>
</div>
<div class="form-grid">
<dees-input-text
.key=${'azureTenantId'}
.label=${'Directory (Tenant) ID'}
.value=${this.settings.azureTenantId || ''}
.description=${'Azure AD tenant identifier'}
.required=${false}
></dees-input-text>
<dees-input-text
.key=${'azureSubscriptionId'}
.label=${'Subscription ID'}
.value=${this.settings.azureSubscriptionId || ''}
.description=${'Azure subscription for resource management'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- Google Cloud -->
<dees-panel
.title=${'Google Cloud Platform'}
.subtitle=${'Configure GCP service account'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('google')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('google');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-textarea
.key=${'googleCloudKeyJson'}
.label=${'Service Account Key (JSON)'}
.value=${this.settings.googleCloudKeyJson || ''}
.isPasswordBool=${true}
.description=${'Complete JSON key file for service account authentication'}
.required=${false}
></dees-input-textarea>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'googleCloudProjectId'}
.label=${'Project ID'}
.value=${this.settings.googleCloudProjectId || ''}
.description=${'Google Cloud project identifier'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<div class="actions-container">
<dees-form-submit
.text=${'Save All Settings'}
.disabled=${this.isLoading}
></dees-form-submit>
</div>
</dees-form>
</div>
`;
}
}