Compare commits

..

10 Commits

16 changed files with 993 additions and 465 deletions

View File

@@ -1,140 +0,0 @@
# Onebox Development Notes
## ⚠️ CRITICAL DEVELOPMENT RULES ⚠️
### NEVER GUESS - ALWAYS READ THE ACTUAL CODE
**FUCKING ALWAYS look at the dependency actual code. Don't start fucking guessing stuff.**
run "pnpm run watch" when starting to do stuff, so the UI gets recompiled and the server automatically restarts on file changes.
When working with any dependency:
1. **READ the actual source code** in `node_modules/` or check the package documentation
2. **CHECK the exact API** - don't assume based on similar libraries
3. **VERIFY method names, return types, and property structures** before using them
4. **TEST with the actual implementation** - APIs change between versions
Common mistakes to avoid:
- ❌ Assuming API structure based on similar libraries
- ❌ Guessing method names or property paths
- ❌ Using outdated documentation without checking current version
- ✅ Read the actual TypeScript definitions in node_modules
- ✅ Check the package's README and changelog
- ✅ Test the actual behavior before implementing
## Architecture Changes
### Reverse Proxy Implementation
- **Replaced Nginx** with native Deno reverse proxy (`ts/classes/reverseproxy.ts`)
- Features:
- HTTP/HTTPS dual servers (ports 80/443)
- TLS/SSL certificate management with hot-reload
- WebSocket bidirectional proxying
- Dynamic routing from database
- SNI (Server Name Indication) support
### Code Organization
- Removed "onebox." prefix from all TypeScript files
- Organized into subfolders:
- `ts/classes/` - All class implementations
- `ts/` - Root level utilities (logging, types, plugins, cli, info)
### WebSocket Real-time Communication
- **Backend**: WebSocket endpoint at `/api/ws` (`ts/classes/httpserver.ts:96-174`)
- Connection management with client Set tracking
- Broadcast methods: `broadcast()`, `broadcastServiceUpdate()`, `broadcastServiceStatus()`
- Integrated with service lifecycle (start/stop/restart actions)
- Status monitoring loop broadcasts changes automatically
- **Frontend**: Angular WebSocket service (`ui/src/app/core/services/websocket.service.ts`)
- Auto-connects on app initialization
- Exponential backoff reconnection (max 5 attempts)
- RxJS Observable-based message streaming
- Components subscribe to real-time updates
- **Message Types**:
- `connected` - Initial connection confirmation
- `service_update` - Service lifecycle changes (action: created/updated/deleted/started/stopped)
- `service_status` - Real-time status changes from monitoring loop
- `system_status` - System-wide updates
- **Testing**: Use `.nogit/test-ws-updates.ts` to monitor WebSocket messages
### Docker Configuration
- **System Docker**: Uses root Docker at `/var/run/docker.sock` (NOT rootless)
- **Swarm Mode**: Enabled for service orchestration
- **API Access**: Interact with Docker via direct API calls to the socket
- ❌ DO NOT switch Docker CLI contexts
- ✅ Use curl/HTTP requests to `/var/run/docker.sock`
- **Network**: Overlay network `onebox-network` with `Attachable: true`
- **Services vs Containers**: All workloads run as Swarm services (not standalone containers)
## Debugging Tips
### Backend Logs
Use the background bash task to check server logs:
```bash
# Check for specific patterns (e.g., Login attempts)
BashOutput tool with filter: "Login|error|Error"
# Check all recent output
BashOutput tool without filter
```
The dev server runs with `--watch` so it auto-restarts on file changes.
### Frontend Testing
Use Playwright for UI testing:
```typescript
// Navigate to app
mcp__playwright__browser_navigate({ url: "http://localhost:3000" })
// Fill login form
mcp__playwright__browser_fill_form({
fields: [
{ name: "Username", type: "textbox", ref: "...", value: "admin" },
{ name: "Password", type: "textbox", ref: "...", value: "admin" }
]
})
// Click button
mcp__playwright__browser_click({ element: "Sign in button", ref: "..." })
// Check console errors
// Playwright automatically shows console messages in results
```
### Common Issues
#### Login Issue (Fixed)
**Problem**: `admin/admin` credentials returned "Invalid credentials"
**Root Cause**: `rowToUser()` function in database.ts was accessing rows as arrays `row[2]` instead of objects `row.password_hash`. The @db/sqlite library returns rows as objects with snake_case column names.
**Fix**: Updated `rowToUser()` to support both access patterns:
```typescript
private rowToUser(row: any): IUser {
return {
passwordHash: String(row.password_hash || row[2]),
// ... other fields
};
}
```
**Location**: `ts/classes/database.ts:506-515`
## Default Credentials
- Username: `admin`
- Password: `admin`
- ⚠️ Change immediately after first login!
## Development Server
```bash
# Main server (port 3000)
deno task dev
# Check server status
curl http://localhost:3000/api/status
```
## API Endpoints
- `POST /api/auth/login` - Login (returns JWT-like token)
- `GET /api/status` - System status (requires auth)
- `GET /api/services` - List services (requires auth)
- See `ts/classes/httpserver.ts` for full API

View File

@@ -0,0 +1,37 @@
## Onebox {{VERSION}}
Pre-compiled binaries for multiple platforms.
### Installation
#### Option 1: Via npm (recommended)
```bash
npm install -g @serve.zone/onebox
```
#### Option 2: Via installer script
```bash
curl -sSL https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh | sudo bash
```
#### Option 3: Direct binary download
Download the appropriate binary for your platform from the assets below and make it executable.
### Supported Platforms
- Linux x86_64 (x64)
- Linux ARM64 (aarch64)
- macOS x86_64 (Intel)
- macOS ARM64 (Apple Silicon)
- Windows x86_64
### Checksums
SHA256 checksums are provided in `SHA256SUMS.txt` for binary verification.
### npm Package
The npm package includes automatic binary detection and installation for your platform.

83
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,83 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
check:
name: Type Check & Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- 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
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Compile for current platform
run: |
echo "Testing compilation for Linux x86_64..."
deno 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
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Compile all platform binaries
run: bash scripts/compile-all.sh
- name: Upload all binaries as artifact
uses: actions/upload-artifact@v3
with:
name: onebox-binaries.zip
path: dist/binaries/*
retention-days: 30

View File

@@ -0,0 +1,129 @@
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
npm-publish:
runs-on: ubuntu-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

@@ -0,0 +1,248 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- 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 "Building 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 all platforms
run: |
echo "================================================"
echo " Onebox Release Compilation"
echo " Version: ${{ steps.version.outputs.version }}"
echo "================================================"
echo ""
# Clean up old binaries and create fresh directory
rm -rf dist/binaries
mkdir -p dist/binaries
echo "-> Cleaned old binaries from dist/binaries"
echo ""
# Linux x86_64
echo "-> Compiling for Linux x86_64..."
deno compile --allow-all --no-check \
--output dist/binaries/onebox-linux-x64 \
--target x86_64-unknown-linux-gnu mod.ts
echo " Done: Linux x86_64"
# Linux ARM64
echo "-> Compiling for Linux ARM64..."
deno compile --allow-all --no-check \
--output dist/binaries/onebox-linux-arm64 \
--target aarch64-unknown-linux-gnu mod.ts
echo " Done: Linux ARM64"
# macOS x86_64
echo "-> Compiling for macOS x86_64..."
deno compile --allow-all --no-check \
--output dist/binaries/onebox-macos-x64 \
--target x86_64-apple-darwin mod.ts
echo " Done: macOS x86_64"
# macOS ARM64
echo "-> Compiling for macOS ARM64..."
deno compile --allow-all --no-check \
--output dist/binaries/onebox-macos-arm64 \
--target aarch64-apple-darwin mod.ts
echo " Done: macOS ARM64"
# Windows x86_64
echo "-> Compiling for Windows x86_64..."
deno compile --allow-all --no-check \
--output dist/binaries/onebox-windows-x64.exe \
--target x86_64-pc-windows-msvc mod.ts
echo " Done: Windows x86_64"
echo ""
echo "All binaries compiled successfully!"
ls -lh dist/binaries/
- name: Generate SHA256 checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
cd ../..
- name: Extract changelog for this version
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
# Check if CHANGELOG.md exists
if [ ! -f CHANGELOG.md ] && [ ! -f changelog.md ]; then
echo "No changelog found, using default release notes"
cat > /tmp/release_notes.md << EOF
## Onebox $VERSION
Pre-compiled binaries for multiple platforms.
### Installation
Use the installation script:
\`\`\`bash
curl -sSL https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh | sudo bash
\`\`\`
Or download the binary for your platform and make it executable.
### Supported Platforms
- Linux x86_64 (x64)
- Linux ARM64 (aarch64)
- macOS x86_64 (Intel)
- macOS ARM64 (Apple Silicon)
- Windows x86_64
### Checksums
SHA256 checksums are provided in SHA256SUMS.txt
EOF
else
CHANGELOG_FILE=$([ -f CHANGELOG.md ] && echo "CHANGELOG.md" || echo "changelog.md")
awk "/## \[$VERSION\]/,/## \[/" "$CHANGELOG_FILE" | sed '$d' > /tmp/release_notes.md || cat > /tmp/release_notes.md << EOF
## Onebox $VERSION
See changelog.md for full details.
### Installation
Use the installation script:
\`\`\`bash
curl -sSL https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh | sudo bash
\`\`\`
EOF
fi
echo "Release notes:"
cat /tmp/release_notes.md
- name: Delete existing release if it exists
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Checking for existing release $VERSION..."
# Try to get existing release by tag
EXISTING_RELEASE_ID=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/tags/$VERSION" \
| jq -r '.id // empty')
if [ -n "$EXISTING_RELEASE_ID" ]; then
echo "Found existing release (ID: $EXISTING_RELEASE_ID), deleting..."
curl -X DELETE -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/$EXISTING_RELEASE_ID"
echo "Existing release deleted"
sleep 2
else
echo "No existing release found, proceeding with creation"
fi
- name: Create Gitea Release
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_NOTES=$(cat /tmp/release_notes.md)
# Create the release
echo "Creating release for $VERSION..."
RELEASE_ID=$(curl -X POST -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
"https://code.foss.global/api/v1/repos/serve.zone/onebox/releases" \
-d "{
\"tag_name\": \"$VERSION\",
\"name\": \"Onebox $VERSION\",
\"body\": $(jq -Rs . /tmp/release_notes.md),
\"draft\": false,
\"prerelease\": false
}" | jq -r '.id')
echo "Release created with ID: $RELEASE_ID"
# Upload binaries as release assets
for binary in dist/binaries/*; do
filename=$(basename "$binary")
echo "Uploading $filename..."
curl -X POST -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$binary" \
"https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/$RELEASE_ID/assets?name=$filename"
done
echo "All assets uploaded successfully"
- name: Clean up old releases
run: |
echo "Cleaning up old releases (keeping only last 3)..."
# Fetch all releases sorted by creation date
RELEASES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/serve.zone/onebox/releases" | \
jq -r 'sort_by(.created_at) | reverse | .[3:] | .[].id')
# Delete old releases
if [ -n "$RELEASES" ]; then
echo "Found releases to delete:"
for release_id in $RELEASES; do
echo " Deleting release ID: $release_id"
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/$release_id"
done
echo "Old releases deleted successfully"
else
echo "No old releases to delete (less than 4 releases total)"
fi
echo ""
- name: Release Summary
run: |
echo "================================================"
echo " Release ${{ steps.version.outputs.version }} Complete!"
echo "================================================"
echo ""
echo "Binaries published:"
ls -lh dist/binaries/
echo ""
echo "Release URL:"
echo "https://code.foss.global/serve.zone/onebox/releases/tag/${{ steps.version.outputs.version }}"
echo ""
echo "Installation command:"
echo "curl -sSL https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh | sudo bash"
echo ""

View File

@@ -1,5 +1,43 @@
# Changelog
## 2026-03-15 - 1.12.0 - feat(cli,release)
add self-upgrade command and automate CI, release, and npm publishing workflows
- adds a new `onebox upgrade` CLI command that checks the latest release and reinstalls the current binary via the installer script
- introduces Gitea CI workflows for type checks, build verification, multi-platform binary compilation, release creation, and npm publishing
- adds a reusable release template describing installation options, supported platforms, and checksum availability
## 2026-03-03 - 1.11.0 - feat(services)
map backend service data to UI components, add stats & logs parsing, fetch service stats, and fix logs request param
- Fix: rename service logs request property from 'lines' to 'tail' when calling typedRequest
- Add data transformation helpers: formatBytes, parseImageString, mapStatus, toServiceDetail, toServiceStats, parseLogs
- Transform service list and detail props to match @serve.zone/catalog component interfaces (map status, image, repo/tag, timestamps, registry)
- Dispatch fetchServiceStatsAction on service click and surface transformed stats with default values to avoid nulls
- Parse and normalize logs into timestamp/message pairs for the detail view
## 2026-03-02 - 1.10.3 - fix(bin)
make bin/onebox-wrapper.js executable
- Metadata-only change: file mode updated for bin/onebox-wrapper.js to include the executable bit
- No source or behavior changes to the code
## 2026-03-02 - 1.10.2 - fix(build)
update build/watch configuration, switch to esbuild bundler and tswatch, and bump catalog and tooling dependencies
- Switch watch script to 'tswatch' (replaced previous concurrently command invoking deno + tswatch).
- npmextra.json: set bundler to 'esbuild', enable production mode, include html/index.html in the bundle, and extend watchPatterns to include ./html/**/*.
- Backend watcher: expanded watch globs and changed command to include --unstable-ffi and runtime flags (--ephemeral --monitor); restart and debounce kept.
- Bump runtime deps: @design.estate/dees-catalog -> ^3.43.3, @serve.zone/catalog -> ^2.5.0.
- Bump devDependencies: @git.zone/tsbundle -> ^2.9.0, @git.zone/tswatch -> ^3.2.0.
## 2026-02-24 - 1.10.1 - fix(package.json)
update package metadata
- Single metadata-only file changed (+1 -1)
- No source code or runtime behavior modified; safe patch release
- Current package version is 1.10.0; recommend patch bump to 1.10.1
## 2026-02-24 - 1.10.0 - feat(opsserver)
introduce OpsServer (TypedRequest API) and new lightweight web UI; replace legacy Angular UI and add typed interfaces

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/onebox",
"version": "1.10.0",
"version": "1.12.0",
"exports": "./mod.ts",
"nodeModulesDir": "auto",
"tasks": {

View File

@@ -17,15 +17,17 @@
"from": "./ts_web/index.ts",
"to": "./ts_bundled/bundle.ts",
"outputMode": "base64ts",
"watchPatterns": ["./ts_web/**/*"],
"triggerReload": true
"bundler": "esbuild",
"production": true,
"watchPatterns": ["./ts_web/**/*", "./html/**/*"],
"includeFiles": [{"from": "./html/index.html", "to": "index.html"}]
}
],
"watchers": [
{
"name": "backend",
"watch": "./ts/**/*",
"command": "deno run --allow-all mod.ts server",
"watch": ["./ts/**/*", "./ts_interfaces/**/*", "./ts_bundled/**/*"],
"command": "deno run --allow-all --unstable-ffi mod.ts server --ephemeral --monitor",
"restart": true,
"debounce": 500,
"runOnStart": true

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/onebox",
"version": "1.10.0",
"version": "1.12.0",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts",
"type": "module",
@@ -9,7 +9,7 @@
},
"scripts": {
"postinstall": "node scripts/install-binary.js",
"watch": "concurrently --kill-others --names \"BACKEND,UI\" --prefix-colors \"cyan,magenta\" \"deno run --allow-all --unstable-ffi --watch mod.ts server --ephemeral --monitor\" \"tswatch\"",
"watch": "tswatch",
"build": "tsbundle",
"bundle": "tsbundle"
},
@@ -55,13 +55,12 @@
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"dependencies": {
"@api.global/typedrequest-interfaces": "^3.0.19",
"@design.estate/dees-catalog": "^3.43.0",
"@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6",
"@serve.zone/catalog": "^2.1.0"
"@serve.zone/catalog": "^2.5.0"
},
"devDependencies": {
"@git.zone/tsbundle": "^2.8.4",
"@git.zone/tswatch": "^2.3.13",
"concurrently": "^9.1.2"
"@git.zone/tsbundle": "^2.9.0",
"@git.zone/tswatch": "^3.2.0"
}
}

561
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -72,6 +72,10 @@ export async function runCli(): Promise<void> {
await handleStatusCommand(onebox);
break;
case 'upgrade':
await handleUpgradeCommand();
break;
default:
logger.error(`Unknown command: ${command}`);
printHelp();
@@ -386,6 +390,78 @@ async function handleStatusCommand(onebox: Onebox) {
console.log(JSON.stringify(status, null, 2));
}
// Upgrade command - self-update onebox to latest version
async function handleUpgradeCommand(): Promise<void> {
// Check if running as root
if (Deno.uid() !== 0) {
logger.error('This command must be run as root to upgrade Onebox.');
logger.info('Try: sudo onebox upgrade');
Deno.exit(1);
}
logger.info('Checking for updates...');
try {
// Get current version
const currentVersion = projectInfo.version;
// Fetch latest version from Gitea API
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/latest';
const curlCmd = new Deno.Command('curl', {
args: ['-sSL', apiUrl],
stdout: 'piped',
stderr: 'piped',
});
const curlResult = await curlCmd.output();
const response = new TextDecoder().decode(curlResult.stdout);
const release = JSON.parse(response);
const latestVersion = release.tag_name as string; // e.g., "v1.11.0"
// Normalize versions for comparison (ensure both have "v" prefix)
const normalizedCurrent = currentVersion.startsWith('v')
? currentVersion
: `v${currentVersion}`;
const normalizedLatest = latestVersion.startsWith('v')
? latestVersion
: `v${latestVersion}`;
console.log(` Current version: ${normalizedCurrent}`);
console.log(` Latest version: ${normalizedLatest}`);
console.log('');
// Compare normalized versions
if (normalizedCurrent === normalizedLatest) {
logger.success('Already up to date!');
return;
}
logger.info(`New version available: ${latestVersion}`);
logger.info('Downloading and installing...');
console.log('');
// Download and run the install script
const installUrl = 'https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh';
const installCmd = new Deno.Command('bash', {
args: ['-c', `curl -sSL ${installUrl} | bash`],
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
});
const installResult = await installCmd.output();
if (!installResult.success) {
logger.error('Upgrade failed');
Deno.exit(1);
}
console.log('');
logger.success(`Upgraded to ${latestVersion}`);
} catch (error) {
logger.error(`Upgrade failed: ${getErrorMessage(error)}`);
Deno.exit(1);
}
}
// Helpers
function getArg(args: string[], flag: string): string {
const arg = args.find((a) => a.startsWith(`${flag}=`));
@@ -441,6 +517,9 @@ Commands:
status
upgrade
Upgrade Onebox to the latest version (requires root)
Options:
--help, -h Show this help message
--version, -v Show version

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -381,7 +381,7 @@ export const fetchServiceLogsAction = servicesStatePart.createAction<{
const response = await typedRequest.fire({
identity: context.identity!,
serviceName: dataArg.name,
lines: dataArg.lines || 200,
tail: dataArg.lines || 200,
});
return { ...statePartArg.getState(), currentServiceLogs: response.logs };
} catch (err) {

View File

@@ -1,6 +1,7 @@
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,
@@ -11,6 +12,91 @@ import {
type TemplateResult,
} from '@design.estate/dees-element';
// ============================================================================
// Data transformation helpers
// Maps backend data shapes to @serve.zone/catalog component interfaces
// ============================================================================
function formatBytes(bytes: number): string {
if (!bytes || bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
const value = bytes / Math.pow(k, i);
return `${value.toFixed(1)} ${units[i]}`;
}
function parseImageString(image: string): { repository: string; tag: string } {
const lastColon = image.lastIndexOf(':');
const lastSlash = image.lastIndexOf('/');
if (lastColon > lastSlash && lastColon > 0) {
return {
repository: image.substring(0, lastColon),
tag: image.substring(lastColon + 1),
};
}
return { repository: image, tag: 'latest' };
}
function mapStatus(status: string): 'running' | 'stopped' | 'starting' | 'error' {
switch (status) {
case 'running': return 'running';
case 'starting': return 'starting';
case 'failed': return 'error';
case 'stopped':
case 'stopping':
default: return 'stopped';
}
}
function toServiceDetail(service: interfaces.data.IService) {
const parsed = parseImageString(service.image);
return {
name: service.name,
status: mapStatus(service.status),
image: service.image,
port: service.port,
domain: service.domain || null,
containerId: service.containerID || '',
created: service.createdAt ? new Date(service.createdAt).toLocaleString() : '-',
updated: service.updatedAt ? new Date(service.updatedAt).toLocaleString() : '-',
registry: service.useOneboxRegistry ? 'Onebox Registry' : (service.registry || 'Docker Hub'),
repository: service.registryRepository || parsed.repository,
tag: service.registryImageTag || parsed.tag,
};
}
function toServiceStats(stats: interfaces.data.IContainerStats) {
return {
cpu: stats.cpuPercent,
memory: formatBytes(stats.memoryUsed),
memoryLimit: formatBytes(stats.memoryLimit),
networkIn: formatBytes(stats.networkRx),
networkOut: formatBytes(stats.networkTx),
};
}
function parseLogs(logs: any): Array<{ timestamp: string; message: string }> {
if (Array.isArray(logs)) {
return logs.map((entry: any) => ({
timestamp: entry.timestamp ? String(entry.timestamp) : '',
message: entry.message || String(entry),
}));
}
if (typeof logs === 'string' && logs.trim()) {
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+(.*)/);
if (match) {
return { timestamp: match[1], message: match[2] };
}
return { timestamp: '', message: line };
});
}
return [];
}
const defaultStats = { cpu: 0, memory: '0 B', memoryLimit: '0 B', networkIn: '0 B', networkOut: '0 B' };
@customElement('ob-view-services')
export class ObViewServices extends DeesElement {
@state()
@@ -86,10 +172,16 @@ export class ObViewServices extends DeesElement {
}
private renderListView(): TemplateResult {
const mappedServices = this.servicesState.services.map((s) => ({
name: s.name,
image: s.image,
domain: s.domain || null,
status: mapStatus(s.status),
}));
return html`
<ob-sectionheading>Services</ob-sectionheading>
<sz-services-list-view
.services=${this.servicesState.services}
.services=${mappedServices}
@service-click=${(e: CustomEvent) => {
this.selectedServiceName = e.detail.name || e.detail.service?.name;
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceAction, {
@@ -98,6 +190,9 @@ export class ObViewServices extends DeesElement {
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceLogsAction, {
name: this.selectedServiceName,
});
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceStatsAction, {
name: this.selectedServiceName,
});
this.currentView = 'detail';
}}
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
@@ -124,12 +219,19 @@ export class ObViewServices extends DeesElement {
}
private renderDetailView(): TemplateResult {
const service = this.servicesState.currentService;
const transformedService = service ? toServiceDetail(service) : null;
const transformedStats = this.servicesState.currentServiceStats
? toServiceStats(this.servicesState.currentServiceStats)
: defaultStats;
const transformedLogs = parseLogs(this.servicesState.currentServiceLogs);
return html`
<ob-sectionheading>Service Details</ob-sectionheading>
<sz-service-detail-view
.service=${this.servicesState.currentService}
.logs=${this.servicesState.currentServiceLogs}
.stats=${this.servicesState.currentServiceStats}
.service=${transformedService}
.logs=${transformedLogs}
.stats=${transformedStats}
@back=${() => {
this.currentView = 'list';
}}