Compare commits

..

19 commits

Author SHA1 Message Date
Richard Koliser
9b173217d1
Add escaping for the JQuery selector on image friendly name 2025-05-23 13:26:58 -04:00
j-travis
8622084a02
Merge pull request #7 from kasmtech/116-updates
nvidia runtime updates for 116 release with compatibility
2024-07-15 12:39:25 -04:00
thelamer
08280190ea disable lossless 2024-07-10 14:50:23 -04:00
thelamer
84f2bfdd48 nvidia runtime updates for 116 release with compatibility 2024-07-08 19:07:44 -04:00
j-travis
b5c356997f
Merge pull request #5 from kasmtech/114-updates
fix old bug if no images are selected install and upgrade bombs out
2023-08-21 17:07:02 -04:00
ryan.kuba
58775a6491 fix old bug if no images are selected install and upgrade bombs out 2023-08-21 15:59:23 -04:00
j-travis
e599398710
Merge pull request #4 from kasmtech/feature/KASM-4200-113-updates
KASM-4200 Updates for 1.13 release
2023-03-28 08:27:34 -04:00
thelamer
0c654da05a KASM-4200 restructure code to handle new yaml format and update gpu settings for non NVIDIA cards to use DRI3 2023-03-27 19:06:13 -07:00
j-travis
3e9264d25d
Merge pull request #3 from kasmtech/bugfix/KASM-3699-fix-gpu-info-error
KASM-3699 have a fallback mode for gpuinfo script to simply list the cards
2022-12-05 17:58:44 -05:00
ryan.kuba
a8735cf7e3 KASM-3699 have a fallback mode for gpuinfo script to simply list the cards 2022-12-05 14:18:18 -08:00
j-travis
bfacaa3734
Merge pull request #2 from kasmtech/feature/KASM-2903-upgrade-support
Feature/kasm 2903 upgrade support
2022-11-30 17:54:56 -05:00
ryan.kuba
ee1ea94b59 KASM-2903 need globals for install and upgrade settings 2022-11-28 09:46:28 -05:00
ryan.kuba
a5c2f0635c KASM-2903 add initial upgrade support 2022-11-27 20:27:27 -05:00
j-travis
675ca12955
Merge pull request #1 from kasmtech/feature/KASM-3512-gpu-option
Feature/kasm 3512 gpu option
2022-11-23 11:05:23 -05:00
ryan.kuba
c51cbe881d KASM-3512 nvidia bugfix 2022-11-17 17:05:13 -05:00
ryan.kuba
0aa9746d3b KASM-3512 add support for nvidia GPUS 2022-11-17 16:33:15 -05:00
ryan.kuba
37dbb06b8c KASM-3512 make lossless install default 2022-11-16 16:05:03 -08:00
ryan.kuba
64bd739f1d KASM-3512 use deep merge on the exec and run configs to load the default values alongside the gpu modifications 2022-11-05 14:57:43 -07:00
ryan.kuba
1e5706208c KASM-3512 adding select dropdown during installation to force a GPU on all containers 2022-11-01 15:44:15 -07:00
11 changed files with 211 additions and 445 deletions

View file

@ -1,38 +1,21 @@
image: alpine
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_AUTH_CONFIG: ${_DOCKER_AUTH_CONFIG}
default:
tags:
- oci-fixed-amd
image: docker
services:
- docker:dind
stages:
- update
- upload
update_list_json:
stage: update
rules:
- if: '$CI_COMMIT_REF_NAME =~ /^release\//'
when: on_success
before_script:
- export SANITIZED_BRANCH="$(echo $CI_COMMIT_REF_NAME | sed -r 's#^release/##' | sed 's/\//_/g')"
push_to_s3:
stage: upload
script:
- apk add curl git
- |
if curl -fsSL https://registry.kasmweb.com/1.1/list.json -o list.json.tmp; then
mv list.json.tmp list.json
git config user.email "ci@gitlab"
git config user.name "GitLab CI"
git remote set-url origin "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
git add list.json
if ! git diff --cached --quiet; then
git commit -m "chore: update list.json from registry [skip ci]"
git push origin HEAD:${CI_COMMIT_REF_NAME}
else
echo "list.json unchanged, nothing to commit"
fi
else
echo "Failed to fetch list.json, skipping update"
rm -f list.json.tmp
fi
- apk add aws-cli curl gzip tar
- export S3_URL="https://${S3_BUCKET}.s3.amazonaws.com/wizard/${CI_COMMIT_SHA}.tar.gz"
- tar -czf ${CI_COMMIT_SHA}.tar.gz *
- aws s3 cp ${CI_COMMIT_SHA}.tar.gz s3://${S3_BUCKET}/wizard/
- aws s3 cp ${CI_COMMIT_SHA}.tar.gz s3://${S3_BUCKET}/wizard/${SANITIZED_BRANCH}.tar.gz
- curl --request POST --header "PRIVATE-TOKEN:${GITLAB_API_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/statuses/${CI_COMMIT_SHA}?state=success&name=build-url&target_url=${S3_URL}"
tags:
- aws-autoscale

View file

@ -36,8 +36,3 @@ As configured files ingested from a current Kasm Workspaces installer are needed
/kasm_release/
└── Full Kasm workspaces installer
```
## Reporting Issues
To report any issues for this repository, please use our central issue tracker: **[Kasm Workspaces Issue Tracker](https://github.com/kasmtech/workspaces-issues/issues)**

311
index.js
View file

@ -1,176 +1,60 @@
// Imports
const Docker = require('dockerode');
const socketIO = require('socket.io');
const pty = require("node-pty");
const fsw = require('fs').promises;
const fs = require('fs');
const os = require('os');
const yaml = require('js-yaml');
const _ = require('lodash');
const si = require('systeminformation');
const express = require('express');
const app = require('express')();
const privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8');
const certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8');
const credentials = {key: privateKey, cert: certificate};
const https = require('https').Server(credentials, app);
const baserouter = express.Router();
const docker = new Docker({socketPath: '/var/run/docker.sock'});
const arch = os.arch().replace('x64', 'amd64');
const baseUrl = process.env.SUBFOLDER || '/';
const port = process.env.KASM_PORT || '443';
var Docker = require('dockerode');
var socketIO = require('socket.io');
var pty = require("node-pty");
var fsw = require('fs').promises;
var fs = require('fs');
var os = require('os');
var yaml = require('js-yaml');
var _ = require('lodash');
var si = require('systeminformation');
var express = require('express');
var app = require('express')();
var privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8');
var certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8');
var credentials = {key: privateKey, cert: certificate};
var https = require('https').Server(credentials, app);
var baserouter = express.Router();
var docker = new Docker({socketPath: '/var/run/docker.sock'});
var arch = os.arch().replace('x64', 'amd64');
var baseUrl = process.env.SUBFOLDER || '/';
var version = process.env.VERSION || 'stable';
var port = process.env.KASM_PORT || '443';
const { spawn } = require('node:child_process');
let EULA;
let images;
let currentVersion;
let sourceLocal = false;
let gpuInfo;
let installSettings = {};
let upgradeSettings = {};
// Find the best matching compatibility entry for the current version.
// Exact version match takes precedence over wildcard (e.g. "1.18.x").
function matchVersion(currentVer, compatibilityList) {
if (!compatibilityList || compatibilityList.length === 0) return null;
const parts = currentVer.split('.');
const major = parts[0];
const minor = parts[1] !== undefined ? parts[1] : '0';
// Exact match (most precise)
let match = compatibilityList.find(c => c.version === currentVer);
if (match) return match;
// Major.minor wildcard, e.g. "1.18.x"
match = compatibilityList.find(c => c.version === `${major}.${minor}.x`);
if (match) return match;
return null;
}
// Build the image tag for a given compat entry.
// Returns e.g. "1.18.1-rolling-weekly", falling back to "develop".
function buildImageTag(compat) {
const compatTag = compat.image.split(':')[1] || '';
const versionPrefix = compatTag.replace(/-[^-]+-[^-]+$/, ''); // strip last two dash-segments
const candidate = versionPrefix + '-rolling-weekly';
return (compat.available_tags && compat.available_tags.includes(candidate)) ? candidate : 'develop';
}
// Fetch the registry workspace list, with local fallback.
async function fetchListData() {
try {
const response = await fetch('https://registry.kasmweb.com/1.1/list.json');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
console.error('Failed to fetch workspace list, falling back to local list.json:', err.message);
sourceLocal = true;
return JSON.parse(await fsw.readFile('/wizard/list.json', 'utf8'));
}
}
// Return the rolling-weekly tag string for the current version by finding
// the first compatible workspace in the registry (e.g. "1.18.1-rolling-weekly").
async function getRollingWeeklyTag(currentVer, archName) {
const listData = await fetchListData();
const ws = (listData.workspaces || []).find(
w => w.architecture && w.architecture.includes(archName) && matchVersion(currentVer, w.compatibility || [])
);
if (!ws) return currentVer + '-rolling-weekly';
return buildImageTag(matchVersion(currentVer, ws.compatibility));
}
// Fetch workspace list from registry and convert to the images YAML structure.
// Falls back to local list.json if the remote is unavailable.
async function fetchWorkspaceList(currentVer, archName) {
const listData = await fetchListData();
const filtered = (listData.workspaces || []).filter(
ws => ws.architecture && ws.architecture.includes(archName)
);
let imageIdx = 1;
const imagesList = [];
for (const ws of filtered) {
const compat = matchVersion(currentVer, ws.compatibility || []);
if (!compat) continue;
const imageBase = compat.image.split(':')[0];
const imageTag = buildImageTag(compat);
const imageName = imageBase + ':' + imageTag;
imagesList.push({
categories: ws.categories,
cores: 2.0,
cpu_allocation_method: 'Inherit',
description: ws.description,
docker_registry: ws.docker_registry,
enabled: true,
exec_config: {},
friendly_name: ws.friendly_name,
gpu_count: 0,
hidden: false,
image_id: '${uuid:image_id:' + imageIdx + '}',
image_src: 'https://registry.kasmweb.com/1.1/icons/' + ws.image_src,
image_type: 'Container',
launch_config: {},
memory: 2768000000,
name: imageName,
notes: ws.notes || null,
run_config: {},
uncompressed_size_mb: compat.uncompressed_size_mb,
zone_id: null
});
imageIdx++;
}
let alembicVersion = 'e3900d8a4fee';
try {
const propsText = await fsw.readFile('/kasm_release/conf/database/seed_data/default_properties.yaml', 'utf8');
const props = yaml.load(propsText);
if (props && props.alembic_version) alembicVersion = props.alembic_version;
} catch (err) {
console.error('Could not read default_properties.yaml, using hardcoded alembic_version:', err.message);
}
return { alembic_version: alembicVersion, images: imagesList };
}
var EULA;
var images;
var currentVersion;
var gpuInfo;
var installSettings = {};
var upgradeSettings = {};
// Grab installer variables
async function installerBlobs() {
EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8');
try {
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
} catch (err) {
currentVersion = '1.18.1';
}
images = await fetchWorkspaceList(currentVersion, arch);
gpuInfo = {};
await new Promise((resolve) => {
let gpuData = [];
let gpuCmd = spawn('/gpuinfo.sh');
gpuCmd.stdout.on('data', function(data) {
gpuData.push(data);
});
gpuCmd.on('close', function(code) {
try {
if (code == 0) {
gpuInfo = JSON.parse(gpuData.join(''));
} else {
gpuInfo = {};
}
} catch (err) {
// Manually backfill GPU info if available
let imagesText = await fsw.readFile('/wizard/default_images_' + arch + '.yaml', 'utf8');
images = yaml.load(imagesText);
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
let gpuData = [];
let gpuCmd = spawn('/gpuinfo.sh');
gpuCmd.stdout.on('data', function(data) {
gpuData.push(data);
});
gpuCmd.on('close', function(code) {
try {
if (code == 0) {
gpuInfo = JSON.parse(gpuData.join(''));
} else {
gpuInfo = {};
for (let i = 0; i < 10; i++) {
let num = i.toString();
if (fs.existsSync('/dev/dri/card' + num)) {
gpuInfo['/dev/dri/card' + num] = "Unknown GPU";
}
}
} catch (err) {
// Manually backfill GPU info if available
gpuInfo = {};
for (let i = 0; i < 10; i++) {
let num = i.toString();
if (fs.existsSync('/dev/dri/card' + num)) {
gpuInfo['/dev/dri/card' + num] = "Unknown GPU";
}
}
resolve();
});
}
});
}
installerBlobs();
@ -180,59 +64,28 @@ async function setGpu(imagesI) {
if (upgradeSettings['forceGpu'] !== undefined) {
installSettings = upgradeSettings;
}
const forceGpu = installSettings.forceGpu;
if (!forceGpu || forceGpu === 'disabled' || !forceGpu.includes('|')) {
console.error('setGpu: invalid or missing forceGpu value:', forceGpu);
return imagesI;
}
const [gpu, gpuName] = forceGpu.split('|');
if (!gpu || !gpuName) {
console.error('setGpu: could not parse GPU path or name from forceGpu:', forceGpu);
return imagesI;
}
const card = gpu.slice(-1);
const render = (Number(card) + 128).toString();
let gpu = installSettings.forceGpu.split('|')[0];
let gpuName = installSettings.forceGpu.split('|')[1];
let card = gpu.slice(-1);
let render = (Number(card) + 128).toString();
// Handle NVIDIA Gpus
let baseRun;
if (gpuName.includes('NVIDIA')) {
var baseRun;
if (gpuName.indexOf('NVIDIA') !== -1) {
baseRun = JSON.parse('{"runtime":"nvidia","environment":{"NVIDIA_DRIVER_CAPABILITIES":"all","KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"device_requests":[{"driver": "","count": -1,"device_ids": null,"capabilities":[["gpu"]],"options":{}}]}');
} else {
baseRun = JSON.parse('{"environment":{"DRINODE":"/dev/dri/renderD' + render + '", "HW3D": true},"devices":["/dev/dri/card' + card + ':/dev/dri/card' + card + ':rwm","/dev/dri/renderD' + render + ':/dev/dri/renderD' + render + ':rwm"]}');
}
let baseExec = JSON.parse('{"first_launch":{"user":"root","cmd": "bash -c \'chown -R kasm-user:kasm-user /dev/dri/*\'"}}');
for (let i=0; i<imagesI.images.length; i++) {
for (var i=0; i<imagesI.images.length; i++) {
console.log(imagesI.images[i]['run_config']);
let finalRun = _.merge(imagesI.images[i]['run_config'], baseRun)
let finalExec = _.merge(imagesI.images[i]['exec_config'], baseExec)
finalRun = _.merge(imagesI.images[i]['run_config'], baseRun)
finalExec = _.merge(imagesI.images[i]['exec_config'], baseExec)
imagesI.images[i]['run_config'] = finalRun;
imagesI.images[i]['exec_config'] = finalExec;
}
return imagesI;
}
// For rolling-weekly installs, append -rolling to service image tags in docker conf yamls.
// Prevents -rolling-rolling by only modifying tags that don't already end with -rolling.
async function appendRollingToServiceImages() {
const confDir = '/kasm_release/docker';
let entries;
try {
entries = await fsw.readdir(confDir);
} catch (err) {
return;
}
for (const file of entries.filter(f => f.endsWith('.yaml'))) {
const filePath = confDir + '/' + file;
const content = await fsw.readFile(filePath, 'utf8');
const updated = content.split('\n').map(line => {
if (/ image:/.test(line) && /"$/.test(line) && !/-rolling"$/.test(line) && !/develop"$/.test(line)) {
return line.replace(/"$/, '-rolling"');
}
return line;
}).join('\n');
await fsw.writeFile(filePath, updated);
}
}
//// Http server ////
baserouter.use('/public', express.static(__dirname + '/public'));
baserouter.get("/", function (req, res) {
@ -245,20 +98,20 @@ app.use(baseUrl, baserouter);
https.listen(3000);
//// socketIO comms ////
const io = socketIO(https, {path: baseUrl + 'socket.io'});
io = socketIO(https, {path: baseUrl + 'socket.io'});
io.on('connection', async function (socket) {
// Run bash install with our custom flags
async function install(data) {
// Determine install settings
installSettings = data[0];
let imagesI = data[1];
let installFlags = ['/kasm_release/install.sh', '-W', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass];
if (imagesI && typeof imagesI === 'object' && Array.isArray(imagesI.images) && imagesI.images.length < 10) {
var imagesI = data[1];
installFlags = ['/kasm_release/install.sh', '-W', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass];
if ((imagesI.hasOwnProperty('images')) && (imagesI.images.length < 10)) {
installFlags.push('-b');
}
// GPU yaml merge
if (installSettings.forceGpu !== 'disabled' && imagesI && imagesI.images) {
if (installSettings.forceGpu !== 'disabled') {
imagesI = await setGpu(imagesI);
}
@ -268,7 +121,6 @@ io.on('connection', async function (socket) {
installFlags = installFlags.filter(function(e) { return e !== '-W' });
} else {
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
await appendRollingToServiceImages();
}
// Copy over version
@ -288,21 +140,41 @@ io.on('connection', async function (socket) {
// Run bash upgrade with our custom flags
async function upgrade(data) {
let upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
// Determine upgrade settings
upgradeSettings = data[0];
var imagesI = data[1];
upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
if (upgradeSettings.keepOldImages == true) {
upgradeFlags.push('-K');
} else {
upgradeFlags.push('-U');
}
// Patch service image tags for rolling builds
await appendRollingToServiceImages();
// GPU yaml merge
if (upgradeSettings.forceGpu !== 'disabled') {
imagesI = await setGpu(imagesI);
}
// Write finalized image data
let yamlStr = yaml.dump(imagesI);
if (yamlStr.startsWith("false")) {
upgradeFlags = upgradeFlags.filter(function(e) { return e !== '-K' });
upgradeFlags = upgradeFlags.filter(function(e) { return e !== '-U' });
} else {
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
}
// Copy over version
await fsw.copyFile('/version.txt', '/opt/version.txt');
// Run upgrade
let cmd = pty.spawn('/bin/bash', upgradeFlags);
cmd.on('data', function(data) {
socket.emit('term', data);
});
cmd.on('exit', async function(code, signal) {
cmd.on('exit', function(code, signal) {
if (code == 0) {
await fsw.copyFile('/version.txt', '/opt/version.txt');
const rollingTag = await getRollingWeeklyTag(currentVersion, arch);
socket.emit('done', {port, rollingTag});
socket.emit('done', port);
}
});
}
@ -320,7 +192,6 @@ io.on('connection', async function (socket) {
dashinfo['localVersion'] = 'Unknown';
}
dashinfo['currentVersion'] = currentVersion;
dashinfo['sourceLocal'] = sourceLocal;
dashinfo['gpuInfo'] = gpuInfo;
dashinfo['containers'] = containers;
dashinfo['cpu'] = await si.cpu();
@ -330,7 +201,7 @@ io.on('connection', async function (socket) {
socket.emit('renderdash', [dashinfo, images]);
// Render installer
} else {
socket.emit('renderinstall', [EULA, images, gpuInfo, currentVersion, sourceLocal]);
socket.emit('renderinstall', [EULA, images, gpuInfo]);
}
}
// Disable wizard

File diff suppressed because one or more lines are too long

View file

@ -8,11 +8,8 @@
},
"author": "",
"license": "MIT",
"engines": {
"node": ">=18"
},
"dependencies": {
"dockerode": "^4.0.10",
"dockerode": "^3.3.2",
"express": "^4.18.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",

View file

@ -2,17 +2,17 @@
display: none;
position: fixed;
width: 100%;
height: calc(100% - 65px);
height: calc(100% - 50px);
left: 0;
top: 65px;
top: 50px;
}
#container {
position: fixed;
width: 100%;
height: calc(100% - 65px);
height: calc(100% - 50px);
left: 0;
top: 65px;
top: 50px;
overflow: auto;
}
@ -25,12 +25,12 @@
top: 10%;
left: 50%;
transform: translate(-50%, -10%);
max-width: 740px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
}
#EULAButton {
position: absolute;
bottom: 0%;
top: 90%;
left: 50%;
transform: translate(-50%, -90%);
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
@ -38,10 +38,10 @@
#titleBar {
width: 100%;
height: 65px;
height: 50px;
background: #595959;
color: white;
display: inline-block;
border-bottom: 1px solid #ffffff26;
}
form {
@ -52,16 +52,16 @@ form {
#title {
float: right;
height: 65px;
line-height: 65px;
height: 50px;
line-height: 50px;
text-align: center;
margin-right: 10px;
}
#logo {
width: 170px;
height: 35px;
margin: 15px;
width: 138px;
height: 40px;
margin: 5px;
}
.center {
@ -115,38 +115,3 @@ form {
height: 100px;
}
fieldset {
background: #2b3352b3;
border: 1px solid #334155b3;
border-radius: 8px;
}
input[type=email], input[type=number], input[type=password], input[type=search], input[type=text] {
border-radius: 0.25rem;
border: 1px solid #ffffff26;
border-radius: 5px;
background: #0f172ab3;
width: 100%;
padding: 0.75rem;
font-size: 1rem;
}
input::placeholder {
color: var(--font-color)!important;
opacity: 0.7; /* Firefox reduces opacity by default */
}
label {
font-weight: bold;
margin-bottom:0.5rem;
display: inline-block;
}
legend {
font-size: 1.125rem;
background: #2b3352b3;
border: 1px solid #334155b3;
border-radius: 8px;
padding: 5px 20px;
}

File diff suppressed because one or more lines are too long

BIN
public/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="4458.45" height="919.08" viewBox="0 0 4458.45 919.08">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #185f85;
}
.cls-3 {
fill: #2a2d31;
}
.cls-4 {
fill: #1a9bd7;
}
.cls-5 {
fill: #0d84be;
}
</style>
</defs>
<polygon class="cls-1" points="952.91 0 925.94 0 925.94 919.08 952.91 919.08 952.91 0"/>
<path class="cls-1" d="M1601.72,654.51h-128.28l-258.01-311.95v311.95h-107.14V0h107.14V293.73L1464.7,0h127.55l-264.57,318.51,274.05,336Zm411.07-467.92h101.31v467.92h-101.31v-67.78c-37.9,53.21-92.56,79.44-163.26,79.44-64.14,0-119.53-23.32-164.72-71.43-45.19-47.38-68.51-105.68-68.51-174.2s23.32-127.55,68.51-174.92c45.19-47.37,100.58-71.43,164.72-71.43,70.7,0,125.36,26.24,163.26,78.72v-66.33Zm-252.91,341.1h0c27.7,28.43,63.41,42.27,105.68,42.27s77.26-13.85,105.68-42.27c27.7-28.42,41.54-64.14,41.54-107.14s-13.85-78.72-41.54-107.14c-28.42-28.43-63.41-42.27-105.68-42.27s-77.99,13.85-105.68,42.27c-28.42,28.42-42.27,64.14-42.27,107.14s13.85,78.72,42.27,107.14Zm580.16-212.82h0c0,15.31,8.75,27.7,26.24,36.44,16.76,9.47,37.9,17.49,61.95,24.78,24.05,7.29,48.83,15.31,74.34,23.32,25.51,7.29,46.65,21.87,63.41,43,16.76,21.14,25.51,48.1,25.51,80.17,0,44.46-17.49,80.17-51.75,105.68-34.99,25.51-77.99,37.9-130.46,37.9-45.92,0-86-9.47-118.8-28.42-32.8-19.68-56.85-46.65-72.16-80.9l87.46-50.29c16.03,44.46,50.29,67.05,103.5,67.05s79.44-17.49,79.44-52.48c0-14.58-8.75-26.97-25.51-36.44-17.49-8.75-37.9-17.49-61.95-24.78-24.78-8.02-49.56-15.31-74.34-23.32-25.51-7.29-46.65-21.87-64.14-41.54-16.76-20.41-25.51-45.92-25.51-77.26,0-43,16.04-77.99,48.83-104.22,32.8-26.24,73.61-39.36,122.45-39.36,38.63,0,72.89,8.75,102.77,26.24,30.61,16.76,53.93,40.82,69.97,71.43l-85.28,47.37c-16.03-34.98-45.19-53.21-87.46-53.21-19.68,0-35.71,4.37-48.83,12.39-13.12,8.75-19.68,20.41-19.68,36.44Zm870.98-140.67h0c53.21,0,96.21,17.49,128.28,51.75,32.8,34.26,48.83,80.17,48.83,138.48v290.08h-101.31V370.98c0-32.8-8.02-57.58-24.05-75.07-16.03-18.22-38.63-26.97-66.32-26.97-31.34,0-56.12,10.2-74.34,30.61-18.22,21.14-27.7,51.75-27.7,92.56v262.39h-101.31V370.98c0-32.8-7.29-57.58-22.59-75.07-15.31-18.22-37.17-26.97-64.87-26.97-30.61,0-55.39,10.2-75.07,31.34-18.95,21.14-29.15,51.75-29.15,91.84v262.39h-100.58V186.59h100.58v56.12c29.88-45.19,75.07-68.51,134.11-68.51s104.23,24.78,132.65,74.34c31.34-49.56,78.72-74.34,142.85-74.34Z"/>
<path class="cls-1" d="M1149.1,916.16l-40.82-150.14h25.51l32.07,117.34,34.26-117.34h22.59l33.53,117.34,31.34-117.34h24.78l-40.82,150.14h-28.42l-32.07-110.06-32.07,110.06h-29.88Zm403.05-19.68h0c-15.31,15.31-33.53,22.59-55.39,22.59s-40.09-7.29-55.39-22.59c-15.31-15.31-22.59-33.53-22.59-55.39s7.29-40.09,22.59-54.66c15.31-15.31,33.53-23.32,55.39-23.32s40.09,8.02,55.39,23.32c14.58,14.58,22.59,32.8,22.59,54.66s-8.02,40.09-22.59,55.39Zm-93.29-16.76h0c10.2,10.2,22.59,15.31,37.9,15.31s27.7-5.1,37.9-15.31c10.2-10.2,15.31-23.32,15.31-38.63s-5.1-28.42-15.31-38.63c-10.2-10.2-23.32-15.31-37.9-15.31-15.31,0-27.7,5.1-37.9,15.31-10.2,10.2-15.31,23.32-15.31,38.63s5.1,28.43,15.31,38.63Zm323.61,36.44h0l-32.07-54.66h-29.15v54.66h-24.78v-150.14h59.77c13.85,0,25.51,5.1,34.26,14.58,9.47,8.75,14.58,20.41,14.58,33.53,0,10.2-2.92,18.95-8.75,26.97-5.1,7.29-12.39,13.12-21.14,16.76l34.26,58.31h-26.97Zm-61.22-126.82h0v50.29h34.98c7.29,0,12.39-2.19,16.76-7.29,5.1-5.1,7.29-10.93,7.29-18.22,0-6.56-2.19-12.39-7.29-17.49-4.37-5.1-9.47-7.29-16.76-7.29h-34.98Zm319.24,126.82h-29.15l-59.04-71.43v71.43h-24.78v-150.14h24.78v67.78l56.85-67.78h29.15l-60.49,73.61,62.68,76.53Zm157.43,2.92h0c-14.58,0-26.97-2.92-37.17-9.47-9.47-6.56-16.76-16.03-21.14-26.97l21.87-13.12c5.83,17.49,18.22,25.51,37.17,25.51,8.75,0,16.04-1.46,20.41-5.1,5.1-3.64,7.29-8.75,7.29-14.58s-2.19-10.93-7.29-13.85c-5.1-2.92-13.85-6.56-26.97-10.93-6.56-2.19-12.39-4.37-16.76-5.83-5.1-2.19-9.48-4.37-14.58-8.02-5.1-2.91-8.75-7.29-10.93-12.39-2.19-5.1-3.64-10.93-3.64-17.49,0-13.12,4.37-24.05,13.85-32.07,9.48-7.29,21.14-11.66,34.26-11.66,12.39,0,22.59,2.92,31.34,9.48,9.47,5.83,16.03,13.85,21.14,24.05l-21.14,11.66c-5.83-13.85-16.76-21.14-31.34-21.14-7.29,0-13.12,2.19-16.76,5.1-4.37,3.64-6.56,8.02-6.56,13.85s2.19,9.48,6.56,13.12c4.37,2.92,12.39,6.56,24.05,9.48,5.1,2.19,9.48,3.64,11.66,5.1,2.91,.73,6.56,2.19,11.66,4.37,5.1,1.46,8.75,3.64,10.93,5.83,2.19,1.46,5.1,4.37,7.29,7.29,2.92,2.92,5.1,6.56,5.83,10.2,1.46,4.37,2.19,8.75,2.19,13.12,0,13.85-5.1,24.78-14.58,32.8-10.2,8.02-22.59,11.66-38.63,11.66Zm228.13-153.06h0c13.85,0,26.24,5.1,35.71,14.58,9.47,9.48,13.85,21.14,13.85,34.99s-4.37,25.51-13.85,34.98c-9.48,9.47-21.87,14.58-35.71,14.58h-30.61v51.02h-24.78v-150.14h55.39Zm0,75.8h0c7.29,0,13.12-2.92,18.22-7.29,4.37-5.1,6.56-11.66,6.56-18.95s-2.19-13.85-6.56-18.95c-5.1-5.1-10.93-7.29-18.22-7.29h-30.61v52.48h30.61Zm253.64,74.34h0l-10.93-29.88h-63.41l-10.93,29.88h-26.97l54.66-150.14h29.88l53.93,150.14h-26.24Zm-66.32-53.21h47.37l-23.32-67.78-24.05,67.78Zm274.05,56.12h0c-22.59,0-41.54-7.29-56.12-22.59-15.31-14.58-22.59-32.8-22.59-55.39s7.29-40.09,22.59-55.39c14.58-14.58,33.53-22.59,56.12-22.59,13.12,0,26.24,3.64,37.17,10.2,11.66,6.56,20.41,14.58,26.97,26.24l-21.14,12.39c-4.37-8.02-10.2-13.85-17.49-18.22-8.02-4.37-16.03-6.56-25.51-6.56-16.04,0-29.15,5.1-39.36,15.31-9.48,10.2-14.58,23.32-14.58,38.63s5.1,28.43,14.58,38.63c10.2,10.2,23.32,15.31,39.36,15.31,9.47,0,17.49-2.19,25.51-6.56,8.02-4.37,13.85-10.2,17.49-17.49l21.14,12.39c-5.83,10.93-15.31,19.68-26.24,26.24-11.66,6.56-24.05,9.47-37.9,9.47Zm209.18-26.24h66.32v23.32h-91.11v-150.14h89.65v24.05h-64.87v38.63h59.77v23.32h-59.77v40.82Zm238.33,26.24h0c-14.58,0-26.97-2.92-37.17-9.47-9.47-6.56-16.76-16.03-21.14-26.97l21.87-13.12c5.83,17.49,18.22,25.51,37.17,25.51,9.48,0,16.04-1.46,20.41-5.1,5.1-3.64,7.29-8.75,7.29-14.58s-2.19-10.93-7.29-13.85c-5.1-2.92-13.85-6.56-26.97-10.93-6.56-2.19-12.39-4.37-16.76-5.83-5.1-2.19-9.48-4.37-14.58-8.02-5.1-2.91-8.75-7.29-10.93-12.39-2.19-5.1-3.64-10.93-3.64-17.49,0-13.12,4.37-24.05,13.85-32.07,9.48-7.29,21.14-11.66,34.26-11.66,12.39,0,22.59,2.92,31.34,9.48,9.47,5.83,16.03,13.85,21.14,24.05l-21.14,11.66c-5.83-13.85-16.76-21.14-31.34-21.14-7.29,0-12.39,2.19-16.76,5.1-4.37,3.64-6.56,8.02-6.56,13.85s2.19,9.48,6.56,13.12c4.37,2.92,12.39,6.56,24.05,9.48,5.1,2.19,9.48,3.64,11.66,5.1,2.91,.73,6.56,2.19,11.66,4.37,5.1,1.46,8.75,3.64,10.93,5.83,2.19,1.46,5.1,4.37,8.02,7.29,2.19,2.92,4.37,6.56,5.83,10.2,.73,4.37,1.46,8.75,1.46,13.12,0,13.85-5.1,24.78-14.58,32.8-10.2,8.02-22.59,11.66-38.63,11.66Z"/>
<polygon class="cls-1" points="33.6 235.37 421.24 13.5 807.32 237.72 807.32 695.77 421.24 916.16 33.6 698.38 33.6 235.37"/>
<polygon class="cls-2" points="260.79 678.63 260.79 707.58 260.79 746.57 363 805.06 387.81 819.83 421.48 839.33 445.11 796.79 455.16 751.3 421.48 721.76 260.79 628.42 260.79 678.63"/>
<path class="cls-5" d="M743.46,653.82V282.22l-67.35-2.36-14.18,21.86-20.68,39.58v253.45l-219.77,127.02v117.57c107.52-62.03,214.46-124.06,321.98-185.51Z"/>
<polygon class="cls-3" points="201.71 409.23 201.71 223.14 100.1 282.22 100.1 653.82 201.71 712.31 201.71 526.8 421.48 653.82 421.48 653.82 421.48 535.66 303.92 467.72 421.48 399.78 421.48 282.22 201.71 409.23"/>
<path class="cls-4" d="M421.48,96.71c107.52,62.03,214.46,123.47,321.98,185.51l-59.08,34.27-43.13,24.81-219.77-127.02-160.69,92.75v-117.57l160.69-92.75Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -13,7 +13,7 @@
</head>
<body>
<div id="titleBar">
<img src="public/img/logo.svg" id="logo">
<img src="public/img/logo.png" id="logo">
<div id="title"></div>
</div>
<div id="spacing"></div>

View file

@ -1,21 +1,19 @@
// Variables
let EULA;
let images;
let gpus;
let term;
let installImages = [];
const installSettings = {};
const upgradeSettings = {};
let currentVersion = '';
let isUpgrade = false;
let selected = false;
var EULA;
var images;
var gpus;
var term;
var installImages = [];
var installSettings = {};
var upgradeSettings = {};
var selected = false;
// Socket.io connection
const host = window.location.hostname;
const port = window.location.port;
const protocol = window.location.protocol;
const path = window.location.pathname;
const socket = io(protocol + '//' + host + ':' + port, { path: path + 'socket.io'});
var host = window.location.hostname;
var port = window.location.port;
var protocol = window.location.protocol;
var path = window.location.pathname;
var socket = io(protocol + '//' + host + ':' + port, { path: path + 'socket.io'});
//// Page Functions ////
@ -33,12 +31,8 @@ async function install() {
if (installImages.length == 0) {
socket.emit('install', [installSettings, false]);
} else {
for (let image of installImages) {
for await (let image of installImages) {
let srcImage = images.images.find(x => x.friendly_name === image);
if (!srcImage) {
alert('Error: image "' + image + '" not found. Please refresh and try again.');
return;
}
srcImage['enabled'] = true;
selectedImages.images.push(srcImage);
selectedImages.group_images.push({image_id: srcImage.image_id, group_id: "68d557ac-4cac-42cc-a9f3-1c7c853de0f3"});
@ -49,10 +43,21 @@ async function install() {
// Execute upgrade
async function upgrade() {
isUpgrade = true;
showTerminal();
showTerminal()
titleChange('Upgrading');
socket.emit('upgrade', [upgradeSettings, false]);
// Create new object based on image selection
let selectedImages = {alembic_version: images.alembic_version, images: [], group_images: []};
if (installImages.length == 0) {
socket.emit('upgrade', [upgradeSettings, false]);
} else {
for await (let image of installImages) {
let srcImage = images.images.find(x => x.friendly_name === image);
srcImage['enabled'] = true;
selectedImages.images.push(srcImage);
selectedImages.group_images.push({image_id: srcImage.image_id, group_id: "68d557ac-4cac-42cc-a9f3-1c7c853de0f3"});
}
socket.emit('upgrade', [upgradeSettings, selectedImages]);
}
}
// Show page container
@ -71,7 +76,7 @@ function showTerminal() {
$('#terminal').empty();
$('#terminal').css('display', 'block');
term = new Terminal();
let fitaddon = new FitAddon.FitAddon();
fitaddon = new FitAddon.FitAddon();
term.loadAddon(fitaddon);
term.open($('#terminal')[0]);
fitaddon.fit();
@ -83,18 +88,13 @@ function titleChange(value) {
$('#title').text(value);
}
function versionChange(ver, local) {
document.title = 'Kasm Wizard ' + ver + (local ? ' (local)' : '');
}
// Render landing as installer page
function renderInstall(data) {
showContainer();
titleChange('EULA');
EULA = data[0];
images = data[1];
gpus = data[2] || {};
versionChange(data[3], data[4]);
gpus = data[2];
let EULADiv = $('<div>', {id: 'EULA'}).text(EULA);
$('#container').append(EULADiv);
let EULAButton = $('<button>', {id: 'EULAButton', onclick: 'pickSettings()', class: 'btn btn-default btn-ghost'}).text('Accept and continue');
@ -107,19 +107,17 @@ async function renderDash(data) {
titleChange('Dashboard');
let info = data[0];
images = data[1];
currentVersion = info.currentVersion;
versionChange(info.currentVersion, info.sourceLocal);
// Store GPU info
$('body').data('gpuInfo', info.gpuInfo);
// Upgrade button if needed
let upgrade;
var upgrade;
if (info.currentVersion !== info.localVersion) {
upgrade = $('<button>', {class: 'btn btn-primary', onclick: 'renderUpgrade()'}).text('Upgrade to ' + info.currentVersion)
} else {
upgrade = info.currentVersion;
}
// Kasm docker containers
let containersTable = $('<tbody>');
containersTable = $('<tbody>');
containersTable.append(
$('<tr>').append([
$('<th>').text('Web URL'),
@ -134,7 +132,7 @@ async function renderDash(data) {
$('<td>').append(upgrade)
])
);
for (let container of info.containers) {
for await (let container of info.containers) {
containersTable.append(
$('<tr>').append([
$('<th>').text(container.Names[0]),
@ -142,7 +140,7 @@ async function renderDash(data) {
])
);
}
let dockerCard = $('<div>', {id: 'dockerinfo', class: 'terminal-card'}).append([
dockerCard = $('<div>', {id: 'dockerinfo', class: 'terminal-card'}).append([
$('<header>').text('Kasm Docker containers'),
$('<table>').append(containersTable)
]);
@ -150,7 +148,7 @@ async function renderDash(data) {
let usedmem = (info.mem.active/info.mem.total)*100;
let totalmem = parseFloat(info.mem.total/1000000000).toFixed(2);
let diskbuffer = parseFloat(info.mem.buffcache/1000000000).toFixed(2);
let sysinfoTable = $('<tbody>').append([
sysinfoTable = $('<tbody>').append([
$('<tr>').append([
$('<th>').text('CPU'),
$('<td>').text(info.cpu.vendor + ' ' + info.cpu.brand)
@ -190,7 +188,7 @@ async function renderDash(data) {
)
])
]);
let systemCard = $('<div>', {id: 'systeminfo', class: 'terminal-card'}).append([
systemCard = $('<div>', {id: 'systeminfo', class: 'terminal-card'}).append([
$('<header>').text('System Information'),
$('<table>').append(sysinfoTable)
]);
@ -228,10 +226,10 @@ async function pickSettings() {
]);
let userPass = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'userPass'}).text('user@kasm.local Password: '),
$('<input>', {name: 'userPass', id: 'userPass', type: 'password', required: true, placeholder: 'required'})
$('<input>', {name: 'userPass', id: 'userPass', type: 'password', placeholder: 'required'}).prop('required',true)
]);
let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
for (let card of Object.keys(gpus || {})) {
for await (let card of Object.keys(gpus)) {
gpuOptions.push($('<option>', {value: card + '|' + gpus[card]}).text(card + ' - ' + gpus[card]));
}
let forceGpu = $('<div>', {class: 'form-group'}).append([
@ -241,12 +239,7 @@ async function pickSettings() {
let submit = $('<div>', {class: 'form-group'}).append([
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
]);
let passHint = $('<p>', {
id: 'pass-hint',
style: 'color: inherit;'
}).text('Passwords must be at least 8 characters and include uppercase, lowercase, a number, and a special character (!#$%^&*@).');
fieldset.append([
passHint,
adminPass,
userPass,
forceGpu,
@ -257,49 +250,56 @@ async function pickSettings() {
// Grab data and move to image selection
form.on('submit', function (e) {
e.preventDefault();
const passRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!#$%^&*@]).{8,}$/;
const adminPassVal = $('#adminPass').val();
const userPassVal = $('#userPass').val();
if (!passRegex.test(adminPassVal) || !passRegex.test(userPassVal)) {
$('#pass-hint').css('color', '#df2f87');
return;
}
$('#pass-hint').css('color', 'inherit');
installSettings.adminPass = adminPassVal;
installSettings.userPass = userPassVal;
installSettings.adminPass = $('#adminPass').val();
installSettings.userPass = $('#userPass').val();
installSettings.forceGpu = $('#forceGpu').val();
pickImages(false);
});
}
// Render upgrade form
function renderUpgrade() {
async function renderUpgrade() {
showContainer();
let gpus = $('body').data('gpuInfo');
titleChange('Upgrade Settings');
let form = $('<form>', {id: 'settingsform'});
let fieldset = $('<fieldset>').append($('<legend>').text('Kasm Upgrade Settings'));
fieldset.append($('<p>').text('Are you sure you want to upgrade Kasm to version ' + currentVersion + '?'));
let confirmCheck = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'confirmUpgrade'}).text('I confirm I want to upgrade'),
$('<input>', {name: 'confirmUpgrade', id: 'confirmUpgrade', type: 'checkbox'})
let keepOldImages = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'keepOldImages'}).text('Do not purge existing images: '),
$('<input>', {name: 'keepOldImages', id: 'keepOldImages', type: 'checkbox'})
]);
fieldset.append(confirmCheck);
let upgradeButton = $('<button>', {
class: 'btn btn-default btn-ghost',
id: 'upgradeButton',
onclick: 'upgrade()',
disabled: true
}).text('Upgrade');
$('#container').append([$('<div>', {class: 'terminal-card', style: 'margin: 20px 0; border: none;'}).append(fieldset), upgradeButton]);
$('#confirmUpgrade').on('change', function() {
$('#upgradeButton').prop('disabled', !this.checked);
let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
for await (let card of Object.keys(gpus)) {
gpuOptions.push($('<option>', {value: card + '|' + gpus[card]}).text(card + ' - ' + gpus[card]));
}
let forceGpu = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'forceGpu'}).text('Use GPU on all new images: '),
$('<select>', {name: 'forceGpu', id: 'forceGpu',}).append(gpuOptions)
]);
let submit = $('<div>', {class: 'form-group'}).append([
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
]);
fieldset.append([
keepOldImages,
forceGpu,
submit
]);
form.append(fieldset);
$('#container').append(form);
// Grab data and move to image selection
form.on('submit', function (e) {
e.preventDefault();
upgradeSettings.keepOldImages = $('#keepOldImages').is(":checked");
upgradeSettings.forceGpu = $('#forceGpu').val();
pickImages(true);
});
}
// Render image selection
async function pickImages(upgrade) {
let installText;
let installFunction;
var installText;
var installFunction;
if (upgrade) {
installText = 'Upgrade';
installFunction = 'upgrade()';
@ -311,18 +311,15 @@ async function pickImages(upgrade) {
titleChange('Image Selection');
let imagesDiv = $('<div>', {class: 'cardcontainer', id: 'images'});
$('#container').append(imagesDiv);
for (let image of images.images) {
for await (let image of images.images) {
let imageName = $('<p>').text(image.friendly_name);
let imageDiv = $('<div>', {
class: 'card',
id: image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-'),
title: image.description,
'data-friendly-name': image.friendly_name
}).append(imageName).css('filter', 'grayscale(100%)');
imageDiv.on('click', function() {
selectImage($(this).data('friendly-name'));
});
let thumb = $('<img>', {class: 'thumb', src: image.image_src, loading: 'lazy'});
onclick: 'selectImage(\'' + image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-') + '\')'
}).append(imageName).css('filter', 'grayscale(100%)')
let thumb = $('<img>', {class: 'thumb', src: 'public/' + image.image_src});
imageDiv.append(thumb);
$('#images').append(imageDiv);
}
@ -335,20 +332,20 @@ async function pickImages(upgrade) {
}
// Select an individual image
function selectImage(friendlyName) {
let elemId = friendlyName.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
let safeSelectorId = $.escapeSelector(elemId); // Escape the ID for jQuery
if (installImages.includes(friendlyName)) {
installImages = installImages.filter(e => e !== friendlyName);
function selectImage(image) {
let imageKey = image.replace(new RegExp('_', 'g'), ' ').replace('-', '.');
let safeSelectorId = $.escapeSelector(image); // Escape the ID for jQuery
if (installImages.includes(imageKey)) {
installImages = installImages.filter(e => e !== imageKey)
$('#' + safeSelectorId).css({
filter: 'grayscale(100%) brightness(0.7)',
filter: 'grayscale(100%)',
background: ''
});
} else {
installImages.push(friendlyName);
installImages.push(imageKey);
$('#' + safeSelectorId).css({
filter: '',
background: '#30426c'
background: '#89cff0'
});
}
}
@ -381,24 +378,17 @@ function selectAll() {
}
// Show finished page
function done(data) {
const port = (typeof data === 'object') ? data.port : data;
const rollingTag = (typeof data === 'object') ? data.rollingTag : null;
function done(port) {
showContainer();
titleChange('Complete');
let titleBar = $('<div>');
if (isUpgrade) {
titleBar.append($('<h2>', {class: 'center'}).text('Upgrade Complete'));
titleBar.append($('<h3>', {class: 'center'}).text('Remember to update your workspace image tags to :' + (rollingTag || (currentVersion + '-rolling-weekly'))));
} else {
titleBar.append($('<h2>', {class: 'center'}).text('Installation Complete'));
}
titleBar.append($('<h2>', {class: 'center'}).text('Installation Complete'));
titleBar.append($('<h3>', {class: 'center'}).text('This page will reload in 5 seconds'));
titleBar.append($('<h3>', {class: 'center'}).text('Your installation is available on port ' + port));
$('#container').append(titleBar);
setTimeout(function(){
location.reload(true);
}, 5000);
}, 5000);
}
//// Socket events ////