2025-03-12 20:16:51 +00:00
import { requestPVE , requestAPI , goToPage , setAppearance , getSearchSettings , goToURL , getInstancesFragment } from "./utils.js" ;
2023-06-29 22:20:15 +00:00
import { alert , dialog } from "./dialog.js" ;
2023-07-28 18:32:04 +00:00
import { setupClientSync } from "./clientsync.js" ;
2024-11-07 21:23:38 +00:00
import wfaInit from "../modules/wfa.js" ;
2024-07-26 18:34:14 +00:00
class InstanceCard extends HTMLElement {
2025-03-12 20:16:51 +00:00
actionLock = false ;
shadowRoot = null ;
2024-07-26 18:34:14 +00:00
constructor ( ) {
super ( ) ;
2025-03-12 20:16:51 +00:00
const internals = this . attachInternals ( ) ;
this . shadowRoot = internals . shadowRoot ;
2024-07-26 18:34:14 +00:00
this . actionLock = false ;
}
2025-03-12 20:16:51 +00:00
get type ( ) {
return this . dataset . type ;
}
set type ( type ) {
this . dataset . type = type ;
}
get status ( ) {
return this . dataset . status ;
}
set status ( status ) {
this . dataset . status = status ;
}
get vmid ( ) {
return this . dataset . vmid ;
}
set vmid ( vmid ) {
this . dataset . vmid = vmid ;
}
get name ( ) {
return this . dataset . name ;
}
set name ( name ) {
this . dataset . name = name ;
}
get node ( ) {
2024-07-26 18:34:14 +00:00
return {
2025-03-12 20:16:51 +00:00
name : this . dataset . node ,
status : this . dataset . nodestatus
2024-07-26 18:34:14 +00:00
} ;
}
2025-03-12 20:16:51 +00:00
set node ( node ) {
this . dataset . node = node . name ;
this . dataset . nodetsatus = node . status ;
2024-07-26 18:34:14 +00:00
}
2025-03-12 20:16:51 +00:00
set searchQueryResult ( result ) {
this . dataset . searchqueryresult = JSON . stringify ( result ) ;
}
get searchQueryResult ( ) {
return JSON . parse ( ! this . dataset . searchqueryresult ? "{}" : this . dataset . searchqueryresult ) ;
}
2024-07-26 18:34:14 +00:00
2025-03-12 20:16:51 +00:00
update ( ) {
2024-07-26 18:34:14 +00:00
const nameParagraph = this . shadowRoot . querySelector ( "#instance-name" ) ;
2025-03-12 20:16:51 +00:00
nameParagraph . innerText = "" ;
2024-08-25 03:09:41 +00:00
if ( this . searchQueryResult . alignment ) {
let i = 0 ; // name index
let c = 0 ; // alignment index
const alignment = this . searchQueryResult . alignment ;
while ( i < this . name . length && c < alignment . length ) {
if ( alignment [ c ] === "M" ) {
const part = document . createElement ( "span" ) ;
part . innerText = this . name [ i ] ;
2024-07-26 18:34:14 +00:00
part . style = "color: var(--lightbg-text-color); background-color: var(--highlight-color);" ;
2024-08-25 03:09:41 +00:00
nameParagraph . append ( part ) ;
i ++ ;
c ++ ;
}
else if ( alignment [ c ] === "I" ) {
const part = document . createElement ( "span" ) ;
part . innerText = this . name [ i ] ;
nameParagraph . append ( part ) ;
i ++ ;
c ++ ;
}
else if ( alignment [ c ] === "D" ) {
c ++ ;
}
else if ( alignment [ c ] === "X" ) {
const part = document . createElement ( "span" ) ;
part . innerText = this . name [ i ] ;
nameParagraph . append ( part ) ;
i ++ ;
c ++ ;
2024-07-26 18:34:14 +00:00
}
}
}
else {
nameParagraph . innerHTML = this . name ? this . name : " " ;
}
const powerButton = this . shadowRoot . querySelector ( "#power-btn" ) ;
2025-03-12 20:16:51 +00:00
if ( powerButton . classList . contains ( "clickable" ) ) {
2024-07-26 18:34:14 +00:00
powerButton . onclick = this . handlePowerButton . bind ( this ) ;
}
const configButton = this . shadowRoot . querySelector ( "#configure-btn" ) ;
2025-03-12 20:16:51 +00:00
if ( configButton . classList . contains ( "clickable" ) ) {
2024-07-26 18:34:14 +00:00
configButton . onclick = this . handleConfigButton . bind ( this ) ;
}
const consoleButton = this . shadowRoot . querySelector ( "#console-btn" ) ;
2025-03-12 20:16:51 +00:00
if ( consoleButton . classList . contains ( "clickable" ) ) {
2024-07-26 18:34:14 +00:00
consoleButton . classList . add ( "clickable" ) ;
consoleButton . onclick = this . handleConsoleButton . bind ( this ) ;
}
const deleteButton = this . shadowRoot . querySelector ( "#delete-btn" ) ;
2025-03-12 20:16:51 +00:00
if ( deleteButton . classList . contains ( "clickable" ) ) {
2024-07-26 18:34:14 +00:00
deleteButton . onclick = this . handleDeleteButton . bind ( this ) ;
}
if ( this . node . status !== "online" ) {
powerButton . classList . add ( "hidden" ) ;
configButton . classList . add ( "hidden" ) ;
consoleButton . classList . add ( "hidden" ) ;
deleteButton . classList . add ( "hidden" ) ;
}
}
async handlePowerButton ( ) {
if ( ! this . actionLock ) {
const header = ` ${ this . status === "running" ? "Stop" : "Start" } VM ${ this . vmid } ` ;
const body = ` <p>Are you sure you want to ${ this . status === "running" ? "stop" : "start" } VM ${ this . vmid } </p> ` ;
dialog ( header , body , async ( result , form ) => {
if ( result === "confirm" ) {
this . actionLock = true ;
const targetAction = this . status === "running" ? "stop" : "start" ;
const targetStatus = this . status === "running" ? "stopped" : "running" ;
const prevStatus = this . status ;
this . status = "loading" ;
this . update ( ) ;
const result = await requestPVE ( ` /nodes/ ${ this . node . name } / ${ this . type } / ${ this . vmid } /status/ ${ targetAction } ` , "POST" , { node : this . node . name , vmid : this . vmid } ) ;
const waitFor = delay => new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
while ( true ) {
const taskStatus = await requestPVE ( ` /nodes/ ${ this . node . name } /tasks/ ${ result . data } /status ` , "GET" ) ;
if ( taskStatus . data . status === "stopped" && taskStatus . data . exitstatus === "OK" ) { // task stopped and was successful
this . status = targetStatus ;
this . update ( ) ;
this . actionLock = false ;
break ;
}
else if ( taskStatus . data . status === "stopped" ) { // task stopped but was not successful
this . status = prevStatus ;
2025-02-10 20:33:46 +00:00
alert ( ` Attempted to ${ targetAction } ${ this . vmid } but got: ${ taskStatus . data . exitstatus } ` ) ;
2024-07-26 18:34:14 +00:00
this . update ( ) ;
this . actionLock = false ;
break ;
}
else { // task has not stopped
await waitFor ( 1000 ) ;
}
}
}
} ) ;
}
}
handleConfigButton ( ) {
2024-08-05 20:45:37 +00:00
if ( ! this . actionLock && this . status === "stopped" ) { // if the action lock is false, and the node is stopped, then navigate to the config page with the node info in the search query
2024-11-07 21:23:38 +00:00
goToPage ( "instance.html" , { node : this . node . name , type : this . type , vmid : this . vmid } ) ;
2024-07-26 18:34:14 +00:00
}
}
handleConsoleButton ( ) {
if ( ! this . actionLock && this . status === "running" ) {
2025-03-12 20:16:51 +00:00
const data = { console : ` ${ this . type === "qemu" ? "kvm" : "lxc" } ` , vmid : this . vmid , vmname : this . name , node : this . node , resize : "off" , cmd : "" } ;
2024-07-26 18:34:14 +00:00
data [ ` ${ this . type === "qemu" ? "novnc" : "xtermjs" } ` ] = 1 ;
2025-02-25 21:35:11 +00:00
goToURL ( window . PVE , data , true ) ;
2024-07-26 18:34:14 +00:00
}
}
handleDeleteButton ( ) {
if ( ! this . actionLock && this . status === "stopped" ) {
const header = ` Delete VM ${ this . vmid } ` ;
const body = ` <p>Are you sure you want to <strong>delete</strong> VM ${ this . vmid } </p> ` ;
dialog ( header , body , async ( result , form ) => {
if ( result === "confirm" ) {
this . actionLock = true ;
this . status = "loading" ;
this . update ( ) ;
const action = { } ;
action . purge = 1 ;
action [ "destroy-unreferenced-disks" ] = 1 ;
const result = await requestAPI ( ` /cluster/ ${ this . node . name } / ${ this . type } / ${ this . vmid } /delete ` , "DELETE" ) ;
if ( result . status === 200 ) {
if ( this . parentElement ) {
this . parentElement . removeChild ( this ) ;
}
}
else {
2025-02-10 20:33:46 +00:00
alert ( ` Attempted to delete ${ this . vmid } but got: ${ result . error } ` ) ;
2024-07-26 18:34:14 +00:00
this . status = this . prevStatus ;
this . update ( ) ;
this . actionLock = false ;
}
}
} ) ;
}
}
}
customElements . define ( "instance-card" , InstanceCard ) ;
2023-06-29 22:20:15 +00:00
window . addEventListener ( "DOMContentLoaded" , init ) ;
async function init ( ) {
2024-06-14 06:17:15 +00:00
setAppearance ( ) ;
2023-06-29 22:20:15 +00:00
const cookie = document . cookie ;
if ( cookie === "" ) {
goToPage ( "login.html" ) ;
}
2023-07-28 18:32:04 +00:00
2024-11-07 21:23:38 +00:00
wfaInit ( "modules/wfa.wasm" ) ;
2025-03-12 20:16:51 +00:00
initInstances ( ) ;
2024-11-07 21:23:38 +00:00
2023-09-15 22:13:21 +00:00
document . querySelector ( "#instance-add" ) . addEventListener ( "click" , handleInstanceAdd ) ;
2025-03-12 20:16:51 +00:00
document . querySelector ( "#vm-search" ) . addEventListener ( "input" , sortInstances ) ;
2023-07-11 21:14:54 +00:00
2023-09-18 19:37:07 +00:00
setupClientSync ( refreshInstances ) ;
}
2023-09-15 22:13:21 +00:00
2023-09-18 19:37:07 +00:00
async function refreshInstances ( ) {
2025-03-12 20:16:51 +00:00
let instances = await getInstancesFragment ( ) ;
if ( instances . status !== 200 ) {
alert ( "Error fetching instances." ) ;
}
else {
instances = instances . data ;
const container = document . querySelector ( "#instance-container" ) ;
container . setHTMLUnsafe ( instances ) ;
sortInstances ( ) ;
}
2023-06-29 22:20:15 +00:00
}
2025-03-12 20:16:51 +00:00
function initInstances ( ) {
const container = document . querySelector ( "#instance-container" ) ;
let instances = container . children ;
instances = [ ] . slice . call ( instances ) ;
for ( let i = 0 ; i < instances . length ; i ++ ) {
instances [ i ] . update ( ) ;
}
2023-09-18 19:37:07 +00:00
}
2023-06-29 22:20:15 +00:00
2025-03-12 20:16:51 +00:00
function sortInstances ( ) {
2024-06-17 21:02:15 +00:00
const searchCriteria = getSearchSettings ( ) ;
2023-09-20 22:17:06 +00:00
const searchQuery = document . querySelector ( "#search" ) . value || null ;
2023-09-18 19:37:07 +00:00
let criteria ;
2023-09-20 22:17:06 +00:00
if ( ! searchQuery ) {
2024-08-25 03:09:41 +00:00
criteria = ( item , query = null ) => {
return { score : item . vmid , alignment : null } ;
2023-09-18 19:37:07 +00:00
} ;
}
2024-06-05 22:11:53 +00:00
else if ( searchCriteria === "exact" ) {
2024-08-25 03:09:41 +00:00
criteria = ( item , query ) => {
const substrInc = item . includes ( query ) ;
if ( substrInc ) {
const substrStartIndex = item . indexOf ( query ) ;
const queryLength = query . length ;
const remaining = item . length - substrInc - queryLength ;
const alignment = ` ${ "X" . repeat ( substrStartIndex ) } ${ "M" . repeat ( queryLength ) } ${ "X" . repeat ( remaining ) } ` ;
return { score : 1 , alignment } ;
2023-09-18 19:37:07 +00:00
}
else {
2024-08-25 03:09:41 +00:00
const alignment = ` ${ "X" . repeat ( item . length ) } ` ;
return { score : 0 , alignment } ;
2023-09-18 19:37:07 +00:00
}
} ;
}
2024-06-05 22:11:53 +00:00
else if ( searchCriteria === "fuzzy" ) {
const penalties = {
m : 0 ,
x : 1 ,
2024-08-25 03:09:41 +00:00
o : 0 ,
2024-06-05 22:11:53 +00:00
e : 1
} ;
2024-08-25 03:09:41 +00:00
criteria = ( item , query ) => {
2024-06-05 22:11:53 +00:00
// lower is better
2024-11-07 21:23:38 +00:00
const { score , CIGAR } = global . wfAlign ( query , item , penalties , true ) ;
2024-11-07 21:27:33 +00:00
const alignment = global . DecodeCIGAR ( CIGAR ) ;
return { score : score / item . length , alignment } ;
2024-06-05 22:11:53 +00:00
} ;
}
2023-06-29 22:20:15 +00:00
2025-03-12 20:16:51 +00:00
const container = document . querySelector ( "#instance-container" ) ;
let instances = container . children ;
instances = [ ] . slice . call ( instances ) ;
2024-08-25 03:09:41 +00:00
for ( let i = 0 ; i < instances . length ; i ++ ) {
2025-03-12 20:16:51 +00:00
if ( ! instances [ i ] . dataset . name ) { // if the instance has no name, assume its just empty string
instances [ i ] . dataset . name = "" ;
2025-01-06 20:34:09 +00:00
}
2025-03-12 20:16:51 +00:00
const { score , alignment } = criteria ( instances [ i ] . dataset . name . toLowerCase ( ) , searchQuery ? searchQuery . toLowerCase ( ) : "" ) ;
2024-08-25 03:09:41 +00:00
instances [ i ] . searchQueryResult = { score , alignment } ;
}
const sortCriteria = ( a , b ) => {
const aScore = a . searchQueryResult . score ;
const bScore = b . searchQueryResult . score ;
if ( aScore === bScore ) {
return a . vmid > b . vmid ? 1 : - 1 ;
}
else {
return aScore - bScore ;
}
} ;
2025-03-12 20:16:51 +00:00
2024-08-25 03:09:41 +00:00
instances . sort ( sortCriteria ) ;
2025-03-12 20:16:51 +00:00
for ( let i = 0 ; i < instances . length ; i ++ ) {
container . appendChild ( instances [ i ] ) ;
instances [ i ] . update ( ) ;
}
2024-08-25 03:09:41 +00:00
}
2023-06-29 22:20:15 +00:00
async function handleInstanceAdd ( ) {
const header = "Create New Instance" ;
const body = `
2023-11-14 00:09:41 +00:00
< form method = "dialog" class = "input-grid" style = "grid-template-columns: auto 1fr;" id = "form" >
< label for = "type" > Instance Type < / l a b e l >
< select class = "w3-select w3-border" name = "type" id = "type" required >
< option value = "lxc" > Container < / o p t i o n >
< option value = "qemu" > Virtual Machine < / o p t i o n >
< / s e l e c t >
< label for = "node" > Node < / l a b e l >
< select class = "w3-select w3-border" name = "node" id = "node" required > < / s e l e c t >
< label for = "name" > Name < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border" name = "name" id = "name" required >
2023-11-14 00:09:41 +00:00
< label for = "vmid" > ID < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border" name = "vmid" id = "vmid" type = "number" required >
2024-04-16 21:38:25 +00:00
< label for = "pool" > Pool < / l a b e l >
< select class = "w3-select w3-border" name = "pool" id = "pool" required > < / s e l e c t >
2023-11-14 00:09:41 +00:00
< label for = "cores" > Cores ( Threads ) < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border" name = "cores" id = "cores" type = "number" min = "1" max = "8192" required >
2023-11-14 00:09:41 +00:00
< label for = "memory" > Memory ( MiB ) < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border" name = "memory" id = "memory" type = "number" min = "16" , step = "1" required >
2023-11-14 00:09:41 +00:00
< p class = "container-specific none" style = "grid-column: 1 / span 2; text-align: center;" > Container Options < / p >
< label class = "container-specific none" for = "swap" > Swap ( MiB ) < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border container-specific none" name = "swap" id = "swap" type = "number" min = "0" step = "1" required disabled >
2023-11-14 00:09:41 +00:00
< label class = "container-specific none" for = "template-image" > Template Image < / l a b e l >
< select class = "w3-select w3-border container-specific none" name = "template-image" id = "template-image" required disabled > < / s e l e c t >
< label class = "container-specific none" for = "rootfs-storage" > ROOTFS Storage < / l a b e l >
< select class = "w3-select w3-border container-specific none" name = "rootfs-storage" id = "rootfs-storage" required disabled > < / s e l e c t >
< label class = "container-specific none" for = "rootfs-size" > ROOTFS Size ( GiB ) < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border container-specific none" name = "rootfs-size" id = "rootfs-size" type = "number" min = "0" max = "131072" required disabled >
2023-11-14 00:09:41 +00:00
< label class = "container-specific none" for = "password" > Password < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border container-specific none" name = "password" id = "password" type = "password" required disabled >
2024-12-22 07:02:29 +00:00
< label class = "container-specific none" for = "confirm-password" > Confirm Password < / l a b e l >
2024-08-05 20:45:37 +00:00
< input class = "w3-input w3-border container-specific none" name = "confirm-password" id = "confirm-password" type = "password" required disabled >
2023-11-14 00:09:41 +00:00
< / f o r m >
2023-06-29 22:20:15 +00:00
` ;
2024-04-16 21:38:25 +00:00
const templates = await requestAPI ( "/user/ct-templates" , "GET" ) ;
2023-06-29 22:20:15 +00:00
const d = dialog ( header , body , async ( result , form ) => {
if ( result === "confirm" ) {
const body = {
name : form . get ( "name" ) ,
cores : form . get ( "cores" ) ,
2024-04-16 21:38:25 +00:00
memory : form . get ( "memory" ) ,
pool : form . get ( "pool" )
2023-06-29 22:20:15 +00:00
} ;
if ( form . get ( "type" ) === "lxc" ) {
body . swap = form . get ( "swap" ) ;
body . password = form . get ( "password" ) ;
body . ostemplate = form . get ( "template-image" ) ;
body . rootfslocation = form . get ( "rootfs-storage" ) ;
body . rootfssize = form . get ( "rootfs-size" ) ;
}
2023-07-04 04:41:39 +00:00
const node = form . get ( "node" ) ;
const type = form . get ( "type" ) ;
const vmid = form . get ( "vmid" ) ;
2023-08-03 00:35:56 +00:00
const result = await requestAPI ( ` /cluster/ ${ node } / ${ type } / ${ vmid } /create ` , "POST" , body ) ;
2023-06-29 22:20:15 +00:00
if ( result . status === 200 ) {
2025-03-12 20:16:51 +00:00
refreshInstances ( ) ;
2023-06-29 22:20:15 +00:00
}
else {
2025-02-10 20:33:46 +00:00
alert ( ` Attempted to create new instance ${ vmid } but got: ${ result . error } ` ) ;
2025-03-12 20:16:51 +00:00
refreshInstances ( ) ;
2023-06-29 22:20:15 +00:00
}
}
} ) ;
const typeSelect = d . querySelector ( "#type" ) ;
typeSelect . selectedIndex = - 1 ;
typeSelect . addEventListener ( "change" , ( ) => {
if ( typeSelect . value === "qemu" ) {
d . querySelectorAll ( ".container-specific" ) . forEach ( ( element ) => {
element . classList . add ( "none" ) ;
element . disabled = true ;
} ) ;
}
else {
d . querySelectorAll ( ".container-specific" ) . forEach ( ( element ) => {
element . classList . remove ( "none" ) ;
element . disabled = false ;
} ) ;
}
} ) ;
const rootfsContent = "rootdir" ;
const rootfsStorage = d . querySelector ( "#rootfs-storage" ) ;
rootfsStorage . selectedIndex = - 1 ;
2023-11-15 20:18:01 +00:00
const userResources = await requestAPI ( "/user/dynamic/resources" , "GET" ) ;
const userCluster = await requestAPI ( "/user/config/cluster" , "GET" ) ;
2023-06-29 22:20:15 +00:00
const nodeSelect = d . querySelector ( "#node" ) ;
const clusterNodes = await requestPVE ( "/nodes" , "GET" ) ;
2024-04-27 03:50:29 +00:00
const allowedNodes = Object . keys ( userCluster . nodes ) ;
2023-06-29 22:20:15 +00:00
clusterNodes . data . forEach ( ( element ) => {
if ( element . status === "online" && allowedNodes . includes ( element . node ) ) {
nodeSelect . add ( new Option ( element . node ) ) ;
}
} ) ;
nodeSelect . selectedIndex = - 1 ;
2024-04-16 21:38:25 +00:00
nodeSelect . addEventListener ( "change" , async ( ) => { // change rootfs storage based on node
2023-06-29 22:20:15 +00:00
const node = nodeSelect . value ;
const storage = await requestPVE ( ` /nodes/ ${ node } /storage ` , "GET" ) ;
storage . data . forEach ( ( element ) => {
if ( element . content . includes ( rootfsContent ) ) {
rootfsStorage . add ( new Option ( element . storage ) ) ;
}
} ) ;
rootfsStorage . selectedIndex = - 1 ;
2023-11-15 20:18:01 +00:00
2024-04-16 21:38:25 +00:00
// set core and memory min/max depending on node selected
2023-11-15 20:18:01 +00:00
if ( node in userResources . cores . nodes ) {
d . querySelector ( "#cores" ) . max = userResources . cores . nodes [ node ] . avail ;
}
else {
d . querySelector ( "#cores" ) . max = userResources . cores . global . avail ;
}
if ( node in userResources . memory . nodes ) {
d . querySelector ( "#memory" ) . max = userResources . memory . nodes [ node ] . avail ;
}
else {
d . querySelector ( "#memory" ) . max = userResources . memory . global . avail ;
}
2024-04-16 21:38:25 +00:00
} ) ;
// set vmid min/max
d . querySelector ( "#vmid" ) . min = userCluster . vmid . min ;
d . querySelector ( "#vmid" ) . max = userCluster . vmid . max ;
2023-11-15 20:18:01 +00:00
2024-04-16 21:38:25 +00:00
// add user pools to selector
const poolSelect = d . querySelector ( "#pool" ) ;
const userPools = Object . keys ( userCluster . pools ) ;
userPools . forEach ( ( element ) => {
poolSelect . add ( new Option ( element ) ) ;
2023-06-29 22:20:15 +00:00
} ) ;
2024-04-16 21:38:25 +00:00
poolSelect . selectedIndex = - 1 ;
2023-06-29 22:20:15 +00:00
2024-04-16 21:38:25 +00:00
// add template images to selector
2023-06-29 22:20:15 +00:00
const templateImage = d . querySelector ( "#template-image" ) ; // populate templateImage depending on selected image storage
2024-04-16 21:38:25 +00:00
for ( const template of templates ) {
2024-04-27 03:50:29 +00:00
templateImage . append ( new Option ( template . name , template . volid ) ) ;
2024-04-16 21:38:25 +00:00
}
templateImage . selectedIndex = - 1 ;
2024-08-05 20:45:37 +00:00
const password = d . querySelector ( "#password" ) ;
const confirmPassword = d . querySelector ( "#confirm-password" ) ;
function validatePassword ( ) {
confirmPassword . setCustomValidity ( password . value !== confirmPassword . value ? "Passwords Don't Match" : "" ) ;
}
password . addEventListener ( "change" , validatePassword ) ;
confirmPassword . addEventListener ( "keyup" , validatePassword ) ;
2023-06-29 22:20:15 +00:00
}