2026-05-07 17:44:31 +00:00
import * as plugins from '../../../plugins.js' ;
import * as appstate from '../../../appstate.js' ;
import * as shared from '../../shared/index.js' ;
import {
DeesElement ,
css ,
cssManager ,
customElement ,
html ,
state ,
} from '@design.estate/dees-element' ;
type TBaseOsImageBuild = any ;
2026-05-07 20:33:14 +00:00
type TBaseOsImageSourcePreset =
| 'balena-generic-amd64'
| 'balena-generic-aarch64'
| 'balena-raspberrypi4-64' ;
const sourcePresetArchitectures : Record < TBaseOsImageSourcePreset , string > = {
'balena-generic-amd64' : 'amd64' ,
'balena-generic-aarch64' : 'arm64' ,
'balena-raspberrypi4-64' : 'rpi' ,
} ;
2026-05-07 17:44:31 +00:00
@customElement ( 'cloudly-view-baseos' )
export class CloudlyViewBaseOs extends DeesElement {
@state ( ) private builds : TBaseOsImageBuild [ ] = [ ] ;
@state ( ) private isLoading = false ;
private refreshTimer? : number ;
public static styles = [
cssManager . defaultStyles ,
shared . viewHostCss ,
css `
.layout {
display: grid;
grid-template-columns: 420px 1fr;
gap: 16px;
padding: 24px 0;
}
.builds {
display: flex;
flex-direction: column;
gap: 12px;
}
.build {
border: 1px solid #2a2f3a;
border-radius: 12px;
padding: 16px;
background: #10151f;
}
.build-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.meta {
color: #9aa4b2;
font-size: 13px;
margin-top: 8px;
}
.logs {
margin-top: 12px;
max-height: 120px;
overflow: auto;
font-family: monospace;
font-size: 12px;
color: #bac4d1;
white-space: pre-wrap;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
}
` ,
] ;
public async connectedCallback() {
await super . connectedCallback ( ) ;
await this . loadBuilds ( ) ;
this . refreshTimer = window . setInterval ( ( ) = > this . loadBuilds ( ) , 5000 ) ;
}
public async disconnectedCallback() {
await super . disconnectedCallback ( ) ;
if ( this . refreshTimer ) {
window . clearInterval ( this . refreshTimer ) ;
}
}
public render() {
return html `
<cloudly-sectionheading>BaseOS Images</cloudly-sectionheading>
<div class="layout">
2026-05-07 19:49:56 +00:00
<dees-panel .title= ${ 'Create Image' } .subtitle= ${ 'Build a Cloudly-bound BaseOS artifact' } .variant= ${ 'outline' } >
2026-05-07 17:44:31 +00:00
<dees-form @formData= ${ ( eventArg : CustomEvent ) = > this . createBuild ( ( eventArg . detail as any ) . data ) } >
2026-05-07 19:49:56 +00:00
<dees-input-dropdown
.key= ${ 'imageKind' }
.label= ${ 'Image Type' }
.selectedOption= ${ 'balena-raw' }
.options= ${ [
{ key : 'balena-raw' , option : 'balenaOS raw image' , payload : null } ,
{ key: 'ubuntu-iso', option: 'Ubuntu bootstrap ISO', payload: null },
]}
></dees-input-dropdown>
2026-05-07 17:44:31 +00:00
<dees-input-dropdown
.key= ${ 'architecture' }
.label= ${ 'Architecture' }
.selectedOption= ${ 'amd64' }
.options= ${ [
2026-05-07 19:49:56 +00:00
{ key : 'amd64' , option : 'amd64' , payload : null } ,
{ key: 'arm64', option: 'arm64', payload: null },
{ key: 'rpi', option: 'Raspberry Pi', payload: null },
2026-05-07 17:44:31 +00:00
]}
></dees-input-dropdown>
<dees-input-text .key= ${ 'cloudlyUrl' } .label= ${ 'Cloudly URL' } .value= ${ window . location . origin } .required= ${ true } ></dees-input-text>
<dees-input-text .key= ${ 'hostname' } .label= ${ 'Hostname' } .value= ${ 'baseos-node' } .required= ${ false } ></dees-input-text>
2026-05-07 20:33:14 +00:00
<dees-input-dropdown
.key= ${ 'sourceImagePreset' }
.label= ${ 'balenaOS Source Preset' }
.selectedOption= ${ 'auto' }
.options= ${ [
{ key : 'auto' , option : 'Auto by architecture' , payload : null } ,
{ key: 'balena-generic-amd64', option: 'Generic x86_64 (GPT)', payload: null },
{ key: 'balena-generic-aarch64', option: 'Generic AARCH64', payload: null },
{ key: 'balena-raspberrypi4-64', option: 'Raspberry Pi 4 64-bit', payload: null },
{ key: 'custom-url', option: 'Custom source URL', payload: null },
]}
.description= ${ 'Used for balenaOS raw images when no custom source URL is provided.' }
></dees-input-dropdown>
<dees-input-text .key= ${ 'balenaOsVersion' } .label= ${ 'balenaOS Version' } .value= ${ 'latest' } .description= ${ 'Use latest, or an explicit balenaOS raw_version such as 2026.1.0.' } .required= ${ false } ></dees-input-text>
2026-05-07 17:44:31 +00:00
<dees-input-text .key= ${ 'wifiSsid' } .label= ${ 'WiFi SSID' } .required= ${ false } ></dees-input-text>
<dees-input-text .key= ${ 'wifiPassword' } .label= ${ 'WiFi Password' } .isPasswordBool= ${ true } .required= ${ false } ></dees-input-text>
<dees-input-textarea .key= ${ 'sshPublicKey' } .label= ${ 'SSH Public Key' } .required= ${ false } ></dees-input-textarea>
2026-05-07 20:33:14 +00:00
<dees-input-text .key= ${ 'sourceImageUrl' } .label= ${ 'Custom Source Image URL' } .description= ${ 'Optional override for balenaOS raw images (.img, .img.xz, .zip, or balena download URL with fileType=.zip).' } .required= ${ false } ></dees-input-text>
2026-05-07 17:44:31 +00:00
<dees-form-submit .text= ${ this . isLoading ? 'Creating...' : 'Create BaseOS Image' } .disabled= ${ this . isLoading } ></dees-form-submit>
</dees-form>
</dees-panel>
<div class="builds">
${ this . builds . length === 0
? html ` <dees-panel .title= ${ 'No image builds yet' } .subtitle= ${ 'Create an image to start a corebuild job.' } ></dees-panel> `
: this . builds . map ( ( buildArg ) = > this . renderBuild ( buildArg ) ) }
</div>
</div>
` ;
}
private renderBuild ( buildArg : TBaseOsImageBuild ) {
const data = buildArg . data ;
return html `
<div class="build">
<div class="build-head">
<div>
<strong> ${ data . hostname || buildArg . id } </strong>
2026-05-07 19:49:56 +00:00
<div class="meta"> ${ data . imageKind || 'ubuntu-iso' } · ${ data . architecture } · ${ data . cloudlyUrl } </div>
2026-05-07 20:33:14 +00:00
${ data . sourceImagePreset ? html ` <div class="meta"> ${ data . sourceImagePreset } · balenaOS ${ data . balenaOsVersion || 'latest' } </div> ` : '' }
2026-05-07 17:44:31 +00:00
</div>
<dees-badge .text= ${ data . status } .type= ${ data . status === 'ready' ? 'success' : data . status === 'failed' ? 'error' : 'info' } ></dees-badge>
</div>
<div class="meta">
${ data . artifact ? ` ${ data . artifact . filename } · ${ Math . round ( data . artifact . size / 1024 / 1024 ) } MB · ${ data . artifact . sha256 . slice ( 0 , 12 ) } ... ` : data . errorText || 'Waiting for artifact' }
</div>
${ data . status === 'ready'
? html ` <dees-button .text= ${ 'Download' } .type= ${ 'primary' } @click= ${ ( ) = > this . downloadBuild ( buildArg . id ) } ></dees-button> `
: '' }
${ data . logs ? . length ? html ` <div class="logs"> ${ data . logs . slice ( - 8 ) . join ( '\n' ) } </div> ` : '' }
</div>
` ;
}
private async loadBuilds() {
try {
const response = await this . fireBaseOsRequest ( 'getBaseOsImageBuilds' , { } ) ;
this . builds = response . builds || [ ] ;
} catch ( error ) {
console . error ( 'Failed to load BaseOS image builds:' , error ) ;
}
}
private async createBuild ( formDataArg : any ) {
try {
2026-05-07 20:33:14 +00:00
const architecture = formDataArg . architecture || 'amd64' ;
const imageKind = formDataArg . imageKind || 'balena-raw' ;
const sourceImageUrl = formDataArg . sourceImageUrl ? . trim ( ) || undefined ;
const selectedSourceImagePreset = formDataArg . sourceImagePreset || 'auto' ;
const sourceImagePreset = this . getSourceImagePreset ( selectedSourceImagePreset , imageKind , sourceImageUrl ) ;
const balenaOsVersion = imageKind === 'balena-raw' && ! sourceImageUrl
? formDataArg . balenaOsVersion ? . trim ( ) || 'latest'
: undefined ;
this . validateBuildForm ( {
architecture ,
imageKind ,
selectedSourceImagePreset ,
sourceImagePreset ,
sourceImageUrl ,
wifiSsid : formDataArg.wifiSsid ,
wifiPassword : formDataArg.wifiPassword ,
} ) ;
this . isLoading = true ;
2026-05-07 17:44:31 +00:00
const response = await this . fireBaseOsRequest ( 'createBaseOsImageBuild' , {
build : {
2026-05-07 20:33:14 +00:00
architecture ,
imageKind ,
2026-05-07 17:44:31 +00:00
cloudlyUrl : formDataArg.cloudlyUrl || window . location . origin ,
hostname : formDataArg.hostname || undefined ,
2026-05-07 20:33:14 +00:00
sourceImageUrl ,
sourceImagePreset ,
balenaOsVersion ,
2026-05-07 17:44:31 +00:00
wifi : formDataArg.wifiSsid
? {
ssid : formDataArg.wifiSsid ,
password : formDataArg.wifiPassword || undefined ,
}
: undefined ,
sshPublicKey : formDataArg.sshPublicKey || undefined ,
} ,
} ) ;
this . builds = [ response . build , . . . this . builds ] ;
plugins . deesCatalog . DeesToast . createAndShow ( { message : 'BaseOS image build queued' , type : 'success' } ) ;
} catch ( error : any ) {
plugins . deesCatalog . DeesToast . createAndShow ( { message : ` Failed to create image build: ${ error . message } ` , type : 'error' } ) ;
} finally {
this . isLoading = false ;
}
}
2026-05-07 20:33:14 +00:00
private getSourceImagePreset (
sourceImagePresetArg : string | undefined ,
imageKindArg : string ,
sourceImageUrlArg? : string ,
) {
if ( imageKindArg !== 'balena-raw' || sourceImageUrlArg || sourceImagePresetArg === 'auto' ) {
return undefined ;
}
if ( sourceImagePresetArg === 'custom-url' ) {
return undefined ;
}
return sourceImagePresetArg as TBaseOsImageSourcePreset | undefined ;
}
private validateBuildForm ( optionsArg : {
architecture : string ;
imageKind : string ;
selectedSourceImagePreset? : string ;
sourceImagePreset? : TBaseOsImageSourcePreset ;
sourceImageUrl? : string ;
wifiSsid? : string ;
wifiPassword? : string ;
} ) {
if ( optionsArg . architecture === 'rpi' && optionsArg . imageKind === 'ubuntu-iso' ) {
throw new Error ( 'Raspberry Pi BaseOS images require the balenaOS raw image type.' ) ;
}
if (
optionsArg . imageKind === 'balena-raw'
&& optionsArg . selectedSourceImagePreset === 'custom-url'
&& ! optionsArg . sourceImageUrl
) {
throw new Error ( 'A custom source image URL is required when the custom source preset is selected.' ) ;
}
if ( optionsArg . imageKind === 'balena-raw' && optionsArg . sourceImagePreset ) {
const expectedArchitecture = sourcePresetArchitectures [ optionsArg . sourceImagePreset ] ;
if ( expectedArchitecture !== optionsArg . architecture ) {
throw new Error ( ` ${ optionsArg . sourceImagePreset } is only valid for ${ expectedArchitecture } images. ` ) ;
}
}
if ( optionsArg . wifiPassword && ! optionsArg . wifiSsid ) {
throw new Error ( 'A WiFi SSID is required when a WiFi password is set.' ) ;
}
}
2026-05-07 17:44:31 +00:00
private async downloadBuild ( buildIdArg : string ) {
const response = await this . fireBaseOsRequest ( 'createBaseOsImageDownloadUrl' , {
buildId : buildIdArg ,
} ) ;
window . location . href = response . url ;
}
private async fireBaseOsRequest ( methodArg : string , payloadArg : Record < string , unknown > ) {
appstate . apiClient . identity = appstate . loginStatePart . getState ( ) ? . identity || null as any ;
if ( ! appstate . apiClient . typedsocketClient ) {
await appstate . apiClient . start ( ) ;
}
const request = appstate . apiClient . typedsocketClient . createTypedRequest < any > ( methodArg ) ;
return await request . fire ( {
identity : appstate.apiClient.identity ,
. . . payloadArg ,
} ) ;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-baseos' : CloudlyViewBaseOs ;
}
}