feat(settings): Add runtime settings management, node & baremetal managers, and settings UI
This commit is contained in:
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal 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"
|
13
changelog.md
13
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
|
||||
|
||||
|
@@ -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
264
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
|
@@ -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.'
|
||||
}
|
||||
|
@@ -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();
|
||||
await this.nodeManager.start();
|
||||
await this.baremetalManager.start();
|
||||
|
||||
// connectors
|
||||
await this.mongodbConnector.init();
|
||||
await this.cloudflareConnector.init();
|
||||
await this.letsencryptConnector.init();
|
||||
await this.clusterManager.init();
|
||||
|
@@ -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',
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
104
ts/manager.baremetal/classes.baremetal.ts
Normal file
104
ts/manager.baremetal/classes.baremetal.ts
Normal 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;
|
||||
}
|
||||
}
|
176
ts/manager.baremetal/classes.baremetalmanager.ts
Normal file
176
ts/manager.baremetal/classes.baremetalmanager.ts
Normal 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`);
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
61
ts/manager.node/classes.clusternode.ts
Normal file
61
ts/manager.node/classes.clusternode.ts
Normal 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();
|
||||
}
|
||||
}
|
@@ -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:
|
131
ts/manager.node/classes.nodemanager.ts
Normal file
131
ts/manager.node/classes.nodemanager.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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 [];
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
255
ts/manager.settings/classes.settingsmanager.ts
Normal file
255
ts/manager.settings/classes.settingsmanager.ts
Normal 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
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
1
ts/manager.settings/index.ts
Normal file
1
ts/manager.settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.settingsmanager.js';
|
73
ts_interfaces/data/baremetal.ts
Normal file
73
ts_interfaces/data/baremetal.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,8 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export interface ICloudlyConfig {
|
||||
cfToken?: string;
|
||||
hetznerToken?: string;
|
||||
environment?: 'production' | 'integration';
|
||||
letsEncryptEmail?: string;
|
||||
letsEncryptPrivateKey?: string;
|
||||
|
@@ -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.
|
||||
|
71
ts_interfaces/data/clusternode.ts
Normal file
71
ts_interfaces/data/clusternode.ts
Normal 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[];
|
||||
};
|
||||
}
|
@@ -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;
|
||||
};
|
||||
}
|
@@ -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';
|
||||
|
@@ -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: {
|
||||
|
56
ts_interfaces/data/settings.ts
Normal file
56
ts_interfaces/data/settings.ts
Normal 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;
|
||||
};
|
22
ts_interfaces/requests/baremetal.ts
Normal file
22
ts_interfaces/requests/baremetal.ts
Normal 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;
|
||||
};
|
||||
}
|
@@ -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,
|
||||
};
|
||||
|
33
ts_interfaces/requests/node.ts
Normal file
33
ts_interfaces/requests/node.ts
Normal 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[];
|
||||
};
|
||||
}
|
59
ts_interfaces/requests/settings.ts
Normal file
59
ts_interfaces/requests/settings.ts
Normal 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;
|
||||
};
|
||||
}
|
@@ -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.'
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
478
ts_web/elements/cloudly-view-settings.ts
Normal file
478
ts_web/elements/cloudly-view-settings.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user