Another big rewrite

This commit is contained in:
lllllllillllllillll 2024-01-28 00:33:50 -08:00
parent 9c79290560
commit 0f5575075e
42 changed files with 488 additions and 2896 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
**/db.sqlite
**/node_modules
**/appdata
.gitignore
**/screenshots

11
.gitignore vendored
View file

@ -1,8 +1,3 @@
**/node_modules/
**/database.sqlite
**/appdata/
.github
test
.dockerignore
.gitignore
docker-compose.yaml
**/db.sqlite
**/node_modules
**/appdata

View file

@ -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.

View file

@ -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"]
CMD ["node", "server.js"]

View file

@ -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: )
[![GitHub Activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![Docker Pulls](https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui)](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
[![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
[![GitHub License](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
[![Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](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.

View file

@ -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>

View file

@ -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) => {

View file

@ -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");

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

View file

@ -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>`;
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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") {
@ -58,4 +59,4 @@ router.get("/settings", auth, Settings);
// Functions
router.post("/install", auth, Install);
router.post("/uninstall", auth, Uninstall);
router.post("/uninstall", auth, Uninstall);

429
server.js
View file

@ -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,193 +127,198 @@ 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 != '') {
await getHidden();
await containerCards();
dockerEvents = '';
if (activeEvent == '') { return; }
activeEvent = '';
await getHidden();
await containerCards();
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);
}
// 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}`);
// 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');
}
setInterval(() => {
socket.emit('metrics', [cpu, ram, tx, rx, disk]);
if (sent != cardList) {
sent = cardList;
socket.emit('containers', cardList);
}
socket.emit('containerStats', statsArray);
}, 1000);
// 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;
// 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);
});
}
// start, stop, pause, restart container
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
var containerName = docker.getContainer(name);
if ((id == 'start') && (value == 'stopped')) {
containerName.start();
} else if ((id == 'start') && (value == 'paused')) {
containerName.unpause();
} else if ((id == 'stop') && (value != 'stopped')) {
containerName.stop();
} else if ((id == 'pause') && (value == 'running')) {
containerName.pause();
} else if ((id == 'pause') && (value == 'paused')) {
containerName.unpause();
} else if (id == 'restart') {
containerName.restart();
}
}
// hide container
if (id == 'hide') {
async function hideContainer() {
let containerExists = await Container.findOne({ where: {name: name}});
if(!containerExists) {
const newContainer = await Container.create({ name: name, visibility: false, });
getHidden();
} else {
containerExists.update({ visibility: false });
getHidden();
}
}
hideContainer();
}
// unhide containers
if (id == 'resetView') {
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');
// 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;
}
});
router.get('/containers', async (req, res) => {
await getHidden();
await containerCards();
sentList = cardList;
res.send(cardList);
});
// 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', });
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`)
}
}, 1000);
req.on('close', () => {
clearInterval(eventCheck);
});
});
// HTMX buttons
router.get('/click', async (req, res) => {
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'){
var containerName = docker.getContainer(name);
if ((id == 'start') && (value == 'stopped')) {
containerName.start();
} else if ((id == 'start') && (value == 'paused')) {
containerName.unpause();
} else if ((id == 'stop') && (value != 'stopped')) {
containerName.stop();
} else if ((id == 'pause') && (value == 'running')) {
containerName.pause();
} else if ((id == 'pause') && (value == 'paused')) {
containerName.unpause();
} else if (id == 'restart') {
containerName.restart();
}
}
// let link = '';
// networkInterfaces().then(data => {
// link = data[0].ip4;
// });
// hide container
if (id == 'hide') {
let exists = await Container.findOne({ where: {name: name}});
if (!exists) {
const newContainer = await Container.create({ name: name, visibility: false, });
SSE = true;
} else {
exists.update({ visibility: false });
SSE = true;
}
}
// reset hidden
if (id == 'reset') {
Container.update({ visibility: true }, { where: {} });
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
}
}
var chart = new ApexCharts(document.querySelector("#${name}_chart"), options);
chart.render();
</script>`
res.send(chart);
});

View file

@ -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 -->

View file

@ -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>

View file

@ -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,9 +136,13 @@
</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">
</div>
</div>
<!-- containerCards get inserted here from public/js/main.js -->
<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-content">
@ -989,23 +1006,15 @@
</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>

View file

@ -24,7 +24,7 @@
</li>
<li class="list-inline-item">
<a href="#" class="link-secondary" rel="noopener">
v0.10
v0.21
</a>
</li>
</ul>

View file

@ -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>

View file

@ -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,24 +17,29 @@
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">
<h2 class="h2 text-center mb-4">Login to your account</h2>
<form action="/login" method="POST" novalidate>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<div class="mb-2">
<label class="form-label">Email address</label>
@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 -->

View file

@ -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 -->

View file

@ -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 -->

View file

@ -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>