Compare commits

..

22 Commits

Author SHA1 Message Date
1b6de75097 v1.17.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 56s
Release / build-and-release (push) Failing after 37s
CI / Build All Platforms (push) Successful in 2m7s
2026-03-16 12:36:02 +00:00
497f8f59a7 feat(web/services): add deploy service action to the services view 2026-03-16 12:36:02 +00:00
0c7d65e4ad v1.16.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 28s
CI / Build Test (Current Platform) (push) Successful in 52s
CI / Build All Platforms (push) Successful in 1m50s
Release / build-and-release (push) Successful in 2m49s
2026-03-16 11:45:56 +00:00
3f2cd074ce feat(services): add platform service navigation and stats in the services UI 2026-03-16 11:45:56 +00:00
59ed7233bd v1.15.3
Some checks failed
CI / Build All Platforms (push) Failing after 3s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 25s
CI / Build Test (Current Platform) (push) Successful in 54s
Release / build-and-release (push) Successful in 2m35s
2026-03-16 11:07:00 +00:00
01e3ba16c4 fix(install): refresh systemd service configuration before restarting previously running installations 2026-03-16 11:07:00 +00:00
f5c1d5fcda v1.15.2
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 1m58s
Release / build-and-release (push) Successful in 3m9s
2026-03-16 10:58:08 +00:00
45b0971f2f fix(systemd): set HOME and DENO_DIR for the systemd service environment 2026-03-16 10:58:08 +00:00
178f440d7e v1.15.1
Some checks failed
CI / Type Check & Lint (push) Failing after 30s
Publish to npm / npm-publish (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m0s
CI / Build All Platforms (push) Successful in 2m8s
Release / build-and-release (push) Successful in 2m53s
2026-03-16 10:23:05 +00:00
7fff15a90c fix(systemd): move Docker installation and swarm initialization to systemd enable flow 2026-03-16 10:23:05 +00:00
69e23f667e v1.15.0
Some checks failed
CI / Build All Platforms (push) Failing after 7s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 26s
CI / Build Test (Current Platform) (push) Successful in 58s
Release / build-and-release (push) Successful in 2m58s
2026-03-16 10:02:59 +00:00
a2bf4df7c2 feat(systemd): replace smartdaemon-based service management with native systemd commands 2026-03-16 10:02:59 +00:00
9e0a0b5a89 v1.14.10
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 59s
CI / Build All Platforms (push) Successful in 2m1s
Release / build-and-release (push) Successful in 2m42s
2026-03-16 08:40:48 +00:00
3a227bd838 fix(services): stop auto-update monitoring during shutdown 2026-03-16 08:40:48 +00:00
f5a7fccfc2 v1.14.9
Some checks failed
CI / Type Check & Lint (push) Failing after 25s
Publish to npm / npm-publish (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 57s
CI / Build All Platforms (push) Successful in 2m20s
Release / build-and-release (push) Successful in 3m50s
2026-03-16 08:25:32 +00:00
a30d2029a5 fix(repo): no changes to commit 2026-03-16 08:25:32 +00:00
88727dd47d v1.14.8
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 45s
CI / Build Test (Current Platform) (push) Successful in 1m19s
CI / Build All Platforms (push) Successful in 2m46s
Release / build-and-release (push) Successful in 5m20s
2026-03-16 03:06:23 +00:00
9a5ed2220e fix(repo): no changes to commit 2026-03-16 03:06:23 +00:00
decd39e7c4 v1.14.7
Some checks failed
CI / Build All Platforms (push) Has been cancelled
CI / Type Check & Lint (push) Has been cancelled
CI / Build Test (Current Platform) (push) Has been cancelled
Publish to npm / npm-publish (push) Failing after 8s
Release / build-and-release (push) Successful in 5m34s
2026-03-16 03:06:17 +00:00
ad2e228208 fix(repo): no changes to commit 2026-03-16 03:06:17 +00:00
cf06019d79 v1.14.6
Some checks failed
CI / Build Test (Current Platform) (push) Has been cancelled
CI / Type Check & Lint (push) Has been cancelled
CI / Build All Platforms (push) Has been cancelled
Publish to npm / npm-publish (push) Failing after 6s
Release / build-and-release (push) Successful in 5m30s
2026-03-16 03:06:08 +00:00
cf44b0047d fix(project): no changes to commit 2026-03-16 03:06:08 +00:00
19 changed files with 581 additions and 234 deletions

View File

@@ -1,5 +1,68 @@
# Changelog
## 2026-03-16 - 1.17.0 - feat(web/services)
add deploy service action to the services view
- Adds a prominent "Deploy Service" button to the services page header.
- Routes users into the create service view directly from the services listing.
- Includes a new service creation form screenshot asset for the updated interface.
## 2026-03-16 - 1.16.0 - feat(services)
add platform service navigation and stats in the services UI
- add platform service stats state and fetch action
- show platform services in the services list and open a platform detail view
- enable dashboard clicks to jump directly to the selected platform service
- refresh platform service stats after start and restart actions
- bump @serve.zone/catalog to ^2.6.0 for the new platform service UI components
## 2026-03-16 - 1.15.3 - fix(install)
refresh systemd service configuration before restarting previously running installations
- Re-enable the systemd service during updates so unit file changes are applied before restart
- Add a log message indicating the service configuration is being refreshed
## 2026-03-16 - 1.15.2 - fix(systemd)
set HOME and DENO_DIR for the systemd service environment
- Adds HOME=/root to the generated onebox systemd unit
- Adds DENO_DIR=/root/.cache/deno so Deno cache paths are available when running as a service
## 2026-03-16 - 1.15.1 - fix(systemd)
move Docker installation and swarm initialization to systemd enable flow
- Ensures Docker is installed before writing and enabling the systemd unit that depends on docker.service.
- Removes Docker auto-installation from Onebox initialization so setup happens in the service management path.
## 2026-03-16 - 1.15.0 - feat(systemd)
replace smartdaemon-based service management with native systemd commands
- adds a dedicated OneboxSystemd manager for enabling, disabling, starting, stopping, checking status, and following logs
- introduces a new `onebox systemd` CLI command set and updates install and help output to use it
- removes the smartdaemon dependency and related service management code
## 2026-03-16 - 1.14.10 - fix(services)
stop auto-update monitoring during shutdown
- Track the auto-update polling interval in the services manager
- Clear the auto-update interval when Onebox shuts down to prevent background checks after shutdown
## 2026-03-16 - 1.14.9 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.8 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.7 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.6 - fix(project)
no changes to commit
## 2026-03-16 - 1.14.5 - fix(onebox)
move Docker auto-install and swarm initialization into Onebox startup flow

BIN
create-service-form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

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

View File

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

View File

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

10
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import { OneboxReverseProxy } from './reverseproxy.ts';
import { OneboxDnsManager } from './dns.ts';
import { OneboxSslManager } from './ssl.ts';
import { OneboxDaemon } from './daemon.ts';
import { OneboxSystemd } from './systemd.ts';
import { OneboxHttpServer } from './httpserver.ts';
import { CloudflareDomainSync } from './cloudflare-sync.ts';
import { CertRequirementManager } from './cert-requirement-manager.ts';
@@ -33,6 +34,7 @@ export class Onebox {
public dns: OneboxDnsManager;
public ssl: OneboxSslManager;
public daemon: OneboxDaemon;
public systemd: OneboxSystemd;
public httpServer: OneboxHttpServer;
public cloudflareDomainSync: CloudflareDomainSync;
public certRequirementManager: CertRequirementManager;
@@ -57,6 +59,7 @@ export class Onebox {
this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this);
this.systemd = new OneboxSystemd();
this.httpServer = new OneboxHttpServer(this);
this.registry = new RegistryManager({
dataDir: './.nogit/registry-data',
@@ -97,9 +100,6 @@ export class Onebox {
// Ensure default admin user exists
await this.ensureDefaultUser();
// Ensure Docker is installed (auto-install on fresh servers)
await this.ensureDocker();
// Initialize Docker
await this.docker.init();
@@ -224,59 +224,6 @@ export class Onebox {
}
}
/**
* Ensure Docker is installed, installing it if necessary
*/
private async ensureDocker(): Promise<void> {
try {
const cmd = new Deno.Command('docker', {
args: ['--version'],
stdout: 'piped',
stderr: 'piped',
});
const result = await cmd.output();
if (result.success) {
const version = new TextDecoder().decode(result.stdout).trim();
logger.info(`Docker found: ${version}`);
return;
}
} catch {
// docker command not found
}
logger.info('Docker not found. Installing Docker...');
const installCmd = new Deno.Command('bash', {
args: ['-c', 'curl -fsSL https://get.docker.com | sh'],
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
});
const installResult = await installCmd.output();
if (!installResult.success) {
throw new Error('Failed to install Docker. Please install it manually: curl -fsSL https://get.docker.com | sh');
}
logger.success('Docker installed successfully');
// Initialize Docker Swarm
logger.info('Initializing Docker Swarm...');
const swarmCmd = new Deno.Command('docker', {
args: ['swarm', 'init'],
stdout: 'piped',
stderr: 'piped',
});
const swarmResult = await swarmCmd.output();
if (swarmResult.success) {
logger.success('Docker Swarm initialized');
} else {
const stderr = new TextDecoder().decode(swarmResult.stderr);
if (stderr.includes('already part of a swarm')) {
logger.info('Docker Swarm already initialized');
} else {
logger.warn(`Docker Swarm init warning: ${stderr.trim()}`);
}
}
}
/**
* Check if Onebox is initialized
*/
@@ -376,20 +323,6 @@ export class Onebox {
}
}
/**
* Start daemon mode
*/
async startDaemon(): Promise<void> {
await this.daemon.start();
}
/**
* Stop daemon mode
*/
async stopDaemon(): Promise<void> {
await this.daemon.stop();
}
/**
* Start OpsServer (TypedRequest-based, serves new UI)
*/
@@ -411,6 +344,9 @@ export class Onebox {
try {
logger.info('Shutting down Onebox...');
// Stop auto-update monitoring
this.services.stopAutoUpdateMonitoring();
// Stop backup scheduler
await this.backupScheduler.stop();

View File

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

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

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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