feat(hostedapp): add hosted app lifecycle protocol support
This commit is contained in:
@@ -2,6 +2,20 @@
|
||||
|
||||
## Pending
|
||||
|
||||
- add hosted app lifecycle protocol support (hostedapp)
|
||||
- Injects generic hosted-app runtime identity environment variables into managed services.
|
||||
- Adds token-scoped Hosted App TypedRequest handlers for lifecycle reporting, bootstrap setup, and managed self-upgrades.
|
||||
- Injects a Docker host alias so hosted apps can reach the Onebox runtime URL from managed containers.
|
||||
- Shows hosted-app bootstrap and upgrade state in the service detail view with a mark-done cleanup action.
|
||||
|
||||
### Features
|
||||
|
||||
- add hosted app lifecycle protocol support (hostedapp)
|
||||
- Inject hosted app runtime identity environment variables into managed services and persist lifecycle records.
|
||||
- Add token-scoped TypedRequest handlers for lifecycle reporting, bootstrap actions, and managed app upgrades.
|
||||
- Show hosted app bootstrap and upgrade state in the service detail view with a mark-done cleanup action.
|
||||
- Configure Docker host gateway aliases so managed containers can reach the Onebox runtime URL.
|
||||
- Bump @serve.zone/interfaces to ^6.1.0.
|
||||
|
||||
## 2026-05-25 - 2.1.3
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.2",
|
||||
"@api.global/typedsocket": "npm:@api.global/typedsocket@^4.1.3",
|
||||
"@serve.zone/containerarchive": "npm:@serve.zone/containerarchive@^0.1.3",
|
||||
"@serve.zone/interfaces": "npm:@serve.zone/interfaces@^6.0.0",
|
||||
"@serve.zone/interfaces": "npm:@serve.zone/interfaces@^6.1.0",
|
||||
"@serve.zone/appstore": "npm:@serve.zone/appstore@^0.2.0"
|
||||
},
|
||||
"compilerOptions": {
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@serve.zone/appstore": "^0.2.0",
|
||||
"@serve.zone/catalog": "^2.12.6",
|
||||
"@serve.zone/interfaces": "^6.0.0"
|
||||
"@serve.zone/interfaces": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbundle": "^2.10.4",
|
||||
|
||||
Generated
+6
-6
@@ -27,8 +27,8 @@ importers:
|
||||
specifier: ^2.12.6
|
||||
version: 2.12.6(@tiptap/pm@2.27.2)
|
||||
'@serve.zone/interfaces':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
devDependencies:
|
||||
'@git.zone/tsbundle':
|
||||
specifier: ^2.10.4
|
||||
@@ -986,8 +986,8 @@ packages:
|
||||
'@serve.zone/catalog@2.12.6':
|
||||
resolution: {integrity: sha512-FjieZNCHTCHufMre8OSP8bFP9L4DPL9yNtd7UMwD1yQ8wublgAq6eWrx6Tfb+3k8Hyof33BBt4rbFyrvIEBk+A==}
|
||||
|
||||
'@serve.zone/interfaces@6.0.0':
|
||||
resolution: {integrity: sha512-nCidhOH0XlX+7e6xaJDq6fwnwaWasB/4w2LHkV7A96G+m+7EXZqbbaKSboUlaiGDly0dWNajk2FrYFo64ZucPA==}
|
||||
'@serve.zone/interfaces@6.1.0':
|
||||
resolution: {integrity: sha512-nhxMmMfemBaGM1xxFpbNM8/zPM4Y59mVsgz9XBNGZr6n7kn81QsY+Xcn5HnLywztuGHqgEZRWGmI4MPzORRktw==}
|
||||
|
||||
'@tempfix/lenis@1.3.20':
|
||||
resolution: {integrity: sha512-ypeB0FuHLHOCQXW4d0RQ69txPJJH+1CHcpsZIUdcv2t1vR0IVyQr2vHihtde9UOXhjzqEnUphWon/UcJNsa0YA==}
|
||||
@@ -3571,7 +3571,7 @@ snapshots:
|
||||
|
||||
'@serve.zone/appstore@0.2.0':
|
||||
dependencies:
|
||||
'@serve.zone/interfaces': 6.0.0
|
||||
'@serve.zone/interfaces': 6.1.0
|
||||
|
||||
'@serve.zone/catalog@2.12.6(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
@@ -3586,7 +3586,7 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@serve.zone/interfaces@6.0.0':
|
||||
'@serve.zone/interfaces@6.1.0':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
|
||||
@@ -429,6 +429,7 @@ export class OneboxDockerManager {
|
||||
},
|
||||
PortBindings: portConfig.portBindings,
|
||||
Binds: this.getStandaloneVolumeBinds(service),
|
||||
ExtraHosts: ['host.docker.internal:host-gateway'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -489,6 +490,7 @@ export class OneboxDockerManager {
|
||||
Image: fullImage,
|
||||
Env: env,
|
||||
Mounts: this.getSwarmVolumeMounts(service),
|
||||
Hosts: ['host.docker.internal:host-gateway'],
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
'onebox-service': service.name,
|
||||
|
||||
+52
-3
@@ -72,6 +72,35 @@ export class OneboxServicesManager {
|
||||
}
|
||||
}
|
||||
|
||||
private createHostedAppControlToken(): string {
|
||||
const bytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(bytes);
|
||||
return Array.from(bytes).map((byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
private createHostedAppRuntimeEnvVars(): {
|
||||
appInstanceId: string;
|
||||
appControlToken: string;
|
||||
envVars: Record<string, string>;
|
||||
} {
|
||||
const appInstanceId = crypto.randomUUID();
|
||||
const appControlToken = this.createHostedAppControlToken();
|
||||
const runtimeUrl =
|
||||
Deno.env.get('SERVEZONE_RUNTIME_URL') ||
|
||||
Deno.env.get('ONEBOX_RUNTIME_URL') ||
|
||||
'http://host.docker.internal:3000';
|
||||
return {
|
||||
appInstanceId,
|
||||
appControlToken,
|
||||
envVars: {
|
||||
SERVEZONE_RUNTIME_URL: runtimeUrl,
|
||||
SERVEZONE_APP_INSTANCE_ID: appInstanceId,
|
||||
SERVEZONE_APP_CONTROL_TOKEN: appControlToken,
|
||||
SERVEZONE_APP_HOST_TYPE: 'onebox',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a new service (full workflow)
|
||||
*/
|
||||
@@ -109,12 +138,18 @@ export class OneboxServicesManager {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const hostedAppRuntime = this.createHostedAppRuntimeEnvVars();
|
||||
const requestedEnvVars = {
|
||||
...(options.envVars || {}),
|
||||
...hostedAppRuntime.envVars,
|
||||
};
|
||||
|
||||
// Create service record in database
|
||||
const service = await this.database.createService({
|
||||
name: options.name,
|
||||
image: options.useOneboxRegistry ? imageToPull : options.image,
|
||||
registry: options.registry,
|
||||
envVars: options.envVars || {},
|
||||
envVars: requestedEnvVars,
|
||||
volumes: options.volumes || [],
|
||||
publishedPorts: options.publishedPorts || [],
|
||||
port: options.port,
|
||||
@@ -135,6 +170,20 @@ export class OneboxServicesManager {
|
||||
appTemplateVersion: options.appTemplateVersion,
|
||||
});
|
||||
|
||||
this.database.upsertHostedAppLifecycle({
|
||||
serviceId: service.id!,
|
||||
appInstanceId: hostedAppRuntime.appInstanceId,
|
||||
controlToken: hostedAppRuntime.appControlToken,
|
||||
hostType: 'onebox',
|
||||
state: {
|
||||
appInstanceId: hostedAppRuntime.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
appName: options.name,
|
||||
publicUrl: options.domain ? `https://${options.domain}` : undefined,
|
||||
runtimeStatus: 'unknown',
|
||||
},
|
||||
});
|
||||
|
||||
// Provision platform resources if needed
|
||||
let platformEnvVars: Record<string, string> = {};
|
||||
if (platformRequirements) {
|
||||
@@ -151,8 +200,8 @@ export class OneboxServicesManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge platform env vars with user-specified env vars (user vars take precedence)
|
||||
const mergedEnvVars = { ...platformEnvVars, ...(options.envVars || {}) };
|
||||
// Merge platform and app env vars, with host lifecycle values kept authoritative.
|
||||
const mergedEnvVars = { ...platformEnvVars, ...requestedEnvVars };
|
||||
this.resolveEnvVarTemplates(mergedEnvVars, {
|
||||
...platformEnvVars,
|
||||
SERVICE_NAME: options.name,
|
||||
|
||||
@@ -21,6 +21,8 @@ import type {
|
||||
IBackup,
|
||||
IBackupSchedule,
|
||||
IBackupScheduleUpdate,
|
||||
IHostedAppLifecycleRecord,
|
||||
IHostedAppLifecycleState,
|
||||
} from '../types.ts';
|
||||
import type { TBindValue } from './types.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
@@ -116,6 +118,20 @@ export class OneboxDatabase {
|
||||
)
|
||||
`);
|
||||
|
||||
this.query(`
|
||||
CREATE TABLE IF NOT EXISTS hosted_app_lifecycle (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
service_id INTEGER NOT NULL UNIQUE,
|
||||
app_instance_id TEXT NOT NULL UNIQUE,
|
||||
control_token TEXT NOT NULL,
|
||||
host_type TEXT NOT NULL,
|
||||
state_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Registries table
|
||||
this.query(`
|
||||
CREATE TABLE IF NOT EXISTS registries (
|
||||
@@ -316,6 +332,85 @@ export class OneboxDatabase {
|
||||
this.serviceRepo.delete(id);
|
||||
}
|
||||
|
||||
// ============ Hosted App Lifecycle ============
|
||||
|
||||
upsertHostedAppLifecycle(recordArg: Omit<IHostedAppLifecycleRecord, 'id' | 'createdAt' | 'updatedAt'>): IHostedAppLifecycleRecord {
|
||||
const existing = this.getHostedAppLifecycleByServiceId(recordArg.serviceId);
|
||||
const now = Date.now();
|
||||
if (existing) {
|
||||
this.query(
|
||||
`UPDATE hosted_app_lifecycle
|
||||
SET app_instance_id = ?, control_token = ?, host_type = ?, state_json = ?, updated_at = ?
|
||||
WHERE service_id = ?`,
|
||||
[
|
||||
recordArg.appInstanceId,
|
||||
recordArg.controlToken,
|
||||
recordArg.hostType,
|
||||
JSON.stringify(recordArg.state),
|
||||
now,
|
||||
recordArg.serviceId,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
this.query(
|
||||
`INSERT INTO hosted_app_lifecycle (
|
||||
service_id, app_instance_id, control_token, host_type, state_json, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
recordArg.serviceId,
|
||||
recordArg.appInstanceId,
|
||||
recordArg.controlToken,
|
||||
recordArg.hostType,
|
||||
JSON.stringify(recordArg.state),
|
||||
now,
|
||||
now,
|
||||
],
|
||||
);
|
||||
}
|
||||
return this.getHostedAppLifecycleByServiceId(recordArg.serviceId)!;
|
||||
}
|
||||
|
||||
getHostedAppLifecycleByServiceId(serviceIdArg: number): IHostedAppLifecycleRecord | null {
|
||||
const rows = this.query('SELECT * FROM hosted_app_lifecycle WHERE service_id = ?', [serviceIdArg]);
|
||||
return rows[0] ? this.rowToHostedAppLifecycle(rows[0]) : null;
|
||||
}
|
||||
|
||||
getHostedAppLifecycleByInstanceId(appInstanceIdArg: string): IHostedAppLifecycleRecord | null {
|
||||
const rows = this.query('SELECT * FROM hosted_app_lifecycle WHERE app_instance_id = ?', [appInstanceIdArg]);
|
||||
return rows[0] ? this.rowToHostedAppLifecycle(rows[0]) : null;
|
||||
}
|
||||
|
||||
updateHostedAppLifecycleState(serviceIdArg: number, stateArg: IHostedAppLifecycleState): IHostedAppLifecycleRecord {
|
||||
this.query(
|
||||
'UPDATE hosted_app_lifecycle SET state_json = ?, updated_at = ? WHERE service_id = ?',
|
||||
[JSON.stringify(stateArg), Date.now(), serviceIdArg],
|
||||
);
|
||||
return this.getHostedAppLifecycleByServiceId(serviceIdArg)!;
|
||||
}
|
||||
|
||||
private rowToHostedAppLifecycle(rowArg: Record<string, unknown>): IHostedAppLifecycleRecord {
|
||||
let state: IHostedAppLifecycleState;
|
||||
try {
|
||||
state = JSON.parse(String(rowArg.state_json || '{}'));
|
||||
} catch {
|
||||
state = {
|
||||
appInstanceId: String(rowArg.app_instance_id),
|
||||
hostType: String(rowArg.host_type),
|
||||
runtimeStatus: 'unknown',
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: Number(rowArg.id),
|
||||
serviceId: Number(rowArg.service_id),
|
||||
appInstanceId: String(rowArg.app_instance_id),
|
||||
controlToken: String(rowArg.control_token),
|
||||
hostType: 'onebox',
|
||||
state,
|
||||
createdAt: Number(rowArg.created_at),
|
||||
updatedAt: Number(rowArg.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Registries CRUD (delegated to repository) ============
|
||||
|
||||
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import type { TQueryFunction } from '../types.ts';
|
||||
|
||||
export class Migration018HostedAppLifecycle extends BaseMigration {
|
||||
readonly version = 18;
|
||||
readonly description = 'Add hosted app lifecycle state';
|
||||
|
||||
up(query: TQueryFunction): void {
|
||||
query(`
|
||||
CREATE TABLE IF NOT EXISTS hosted_app_lifecycle (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
service_id INTEGER NOT NULL UNIQUE,
|
||||
app_instance_id TEXT NOT NULL UNIQUE,
|
||||
control_token TEXT NOT NULL,
|
||||
host_type TEXT NOT NULL,
|
||||
state_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { Migration014ContainerArchive } from './migration-014-containerarchive.t
|
||||
import { Migration015SmartProxyPlatformService } from './migration-015-smartproxy-platform-service.ts';
|
||||
import { Migration016ServiceVolumes } from './migration-016-service-volumes.ts';
|
||||
import { Migration017ServicePublishedPorts } from './migration-017-service-published-ports.ts';
|
||||
import { Migration018HostedAppLifecycle } from './migration-018-hosted-app-lifecycle.ts';
|
||||
import type { BaseMigration } from './base-migration.ts';
|
||||
|
||||
export class MigrationRunner {
|
||||
@@ -52,6 +53,7 @@ export class MigrationRunner {
|
||||
new Migration015SmartProxyPlatformService(),
|
||||
new Migration016ServiceVolumes(),
|
||||
new Migration017ServicePublishedPorts(),
|
||||
new Migration018HostedAppLifecycle(),
|
||||
].sort((a, b) => a.version - b.version);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export class OpsServer {
|
||||
public logsHandler!: handlers.LogsHandler;
|
||||
public workspaceHandler!: handlers.WorkspaceHandler;
|
||||
public appStoreHandler!: handlers.AppStoreHandler;
|
||||
public hostedAppHandler!: handlers.HostedAppHandler;
|
||||
|
||||
constructor(oneboxRef: Onebox) {
|
||||
this.oneboxRef = oneboxRef;
|
||||
@@ -71,6 +72,7 @@ export class OpsServer {
|
||||
this.logsHandler = new handlers.LogsHandler(this);
|
||||
this.workspaceHandler = new handlers.WorkspaceHandler(this);
|
||||
this.appStoreHandler = new handlers.AppStoreHandler(this);
|
||||
this.hostedAppHandler = new handlers.HostedAppHandler(this);
|
||||
|
||||
logger.success('OpsServer TypedRequest handlers initialized');
|
||||
}
|
||||
|
||||
@@ -23,6 +23,16 @@ export class AppStoreHandler {
|
||||
.slice(0, 25);
|
||||
}
|
||||
|
||||
public getUpgradeOperationsForService(serviceNameArg: string): IAppStoreUpgradeOperation[] {
|
||||
return this.getUpgradeOperations().filter((operationArg) => operationArg.serviceName === serviceNameArg);
|
||||
}
|
||||
|
||||
public async startHostedAppUpgrade(serviceNameArg: string, targetVersionArg: string): Promise<IAppStoreUpgradeOperation> {
|
||||
const operation = await this.createUpgradeOperation(serviceNameArg, targetVersionArg);
|
||||
void this.performUpgrade(operation.id).catch(() => {});
|
||||
return operation;
|
||||
}
|
||||
|
||||
private getRunningUpgrade(serviceNameArg: string): IAppStoreUpgradeOperation | null {
|
||||
for (const operation of this.upgradeOperations.values()) {
|
||||
if (operation.serviceName === serviceNameArg && operation.status === 'running') {
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import * as plugins from '../../plugins.ts';
|
||||
import type { OpsServer } from '../classes.opsserver.ts';
|
||||
import * as interfaces from '../../../ts_interfaces/index.ts';
|
||||
import { requireAdminIdentity } from '../helpers/guards.ts';
|
||||
|
||||
type IHostedAppLifecycleRecord = import('../../types.ts').IHostedAppLifecycleRecord;
|
||||
type IService = import('../../types.ts').IService;
|
||||
type IHostedAppLifecycleState = interfaces.data.IHostedAppLifecycleState;
|
||||
type IHostedAppUpgradeState = interfaces.data.IHostedAppUpgradeState;
|
||||
|
||||
export class HostedAppHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private async requireHostedAppIdentity(
|
||||
identityArg: import('@serve.zone/interfaces').data.IHostedAppRuntimeIdentity,
|
||||
): Promise<{ record: IHostedAppLifecycleRecord; service: IService }> {
|
||||
const record = identityArg?.appInstanceId
|
||||
? this.opsServerRef.oneboxRef.database.getHostedAppLifecycleByInstanceId(identityArg.appInstanceId)
|
||||
: null;
|
||||
if (!record || record.controlToken !== identityArg?.appControlToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Hosted app identity is invalid');
|
||||
}
|
||||
const service = this.opsServerRef.oneboxRef.database.getServiceByID(record.serviceId);
|
||||
if (!service) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Hosted app service not found');
|
||||
}
|
||||
return { record, service };
|
||||
}
|
||||
|
||||
private async getUpgradeState(serviceArg: IService): Promise<IHostedAppUpgradeState> {
|
||||
const latestOperation = this.opsServerRef.appStoreHandler.getUpgradeOperationsForService(serviceArg.name)[0];
|
||||
if (latestOperation) {
|
||||
return {
|
||||
status: latestOperation.status === 'running' ? 'running' : latestOperation.status,
|
||||
appTemplateId: latestOperation.appTemplateId,
|
||||
currentVersion: latestOperation.fromVersion,
|
||||
targetVersion: latestOperation.targetVersion,
|
||||
operationId: latestOperation.id,
|
||||
warnings: latestOperation.warnings,
|
||||
error: latestOperation.error,
|
||||
startedAt: latestOperation.startedAt,
|
||||
updatedAt: latestOperation.updatedAt,
|
||||
completedAt: latestOperation.completedAt,
|
||||
};
|
||||
}
|
||||
|
||||
if (!serviceArg.appTemplateId || !serviceArg.appTemplateVersion) {
|
||||
return { status: 'unknown' };
|
||||
}
|
||||
|
||||
const upgradeableServices = await this.opsServerRef.oneboxRef.appStore.getUpgradeableAppStoreServices();
|
||||
const upgradeable = upgradeableServices.find((serviceArg2) => serviceArg2.serviceName === serviceArg.name);
|
||||
if (!upgradeable) {
|
||||
return {
|
||||
status: 'upToDate',
|
||||
appTemplateId: serviceArg.appTemplateId,
|
||||
currentVersion: serviceArg.appTemplateVersion,
|
||||
latestVersion: serviceArg.appTemplateVersion,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'available',
|
||||
appTemplateId: upgradeable.appTemplateId,
|
||||
currentVersion: upgradeable.currentVersion,
|
||||
latestVersion: upgradeable.latestVersion,
|
||||
targetVersion: upgradeable.latestVersion,
|
||||
};
|
||||
}
|
||||
|
||||
private async getLifecycleState(recordArg: IHostedAppLifecycleRecord, serviceArg: IService): Promise<IHostedAppLifecycleState> {
|
||||
const upgradeState = await this.getUpgradeState(serviceArg);
|
||||
const state: IHostedAppLifecycleState = {
|
||||
...recordArg.state,
|
||||
appInstanceId: recordArg.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
appName: recordArg.state.appName || serviceArg.name,
|
||||
publicUrl: recordArg.state.publicUrl || (serviceArg.domain ? `https://${serviceArg.domain}` : undefined),
|
||||
upgradeState,
|
||||
};
|
||||
this.opsServerRef.oneboxRef.database.updateHostedAppLifecycleState(recordArg.serviceId, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
private async getLifecycleStateByServiceName(serviceNameArg: string): Promise<IHostedAppLifecycleState | undefined> {
|
||||
const service = this.opsServerRef.oneboxRef.database.getServiceByName(serviceNameArg);
|
||||
if (!service?.id) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Service not found');
|
||||
}
|
||||
const record = this.opsServerRef.oneboxRef.database.getHostedAppLifecycleByServiceId(service.id);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
return await this.getLifecycleState(record, service);
|
||||
}
|
||||
|
||||
private async updateLifecycleState(
|
||||
recordArg: IHostedAppLifecycleRecord,
|
||||
serviceArg: IService,
|
||||
stateArg: IHostedAppLifecycleState,
|
||||
): Promise<IHostedAppLifecycleState> {
|
||||
const record = this.opsServerRef.oneboxRef.database.updateHostedAppLifecycleState(recordArg.serviceId, stateArg);
|
||||
return await this.getLifecycleState(record, serviceArg);
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_HostedApp_ReportLifecycleState>(
|
||||
'hostedAppReportLifecycleState',
|
||||
async (dataArg) => {
|
||||
const { record, service } = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const state = await this.updateLifecycleState(record, service, {
|
||||
...record.state,
|
||||
...dataArg.report,
|
||||
appInstanceId: record.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
reportedAt: Date.now(),
|
||||
});
|
||||
return { state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_HostedApp_GetLifecycleState>(
|
||||
'hostedAppGetLifecycleState',
|
||||
async (dataArg) => {
|
||||
const { record, service } = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
return { state: await this.getLifecycleState(record, service) };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_HostedApp_RequestBootstrapAction>(
|
||||
'hostedAppRequestBootstrapAction',
|
||||
async (dataArg) => {
|
||||
const { record, service } = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const now = Date.now();
|
||||
const action = {
|
||||
...dataArg.action,
|
||||
id: dataArg.action.id || crypto.randomUUID(),
|
||||
status: 'ready' as const,
|
||||
label: dataArg.action.label || 'Initial setup',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
const state = await this.updateLifecycleState(record, service, {
|
||||
...record.state,
|
||||
appInstanceId: record.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
runtimeStatus: 'setupRequired',
|
||||
bootstrapAction: action,
|
||||
reportedAt: now,
|
||||
});
|
||||
return { action, state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_HostedApp_CompleteBootstrapAction>(
|
||||
'hostedAppCompleteBootstrapAction',
|
||||
async (dataArg) => {
|
||||
const { record, service } = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const now = Date.now();
|
||||
const existingAction = record.state.bootstrapAction;
|
||||
const bootstrapAction = existingAction
|
||||
? {
|
||||
...existingAction,
|
||||
id: dataArg.actionId || existingAction.id,
|
||||
status: 'completed' as const,
|
||||
message: dataArg.message || existingAction.message,
|
||||
updatedAt: now,
|
||||
completedAt: now,
|
||||
}
|
||||
: undefined;
|
||||
const state = await this.updateLifecycleState(record, service, {
|
||||
...record.state,
|
||||
appInstanceId: record.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
runtimeStatus: record.state.runtimeStatus === 'setupRequired' ? 'running' : record.state.runtimeStatus,
|
||||
bootstrapAction,
|
||||
reportedAt: now,
|
||||
});
|
||||
return { state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_HostedApp_StartManagedUpgrade>(
|
||||
'hostedAppStartManagedUpgrade',
|
||||
async (dataArg) => {
|
||||
const { record, service } = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const upgradeState = await this.getUpgradeState(service);
|
||||
const targetVersion = dataArg.targetVersion || upgradeState.targetVersion || upgradeState.latestVersion;
|
||||
if (!targetVersion) {
|
||||
throw new plugins.typedrequest.TypedResponseError('No managed upgrade target is available');
|
||||
}
|
||||
const operation = await this.opsServerRef.appStoreHandler.startHostedAppUpgrade(service.name, targetVersion);
|
||||
const nextUpgradeState: IHostedAppUpgradeState = {
|
||||
status: 'running',
|
||||
appTemplateId: operation.appTemplateId,
|
||||
currentVersion: operation.fromVersion,
|
||||
targetVersion: operation.targetVersion,
|
||||
operationId: operation.id,
|
||||
warnings: operation.warnings,
|
||||
startedAt: operation.startedAt,
|
||||
updatedAt: operation.updatedAt,
|
||||
};
|
||||
const state = await this.updateLifecycleState(record, service, {
|
||||
...record.state,
|
||||
appInstanceId: record.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
upgradeState: nextUpgradeState,
|
||||
reportedAt: Date.now(),
|
||||
});
|
||||
return { upgradeState: nextUpgradeState, state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_HostedApp_GetManagedUpgradeStatus>(
|
||||
'hostedAppGetManagedUpgradeStatus',
|
||||
async (dataArg) => {
|
||||
const { service } = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
return { upgradeState: await this.getUpgradeState(service) };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHostedAppLifecycleForService>(
|
||||
'getHostedAppLifecycleForService',
|
||||
async (dataArg) => {
|
||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||
return { state: await this.getLifecycleStateByServiceName(dataArg.serviceName) };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DismissHostedAppBootstrapAction>(
|
||||
'dismissHostedAppBootstrapAction',
|
||||
async (dataArg) => {
|
||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||
const service = this.opsServerRef.oneboxRef.database.getServiceByName(dataArg.serviceName);
|
||||
if (!service?.id) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Service not found');
|
||||
}
|
||||
const record = this.opsServerRef.oneboxRef.database.getHostedAppLifecycleByServiceId(service.id);
|
||||
if (!record) {
|
||||
return { state: undefined };
|
||||
}
|
||||
const nextState = {
|
||||
...record.state,
|
||||
appInstanceId: record.appInstanceId,
|
||||
hostType: 'onebox',
|
||||
bootstrapAction: undefined,
|
||||
};
|
||||
return { state: await this.updateLifecycleState(record, service, nextState) };
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@ export * from './managed-dcrouter.handler.ts';
|
||||
export * from './logs.handler.ts';
|
||||
export * from './workspace.handler.ts';
|
||||
export * from './appstore.handler.ts';
|
||||
export * from './hostedapp.handler.ts';
|
||||
|
||||
+15
@@ -2,6 +2,8 @@
|
||||
* Type definitions for Onebox
|
||||
*/
|
||||
|
||||
import type * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||
|
||||
// Service types
|
||||
export interface IService {
|
||||
id?: number;
|
||||
@@ -32,6 +34,19 @@ export interface IService {
|
||||
appTemplateVersion?: string;
|
||||
}
|
||||
|
||||
export type IHostedAppLifecycleState = servezoneInterfaces.data.IHostedAppLifecycleState;
|
||||
|
||||
export interface IHostedAppLifecycleRecord {
|
||||
id?: number;
|
||||
serviceId: number;
|
||||
appInstanceId: string;
|
||||
controlToken: string;
|
||||
hostType: 'onebox';
|
||||
state: IHostedAppLifecycleState;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface IServiceVolume {
|
||||
name?: string;
|
||||
source?: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
import type * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||
|
||||
export type IHostedAppLifecycleState = servezoneInterfaces.data.IHostedAppLifecycleState;
|
||||
export type IHostedAppBootstrapAction = servezoneInterfaces.data.IHostedAppBootstrapAction;
|
||||
export type IHostedAppUpgradeState = servezoneInterfaces.data.IHostedAppUpgradeState;
|
||||
@@ -3,6 +3,7 @@ export * from './service.ts';
|
||||
export * from './platform.ts';
|
||||
export * from './network.ts';
|
||||
export * from './domain.ts';
|
||||
export * from './hostedapp.ts';
|
||||
export * from './registry.ts';
|
||||
export * from './backup.ts';
|
||||
export * from './settings.ts';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||
import * as plugins from '../plugins.ts';
|
||||
import * as data from '../data/index.ts';
|
||||
|
||||
export type IReq_HostedApp_ReportLifecycleState = servezoneInterfaces.requests.hostedapp.IReq_HostedApp_ReportLifecycleState;
|
||||
export type IReq_HostedApp_GetLifecycleState = servezoneInterfaces.requests.hostedapp.IReq_HostedApp_GetLifecycleState;
|
||||
export type IReq_HostedApp_RequestBootstrapAction = servezoneInterfaces.requests.hostedapp.IReq_HostedApp_RequestBootstrapAction;
|
||||
export type IReq_HostedApp_CompleteBootstrapAction = servezoneInterfaces.requests.hostedapp.IReq_HostedApp_CompleteBootstrapAction;
|
||||
export type IReq_HostedApp_StartManagedUpgrade = servezoneInterfaces.requests.hostedapp.IReq_HostedApp_StartManagedUpgrade;
|
||||
export type IReq_HostedApp_GetManagedUpgradeStatus = servezoneInterfaces.requests.hostedapp.IReq_HostedApp_GetManagedUpgradeStatus;
|
||||
|
||||
export interface IReq_GetHostedAppLifecycleForService extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetHostedAppLifecycleForService
|
||||
> {
|
||||
method: 'getHostedAppLifecycleForService';
|
||||
request: {
|
||||
identity: data.IIdentity;
|
||||
serviceName: string;
|
||||
};
|
||||
response: {
|
||||
state?: data.IHostedAppLifecycleState;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DismissHostedAppBootstrapAction extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DismissHostedAppBootstrapAction
|
||||
> {
|
||||
method: 'dismissHostedAppBootstrapAction';
|
||||
request: {
|
||||
identity: data.IIdentity;
|
||||
serviceName: string;
|
||||
};
|
||||
response: {
|
||||
state?: data.IHostedAppLifecycleState;
|
||||
};
|
||||
}
|
||||
@@ -13,3 +13,4 @@ export * from './settings.ts';
|
||||
export * from './logs.ts';
|
||||
export * from './workspace.ts';
|
||||
export * from './appstore.ts';
|
||||
export * from './hostedapp.ts';
|
||||
|
||||
+46
-5
@@ -22,6 +22,7 @@ export interface ISystemState {
|
||||
export interface IServicesState {
|
||||
services: interfaces.data.IService[];
|
||||
currentService: interfaces.data.IService | null;
|
||||
currentHostedAppLifecycle: interfaces.data.IHostedAppLifecycleState | null;
|
||||
currentServiceLogs: interfaces.data.ILogEntry[];
|
||||
currentServiceStats: interfaces.data.IContainerStats | null;
|
||||
platformServices: interfaces.data.IPlatformService[];
|
||||
@@ -97,6 +98,7 @@ export const servicesStatePart = await appState.getStatePart<IServicesState>(
|
||||
{
|
||||
services: [],
|
||||
currentService: null,
|
||||
currentHostedAppLifecycle: null,
|
||||
currentServiceLogs: [],
|
||||
currentServiceStats: null,
|
||||
platformServices: [],
|
||||
@@ -268,11 +270,24 @@ export const fetchServiceAction = servicesStatePart.createAction<{ name: string
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetService
|
||||
>('/typedrequest', 'getService');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
return { ...statePartArg.getState(), currentService: response.service };
|
||||
const hostedAppRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetHostedAppLifecycleForService
|
||||
>('/typedrequest', 'getHostedAppLifecycleForService');
|
||||
const [response, hostedAppResponse] = await Promise.all([
|
||||
typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
}),
|
||||
hostedAppRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
currentService: response.service,
|
||||
currentHostedAppLifecycle: hostedAppResponse.state || null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch service:', err);
|
||||
return statePartArg.getState();
|
||||
@@ -320,6 +335,7 @@ export const deleteServiceAction = servicesStatePart.createAction<{ name: string
|
||||
...state,
|
||||
services: state.services.filter((s) => s.name !== dataArg.name),
|
||||
currentService: null,
|
||||
currentHostedAppLifecycle: null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to delete service:', err);
|
||||
@@ -328,6 +344,28 @@ export const deleteServiceAction = servicesStatePart.createAction<{ name: string
|
||||
},
|
||||
);
|
||||
|
||||
export const dismissHostedAppBootstrapAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DismissHostedAppBootstrapAction
|
||||
>('/typedrequest', 'dismissHostedAppBootstrapAction');
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity!,
|
||||
serviceName: dataArg.name,
|
||||
});
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
currentHostedAppLifecycle: response.state || null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to dismiss hosted app bootstrap action:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const startServiceAction = servicesStatePart.createAction<{ name: string }>(
|
||||
async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
@@ -1123,6 +1161,7 @@ socketRouter.addTypedHandler(
|
||||
const state = servicesStatePart.getState();
|
||||
let services = state.services;
|
||||
let currentService = state.currentService;
|
||||
let currentHostedAppLifecycle = state.currentHostedAppLifecycle;
|
||||
let currentServiceLogs = state.currentServiceLogs;
|
||||
let currentServiceStats = state.currentServiceStats;
|
||||
|
||||
@@ -1130,6 +1169,7 @@ socketRouter.addTypedHandler(
|
||||
services = services.filter((service) => service.name !== dataArg.serviceName);
|
||||
if (currentService?.name === dataArg.serviceName) {
|
||||
currentService = null;
|
||||
currentHostedAppLifecycle = null;
|
||||
currentServiceLogs = [];
|
||||
currentServiceStats = null;
|
||||
}
|
||||
@@ -1144,6 +1184,7 @@ socketRouter.addTypedHandler(
|
||||
...state,
|
||||
services,
|
||||
currentService,
|
||||
currentHostedAppLifecycle,
|
||||
currentServiceLogs,
|
||||
currentServiceStats,
|
||||
});
|
||||
|
||||
@@ -113,6 +113,7 @@ export class ObViewServices extends DeesElement {
|
||||
accessor servicesState: appstate.IServicesState = {
|
||||
services: [],
|
||||
currentService: null,
|
||||
currentHostedAppLifecycle: null,
|
||||
currentServiceLogs: [],
|
||||
currentServiceStats: null,
|
||||
platformServices: [],
|
||||
@@ -488,6 +489,9 @@ export class ObViewServices extends DeesElement {
|
||||
${!upgradeOperation && latestUpgradeOperation?.status === 'failed'
|
||||
? this.renderUpgradeOperation(latestUpgradeOperation)
|
||||
: ''}
|
||||
${service && this.servicesState.currentHostedAppLifecycle
|
||||
? this.renderHostedAppLifecycleCard(service, this.servicesState.currentHostedAppLifecycle)
|
||||
: ''}
|
||||
${upgradeInfo ? html`
|
||||
<div style="
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
|
||||
@@ -601,6 +605,102 @@ export class ObViewServices extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderHostedAppLifecycleCard(
|
||||
serviceArg: interfaces.data.IService,
|
||||
lifecycleArg: interfaces.data.IHostedAppLifecycleState,
|
||||
): TemplateResult | '' {
|
||||
const action = lifecycleArg.bootstrapAction;
|
||||
const upgrade = lifecycleArg.upgradeState;
|
||||
const showUpgrade = upgrade?.status === 'available' || upgrade?.status === 'running' || upgrade?.status === 'failed';
|
||||
if (!action && !showUpgrade) return '';
|
||||
|
||||
return html`
|
||||
<div style="
|
||||
background: var(--ci-shade-1, #09090b);
|
||||
border: 1px solid ${action ? '#f59e0b' : '#60a5fa'};
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
">
|
||||
<div style="display: flex; justify-content: space-between; gap: 16px; align-items: flex-start;">
|
||||
<div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: var(--ci-shade-7, #e4e4e7);">
|
||||
Hosted App Lifecycle
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 4px;">
|
||||
Runtime reports from ${lifecycleArg.appName || serviceArg.name}${lifecycleArg.publicUrl ? html` at ${lifecycleArg.publicUrl}` : ''}.
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size: 12px; color: var(--ci-shade-4, #71717a); text-transform: uppercase; letter-spacing: 0.04em;">
|
||||
${lifecycleArg.runtimeStatus || 'unknown'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
${action ? html`
|
||||
<div style="margin-top: 14px; padding: 12px; background: var(--ci-shade-0, #030305); border-radius: 6px;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: var(--ci-shade-7, #e4e4e7); margin-bottom: 8px;">
|
||||
${action.label} ${action.status === 'completed' ? '(completed)' : ''}
|
||||
</div>
|
||||
${action.message ? html`<div style="font-size: 12px; color: var(--ci-shade-5, #a1a1aa); margin-bottom: 8px;">${action.message}</div>` : ''}
|
||||
${action.url ? this.renderHostedAppValueRow('Setup URL', action.url) : ''}
|
||||
${action.username ? this.renderHostedAppValueRow('Username', action.username) : ''}
|
||||
${action.password ? this.renderHostedAppValueRow('Password', action.password) : ''}
|
||||
${action.expiresAt ? html`
|
||||
<div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 8px;">
|
||||
Expires ${new Date(action.expiresAt).toLocaleString()}
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="font-size: 12px; color: #fbbf24; margin-top: 10px;">
|
||||
Mark done only after you saved the information or completed the app setup. This deletes the displayed bootstrap data from Onebox.
|
||||
</div>
|
||||
<button
|
||||
class="deploy-button"
|
||||
style="margin-top: 12px; padding: 8px 14px; font-size: 12px; background: transparent; border: 1px solid #f59e0b; color: #fbbf24;"
|
||||
@click=${() => this.handleDismissHostedAppBootstrap(serviceArg.name)}
|
||||
>Mark done and delete</button>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${showUpgrade ? html`
|
||||
<div style="margin-top: ${action ? '12px' : '14px'}; padding: 12px; background: var(--ci-shade-0, #030305); border-radius: 6px;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: var(--ci-shade-7, #e4e4e7);">
|
||||
Managed upgrade: ${upgrade!.status}
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--ci-shade-5, #a1a1aa); margin-top: 4px;">
|
||||
${upgrade!.currentVersion || '-'} ${upgrade!.targetVersion || upgrade!.latestVersion ? html`→ ${upgrade!.targetVersion || upgrade!.latestVersion}` : ''}
|
||||
</div>
|
||||
${upgrade!.error ? html`<div style="font-size: 12px; color: #f87171; margin-top: 6px;">${upgrade!.error}</div>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderHostedAppValueRow(labelArg: string, valueArg: string): TemplateResult {
|
||||
return html`
|
||||
<div style="display: grid; grid-template-columns: 110px 1fr auto; gap: 8px; align-items: center; margin-top: 6px;">
|
||||
<span style="font-size: 12px; color: var(--ci-shade-4, #71717a);">${labelArg}</span>
|
||||
<code style="font-size: 12px; color: var(--ci-shade-7, #e4e4e7); overflow-wrap: anywhere;">${valueArg}</code>
|
||||
<button
|
||||
class="deploy-button"
|
||||
style="padding: 4px 8px; font-size: 11px; background: transparent; border: 1px solid var(--ci-shade-3, #3f3f46); color: var(--ci-shade-6, #d4d4d8);"
|
||||
@click=${() => this.copyHostedAppValue(valueArg)}
|
||||
>Copy</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async copyHostedAppValue(valueArg: string): Promise<void> {
|
||||
await navigator.clipboard?.writeText(valueArg);
|
||||
}
|
||||
|
||||
private async handleDismissHostedAppBootstrap(serviceNameArg: string): Promise<void> {
|
||||
if (!confirm('Delete the hosted app bootstrap information from Onebox?')) return;
|
||||
await appstate.servicesStatePart.dispatchAction(appstate.dismissHostedAppBootstrapAction, {
|
||||
name: serviceNameArg,
|
||||
});
|
||||
}
|
||||
|
||||
private renderBackupsView(): TemplateResult {
|
||||
return html`
|
||||
<ob-sectionheading>Backups</ob-sectionheading>
|
||||
|
||||
Reference in New Issue
Block a user