Compare commits

...

56 Commits

Author SHA1 Message Date
7ab0fb3c1f v1.18.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 27s
CI / Build Test (Current Platform) (push) Successful in 58s
CI / Build All Platforms (push) Successful in 1m52s
Release / build-and-release (push) Successful in 2m58s
2026-03-16 13:51:43 +00:00
713fda2a86 feat(platform-services): add platform service log retrieval and display in the services UI 2026-03-16 13:51:43 +00:00
ec32c19300 v1.17.4
Some checks failed
CI / Type Check & Lint (push) Failing after 30s
Publish to npm / npm-publish (push) Failing after 24s
CI / Build Test (Current Platform) (push) Successful in 1m1s
CI / Build All Platforms (push) Successful in 2m12s
Release / build-and-release (push) Successful in 4m0s
2026-03-16 13:26:56 +00:00
7d1d91157c fix(docs): add hello world running screenshot for documentation 2026-03-16 13:26:56 +00:00
b69c96c240 v1.17.3
Some checks failed
CI / Build Test (Current Platform) (push) Failing after 6s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 32s
CI / Build All Platforms (push) Successful in 2m4s
Release / build-and-release (push) Successful in 3m13s
2026-03-16 13:05:47 +00:00
9ee8851d03 fix(mongodb): downgrade the MongoDB service image to 4.4 and use the legacy mongo shell for container operations 2026-03-16 13:05:47 +00:00
7f6031f31a v1.17.2
Some checks failed
CI / Type Check & Lint (push) Failing after 29s
Publish to npm / npm-publish (push) Failing after 25s
CI / Build Test (Current Platform) (push) Successful in 1m4s
CI / Build All Platforms (push) Successful in 2m5s
Release / build-and-release (push) Successful in 4m12s
2026-03-16 12:45:44 +00:00
6f1b8469e0 fix(platform-services): provision ClickHouse, MinIO, and MongoDB resources via docker exec instead of host port access 2026-03-16 12:45:44 +00:00
cd06c74cc3 v1.17.1
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 41s
CI / Build Test (Current Platform) (push) Successful in 1m12s
Release / build-and-release (push) Failing after 1m54s
CI / Build All Platforms (push) Successful in 2m17s
2026-03-16 12:40:39 +00:00
d3acc720ca fix(repo): no changes to commit 2026-03-16 12:40:39 +00:00
1b6de75097 v1.17.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 56s
Release / build-and-release (push) Failing after 37s
CI / Build All Platforms (push) Successful in 2m7s
2026-03-16 12:36:02 +00:00
497f8f59a7 feat(web/services): add deploy service action to the services view 2026-03-16 12:36:02 +00:00
0c7d65e4ad v1.16.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 28s
CI / Build Test (Current Platform) (push) Successful in 52s
CI / Build All Platforms (push) Successful in 1m50s
Release / build-and-release (push) Successful in 2m49s
2026-03-16 11:45:56 +00:00
3f2cd074ce feat(services): add platform service navigation and stats in the services UI 2026-03-16 11:45:56 +00:00
59ed7233bd v1.15.3
Some checks failed
CI / Build All Platforms (push) Failing after 3s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 25s
CI / Build Test (Current Platform) (push) Successful in 54s
Release / build-and-release (push) Successful in 2m35s
2026-03-16 11:07:00 +00:00
01e3ba16c4 fix(install): refresh systemd service configuration before restarting previously running installations 2026-03-16 11:07:00 +00:00
f5c1d5fcda v1.15.2
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 1m58s
Release / build-and-release (push) Successful in 3m9s
2026-03-16 10:58:08 +00:00
45b0971f2f fix(systemd): set HOME and DENO_DIR for the systemd service environment 2026-03-16 10:58:08 +00:00
178f440d7e v1.15.1
Some checks failed
CI / Type Check & Lint (push) Failing after 30s
Publish to npm / npm-publish (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m0s
CI / Build All Platforms (push) Successful in 2m8s
Release / build-and-release (push) Successful in 2m53s
2026-03-16 10:23:05 +00:00
7fff15a90c fix(systemd): move Docker installation and swarm initialization to systemd enable flow 2026-03-16 10:23:05 +00:00
69e23f667e v1.15.0
Some checks failed
CI / Build All Platforms (push) Failing after 7s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 26s
CI / Build Test (Current Platform) (push) Successful in 58s
Release / build-and-release (push) Successful in 2m58s
2026-03-16 10:02:59 +00:00
a2bf4df7c2 feat(systemd): replace smartdaemon-based service management with native systemd commands 2026-03-16 10:02:59 +00:00
9e0a0b5a89 v1.14.10
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 59s
CI / Build All Platforms (push) Successful in 2m1s
Release / build-and-release (push) Successful in 2m42s
2026-03-16 08:40:48 +00:00
3a227bd838 fix(services): stop auto-update monitoring during shutdown 2026-03-16 08:40:48 +00:00
f5a7fccfc2 v1.14.9
Some checks failed
CI / Type Check & Lint (push) Failing after 25s
Publish to npm / npm-publish (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 57s
CI / Build All Platforms (push) Successful in 2m20s
Release / build-and-release (push) Successful in 3m50s
2026-03-16 08:25:32 +00:00
a30d2029a5 fix(repo): no changes to commit 2026-03-16 08:25:32 +00:00
88727dd47d v1.14.8
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 45s
CI / Build Test (Current Platform) (push) Successful in 1m19s
CI / Build All Platforms (push) Successful in 2m46s
Release / build-and-release (push) Successful in 5m20s
2026-03-16 03:06:23 +00:00
9a5ed2220e fix(repo): no changes to commit 2026-03-16 03:06:23 +00:00
decd39e7c4 v1.14.7
Some checks failed
CI / Build All Platforms (push) Has been cancelled
CI / Type Check & Lint (push) Has been cancelled
CI / Build Test (Current Platform) (push) Has been cancelled
Publish to npm / npm-publish (push) Failing after 8s
Release / build-and-release (push) Successful in 5m34s
2026-03-16 03:06:17 +00:00
ad2e228208 fix(repo): no changes to commit 2026-03-16 03:06:17 +00:00
cf06019d79 v1.14.6
Some checks failed
CI / Build Test (Current Platform) (push) Has been cancelled
CI / Type Check & Lint (push) Has been cancelled
CI / Build All Platforms (push) Has been cancelled
Publish to npm / npm-publish (push) Failing after 6s
Release / build-and-release (push) Successful in 5m30s
2026-03-16 03:06:08 +00:00
cf44b0047d fix(project): no changes to commit 2026-03-16 03:06:08 +00:00
260b5364e6 v1.14.5
Some checks failed
CI / Build All Platforms (push) Failing after 6s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 24s
CI / Build Test (Current Platform) (push) Successful in 51s
Release / build-and-release (push) Successful in 3m30s
2026-03-16 03:04:57 +00:00
51c1962042 fix(onebox): move Docker auto-install and swarm initialization into Onebox startup flow 2026-03-16 03:04:57 +00:00
d3b78054ad v1.14.4
Some checks failed
CI / Build Test (Current Platform) (push) Failing after 5s
Publish to npm / npm-publish (push) Failing after 7s
CI / Type Check & Lint (push) Failing after 30s
CI / Build All Platforms (push) Successful in 1m57s
Release / build-and-release (push) Successful in 3m15s
2026-03-16 02:37:59 +00:00
d2ae35f0ce fix(repo): no changes to commit 2026-03-16 02:37:59 +00:00
a605477663 v1.14.3
Some checks failed
Publish to npm / npm-publish (push) Failing after 7s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 52s
CI / Build All Platforms (push) Successful in 1m54s
Release / build-and-release (push) Successful in 3m14s
2026-03-16 02:17:20 +00:00
ba98086548 fix(repo): no changes to commit 2026-03-16 02:17:20 +00:00
0b3c22556b v1.14.2
Some checks failed
CI / Build All Platforms (push) Failing after 6s
Publish to npm / npm-publish (push) Failing after 7s
CI / Type Check & Lint (push) Failing after 23s
CI / Build Test (Current Platform) (push) Successful in 51s
Release / build-and-release (push) Has been cancelled
2026-03-16 02:11:41 +00:00
069e6e6c8f fix(repo): no changes to commit 2026-03-16 02:11:41 +00:00
10598520d8 v1.14.1
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 28s
CI / Build Test (Current Platform) (push) Successful in 57s
CI / Build All Platforms (push) Successful in 2m6s
Release / build-and-release (push) Successful in 3m32s
2026-03-16 01:33:07 +00:00
075b7946b1 fix(repo): no changes to commit 2026-03-16 01:33:07 +00:00
f47fca3304 v1.14.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 7s
CI / Type Check & Lint (push) Failing after 31s
CI / Build Test (Current Platform) (push) Successful in 1m2s
CI / Build All Platforms (push) Successful in 2m5s
Release / build-and-release (push) Successful in 4m57s
2026-03-16 01:19:58 +00:00
575e010a6b feat(daemon): auto-install Docker and initialize Swarm during daemon service setup 2026-03-16 01:19:58 +00:00
60a5dc4663 v1.13.17
Some checks failed
CI / Type Check & Lint (push) Failing after 39s
Publish to npm / npm-publish (push) Failing after 54s
CI / Build Test (Current Platform) (push) Successful in 1m49s
CI / Build All Platforms (push) Successful in 3m18s
Release / build-and-release (push) Successful in 4m38s
2026-03-16 01:10:23 +00:00
36d80b1e27 fix(ci): remove forced container image pulling from Gitea workflow jobs 2026-03-16 01:10:23 +00:00
465cf0ee72 v1.13.16
Some checks failed
CI / Type Check & Lint (push) Failing after 0s
CI / Build Test (Current Platform) (push) Failing after 0s
CI / Build All Platforms (push) Failing after 0s
Publish to npm / npm-publish (push) Failing after 0s
Release / build-and-release (push) Failing after 2m33s
2026-03-16 00:59:27 +00:00
bd5cd5c0cb fix(ci): refresh workflow container images on every run and bump @apiclient.xyz/docker to ^5.1.1 2026-03-16 00:59:27 +00:00
b622565e34 v1.13.15
Some checks failed
CI / Build Test (Current Platform) (push) Failing after 5s
CI / Type Check & Lint (push) Failing after 30s
Publish to npm / npm-publish (push) Failing after 35s
CI / Build All Platforms (push) Successful in 2m25s
Release / build-and-release (push) Successful in 3m43s
2026-03-15 21:13:56 +00:00
56376121ab fix(repo): no changes to commit 2026-03-15 21:13:56 +00:00
e3359d1235 v1.13.14
Some checks failed
CI / Type Check & Lint (push) Failing after 2s
CI / Build Test (Current Platform) (push) Failing after 2s
CI / Build All Platforms (push) Failing after 2s
Publish to npm / npm-publish (push) Failing after 2s
Release / build-and-release (push) Successful in 5m55s
2026-03-15 20:56:22 +00:00
f1eeec6922 fix(repo): no changes to commit 2026-03-15 20:56:22 +00:00
69362bb529 v1.13.13
Some checks failed
CI / Build All Platforms (push) Failing after 6s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Failing after 50s
Release / build-and-release (push) Failing after 4s
2026-03-15 20:49:49 +00:00
857fcc50ba fix(repo): no changes to commit 2026-03-15 20:49:49 +00:00
5d0df006eb v1.13.12
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 30s
CI / Build Test (Current Platform) (push) Failing after 59s
Release / build-and-release (push) Failing after 1m10s
CI / Build All Platforms (push) Successful in 1m49s
2026-03-15 19:27:13 +00:00
e6256502ce fix(ci): run pnpm install with --ignore-scripts in CI and release workflows 2026-03-15 19:27:13 +00:00
30 changed files with 886 additions and 439 deletions

View File

@@ -62,7 +62,7 @@ jobs:
run: corepack enable
- name: Install dependencies
run: pnpm install
run: pnpm install --ignore-scripts
- name: Compile for current platform
run: |
@@ -101,7 +101,7 @@ jobs:
run: corepack enable
- name: Install dependencies
run: pnpm install
run: pnpm install --ignore-scripts
- name: Compile all platform binaries
run: mkdir -p dist/binaries && npx tsdeno compile

View File

@@ -31,7 +31,7 @@ jobs:
run: corepack enable
- name: Install dependencies
run: pnpm install
run: pnpm install --ignore-scripts
- name: Get version from tag
id: version

View File

@@ -1,5 +1,158 @@
# Changelog
## 2026-03-16 - 1.18.0 - feat(platform-services)
add platform service log retrieval and display in the services UI
- add typed request support in the ops server to fetch Docker logs for platform service containers
- store fetched platform service logs in web app state and load them when opening platform service details
- render platform service logs in the services detail view and add sidebar icons for main navigation tabs
## 2026-03-16 - 1.17.4 - fix(docs)
add hello world running screenshot for documentation
- Adds a new PNG asset showing the application in a running hello world state.
- Supports project documentation or README usage without changing runtime behavior.
## 2026-03-16 - 1.17.3 - fix(mongodb)
downgrade the MongoDB service image to 4.4 and use the legacy mongo shell for container operations
- changes the default MongoDB container image from mongo:7 to mongo:4.4
- replaces mongosh with mongo for health checks, provisioning, and deprovisioning inside the container
## 2026-03-16 - 1.17.2 - fix(platform-services)
provision ClickHouse, MinIO, and MongoDB resources via docker exec instead of host port access
- switch ClickHouse provisioning and teardown to in-container client commands to avoid host port mapping issues
- replace MinIO host-side S3 API calls with in-container mc commands for bucket creation and removal
- run MongoDB provisioning and deprovisioning through mongosh inside the container and improve docker exec failure reporting
## 2026-03-16 - 1.17.1 - fix(repo)
no changes to commit
## 2026-03-16 - 1.17.0 - feat(web/services)
add deploy service action to the services view
- Adds a prominent "Deploy Service" button to the services page header.
- Routes users into the create service view directly from the services listing.
- Includes a new service creation form screenshot asset for the updated interface.
## 2026-03-16 - 1.16.0 - feat(services)
add platform service navigation and stats in the services UI
- add platform service stats state and fetch action
- show platform services in the services list and open a platform detail view
- enable dashboard clicks to jump directly to the selected platform service
- refresh platform service stats after start and restart actions
- bump @serve.zone/catalog to ^2.6.0 for the new platform service UI components
## 2026-03-16 - 1.15.3 - fix(install)
refresh systemd service configuration before restarting previously running installations
- Re-enable the systemd service during updates so unit file changes are applied before restart
- Add a log message indicating the service configuration is being refreshed
## 2026-03-16 - 1.15.2 - fix(systemd)
set HOME and DENO_DIR for the systemd service environment
- Adds HOME=/root to the generated onebox systemd unit
- Adds DENO_DIR=/root/.cache/deno so Deno cache paths are available when running as a service
## 2026-03-16 - 1.15.1 - fix(systemd)
move Docker installation and swarm initialization to systemd enable flow
- Ensures Docker is installed before writing and enabling the systemd unit that depends on docker.service.
- Removes Docker auto-installation from Onebox initialization so setup happens in the service management path.
## 2026-03-16 - 1.15.0 - feat(systemd)
replace smartdaemon-based service management with native systemd commands
- adds a dedicated OneboxSystemd manager for enabling, disabling, starting, stopping, checking status, and following logs
- introduces a new `onebox systemd` CLI command set and updates install and help output to use it
- removes the smartdaemon dependency and related service management code
## 2026-03-16 - 1.14.10 - fix(services)
stop auto-update monitoring during shutdown
- Track the auto-update polling interval in the services manager
- Clear the auto-update interval when Onebox shuts down to prevent background checks after shutdown
## 2026-03-16 - 1.14.9 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.8 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.7 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.6 - fix(project)
no changes to commit
## 2026-03-16 - 1.14.5 - fix(onebox)
move Docker auto-install and swarm initialization into Onebox startup flow
- removes Docker setup from daemon service installation
- ensures Docker is installed before Docker initialization during Onebox startup
- preserves automatic Docker Swarm initialization on fresh servers
## 2026-03-16 - 1.14.4 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.3 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.2 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.1 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.0 - feat(daemon)
auto-install Docker and initialize Swarm during daemon service setup
- Adds a Docker availability check before installing the Onebox daemon service
- Installs Docker automatically when it is missing using the standard installation script
- Attempts to initialize Docker Swarm after installation and handles already-initialized environments gracefully
## 2026-03-16 - 1.13.17 - fix(ci)
remove forced container image pulling from Gitea workflow jobs
- Drops the `--pull always` container option from CI, npm publish, and release workflows.
- Keeps workflow container images unchanged while avoiding forced pulls on every job run.
## 2026-03-16 - 1.13.16 - fix(ci)
refresh workflow container images on every run and bump @apiclient.xyz/docker to ^5.1.1
- add --pull always to CI, release, and npm publish workflow containers to avoid stale images
- update @apiclient.xyz/docker from ^5.1.0 to ^5.1.1 in deno.json
## 2026-03-15 - 1.13.15 - fix(repo)
no changes to commit
## 2026-03-15 - 1.13.14 - fix(repo)
no changes to commit
## 2026-03-15 - 1.13.13 - fix(repo)
no changes to commit
## 2026-03-15 - 1.13.12 - fix(ci)
run pnpm install with --ignore-scripts in CI and release workflows
- Update CI workflow dependency installation steps to skip lifecycle scripts during builds.
- Apply the same install change to the release workflow for consistent automation behavior.
## 2026-03-15 - 1.13.11 - fix(project)
no changes to commit

BIN
create-service-form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/onebox",
"version": "1.13.11",
"version": "1.18.0",
"exports": "./mod.ts",
"tasks": {
"test": "deno test --allow-all test/",
@@ -15,8 +15,7 @@
"@std/assert": "jsr:@std/assert@^1.0.15",
"@std/encoding": "jsr:@std/encoding@^1.0.10",
"@db/sqlite": "jsr:@db/sqlite@0.12.0",
"@push.rocks/smartdaemon": "npm:@push.rocks/smartdaemon@^2.1.0",
"@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.1.0",
"@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.1.1",
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3",
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0",
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.2.0",

BIN
hello-world-running.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -23,7 +23,7 @@ SPECIFIED_VERSION=""
INSTALL_DIR="/opt/onebox"
GITEA_BASE_URL="https://code.foss.global"
GITEA_REPO="serve.zone/onebox"
SERVICE_NAME="smartdaemon_onebox"
SERVICE_NAME="onebox"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
@@ -250,8 +250,10 @@ echo ""
mkdir -p /var/lib/onebox
mkdir -p /var/www/certbot
# Restart service if it was running before update
# Re-enable and restart service if it was previously running (refreshes unit file)
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
echo "Refreshing systemd service..."
onebox systemd enable
echo "Restarting Onebox service..."
systemctl restart "$SERVICE_NAME"
echo "Service restarted successfully."
@@ -276,7 +278,7 @@ if [ -f "/var/lib/onebox/onebox.db" ]; then
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
echo "The service has been restarted with your current settings."
else
echo "Start the service with: onebox daemon start"
echo "Start the service with: onebox systemd start"
fi
else
echo "Get started:"
@@ -293,11 +295,11 @@ else
echo " 2. Configure ACME email:"
echo " onebox config set acmeEmail <your@email.com>"
echo ""
echo " 3. Install daemon:"
echo " onebox daemon install"
echo " 3. Enable systemd service:"
echo " onebox systemd enable"
echo ""
echo " 4. Start daemon:"
echo " onebox daemon start"
echo " 4. Start service:"
echo " onebox systemd start"
echo ""
echo " 5. Deploy your first service:"
echo " onebox service add myapp --image nginx:latest --domain app.example.com"

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/onebox",
"version": "1.13.11",
"version": "1.18.0",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts",
"type": "module",
@@ -57,7 +57,7 @@
"@api.global/typedrequest-interfaces": "^3.0.19",
"@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6",
"@serve.zone/catalog": "^2.5.0"
"@serve.zone/catalog": "^2.6.0"
},
"devDependencies": {
"@git.zone/tsbundle": "^2.9.0",

10
pnpm-lock.yaml generated
View File

@@ -18,8 +18,8 @@ importers:
specifier: ^2.1.6
version: 2.2.3
'@serve.zone/catalog':
specifier: ^2.5.0
version: 2.5.0(@tiptap/pm@2.27.2)
specifier: ^2.6.0
version: 2.6.0(@tiptap/pm@2.27.2)
devDependencies:
'@git.zone/tsbundle':
specifier: ^2.9.0
@@ -836,8 +836,8 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@serve.zone/catalog@2.5.0':
resolution: {integrity: sha512-bRwk7pbDxUB471wUAS7p22MTOOBCHlMWijsE43K9tDAPcxlRarhtf2Dgx0Y25s/dFXqj2JHwe6jjE84S80jFzg==}
'@serve.zone/catalog@2.6.0':
resolution: {integrity: sha512-Gp91Ed0MMLMPSAZrH2/UimGxU9AaTu5IPUobPD49PSvh3UnUl+HEFiy81kpKJhW16V37N3/vcNgZrn2VI7/LxQ==}
'@tempfix/idb@8.0.3':
resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==}
@@ -3474,7 +3474,7 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@serve.zone/catalog@2.5.0(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.6.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.1

BIN
sidebar-icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/onebox',
version: '1.13.11',
version: '1.18.0',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
}

View File

@@ -4,9 +4,7 @@
* Handles background monitoring, metrics collection, and automatic tasks
*/
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { projectInfo } from '../info.ts';
import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts';
@@ -18,7 +16,6 @@ const FALLBACK_PID_FILE = `${FALLBACK_PID_DIR}/onebox.pid`;
export class OneboxDaemon {
private oneboxRef: Onebox;
private smartdaemon: plugins.smartdaemon.SmartDaemon | null = null;
private running = false;
private monitoringInterval: number | null = null;
private statsInterval: number | null = null;
@@ -46,68 +43,6 @@ export class OneboxDaemon {
}
}
/**
* Install systemd service
*/
async installService(): Promise<void> {
try {
logger.info('Installing Onebox daemon service...');
// Initialize smartdaemon if needed
if (!this.smartdaemon) {
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
}
// Get installation directory
const execPath = Deno.execPath();
const service = await this.smartdaemon.addService({
name: 'onebox',
version: projectInfo.version,
command: `${execPath} run --allow-all ${Deno.cwd()}/mod.ts daemon start`,
description: 'Onebox - Self-hosted container platform',
workingDir: Deno.cwd(),
});
await service.save();
await service.enable();
logger.success('Onebox daemon service installed');
logger.info('Start with: sudo systemctl start smartdaemon_onebox');
} catch (error) {
logger.error(`Failed to install daemon service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Uninstall systemd service
*/
async uninstallService(): Promise<void> {
try {
logger.info('Uninstalling Onebox daemon service...');
// Initialize smartdaemon if needed
if (!this.smartdaemon) {
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
}
const services = await this.smartdaemon.systemdManager.getServices();
const service = services.find(s => s.name === 'onebox');
if (service) {
await service.stop();
await service.disable();
await service.delete();
}
logger.success('Onebox daemon service uninstalled');
} catch (error) {
logger.error(`Failed to uninstall daemon service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Start daemon mode (background monitoring)
*/
@@ -482,36 +417,7 @@ export class OneboxDaemon {
static async ensureNoDaemon(): Promise<void> {
const running = await OneboxDaemon.isDaemonRunning();
if (running) {
throw new Error('Daemon is already running. Please stop it first with: onebox daemon stop');
}
}
/**
* Get service status from systemd
*/
async getServiceStatus(): Promise<string> {
try {
// Don't need smartdaemon to check status, just use systemctl directly
const command = new Deno.Command('systemctl', {
args: ['status', 'smartdaemon_onebox'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
return 'not-installed';
throw new Error('Daemon is already running. Please stop it first with: onebox systemd stop');
}
}
}

View File

@@ -881,12 +881,12 @@ export class OneboxDockerManager {
]);
const execInfo = await inspect();
const exitCode = execInfo.ExitCode || 0;
const exitCode = execInfo.ExitCode ?? -1;
return { stdout, stderr, exitCode };
} catch (error) {
logger.error(`Failed to exec in container ${containerID}: ${getErrorMessage(error)}`);
throw error;
return { stdout: '', stderr: getErrorMessage(error), exitCode: -1 };
}
}

View File

@@ -14,6 +14,7 @@ import { OneboxReverseProxy } from './reverseproxy.ts';
import { OneboxDnsManager } from './dns.ts';
import { OneboxSslManager } from './ssl.ts';
import { OneboxDaemon } from './daemon.ts';
import { OneboxSystemd } from './systemd.ts';
import { OneboxHttpServer } from './httpserver.ts';
import { CloudflareDomainSync } from './cloudflare-sync.ts';
import { CertRequirementManager } from './cert-requirement-manager.ts';
@@ -33,6 +34,7 @@ export class Onebox {
public dns: OneboxDnsManager;
public ssl: OneboxSslManager;
public daemon: OneboxDaemon;
public systemd: OneboxSystemd;
public httpServer: OneboxHttpServer;
public cloudflareDomainSync: CloudflareDomainSync;
public certRequirementManager: CertRequirementManager;
@@ -57,6 +59,7 @@ export class Onebox {
this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this);
this.systemd = new OneboxSystemd();
this.httpServer = new OneboxHttpServer(this);
this.registry = new RegistryManager({
dataDir: './.nogit/registry-data',
@@ -320,20 +323,6 @@ export class Onebox {
}
}
/**
* Start daemon mode
*/
async startDaemon(): Promise<void> {
await this.daemon.start();
}
/**
* Stop daemon mode
*/
async stopDaemon(): Promise<void> {
await this.daemon.stop();
}
/**
* Start OpsServer (TypedRequest-based, serves new UI)
*/
@@ -355,6 +344,9 @@ export class Onebox {
try {
logger.info('Shutting down Onebox...');
// Stop auto-update monitoring
this.services.stopAutoUpdateMonitoring();
// Stop backup scheduler
await this.backupScheduler.stop();

View File

@@ -194,12 +194,6 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName();
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 8123);
if (!hostPort) {
throw new Error('Could not get ClickHouse container host port');
}
// Generate resource names and credentials
const dbName = this.generateResourceName(userService.name);
const username = this.generateResourceName(userService.name);
@@ -207,35 +201,16 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
logger.info(`Provisioning ClickHouse database '${dbName}' for service '${userService.name}'...`);
// Connect to ClickHouse via localhost and the mapped host port
const baseUrl = `http://127.0.0.1:${hostPort}`;
// Use docker exec to provision inside the container (avoids host port mapping issues)
const queries = [
`CREATE DATABASE IF NOT EXISTS ${dbName}`,
`CREATE USER IF NOT EXISTS ${username} IDENTIFIED BY '${password}'`,
`GRANT ALL ON ${dbName}.* TO ${username}`,
];
// Create database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`CREATE DATABASE IF NOT EXISTS ${dbName}`
);
logger.info(`Created ClickHouse database '${dbName}'`);
// Create user with access to this database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`CREATE USER IF NOT EXISTS ${username} IDENTIFIED BY '${password}'`
);
logger.info(`Created ClickHouse user '${username}'`);
// Grant permissions on the database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`GRANT ALL ON ${dbName}.* TO ${username}`
);
logger.info(`Granted permissions to user '${username}' on database '${dbName}'`);
for (const query of queries) {
await this.execClickHouseQuery(platformService.containerId, adminCreds, query);
}
logger.success(`ClickHouse database '${dbName}' provisioned with user '${username}'`);
@@ -274,37 +249,11 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 8123);
if (!hostPort) {
throw new Error('Could not get ClickHouse container host port');
}
logger.info(`Deprovisioning ClickHouse database '${resource.resourceName}'...`);
const baseUrl = `http://127.0.0.1:${hostPort}`;
try {
// Drop the user
try {
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`DROP USER IF EXISTS ${credentials.username}`
);
logger.info(`Dropped ClickHouse user '${credentials.username}'`);
} catch (e) {
logger.warn(`Could not drop ClickHouse user: ${getErrorMessage(e)}`);
}
// Drop the database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`DROP DATABASE IF EXISTS ${resource.resourceName}`
);
await this.execClickHouseQuery(platformService.containerId, adminCreds, `DROP USER IF EXISTS ${credentials.username}`);
await this.execClickHouseQuery(platformService.containerId, adminCreds, `DROP DATABASE IF EXISTS ${resource.resourceName}`);
logger.success(`ClickHouse database '${resource.resourceName}' dropped`);
} catch (e) {
logger.error(`Failed to deprovision ClickHouse database: ${getErrorMessage(e)}`);
@@ -313,26 +262,27 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
}
/**
* Execute a ClickHouse SQL query via HTTP interface
* Execute a ClickHouse SQL query via docker exec inside the container
*/
private async executeQuery(
baseUrl: string,
username: string,
password: string,
private async execClickHouseQuery(
containerId: string,
adminCreds: { username: string; password: string },
query: string
): Promise<string> {
const url = `${baseUrl}/?user=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`;
const result = await this.oneboxRef.docker.execInContainer(
containerId,
[
'clickhouse-client',
'--user', adminCreds.username,
'--password', adminCreds.password,
'--query', query,
]
);
const response = await fetch(url, {
method: 'POST',
body: query,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`ClickHouse query failed: ${errorText}`);
if (result.exitCode !== 0) {
throw new Error(`ClickHouse query failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`);
}
return await response.text();
return result.stdout;
}
}

View File

@@ -196,84 +196,28 @@ export class MinioProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName();
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000);
if (!hostPort) {
throw new Error('Could not get MinIO container host port');
}
// Generate bucket name and credentials
// Generate bucket name
const bucketName = this.generateBucketName(userService.name);
const accessKey = credentialEncryption.generateAccessKey(20);
const secretKey = credentialEncryption.generateSecretKey(40);
logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`);
// Connect to MinIO via localhost and the mapped host port (for provisioning from host)
const provisioningEndpoint = `http://127.0.0.1:${hostPort}`;
// Import AWS S3 client
const { S3Client, CreateBucketCommand, PutBucketPolicyCommand } = await import('npm:@aws-sdk/client-s3@3');
// Create S3 client with admin credentials - connect via host port
const s3Client = new S3Client({
endpoint: provisioningEndpoint,
region: 'us-east-1',
credentials: {
accessKeyId: adminCreds.username,
secretAccessKey: adminCreds.password,
},
forcePathStyle: true,
});
// Use docker exec with mc (MinIO Client) inside the container
// First configure mc alias for local server
await this.execMc(platformService.containerId, [
'alias', 'set', 'local', 'http://localhost:9000',
adminCreds.username, adminCreds.password,
]);
// Create the bucket
try {
await s3Client.send(new CreateBucketCommand({
Bucket: bucketName,
}));
logger.info(`Created MinIO bucket '${bucketName}'`);
} catch (e: any) {
if (e.name !== 'BucketAlreadyOwnedByYou' && e.name !== 'BucketAlreadyExists') {
throw e;
}
logger.warn(`Bucket '${bucketName}' already exists`);
}
const mbResult = await this.execMc(platformService.containerId, [
'mb', '--ignore-existing', `local/${bucketName}`,
]);
logger.info(`Created MinIO bucket '${bucketName}'`);
// Create service account/access key using MinIO Admin API
// MinIO Admin API requires mc client or direct API calls
// For simplicity, we'll use root credentials and bucket policy isolation
// In production, you'd use MinIO's Admin API to create service accounts
// Set bucket policy to allow access only with this bucket's credentials
const bucketPolicy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: ['*'] },
Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket'],
Resource: [
`arn:aws:s3:::${bucketName}`,
`arn:aws:s3:::${bucketName}/*`,
],
},
],
};
try {
await s3Client.send(new PutBucketPolicyCommand({
Bucket: bucketName,
Policy: JSON.stringify(bucketPolicy),
}));
logger.info(`Set bucket policy for '${bucketName}'`);
} catch (e) {
logger.warn(`Could not set bucket policy: ${getErrorMessage(e)}`);
}
// Note: For proper per-service credentials, MinIO Admin API should be used
// For now, we're providing the bucket with root access
// TODO: Implement MinIO service account creation
logger.warn('Using root credentials for MinIO access. Consider implementing service accounts for production.');
// Set bucket policy to allow public read/write (services on the same network use root creds)
await this.execMc(platformService.containerId, [
'anonymous', 'set', 'none', `local/${bucketName}`,
]);
// Use container name for the endpoint in credentials (user services run in same network)
const serviceEndpoint = `http://${containerName}:9000`;
@@ -281,7 +225,7 @@ export class MinioProvider extends BasePlatformServiceProvider {
const credentials: Record<string, string> = {
endpoint: serviceEndpoint,
bucket: bucketName,
accessKey: adminCreds.username, // Using root for now
accessKey: adminCreds.username,
secretKey: adminCreds.password,
region: 'us-east-1',
};
@@ -312,57 +256,37 @@ export class MinioProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000);
if (!hostPort) {
throw new Error('Could not get MinIO container host port');
}
logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`);
const { S3Client, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = await import('npm:@aws-sdk/client-s3@3');
const s3Client = new S3Client({
endpoint: `http://127.0.0.1:${hostPort}`,
region: 'us-east-1',
credentials: {
accessKeyId: adminCreds.username,
secretAccessKey: adminCreds.password,
},
forcePathStyle: true,
});
// Configure mc alias
await this.execMc(platformService.containerId, [
'alias', 'set', 'local', 'http://localhost:9000',
adminCreds.username, adminCreds.password,
]);
try {
// First, delete all objects in the bucket
let continuationToken: string | undefined;
do {
const listResponse = await s3Client.send(new ListObjectsV2Command({
Bucket: resource.resourceName,
ContinuationToken: continuationToken,
}));
if (listResponse.Contents && listResponse.Contents.length > 0) {
await s3Client.send(new DeleteObjectsCommand({
Bucket: resource.resourceName,
Delete: {
Objects: listResponse.Contents.map(obj => ({ Key: obj.Key! })),
},
}));
logger.info(`Deleted ${listResponse.Contents.length} objects from bucket`);
}
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined;
} while (continuationToken);
// Now delete the bucket
await s3Client.send(new DeleteBucketCommand({
Bucket: resource.resourceName,
}));
// Remove all objects and the bucket
await this.execMc(platformService.containerId, [
'rb', '--force', `local/${resource.resourceName}`,
]);
logger.success(`MinIO bucket '${resource.resourceName}' deleted`);
} catch (e) {
logger.error(`Failed to delete MinIO bucket: ${getErrorMessage(e)}`);
throw e;
}
}
/**
* Execute mc (MinIO Client) command inside the container
*/
private async execMc(
containerId: string,
args: string[],
): Promise<{ stdout: string; stderr: string }> {
const result = await this.oneboxRef.docker.execInContainer(containerId, ['mc', ...args]);
if (result.exitCode !== 0) {
throw new Error(`mc command failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`);
}
return result;
}
}

View File

@@ -28,7 +28,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
getDefaultConfig(): IPlatformServiceConfig {
return {
image: 'mongo:7',
image: 'mongo:4.4',
port: 27017,
volumes: ['/var/lib/onebox/mongodb:/data/db'],
environment: {
@@ -165,7 +165,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
// This avoids network issues with overlay networks
const result = await this.oneboxRef.docker.execInContainer(
platformService.containerId,
['mongosh', '--eval', 'db.adminCommand("ping")', '--username', adminCreds.username, '--password', adminCreds.password, '--authenticationDatabase', 'admin', '--quiet']
['mongo', '--eval', 'db.adminCommand("ping")', '--username', adminCreds.username, '--password', adminCreds.password, '--authenticationDatabase', 'admin', '--quiet']
);
if (result.exitCode === 0) {
@@ -190,12 +190,6 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName();
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 27017);
if (!hostPort) {
throw new Error('Could not get MongoDB container host port');
}
// Generate resource names and credentials
const dbName = this.generateResourceName(userService.name);
const username = this.generateResourceName(userService.name);
@@ -203,32 +197,40 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
logger.info(`Provisioning MongoDB database '${dbName}' for service '${userService.name}'...`);
// Connect to MongoDB via localhost and the mapped host port
const { MongoClient } = await import('npm:mongodb@6');
const adminUri = `mongodb://${adminCreds.username}:${adminCreds.password}@127.0.0.1:${hostPort}/?authSource=admin`;
// Use docker exec to provision inside the container (avoids host port mapping issues)
const escapedPassword = password.replace(/'/g, "'\\''");
const escapedAdminPassword = adminCreds.password.replace(/'/g, "'\\''");
const client = new MongoClient(adminUri);
await client.connect();
try {
// Create the database by switching to it (MongoDB creates on first write)
const db = client.db(dbName);
// Create a collection to ensure the database exists
await db.createCollection('_onebox_init');
// Create user with readWrite access to this database
await db.command({
createUser: username,
pwd: password,
roles: [{ role: 'readWrite', db: dbName }],
// Create database and user via mongo inside the container
const mongoScript = `
db = db.getSiblingDB('${dbName}');
db.createCollection('_onebox_init');
db.createUser({
user: '${username}',
pwd: '${escapedPassword}',
roles: [{ role: 'readWrite', db: '${dbName}' }]
});
print('PROVISION_SUCCESS');
`;
logger.success(`MongoDB database '${dbName}' provisioned with user '${username}'`);
} finally {
await client.close();
const result = await this.oneboxRef.docker.execInContainer(
platformService.containerId,
[
'mongo',
'--username', adminCreds.username,
'--password', escapedAdminPassword,
'--authenticationDatabase', 'admin',
'--quiet',
'--eval', mongoScript,
]
);
if (result.exitCode !== 0 || !result.stdout.includes('PROVISION_SUCCESS')) {
throw new Error(`Failed to provision MongoDB database: exit code ${result.exitCode}, output: ${result.stdout.substring(0, 200)} ${result.stderr.substring(0, 200)}`);
}
logger.success(`MongoDB database '${dbName}' provisioned with user '${username}'`);
// Build the credentials and env vars
const credentials: Record<string, string> = {
host: containerName,
@@ -262,37 +264,33 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
}
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 27017);
if (!hostPort) {
throw new Error('Could not get MongoDB container host port');
}
const escapedAdminPassword = adminCreds.password.replace(/'/g, "'\\''");
logger.info(`Deprovisioning MongoDB database '${resource.resourceName}'...`);
const { MongoClient } = await import('npm:mongodb@6');
const adminUri = `mongodb://${adminCreds.username}:${adminCreds.password}@127.0.0.1:${hostPort}/?authSource=admin`;
const mongoScript = `
db = db.getSiblingDB('${resource.resourceName}');
try { db.dropUser('${credentials.username}'); } catch(e) { print('User drop failed: ' + e); }
db.dropDatabase();
print('DEPROVISION_SUCCESS');
`;
const client = new MongoClient(adminUri);
await client.connect();
const result = await this.oneboxRef.docker.execInContainer(
platformService.containerId,
[
'mongo',
'--username', adminCreds.username,
'--password', escapedAdminPassword,
'--authenticationDatabase', 'admin',
'--quiet',
'--eval', mongoScript,
]
);
try {
const db = client.db(resource.resourceName);
// Drop the user
try {
await db.command({ dropUser: credentials.username });
logger.info(`Dropped MongoDB user '${credentials.username}'`);
} catch (e) {
logger.warn(`Could not drop MongoDB user: ${getErrorMessage(e)}`);
}
// Drop the database
await db.dropDatabase();
logger.success(`MongoDB database '${resource.resourceName}' dropped`);
} finally {
await client.close();
if (result.exitCode !== 0) {
logger.warn(`MongoDB deprovision returned exit code ${result.exitCode}: ${result.stderr.substring(0, 200)}`);
}
logger.success(`MongoDB database '${resource.resourceName}' dropped`);
}
}

View File

@@ -15,6 +15,7 @@ export class OneboxServicesManager {
private oneboxRef: any; // Will be Onebox instance
private database: OneboxDatabase;
private docker: OneboxDockerManager;
private autoUpdateIntervalId: number | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
@@ -681,7 +682,7 @@ export class OneboxServicesManager {
*/
startAutoUpdateMonitoring(): void {
// Check every 30 seconds
setInterval(async () => {
this.autoUpdateIntervalId = setInterval(async () => {
try {
await this.checkForRegistryUpdates();
} catch (error) {
@@ -692,6 +693,17 @@ export class OneboxServicesManager {
logger.info('Auto-update monitoring started (30s interval)');
}
/**
* Stop auto-update monitoring
*/
stopAutoUpdateMonitoring(): void {
if (this.autoUpdateIntervalId !== null) {
clearInterval(this.autoUpdateIntervalId);
this.autoUpdateIntervalId = null;
logger.debug('Auto-update monitoring stopped');
}
}
/**
* Check all services using onebox registry for updates
*/

243
ts/classes/systemd.ts Normal file
View File

@@ -0,0 +1,243 @@
/**
* Systemd Service Manager for Onebox
*
* Handles systemd unit file installation, enabling, starting, stopping,
* and status checking. Modeled on nupst's direct systemctl approach —
* no external library dependencies.
*/
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
const SERVICE_NAME = 'onebox';
const SERVICE_FILE_PATH = '/etc/systemd/system/onebox.service';
const SERVICE_UNIT_TEMPLATE = `[Unit]
Description=Onebox - Self-hosted container platform
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/local/bin/onebox systemd start-daemon
Restart=always
RestartSec=10
WorkingDirectory=/var/lib/onebox
Environment=PATH=/usr/bin:/usr/local/bin
Environment=HOME=/root
Environment=DENO_DIR=/root/.cache/deno
[Install]
WantedBy=multi-user.target
`;
export class OneboxSystemd {
/**
* Install and enable the systemd service
*/
async enable(): Promise<void> {
try {
// Ensure Docker is installed before writing unit file (it requires docker.service)
await this.ensureDocker();
// Write the unit file
logger.info('Writing systemd unit file...');
await Deno.writeTextFile(SERVICE_FILE_PATH, SERVICE_UNIT_TEMPLATE);
logger.info(`Unit file written to ${SERVICE_FILE_PATH}`);
// Reload systemd daemon
await this.runSystemctl(['daemon-reload']);
// Enable the service
const result = await this.runSystemctl(['enable', `${SERVICE_NAME}.service`]);
if (!result.success) {
throw new Error(`Failed to enable service: ${result.stderr}`);
}
logger.success('Onebox systemd service enabled');
logger.info('Start with: onebox systemd start');
} catch (error) {
logger.error(`Failed to enable service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Stop, disable, and remove the systemd service
*/
async disable(): Promise<void> {
try {
// Stop the service (ignore errors if not running)
await this.runSystemctl(['stop', `${SERVICE_NAME}.service`]);
// Disable the service
await this.runSystemctl(['disable', `${SERVICE_NAME}.service`]);
// Remove the unit file
try {
await Deno.remove(SERVICE_FILE_PATH);
logger.info(`Removed ${SERVICE_FILE_PATH}`);
} catch {
// File might not exist
}
// Reload systemd daemon
await this.runSystemctl(['daemon-reload']);
logger.success('Onebox systemd service disabled and removed');
} catch (error) {
logger.error(`Failed to disable service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Start the service via systemctl
*/
async start(): Promise<void> {
const result = await this.runSystemctl(['start', `${SERVICE_NAME}.service`]);
if (!result.success) {
logger.error(`Failed to start service: ${result.stderr}`);
throw new Error(`Failed to start onebox service`);
}
logger.success('Onebox service started');
}
/**
* Stop the service via systemctl
*/
async stop(): Promise<void> {
const result = await this.runSystemctl(['stop', `${SERVICE_NAME}.service`]);
if (!result.success) {
logger.error(`Failed to stop service: ${result.stderr}`);
throw new Error(`Failed to stop onebox service`);
}
logger.success('Onebox service stopped');
}
/**
* Get and display service status
*/
async getStatus(): Promise<string> {
const result = await this.runSystemctl(['status', `${SERVICE_NAME}.service`]);
const output = result.stdout;
let status: string;
if (output.includes('active (running)')) {
status = 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
status = 'stopped';
} else if (output.includes('failed')) {
status = 'failed';
} else if (!result.success && result.stderr.includes('could not be found')) {
status = 'not-installed';
} else {
status = 'unknown';
}
// Print the raw systemctl output for full details
if (output.trim()) {
console.log(output);
}
return status;
}
/**
* Show service logs via journalctl
*/
async showLogs(): Promise<void> {
const cmd = new Deno.Command('journalctl', {
args: ['-u', `${SERVICE_NAME}.service`, '-f'],
stdout: 'inherit',
stderr: 'inherit',
});
await cmd.output();
}
/**
* Check if the service unit file is installed
*/
async isInstalled(): Promise<boolean> {
try {
await Deno.stat(SERVICE_FILE_PATH);
return true;
} catch {
return false;
}
}
/**
* Ensure Docker is installed, installing it if necessary
*/
private async ensureDocker(): Promise<void> {
try {
const cmd = new Deno.Command('docker', {
args: ['--version'],
stdout: 'piped',
stderr: 'piped',
});
const result = await cmd.output();
if (result.success) {
const version = new TextDecoder().decode(result.stdout).trim();
logger.info(`Docker found: ${version}`);
return;
}
} catch {
// docker command not found
}
logger.info('Docker not found. Installing Docker...');
const installCmd = new Deno.Command('bash', {
args: ['-c', 'curl -fsSL https://get.docker.com | sh'],
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
});
const installResult = await installCmd.output();
if (!installResult.success) {
throw new Error('Failed to install Docker. Please install it manually: curl -fsSL https://get.docker.com | sh');
}
logger.success('Docker installed successfully');
// Initialize Docker Swarm
logger.info('Initializing Docker Swarm...');
const swarmCmd = new Deno.Command('docker', {
args: ['swarm', 'init'],
stdout: 'piped',
stderr: 'piped',
});
const swarmResult = await swarmCmd.output();
if (swarmResult.success) {
logger.success('Docker Swarm initialized');
} else {
const stderr = new TextDecoder().decode(swarmResult.stderr);
if (stderr.includes('already part of a swarm')) {
logger.info('Docker Swarm already initialized');
} else {
logger.warn(`Docker Swarm init warning: ${stderr.trim()}`);
}
}
}
/**
* Run a systemctl command and return results
*/
private async runSystemctl(
args: string[]
): Promise<{ success: boolean; stdout: string; stderr: string }> {
const cmd = new Deno.Command('systemctl', {
args,
stdout: 'piped',
stderr: 'piped',
});
const result = await cmd.output();
return {
success: result.success,
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
};
}
}

View File

@@ -7,6 +7,7 @@ import { projectInfo } from './info.ts';
import { getErrorMessage } from './utils/error.ts';
import { Onebox } from './classes/onebox.ts';
import { OneboxDaemon } from './classes/daemon.ts';
import { OneboxSystemd } from './classes/systemd.ts';
export async function runCli(): Promise<void> {
const args = Deno.args;
@@ -25,6 +26,19 @@ export async function runCli(): Promise<void> {
const subcommand = args[1];
try {
// === LIGHTWEIGHT COMMANDS (no init()) ===
if (command === 'systemd') {
await handleSystemdCommand(subcommand, args.slice(2));
return;
}
if (command === 'upgrade') {
await handleUpgradeCommand();
return;
}
// === HEAVY COMMANDS (require full init()) ===
// Server command has special handling (doesn't shut down)
if (command === 'server') {
const onebox = new Onebox();
@@ -60,10 +74,6 @@ export async function runCli(): Promise<void> {
await handleNginxCommand(onebox, subcommand, args.slice(2));
break;
case 'daemon':
await handleDaemonCommand(onebox, subcommand, args.slice(2));
break;
case 'config':
await handleConfigCommand(onebox, subcommand, args.slice(2));
break;
@@ -72,10 +82,6 @@ export async function runCli(): Promise<void> {
await handleStatusCommand(onebox);
break;
case 'upgrade':
await handleUpgradeCommand();
break;
default:
logger.error(`Unknown command: ${command}`);
printHelp();
@@ -282,7 +288,7 @@ async function handleServerCommand(onebox: Onebox, args: string[]) {
await OneboxDaemon.ensureNoDaemon();
} catch (error) {
logger.error('Cannot start in ephemeral mode: Daemon is already running');
logger.info('Stop the daemon first: onebox daemon stop');
logger.info('Stop the daemon first: onebox systemd stop');
logger.info('Or run without --ephemeral to use the existing daemon');
Deno.exit(1);
}
@@ -326,39 +332,49 @@ async function handleServerCommand(onebox: Onebox, args: string[]) {
}
}
// Daemon commands
async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) {
// Systemd service commands (lightweight — no Onebox init)
async function handleSystemdCommand(subcommand: string, _args: string[]) {
const systemd = new OneboxSystemd();
switch (subcommand) {
case 'install':
await onebox.daemon.installService();
case 'enable':
await systemd.enable();
break;
case 'disable':
await systemd.disable();
break;
case 'start':
await onebox.startDaemon();
await systemd.start();
break;
case 'stop':
await onebox.stopDaemon();
await systemd.stop();
break;
case 'logs': {
const command = new Deno.Command('journalctl', {
args: ['-u', 'smartdaemon_onebox', '-f'],
stdout: 'inherit',
stderr: 'inherit',
});
await command.output();
case 'status': {
const status = await systemd.getStatus();
logger.info(`Service status: ${status}`);
break;
}
case 'status': {
const status = await onebox.daemon.getServiceStatus();
logger.info(`Daemon status: ${status}`);
case 'logs':
await systemd.showLogs();
break;
case 'start-daemon': {
// This is what systemd's ExecStart calls — full init + daemon loop
const onebox = new Onebox();
await onebox.init();
await onebox.daemon.start();
// start() blocks (keepAlive loop) until SIGTERM/SIGINT
break;
}
default:
logger.error(`Unknown daemon subcommand: ${subcommand}`);
logger.error(`Unknown systemd subcommand: ${subcommand}`);
logger.info('Available: enable, disable, start, stop, status, logs');
}
}
@@ -506,11 +522,12 @@ Commands:
nginx test
nginx status
daemon install
daemon start
daemon stop
daemon logs
daemon status
systemd enable Install and enable systemd service
systemd disable Stop, disable, and remove systemd service
systemd start Start onebox via systemctl
systemd stop Stop onebox via systemctl
systemd status Show systemd service status
systemd logs Follow service logs (journalctl)
config show
config set <key> <value>
@@ -530,15 +547,15 @@ Development Workflow:
onebox service add ... # In another terminal
Production Workflow:
onebox daemon install # Install systemd service
onebox daemon start # Start daemon
onebox service add ... # CLI uses daemon
onebox systemd enable # Install and enable systemd service
onebox systemd start # Start via systemctl
onebox service add ... # CLI manages services
Examples:
onebox server --ephemeral # Start dev server
onebox service add myapp --image nginx:latest --domain app.example.com --port 80
onebox registry add --url registry.example.com --username user --password pass
onebox daemon install
onebox daemon start
onebox systemd enable
onebox systemd start
`);
}

View File

@@ -12,6 +12,7 @@ export { OneboxReverseProxy } from './classes/reverseproxy.ts';
export { OneboxDnsManager } from './classes/dns.ts';
export { OneboxSslManager } from './classes/ssl.ts';
export { OneboxDaemon } from './classes/daemon.ts';
export { OneboxSystemd } from './classes/systemd.ts';
export { OneboxHttpServer } from './classes/httpserver.ts';
export { OneboxApiClient } from './classes/apiclient.ts';

View File

@@ -165,5 +165,42 @@ export class PlatformHandler {
},
),
);
// Get platform service logs
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceLogs>(
'getPlatformServiceLogs',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(dataArg.serviceType);
if (!service || !service.containerId) {
throw new plugins.typedrequest.TypedResponseError('Platform service has no container');
}
const tail = dataArg.tail || 100;
const rawLogs = await this.opsServerRef.oneboxRef.docker.getContainerLogs(service.containerId, tail);
// Parse raw log output into structured entries
const logLines = (rawLogs.stdout + rawLogs.stderr)
.split('\n')
.filter((line: string) => line.trim());
const logs = logLines.map((line: string, index: number) => {
const isError = line.toLowerCase().includes('error') || line.toLowerCase().includes('fatal');
const isWarn = line.toLowerCase().includes('warn');
return {
id: index,
serviceId: 0,
timestamp: Date.now(),
message: line,
level: (isError ? 'error' : isWarn ? 'warn' : 'info') as 'info' | 'warn' | 'error' | 'debug',
source: 'stdout' as const,
};
});
return { logs };
},
),
);
}
}

View File

@@ -17,10 +17,6 @@ export { path, fs, http, encoding };
import { Database } from '@db/sqlite';
export const sqlite = { DB: Database };
// Systemd Daemon Integration
import * as smartdaemon from '@push.rocks/smartdaemon';
export { smartdaemon };
// Docker API Client
import { DockerHost } from '@apiclient.xyz/docker';
export const docker = { Docker: DockerHost };

File diff suppressed because one or more lines are too long

View File

@@ -69,3 +69,18 @@ export interface IReq_GetPlatformServiceStats extends plugins.typedrequestInterf
stats: data.IContainerStats;
};
}
export interface IReq_GetPlatformServiceLogs extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetPlatformServiceLogs
> {
method: 'getPlatformServiceLogs';
request: {
identity: data.IIdentity;
serviceType: data.TPlatformServiceType;
tail?: number;
};
response: {
logs: data.ILogEntry[];
};
}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/onebox',
version: '1.13.11',
version: '1.18.0',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
}

View File

@@ -26,6 +26,8 @@ export interface IServicesState {
currentServiceStats: interfaces.data.IContainerStats | null;
platformServices: interfaces.data.IPlatformService[];
currentPlatformService: interfaces.data.IPlatformService | null;
currentPlatformServiceStats: interfaces.data.IContainerStats | null;
currentPlatformServiceLogs: interfaces.data.ILogEntry[];
}
export interface INetworkState {
@@ -88,6 +90,8 @@ export const servicesStatePart = await appState.getStatePart<IServicesState>(
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
},
'soft',
);
@@ -476,6 +480,46 @@ export const stopPlatformServiceAction = servicesStatePart.createAction<{
}
});
export const fetchPlatformServiceStatsAction = servicesStatePart.createAction<{
serviceType: interfaces.data.TPlatformServiceType;
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetPlatformServiceStats
>('/typedrequest', 'getPlatformServiceStats');
const response = await typedRequest.fire({
identity: context.identity!,
serviceType: dataArg.serviceType,
});
return { ...statePartArg.getState(), currentPlatformServiceStats: response.stats };
} catch (err) {
console.error('Failed to fetch platform service stats:', err);
return { ...statePartArg.getState(), currentPlatformServiceStats: null };
}
});
export const fetchPlatformServiceLogsAction = servicesStatePart.createAction<{
serviceType: interfaces.data.TPlatformServiceType;
tail?: number;
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetPlatformServiceLogs
>('/typedrequest', 'getPlatformServiceLogs');
const response = await typedRequest.fire({
identity: context.identity!,
serviceType: dataArg.serviceType,
tail: dataArg.tail || 100,
});
return { ...statePartArg.getState(), currentPlatformServiceLogs: response.logs };
} catch (err) {
console.error('Failed to fetch platform service logs:', err);
return { ...statePartArg.getState(), currentPlatformServiceLogs: [] };
}
});
// ============================================================================
// Network Actions
// ============================================================================

View File

@@ -37,15 +37,15 @@ export class ObAppShell extends DeesElement {
accessor loginError: string = '';
private viewTabs = [
{ name: 'Dashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
{ name: 'Services', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
{ name: 'Network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
{ name: 'Registries', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
{ name: 'Tokens', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
{ name: 'Settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
{ name: 'Dashboard', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
{ name: 'Services', iconName: 'lucide:boxes', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
{ name: 'Network', iconName: 'lucide:network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
{ name: 'Registries', iconName: 'lucide:package', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
{ name: 'Tokens', iconName: 'lucide:key', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
{ name: 'Settings', iconName: 'lucide:settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
];
private resolvedViewTabs: Array<{ name: string; element: any }> = [];
private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
constructor() {
super();
@@ -104,6 +104,7 @@ export class ObAppShell extends DeesElement {
this.resolvedViewTabs = await Promise.all(
this.viewTabs.map(async (tab) => ({
name: tab.name,
iconName: tab.iconName,
element: await tab.element,
})),
);

View File

@@ -24,6 +24,8 @@ export class ObViewDashboard extends DeesElement {
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
};
@state()
@@ -149,6 +151,7 @@ export class ObViewDashboard extends DeesElement {
],
}}
@action-click=${(e: CustomEvent) => this.handleQuickAction(e)}
@service-click=${(e: CustomEvent) => this.handlePlatformServiceClick(e)}
></sz-dashboard-view>
`;
}
@@ -161,4 +164,21 @@ export class ObViewDashboard extends DeesElement {
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'network' });
}
}
private handlePlatformServiceClick(e: CustomEvent) {
// Find the platform service type from the click event
const name = e.detail?.name;
const ps = this.servicesState.platformServices.find(
(p) => p.displayName === name,
);
if (ps) {
// Navigate to services tab — the ObViewServices component will pick up the type
// Store the selected platform type so the services view can open it
appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformService: ps,
});
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'services' });
}
}
}

View File

@@ -107,6 +107,8 @@ export class ObViewServices extends DeesElement {
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
};
@state()
@@ -145,7 +147,37 @@ export class ObViewServices extends DeesElement {
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css``,
css`
.page-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
.deploy-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 200ms ease;
}
.deploy-button:hover {
opacity: 0.9;
}
.deploy-button svg {
width: 16px;
height: 16px;
}
`,
];
async connectedCallback() {
@@ -154,6 +186,18 @@ export class ObViewServices extends DeesElement {
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
]);
// If a platform service was selected from the dashboard, navigate to its detail
const state = appstate.servicesStatePart.getState();
if (state.currentPlatformService) {
const type = state.currentPlatformService.type;
// Clear the selection so it doesn't persist on next visit
appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformService: null,
});
this.navigateToPlatformDetail(type);
}
}
public render(): TemplateResult {
@@ -178,8 +222,23 @@ export class ObViewServices extends DeesElement {
domain: s.domain || null,
status: mapStatus(s.status),
}));
const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({
name: ps.displayName,
status: ps.status === 'running' ? `Running` : ps.status,
running: ps.status === 'running',
type: ps.type,
}));
return html`
<ob-sectionheading>Services</ob-sectionheading>
<div class="page-actions">
<button class="deploy-button" @click=${() => { this.currentView = 'create'; }}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Deploy Service
</button>
</div>
<sz-services-list-view
.services=${mappedServices}
@service-click=${(e: CustomEvent) => {
@@ -197,6 +256,20 @@ export class ObViewServices extends DeesElement {
}}
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
></sz-services-list-view>
<ob-sectionheading style="margin-top: 32px;">Platform Services</ob-sectionheading>
<div style="max-width: 500px;">
<sz-platform-services-card
.services=${mappedPlatformServices}
@service-click=${(e: CustomEvent) => {
const type = e.detail.type || this.servicesState.platformServices.find(
(ps) => ps.displayName === e.detail.name,
)?.type;
if (type) {
this.navigateToPlatformDetail(type);
}
}}
></sz-platform-services-card>
</div>
`;
}
@@ -206,8 +279,26 @@ export class ObViewServices extends DeesElement {
<sz-service-create-view
.registries=${[]}
@create-service=${async (e: CustomEvent) => {
const formConfig = e.detail;
const serviceConfig: interfaces.data.IServiceCreate = {
name: formConfig.name,
image: formConfig.image,
port: formConfig.ports?.[0]?.containerPort
? parseInt(formConfig.ports[0].containerPort, 10)
: 80,
envVars: formConfig.envVars?.reduce(
(acc: Record<string, string>, ev: { key: string; value: string }) => {
if (ev.key) acc[ev.key] = ev.value;
return acc;
},
{} as Record<string, string>,
),
enableMongoDB: formConfig.enableMongoDB || false,
enableS3: formConfig.enableS3 || false,
enableClickHouse: formConfig.enableClickHouse || false,
};
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
config: e.detail,
config: serviceConfig,
});
this.currentView = 'list';
}}
@@ -265,10 +356,29 @@ export class ObViewServices extends DeesElement {
`;
}
private navigateToPlatformDetail(type: string): void {
this.selectedPlatformType = type;
// Fetch stats and logs for this platform service
const serviceType = type as interfaces.data.TPlatformServiceType;
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, { serviceType });
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceLogsAction, { serviceType });
this.currentView = 'platform-detail';
}
private renderPlatformDetailView(): TemplateResult {
const platformService = this.servicesState.platformServices.find(
(ps) => ps.type === this.selectedPlatformType,
);
const stats = this.servicesState.currentPlatformServiceStats;
const metrics = stats
? {
cpu: Math.round(stats.cpuPercent),
memory: Math.round(stats.memoryPercent),
storage: 0,
connections: 0,
}
: undefined;
return html`
<ob-sectionheading>Platform Service</ob-sectionheading>
<sz-platform-service-detail-view
@@ -277,22 +387,49 @@ export class ObViewServices extends DeesElement {
id: platformService.type,
name: platformService.displayName,
type: platformService.type,
status: platformService.status,
status: platformService.status === 'running'
? 'running'
: platformService.status === 'failed'
? 'error'
: 'stopped',
version: '',
host: 'localhost',
port: 0,
config: {},
metrics,
}
: null}
.logs=${[]}
@start=${() => {
appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
serviceType: this.selectedPlatformType as any,
.logs=${this.servicesState.currentPlatformServiceLogs.map((log) => ({
timestamp: new Date(log.timestamp).toLocaleString(),
level: log.level,
message: log.message,
}))}
@back=${() => {
this.currentView = 'list';
}}
@start=${async () => {
await appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
// Refresh stats after starting
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
}}
@stop=${() => {
appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
serviceType: this.selectedPlatformType as any,
@stop=${async () => {
await appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
}}
@restart=${async () => {
await appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
await appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
}}
></sz-platform-service-detail-view>