Portal page for regular users. Fix apps.js submit.
This commit is contained in:
parent
a105e5fbb6
commit
6d8a919d18
13 changed files with 392 additions and 14 deletions
3
app.js
3
app.js
|
@ -212,14 +212,17 @@ io.on('connection', (socket) => {
|
|||
|
||||
// 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');
|
||||
}
|
||||
|
|
|
@ -219,7 +219,7 @@ export const appCard = (data) => {
|
|||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 text-center">
|
||||
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"></img></span>
|
||||
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"/></span>
|
||||
<h3 class="m-0 mb-1"><a href="#">${shortened_name}</a></h3>
|
||||
<div class="text-secondary">${shortened_desc}</div>
|
||||
<div class="mt-3">
|
||||
|
|
|
@ -11,6 +11,8 @@ templates = templates.sort((a, b) => {
|
|||
});
|
||||
|
||||
export const Apps = (req, res) => {
|
||||
// console.log(req.body);
|
||||
|
||||
let page = Number(req.params.page) || 1;
|
||||
let list_start = (page-1)*28;
|
||||
let list_end = (page*28);
|
||||
|
@ -50,6 +52,7 @@ export const Apps = (req, res) => {
|
|||
|
||||
export const searchApps = async (req, res) => {
|
||||
|
||||
// console.log(req.body);
|
||||
let page = Number(req.query.page) || 1;
|
||||
let list_start = (page - 1) * 28;
|
||||
let list_end = (page * 28);
|
||||
|
@ -67,13 +70,14 @@ export const searchApps = async (req, res) => {
|
|||
let apps_list = '';
|
||||
let search_results = [];
|
||||
|
||||
console.log(req.body);
|
||||
let search = req.body.search;
|
||||
|
||||
// split value of search into an array of words
|
||||
search = search.split(' ');
|
||||
try {console.log(search[0]);} catch (error) {}
|
||||
try {console.log(search[1]);} catch (error) {}
|
||||
try {console.log(search[2]);} catch (error) {}
|
||||
// try {console.log(search[0]);} catch (error) {}
|
||||
// try {console.log(search[1]);} catch (error) {}
|
||||
// try {console.log(search[2]);} catch (error) {}
|
||||
|
||||
function searchTemplates(word) {
|
||||
|
||||
|
|
12
controllers/portal.js
Normal file
12
controllers/portal.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
export const Portal = (req, res) => {
|
||||
|
||||
|
||||
res.render("portal", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
});
|
||||
|
||||
}
|
||||
|
|
@ -159,4 +159,31 @@ export const Syslog = sequelize.define('Syslog', {
|
|||
ip : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const Notification = sequelize.define('Notification', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
createdAt : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdBy : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
213
functions/install.js
Normal file
213
functions/install.js
Normal file
|
@ -0,0 +1,213 @@
|
|||
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
||||
import yaml from 'js-yaml';
|
||||
import { execSync } from "child_process";
|
||||
import { docker } from "../app.js";
|
||||
import DockerodeCompose from "dockerode-compose";
|
||||
|
||||
|
||||
|
||||
export const Install = async (req, res) => {
|
||||
|
||||
console.log('getInstall:')
|
||||
console.log(req.body);
|
||||
|
||||
res.render("/apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
list_start: 0,
|
||||
list_end: 28,
|
||||
app_count: 0,
|
||||
prev: 0,
|
||||
next: 0,
|
||||
apps_list: 0,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// module.exports.install = async function (data) {
|
||||
|
||||
// console.log(`[Start of install function]`);
|
||||
|
||||
// let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
|
||||
// let { port0, port1, port2, port3, port4, port5 } = data;
|
||||
// let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
|
||||
// let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
|
||||
// let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
|
||||
|
||||
// let docker_volumes = [];
|
||||
|
||||
// if (image.startsWith('https://')){
|
||||
// mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
// execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
|
||||
// console.log(`Downloaded stackfile: ${image}`);
|
||||
// let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
|
||||
// let services = Object.keys(stackfile.services);
|
||||
|
||||
// for ( let i = 0; i < services.length; i++ ) {
|
||||
// try {
|
||||
// console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
|
||||
// } catch { console.log('no env') }
|
||||
// }
|
||||
|
||||
// } else {
|
||||
|
||||
// let compose_file = `version: '3'`;
|
||||
// compose_file += `\nservices:`
|
||||
// compose_file += `\n ${service_name}:`
|
||||
// compose_file += `\n container_name: ${name}`;
|
||||
// compose_file += `\n image: ${image}`;
|
||||
|
||||
// // Command
|
||||
// if (command_check == 'on') {
|
||||
// compose_file += `\n command: ${command}`
|
||||
// }
|
||||
|
||||
// // Network mode
|
||||
// if (net_mode == 'host') {
|
||||
// compose_file += `\n network_mode: 'host'`
|
||||
// }
|
||||
// else if (net_mode != 'host' && net_mode != 'docker') {
|
||||
// compose_file += `\n network_mode: '${net_mode}'`
|
||||
// }
|
||||
|
||||
// // Restart policy
|
||||
// if (restart_policy != '') {
|
||||
// compose_file += `\n restart: ${restart_policy}`
|
||||
// }
|
||||
|
||||
// // Ports
|
||||
// if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
|
||||
// compose_file += `\n ports:`
|
||||
|
||||
// for (let i = 0; i < 6; i++) {
|
||||
// if (data[`port${i}`] == 'on') {
|
||||
// compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Volumes
|
||||
// if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
|
||||
// compose_file += `\n volumes:`
|
||||
|
||||
// for (let i = 0; i < 6; i++) {
|
||||
|
||||
// // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
|
||||
// if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
|
||||
// compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
// }
|
||||
|
||||
// // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
|
||||
// else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
|
||||
// let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
|
||||
// compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
// docker_volumes.push(`${name}_${volume_name}`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Environment variables
|
||||
// if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
// compose_file += `\n environment:`
|
||||
// }
|
||||
// for (let i = 0; i < 12; i++) {
|
||||
// if (data[`env${i}`] == 'on') {
|
||||
// compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Add labels
|
||||
// if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
|
||||
// compose_file += `\n labels:`
|
||||
// }
|
||||
// for (let i = 0; i < 12; i++) {
|
||||
// if (data[`label${i}`] == 'on') {
|
||||
// compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Add privileged mode
|
||||
|
||||
// if (data.privileged == 'on') {
|
||||
// compose_file += `\n privileged: true`
|
||||
// }
|
||||
|
||||
|
||||
// // Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
|
||||
// if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
// for (let i = 0; i < 12; i++) {
|
||||
// if (data[`env${i}`] == 'on') {
|
||||
// if (data[`env_${i}_name`] == 'DRINODE') {
|
||||
// compose_file += `\n deploy:`
|
||||
// compose_file += `\n resources:`
|
||||
// compose_file += `\n reservations:`
|
||||
// compose_file += `\n devices:`
|
||||
// compose_file += `\n - driver: nvidia`
|
||||
// compose_file += `\n count: 1`
|
||||
// compose_file += `\n capabilities: [gpu]`
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// // add any docker volumes to the docker-compose file
|
||||
// if ( docker_volumes.length > 0 ) {
|
||||
// compose_file += `\n`
|
||||
// compose_file += `\nvolumes:`
|
||||
|
||||
// // check docker_volumes for duplicates and remove them completely
|
||||
// docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
|
||||
|
||||
// for (let i = 0; i < docker_volumes.length; i++) {
|
||||
// if ( docker_volumes[i] != '') {
|
||||
// compose_file += `\n ${docker_volumes[i]}:`
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// try {
|
||||
// mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
// writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
|
||||
// } catch { console.log('error creating directory or compose file') }
|
||||
|
||||
// try {
|
||||
// var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
|
||||
|
||||
// (async () => {
|
||||
// await compose.pull();
|
||||
// await compose.up();
|
||||
// })();
|
||||
|
||||
// } catch { console.log('error running compose file')}
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// module.exports.uninstall = async function (data) {
|
||||
// if (data.confirm == 'Yes') {
|
||||
// console.log(`Uninstalling ${data.service_name}: ${data}`);
|
||||
// var containerName = docker.getContainer(`${data.service_name}`);
|
||||
// try {
|
||||
// await containerName.stop();
|
||||
// console.log(`Stopped ${data.service_name} container`);
|
||||
// } catch {
|
||||
// console.log(`Error stopping ${data.service_name} container`);
|
||||
// }
|
||||
// try {
|
||||
// await containerName.remove();
|
||||
// console.log(`Removed ${data.service_name} container`);
|
||||
// } catch {
|
||||
// console.log(`Error removing ${data.service_name} container`);
|
||||
// }
|
||||
// }
|
||||
// }
|
28
functions/uninstall.js
Normal file
28
functions/uninstall.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
||||
import yaml from 'js-yaml';
|
||||
import { execSync } from "child_process";
|
||||
import { docker } from "../app.js";
|
||||
import DockerodeCompose from "dockerode-compose";
|
||||
|
||||
|
||||
|
||||
export const Uninstall = async (req, res) => {
|
||||
|
||||
console.log('Uninstall')
|
||||
console.log(req.body);
|
||||
|
||||
res.render("/apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
list_start: 0,
|
||||
list_end: 28,
|
||||
app_count: 0,
|
||||
prev: 0,
|
||||
next: 0,
|
||||
apps_list: 0,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -17,11 +17,13 @@
|
|||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^4.0.1",
|
||||
"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",
|
||||
"sequelize": "^6.35.2",
|
||||
"sinon": "^17.0.1",
|
||||
|
@ -1672,6 +1674,32 @@
|
|||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode-compose": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dockerode-compose/-/dockerode-compose-1.4.0.tgz",
|
||||
"integrity": "sha512-6x5ZlK06H+cgoTR4ffucqN5kWVvxNvxwTLcHQUZcegCJBEDGrdzXMOEGDMsxbHwiLtLo2dNwG0eZK7B2RfEWSw==",
|
||||
"dependencies": {
|
||||
"dockerode": "^4.0.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"tar-fs": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode-compose/node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"node_modules/dockerode-compose/node_modules/tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dottie": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
|
||||
|
|
|
@ -19,11 +19,13 @@
|
|||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^4.0.1",
|
||||
"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",
|
||||
"sequelize": "^6.35.2",
|
||||
"sinon": "^17.0.1",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import express from "express";
|
||||
import { io } from "../app.js";
|
||||
|
||||
// Controllers
|
||||
import { Login, submitLogin } from "../controllers/login.js";
|
||||
import { Register, submitRegister } from "../controllers/register.js";
|
||||
import { Dashboard, searchDashboard } from "../controllers/dashboard.js";
|
||||
|
@ -11,19 +12,24 @@ import { Account } from "../controllers/account.js";
|
|||
import { Settings } from "../controllers/settings.js";
|
||||
import { Networks } from "../controllers/networks.js";
|
||||
import { Volumes } from "../controllers/volumes.js";
|
||||
import { Syslogs } from "../controllers/syslogs.js";
|
||||
import { Syslogs } from "../controllers/syslogs.js";
|
||||
import { Portal } from "../controllers/portal.js"
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
/// 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") {
|
||||
next();
|
||||
} else {
|
||||
res.redirect("/login");
|
||||
res.redirect("/portal");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
router.get("/login", Login);
|
||||
router.post("/login", submitLogin);
|
||||
|
||||
|
@ -32,7 +38,8 @@ router.post("/register", submitRegister);
|
|||
|
||||
router.get("/", auth, Dashboard);
|
||||
router.post("/", auth, searchDashboard);
|
||||
router.post("/:search", auth, searchDashboard);
|
||||
|
||||
router.get("/portal", auth, Portal)
|
||||
|
||||
router.get("/apps", auth, Apps);
|
||||
router.get("/apps/:page", auth, Apps);
|
||||
|
@ -57,4 +64,8 @@ router.get("/logout", (req, res) => {
|
|||
io.to(sessionId).disconnectSockets();
|
||||
res.redirect("/login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post("/install", auth, Install);
|
||||
router.post("/uninstall", auth, Uninstall);
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<form action="/apps" id="search" name="search" method="POST">
|
||||
<input type="search" class="form-control" name="search" placeholder="Search apps…">
|
||||
</form>
|
||||
<input type="submit" form="search" class="btn btn-outline-success h-50" value="search">
|
||||
<input type="submit" form="search" class="btn btn-outline-success h-50" value="search">
|
||||
|
||||
<div class="card-actions btn-actions">
|
||||
<div class="card-actions btn-actions">
|
||||
|
|
|
@ -255,17 +255,17 @@
|
|||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<a class="nav-link" href="/portal">
|
||||
<span
|
||||
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tool" 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="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"></path> </svg> </span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-star" 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 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" /></svg> </span>
|
||||
<span class="nav-link-title">
|
||||
Tools
|
||||
Portal
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">
|
||||
|
|
50
views/portal.ejs
Normal file
50
views/portal.ejs
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Dashboard</title>
|
||||
<!-- CSS files -->
|
||||
<link href="css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="css/meters.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="libs/apexcharts/dist/apexcharts.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="js/tabler.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Add table
Reference in a new issue