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/
|
||||
**/database.sqlite
|
||||
**/appdata/
|
||||
.github
|
||||
test
|
||||
.dockerignore
|
||||
.gitignore
|
||||
docker-compose.yaml
|
||||
**/db.sqlite
|
||||
**/node_modules
|
||||
**/appdata
|
|
@ -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.
|
||||
* Changed to ES6 imports.
|
||||
* Cleaned up file structure and code layout.
|
||||
|
|
20
Dockerfile
|
@ -1,19 +1,17 @@
|
|||
FROM node:21-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=bind,source=package.json,target=package.json \
|
||||
--mount=type=bind,source=package-lock.json,target=package-lock.json \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm ci --omit=dev
|
||||
|
||||
|
||||
USER root
|
||||
|
||||
COPY . .
|
||||
RUN chown node:node /app
|
||||
USER node
|
||||
|
||||
COPY package.json package-lock.json* /app/
|
||||
RUN npm ci && npm cache clean --force
|
||||
COPY . /app
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["node", "server.js"]
|
|
@ -1,7 +1,7 @@
|
|||
# DweebUI
|
||||
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)
|
||||
|
@ -10,7 +10,7 @@ Alpha v0.20 ( :fire: Experimental :fire: )
|
|||
[](https://github.com/lllllllillllllillll)
|
||||
[](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
|
||||
[](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.
|
||||
* Some UI elements are placeholders and every version may have breaking changes.
|
||||
|
|
|
@ -49,7 +49,7 @@ export const containerCard = (data) => {
|
|||
|
||||
|
||||
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-body">
|
||||
<div class="card-stamp card-stamp-sm">
|
||||
|
@ -60,16 +60,16 @@ export const containerCard = (data) => {
|
|||
<div class="ms-auto lh-1">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
<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>
|
||||
</a>
|
||||
<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" 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="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="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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,7 +111,9 @@ export const containerCard = (data) => {
|
|||
</span>
|
||||
</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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { appCard } from '../components/appCard.js';
|
||||
|
||||
let templatesJSON = readFileSync('./templates.json');
|
||||
let templatesJSON = readFileSync('./templates/templates.json');
|
||||
let templates = JSON.parse(templatesJSON).templates;
|
||||
|
||||
templates = templates.sort((a, b) => {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { User, Syslog } from '../database/models.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
|
||||
|
||||
export const Login = function(req,res){
|
||||
if(req.session.user){
|
||||
res.redirect("/logout");
|
||||
|
|
|
@ -55,7 +55,7 @@ export const submitRegister = async function(req,res){
|
|||
password: bcrypt.hashSync(password,10),
|
||||
role: await userRole(),
|
||||
group: 'all',
|
||||
avatar: `<img src="img/avatars/${avatar}">`,
|
||||
avatar: `<img src="/images/avatars/${avatar}">`,
|
||||
lastLogin: newLogin,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
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({
|
||||
dialect: 'sqlite',
|
||||
storage: './database/database.sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -2,16 +2,15 @@ version: "3.9"
|
|||
services:
|
||||
dweebui:
|
||||
container_name: dweebui
|
||||
image: lllllllillllllillll/dweebui:v0.20
|
||||
image: lllllllillllllillll/dweebui:v0.21-dev
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 8000
|
||||
SECRET: MrWiskers
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
- dweebui:/app/database/db.sqlite
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- dweebui_net
|
||||
|
|
2547
package-lock.json
generated
20
package.json
|
@ -1,37 +1,27 @@
|
|||
{
|
||||
"name": "dweebui",
|
||||
"version": "1.0.0",
|
||||
"description": "A web UI for Docker",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --require @babel/register"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/register": "^7.23.7",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"chai": "^5.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^4.0.2",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-session": "^1.17.3",
|
||||
"helmet": "^7.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mocha": "^10.2.0",
|
||||
"memorystore": "^1.6.7",
|
||||
"sequelize": "^6.35.2",
|
||||
"sinon": "^17.0.1",
|
||||
"socket.io": "^4.7.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
"stream": "^0.0.2",
|
||||
"supertest": "^6.3.3",
|
||||
"systeminformation": "^5.21.22"
|
||||
"systeminformation": "^5.21.23"
|
||||
}
|
||||
}
|
||||
|
|
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 { Portal } from "../controllers/portal.js"
|
||||
|
||||
/// Functions
|
||||
// Functions
|
||||
import { Install } from "../functions/install.js"
|
||||
import { Uninstall } from "../functions/uninstall.js"
|
||||
|
||||
|
||||
// Auth middleware
|
||||
const auth = (req, res, next) => {
|
||||
if (req.session.role == "admin") {
|
||||
|
|
369
server.js
|
@ -1,44 +1,26 @@
|
|||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import compression from 'compression';
|
||||
import helmet from 'helmet';
|
||||
import memorystore from 'memorystore';
|
||||
import ejs from 'ejs';
|
||||
import Docker from 'dockerode';
|
||||
import cors from 'cors';
|
||||
import { Readable } from 'stream';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { instrument } from '@socket.io/admin-ui'
|
||||
import { router } from './router/index.js';
|
||||
import { createServer } from 'node:http';
|
||||
import { Server } from 'socket.io';
|
||||
import { sequelize, Container } from './database/models.js';
|
||||
import { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces } from 'systeminformation';
|
||||
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();
|
||||
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
|
||||
export const io = new Server(server, {
|
||||
cors: {
|
||||
origin: ['http://localhost:8000', 'https://admin.socket.io'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
instrument(io, {
|
||||
auth: false,
|
||||
readonly: true
|
||||
});
|
||||
const app = express();
|
||||
const MemoryStore = memorystore(session);
|
||||
const port = process.env.PORT || 8000;
|
||||
let [ hidden, activeEvent, cardList, clicked ] = ['', '', '', false];
|
||||
let sentList = '';
|
||||
let SSE = false;
|
||||
let clicks = 0;
|
||||
|
||||
// Session middleware
|
||||
const sessionMiddleware = session({
|
||||
store: new MemoryStore({ checkPeriod: 86400000 }), // Prune expired entries every 24h
|
||||
secret: "keyboard cat",
|
||||
resave: 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
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('view engine', 'html');
|
||||
app.engine('html', ejs.renderFile);
|
||||
app.use([
|
||||
compression(),
|
||||
cors(),
|
||||
helmet({contentSecurityPolicy: false}),
|
||||
express.static("public"),
|
||||
express.static('public'),
|
||||
express.json(),
|
||||
express.urlencoded({ extended: true }),
|
||||
sessionMiddleware,
|
||||
router,
|
||||
limiter
|
||||
router
|
||||
]);
|
||||
|
||||
// Initialize server
|
||||
server.listen(port, async () => {
|
||||
app.listen(port, async () => {
|
||||
async function init() {
|
||||
try { await sequelize.authenticate().then(() => { console.log('[Connected to DB]') }); }
|
||||
catch { console.log('[Could not connect to DB]'); }
|
||||
try { await sequelize.sync().then(() => { console.log('[Models Synced]') }); }
|
||||
catch { console.log('[Could not Sync Models]', error); }
|
||||
await getHidden();
|
||||
containerCards();
|
||||
try { await sequelize.authenticate().then(
|
||||
() => { console.log('DB Connection: ✔️') }); }
|
||||
catch { console.log('DB Connection: ❌'); }
|
||||
try { await sequelize.sync().then( // check out that formatting
|
||||
() => { console.log('Synced Models: ✔️') }); }
|
||||
catch { console.log('Synced Models: ❌'); }
|
||||
}
|
||||
await init();
|
||||
app.emit("appStarted");
|
||||
console.log(`\nServer listening on http://localhost:${port}`);
|
||||
await init().then(() => {
|
||||
console.log(`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
|
||||
let [ cpu, ram, tx, rx, disk ] = [0, 0, 0, 0, 0];
|
||||
let serverMetrics = async () => {
|
||||
currentLoad().then(data => {
|
||||
cpu = Math.round(data.currentLoad);
|
||||
|
@ -105,8 +80,9 @@ let serverMetrics = async () => {
|
|||
disk = data[0].use;
|
||||
});
|
||||
}
|
||||
setInterval(serverMetrics, 1000);
|
||||
|
||||
// List docker containers
|
||||
// Docker containers
|
||||
let containerCards = async () => {
|
||||
let list = '';
|
||||
const allContainers = await docker.listContainers({ all: true });
|
||||
|
@ -151,120 +127,100 @@ let containerCards = async () => {
|
|||
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
|
||||
docker.getEvents((err, stream) => {
|
||||
if (err) throw err;
|
||||
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 () => {
|
||||
if (dockerEvents != '') {
|
||||
if (activeEvent == '') { return; }
|
||||
activeEvent = '';
|
||||
await getHidden();
|
||||
await containerCards();
|
||||
dockerEvents = '';
|
||||
if (cardList != sentList) {
|
||||
cardList = sentList;
|
||||
SSE = true;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Get hidden containers
|
||||
async function getHidden() {
|
||||
hidden = await Container.findAll({ where: {visibility:false}});
|
||||
hidden = hidden.map((container) => container.name);
|
||||
}
|
||||
// HTMX triggers
|
||||
router.get('/stats', async (req, res) => {
|
||||
switch (req.header('HX-Trigger')) {
|
||||
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
|
||||
io.on('connection', (socket) => {
|
||||
let sessionData = socket.request.session;
|
||||
let sent = '';
|
||||
if (sessionData.user != undefined) {
|
||||
console.log(`${sessionData.user} connected from ${socket.handshake.headers.host}`);
|
||||
router.get('/containers', async (req, res) => {
|
||||
await getHidden();
|
||||
await containerCards();
|
||||
sentList = cardList;
|
||||
res.send(cardList);
|
||||
});
|
||||
|
||||
// Start intervals if not already started
|
||||
if (!metricsInterval) {
|
||||
serverMetrics();
|
||||
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');
|
||||
}
|
||||
// Server-side event trigger
|
||||
router.get('/sse_event', (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', });
|
||||
|
||||
setInterval(() => {
|
||||
socket.emit('metrics', [cpu, ram, tx, rx, disk]);
|
||||
if (sent != cardList) {
|
||||
sent = cardList;
|
||||
socket.emit('containers', cardList);
|
||||
let eventCheck = setInterval(async () => {
|
||||
if (SSE == true) {
|
||||
SSE = false;
|
||||
res.write(`event: docker\n`);
|
||||
res.write(`data: there was a docker event!\n\n`);
|
||||
console.log(`server-side event sent`)
|
||||
}
|
||||
socket.emit('containerStats', statsArray);
|
||||
}, 1000);
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(eventCheck);
|
||||
});
|
||||
});
|
||||
|
||||
// Client input
|
||||
socket.on('clicked', (data) => {
|
||||
let { name, id, value } = data;
|
||||
console.log(`${sessionData.user} clicked: ${id} ${value} ${name}`);
|
||||
if (clicked == true) { return; } clicked = true;
|
||||
// HTMX buttons
|
||||
router.get('/click', async (req, res) => {
|
||||
|
||||
// View container logs
|
||||
if (id == 'logs'){
|
||||
function containerLogs (data) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
let name = req.header('hx-trigger-name');
|
||||
let id = req.header('hx-trigger');
|
||||
let value = req.query[name];
|
||||
|
||||
// start, stop, pause, restart container
|
||||
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
|
||||
|
@ -287,57 +243,82 @@ io.on('connection', (socket) => {
|
|||
|
||||
// hide container
|
||||
if (id == 'hide') {
|
||||
async function hideContainer() {
|
||||
let containerExists = await Container.findOne({ where: {name: name}});
|
||||
if(!containerExists) {
|
||||
let exists = await Container.findOne({ where: {name: name}});
|
||||
if (!exists) {
|
||||
const newContainer = await Container.create({ name: name, visibility: false, });
|
||||
getHidden();
|
||||
SSE = true;
|
||||
} else {
|
||||
containerExists.update({ visibility: false });
|
||||
getHidden();
|
||||
exists.update({ visibility: false });
|
||||
SSE = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
hideContainer();
|
||||
}
|
||||
|
||||
// unhide containers
|
||||
if (id == 'resetView') {
|
||||
// reset hidden
|
||||
if (id == 'reset') {
|
||||
Container.update({ visibility: true }, { where: {} });
|
||||
getHidden();
|
||||
}
|
||||
|
||||
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');
|
||||
SSE = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// let link = '';
|
||||
// networkInterfaces().then(data => {
|
||||
// link = data[0].ip4;
|
||||
// });
|
||||
|
||||
var chart = new ApexCharts(document.querySelector("#${name}_chart"), options);
|
||||
chart.render();
|
||||
</script>`
|
||||
res.send(chart);
|
||||
});
|
|
@ -21,7 +21,7 @@
|
|||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
|
@ -133,7 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
|
@ -22,7 +22,7 @@
|
|||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
@ -98,7 +98,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -8,6 +8,8 @@
|
|||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.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>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
|
@ -19,16 +21,18 @@
|
|||
</style>
|
||||
</head>
|
||||
<body >
|
||||
|
||||
<div class="page">
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12" id="cards">
|
||||
<div class="col-12">
|
||||
<div class="row row-cards">
|
||||
|
||||
<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>
|
||||
</span>
|
||||
</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">
|
||||
<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 id="cpu-bar" class="cpu-bar meter animate">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
<div class="cpu-bar meter animate">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</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>
|
||||
</span>
|
||||
</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">
|
||||
<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 id="ram-bar" class="ram-bar meter animate orange">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
<div class="ram-bar meter animate orange">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</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>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -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>
|
||||
</span>
|
||||
</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">
|
||||
<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 id="disk-bar" class="meter animate red">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
<div class="meter animate red">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -123,8 +136,12 @@
|
|||
</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-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
|
@ -989,22 +1006,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/apexcharts/dist/apexcharts.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/libs/apexcharts/dist/apexcharts.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>
|
||||
</html>
|
|
@ -24,7 +24,7 @@
|
|||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="#" class="link-secondary" rel="noopener">
|
||||
v0.10
|
||||
v0.21
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
|
@ -20,7 +20,7 @@
|
|||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
|
@ -144,7 +144,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -9,7 +9,7 @@
|
|||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('fonts/inter.css');
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--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";
|
||||
}
|
||||
</style>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body class=" d-flex flex-column">
|
||||
<script src="/js/demo-theme.js"></script>
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-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 class="card card-md">
|
||||
<div class="card-body">
|
||||
|
@ -70,9 +75,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" hx-get="/click" hx-trigger="click" hx-swap="innerHTML" id="incriment">0</button>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -35,7 +35,7 @@
|
|||
</button>
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<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>
|
||||
</h1>
|
||||
<div class="navbar-nav flex-row order-md-last">
|
|
@ -20,7 +20,7 @@
|
|||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
|
@ -144,7 +144,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -21,7 +21,7 @@
|
|||
<body >
|
||||
<div class="page">
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
|
||||
<div class="page-body">
|
||||
|
@ -131,7 +131,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -9,7 +9,7 @@
|
|||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('fonts/inter.css');
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--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>
|
||||
|
||||
<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 class="card">
|
||||
|
@ -88,7 +88,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="rus.jpg" class="form-imagecheck-input" checked/>
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -96,7 +96,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="burns.jpg" class="form-imagecheck-input"/>
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -104,7 +104,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="frank.jpg" class="form-imagecheck-input" />
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -112,7 +112,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="moe.jpg" class="form-imagecheck-input"/>
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -120,7 +120,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="poochie.jpg" class="form-imagecheck-input" />
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -128,7 +128,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="skinner.jpg" class="form-imagecheck-input" />
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -136,7 +136,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="moleman.png" class="form-imagecheck-input" />
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -144,7 +144,7 @@
|
|||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="duffman.png" class="form-imagecheck-input" />
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -176,11 +176,9 @@
|
|||
<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">
|
||||
<!-- 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>
|
||||
</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">
|
||||
<!-- 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>
|
||||
</a>
|
||||
</div>
|
|
@ -21,7 +21,7 @@
|
|||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
|
@ -125,7 +125,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
|
@ -21,7 +21,7 @@
|
|||
<div class="page">
|
||||
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
|
@ -68,7 +68,7 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
|
@ -21,7 +21,7 @@
|
|||
<div class="page">
|
||||
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
|
@ -20,7 +20,7 @@
|
|||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
|
@ -144,7 +144,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|