Compare commits

...

50 Commits

Author SHA1 Message Date
c0f9f979c7 v1.22.2
All checks were successful
Release / build-and-release (push) Successful in 2m59s
2026-03-18 09:48:12 +00:00
6e5743c837 fix(web-ui): stabilize app store service creation flow and add Ghost sqlite defaults 2026-03-18 09:48:12 +00:00
829f7e47f1 v1.22.1
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m2s
CI / Build All Platforms (push) Successful in 2m19s
Release / build-and-release (push) Successful in 3m45s
2026-03-18 02:44:14 +00:00
a36af5c3d5 fix(repo): no changes to commit 2026-03-18 02:44:14 +00:00
3c99ee5f83 v1.22.0
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 1m1s
CI / Build All Platforms (push) Successful in 2m11s
Release / build-and-release (push) Successful in 3m56s
2026-03-18 02:37:39 +00:00
2faa416895 feat(web-appstore): add an App Store view for quick service deployment from curated templates 2026-03-18 02:37:39 +00:00
acbf448c6f v1.21.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 27s
CI / Build Test (Current Platform) (push) Successful in 1m2s
CI / Build All Platforms (push) Successful in 2m13s
Release / build-and-release (push) Successful in 3m34s
2026-03-18 02:22:45 +00:00
5c48ae4156 feat(opsserver): add container workspace API and backend execution environment for services 2026-03-18 02:22:45 +00:00
3108408133 v1.20.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 7s
CI / Type Check & Lint (push) Failing after 39s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 2m52s
Release / build-and-release (push) Successful in 6m2s
2026-03-17 23:39:25 +00:00
6defdb4431 feat(ops-dashboard): stream user service logs to the ops dashboard and resolve service containers for Docker log streaming 2026-03-17 23:39:24 +00:00
f63be883ce v1.19.12
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Build All Platforms (push) Failing after 13m15s
CI / Build Test (Current Platform) (push) Failing after 13m17s
CI / Type Check & Lint (push) Failing after 13m19s
Release / build-and-release (push) Successful in 6m6s
2026-03-17 12:52:34 +00:00
87844bbb8e fix(repo): no changes to commit 2026-03-17 12:52:34 +00:00
02b7cda2be v1.19.11
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 1m0s
CI / Build All Platforms (push) Successful in 1m57s
Release / build-and-release (push) Successful in 5m51s
2026-03-17 10:40:40 +00:00
3b8f95e8e1 fix(repo): no changes to commit 2026-03-17 10:40:40 +00:00
ee774e3f41 v1.19.10
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 38s
CI / Build Test (Current Platform) (push) Successful in 1m6s
CI / Build All Platforms (push) Successful in 2m17s
Release / build-and-release (push) Successful in 7m46s
2026-03-17 02:09:50 +00:00
6d93dfa459 fix(repo): no changes to commit 2026-03-17 02:09:50 +00:00
ac394cfafc v1.19.9
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m2s
CI / Build All Platforms (push) Successful in 2m0s
Release / build-and-release (push) Successful in 2m51s
2026-03-17 01:53:38 +00:00
97e9f232fa fix(repo): no changes to commit 2026-03-17 01:53:38 +00:00
3dcb6a38e5 v1.19.8
Some checks failed
CI / Build All Platforms (push) Failing after 9s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 28s
CI / Build Test (Current Platform) (push) Successful in 59s
Release / build-and-release (push) Failing after 1m24s
2026-03-17 01:37:18 +00:00
ca33970e9a fix(repo): no changes to commit 2026-03-17 01:37:18 +00:00
cd34b98a25 v1.19.7
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 33s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 1m52s
Release / build-and-release (push) Successful in 3m50s
2026-03-17 01:03:14 +00:00
a089e5bedb fix(repo): no changes to commit 2026-03-17 01:03:14 +00:00
9786ff62f0 v1.19.6
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 31s
CI / Build Test (Current Platform) (push) Successful in 1m6s
CI / Build All Platforms (push) Successful in 2m15s
Release / build-and-release (push) Failing after 3m30s
2026-03-17 00:45:56 +00:00
4a5abc4a0a fix(repository): no changes to commit 2026-03-17 00:45:55 +00:00
893a532758 v1.19.5
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
Release / build-and-release (push) Failing after 3m34s
2026-03-17 00:45:37 +00:00
7ea286c0a9 fix(repo): no changes to commit 2026-03-17 00:45:37 +00:00
f94f47e313 v1.19.4
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 2m8s
Release / build-and-release (push) Successful in 3m38s
2026-03-17 00:01:08 +00:00
b1a46f8757 fix(repository): no changes to commit 2026-03-17 00:01:08 +00:00
56c71226e5 v1.19.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 36s
CI / Build Test (Current Platform) (push) Successful in 1m3s
Release / build-and-release (push) Successful in 2m48s
2026-03-16 20:06:11 +00:00
f53109a01e fix(repo): no changes to commit 2026-03-16 20:06:11 +00:00
bcb2473cc5 v1.19.2
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 34s
CI / Build Test (Current Platform) (push) Successful in 1m10s
CI / Build All Platforms (push) Successful in 2m34s
Release / build-and-release (push) Failing after 4m2s
2026-03-16 20:00:10 +00:00
689dcf295b fix(docs): remove outdated UI screenshot assets from project documentation 2026-03-16 20:00:10 +00:00
c1e14e9fc7 v1.19.1
Some checks failed
CI / Type Check & Lint (push) Failing after 30s
CI / Build Test (Current Platform) (push) Successful in 1m3s
CI / Build All Platforms (push) Successful in 2m5s
2026-03-16 19:55:27 +00:00
d5fd57e2c3 fix(dashboard): add updated dashboard screenshots for refresh and resource usage states 2026-03-16 19:55:27 +00:00
079e6a64a9 fix(dashboard): add aggregated resource usage stats to the dashboard
Some checks failed
Publish to npm / npm-publish (push) Failing after 4s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m1s
Release / build-and-release (push) Failing after 1m43s
CI / Build All Platforms (push) Successful in 2m8s
- Aggregate CPU, memory, and network stats across all running containers
- Extend ISystemStatus.docker with resource usage fields
- Fix getContainerStats Swarm service ID fallback
- Wire dashboard resource usage card to real backend data
2026-03-16 16:47:05 +00:00
a04cf053db v1.19.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 11s
CI / Type Check & Lint (push) Failing after 30s
CI / Build Test (Current Platform) (push) Successful in 58s
CI / Build All Platforms (push) Successful in 2m21s
Release / build-and-release (push) Successful in 4m11s
2026-03-16 16:19:39 +00:00
ec0e377ccb feat(opsserver,web): add real-time platform service log streaming to the dashboard 2026-03-16 16:19:39 +00:00
3b3d0433cb fix(platform-services): fix detail view navigation and log display
Some checks failed
CI / Build All Platforms (push) Failing after 4s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m6s
Release / build-and-release (push) Successful in 3m21s
- Add back button for returning to services list
- Fix DOM lifecycle when switching between platform services
- Fix timestamp format for dees-chart-log compatibility
- Clear previous stats/logs state before fetching new data
2026-03-16 14:48:46 +00:00
5f876449ca v1.18.4
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 54s
Release / build-and-release (push) Successful in 2m19s
2026-03-16 14:35:45 +00:00
8e781c7f9d fix(repo): no changes to commit 2026-03-16 14:35:45 +00:00
a3eefbe92c v1.18.3
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m1s
CI / Build All Platforms (push) Successful in 2m3s
Release / build-and-release (push) Successful in 3m2s
2026-03-16 14:22:37 +00:00
41679427c6 fix(deps): bump @serve.zone/catalog to ^2.6.1 2026-03-16 14:22:37 +00:00
c420a30341 v1.18.2
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 59s
CI / Build All Platforms (push) Successful in 2m0s
Release / build-and-release (push) Successful in 3m41s
2026-03-16 14:14:55 +00:00
fe109f0953 fix(repo): no changes to commit 2026-03-16 14:14:55 +00:00
012dce63b1 v1.18.1
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
Release / build-and-release (push) Successful in 4m0s
2026-03-16 14:14:34 +00:00
54780482c7 fix(repo): no changes to commit 2026-03-16 14:14:34 +00:00
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
28 changed files with 1598 additions and 412 deletions

View File

@@ -1,114 +0,0 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
check:
name: Type Check & Lint
runs-on: ubuntu-latest
container:
image: code.foss.global/host.today/ht-docker-node:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Install dependencies
run: deno install --entrypoint mod.ts
- name: Check TypeScript types
run: deno check mod.ts
- name: Lint code
run: deno lint
continue-on-error: true
- name: Format check
run: deno fmt --check
continue-on-error: true
build:
name: Build Test (Current Platform)
runs-on: ubuntu-latest
container:
image: code.foss.global/host.today/ht-docker-node:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Compile for current platform
run: |
echo "Testing compilation for Linux x86_64..."
npx tsdeno compile --allow-all --no-check \
--output onebox-test \
--target x86_64-unknown-linux-gnu mod.ts
- name: Test binary execution
run: |
chmod +x onebox-test
./onebox-test --version
./onebox-test --help
build-all:
name: Build All Platforms
runs-on: ubuntu-latest
container:
image: code.foss.global/host.today/ht-docker-node:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Compile all platform binaries
run: mkdir -p dist/binaries && npx tsdeno compile
- name: Upload all binaries as artifact
uses: actions/upload-artifact@v3
with:
name: onebox-binaries.zip
path: dist/binaries/*
retention-days: 30

View File

@@ -1,131 +0,0 @@
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
npm-publish:
runs-on: ubuntu-latest
container:
image: code.foss.global/host.today/ht-docker-node:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org/'
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
echo "Publishing version: $VERSION"
- name: Verify deno.json version matches tag
run: |
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
TAG_VERSION="${{ steps.version.outputs.version_number }}"
echo "deno.json version: $DENO_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Version mismatch!"
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
exit 1
fi
- name: Compile binaries for npm package
run: |
echo "Compiling binaries for npm package..."
deno task compile
echo ""
echo "Binary sizes:"
ls -lh dist/binaries/
- name: Generate SHA256 checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS
cat SHA256SUMS
cd ../..
- name: Sync package.json version
run: |
VERSION="${{ steps.version.outputs.version_number }}"
echo "Syncing package.json to version ${VERSION}..."
npm version ${VERSION} --no-git-tag-version --allow-same-version
echo "package.json version: $(grep '"version"' package.json | head -1)"
- name: Create npm package
run: |
echo "Creating npm package..."
npm pack
echo ""
echo "Package created:"
ls -lh *.tgz
- name: Test local installation
run: |
echo "Testing local package installation..."
PACKAGE_FILE=$(ls *.tgz)
npm install -g ${PACKAGE_FILE}
echo ""
echo "Testing onebox command:"
onebox --version || echo "Note: Binary execution may fail in CI environment"
echo ""
echo "Checking installed files:"
npm ls -g @serve.zone/onebox || true
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "Publishing to npm registry..."
npm publish --access public
echo ""
echo "Successfully published @serve.zone/onebox to npm!"
echo ""
echo "Package info:"
npm view @serve.zone/onebox
- name: Verify npm package
run: |
echo "Waiting for npm propagation..."
sleep 30
echo ""
echo "Verifying published package..."
npm view @serve.zone/onebox
echo ""
echo "Testing installation from npm:"
npm install -g @serve.zone/onebox
echo ""
echo "Package installed successfully!"
which onebox || echo "Binary location check skipped"
- name: Publish Summary
run: |
echo "================================================"
echo " npm Publish Complete!"
echo "================================================"
echo ""
echo "Package: @serve.zone/onebox"
echo "Version: ${{ steps.version.outputs.version }}"
echo ""
echo "Installation:"
echo " npm install -g @serve.zone/onebox"
echo ""
echo "Registry:"
echo " https://www.npmjs.com/package/@serve.zone/onebox"
echo ""

View File

@@ -1,5 +1,147 @@
# Changelog # Changelog
## 2026-03-18 - 1.22.2 - fix(web-ui)
stabilize app store service creation flow and add Ghost sqlite defaults
- Defers App Store navigation to the services view to avoid destroying the current view during the deploy event handler.
- Processes pending app templates after services view updates so the create flow opens reliably.
- Adds default Ghost environment variables for sqlite3 and the database file path in the App Store template.
- Removes obsolete Gitea CI and npm publish workflow definitions.
## 2026-03-18 - 1.22.1 - fix(repo)
no changes to commit
## 2026-03-18 - 1.22.0 - feat(web-appstore)
add an App Store view for quick service deployment from curated templates
- adds a new App Store tab to the web UI with curated Docker app templates
- passes selected app templates through UI state into the services view for quick deployment
- supports quick deploy creation with prefilled image, port, environment variables, and optional platform service flags
- updates @serve.zone/catalog to ^2.8.0 to support the new app store view
## 2026-03-18 - 1.21.0 - feat(opsserver)
add container workspace API and backend execution environment for services
- introduces typed workspace handlers for reading, writing, listing, creating, removing, and executing commands inside service containers
- adds frontend backend-execution environment integration so the service view can open a workspace against a selected service
- extends Docker exec lookup to resolve Swarm service container IDs when a direct container ID is unavailable
## 2026-03-17 - 1.20.0 - feat(ops-dashboard)
stream user service logs to the ops dashboard and resolve service containers for Docker log streaming
- add typed socket support for pushing live user service log entries to the web app
- extend platform log streaming to include running user services with separate dashboard handlers
- fall back from direct container lookup to service-to-container resolution when streaming Docker logs
- update log parsing to preserve timestamps and infer log levels for service log entries
- bump @serve.zone/catalog to ^2.7.0
## 2026-03-17 - 1.19.12 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.11 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.10 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.9 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.8 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.7 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.6 - fix(repository)
no changes to commit
## 2026-03-17 - 1.19.5 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.4 - fix(repository)
no changes to commit
## 2026-03-16 - 1.19.3 - fix(repo)
no changes to commit
## 2026-03-16 - 1.19.2 - fix(docs)
remove outdated UI screenshot assets from project documentation
- Deletes multiple PNG screenshots that documented previous dashboard, service form, and hello-world states.
- Reduces repository clutter by removing obsolete image assets no longer needed in docs.
## 2026-03-16 - 1.19.1 - fix(dashboard)
add updated dashboard screenshots for refresh and resource usage states
- Adds new dashboard screenshots covering post-refresh, resource usage, and populated data views.
- Updates visual assets to document current dashboard behavior and UI states.
## 2026-03-16 - 1.19.1 - fix(dashboard)
add aggregated resource usage stats to the dashboard
- Aggregate CPU, memory, and network stats across all running user and platform service containers in getSystemStatus
- Extend ISystemStatus.docker interface with cpuUsage, memoryUsage, memoryTotal, networkIn, networkOut fields
- Fix getContainerStats to properly handle Swarm service IDs by catching exceptions and falling back to label-based container lookup
- Wire dashboard resource usage card to display real aggregated data from the backend
## 2026-03-16 - 1.19.0 - feat(opsserver,web)
add real-time platform service log streaming to the dashboard
- stream running platform service container logs from the ops server to connected dashboard clients via TypedSocket
- parse Docker log timestamps and levels for both pushed and fetched platform service log entries
- enhance the platform service detail view with mapped statuses and predefined host, port, version, and config metadata
- add the typedsocket dependency and update the catalog package for dashboard support
## 2026-03-16 - 1.18.5 - fix(platform-services)
fix platform service detail view navigation and log display
- Add back button to platform service detail view for returning to services list
- Fix DOM lifecycle when switching between platform services (destroy and recreate dees-chart-log)
- Fix timestamp format for log entries to use ISO 8601 for dees-chart-log compatibility
- Clear previous stats/logs state before fetching new platform service data
## 2026-03-16 - 1.18.4 - fix(repo)
no changes to commit
## 2026-03-16 - 1.18.3 - fix(deps)
bump @serve.zone/catalog to ^2.6.1
- Updates the @serve.zone/catalog runtime dependency from ^2.6.0 to ^2.6.1.
## 2026-03-16 - 1.18.2 - fix(repo)
no changes to commit
## 2026-03-16 - 1.18.1 - fix(repo)
no changes to commit
## 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) ## 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 downgrade the MongoDB service image to 4.4 and use the legacy mongo shell for container operations

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.17.3", "version": "1.22.2",
"exports": "./mod.ts", "exports": "./mod.ts",
"tasks": { "tasks": {
"test": "deno test --allow-all test/", "test": "deno test --allow-all test/",
@@ -25,7 +25,8 @@
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6", "@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1", "@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0", "@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1" "@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
"@api.global/typedsocket": "npm:@api.global/typedsocket@^4.1.2"
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.17.3", "version": "1.22.2",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers", "description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts", "main": "mod.ts",
"type": "module", "type": "module",
@@ -55,9 +55,10 @@
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34", "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"dependencies": { "dependencies": {
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^4.1.2",
"@design.estate/dees-catalog": "^3.43.3", "@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@serve.zone/catalog": "^2.6.0" "@serve.zone/catalog": "^2.8.0"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbundle": "^2.9.0", "@git.zone/tsbundle": "^2.9.0",

229
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@api.global/typedrequest-interfaces': '@api.global/typedrequest-interfaces':
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
'@api.global/typedsocket':
specifier: ^4.1.2
version: 4.1.2(@push.rocks/smartserve@2.0.1)
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^3.43.3 specifier: ^3.43.3
version: 3.48.5(@tiptap/pm@2.27.2) version: 3.48.5(@tiptap/pm@2.27.2)
@@ -18,8 +21,8 @@ importers:
specifier: ^2.1.6 specifier: ^2.1.6
version: 2.2.3 version: 2.2.3
'@serve.zone/catalog': '@serve.zone/catalog':
specifier: ^2.6.0 specifier: ^2.8.0
version: 2.6.0(@tiptap/pm@2.27.2) version: 2.8.0(@tiptap/pm@2.27.2)
devDependencies: devDependencies:
'@git.zone/tsbundle': '@git.zone/tsbundle':
specifier: ^2.9.0 specifier: ^2.9.0
@@ -60,8 +63,8 @@ packages:
'@cfworker/json-schema@4.1.1': '@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@cloudflare/workers-types@4.20260313.1': '@cloudflare/workers-types@4.20260317.1':
resolution: {integrity: sha512-jMEeX3RKfOSVqqXRKr/ulgglcTloeMzSH3FdzIfqJHtvc12/ELKd5Ldsg8ZHahKX/4eRxYdw3kbzb8jLXbq/jQ==} resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==}
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
@@ -373,74 +376,74 @@ packages:
'@module-federation/webpack-bundler-runtime@0.22.0': '@module-federation/webpack-bundler-runtime@0.22.0':
resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==}
'@napi-rs/canvas-android-arm64@0.1.96': '@napi-rs/canvas-android-arm64@0.1.97':
resolution: {integrity: sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg==} resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.96': '@napi-rs/canvas-darwin-arm64@0.1.97':
resolution: {integrity: sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg==} resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.96': '@napi-rs/canvas-darwin-x64@0.1.97':
resolution: {integrity: sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA==} resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.96': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
resolution: {integrity: sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ==} resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.96': '@napi-rs/canvas-linux-arm64-gnu@0.1.97':
resolution: {integrity: sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw==} resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.96': '@napi-rs/canvas-linux-arm64-musl@0.1.97':
resolution: {integrity: sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==} resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.96': '@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
resolution: {integrity: sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==} resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.96': '@napi-rs/canvas-linux-x64-gnu@0.1.97':
resolution: {integrity: sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==} resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.96': '@napi-rs/canvas-linux-x64-musl@0.1.97':
resolution: {integrity: sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==} resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-win32-arm64-msvc@0.1.96': '@napi-rs/canvas-win32-arm64-msvc@0.1.97':
resolution: {integrity: sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==} resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.96': '@napi-rs/canvas-win32-x64-msvc@0.1.97':
resolution: {integrity: sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw==} resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@napi-rs/canvas@0.1.96': '@napi-rs/canvas@0.1.97':
resolution: {integrity: sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew==} resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.0.7': '@napi-rs/wasm-runtime@1.0.7':
@@ -769,60 +772,60 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.52': '@rolldown/pluginutils@1.0.0-beta.52':
resolution: {integrity: sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==} resolution: {integrity: sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==}
'@rspack/binding-darwin-arm64@1.7.8': '@rspack/binding-darwin-arm64@1.7.9':
resolution: {integrity: sha512-KS6SRc+4VYRdX1cKr1j1HEuMNyEzt7onBS0rkenaiCRRYF0z4WNZNyZqRiuxgM3qZ3TISF7gdmgJQyd4ZB43ig==} resolution: {integrity: sha512-64dgstte0If5czi9bA/cpOe0ryY6wC9AIQRtyJ3DlOF6Tt+y9cKkmUoGu3V+WYaYIZRT7HNk8V7kL8amVjFTYw==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@rspack/binding-darwin-x64@1.7.8': '@rspack/binding-darwin-x64@1.7.9':
resolution: {integrity: sha512-uyXSDKLg2CtqIJrsJDlCqQH80YIPsCUiTToJ59cXAG3v4eke0Qbiv6d/+pV0h/mc0u4inAaSkr5dD18zkMIghw==} resolution: {integrity: sha512-2QSLs3w4rLy4UUGVnIlkt6IlIKOzR1e0RPsq2FYQW6s3p9JrwRCtOeHohyh7EJSqF54dtfhe9UZSAwba3LqH1Q==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@rspack/binding-linux-arm64-gnu@1.7.8': '@rspack/binding-linux-arm64-gnu@1.7.9':
resolution: {integrity: sha512-dD6gSHA18Uj0eqc1FCwwQ5IO5mIckrpYN4H4kPk9Pjau+1mxWvC4y5Lryz1Z8P/Rh1lnQ/wwGE0XL9nd80+LqQ==} resolution: {integrity: sha512-qhUGI/uVfvLmKWts4QkVHGL8yfUyJkblZs+OFD5Upa2y676EOsbQgWsCwX4xGB6Tv+TOzFP0SLh/UfO8ZfdE+w==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@rspack/binding-linux-arm64-musl@1.7.8': '@rspack/binding-linux-arm64-musl@1.7.9':
resolution: {integrity: sha512-m+uBi9mEVGkZ02PPOAYN2BSmmvc00XGa6v9CjV8qLpolpUXQIMzDNG+i1fD5SHp8LO+XWsZJOHypMsT0MzGTGw==} resolution: {integrity: sha512-VjfmR1hgO9n3L6MaE5KG+DXSrrLVqHHOkVcOtS2LMq3bjMTwbBywY7ycymcLnX5KJsol8d3ZGYep6IfSOt3lFA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@rspack/binding-linux-x64-gnu@1.7.8': '@rspack/binding-linux-x64-gnu@1.7.9':
resolution: {integrity: sha512-IAPp2L3yS33MAEkcGn/I1gO+a+WExJHXz2ZlRlL2oFCUGpYi2ZQHyAcJ3o2tJqkXmdqsTiN+OjEVMd/RcLa24g==} resolution: {integrity: sha512-0kldV+3WTs/VYDWzxJ7K40hCW26IHtnk8xPK3whKoo1649rgeXXa0EdsU5P7hG8Ef5SWQjHHHZ/fuHYSO3Y6HA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@rspack/binding-linux-x64-musl@1.7.8': '@rspack/binding-linux-x64-musl@1.7.9':
resolution: {integrity: sha512-do/QNzb4GWdXCsipblDcroqRDR3BFcbyzpZpAw/3j9ajvEqsOKpdHZpILT2NZX/VahhjqfqB3k0kJVt3uK7UYQ==} resolution: {integrity: sha512-Gi4872cFtc2d83FKATR6Qcf2VBa/tFCqffI/IwRRl6Hx5FulEBqx+tH7gAuRVF693vrbXNxK+FQ+k4iEsEJxrw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@rspack/binding-wasm32-wasi@1.7.8': '@rspack/binding-wasm32-wasi@1.7.9':
resolution: {integrity: sha512-mHtgYTpdhx01i0XNKFYBZyCjtv9YUe/sDfpD1QK4FytPFB+1VpYnmZiaJIMM77VpNsjxGAqWhmUYxi2P6jWifw==} resolution: {integrity: sha512-5QEzqo6EaolpuZmK6w/mgSueorgGnnzp7dJaAvBj6ECFIg/aLXhXXmWCWbxt7Ws2gKvG5/PgaxDqbUxYL51juA==}
cpu: [wasm32] cpu: [wasm32]
'@rspack/binding-win32-arm64-msvc@1.7.8': '@rspack/binding-win32-arm64-msvc@1.7.9':
resolution: {integrity: sha512-Mkxg86F7kIT4pM9XvE/1LAGjK5NOQi/GJxKyyiKbUAeKM8XBUizVeNuvKR0avf2V5IDAIRXiH1SX8SpujMJteA==} resolution: {integrity: sha512-MMqvcrIc8aOqTuHjWkjdzilvoZ3Hv07Od0Foogiyq3JMudsS3Wcmh7T1dFerGg19MOJcRUeEkrg2NQOMOQ6xDA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@rspack/binding-win32-ia32-msvc@1.7.8': '@rspack/binding-win32-ia32-msvc@1.7.9':
resolution: {integrity: sha512-VmTOZ/X7M85lKFNwb2qJpCRzr4SgO42vucq/X7Uz1oSoTPAf8UUMNdi7BPnu+D4lgy6l8PwV804ZyHO3gGsvPA==} resolution: {integrity: sha512-4kYYS+NZ2CuNbKjq40yB/UEyB51o1PHj5wpr+Y943oOJXpEKWU2Q4vkF8VEohPEcnA9cKVotYCnqStme+02suA==}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@rspack/binding-win32-x64-msvc@1.7.8': '@rspack/binding-win32-x64-msvc@1.7.9':
resolution: {integrity: sha512-BK0I4HAwp/yQLnmdJpUtGHcht3x11e9fZwyaiMzznznFc+Oypbf+FS5h+aBgpb53QnNkPpdG7MfAPoKItOcU8A==} resolution: {integrity: sha512-1g+QyXXvs+838Un/4GaUvJfARDGHMCs15eXDYWBl5m/Skubyng8djWAgr6ag1+cVoJZXCPOvybTItcblWF3gbQ==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@rspack/binding@1.7.8': '@rspack/binding@1.7.9':
resolution: {integrity: sha512-P4fbrQx5hRhAiC8TBTEMCTnNawrIzJLjWwAgrTwRxjgenpjNvimEkQBtSGrXOY+c+MV5Q74P+9wPvVWLKzRkQQ==} resolution: {integrity: sha512-A56e0NdfNwbOSJoilMkxzaPuVYaKCNn1shuiwWnCIBmhV9ix1n9S1XvquDjkGyv+gCdR1+zfJBOa5DMB7htLHw==}
'@rspack/core@1.7.8': '@rspack/core@1.7.9':
resolution: {integrity: sha512-kT6yYo8xjKoDfM7iB8N9AmN9DJIlrs7UmQDbpTu1N4zaZocN1/t2fIAWOKjr5+3eJlZQR2twKZhDVHNLbLPjOw==} resolution: {integrity: sha512-VHuSKvRkuv42Ya+TxEGO0LE0r9+8P4tKGokmomj4R1f/Nu2vtS3yoaIMfC4fR6VuHGd3MZ+KTI0cNNwHfFcskw==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
peerDependencies: peerDependencies:
'@swc/helpers': '>=0.5.1' '@swc/helpers': '>=0.5.1'
@@ -836,8 +839,8 @@ packages:
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@serve.zone/catalog@2.6.0': '@serve.zone/catalog@2.8.0':
resolution: {integrity: sha512-Gp91Ed0MMLMPSAZrH2/UimGxU9AaTu5IPUobPD49PSvh3UnUl+HEFiy81kpKJhW16V37N3/vcNgZrn2VI7/LxQ==} resolution: {integrity: sha512-p0ES14JwUoJE88DBtLSHcCfFPVa0vKhvHnQLaAY3OC15kfheNKidi1SwTFyMh43jj0ZNi4Lecc3W02wG6sasHw==}
'@tempfix/idb@8.0.3': '@tempfix/idb@8.0.3':
resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==} resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==}
@@ -1362,15 +1365,15 @@ packages:
fast-json-stable-stringify@2.1.0: fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-xml-builder@1.1.3: fast-xml-builder@1.1.4:
resolution: {integrity: sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==} resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
fast-xml-parser@4.5.4: fast-xml-parser@4.5.4:
resolution: {integrity: sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==} resolution: {integrity: sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==}
hasBin: true hasBin: true
fast-xml-parser@5.5.5: fast-xml-parser@5.5.6:
resolution: {integrity: sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg==} resolution: {integrity: sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==}
hasBin: true hasBin: true
fault@2.0.1: fault@2.0.1:
@@ -2337,7 +2340,7 @@ snapshots:
'@api.global/typedrequest': 3.3.0 '@api.global/typedrequest': 3.3.0
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260313.1 '@cloudflare/workers-types': 4.20260317.1
'@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.3.1 '@push.rocks/lik': 6.3.1
@@ -2397,7 +2400,7 @@ snapshots:
'@cfworker/json-schema@4.1.1': {} '@cfworker/json-schema@4.1.1': {}
'@cloudflare/workers-types@4.20260313.1': {} '@cloudflare/workers-types@4.20260317.1': {}
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
dependencies: dependencies:
@@ -2620,7 +2623,7 @@ snapshots:
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartspawn': 3.0.3 '@push.rocks/smartspawn': 3.0.3
'@rspack/core': 1.7.8 '@rspack/core': 1.7.9
'@types/html-minifier': 4.0.6 '@types/html-minifier': 4.0.6
esbuild: 0.27.4 esbuild: 0.27.4
html-minifier: 4.0.0 html-minifier: 4.0.0
@@ -2814,52 +2817,52 @@ snapshots:
'@module-federation/runtime': 0.22.0 '@module-federation/runtime': 0.22.0
'@module-federation/sdk': 0.22.0 '@module-federation/sdk': 0.22.0
'@napi-rs/canvas-android-arm64@0.1.96': '@napi-rs/canvas-android-arm64@0.1.97':
optional: true optional: true
'@napi-rs/canvas-darwin-arm64@0.1.96': '@napi-rs/canvas-darwin-arm64@0.1.97':
optional: true optional: true
'@napi-rs/canvas-darwin-x64@0.1.96': '@napi-rs/canvas-darwin-x64@0.1.97':
optional: true optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.96': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.96': '@napi-rs/canvas-linux-arm64-gnu@0.1.97':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.96': '@napi-rs/canvas-linux-arm64-musl@0.1.97':
optional: true optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.96': '@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
optional: true optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.96': '@napi-rs/canvas-linux-x64-gnu@0.1.97':
optional: true optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.96': '@napi-rs/canvas-linux-x64-musl@0.1.97':
optional: true optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.96': '@napi-rs/canvas-win32-arm64-msvc@0.1.97':
optional: true optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.96': '@napi-rs/canvas-win32-x64-msvc@0.1.97':
optional: true optional: true
'@napi-rs/canvas@0.1.96': '@napi-rs/canvas@0.1.97':
optionalDependencies: optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.96 '@napi-rs/canvas-android-arm64': 0.1.97
'@napi-rs/canvas-darwin-arm64': 0.1.96 '@napi-rs/canvas-darwin-arm64': 0.1.97
'@napi-rs/canvas-darwin-x64': 0.1.96 '@napi-rs/canvas-darwin-x64': 0.1.97
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.96 '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97
'@napi-rs/canvas-linux-arm64-gnu': 0.1.96 '@napi-rs/canvas-linux-arm64-gnu': 0.1.97
'@napi-rs/canvas-linux-arm64-musl': 0.1.96 '@napi-rs/canvas-linux-arm64-musl': 0.1.97
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.96 '@napi-rs/canvas-linux-riscv64-gnu': 0.1.97
'@napi-rs/canvas-linux-x64-gnu': 0.1.96 '@napi-rs/canvas-linux-x64-gnu': 0.1.97
'@napi-rs/canvas-linux-x64-musl': 0.1.96 '@napi-rs/canvas-linux-x64-musl': 0.1.97
'@napi-rs/canvas-win32-arm64-msvc': 0.1.96 '@napi-rs/canvas-win32-arm64-msvc': 0.1.97
'@napi-rs/canvas-win32-x64-msvc': 0.1.96 '@napi-rs/canvas-win32-x64-msvc': 0.1.97
optional: true optional: true
'@napi-rs/wasm-runtime@1.0.7': '@napi-rs/wasm-runtime@1.0.7':
@@ -3273,7 +3276,7 @@ snapshots:
'@push.rocks/smartxml@2.0.0': '@push.rocks/smartxml@2.0.0':
dependencies: dependencies:
fast-xml-parser: 5.5.5 fast-xml-parser: 5.5.6
'@push.rocks/smartyaml@2.0.5': '@push.rocks/smartyaml@2.0.5':
dependencies: dependencies:
@@ -3419,62 +3422,62 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.52': {} '@rolldown/pluginutils@1.0.0-beta.52': {}
'@rspack/binding-darwin-arm64@1.7.8': '@rspack/binding-darwin-arm64@1.7.9':
optional: true optional: true
'@rspack/binding-darwin-x64@1.7.8': '@rspack/binding-darwin-x64@1.7.9':
optional: true optional: true
'@rspack/binding-linux-arm64-gnu@1.7.8': '@rspack/binding-linux-arm64-gnu@1.7.9':
optional: true optional: true
'@rspack/binding-linux-arm64-musl@1.7.8': '@rspack/binding-linux-arm64-musl@1.7.9':
optional: true optional: true
'@rspack/binding-linux-x64-gnu@1.7.8': '@rspack/binding-linux-x64-gnu@1.7.9':
optional: true optional: true
'@rspack/binding-linux-x64-musl@1.7.8': '@rspack/binding-linux-x64-musl@1.7.9':
optional: true optional: true
'@rspack/binding-wasm32-wasi@1.7.8': '@rspack/binding-wasm32-wasi@1.7.9':
dependencies: dependencies:
'@napi-rs/wasm-runtime': 1.0.7 '@napi-rs/wasm-runtime': 1.0.7
optional: true optional: true
'@rspack/binding-win32-arm64-msvc@1.7.8': '@rspack/binding-win32-arm64-msvc@1.7.9':
optional: true optional: true
'@rspack/binding-win32-ia32-msvc@1.7.8': '@rspack/binding-win32-ia32-msvc@1.7.9':
optional: true optional: true
'@rspack/binding-win32-x64-msvc@1.7.8': '@rspack/binding-win32-x64-msvc@1.7.9':
optional: true optional: true
'@rspack/binding@1.7.8': '@rspack/binding@1.7.9':
optionalDependencies: optionalDependencies:
'@rspack/binding-darwin-arm64': 1.7.8 '@rspack/binding-darwin-arm64': 1.7.9
'@rspack/binding-darwin-x64': 1.7.8 '@rspack/binding-darwin-x64': 1.7.9
'@rspack/binding-linux-arm64-gnu': 1.7.8 '@rspack/binding-linux-arm64-gnu': 1.7.9
'@rspack/binding-linux-arm64-musl': 1.7.8 '@rspack/binding-linux-arm64-musl': 1.7.9
'@rspack/binding-linux-x64-gnu': 1.7.8 '@rspack/binding-linux-x64-gnu': 1.7.9
'@rspack/binding-linux-x64-musl': 1.7.8 '@rspack/binding-linux-x64-musl': 1.7.9
'@rspack/binding-wasm32-wasi': 1.7.8 '@rspack/binding-wasm32-wasi': 1.7.9
'@rspack/binding-win32-arm64-msvc': 1.7.8 '@rspack/binding-win32-arm64-msvc': 1.7.9
'@rspack/binding-win32-ia32-msvc': 1.7.8 '@rspack/binding-win32-ia32-msvc': 1.7.9
'@rspack/binding-win32-x64-msvc': 1.7.8 '@rspack/binding-win32-x64-msvc': 1.7.9
'@rspack/core@1.7.8': '@rspack/core@1.7.9':
dependencies: dependencies:
'@module-federation/runtime-tools': 0.22.0 '@module-federation/runtime-tools': 0.22.0
'@rspack/binding': 1.7.8 '@rspack/binding': 1.7.9
'@rspack/lite-tapable': 1.1.0 '@rspack/lite-tapable': 1.1.0
'@rspack/lite-tapable@1.1.0': {} '@rspack/lite-tapable@1.1.0': {}
'@sec-ant/readable-stream@0.4.1': {} '@sec-ant/readable-stream@0.4.1': {}
'@serve.zone/catalog@2.6.0(@tiptap/pm@2.27.2)': '@serve.zone/catalog@2.8.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.1 '@design.estate/dees-domtools': 2.5.1
@@ -4004,7 +4007,7 @@ snapshots:
fast-json-stable-stringify@2.1.0: {} fast-json-stable-stringify@2.1.0: {}
fast-xml-builder@1.1.3: fast-xml-builder@1.1.4:
dependencies: dependencies:
path-expression-matcher: 1.1.3 path-expression-matcher: 1.1.3
@@ -4012,9 +4015,9 @@ snapshots:
dependencies: dependencies:
strnum: 1.1.2 strnum: 1.1.2
fast-xml-parser@5.5.5: fast-xml-parser@5.5.6:
dependencies: dependencies:
fast-xml-builder: 1.1.3 fast-xml-builder: 1.1.4
path-expression-matcher: 1.1.3 path-expression-matcher: 1.1.3
strnum: 2.2.0 strnum: 2.2.0
@@ -4736,7 +4739,7 @@ snapshots:
pdfjs-dist@4.10.38: pdfjs-dist@4.10.38:
optionalDependencies: optionalDependencies:
'@napi-rs/canvas': 0.1.96 '@napi-rs/canvas': 0.1.97
peek-readable@5.4.2: {} peek-readable@5.4.2: {}

View File

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

View File

@@ -596,18 +596,26 @@ export class OneboxDockerManager {
async getContainerStats(containerID: string): Promise<IContainerStats | null> { async getContainerStats(containerID: string): Promise<IContainerStats | null> {
try { try {
// Try to get container directly first // Try to get container directly first
let container = await this.dockerClient!.getContainerById(containerID); let container: any = null;
try {
container = await this.dockerClient!.getContainerById(containerID);
} catch {
// Container not found by ID — might be a Swarm service ID
}
// If not found, it might be a service ID - try to get the actual container ID // If not found, it might be a service ID - try to get the actual container ID
if (!container) { if (!container) {
const serviceContainerId = await this.getContainerIdForService(containerID); const serviceContainerId = await this.getContainerIdForService(containerID);
if (serviceContainerId) { if (serviceContainerId) {
container = await this.dockerClient!.getContainerById(serviceContainerId); try {
container = await this.dockerClient!.getContainerById(serviceContainerId);
} catch {
// Service container also not found
}
} }
} }
if (!container) { if (!container) {
// Container/service not found
return null; return null;
} }
@@ -849,7 +857,23 @@ export class OneboxDockerManager {
cmd: string[] cmd: string[]
): Promise<{ stdout: string; stderr: string; exitCode: number }> { ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
try { try {
const container = await this.dockerClient!.getContainerById(containerID); let container: any = null;
try {
container = await this.dockerClient!.getContainerById(containerID);
} catch {
// Not a direct container ID — try Swarm service lookup
}
if (!container) {
const serviceContainerId = await this.getContainerIdForService(containerID);
if (serviceContainerId) {
try {
container = await this.dockerClient!.getContainerById(serviceContainerId);
} catch {
// Service container also not found
}
}
}
if (!container) { if (!container) {
throw new Error(`Container not found: ${containerID}`); throw new Error(`Container not found: ${containerID}`);
@@ -1011,7 +1035,23 @@ export class OneboxDockerManager {
callback: (line: string, isError: boolean) => void callback: (line: string, isError: boolean) => void
): Promise<void> { ): Promise<void> {
try { try {
const container = await this.dockerClient!.getContainerById(containerID); let container: any = null;
try {
container = await this.dockerClient!.getContainerById(containerID);
} catch {
// Not a direct container ID — try Swarm service lookup
}
if (!container) {
const serviceContainerId = await this.getContainerIdForService(containerID);
if (serviceContainerId) {
try {
container = await this.dockerClient!.getContainerById(serviceContainerId);
} catch {
// Service container also not found
}
}
}
if (!container) { if (!container) {
throw new Error(`Container not found: ${containerID}`); throw new Error(`Container not found: ${containerID}`);

View File

@@ -291,10 +291,65 @@ export class Onebox {
// Sort expiring domains by days remaining (ascending) // Sort expiring domains by days remaining (ascending)
expiringDomains.sort((a, b) => a.daysRemaining - b.daysRemaining); expiringDomains.sort((a, b) => a.daysRemaining - b.daysRemaining);
// Aggregate resource usage across all running service containers
let totalCpu = 0;
let totalMemoryUsed = 0;
let totalMemoryLimit = 0;
let totalNetworkIn = 0;
let totalNetworkOut = 0;
if (dockerRunning) {
const allServices = this.services.listServices();
const runningUserServices = allServices.filter((s) => s.status === 'running' && s.containerID);
logger.debug(`Resource stats: ${runningUserServices.length} running user services`);
const statsPromises = runningUserServices
.map((s) => {
logger.debug(`Fetching stats for user service: ${s.name} (${s.containerID})`);
return this.docker.getContainerStats(s.containerID!).catch((err) => {
logger.debug(`Stats failed for ${s.name}: ${(err as Error).message}`);
return null;
});
});
// Also get stats for platform service containers
const allPlatformServices = this.platformServices.getAllPlatformServices();
const runningPlatformServices = allPlatformServices.filter((s) => s.status === 'running' && s.containerId);
logger.debug(`Resource stats: ${runningPlatformServices.length} running platform services`);
const platformStatsPromises = runningPlatformServices
.map((s) => {
logger.debug(`Fetching stats for platform service: ${s.type} (${s.containerId})`);
return this.docker.getContainerStats(s.containerId!).catch((err) => {
logger.debug(`Stats failed for ${s.type}: ${(err as Error).message}`);
return null;
});
});
const allStats = await Promise.all([...statsPromises, ...platformStatsPromises]);
let successCount = 0;
for (const stats of allStats) {
if (stats) {
successCount++;
totalCpu += stats.cpuPercent;
totalMemoryUsed += stats.memoryUsed;
totalMemoryLimit = Math.max(totalMemoryLimit, stats.memoryLimit);
totalNetworkIn += stats.networkRx;
totalNetworkOut += stats.networkTx;
}
}
logger.debug(`Resource stats: ${successCount}/${allStats.length} containers returned stats. CPU: ${totalCpu}, Mem: ${totalMemoryUsed}`);
}
return { return {
docker: { docker: {
running: dockerRunning, running: dockerRunning,
version: dockerRunning ? await this.docker.getDockerVersion() : null, version: dockerRunning ? await this.docker.getDockerVersion() : null,
cpuUsage: Math.round(totalCpu * 10) / 10,
memoryUsage: totalMemoryUsed,
memoryTotal: totalMemoryLimit,
networkIn: totalNetworkIn,
networkOut: totalNetworkOut,
}, },
reverseProxy: proxyStatus, reverseProxy: proxyStatus,
dns: { dns: {

View File

@@ -23,6 +23,7 @@ export class OpsServer {
public schedulesHandler!: handlers.SchedulesHandler; public schedulesHandler!: handlers.SchedulesHandler;
public settingsHandler!: handlers.SettingsHandler; public settingsHandler!: handlers.SettingsHandler;
public logsHandler!: handlers.LogsHandler; public logsHandler!: handlers.LogsHandler;
public workspaceHandler!: handlers.WorkspaceHandler;
constructor(oneboxRef: Onebox) { constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef; this.oneboxRef = oneboxRef;
@@ -63,6 +64,7 @@ export class OpsServer {
this.schedulesHandler = new handlers.SchedulesHandler(this); this.schedulesHandler = new handlers.SchedulesHandler(this);
this.settingsHandler = new handlers.SettingsHandler(this); this.settingsHandler = new handlers.SettingsHandler(this);
this.logsHandler = new handlers.LogsHandler(this); this.logsHandler = new handlers.LogsHandler(this);
this.workspaceHandler = new handlers.WorkspaceHandler(this);
logger.success('OpsServer TypedRequest handlers initialized'); logger.success('OpsServer TypedRequest handlers initialized');
} }

View File

@@ -11,3 +11,4 @@ export * from './backups.handler.ts';
export * from './schedules.handler.ts'; export * from './schedules.handler.ts';
export * from './settings.handler.ts'; export * from './settings.handler.ts';
export * from './logs.handler.ts'; export * from './logs.handler.ts';
export * from './workspace.handler.ts';

View File

@@ -6,10 +6,128 @@ import { requireValidIdentity } from '../helpers/guards.ts';
export class PlatformHandler { export class PlatformHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
private activeLogStreams = new Map<string, boolean>();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
this.startLogStreaming();
}
/**
* Start streaming logs from all running containers (platform + user services)
* and push new entries to connected dashboard clients via TypedSocket
*/
private async startLogStreaming(): Promise<void> {
const checkAndStream = async () => {
// Stream platform service containers
const platformServices = this.opsServerRef.oneboxRef.database.getAllPlatformServices();
for (const service of platformServices) {
if (service.status !== 'running' || !service.containerId) continue;
const key = `platform:${service.type}`;
if (this.activeLogStreams.has(key)) continue;
this.activeLogStreams.set(key, true);
logger.info(`Starting log stream for platform service: ${service.type}`);
try {
await this.opsServerRef.oneboxRef.docker.streamContainerLogs(
service.containerId,
(line: string, isError: boolean) => {
this.pushPlatformLogToClients(service.type as interfaces.data.TPlatformServiceType, line, isError);
}
);
} catch (err) {
logger.warn(`Log stream failed for ${service.type}: ${(err as Error).message}`);
this.activeLogStreams.delete(key);
}
}
// Stream user service containers
const userServices = this.opsServerRef.oneboxRef.services.listServices();
for (const service of userServices) {
if (service.status !== 'running' || !service.containerID) continue;
const key = `service:${service.name}`;
if (this.activeLogStreams.has(key)) continue;
this.activeLogStreams.set(key, true);
logger.info(`Starting log stream for user service: ${service.name}`);
try {
await this.opsServerRef.oneboxRef.docker.streamContainerLogs(
service.containerID,
(line: string, isError: boolean) => {
this.pushServiceLogToClients(service.name, line, isError);
}
);
} catch (err) {
logger.warn(`Log stream failed for ${service.name}: ${(err as Error).message}`);
this.activeLogStreams.delete(key);
}
}
};
// Initial check after a short delay (let services start first)
setTimeout(() => checkAndStream(), 5000);
// Re-check periodically for newly started services
setInterval(() => checkAndStream(), 15000);
}
private parseLogLine(line: string, isError: boolean): { timestamp: string; level: string; message: string } {
const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
const timestamp = tsMatch ? tsMatch[1] : new Date().toISOString();
const message = tsMatch ? tsMatch[2] : line;
const msgLower = message.toLowerCase();
const level = isError || msgLower.includes('error') || msgLower.includes('fatal')
? 'error'
: msgLower.includes('warn')
? 'warn'
: 'info';
return { timestamp, level, message };
}
private pushPlatformLogToClients(
serviceType: interfaces.data.TPlatformServiceType,
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
const entry = this.parseLogLine(line, isError);
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => {
for (const conn of connections) {
typedsocket.createTypedRequest<interfaces.requests.IReq_PushPlatformServiceLog>(
'pushPlatformServiceLog',
conn,
).fire({ serviceType, entry }).catch(() => {});
}
})
.catch(() => {});
}
private pushServiceLogToClients(
serviceName: string,
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
const entry = this.parseLogLine(line, isError);
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => {
for (const conn of connections) {
typedsocket.createTypedRequest<interfaces.requests.IReq_PushServiceLog>(
'pushServiceLog',
conn,
).fire({ serviceName, entry }).catch(() => {});
}
})
.catch(() => {});
} }
private registerHandlers(): void { private registerHandlers(): void {
@@ -165,5 +283,47 @@ 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) => {
// Try to parse Docker timestamp from beginning of line
const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
const timestamp = tsMatch ? new Date(tsMatch[1]).getTime() : Date.now();
const message = tsMatch ? tsMatch[2] : line;
const msgLower = message.toLowerCase();
const isError = msgLower.includes('error') || msgLower.includes('fatal');
const isWarn = msgLower.includes('warn');
return {
id: index,
serviceId: 0,
timestamp,
message,
level: (isError ? 'error' : isWarn ? 'warn' : 'info') as 'info' | 'warn' | 'error' | 'debug',
source: 'stdout' as const,
};
});
return { logs };
},
),
);
} }
} }

View File

@@ -0,0 +1,181 @@
import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
import { requireValidIdentity } from '../helpers/guards.ts';
import { getErrorMessage } from '../../utils/error.ts';
export class WorkspaceHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
/**
* Resolve a service name to a container ID (handling Swarm service IDs)
*/
private async resolveContainerId(serviceName: string): Promise<string> {
const service = this.opsServerRef.oneboxRef.services.getService(serviceName);
if (!service || !service.containerID) {
throw new plugins.typedrequest.TypedResponseError(`Service not found or has no container: ${serviceName}`);
}
return service.containerID;
}
private registerHandlers(): void {
// Read file from container
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceReadFile>(
'workspaceReadFile',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
['cat', dataArg.path],
);
if (result.exitCode !== 0) {
throw new plugins.typedrequest.TypedResponseError(`Failed to read file: ${result.stderr || 'File not found'}`);
}
return { content: result.stdout };
},
),
);
// Write file to container
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceWriteFile>(
'workspaceWriteFile',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
// Use sh -c with printf to write content (handles special characters)
const escaped = dataArg.content.replace(/'/g, "'\\''");
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
['sh', '-c', `printf '%s' '${escaped}' > ${dataArg.path}`],
);
if (result.exitCode !== 0) {
throw new plugins.typedrequest.TypedResponseError(`Failed to write file: ${result.stderr}`);
}
return {};
},
),
);
// Read directory from container
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceReadDir>(
'workspaceReadDir',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
// Use ls with -1 -F to get entries with type indicators (/ for dirs)
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
['ls', '-1', '-F', dataArg.path],
);
if (result.exitCode !== 0) {
throw new plugins.typedrequest.TypedResponseError(`Failed to read directory: ${result.stderr}`);
}
const entries = result.stdout
.split('\n')
.filter((line) => line.trim())
.map((line) => {
const isDir = line.endsWith('/');
const name = isDir ? line.slice(0, -1) : line.replace(/[*@=|]$/, '');
const basePath = dataArg.path.endsWith('/') ? dataArg.path : dataArg.path + '/';
return {
type: (isDir ? 'directory' : 'file') as 'file' | 'directory',
name,
path: basePath + name,
};
});
return { entries };
},
),
);
// Create directory in container
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceMkdir>(
'workspaceMkdir',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
['mkdir', '-p', dataArg.path],
);
if (result.exitCode !== 0) {
throw new plugins.typedrequest.TypedResponseError(`Failed to create directory: ${result.stderr}`);
}
return {};
},
),
);
// Remove file/directory from container
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceRm>(
'workspaceRm',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
const args = dataArg.recursive ? ['rm', '-rf', dataArg.path] : ['rm', '-f', dataArg.path];
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
args,
);
if (result.exitCode !== 0) {
throw new plugins.typedrequest.TypedResponseError(`Failed to remove: ${result.stderr}`);
}
return {};
},
),
);
// Check if path exists in container
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceExists>(
'workspaceExists',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
['test', '-e', dataArg.path],
);
return { exists: result.exitCode === 0 };
},
),
);
// Execute a command in the container (non-interactive)
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_WorkspaceExec>(
'workspaceExec',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const containerId = await this.resolveContainerId(dataArg.serviceName);
const cmd = dataArg.args
? [dataArg.command, ...dataArg.args]
: [dataArg.command];
const result = await this.opsServerRef.oneboxRef.docker.execInContainer(
containerId,
cmd,
);
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
};
},
),
);
logger.info('Workspace handler registered');
}
}

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,11 @@ export interface ISystemStatus {
docker: { docker: {
running: boolean; running: boolean;
version: unknown; version: unknown;
cpuUsage: number;
memoryUsage: number;
memoryTotal: number;
networkIn: number;
networkOut: number;
}; };
reverseProxy: { reverseProxy: {
http: { running: boolean; port: number }; http: { running: boolean; port: number };

View File

@@ -11,3 +11,4 @@ export * from './backups.ts';
export * from './backup-schedules.ts'; export * from './backup-schedules.ts';
export * from './settings.ts'; export * from './settings.ts';
export * from './logs.ts'; export * from './logs.ts';
export * from './workspace.ts';

View File

@@ -69,3 +69,34 @@ export interface IReq_GetPlatformServiceStats extends plugins.typedrequestInterf
stats: data.IContainerStats; 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[];
};
}
export interface IReq_PushPlatformServiceLog extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushPlatformServiceLog
> {
method: 'pushPlatformServiceLog';
request: {
serviceType: data.TPlatformServiceType;
entry: {
timestamp: string;
level: string;
message: string;
};
};
response: {};
}

View File

@@ -212,3 +212,19 @@ export interface IReq_GetServiceBackupSchedules extends plugins.typedrequestInte
schedules: data.IBackupSchedule[]; schedules: data.IBackupSchedule[];
}; };
} }
export interface IReq_PushServiceLog extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushServiceLog
> {
method: 'pushServiceLog';
request: {
serviceName: string;
entry: {
timestamp: string;
level: string;
message: string;
};
};
response: {};
}

View File

@@ -0,0 +1,106 @@
import * as plugins from '../plugins.ts';
import * as data from '../data/index.ts';
export interface IReq_WorkspaceReadFile extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceReadFile
> {
method: 'workspaceReadFile';
request: {
identity: data.IIdentity;
serviceName: string;
path: string;
};
response: {
content: string;
};
}
export interface IReq_WorkspaceWriteFile extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceWriteFile
> {
method: 'workspaceWriteFile';
request: {
identity: data.IIdentity;
serviceName: string;
path: string;
content: string;
};
response: {};
}
export interface IReq_WorkspaceReadDir extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceReadDir
> {
method: 'workspaceReadDir';
request: {
identity: data.IIdentity;
serviceName: string;
path: string;
};
response: {
entries: Array<{ type: 'file' | 'directory'; name: string; path: string }>;
};
}
export interface IReq_WorkspaceMkdir extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceMkdir
> {
method: 'workspaceMkdir';
request: {
identity: data.IIdentity;
serviceName: string;
path: string;
};
response: {};
}
export interface IReq_WorkspaceRm extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceRm
> {
method: 'workspaceRm';
request: {
identity: data.IIdentity;
serviceName: string;
path: string;
recursive?: boolean;
};
response: {};
}
export interface IReq_WorkspaceExists extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceExists
> {
method: 'workspaceExists';
request: {
identity: data.IIdentity;
serviceName: string;
path: string;
};
response: {
exists: boolean;
};
}
export interface IReq_WorkspaceExec extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_WorkspaceExec
> {
method: 'workspaceExec';
request: {
identity: data.IIdentity;
serviceName: string;
command: string;
args?: string[];
};
response: {
stdout: string;
stderr: string;
exitCode: number;
};
}

View File

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

View File

@@ -27,6 +27,7 @@ export interface IServicesState {
platformServices: interfaces.data.IPlatformService[]; platformServices: interfaces.data.IPlatformService[];
currentPlatformService: interfaces.data.IPlatformService | null; currentPlatformService: interfaces.data.IPlatformService | null;
currentPlatformServiceStats: interfaces.data.IContainerStats | null; currentPlatformServiceStats: interfaces.data.IContainerStats | null;
currentPlatformServiceLogs: interfaces.data.ILogEntry[];
} }
export interface INetworkState { export interface INetworkState {
@@ -57,6 +58,7 @@ export interface IUiState {
activeView: string; activeView: string;
autoRefresh: boolean; autoRefresh: boolean;
refreshInterval: number; refreshInterval: number;
pendingAppTemplate?: any;
} }
// ============================================================================ // ============================================================================
@@ -90,6 +92,7 @@ export const servicesStatePart = await appState.getStatePart<IServicesState>(
platformServices: [], platformServices: [],
currentPlatformService: null, currentPlatformService: null,
currentPlatformServiceStats: null, currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
}, },
'soft', 'soft',
); );
@@ -497,6 +500,27 @@ export const fetchPlatformServiceStatsAction = servicesStatePart.createAction<{
} }
}); });
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 // Network Actions
// ============================================================================ // ============================================================================
@@ -938,3 +962,104 @@ const startAutoRefresh = () => {
uiStatePart.select((s) => s).subscribe(() => startAutoRefresh()); uiStatePart.select((s) => s).subscribe(() => startAutoRefresh());
loginStatePart.select((s) => s).subscribe(() => startAutoRefresh()); loginStatePart.select((s) => s).subscribe(() => startAutoRefresh());
startAutoRefresh(); startAutoRefresh();
// ============================================================================
// TypedSocket — real-time server push (logs, events)
// ============================================================================
let socketClient: InstanceType<typeof plugins.typedsocket.TypedSocket> | null = null;
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
// Handle server-pushed platform service log entries
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushPlatformServiceLog>(
'pushPlatformServiceLog',
async (dataArg) => {
const state = servicesStatePart.getState();
const entry: interfaces.data.ILogEntry = {
id: state.currentPlatformServiceLogs.length,
serviceId: 0,
timestamp: new Date(dataArg.entry.timestamp).getTime(),
message: dataArg.entry.message,
level: dataArg.entry.level as 'info' | 'warn' | 'error' | 'debug',
source: 'stdout',
};
const updated = [...state.currentPlatformServiceLogs, entry];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
servicesStatePart.setState({
...state,
currentPlatformServiceLogs: updated,
});
return {};
},
),
);
// Handle server-pushed user service log entries
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushServiceLog>(
'pushServiceLog',
async (dataArg) => {
const state = servicesStatePart.getState();
// Only append if we're currently viewing this service
if (!state.currentService || state.currentService.name !== dataArg.serviceName) {
return {};
}
const entry: interfaces.data.ILogEntry = {
id: state.currentServiceLogs.length,
serviceId: 0,
timestamp: new Date(dataArg.entry.timestamp).getTime(),
message: dataArg.entry.message,
level: dataArg.entry.level as 'info' | 'warn' | 'error' | 'debug',
source: 'stdout',
};
const updated = [...state.currentServiceLogs, entry];
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
servicesStatePart.setState({
...state,
currentServiceLogs: updated,
});
return {};
},
),
);
async function connectSocket() {
if (socketClient) return;
try {
socketClient = await plugins.typedsocket.TypedSocket.createClient(
socketRouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
);
await socketClient.setTag('role', 'ops_dashboard');
console.log('TypedSocket dashboard connection established');
} catch (err) {
console.error('TypedSocket connection failed:', err);
socketClient = null;
}
}
async function disconnectSocket() {
if (socketClient) {
try {
await socketClient.disconnect();
} catch {
// ignore disconnect errors
}
socketClient = null;
}
}
// Connect socket when logged in, disconnect when logged out
loginStatePart.select((s) => s).subscribe((loginState) => {
if (loginState.isLoggedIn) {
connectSocket();
} else {
disconnectSocket();
}
});

View File

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

View File

@@ -0,0 +1,224 @@
import * as plugins from '../plugins.js';
import * as shared from './shared/index.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../ts_interfaces/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
// App template definitions — curated Docker apps
const appTemplates = [
{
id: 'nginx',
name: 'Nginx',
description: 'High-performance web server and reverse proxy. Lightweight, fast, and battle-tested.',
category: 'Web Server',
iconName: 'globe',
image: 'nginx:alpine',
port: 80,
},
{
id: 'wordpress',
name: 'WordPress',
description: 'The world\'s most popular content management system. Powers over 40% of the web.',
category: 'CMS',
iconName: 'file-text',
image: 'wordpress:latest',
port: 80,
enableMongoDB: false,
envVars: [
{ key: 'WORDPRESS_DB_HOST', value: '', description: 'Database host', required: true },
{ key: 'WORDPRESS_DB_USER', value: 'wordpress', description: 'Database user' },
{ key: 'WORDPRESS_DB_PASSWORD', value: '', description: 'Database password', required: true },
{ key: 'WORDPRESS_DB_NAME', value: 'wordpress', description: 'Database name' },
],
},
{
id: 'ghost',
name: 'Ghost',
description: 'Modern publishing platform for creating professional blogs and newsletters.',
category: 'CMS',
iconName: 'book-open',
image: 'ghost:latest',
port: 2368,
envVars: [
{ key: 'database__client', value: 'sqlite3', description: 'Database client (sqlite3 for standalone)' },
{ key: 'database__connection__filename', value: '/var/lib/ghost/content/data/ghost.db', description: 'SQLite database path' },
{ key: 'url', value: 'http://localhost:2368', description: 'Public URL of the blog' },
],
},
{
id: 'gitea',
name: 'Gitea',
description: 'Lightweight self-hosted Git service. Easy to install and maintain.',
category: 'Dev Tools',
iconName: 'git-branch',
image: 'gitea/gitea:latest',
port: 3000,
},
{
id: 'nextcloud',
name: 'Nextcloud',
description: 'Self-hosted file sync and share platform. Your own private cloud.',
category: 'Storage',
iconName: 'package',
image: 'nextcloud:latest',
port: 80,
},
{
id: 'grafana',
name: 'Grafana',
description: 'Open-source observability platform for metrics, logs, and traces visualization.',
category: 'Monitoring',
iconName: 'monitor',
image: 'grafana/grafana:latest',
port: 3000,
envVars: [
{ key: 'GF_SECURITY_ADMIN_PASSWORD', value: 'admin', description: 'Admin password' },
],
},
{
id: 'uptime-kuma',
name: 'Uptime Kuma',
description: 'Self-hosted monitoring tool. Beautiful UI for tracking uptime of services.',
category: 'Monitoring',
iconName: 'monitor',
image: 'louislam/uptime-kuma:latest',
port: 3001,
},
{
id: 'plausible',
name: 'Plausible Analytics',
description: 'Privacy-friendly web analytics. No cookies, GDPR compliant by design.',
category: 'Analytics',
iconName: 'monitor',
image: 'plausible/analytics:latest',
port: 8000,
enableClickHouse: true,
},
{
id: 'vaultwarden',
name: 'Vaultwarden',
description: 'Lightweight Bitwarden-compatible password manager server.',
category: 'Security',
iconName: 'shield',
image: 'vaultwarden/server:latest',
port: 80,
},
{
id: 'n8n',
name: 'N8N',
description: 'Workflow automation tool. Connect anything to everything with a visual editor.',
category: 'Automation',
iconName: 'server',
image: 'n8nio/n8n:latest',
port: 5678,
},
{
id: 'mattermost',
name: 'Mattermost',
description: 'Open-source Slack alternative for team communication and collaboration.',
category: 'Communication',
iconName: 'mail',
image: 'mattermost/mattermost-team-edition:latest',
port: 8065,
},
{
id: 'portainer',
name: 'Portainer',
description: 'Docker management UI. Monitor and manage containers from a web interface.',
category: 'Dev Tools',
iconName: 'package',
image: 'portainer/portainer-ce:latest',
port: 9000,
},
{
id: 'redis',
name: 'Redis',
description: 'In-memory data store used as database, cache, and message broker.',
category: 'Database',
iconName: 'database',
image: 'redis:alpine',
port: 6379,
},
{
id: 'postgres',
name: 'PostgreSQL',
description: 'Advanced open-source relational database. Reliable and feature-rich.',
category: 'Database',
iconName: 'database',
image: 'postgres:16-alpine',
port: 5432,
envVars: [
{ key: 'POSTGRES_PASSWORD', value: '', description: 'Superuser password', required: true },
{ key: 'POSTGRES_USER', value: 'postgres', description: 'Superuser name' },
{ key: 'POSTGRES_DB', value: 'postgres', description: 'Default database name' },
],
},
{
id: 'mariadb',
name: 'MariaDB',
description: 'Community-developed fork of MySQL. Drop-in replacement with enhanced features.',
category: 'Database',
iconName: 'database',
image: 'mariadb:latest',
port: 3306,
envVars: [
{ key: 'MARIADB_ROOT_PASSWORD', value: '', description: 'Root password', required: true },
],
},
{
id: 'adminer',
name: 'Adminer',
description: 'Database management tool in a single PHP file. Supports MySQL, PostgreSQL, SQLite.',
category: 'Dev Tools',
iconName: 'database',
image: 'adminer:latest',
port: 8080,
},
];
@customElement('ob-view-appstore')
export class ObViewAppStore extends DeesElement {
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css``,
];
async connectedCallback() {
super.connectedCallback();
}
public render(): TemplateResult {
return html`
<ob-sectionheading>App Store</ob-sectionheading>
<sz-app-store-view
.apps=${appTemplates}
@deploy-app=${(e: CustomEvent) => this.handleDeployApp(e)}
></sz-app-store-view>
`;
}
private handleDeployApp(e: CustomEvent) {
const app = e.detail?.app;
if (!app) return;
// Store the template and navigate on next microtask to avoid
// destroying the current view while the event handler is still on the call stack
setTimeout(() => {
// Set both pendingAppTemplate and activeView atomically
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
pendingAppTemplate: app,
activeView: 'services',
});
}, 0);
}
}

View File

@@ -25,6 +25,7 @@ export class ObViewDashboard extends DeesElement {
platformServices: [], platformServices: [],
currentPlatformService: null, currentPlatformService: null,
currentPlatformServiceStats: null, currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
}; };
@state() @state()
@@ -109,8 +110,8 @@ export class ObViewDashboard extends DeesElement {
cpu: status?.docker?.cpuUsage || 0, cpu: status?.docker?.cpuUsage || 0,
memoryUsed: status?.docker?.memoryUsage || 0, memoryUsed: status?.docker?.memoryUsage || 0,
memoryTotal: status?.docker?.memoryTotal || 0, memoryTotal: status?.docker?.memoryTotal || 0,
networkIn: 0, networkIn: status?.docker?.networkIn || 0,
networkOut: 0, networkOut: status?.docker?.networkOut || 0,
topConsumers: [], topConsumers: [],
}, },
platformServices: platformServices.map((ps) => ({ platformServices: platformServices.map((ps) => ({

View File

@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
import * as shared from './shared/index.js'; import * as shared from './shared/index.js';
import * as appstate from '../appstate.js'; import * as appstate from '../appstate.js';
import * as interfaces from '../../ts_interfaces/index.js'; import * as interfaces from '../../ts_interfaces/index.js';
import { BackendExecutionEnvironment } from '../environments/backend-environment.js';
import { import {
DeesElement, DeesElement,
customElement, customElement,
@@ -76,20 +77,29 @@ function toServiceStats(stats: interfaces.data.IContainerStats) {
}; };
} }
function parseLogs(logs: any): Array<{ timestamp: string; message: string }> { function parseLogs(logs: any): Array<{ timestamp: string; message: string; level?: string }> {
if (Array.isArray(logs)) { if (Array.isArray(logs)) {
return logs.map((entry: any) => ({ return logs.map((entry: any) => {
timestamp: entry.timestamp ? String(entry.timestamp) : '', const ts = entry.timestamp
message: entry.message || String(entry), ? (typeof entry.timestamp === 'number' ? new Date(entry.timestamp).toISOString() : String(entry.timestamp))
})); : new Date().toISOString();
const message = entry.message || String(entry);
const level = entry.level || 'info';
return { timestamp: ts, message, level };
});
} }
if (typeof logs === 'string' && logs.trim()) { if (typeof logs === 'string' && logs.trim()) {
return logs.split('\n').filter((line: string) => line.trim()).map((line: string) => { return logs.split('\n').filter((line: string) => line.trim()).map((line: string) => {
const match = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/); const match = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
if (match) { const timestamp = match ? match[1] : new Date().toISOString();
return { timestamp: match[1], message: match[2] }; const message = match ? match[2] : line;
} const msgLower = message.toLowerCase();
return { timestamp: '', message: line }; const level = msgLower.includes('error') || msgLower.includes('fatal')
? 'error'
: msgLower.includes('warn')
? 'warn'
: 'info';
return { timestamp, message, level };
}); });
} }
return []; return [];
@@ -108,6 +118,7 @@ export class ObViewServices extends DeesElement {
platformServices: [], platformServices: [],
currentPlatformService: null, currentPlatformService: null,
currentPlatformServiceStats: null, currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
}; };
@state() @state()
@@ -125,6 +136,12 @@ export class ObViewServices extends DeesElement {
@state() @state()
accessor selectedPlatformType: string = ''; accessor selectedPlatformType: string = '';
@state()
accessor workspaceOpen: boolean = false;
@state()
accessor pendingTemplate: any = null;
constructor() { constructor() {
super(); super();
@@ -141,6 +158,8 @@ export class ObViewServices extends DeesElement {
this.backupsState = newState; this.backupsState = newState;
}); });
this.rxSubscriptions.push(backupsSub); this.rxSubscriptions.push(backupsSub);
// No subscription needed — pendingAppTemplate is checked in render()
} }
public static styles = [ public static styles = [
@@ -176,6 +195,18 @@ export class ObViewServices extends DeesElement {
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
:host(.workspace-mode) {
max-width: none;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
}
:host(.workspace-mode) ob-sectionheading {
display: none;
}
`, `,
]; ];
@@ -190,13 +221,27 @@ export class ObViewServices extends DeesElement {
const state = appstate.servicesStatePart.getState(); const state = appstate.servicesStatePart.getState();
if (state.currentPlatformService) { if (state.currentPlatformService) {
const type = state.currentPlatformService.type; const type = state.currentPlatformService.type;
// Clear the selection so it doesn't persist on next visit
appstate.servicesStatePart.setState({ appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(), ...appstate.servicesStatePart.getState(),
currentPlatformService: null, currentPlatformService: null,
}); });
this.navigateToPlatformDetail(type); this.navigateToPlatformDetail(type);
} }
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
// Check for pending app template from the App Store after each update
const uiState = appstate.uiStatePart.getState();
if (uiState.pendingAppTemplate && !this.pendingTemplate) {
this.pendingTemplate = uiState.pendingAppTemplate;
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
pendingAppTemplate: undefined,
});
this.currentView = 'create';
}
} }
public render(): TemplateResult { public render(): TemplateResult {
@@ -221,9 +266,20 @@ export class ObViewServices extends DeesElement {
domain: s.domain || null, domain: s.domain || null,
status: mapStatus(s.status), status: mapStatus(s.status),
})); }));
const displayStatus = (status: string) => {
switch (status) {
case 'running': return 'Running';
case 'stopped': return 'Stopped';
case 'starting': return 'Starting...';
case 'stopping': return 'Stopping...';
case 'failed': return 'Failed';
case 'not-deployed': return 'Not Deployed';
default: return status;
}
};
const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({ const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({
name: ps.displayName, name: ps.displayName,
status: ps.status === 'running' ? `Running` : ps.status, status: displayStatus(ps.status),
running: ps.status === 'running', running: ps.status === 'running',
type: ps.type, type: ps.type,
})); }));
@@ -272,7 +328,63 @@ export class ObViewServices extends DeesElement {
`; `;
} }
private async deployFromTemplate(template: any): Promise<void> {
const name = template.id || template.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
const envVars: Record<string, string> = {};
if (template.envVars) {
for (const ev of template.envVars) {
if (ev.key && ev.value) envVars[ev.key] = ev.value;
}
}
const serviceConfig: interfaces.data.IServiceCreate = {
name,
image: template.image,
port: template.port || 80,
envVars,
enableMongoDB: template.enableMongoDB || false,
enableS3: template.enableS3 || false,
enableClickHouse: template.enableClickHouse || false,
};
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
config: serviceConfig,
});
this.pendingTemplate = null;
this.currentView = 'list';
}
private renderCreateView(): TemplateResult { private renderCreateView(): TemplateResult {
// If we have a pending app template from the App Store, show a quick-deploy confirmation
if (this.pendingTemplate) {
const t = this.pendingTemplate;
return html`
<ob-sectionheading>Deploy ${t.name}</ob-sectionheading>
<div style="max-width: 600px; margin: 0 auto;">
<div style="background: var(--ci-shade-1, #09090b); border: 1px solid var(--ci-shade-2, #27272a); border-radius: 8px; padding: 24px; margin-bottom: 16px;">
<h3 style="margin: 0 0 8px 0; font-size: 18px;">${t.name}</h3>
<p style="margin: 0 0 16px 0; color: var(--ci-shade-5, #a1a1aa); font-size: 14px;">${t.description}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
<div><span style="color: var(--ci-shade-5, #a1a1aa);">Image:</span> <strong>${t.image}</strong></div>
<div><span style="color: var(--ci-shade-5, #a1a1aa);">Port:</span> <strong>${t.port}</strong></div>
<div><span style="color: var(--ci-shade-5, #a1a1aa);">Service Name:</span> <strong>${t.id}</strong></div>
<div><span style="color: var(--ci-shade-5, #a1a1aa);">Category:</span> <strong>${t.category}</strong></div>
</div>
${t.enableMongoDB || t.enableS3 || t.enableClickHouse ? html`
<div style="margin-top: 12px; font-size: 13px; color: var(--ci-shade-5, #a1a1aa);">
Platform Services:
${t.enableMongoDB ? html`<span style="margin-right: 8px;">MongoDB</span>` : ''}
${t.enableS3 ? html`<span style="margin-right: 8px;">S3</span>` : ''}
${t.enableClickHouse ? html`<span>ClickHouse</span>` : ''}
</div>
` : ''}
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button class="deploy-button" style="background: transparent; border: 1px solid var(--ci-shade-2, #27272a); color: inherit;" @click=${() => { this.pendingTemplate = null; this.currentView = 'list'; }}>Cancel</button>
<button class="deploy-button" @click=${() => this.deployFromTemplate(t)}>Deploy ${t.name}</button>
</div>
</div>
`;
}
return html` return html`
<ob-sectionheading>Create Service</ob-sectionheading> <ob-sectionheading>Create Service</ob-sectionheading>
<sz-service-create-view <sz-service-create-view
@@ -326,6 +438,28 @@ export class ObViewServices extends DeesElement {
this.currentView = 'list'; this.currentView = 'list';
}} }}
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)} @service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
@request-workspace=${async (e: CustomEvent) => {
const name = e.detail?.service?.name || this.selectedServiceName;
const identity = appstate.loginStatePart.getState().identity;
if (!name || !identity) return;
try {
const env = new BackendExecutionEnvironment(name, identity);
await env.init();
const detailView = this.shadowRoot?.querySelector('sz-service-detail-view') as any;
if (detailView) {
detailView.workspaceEnvironment = env;
}
this.workspaceOpen = true;
this.classList.add('workspace-mode');
} catch (err) {
console.error('Failed to open workspace:', err);
}
}}
@back=${() => {
this.workspaceOpen = false;
this.classList.remove('workspace-mode');
this.currentView = 'list';
}}
></sz-service-detail-view> ></sz-service-detail-view>
`; `;
} }
@@ -356,12 +490,26 @@ export class ObViewServices extends DeesElement {
} }
private navigateToPlatformDetail(type: string): void { private navigateToPlatformDetail(type: string): void {
// Reset to list first to force fresh DOM for dees-chart-log
this.currentView = 'list';
this.selectedPlatformType = type; this.selectedPlatformType = type;
// Fetch stats for this platform service
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, { // Clear previous stats/logs before fetching new ones
serviceType: type as interfaces.data.TPlatformServiceType, appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
});
// 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 });
// Switch to detail view on next microtask (ensures fresh DOM)
requestAnimationFrame(() => {
this.currentView = 'platform-detail';
}); });
this.currentView = 'platform-detail';
} }
private renderPlatformDetailView(): TemplateResult { private renderPlatformDetailView(): TemplateResult {
@@ -369,36 +517,63 @@ export class ObViewServices extends DeesElement {
(ps) => ps.type === this.selectedPlatformType, (ps) => ps.type === this.selectedPlatformType,
); );
const stats = this.servicesState.currentPlatformServiceStats; const stats = this.servicesState.currentPlatformServiceStats;
const metrics = stats const metrics = {
? { cpu: stats ? Math.round(stats.cpuPercent) : 0,
cpu: Math.round(stats.cpuPercent), memory: stats ? Math.round(stats.memoryPercent) : 0,
memory: Math.round(stats.memoryPercent), storage: 0,
storage: 0, connections: undefined as number | undefined,
connections: 0, };
}
: undefined; // Real service info per platform type
const serviceInfo: Record<string, { host: string; port: number; version: string; config: Record<string, any> }> = {
mongodb: { host: 'onebox-mongodb', port: 27017, version: '4.4', config: { engine: 'WiredTiger', authEnabled: true } },
minio: { host: 'onebox-minio', port: 9000, version: 'latest', config: { consolePort: 9001, region: 'us-east-1' } },
clickhouse: { host: 'onebox-clickhouse', port: 8123, version: 'latest', config: { nativePort: 9000, httpPort: 8123 } },
caddy: { host: 'onebox-caddy', port: 80, version: '2-alpine', config: { httpsPort: 443, adminApi: 2019 } },
};
const info = platformService
? serviceInfo[platformService.type] || { host: 'unknown', port: 0, version: '', config: {} }
: { host: '', port: 0, version: '', config: {} };
// Map backend status to catalog-compatible status
const mapPlatformStatus = (status: string): 'running' | 'stopped' | 'error' => {
switch (status) {
case 'running': return 'running';
case 'failed': return 'error';
case 'starting':
case 'stopping':
case 'stopped':
case 'not-deployed':
default: return 'stopped';
}
};
return html` return html`
<ob-sectionheading>Platform Service</ob-sectionheading> <ob-sectionheading>Platform Service</ob-sectionheading>
<div class="page-actions" style="justify-content: flex-start;">
<button class="deploy-button" style="background: transparent; border: 1px solid var(--ci-shade-2, #27272a); color: inherit;" @click=${() => { this.currentView = 'list'; }}>
&larr; Back to Services
</button>
</div>
<sz-platform-service-detail-view <sz-platform-service-detail-view
.service=${platformService .service=${platformService
? { ? {
id: platformService.type, id: platformService.type,
name: platformService.displayName, name: platformService.displayName,
type: platformService.type, type: platformService.type,
status: platformService.status === 'running' status: mapPlatformStatus(platformService.status),
? 'running' version: info.version,
: platformService.status === 'failed' host: info.host,
? 'error' port: info.port,
: 'stopped', config: info.config,
version: '',
host: 'localhost',
port: 0,
config: {},
metrics, metrics,
} }
: null} : null}
.logs=${[]} .logs=${this.servicesState.currentPlatformServiceLogs.map((log) => ({
timestamp: new Date(log.timestamp).toISOString(),
level: log.level,
message: log.message,
}))}
@back=${() => { @back=${() => {
this.currentView = 'list'; this.currentView = 'list';
}} }}

View File

@@ -0,0 +1,155 @@
/**
* BackendExecutionEnvironment — implements IExecutionEnvironment
* by routing all filesystem and process operations through the onebox API
* to Docker exec on the target container.
*/
import * as plugins from '../plugins.js';
import * as interfaces from '../../ts_interfaces/index.js';
// Import IExecutionEnvironment type from dees-catalog
type IExecutionEnvironment = import('@design.estate/dees-catalog').IExecutionEnvironment;
type IFileEntry = import('@design.estate/dees-catalog').IFileEntry;
type IFileWatcher = import('@design.estate/dees-catalog').IFileWatcher;
type IProcessHandle = import('@design.estate/dees-catalog').IProcessHandle;
const domtools = plugins.deesElement.domtools;
export class BackendExecutionEnvironment implements IExecutionEnvironment {
readonly type = 'backend' as const;
private _ready = false;
private identity: interfaces.data.IIdentity;
constructor(
private serviceName: string,
identity: interfaces.data.IIdentity,
) {
this.identity = identity;
}
get ready(): boolean {
return this._ready;
}
async init(): Promise<void> {
// Verify the container is accessible by checking if root exists
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceExists>(
'workspaceExists',
{ path: '/' },
);
if (!result.exists) {
throw new Error(`Cannot access container filesystem for service: ${this.serviceName}`);
}
this._ready = true;
}
async destroy(): Promise<void> {
this._ready = false;
}
async readFile(path: string): Promise<string> {
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceReadFile>(
'workspaceReadFile',
{ path },
);
return result.content;
}
async writeFile(path: string, contents: string): Promise<void> {
await this.fireRequest<interfaces.requests.IReq_WorkspaceWriteFile>(
'workspaceWriteFile',
{ path, content: contents },
);
}
async readDir(path: string): Promise<IFileEntry[]> {
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceReadDir>(
'workspaceReadDir',
{ path },
);
return result.entries;
}
async mkdir(path: string): Promise<void> {
await this.fireRequest<interfaces.requests.IReq_WorkspaceMkdir>(
'workspaceMkdir',
{ path },
);
}
async rm(path: string, options?: { recursive?: boolean }): Promise<void> {
await this.fireRequest<interfaces.requests.IReq_WorkspaceRm>(
'workspaceRm',
{ path, recursive: options?.recursive },
);
}
async exists(path: string): Promise<boolean> {
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceExists>(
'workspaceExists',
{ path },
);
return result.exists;
}
watch(
_path: string,
_callback: (event: 'rename' | 'change', filename: string | null) => void,
_options?: { recursive?: boolean },
): IFileWatcher {
// Polling-based file watching — check for changes periodically
// For now, return a no-op watcher. Full implementation would poll readDir.
return { stop: () => {} };
}
async spawn(command: string, args?: string[]): Promise<IProcessHandle> {
// For interactive shell: execute the command via the workspace exec API
// and return a process handle that bridges stdin/stdout
const cmd = args ? [command, ...args] : [command];
const fullCommand = cmd.join(' ');
// Use a non-interactive exec for now — full interactive shell would need
// TypedSocket bidirectional streaming (to be implemented)
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceExec>(
'workspaceExec',
{ command: cmd[0], args: cmd.slice(1) },
);
// Create a ReadableStream from the exec output
const output = new ReadableStream<string>({
start(controller) {
if (result.stdout) controller.enqueue(result.stdout);
if (result.stderr) controller.enqueue(result.stderr);
controller.close();
},
});
// Create a writable stream (no-op for non-interactive)
const inputStream = new WritableStream<string>();
return {
output,
input: inputStream,
exit: Promise.resolve(result.exitCode),
kill: () => {},
};
}
/**
* Helper to fire TypedRequests to the workspace API
*/
private async fireRequest<T extends { method: string; request: any; response: any }>(
method: string,
data: Omit<T['request'], 'identity' | 'serviceName'>,
): Promise<T['response']> {
const typedRequest = new domtools.plugins.typedrequest.TypedRequest<T>(
'/typedrequest',
method,
);
return await typedRequest.fire({
identity: this.identity,
serviceName: this.serviceName,
...data,
} as T['request']);
}
}

View File

@@ -5,9 +5,13 @@ import * as deesCatalog from '@design.estate/dees-catalog';
// @serve.zone scope — side-effect import registers all sz-* custom elements // @serve.zone scope — side-effect import registers all sz-* custom elements
import '@serve.zone/catalog'; import '@serve.zone/catalog';
// TypedSocket for real-time server push (logs, events)
import * as typedsocket from '@api.global/typedsocket';
export { export {
deesElement, deesElement,
deesCatalog, deesCatalog,
typedsocket,
}; };
// domtools gives us TypedRequest, smartstate, smartrouter, and other utilities // domtools gives us TypedRequest, smartstate, smartrouter, and other utilities