Another big rewrite
5
.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/db.sqlite
|
||||||
|
**/node_modules
|
||||||
|
**/appdata
|
||||||
|
.gitignore
|
||||||
|
**/screenshots
|
11
.gitignore
vendored
|
@ -1,8 +1,3 @@
|
||||||
**/node_modules/
|
**/db.sqlite
|
||||||
**/database.sqlite
|
**/node_modules
|
||||||
**/appdata/
|
**/appdata
|
||||||
.github
|
|
||||||
test
|
|
||||||
.dockerignore
|
|
||||||
.gitignore
|
|
||||||
docker-compose.yaml
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
## v0.21 (dev) - Another rewrite
|
||||||
|
* Rewrote the dashboard to use HTMX.
|
||||||
|
* Removed Socket.io.
|
||||||
|
* Views are now HTML instead of EJS.
|
||||||
|
* Improved Dockerfile.
|
||||||
|
* Express sessions configured to use memorystore.
|
||||||
|
*
|
||||||
|
|
||||||
## v0.20 (Jan 20th 2024) - The rewrite. Jumping all the way to v0.20.
|
## v0.20 (Jan 20th 2024) - The rewrite. Jumping all the way to v0.20.
|
||||||
* Changed to ES6 imports.
|
* Changed to ES6 imports.
|
||||||
* Cleaned up file structure and code layout.
|
* Cleaned up file structure and code layout.
|
||||||
|
|
20
Dockerfile
|
@ -1,19 +1,17 @@
|
||||||
FROM node:21-alpine
|
FROM node:21-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN --mount=type=bind,source=package.json,target=package.json \
|
RUN chown node:node /app
|
||||||
--mount=type=bind,source=package-lock.json,target=package-lock.json \
|
USER node
|
||||||
--mount=type=cache,target=/root/.npm \
|
|
||||||
npm ci --omit=dev
|
|
||||||
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* /app/
|
||||||
|
RUN npm ci && npm cache clean --force
|
||||||
|
COPY . /app
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
|
@ -1,7 +1,7 @@
|
||||||
# DweebUI
|
# DweebUI
|
||||||
DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
|
DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
|
||||||
|
|
||||||
Alpha v0.20 ( :fire: Experimental :fire: )
|
Alpha v0.21 ( :fire: Experimental :fire: )
|
||||||
|
|
||||||
|
|
||||||
[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
|
[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
|
||||||
|
@ -10,7 +10,7 @@ Alpha v0.20 ( :fire: Experimental :fire: )
|
||||||
[](https://github.com/lllllllillllllillll)
|
[](https://github.com/lllllllillllllillll)
|
||||||
[](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
|
[](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
|
||||||
[](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
|
[](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
|
||||||
[](https://www.buymeacoffee.com/lllllllillllllillll)
|
[](https://www.buymeacoffee.com/lllllllillllllillll)
|
||||||
|
|
||||||
* This is a personal project I started to get more familiar with Javascript and Node.js.
|
* This is a personal project I started to get more familiar with Javascript and Node.js.
|
||||||
* Some UI elements are placeholders and every version may have breaking changes.
|
* Some UI elements are placeholders and every version may have breaking changes.
|
||||||
|
|
|
@ -49,7 +49,7 @@ export const containerCard = (data) => {
|
||||||
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="col-sm-6 col-lg-3 deleteme">
|
<div class="col-sm-6 col-lg-3 pt-1">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-stamp card-stamp-sm">
|
<div class="card-stamp card-stamp-sm">
|
||||||
|
@ -60,16 +60,16 @@ export const containerCard = (data) => {
|
||||||
<div class="ms-auto lh-1">
|
<div class="ms-auto lh-1">
|
||||||
<div class="card-actions btn-actions">
|
<div class="card-actions btn-actions">
|
||||||
<div class="card-actions btn-actions">
|
<div class="card-actions btn-actions">
|
||||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="start" class="btn-action" title="Start" ${actions}><!-- player-play -->
|
<button class="btn-action" title="Start" data-hx-get="/click" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="start" value="${state}" ${actions}><!-- player-play -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="stop" class="btn-action" title="Stop" ${actions}><!-- player-stop -->
|
<button class="btn-action" title="Stop" data-hx-get="/click" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="stop" value="${state}" ${actions}><!-- player-stop -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="pause" class="btn-action" title="Pause" ${actions}><!-- player-pause -->
|
<button class="btn-action" title="Pause" data-hx-get="/click" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="pause" value="${state}" ${actions}><!-- player-pause -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="restart" class="btn-action" title="Restart" ${actions}><!-- reload -->
|
<button class="btn-action" title="Restart" data-hx-get="/click" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="restart" value="${state}" ${actions}><!-- reload -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
|
@ -89,8 +89,8 @@ export const containerCard = (data) => {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-end">
|
<div class="dropdown-menu dropdown-menu-end">
|
||||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="hide" value="hide">Hide</button>
|
<button class="dropdown-item text-secondary" data-hx-get="/click" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="hide" value="hide">Hide</button>
|
||||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="resetView" value="resetView">Reset View</button>
|
<button class="dropdown-item text-secondary" data-hx-get="/click" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="reset" value="reset">Reset View</button>
|
||||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="permissions" value="permissions" data-bs-toggle="modal" data-bs-target="#${name}_permissions">Permissions</button>
|
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="permissions" value="permissions" data-bs-toggle="modal" data-bs-target="#${name}_permissions">Permissions</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,7 +111,9 @@ export const containerCard = (data) => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="${chart}_chart" class="chart-sm"></div>
|
<div class="chart-sm" data-hx-get="/chart" data-hx-trigger="every 2s" name="${name}" data-hx-target="#${name}_chart" hx-swap="innerHTML">
|
||||||
|
<div id="${name}_chart"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { appCard } from '../components/appCard.js';
|
import { appCard } from '../components/appCard.js';
|
||||||
|
|
||||||
let templatesJSON = readFileSync('./templates.json');
|
let templatesJSON = readFileSync('./templates/templates.json');
|
||||||
let templates = JSON.parse(templatesJSON).templates;
|
let templates = JSON.parse(templatesJSON).templates;
|
||||||
|
|
||||||
templates = templates.sort((a, b) => {
|
templates = templates.sort((a, b) => {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { User, Syslog } from '../database/models.js';
|
import { User, Syslog } from '../database/models.js';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const Login = function(req,res){
|
export const Login = function(req,res){
|
||||||
if(req.session.user){
|
if(req.session.user){
|
||||||
res.redirect("/logout");
|
res.redirect("/logout");
|
||||||
|
|
|
@ -55,7 +55,7 @@ export const submitRegister = async function(req,res){
|
||||||
password: bcrypt.hashSync(password,10),
|
password: bcrypt.hashSync(password,10),
|
||||||
role: await userRole(),
|
role: await userRole(),
|
||||||
group: 'all',
|
group: 'all',
|
||||||
avatar: `<img src="img/avatars/${avatar}">`,
|
avatar: `<img src="/images/avatars/${avatar}">`,
|
||||||
lastLogin: newLogin,
|
lastLogin: newLogin,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
import { Sequelize, DataTypes } from 'sequelize';
|
import { Sequelize, DataTypes } from 'sequelize';
|
||||||
|
|
||||||
// let SQLITE_PASS = process.env.SQLITE_PASS || 'some_long_elaborate_password';
|
|
||||||
|
|
||||||
// export const sequelize = new Sequelize('dweebui', 'dweebui', SQLITE_PASS, {
|
|
||||||
// dialect: 'sqlite',
|
|
||||||
// dialectModulePath: '@journeyapps/sqlcipher',
|
|
||||||
// storage: './database/database.sqlite',
|
|
||||||
// logging: false,
|
|
||||||
// });
|
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
export const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: './database/database.sqlite',
|
storage: './database/db.sqlite',
|
||||||
logging: false,
|
logging: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,15 @@ version: "3.9"
|
||||||
services:
|
services:
|
||||||
dweebui:
|
dweebui:
|
||||||
container_name: dweebui
|
container_name: dweebui
|
||||||
image: lllllllillllllillll/dweebui:v0.20
|
image: lllllllillllllillll/dweebui:v0.21-dev
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
|
||||||
PORT: 8000
|
PORT: 8000
|
||||||
SECRET: MrWiskers
|
SECRET: MrWiskers
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
volumes:
|
volumes:
|
||||||
- dweebui:/app
|
- dweebui:/app/database/db.sqlite
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
networks:
|
networks:
|
||||||
- dweebui_net
|
- dweebui_net
|
||||||
|
|
2547
package-lock.json
generated
20
package.json
|
@ -1,37 +1,27 @@
|
||||||
{
|
{
|
||||||
"name": "dweebui",
|
"name": "dweebui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A web UI for Docker",
|
"description": "",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --require @babel/register"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/register": "^7.23.7",
|
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chai": "^5.0.0",
|
|
||||||
"compression": "^1.7.4",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"dockerode-compose": "^1.4.0",
|
"dockerode-compose": "^1.4.0",
|
||||||
"ejs": "^3.1.9",
|
"ejs": "^3.1.9",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^7.1.5",
|
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"helmet": "^7.1.0",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"mocha": "^10.2.0",
|
"memorystore": "^1.6.7",
|
||||||
"sequelize": "^6.35.2",
|
"sequelize": "^6.35.2",
|
||||||
"sinon": "^17.0.1",
|
|
||||||
"socket.io": "^4.7.4",
|
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"stream": "^0.0.2",
|
"systeminformation": "^5.21.23"
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"systeminformation": "^5.21.22"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 413 B |
|
@ -1,141 +0,0 @@
|
||||||
socket.on('connect', () => {
|
|
||||||
console.log('connected');
|
|
||||||
//clear localStorage (because of code in old versions)
|
|
||||||
localStorage.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Server metrics
|
|
||||||
const cpuText = document.getElementById('cpu-text');
|
|
||||||
const cpuBar = document.getElementById('cpu-bar');
|
|
||||||
const ramText = document.getElementById('ram-text');
|
|
||||||
const ramBar = document.getElementById('ram-bar');
|
|
||||||
const netText = document.getElementById('net-text');
|
|
||||||
const netBar = document.getElementById('net-bar');
|
|
||||||
const diskText = document.getElementById('disk-text');
|
|
||||||
const diskBar = document.getElementById('disk-bar');
|
|
||||||
|
|
||||||
// Container cards
|
|
||||||
const dockerCards = document.getElementById('cards');
|
|
||||||
|
|
||||||
// Container logs
|
|
||||||
const logViewer = document.getElementById('logView');
|
|
||||||
|
|
||||||
// Server metrics
|
|
||||||
socket.on('metrics', (data) => {
|
|
||||||
let [cpu, ram, tx, rx, disk] = data;
|
|
||||||
|
|
||||||
cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
|
|
||||||
if (cpu < 7 ) { cpu = 7; }
|
|
||||||
cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
|
|
||||||
|
|
||||||
ramText.innerHTML = `<span>RAM ${ram} %</span>`;
|
|
||||||
if (ram < 7 ) { ram = 7; }
|
|
||||||
ramBar.innerHTML = `<span style="width: ${ram}%"><span></span></span>`;
|
|
||||||
|
|
||||||
tx = Math.round(tx / 1024 / 1024);
|
|
||||||
rx = Math.round(rx / 1024 / 1024);
|
|
||||||
|
|
||||||
netText.innerHTML = `<span>Down: ${rx}MB</span><span> Up: ${tx}MB</span>`;
|
|
||||||
netBar.innerHTML = `<span style="width: 50%"><span></span></span>`;
|
|
||||||
|
|
||||||
diskText.innerHTML = `<span>DISK ${disk} %</span>`;
|
|
||||||
if (disk < 7 ) { disk = 7; }
|
|
||||||
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Container cards
|
|
||||||
socket.on('containers', (data) => {
|
|
||||||
let deleteMeElements = document.querySelectorAll('.deleteme');
|
|
||||||
deleteMeElements.forEach((element) => {
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
});
|
|
||||||
dockerCards.insertAdjacentHTML("afterend", data);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function drawCharts(name, cpuArray, ramArray) {
|
|
||||||
let element = document.querySelector(`${name}`);
|
|
||||||
|
|
||||||
let chart = new ApexCharts(element, {
|
|
||||||
chart: {
|
|
||||||
type: "line",
|
|
||||||
height: 40.0,
|
|
||||||
sparkline: {
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
animations: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
opacity: 1
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
width: [2, 1],
|
|
||||||
dashArray: [0, 3],
|
|
||||||
lineCap: "round",
|
|
||||||
curve: "smooth"
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: "CPU",
|
|
||||||
data: cpuArray
|
|
||||||
}, {
|
|
||||||
name: "RAM",
|
|
||||||
data: ramArray
|
|
||||||
}],
|
|
||||||
tooltip: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
strokeDashArray: 4
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
labels: {
|
|
||||||
padding: 0
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
padding: 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
|
|
||||||
legend: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
chart.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons functions
|
|
||||||
function clicked(button) {
|
|
||||||
socket.emit('clicked', {name: button.name, id: button.id, value: button.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
socket.on('containerStats', (data) => {
|
|
||||||
let containerStats = data;
|
|
||||||
|
|
||||||
for (const [name, statsArray] of Object.entries(containerStats)) {
|
|
||||||
|
|
||||||
let cpuArray = statsArray.cpuArray;
|
|
||||||
let ramArray = statsArray.ramArray;
|
|
||||||
|
|
||||||
let chart = document.getElementById(`${name}_chart`);
|
|
||||||
if (chart) {
|
|
||||||
chart.innerHTML = '';
|
|
||||||
drawCharts(`#${name}_chart`, cpuArray, ramArray);
|
|
||||||
} else {
|
|
||||||
console.log(`Chart element with id ${name}_chart not found in the DOM`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
socket.on('logs', (data) => {
|
|
||||||
logViewer.innerHTML = `<pre>${data}</pre>`;
|
|
||||||
});
|
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -15,10 +15,11 @@ import { Volumes } from "../controllers/volumes.js";
|
||||||
import { Syslogs } from "../controllers/syslogs.js";
|
import { Syslogs } from "../controllers/syslogs.js";
|
||||||
import { Portal } from "../controllers/portal.js"
|
import { Portal } from "../controllers/portal.js"
|
||||||
|
|
||||||
/// Functions
|
// Functions
|
||||||
import { Install } from "../functions/install.js"
|
import { Install } from "../functions/install.js"
|
||||||
import { Uninstall } from "../functions/uninstall.js"
|
import { Uninstall } from "../functions/uninstall.js"
|
||||||
|
|
||||||
|
|
||||||
// Auth middleware
|
// Auth middleware
|
||||||
const auth = (req, res, next) => {
|
const auth = (req, res, next) => {
|
||||||
if (req.session.role == "admin") {
|
if (req.session.role == "admin") {
|
||||||
|
|
369
server.js
|
@ -1,44 +1,26 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import compression from 'compression';
|
import memorystore from 'memorystore';
|
||||||
import helmet from 'helmet';
|
import ejs from 'ejs';
|
||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import cors from 'cors';
|
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { rateLimit } from 'express-rate-limit';
|
|
||||||
import { instrument } from '@socket.io/admin-ui'
|
|
||||||
import { router } from './router/index.js';
|
import { router } from './router/index.js';
|
||||||
import { createServer } from 'node:http';
|
|
||||||
import { Server } from 'socket.io';
|
|
||||||
import { sequelize, Container } from './database/models.js';
|
import { sequelize, Container } from './database/models.js';
|
||||||
import { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces } from 'systeminformation';
|
import { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces } from 'systeminformation';
|
||||||
import { containerCard } from './components/containerCard.js';
|
import { containerCard } from './components/containerCard.js';
|
||||||
|
|
||||||
export const app = express();
|
|
||||||
const server = createServer(app);
|
|
||||||
const port = process.env.PORT || 8000;
|
|
||||||
export var docker = new Docker();
|
export var docker = new Docker();
|
||||||
let [cpu, ram, tx, rx, disk] = [0, 0, 0, 0, 0];
|
|
||||||
let [hidden, clicked, dockerEvents] = ['', false, ''];
|
|
||||||
let metricsInterval, cardsInterval, graphsInterval;
|
|
||||||
let cardList = '';
|
|
||||||
const statsArray = {};
|
|
||||||
|
|
||||||
// Socket.io admin ui
|
const app = express();
|
||||||
export const io = new Server(server, {
|
const MemoryStore = memorystore(session);
|
||||||
cors: {
|
const port = process.env.PORT || 8000;
|
||||||
origin: ['http://localhost:8000', 'https://admin.socket.io'],
|
let [ hidden, activeEvent, cardList, clicked ] = ['', '', '', false];
|
||||||
methods: ['GET', 'POST'],
|
let sentList = '';
|
||||||
credentials: true
|
let SSE = false;
|
||||||
}
|
let clicks = 0;
|
||||||
});
|
|
||||||
instrument(io, {
|
|
||||||
auth: false,
|
|
||||||
readonly: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Session middleware
|
// Session middleware
|
||||||
const sessionMiddleware = session({
|
const sessionMiddleware = session({
|
||||||
|
store: new MemoryStore({ checkPeriod: 86400000 }), // Prune expired entries every 24h
|
||||||
secret: "keyboard cat",
|
secret: "keyboard cat",
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
|
@ -49,47 +31,40 @@ const sessionMiddleware = session({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make session data available to socket.io
|
|
||||||
io.engine.use(sessionMiddleware);
|
|
||||||
|
|
||||||
// Rate limiter
|
|
||||||
const limiter = rateLimit({
|
|
||||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
||||||
limit: 50, // Limit each IP to 50 requests per `window`.
|
|
||||||
standardHeaders: 'draft-7',
|
|
||||||
legacyHeaders: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Express middleware
|
// Express middleware
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'html');
|
||||||
|
app.engine('html', ejs.renderFile);
|
||||||
app.use([
|
app.use([
|
||||||
compression(),
|
express.static('public'),
|
||||||
cors(),
|
|
||||||
helmet({contentSecurityPolicy: false}),
|
|
||||||
express.static("public"),
|
|
||||||
express.json(),
|
express.json(),
|
||||||
express.urlencoded({ extended: true }),
|
express.urlencoded({ extended: true }),
|
||||||
sessionMiddleware,
|
sessionMiddleware,
|
||||||
router,
|
router
|
||||||
limiter
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Initialize server
|
// Initialize server
|
||||||
server.listen(port, async () => {
|
app.listen(port, async () => {
|
||||||
async function init() {
|
async function init() {
|
||||||
try { await sequelize.authenticate().then(() => { console.log('[Connected to DB]') }); }
|
try { await sequelize.authenticate().then(
|
||||||
catch { console.log('[Could not connect to DB]'); }
|
() => { console.log('DB Connection: ✔️') }); }
|
||||||
try { await sequelize.sync().then(() => { console.log('[Models Synced]') }); }
|
catch { console.log('DB Connection: ❌'); }
|
||||||
catch { console.log('[Could not Sync Models]', error); }
|
try { await sequelize.sync().then( // check out that formatting
|
||||||
await getHidden();
|
() => { console.log('Synced Models: ✔️') }); }
|
||||||
containerCards();
|
catch { console.log('Synced Models: ❌'); }
|
||||||
}
|
}
|
||||||
await init();
|
await init().then(() => {
|
||||||
app.emit("appStarted");
|
console.log(`Listening on http://localhost:${port} ✔️`);
|
||||||
console.log(`\nServer listening on http://localhost:${port}`);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get hidden containers
|
||||||
|
async function getHidden() {
|
||||||
|
hidden = await Container.findAll({ where: {visibility:false}});
|
||||||
|
hidden = hidden.map((container) => container.name);
|
||||||
|
}
|
||||||
|
|
||||||
// Server metrics
|
// Server metrics
|
||||||
|
let [ cpu, ram, tx, rx, disk ] = [0, 0, 0, 0, 0];
|
||||||
let serverMetrics = async () => {
|
let serverMetrics = async () => {
|
||||||
currentLoad().then(data => {
|
currentLoad().then(data => {
|
||||||
cpu = Math.round(data.currentLoad);
|
cpu = Math.round(data.currentLoad);
|
||||||
|
@ -105,8 +80,9 @@ let serverMetrics = async () => {
|
||||||
disk = data[0].use;
|
disk = data[0].use;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setInterval(serverMetrics, 1000);
|
||||||
|
|
||||||
// List docker containers
|
// Docker containers
|
||||||
let containerCards = async () => {
|
let containerCards = async () => {
|
||||||
let list = '';
|
let list = '';
|
||||||
const allContainers = await docker.listContainers({ all: true });
|
const allContainers = await docker.listContainers({ all: true });
|
||||||
|
@ -151,120 +127,100 @@ let containerCards = async () => {
|
||||||
cardList = list;
|
cardList = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container metrics
|
|
||||||
let containerStats = async () => {
|
|
||||||
const data = await docker.listContainers({ all: true });
|
|
||||||
for (const container of data) {
|
|
||||||
if (!hidden.includes(container.Names[0].slice(1))) {
|
|
||||||
const stats = await dockerContainerStats(container.Id);
|
|
||||||
const name = container.Names[0].slice(1);
|
|
||||||
|
|
||||||
if (!statsArray[name]) {
|
|
||||||
statsArray[name] = {
|
|
||||||
cpuArray: Array(15).fill(0),
|
|
||||||
ramArray: Array(15).fill(0)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
statsArray[name].cpuArray.push(Math.round(stats[0].cpuPercent));
|
|
||||||
statsArray[name].ramArray.push(Math.round(stats[0].memPercent));
|
|
||||||
|
|
||||||
statsArray[name].cpuArray = statsArray[name].cpuArray.slice(-15);
|
|
||||||
statsArray[name].ramArray = statsArray[name].ramArray.slice(-15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store docker events
|
// Store docker events
|
||||||
docker.getEvents((err, stream) => {
|
docker.getEvents((err, stream) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
stream.on('data', (chunk) => {
|
stream.on('data', (chunk) => {
|
||||||
dockerEvents += chunk.toString('utf8');
|
activeEvent += chunk.toString('utf8');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for docker events
|
// Check if the container cards need to be updated
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
if (dockerEvents != '') {
|
if (activeEvent == '') { return; }
|
||||||
|
activeEvent = '';
|
||||||
await getHidden();
|
await getHidden();
|
||||||
await containerCards();
|
await containerCards();
|
||||||
dockerEvents = '';
|
if (cardList != sentList) {
|
||||||
|
cardList = sentList;
|
||||||
|
SSE = true;
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Get hidden containers
|
// HTMX triggers
|
||||||
async function getHidden() {
|
router.get('/stats', async (req, res) => {
|
||||||
hidden = await Container.findAll({ where: {visibility:false}});
|
switch (req.header('HX-Trigger')) {
|
||||||
hidden = hidden.map((container) => container.name);
|
case 'cpu':
|
||||||
}
|
let info = '<div class="font-weight-medium">';
|
||||||
|
info += '<label class="cpu-text mb-1" for="cpu">CPU ' + cpu + '%</label>';
|
||||||
|
info += '</div>';
|
||||||
|
info += '<div class="cpu-bar meter animate">';
|
||||||
|
info += '<span style="width:' + cpu + '%"><span></span></span>';
|
||||||
|
info += '</div>';
|
||||||
|
res.send(info);
|
||||||
|
break;
|
||||||
|
case 'ram':
|
||||||
|
let info2 = '<div class="font-weight-medium">';
|
||||||
|
info2 += '<label class="ram-text mb-1" for="ram">RAM ' + ram + '%</label>';
|
||||||
|
info2 += '</div>';
|
||||||
|
info2 += '<div class="ram-bar meter animate orange">';
|
||||||
|
info2 += '<span style="width:' + ram + '%"><span></span></span>';
|
||||||
|
info2 += '</div>';
|
||||||
|
res.send(info2);
|
||||||
|
break;
|
||||||
|
case 'tx':
|
||||||
|
res.send('TX ' + tx.toFixed(2) + ' MB');
|
||||||
|
break;
|
||||||
|
case 'rx':
|
||||||
|
res.send('RX ' + rx.toFixed(2) + ' MB');
|
||||||
|
break;
|
||||||
|
case 'disk':
|
||||||
|
let info5 = '<div class="font-weight-medium">';
|
||||||
|
info5 += '<label class="disk-text mb-1" for="disk">Disk ' + disk + '%</label>';
|
||||||
|
info5 += '</div>';
|
||||||
|
info5 += '<div class="disk-bar meter animate red">';
|
||||||
|
info5 += '<span style="width:' + disk + '%"><span></span></span>';
|
||||||
|
info5 += '</div>';
|
||||||
|
res.send(info5);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('Unknown trigger');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Socket.io
|
router.get('/containers', async (req, res) => {
|
||||||
io.on('connection', (socket) => {
|
await getHidden();
|
||||||
let sessionData = socket.request.session;
|
await containerCards();
|
||||||
let sent = '';
|
sentList = cardList;
|
||||||
if (sessionData.user != undefined) {
|
res.send(cardList);
|
||||||
console.log(`${sessionData.user} connected from ${socket.handshake.headers.host}`);
|
});
|
||||||
|
|
||||||
// Start intervals if not already started
|
// Server-side event trigger
|
||||||
if (!metricsInterval) {
|
router.get('/sse_event', (req, res) => {
|
||||||
serverMetrics();
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', });
|
||||||
metricsInterval = setInterval(serverMetrics, 1000);
|
|
||||||
console.log('Metrics interval started');
|
|
||||||
}
|
|
||||||
if (!cardsInterval) {
|
|
||||||
containerCards();
|
|
||||||
cardsInterval = setInterval(containerCards, 1000);
|
|
||||||
console.log('Cards interval started');
|
|
||||||
}
|
|
||||||
if (!graphsInterval) {
|
|
||||||
containerStats();
|
|
||||||
graphsInterval = setInterval(containerStats, 1000);
|
|
||||||
console.log('Graphs interval started');
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(() => {
|
let eventCheck = setInterval(async () => {
|
||||||
socket.emit('metrics', [cpu, ram, tx, rx, disk]);
|
if (SSE == true) {
|
||||||
if (sent != cardList) {
|
SSE = false;
|
||||||
sent = cardList;
|
res.write(`event: docker\n`);
|
||||||
socket.emit('containers', cardList);
|
res.write(`data: there was a docker event!\n\n`);
|
||||||
|
console.log(`server-side event sent`)
|
||||||
}
|
}
|
||||||
socket.emit('containerStats', statsArray);
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
clearInterval(eventCheck);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Client input
|
// HTMX buttons
|
||||||
socket.on('clicked', (data) => {
|
router.get('/click', async (req, res) => {
|
||||||
let { name, id, value } = data;
|
|
||||||
console.log(`${sessionData.user} clicked: ${id} ${value} ${name}`);
|
|
||||||
if (clicked == true) { return; } clicked = true;
|
|
||||||
|
|
||||||
// View container logs
|
let name = req.header('hx-trigger-name');
|
||||||
if (id == 'logs'){
|
let id = req.header('hx-trigger');
|
||||||
function containerLogs (data) {
|
let value = req.query[name];
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let logString = '';
|
|
||||||
var options = {
|
|
||||||
follow: false,
|
|
||||||
stdout: true,
|
|
||||||
stderr: false,
|
|
||||||
timestamps: false
|
|
||||||
};
|
|
||||||
var containerName = docker.getContainer(data);
|
|
||||||
containerName.logs(options, function (err, stream) {
|
|
||||||
if (err) { reject(err); return; }
|
|
||||||
const readableStream = Readable.from(stream);
|
|
||||||
readableStream.on('data', function (chunk) {
|
|
||||||
logString += chunk.toString('utf8');
|
|
||||||
});
|
|
||||||
readableStream.on('end', function () {
|
|
||||||
resolve(logString);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
containerLogs(name).then((data) => {
|
|
||||||
socket.emit('logs', data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// start, stop, pause, restart container
|
// start, stop, pause, restart container
|
||||||
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
|
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
|
||||||
|
@ -287,57 +243,82 @@ io.on('connection', (socket) => {
|
||||||
|
|
||||||
// hide container
|
// hide container
|
||||||
if (id == 'hide') {
|
if (id == 'hide') {
|
||||||
async function hideContainer() {
|
let exists = await Container.findOne({ where: {name: name}});
|
||||||
let containerExists = await Container.findOne({ where: {name: name}});
|
if (!exists) {
|
||||||
if(!containerExists) {
|
|
||||||
const newContainer = await Container.create({ name: name, visibility: false, });
|
const newContainer = await Container.create({ name: name, visibility: false, });
|
||||||
getHidden();
|
SSE = true;
|
||||||
} else {
|
} else {
|
||||||
containerExists.update({ visibility: false });
|
exists.update({ visibility: false });
|
||||||
getHidden();
|
SSE = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// reset hidden
|
||||||
hideContainer();
|
if (id == 'reset') {
|
||||||
}
|
|
||||||
|
|
||||||
// unhide containers
|
|
||||||
if (id == 'resetView') {
|
|
||||||
Container.update({ visibility: true }, { where: {} });
|
Container.update({ visibility: true }, { where: {} });
|
||||||
getHidden();
|
SSE = true;
|
||||||
}
|
|
||||||
|
|
||||||
clicked = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
console.log(`${sessionData.user} disconnected`);
|
|
||||||
socket.disconnect();
|
|
||||||
// clear intervals if no users are connected
|
|
||||||
if (io.engine.clientsCount == 0) {
|
|
||||||
clearInterval(metricsInterval);
|
|
||||||
clearInterval(cardsInterval);
|
|
||||||
clearInterval(graphsInterval);
|
|
||||||
metricsInterval = null;
|
|
||||||
cardsInterval = null;
|
|
||||||
graphsInterval = null;
|
|
||||||
console.log('All intervals cleared');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('Missing session data');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// container charts
|
||||||
|
router.get('/chart', async (req, res) => {
|
||||||
|
let name = req.header('hx-trigger-name');
|
||||||
|
let chart = `
|
||||||
|
<script>
|
||||||
|
var options = {
|
||||||
|
chart: {
|
||||||
|
type: "line",
|
||||||
|
height: 40.0,
|
||||||
|
sparkline: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: [2, 1],
|
||||||
|
dashArray: [0, 3],
|
||||||
|
lineCap: "round",
|
||||||
|
curve: "smooth"
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: "CPU",
|
||||||
|
data: [0,10,0,10,0,10,0,10,0,10]
|
||||||
|
}, {
|
||||||
|
name: "RAM",
|
||||||
|
data: [0,5,0,5,0,5,0,5,0,5]
|
||||||
|
}],
|
||||||
|
tooltip: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
strokeDashArray: 4
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
labels: {
|
||||||
|
padding: 0
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
padding: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chart = new ApexCharts(document.querySelector("#${name}_chart"), options);
|
||||||
|
chart.render();
|
||||||
|
</script>`
|
||||||
|
res.send(chart);
|
||||||
|
});
|
||||||
|
|
||||||
// let link = '';
|
|
||||||
// networkInterfaces().then(data => {
|
|
||||||
// link = data[0].ip4;
|
|
||||||
// });
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<body >
|
<body >
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Page header -->
|
<!-- Page header -->
|
||||||
<div class="page-header d-print-none">
|
<div class="page-header d-print-none">
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Libs JS -->
|
<!-- Libs JS -->
|
|
@ -22,7 +22,7 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
|
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Page header -->
|
<!-- Page header -->
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -8,6 +8,8 @@
|
||||||
<!-- CSS files -->
|
<!-- CSS files -->
|
||||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||||
<link href="/css/meters.css" rel="stylesheet"/>
|
<link href="/css/meters.css" rel="stylesheet"/>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
|
||||||
<style>
|
<style>
|
||||||
@import url('/fonts/inter.css');
|
@import url('/fonts/inter.css');
|
||||||
:root {
|
:root {
|
||||||
|
@ -19,16 +21,18 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body >
|
<body >
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
|
||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
<div class="row row-deck row-cards">
|
<div class="row row-deck row-cards">
|
||||||
|
|
||||||
<div class="col-12" id="cards">
|
<div class="col-12">
|
||||||
<div class="row row-cards">
|
<div class="row row-cards">
|
||||||
|
|
||||||
<div class="col-sm-6 col-lg-3">
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
@ -40,14 +44,17 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
|
||||||
|
<!-- HTMX -->
|
||||||
|
<div class="col" id="cpu" data-hx-get="/stats" data-hx-trigger="load, every 1s" data-hx-target="#cpu">
|
||||||
<div class="font-weight-medium">
|
<div class="font-weight-medium">
|
||||||
<label id="cpu-text" class="cpu-text mb-1" for="cpu">CPU 0%</label>
|
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="cpu-bar" class="cpu-bar meter animate">
|
<div class="cpu-bar meter animate">
|
||||||
<span style="width: 25%"><span></span></span>
|
<span style="width:20%"><span></span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,14 +69,17 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
|
||||||
|
<!-- HTMX -->
|
||||||
|
<div class="col" id="ram" data-hx-get="/stats" data-hx-trigger="load, every 2s">
|
||||||
<div class="font-weight-medium">
|
<div class="font-weight-medium">
|
||||||
<label id="ram-text" class="ram-text mb-1" for="ram">RAM 0%</label>
|
<label class="ram-text mb-1" for="ram">RAM 0%</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="ram-bar" class="ram-bar meter animate orange">
|
<div class="ram-bar meter animate orange">
|
||||||
<span style="width: 25%"><span></span></span>
|
<span style="width:20%"><span></span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,7 +99,7 @@
|
||||||
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
|
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="net-bar" class="meter animate blue">
|
<div id="net-bar" class="meter animate blue">
|
||||||
<span style="width: 25%"><span></span></span>
|
<span style="width: 20%"><span></span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,14 +116,17 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
|
||||||
|
<!-- HTMX -->
|
||||||
|
<div class="col" id="disk" data-hx-get="/stats" data-hx-trigger="load, every 2s">
|
||||||
<div class="font-weight-medium">
|
<div class="font-weight-medium">
|
||||||
<label id="disk-text" class="disk-text mb-1" for="disk">DISK 0%</label>
|
<label class="disk-text mb-1" for="disk">DISK 0%</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="disk-bar" class="meter animate red">
|
<div class="meter animate red">
|
||||||
<span style="width: 25%"><span></span></span>
|
<span style="width:20%"><span></span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,8 +136,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- HTMX -->
|
||||||
|
<div class="col-12" hx-ext="sse" sse-connect="/sse_event">
|
||||||
|
<div class="row row-cards" data-hx-get="/containers" data-hx-trigger="load, sse:docker" data-hx-swap="innerHTML">
|
||||||
|
|
||||||
<!-- containerCards get inserted here from public/js/main.js -->
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal modal-blur fade" id="details_modal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal modal-blur fade" id="details_modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||||
|
@ -989,22 +1006,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Libs JS -->
|
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
|
||||||
<script src="/libs/apexcharts/dist/apexcharts.min.js" defer></script>
|
|
||||||
<!-- Tabler Core -->
|
|
||||||
<script src="/js/tabler.min.js"></script>
|
<script src="/js/tabler.min.js"></script>
|
||||||
<!-- Socket.io -->
|
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
|
||||||
<script>
|
|
||||||
const socket = io();
|
|
||||||
</script>
|
|
||||||
<script src="/js/main.js"></script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -24,7 +24,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<a href="#" class="link-secondary" rel="noopener">
|
<a href="#" class="link-secondary" rel="noopener">
|
||||||
v0.10
|
v0.21
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
|
@ -20,7 +20,7 @@
|
||||||
<body >
|
<body >
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Page header -->
|
<!-- Page header -->
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -9,7 +9,7 @@
|
||||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||||
<style>
|
<style>
|
||||||
@import url('fonts/inter.css');
|
@import url('/fonts/inter.css');
|
||||||
:root {
|
:root {
|
||||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,18 @@
|
||||||
font-feature-settings: "cv03", "cv04", "cv11";
|
font-feature-settings: "cv03", "cv04", "cv11";
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class=" d-flex flex-column">
|
<body class=" d-flex flex-column">
|
||||||
<script src="/js/demo-theme.js"></script>
|
<script src="/js/demo-theme.js"></script>
|
||||||
<div class="page page-center">
|
<div class="page page-center">
|
||||||
<div class="container container-tight py-4">
|
<div class="container container-tight py-4">
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-4">
|
||||||
<a href="." class="navbar-brand navbar-brand-autodark"><img src="/static/logo.svg" height="50" alt=""></a>
|
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||||
|
|
||||||
|
<img src="/images/logo.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
||||||
|
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="card card-md">
|
<div class="card card-md">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -70,9 +75,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success" hx-get="/click" hx-trigger="click" hx-swap="innerHTML" id="incriment">0</button>
|
||||||
|
|
||||||
<!-- Libs JS -->
|
<!-- Libs JS -->
|
||||||
<!-- Tabler Core -->
|
<!-- Tabler Core -->
|
||||||
<script src="/js/tabler.min.js" defer></script>
|
<script src="/js/tabler.min.js" defer></script>
|
||||||
<script src="/js/demo.min.js" defer></script>
|
<script src="/js/demo.min.js" defer></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -35,7 +35,7 @@
|
||||||
</button>
|
</button>
|
||||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<img src="/static/logo.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
<img src="/images/logo.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="navbar-nav flex-row order-md-last">
|
<div class="navbar-nav flex-row order-md-last">
|
|
@ -20,7 +20,7 @@
|
||||||
<body >
|
<body >
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Page header -->
|
<!-- Page header -->
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -21,7 +21,7 @@
|
||||||
<body >
|
<body >
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
|
||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
|
@ -131,7 +131,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -9,7 +9,7 @@
|
||||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||||
<style>
|
<style>
|
||||||
@import url('fonts/inter.css');
|
@import url('/fonts/inter.css');
|
||||||
:root {
|
:root {
|
||||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
<form class="container container-tight py-4" action="/register" method="POST" novalidate>
|
<form class="container container-tight py-4" action="/register" method="POST" novalidate>
|
||||||
|
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-4">
|
||||||
<a href="#" class="navbar-brand navbar-brand-autodark d-none"><img src="/static/logo.svg" height="36" alt=""></a>
|
<a href="#" class="navbar-brand navbar-brand-autodark d-none"><img src="/images/logo.svg" height="36" alt=""></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="rus.jpg" class="form-imagecheck-input" checked/>
|
<input name="avatar" type="radio" value="rus.jpg" class="form-imagecheck-input" checked/>
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/rus.jpg" alt="Rich Uncle Skeleton" title="Rich Uncle Skeleton" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/rus.jpg" alt="Rich Uncle Skeleton" title="Rich Uncle Skeleton" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="burns.jpg" class="form-imagecheck-input"/>
|
<input name="avatar" type="radio" value="burns.jpg" class="form-imagecheck-input"/>
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/burns.jpg" alt="Montgomery Burns" title="Montgomery Burns" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/burns.jpg" alt="Montgomery Burns" title="Montgomery Burns" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="frank.jpg" class="form-imagecheck-input" />
|
<input name="avatar" type="radio" value="frank.jpg" class="form-imagecheck-input" />
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/frank.jpg" alt="Frank Grimes" title= "Frank Grimes" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/frank.jpg" alt="Frank Grimes" title= "Frank Grimes" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="moe.jpg" class="form-imagecheck-input"/>
|
<input name="avatar" type="radio" value="moe.jpg" class="form-imagecheck-input"/>
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/moe.jpg" alt="Moe Szyslak" title="Moe Szyslak" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/moe.jpg" alt="Moe Szyslak" title="Moe Szyslak" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="poochie.jpg" class="form-imagecheck-input" />
|
<input name="avatar" type="radio" value="poochie.jpg" class="form-imagecheck-input" />
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/poochie.jpg" alt="Poochie" title="Poochie" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/poochie.jpg" alt="Poochie" title="Poochie" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="skinner.jpg" class="form-imagecheck-input" />
|
<input name="avatar" type="radio" value="skinner.jpg" class="form-imagecheck-input" />
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/skinner.jpg" alt="Seymour Skinner" title="Seymour Skinner" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/skinner.jpg" alt="Seymour Skinner" title="Seymour Skinner" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,7 +136,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="moleman.png" class="form-imagecheck-input" />
|
<input name="avatar" type="radio" value="moleman.png" class="form-imagecheck-input" />
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/moleman.png" alt="Hans Moleman" title="Hans Moleman" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/moleman.png" alt="Hans Moleman" title="Hans Moleman" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
<label class="form-imagecheck mb-2">
|
<label class="form-imagecheck mb-2">
|
||||||
<input name="avatar" type="radio" value="duffman.png" class="form-imagecheck-input" />
|
<input name="avatar" type="radio" value="duffman.png" class="form-imagecheck-input" />
|
||||||
<span class="form-imagecheck-figure">
|
<span class="form-imagecheck-figure">
|
||||||
<img src="img/avatars/duffman.png" alt="Duffman" title="Duffman" class="form-imagecheck-image" width="100px">
|
<img src="/images/avatars/duffman.png" alt="Duffman" title="Duffman" class="form-imagecheck-image" width="100px">
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,11 +176,9 @@
|
||||||
<div class="d-none d-md-flex">
|
<div class="d-none d-md-flex">
|
||||||
|
|
||||||
<a href="?theme=dark" class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
<a href="?theme=dark" class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||||
<!-- Download SVG icon from http://tabler-icons.io/i/moon -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="?theme=light" class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
<a href="?theme=light" class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||||
<!-- Download SVG icon from http://tabler-icons.io/i/sun -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
|
@ -21,7 +21,7 @@
|
||||||
<body >
|
<body >
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Page header -->
|
<!-- Page header -->
|
||||||
<div class="page-header d-print-none">
|
<div class="page-header d-print-none">
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Libs JS -->
|
<!-- Libs JS -->
|
|
@ -21,7 +21,7 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
|
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Libs JS -->
|
<!-- Libs JS -->
|
|
@ -21,7 +21,7 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
|
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Libs JS -->
|
<!-- Libs JS -->
|
|
@ -20,7 +20,7 @@
|
||||||
<body >
|
<body >
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.html') %>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Page header -->
|
<!-- Page header -->
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%- include('footer.ejs') %>
|
<%- include('footer.html') %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|