Compare commits

..

12 commits
dev ... main

Author SHA1 Message Date
lllllllillllllillll
7d0bbc27fa
Update README.md 2024-08-28 23:35:09 -07:00
lllllllillllllillll
47c9ab68ce
Merge pull request #91 from joestump/main
Add docs for environment variables
2024-07-09 19:52:59 -07:00
Joe Stump
518bc46dab
Add docs for environment variables 2024-07-08 08:56:20 -04:00
lllllllillllllillll
00216663a5
Merge pull request #86 from lllllllillllllillll/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-19 23:49:15 -07:00
dependabot[bot]
f46648e598
Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 19:56:57 +00:00
lllllllillllllillll
d905a95764
Update README.md 2024-06-16 12:45:41 -07:00
lllllllillllllillll
a67f65996e
Update compose.yaml
changed volume to /app/config
2024-06-15 02:27:08 -07:00
lllllllillllllillll
4b5ae32a97
Update README.md
fix compose volume
2024-06-15 02:23:57 -07:00
lllllllillllllillll
ee9870f554
Update README.md 2024-06-09 02:44:49 -07:00
lllllllillllllillll
8f97e17765
Merge pull request #74 from lllllllillllllillll/dev
## v0.60 (June 9th 2024) - Permissions system and import templates
2024-06-09 01:28:36 -07:00
lllllllillllllillll
c7d79b296c
Merge pull request #60 from lllllllillllllillll/dev
v0.40
2024-02-26 15:59:24 -08:00
lllllllillllllillll
c3f10fbb7c
Merge pull request #47 from lllllllillllllillll/dev
v0.20 - The rewrite
2024-01-20 15:36:14 -08:00
101 changed files with 34872 additions and 73613 deletions

View file

@ -1,9 +1,8 @@
**/*.sqlite
**/db.sqlite
**/node_modules
**/screenshots
.gitignore
.github
.git
Dockerfile
docker-compose.yaml
**/*.yaml
docker-compose.yaml

View file

@ -55,7 +55,7 @@ jobs:
# Build image and only publish if not a Pull Request
- name: Build and Publish Docker Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
timeout-minutes: 30
with:
context: .

8
.gitignore vendored
View file

@ -1,7 +1,5 @@
sessions.sqlite
settings.sqlite
**/db.sqlite
**/node_modules
**/*.yaml
**/appdata
.github
.git
package-lock.json
.git

View file

@ -1,47 +1,3 @@
## v0.70 (dev)
* Fixed installs having to be run twice.
* Updated systeminformation.
* Updated adm-zip.
* Updated yaml.
* Pushed new docker image with 'latest' tag.
* Fixed container card links.
* Moved 'Reset view' button.
* New - 'Grid view' and 'List view' button (non-functioning).
* Added try blocks to volumes, images, and networks pages to address GitHub issues.
* Fixed HTTPS env.
* New - Authentication can be reduced or disabled.
* New (again) - PM2 to keep the app running if it encounters an error.
* New - User registration enabled/disabled from Settings page.
* Removed 'SECRET' environment variable.
* New - Custom container_card ports links.
* New - Custom container_card title links.
* Fixed issue updating view permission.
* Fixed issue viewing container logs.
* App icons are now determined by service label instead of image name.
* App icons sourced from new repo with 1000+ icons.
* Rewrote most of the app to use containerIDs and UUIDs universally.
* Dashboard updates now triggered by Docker events instead of constantly polling the API.
* Sessions now stored in sqlite database instead of memory.
* Updated tabler from 1.0.0-beta16 to 1.0.0-beta20.
* Updated htmx (2.0.1) and sse plugin (2.2.1).
* Seperated css and js customizations into dweebui.css and dweebui.js.
* New - Preferences page for individual user settings, like language choice.
* New - Hide username from dashboard.
* New - Footer displays version with build number.
* Updated hide container_card to be **instant**.
* Improved console.log and syslog messages.
* Fixed modal close buttons.
* Reduced amount of html being stored in js files.
* CSS and pages tweaks to make the style more consistent.
* Improved container cards to be more compact.
* Improved sponsors and credits pages.
* New - Secret code for sponsors.
* Fixed installs not appearing or appearing multiple times.
* Improved log view and fixed refresh button.
* Made app cards more compact.
* Updated container_card to only show exposed ports.
## v0.60 (June 9th 2024) - Permissions system and import templates
* Converted JS template literals into HTML.
* Converted modals into HTML/HTMX.

View file

@ -1,9 +1,7 @@
FROM node:23-alpine
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /dweebui
COPY package.json /dweebui
WORKDIR /app
COPY . /app
RUN npm install
RUN npm install pm2 -g
COPY . /dweebui
EXPOSE 8000
CMD ["pm2-runtime", "server.js"]
CMD node server.js

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023-2024 lllllllillllllillll
Copyright (c) 2023 lllllllillllllillll
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,5 +1,5 @@
<h3 align="center"><img width="150" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/public/img/logo.png"></h3>
<h4 align="center">DweebUI v0.70 ( :fire: Experimental :fire: )</h4>
<h4 align="center">DweebUI Beta v0.60 ( :fire: Experimental :fire: )</h4>
<h3 align="center">Free and Open-Source WebUI For Managing Your Containers.</h3>
<p align="center">
<a href=""><img src="https://img.shields.io/github/stars/lllllllillllllillll/DweebUI?style=flat"/></a>
@ -15,20 +15,18 @@
## Features
* [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
* [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
* [x] Multi-user support with permissions system.
* [x] Support for multiple hosts.
* [x] View and manage images, volumes, and networks.
* [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
* [x] Windows, Linux, and MacOS compatable.
* [x] Light/Dark Mode.
* [x] Mobile Friendly.
* [x] Easy to install app templates (Compatible with Portainer).
* [x] Docker Compose.
* [ ] Available updates without image pull (in development).
* [x] Manage your Docker networks, images, and volumes.
* [x] Easy to install app templates.
* [x] Docker Compose Support.
* [ ] Update containers (planned).
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [ ] Preset variables (planned).
* [ ] Themes (planned).
* [*] International language support (Languages still being updated).
## About
@ -39,13 +37,7 @@
## Setup
### Docker Run:
```
docker run -d --name=DweebUI -p 8000:8000 -v dweebui:/app/database -v /var/run/docker.sock:/var/run/docker.sock lllllllillllllillll/dweebui:v0.7X-dev
```
### Docker Compose:
Docker Compose:
```
version: "3.9"
services:
@ -54,8 +46,7 @@ services:
image: lllllllillllllillll/dweebui
environment:
PORT: 8000
HTTPS: false
NO_AUTH: false
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
@ -76,15 +67,19 @@ networks:
dweebui_net:
driver: bridge
```
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
Compose setup:
* Paste the above content into a file named ```docker-compose.yml``` then place it in a folder named ```dweebui```.
* Open a terminal in the ```dweebui``` folder, then enter ```docker compose up -d```.
* You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
[Troubleshooting](https://github.com/lllllllillllllillll/DweebUI/wiki/Troubleshooting)
Configuration:
* `PORT` - Specifies which port the service binds to on startup. Default is `8000`.
* `SECRET` - A shared secret used by the registration page.
## Credits
@ -97,4 +92,4 @@ Compose setup:
## Supporters
* MM (Patreon)
* PD (Buymeacoffee)
* PD (Buymeacoffee)

View file

@ -1 +0,0 @@
!.gitignore

View file

@ -1 +0,0 @@
!.gitignore

View file

@ -2,16 +2,15 @@ version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.70-dev
image: lllllllillllllillll/dweebui:v0.60
environment:
PORT: 8000
HTTPS: false
NO_AUTH: false
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
volumes:
- dweebui:/app
- dweebui:/app/config
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket
@ -25,4 +24,4 @@ volumes:
networks:
dweebui_net:
driver: bridge
driver: bridge

View file

@ -1,47 +1,20 @@
import { User, ServerSettings } from '../db/config.js';
import { Alert, getLanguage, Navbar, Sidebar, Footer } from '../utils/system.js';
import { User } from "../database/models.js";
export const Account = async function(req,res){
export const Account = async (req, res) => {
req.session.host = `${req.params.host || 1}`;
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let container_links = await ServerSettings.findOne({ where: {key: 'container_links'}});
let user_registration = await ServerSettings.findOne({ where: {key: 'user_registration'}});
let user = await User.findOne({ where: {userID: req.session.userID}});
res.render("account",{
alert: '',
res.render("account", {
first_name: user.name,
last_name: user.name,
name: user.name,
username: req.session.username,
id: user.id,
email: user.email,
avatar: user.avatar,
role: req.session.role,
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
});
}
export const searchAccount = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}
export const submitAccount = async function(req,res){
console.log(req.body);
res.render("account",{
role: user.role,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
});
}
}

View file

@ -1,100 +1,86 @@
import { Alert, getLanguage, Navbar, Footer, Capitalize } from '../utils/system.js';
import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
import { parse } from 'yaml';
import multer from 'multer';
import AdmZip from 'adm-zip';
const upload = multer({storage: multer.diskStorage({
destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
filename: function (req, file, cb) { cb(null, file.originalname) },
})});
let alert = '';
let templates_global = '';
let json_templates = '';
let remove_button = '';
const upload = multer({storage: multer.diskStorage({
destination: function (req, file, cb) { cb(null, 'appdata/tmp/') },
filename: function (req, file, cb) { cb(null, file.originalname) },
})});
export const searchApps = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}
export const Apps = async function(req,res){
req.session.host = `${req.params.host || 1}`;
export const Apps = async (req, res) => {
let [apps_list, app_count] = ['', ''];
let page = Number(req.params.page) || 1;
let template = req.params.template || 'default';
let template_param = req.params.template || 'default';
if ((template_param != 'default') && (template_param != 'compose')) {
remove_button = `<a href="/remove_template/${template_param}" class="btn" hx-confirm="Are you sure you want to remove this template?">Remove</a>`;
} else {
remove_button = '';
}
// Pagination
let list_start = (page - 1) * 40 + 1;
let list_end = (page * 40);
let last_page = '';
let pages =`<li class="page-item"><a class="page-link" href="/apps/1/${template}">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2/${template}">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3/${template}">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4/${template}">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5/${template}">5</a></li>`
let prev = '/apps/' + (page - 1) + '/' + template;
let next = '/apps/' + (page + 1) + '/' + template;
if (page == 1) { prev = '/apps/' + (page) + '/' + template; }
if (page == last_page) { next = '/apps/' + (page) + '/' + template;}
// Add a remove button if it's not the default template or a compose template
if ((template != 'default') && (template != 'compose')) {
remove_button = `<a href="/remove_template/${template}" class="btn" hx-confirm="Are you sure you want to remove this template?">Remove</a>`;
} else {
remove_button = '';
json_templates = '';
let json_files = readdirSync('templates/json/');
for (let i = 0; i < json_files.length; i++) {
if (json_files[i] != 'default.json') {
let filename = json_files[i].split('.')[0];
let link = `<li><a class="dropdown-item" href="/apps/1/${filename}">${filename}</a></li>`
json_templates += link;
}
}
let apps_list = '';
let app_count = '';
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = '';
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
let prev = '/apps/' + (page - 1) + '/' + template_param;
let next = '/apps/' + (page + 1) + '/' + template_param;
if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
if (template_param == 'compose') {
let compose_files = readdirSync('templates/compose/');
// Check for other templates and add them to the dropdown list
json_templates = '';
let json_files = readdirSync('appdata/templates/').filter(file => file.endsWith('.json'));
for (let i = 0; i < json_files.length; i++) {
if (json_files[i] != 'default.json') {
let filename = json_files[i].split('.')[0];
let link = `<li><a class="dropdown-item" href="/apps/1/${filename}">${filename}</a></li>`
json_templates += link;
}
}
app_count = compose_files.length;
last_page = Math.ceil(compose_files.length/28);
// Display compose files if the template is set to 'compose'
if (template == 'compose') {
let compose_files = readdirSync('appdata/compose/');
app_count = compose_files.length;
last_page = Math.ceil(compose_files.length/40);
compose_files.forEach(file => {
if (file == '.gitignore') { return; }
let compose = readFileSync(`appdata/compose/${file}/compose.yaml`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services)
let container = compose_data.services[service_name].container_name;
let image = compose_data.services[service_name].image;
// let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
let appCard = readFileSync('views/partials/app_card.html', 'utf8');
appCard = appCard.replace(/AppName/g, service_name);
appCard = appCard.replace(/AppDescription/g, 'Compose File');
appCard = appCard.replace(/AppIcon/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
// appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
appCard = appCard.replace(/AppType/g, 'compose');
apps_list += appCard;
});
} else {
compose_files.forEach(file => {
if (file == '.gitignore') { return; }
let template_file = readFileSync(`appdata/templates/${template}.json`);
let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services)
let container = compose_data.services[service_name].container_name;
let image = compose_data.services[service_name].image;
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
appCard = appCard.replace(/AppName/g, service_name);
appCard = appCard.replace(/AppShortName/g, service_name);
appCard = appCard.replace(/AppDesc/g, 'Compose File');
appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
appCard = appCard.replace(/AppType/g, 'compose');
apps_list += appCard;
});
} else {
let template_file = readFileSync(`./templates/json/${template_param}.json`);
let templates = JSON.parse(template_file).templates;
templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
app_count = templates.length;
@ -102,13 +88,11 @@ export const Apps = async function(req,res){
templates_global = templates;
apps_list = '';
for (let i = list_start; i < list_end && i < templates_global.length; i++) {
let appCard = readFileSync('views/partials/app_card.html', 'utf8');
for (let i = list_start; i < list_end && i < templates.length; i++) {
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
let name = templates[i].name || templates[i].title.toLowerCase();
let title = templates[i].title || templates[i].name;
// let desc = templates[i].description.slice(0, 75) + "...";
let desc = templates[i].description || "no description available";
let desc = templates[i].description.slice(0, 60) + "...";
let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
let image = templates[i].image;
@ -125,41 +109,143 @@ export const Apps = async function(req,res){
appCard = appCard.replace(/AppName/g, name);
appCard = appCard.replace(/AppTitle/g, title);
appCard = appCard.replace(/AppShortName/g, name);
appCard = appCard.replace(/AppDescription/g, desc);
appCard = appCard.replace(/AppIcon/g, logo);
appCard = appCard.replace(/AppDesc/g, desc);
appCard = appCard.replace(/AppLogo/g, logo);
appCard = appCard.replace(/AppCategories/g, categories);
appCard = appCard.replace(/AppType/g, 'json');
apps_list += appCard;
}
}
app_count = `${list_start} - ${list_end} of ${templates_global.length} Apps`;
res.render("apps",{
alert: '',
username: req.session.username,
role: req.session.role,
app_count: app_count,
remove_button: '',
json_templates: '',
apps_list: apps_list,
prev: prev,
next: next,
pages: pages,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const submitApps = async function (req, res) {
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
list_start: list_start + 1,
list_end: list_end,
app_count: app_count,
prev: prev,
next: next,
apps_list: apps_list,
alert: alert,
template_list: '',
json_templates: json_templates,
pages: pages,
remove_button: remove_button
});
alert = '';
}
export const removeTemplate = async (req, res) => {
let template = req.params.template;
unlinkSync(`templates/json/${template}.json`);
res.redirect('/apps');
}
export const appSearch = async (req, res) => {
let search = req.body.search;
let page = Number(req.params.page) || 1;
let template_param = req.params.template || 'default';
let template_file = readFileSync(`./templates/json/${template_param}.json`);
let templates = JSON.parse(template_file).templates;
templates = templates.sort((a, b) => {
if (a.name < b.name) { return -1; }
});
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
let list_start = (page-1)*28;
let list_end = (page*28);
let last_page = Math.ceil(templates.length/28);
let prev = '/apps/' + (page-1);
let next = '/apps/' + (page+1);
if (page == 1) { prev = '/apps/' + (page); }
if (page == last_page) { next = '/apps/' + (page); }
let apps_list = '';
let results = [];
let [cat_1, cat_2, cat_3] = ['','',''];
function searchTemplates(terms) {
terms = terms.toLowerCase();
for (let i = 0; i < templates.length; i++) {
if (templates[i].categories) {
if (templates[i].categories[0]) {
cat_1 = (templates[i].categories[0]).toLowerCase();
}
if (templates[i].categories[1]) {
cat_2 = (templates[i].categories[1]).toLowerCase();
}
if (templates[i].categories[2]) {
cat_3 = (templates[i].categories[2]).toLowerCase();
}
}
if ((templates[i].description.includes(terms)) || (templates[i].name.includes(terms)) || (templates[i].title.includes(terms)) || (cat_1.includes(terms)) || (cat_2.includes(terms)) || (cat_3.includes(terms))){
results.push(templates[i]);
}
}
}
searchTemplates(search);
for (let i = 0; i < results.length; i++) {
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
let name = results[i].name || results[i].title.toLowerCase();
let desc = results[i].description.slice(0, 60) + "...";
let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
let image = results[i].image;
let logo = results[i].logo;let categories = '';
// set data.catagories to 'other' if data.catagories is empty or undefined
if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
results[i].categories = ['Other'];
}
// loop through the categories and add the badge to the card
for (let j = 0; j < results[i].categories.length; j++) {
categories += CatagoryColor(results[i].categories[j]);
}
appCard = appCard.replace(/AppName/g, name);
appCard = appCard.replace(/AppShortName/g, name);
appCard = appCard.replace(/AppDesc/g, desc);
appCard = appCard.replace(/AppLogo/g, logo);
appCard = appCard.replace(/AppCategories/g, categories);
appCard = appCard.replace(/AppType/g, 'json');
apps_list += appCard;
}
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
list_start: list_start + 1,
list_end: list_end,
app_count: results.length,
prev: prev,
next: next,
apps_list: apps_list,
alert: alert,
template_list: '',
json_templates: json_templates,
pages: pages,
remove_button: remove_button
});
}
function CatagoryColor(category) {
@ -207,231 +293,295 @@ function CatagoryColor(category) {
}
}
export const InstallModal = async (req, res) => {
let input = req.header('hx-trigger-name');
let type = req.header('hx-trigger');
export const appsModals = async function (req, res) {
let app_name = req.header('hx-trigger-name');
let app_type = req.header('hx-trigger');
let modal = req.params.modal;
// console.log(`[submitApps] app_name: ${app_name} app_type: ${app_type} modal: ${modal}`);
// Modal for compose files
if (modal == 'view_install' && app_type == 'compose') {
let compose = readFileSync(`appdata/compose/${app_name}/compose.yaml`, 'utf8');
let modal = readFileSync('views/partials/compose.html', 'utf8');
modal = modal.replace(/AppName/g, app_name);
modal = modal.replace(/COMPOSE_CONTENT/g, compose);
res.send(modal);
return;
}
// More Info modal
if (modal == 'info' && app_type == 'json') {
let modal = readFileSync('views/partials/info.html', 'utf8');
let app_title = Capitalize(app_name);
modal = modal.replace(/AppTitle/g, app_title);
let result = templates_global.find(t => t.name == app_name);
modal = modal.replace(/AppDescription/g, result.description);
res.send(modal);
return;
}
// Modal for json templates
if (modal == 'view_install' && app_type == 'json') {
let result = templates_global.find(t => t.name == app_name);
let name = result.name || result.title.toLowerCase();
let short_name = name.slice(0, 25) + "...";
let desc = result.description.replaceAll(". ", ".\n") || "no description available";
let short_desc = desc.slice(0, 60) + "...";
let modal_name = name.replaceAll(" ", "-");
let form_id = name.replaceAll("-", "_");
let note = result.note ? result.note.replaceAll(". ", ".\n") : "no notes available";
let command = result.command ? result.command : "";
let command_check = command ? "checked" : "";
let privileged = result.privileged || "";
let privileged_check = privileged ? "checked" : "";
let repository = result.repository || "";
let image = result.image || "";
let net_host, net_bridge, net_docker = '';
let net_name = 'AppBridge';
let restart_policy = result.restart_policy || 'unless-stopped';
switch (result.network) {
case 'host':
net_host = 'checked';
break;
case 'bridge':
net_bridge = 'checked';
net_name = result.network;
break;
default:
net_docker = 'checked';
}
if (repository != "") {
image = (`${repository.url}/raw/master/${repository.stackfile}`);
}
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
for (let i = 0; i < 20; i++) {
// Get port details
try {
let ports = result.ports[i];
let port_check = ports ? "checked" : "";
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
// remove /tcp or /udp from port_external if it exists
if (port_external.includes("/")) {
port_external = port_external.split("/")[0];
}
ports_data.push({
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_protocol
});
} catch {
ports_data.push({
check: "",
external: "",
internal: "",
protocol: ""
});
}
// Get volume details
try {
let volumes = result.volumes[i];
let volume_check = volumes ? "checked" : "";
let volume_bind = volumes.bind ? volumes.bind : "";
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw";
if (volumes.readonly == true) {
volume_readwrite = "ro";
}
volumes_data.push({
check: volume_check,
bind: volume_bind,
container: volume_container,
readwrite: volume_readwrite
});
} catch {
volumes_data.push({
check: "",
bind: "",
container: "",
readwrite: ""
});
}
// Get environment details
try {
let env = result.env[i];
let env_check = "";
let env_default = env.default ? env.default : "";
if (env.set) { env_default = env.set;}
let env_description = env.description ? env.description : "";
let env_label = env.label ? env.label : "";
let env_name = env.name ? env.name : "";
env_data.push({
check: env_check,
default: env_default,
description: env_description,
label: env_label,
name: env_name
});
} catch {
env_data.push({
check: "",
default: "",
description: "",
label: "",
name: ""
});
}
// Get label details
try {
let label = result.labels[i];
let label_check = "";
let label_name = label.name ? label.name : "";
let label_value = label.value ? label.value : "";
label_data.push({
check: label_check,
name: label_name,
value: label_value
});
} catch {
label_data.push({
check: "",
name: "",
value: ""
});
}
}
let modal = readFileSync('views/partials/install.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppNote/g, note);
modal = modal.replace(/AppImage/g, image);
modal = modal.replace(/RestartPolicy/g, restart_policy);
modal = modal.replace(/NetHost/g, net_host);
modal = modal.replace(/NetBridge/g, net_bridge);
modal = modal.replace(/NetDocker/g, net_docker);
modal = modal.replace(/NetName/g, net_name);
modal = modal.replace(/ModalName/g, modal_name);
modal = modal.replace(/FormId/g, form_id);
modal = modal.replace(/CommandCheck/g, command_check);
modal = modal.replace(/CommandValue/g, command);
modal = modal.replace(/PrivilegedCheck/g, privileged_check);
for (let i = 0; i < 20; i++) {
modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
modal = modal.replaceAll(`Vol${i}Check`, volumes_data[i].check);
modal = modal.replaceAll(`Vol${i}Source`, volumes_data[i].bind);
modal = modal.replaceAll(`Vol${i}Destination`, volumes_data[i].container);
modal = modal.replaceAll(`Vol${i}RW`, volumes_data[i].readwrite);
modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
modal = modal.replaceAll(`Env${i}Key`, env_data[i].name);
modal = modal.replaceAll(`Env${i}Value`, env_data[i].default);
modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
modal = modal.replaceAll(`Label${i}Key`, label_data[i].name);
modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
}
if (type == 'compose') {
let compose = readFileSync(`templates/compose/${input}/compose.yaml`, 'utf8');
let modal = readFileSync('./views/modals/compose.html', 'utf8');
modal = modal.replace(/AppName/g, input);
modal = modal.replace(/COMPOSE_CONTENT/g, compose);
res.send(modal);
return;
} else {
let result = templates_global.find(t => t.name == input);
let name = result.name || result.title.toLowerCase();
let short_name = name.slice(0, 25) + "...";
let desc = result.description.replaceAll(". ", ".\n") || "no description available";
let short_desc = desc.slice(0, 60) + "...";
let modal_name = name.replaceAll(" ", "-");
let form_id = name.replaceAll("-", "_");
let note = result.note ? result.note.replaceAll(". ", ".\n") : "no notes available";
let command = result.command ? result.command : "";
let command_check = command ? "checked" : "";
let privileged = result.privileged || "";
let privileged_check = privileged ? "checked" : "";
let repository = result.repository || "";
let image = result.image || "";
let net_host, net_bridge, net_docker = '';
let net_name = 'AppBridge';
let restart_policy = result.restart_policy || 'unless-stopped';
switch (result.network) {
case 'host':
net_host = 'checked';
break;
case 'bridge':
net_bridge = 'checked';
net_name = result.network;
break;
default:
net_docker = 'checked';
}
if (repository != "") {
image = (`${repository.url}/raw/master/${repository.stackfile}`);
}
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
for (let i = 0; i < 12; i++) {
// Get port details
try {
let ports = result.ports[i];
let port_check = ports ? "checked" : "";
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
// remove /tcp or /udp from port_external if it exists
if (port_external.includes("/")) {
port_external = port_external.split("/")[0];
}
ports_data.push({
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_protocol
});
} catch {
ports_data.push({
check: "",
external: "",
internal: "",
protocol: ""
});
}
// Get volume details
try {
let volumes = result.volumes[i];
let volume_check = volumes ? "checked" : "";
let volume_bind = volumes.bind ? volumes.bind : "";
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw";
if (volumes.readonly == true) {
volume_readwrite = "ro";
}
volumes_data.push({
check: volume_check,
bind: volume_bind,
container: volume_container,
readwrite: volume_readwrite
});
} catch {
volumes_data.push({
check: "",
bind: "",
container: "",
readwrite: ""
});
}
// Get environment details
try {
let env = result.env[i];
let env_check = "";
let env_default = env.default ? env.default : "";
if (env.set) { env_default = env.set;}
let env_description = env.description ? env.description : "";
let env_label = env.label ? env.label : "";
let env_name = env.name ? env.name : "";
env_data.push({
check: env_check,
default: env_default,
description: env_description,
label: env_label,
name: env_name
});
} catch {
env_data.push({
check: "",
default: "",
description: "",
label: "",
name: ""
});
}
// Get label details
try {
let label = result.labels[i];
let label_check = "";
let label_name = label.name ? label.name : "";
let label_value = label.value ? label.value : "";
label_data.push({
check: label_check,
name: label_name,
value: label_value
});
} catch {
label_data.push({
check: "",
name: "",
value: ""
});
}
}
let modal = readFileSync('./views/modals/json.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppNote/g, note);
modal = modal.replace(/AppImage/g, image);
modal = modal.replace(/RestartPolicy/g, restart_policy);
modal = modal.replace(/NetHost/g, net_host);
modal = modal.replace(/NetBridge/g, net_bridge);
modal = modal.replace(/NetDocker/g, net_docker);
modal = modal.replace(/NetName/g, net_name);
modal = modal.replace(/ModalName/g, modal_name);
modal = modal.replace(/FormId/g, form_id);
modal = modal.replace(/CommandCheck/g, command_check);
modal = modal.replace(/CommandValue/g, command);
modal = modal.replace(/PrivilegedCheck/g, privileged_check);
for (let i = 0; i < 12; i++) {
modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
modal = modal.replaceAll(`Volume${i}Check`, volumes_data[i].check);
modal = modal.replaceAll(`Volume${i}Bind`, volumes_data[i].bind);
modal = modal.replaceAll(`Volume${i}Container`, volumes_data[i].container);
modal = modal.replaceAll(`Volume${i}RW`, volumes_data[i].readwrite);
modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
modal = modal.replaceAll(`Env${i}Default`, env_data[i].default);
modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
modal = modal.replaceAll(`Env${i}Name`, env_data[i].name);
modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
modal = modal.replaceAll(`Label${i}Name`, label_data[i].name);
modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
}
res.send(modal);
}
}
}
export const LearnMore = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
if (id == 'compose') {
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppDesc/g, 'Compose File');
res.send(modal);
return;
}
let result = templates_global.find(t => t.name == name);
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
modal = modal.replace(/AppName/g, result.title);
modal = modal.replace(/AppDesc/g, result.description);
res.send(modal);
}
export const ImportModal = async (req, res) => {
let modal = readFileSync('./views/modals/import.html', 'utf8');
res.send(modal);
}
export const Upload = (req, res) => {
upload.array('files', 10)(req, res, () => {
alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-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><path d="M5 12l5 5l10 -10"></path></svg> </div>
<div>Template(s) Uploaded!</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
</div>`;
let exists_alert = `<div class="alert alert-danger alert-dismissible mb-0 py-2" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-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><path d="M5 12l5 5l10 -10"></path></svg> </div>
<div>Template already exists</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
</div>`;
let files = readdirSync('templates/tmp/');
for (let i = 0; i < files.length; i++) {
if (files[i].endsWith('.zip')) {
let zip = new AdmZip(`templates/tmp/${files[i]}`);
zip.extractAllTo('templates/compose', true);
unlinkSync(`templates/tmp/${files[i]}`);
} else if (files[i].endsWith('.json')) {
if (existsSync(`templates/json/${files[i]}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
} else if (files[i].endsWith('.yml')) {
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services);
if (existsSync(`templates/compose/${service_name}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
mkdirSync(`templates/compose/${service_name}`);
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
} else if (files[i].endsWith('.yaml')) {
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services);
if (existsSync(`templates/compose/${service_name}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
mkdirSync(`templates/compose/${service_name}`);
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
} else {
// unsupported file type
unlinkSync(`templates/tmp/${files[i]}`);
}
}
res.redirect('/apps');
});
};

View file

@ -1,35 +0,0 @@
import { ServerSettings, User } from '../db/config.js';
import { Alert, getLanguage, Navbar, Sidebar, Footer, Capitalize } from '../utils/system.js';
import { readdirSync, readFileSync } from 'fs';
export const Credits = async function (req, res) {
let language = await getLanguage(req.session.userID);
let Language = Capitalize(language);
let selected = `<option value="${language}" selected hidden>${Language}</option>`;
let user = await User.findOne({ where: { userID: req.session.userID }});
let preferences = JSON.parse(user.preferences);
let hide_profile = preferences.hide_profile;
let checked = ''; if (hide_profile == true) { checked = 'checked'; }
res.render("credits",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
selected: selected,
hide_profile: checked,
});
}
export const searchCredits = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}

View file

@ -1,48 +1,360 @@
import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
import { docker, containerInfo, containerLogs, GetContainerLists, containerStats, trigger_docker_event } from '../utils/docker.js';
import { Readable } from 'stream';
import { Permission, User } from '../database/models.js';
import { docker } from '../server.js';
import { dockerContainerStats } from 'systeminformation';
import { readFileSync } from 'fs';
import { User, Permission, ServerSettings, ContainerLists, Container } from '../db/config.js';
import { Alert, Navbar, Footer, Capitalize } from '../utils/system.js';
import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
import { Op } from 'sequelize';
let hidden = '';
let alert = '';
let [ cardList, newCards, stats ] = [ '', '', {}];
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
// Dashboard
export const Dashboard = async function (req, res) {
// The page
export const Dashboard = (req, res) => {
console.log(`[Dashboard] ${req.session.username}`);
let username = req.session.username;
let userID = req.session.userID;
let name = req.session.user;
let role = req.session.role;
let host = req.session.host;
alert = req.session.alert;
// Create the lists needed for the dashboard
const [list, created] = await ContainerLists.findOrCreate({
where: { userID: userID },
defaults: { userID: userID, username: username, containers: '[]', new: '[]', updates: '[]', sent: '[]', },
});
res.render("dashboard",{
alert: '',
username: username,
res.render("dashboard", {
name: name,
avatar: name.charAt(0).toUpperCase(),
role: role,
navbar: await Navbar(req),
footer: await Footer(req),
});
alert: alert,
});
}
// The page actions
export const DashboardAction = async (req, res) => {
let name = req.header('hx-trigger-name');
let value = req.header('hx-trigger');
let action = req.params.action;
let modal = '';
// Dashboard search
export const searchDashboard = async function (req, res) {
// console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
switch (action) {
case 'permissions':
let title = name.charAt(0).toUpperCase() + name.slice(1);
let permissions_list = '';
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
let users = await User.findAll({ attributes: ['username', 'UUID']});
for (let i = 0; i < users.length; i++) {
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
permissions_list += user_permissions;
}
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
res.send(permissions_modal);
return;
case 'uninstall':
modal = readFileSync('./views/modals/uninstall.html', 'utf8');
modal = modal.replace(/AppName/g, name);
res.send(modal);
return;
case 'details':
modal = readFileSync('./views/modals/details.html', 'utf8');
let details = await containerInfo(name);
modal = modal.replace(/AppName/g, details.name);
modal = modal.replace(/AppImage/g, details.image);
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
}
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
}
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
}
// console.log(details.env);
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
}
res.send(modal);
return;
case 'updates':
res.send(newCards);
newCards = '';
return;
case 'card':
await userCards(req.session);
if (!req.session.container_list.find(c => c.container === name)) {
res.send('');
return;
} else {
let details = await containerInfo(name);
let card = await createCard(details);
res.send(card);
return;
}
case 'logs':
let logString = '';
let options = { follow: true, stdout: true, stderr: false, timestamps: false };
docker.getContainer(name).logs(options, function (err, stream) {
if (err) { console.log(err); return; }
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
res.send(`<pre>${logString}</pre>`);
});
});
return;
case 'hide':
let user = req.session.user;
let exists = await Permission.findOne({ where: {containerName: name, user: user}});
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: user, hide: true, userID: req.session.UUID}); }
else { exists.update({ hide: true }); }
hidden = await Permission.findAll({ where: {user: user, hide: true}}, { attributes: ['containerName'] });
hidden = hidden.map((container) => container.containerName);
res.send("ok");
return;
case 'reset':
await Permission.update({ hide: false }, { where: { user: req.session.user } });
res.send("ok");
return;
case 'alert':
req.session.alert = '';
res.send('');
return;
}
function status (state) {
return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
${state}
</span>`);
}
// Container actions
if ((action == 'start') && (value == 'stopped')) {
docker.getContainer(name).start();
res.send(status('starting'));
} else if ((action == 'start') && (value == 'paused')) {
docker.getContainer(name).unpause();
res.send(status('starting'));
} else if ((action == 'stop') && (value != 'stopped')) {
docker.getContainer(name).stop();
res.send(status('stopping'));
} else if ((action == 'pause') && (value == 'paused')) {
docker.getContainer(name).unpause();
res.send(status('starting'));
} else if ((action == 'pause') && (value == 'running')) {
docker.getContainer(name).pause();
res.send(status('pausing'));
} else if (action == 'restart') {
docker.getContainer(name).restart();
res.send(status('restarting'));
}
}
async function containerInfo (containerName) {
// get the container info
let container = docker.getContainer(containerName);
let info = await container.inspect();
let image = info.Config.Image;
// grab the service name from the end of the image name
let service = image.split('/').pop();
// remove the tag from the service name if it exists
try { service = service.split(':')[0]; } catch {}
let ports_list = [];
let external = 0;
let internal = 0;
try {
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
try {
external = ports_list[0].external;
internal = ports_list[0].internal;
} catch {}
// console.log(ports_list);
// console.log(info.HostConfig.PortBindings);
// console.log(info.HostConfig.Binds);
// console.log(info.Config.Env);
// console.log(info.Config.Labels);
let details = {
name: containerName,
image: image,
service: service,
state: info.State.Status,
external_port: external,
internal_port: internal,
ports: ports_list,
volumes: info.Mounts,
env: info.Config.Env,
labels: info.Config.Labels,
link: 'localhost',
}
return details;
}
async function createCard (details) {
let shortname = details.name.slice(0, 10) + '...';
let trigger = 'data-hx-trigger="load, every 3s"';
let state = details.state;
let card = readFileSync('./views/partials/containerFull.html', 'utf8');
let state_color = '';
switch (state) {
case 'running':
state_color = 'green';
break;
case 'exited':
state = 'stopped';
state_color = 'red';
trigger = 'data-hx-trigger="load"';
break;
case 'paused':
state_color = 'orange';
trigger = 'data-hx-trigger="load"';
break;
case 'installing':
state_color = 'blue';
trigger = 'data-hx-trigger="load"';
break;
}
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
card = card.replace(/AppName/g, details.name);
card = card.replace(/AppShortName/g, shortname);
card = card.replace(/AppIcon/g, details.service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ExternalPort/g, details.external_port);
card = card.replace(/InternalPort/g, details.internal_port);
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
card = card.replace(/AppNameState/g, `${details.name}State`);
card = card.replace(/data-trigger=""/, trigger);
return card;
}
async function userCards (session) {
session.container_list = [];
// check what containers the user wants hidden
let hidden = await Permission.findAll({ where: {user: session.user, hide: true}}, { attributes: ['containerName'] });
hidden = hidden.map((container) => container.containerName);
// check what containers the user has permission to view
let visable = await Permission.findAll({ where: { user: session.user, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] } });
visable = visable.map((container) => container.containerName);
// get all containers
let containers = await docker.listContainers({ all: true });
// loop through containers
for (let i = 0; i < containers.length; i++) {
let container_name = containers[i].Names[0].replace('/', '');
// skip hidden containers
if (hidden.includes(container_name)) { continue; }
// admin can see all containers that they don't have hidden
if (session.role == 'admin') { session.container_list.push({ container: container_name, state: containers[i].State }); }
// user can see any containers that they have any permissions for
else if (visable.includes(container_name)){ session.container_list.push({ container: container_name, state: containers[i].State }); }
}
// create a sent list if it doesn't exist
if (!session.sent_list) { session.sent_list = []; }
if (!session.update_list) { session.update_list = []; }
if (!session.new_cards) { session.new_cards = []; }
}
async function updateDashboard (session) {
let container_list = session.container_list;
let sent_list = session.sent_list;
session.new_cards = [];
session.update_list = [];
// loop through the containers list
container_list.forEach(info => {
let { container, state } = info;
let sent = sent_list.find(c => c.container === container);
if (!sent) { session.new_cards.push(container);}
else if (sent.state !== state) { session.update_list.push(container); }
});
// loop through the sent list to see if any containers have been removed
sent_list.forEach(info => {
let { container } = info;
let exists = container_list.find(c => c.container === container);
if (!exists) { session.update_list.push(container); }
});
}
// HTMX server-side events
export const SSE = async (req, res) => {
// set the headers for server-sent events
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
// check for container changes every 500ms
let eventCheck = setInterval(async () => {
await userCards(req.session);
// check if the cards displayed are the same as what's in the session
if ((JSON.stringify(req.session.container_list) === JSON.stringify(req.session.sent_list))) { return; }
await updateDashboard(req.session);
for (let i = 0; i < req.session.new_cards.length; i++) {
let details = await containerInfo(req.session.new_cards[i]);
let card = await createCard(details);
newCards += card;
req.session.alert = '';
}
for (let i = 0; i < req.session.update_list.length; i++) {
res.write(`event: ${req.session.update_list[i]}\n`);
res.write(`data: 'update cards'\n\n`);
}
res.write(`event: update\n`);
res.write(`data: 'update cards'\n\n`);
req.session.sent_list = req.session.container_list.slice();
}, 500);
req.on('close', () => {
clearInterval(eventCheck);
});
};
// Server metrics (CPU, RAM, TX, RX, DISK)
export const ServerMetrics = async (req, res) => {
export const Stats = async (req, res) => {
let name = req.header('hx-trigger-name');
let color = req.header('hx-trigger');
let value = 0;
@ -65,448 +377,72 @@ export const ServerMetrics = async (req, res) => {
break;
}
let info = `<div class="font-weight-medium"> <label class="cpu-text mb-1">${name} ${value}%</label></div>
<div class="cpu-bar meter animate ${color}"><span style="width:${value}%"><span></span></span></div>`;
<div class="cpu-bar meter animate ${color}"> <span style="width:${value}%"><span></span></span> </div>`;
res.send(info);
}
async function userCards (req) {
let container_list = [];
// Check what containers the user has hidden.
let hidden = await Permission.findAll({ where: {userID: req.session.userID, hide: true}}, { attributes: ['containerID'] });
hidden = hidden.map((container) => container.containerID);
// Check what containers the user has permission for.
let visable = await Permission.findAll({ where: { userID: req.session.userID, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] }, attributes: ['containerID'] });
visable = visable.map((container) => container.containerID);
let containers = await GetContainerLists(req.session.host);
for (let i = 0; i < containers.length; i++) {
let container_name = containers[i].Names[0].split('/').pop();
// Skip if the ID is found in the hidden list.
if (hidden.includes(containers[i].Id)) { continue; }
// Skip if the state is 'created'.
if (containers[i].State == 'created') { continue; }
// Admin can see all containers that they don't have hidden.
if (req.session.role == 'admin') { container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); }
// User can see any containers that they have any permissions for.
else if (visable.includes(containers[i].Id)){ container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); }
}
return container_list;
}
async function createCard (details) {
let container_card = readFileSync('./views/partials/container_card.html', 'utf8');
let containerName = details.containerName;
let containerTitle = Capitalize(containerName); if (containerTitle.length > 14) { containerTitle = containerTitle.substring(0, 14) + '...'; }
let containerID = details.containerID;
let containerState = details.containerState;
let containerService = details.containerService;
let AltID = `a${containerID}`;
let chart_trigger = `<div name="${containerName}" id="${AltID}info" hx-get="/dashboard/view/chart/${containerID}" hx-swap="outerHTML" hx-trigger="every 3s" hx-target="#${AltID}info">
</div>`;
let containerStateColor = '';
switch (containerState) {
case 'running': containerStateColor = 'green'; break;
case 'exited': containerStateColor = 'red'; containerState = 'stopped'; chart_trigger = ''; break;
case 'paused': containerStateColor = 'orange'; break;
default: containerStateColor = 'blue'; break;
}
let [title_link, created] = await Container.findOrCreate({ where: { containerID: details.containerID }, defaults: { containerName: containerName, containerID: containerID, link: '', cpu: '[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]', ram: '[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]' } });
if (title_link.link != '') { title_link = `<a href="${title_link.link}" class="nav-link" target="_blank">${containerTitle}</a>`; }
else { title_link = containerTitle; }
let [port_link, created_link] = await ServerSettings.findOrCreate({ where: { key: 'custom_link' }, defaults: { key: 'custom_link', value: 'http://localhost' } });
port_link = port_link.value;
let exposed_ports = '';
for (let i = 0; i < details.ports.length; i++) {
if (details.ports[i].external != '' && details.ports[i].protocol != 'udp') { exposed_ports += `<a href="${port_link}:${details.ports[i].external}" target="_blank" style="color: inherit; text-decoration: none;"> ${details.ports[i].external}</a> `; }
}
container_card = container_card.replace(/AppName/g, containerName);
container_card = container_card.replace(/ContainerID/g, containerID);
container_card = container_card.replaceAll(/AltID/g, AltID);
container_card = container_card.replace(/AppPorts/g, exposed_ports);
container_card = container_card.replace(/TitleLink/g, title_link);
container_card = container_card.replace(/AppTitle/g, containerTitle);
container_card = container_card.replace(/AppService/g, containerService);
container_card = container_card.replace(/AppState/g, containerState);
container_card = container_card.replace(/StateColor/g, containerStateColor);
container_card = container_card.replace(/ChartTrigger/g, chart_trigger);
return container_card;
}
// HTMX - Server-side events
export const SSE = async (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
async function eventCheck () {
let list = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['sent'] });
let container_list = await userCards(req);
let new_cards = [];
let update_list = [];
let sent_cards = [];
sent_cards = JSON.parse(list.sent);
if (JSON.stringify(container_list) == list.sent) { return; }
// console.log(`Update for ${req.session.username}`);
// loop through the containers list to see if any new containers have been added or changed
container_list.forEach(container => {
let { containerName, containerID, containerState } = container;
if (list.sent) { sent_cards = JSON.parse(list.sent); }
let found = sent_cards.find(c => c.containerID === containerID);
if (!found) { new_cards.push(containerID); }
else if (found.containerState !== containerState) { update_list.push(containerID); }
});
// loop through the sent list to see if any containers have been removed
sent_cards.forEach(container => {
let { containerName, containerID, containerState } = container;
let found = container_list.find(c => c.containerID === containerID);
if (!found) { update_list.push(containerID); }
});
await ContainerLists.update({ new: JSON.stringify(new_cards), sent: JSON.stringify(container_list), containers: JSON.stringify(container_list) }, { where: { userID: req.session.userID } });
if (update_list.length > 0 ) {
for (let i = 0; i < update_list.length; i++) {
res.write(`event: ${update_list[i]}\n`);
res.write(`data: 'update cards'\n\n`);
}
}
if (new_cards.length > 0) {
res.write(`event: update\n`);
res.write(`data: 'card updates'\n\n`);
}
}
docker.getEvents({}, async function (err, data) {
data.on('data', async function () {
await eventCheck();
});
});
req.on('close', async () => {
});
}
export const DashboardView = async function (req, res) {
let container_name = req.header('hx-trigger-name');
let view = req.params.view;
let containerID = req.params.id;
let AltID = `a${containerID}`;
// console.log(`[container_name] ${container_name} [view] ${view} [containerID] ${containerID}`);
// Container CPU and RAM chart
if (view == 'chart') {
let container = await Container.findOne({ where: { containerID: containerID } });
// Get the cpu and ram stats, remove the oldest entry, add the newest stats, then update container info.
let stats = await containerStats(containerID);
let cpu = JSON.parse(container.cpu); cpu.shift(); cpu.push(stats.cpu);
let ram = JSON.parse(container.ram); ram.shift(); ram.push(stats.ram);
container.update({ cpu: JSON.stringify(cpu), ram: JSON.stringify(ram) });
let chartData = `<div name="${container_name}" id="${AltID}info" hx-get="/dashboard/view/chart/${containerID}" hx-swap="outerHTML" hx-trigger="every 3s" hx-target="#${AltID}info">
<script>
${AltID}chart.updateSeries([{
name: 'CPU',
data: ${container.cpu}
}, {
name: 'RAM',
data: ${container.ram}
}]);
</script>
</div>`;
res.send(chartData);
return;
}
// Permissions modal
if (view == 'permissions') {
let title = Capitalize(container_name);
let users = await User.findAll({ attributes: ['username', 'userID'] });
let modal =`<div class="modal-header">
<h5 class="modal-title">${title} Permissions</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
// Imported by utils/install.js
export async function addAlert (session, type, message) {
session.alert = `<div class="alert alert-${type} alert-dismissible py-2 mb-0" role="alert" id="alert">
<div class="d-flex">
<div class="spinner-border text-info nav-link">
<span class="visually-hidden">Loading...</span>
</div>
<div class="modal-body"><div class="accordion" id="accordion-example">`;
for (let i = 0; i < users.length; i++) {
if (users.length == 1) { modal += 'No other users.'; break; }
// Skip the admin user.
else if (i == 0) { continue; }
let exists = await Permission.findOne({ where: {containerID: containerID, userID: users[i].userID}});
if (!exists) { await Permission.create({ containerName: container_name, containerID: containerID, userID: users[i].userID, username: users[i].username}); }
let permissions = await Permission.findOne({ where: {containerID: containerID, userID: users[i].userID}});
let user_permissions = readFileSync('./views/partials/permissions.html', 'utf8');
if (permissions.uninstall == true && permissions.edit == true && permissions.upgrade == true && permissions.start == true && permissions.stop == true && permissions.pause == true && permissions.restart == true && permissions.logs == true && permissions.view == true) { user_permissions = user_permissions.replace(/data-AllCheck/g, 'checked'); }
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
user_permissions = user_permissions.replace(/Entry/g, i);
user_permissions = user_permissions.replace(/Entry/g, i);
user_permissions = user_permissions.replace(/Entry/g, i);
user_permissions = user_permissions.replace(/container_id/g, containerID);
user_permissions = user_permissions.replace(/container_name/g, container_name);
user_permissions = user_permissions.replace(/user_id/g, users[i].userID);
user_permissions = user_permissions.replace(/Username/g, users[i].username);
modal += user_permissions;
}
modal += `</div></div>
<div class="modal-footer">
<form id="reset_permissions" class="me-auto">
<input type="hidden" name="containerID" value="${containerID}">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" id="submit" hx-post="/dashboard/action/update_permissions/${containerID}" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
</form>
<button type="button" class="btn" data-bs-dismiss="modal">Close</button>
</div>`
res.send(modal);
return;
}
// Logs modal
if (view == 'logs') {
let logs = await containerLogs(containerID);
let modal = readFileSync('./views/partials/logs.html', 'utf8');
modal = modal.replace(/AppName/g, container_name);
modal = modal.replace(/ContainerID/g, containerID);
modal = modal.replace(/ContainerLogs/g, logs);
res.send(modal);
return;
}
// Details modal
if (view == 'details') {
let container = await containerInfo(containerID);
let modal = readFileSync('./views/partials/details.html', 'utf8');
modal = modal.replace(/AppName/g, container.containerName);
modal = modal.replace(/AppImage/g, container.containerImage);
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Port${i}Check`, container.ports[i]?.check || '');
modal = modal.replaceAll(`Port${i}External`, container.ports[i]?.external || '');
modal = modal.replaceAll(`Port${i}Internal`, container.ports[i]?.internal || '');
modal = modal.replaceAll(`Port${i}Protocol`, container.ports[i]?.protocol || '');
}
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Vol${i}Source`, container.volumes[i]?.Source || '');
modal = modal.replaceAll(`Vol${i}Destination`, container.volumes[i]?.Destination || '');
modal = modal.replaceAll(`Vol${i}RW`, container.volumes[i]?.RW || '');
}
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Label${i}Key`, Object.keys(container.labels)[i] || '');
modal = modal.replaceAll(`Label${i}Value`, Object.values(container.labels)[i] || '');
}
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Env${i}Key`, container.env[i]?.split('=')[0] || '');
modal = modal.replaceAll(`Env${i}Value`, container.env[i]?.split('=')[1] || '');
}
res.send(modal);
return;
}
// Uninstall modal
if (view == 'uninstall') {
let modal = readFileSync('./views/partials/uninstall.html', 'utf8');
modal = modal.replace(/AppName/g, container_name);
modal = modal.replace(/ContainerID/g, containerID);
res.send(modal);
return;
}
// Update link modal
if (view == 'link_modal') {
const [container, created] = await Container.findOrCreate({ where: { containerID: containerID }, defaults: { containerName: container_name, containerID: containerID, link: '' } });
let modal = readFileSync('./views/partials/link.html', 'utf8');
modal = modal.replace(/AppName/g, container_name);
modal = modal.replace(/ContainerID/g, containerID);
modal = modal.replace(/AppLink/g, container.link);
res.send(modal);
return;
}
// Update container_card
if (view == 'update_card'){
let lists = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['containers'] });
let container_list = JSON.parse(lists.containers);
let found = container_list.find(c => c.containerID === containerID);
if (!found) { res.send(''); return; }
let details = await containerInfo(containerID);
let card = await createCard(details);
res.send(card);
return;
}
// Generate list of container_cards for the dashboard
if (view == 'card_list'){
let cards_list = '';
// Check if there are any new cards in queue.
let new_cards = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['new'] });
let new_list = JSON.parse(new_cards.new);
// Check what containers the user should see.
let containers = await userCards(req);
// Create the cards.
if (new_list.length > 0) {
for (let i = 0; i < new_list.length; i++) {
let details = await containerInfo(new_list[i]);
let card = await createCard(details);
cards_list += card;
}
} else {
for (let i = 0; i < containers.length; i++) {
let details = await containerInfo(containers[i].containerID);
let card = await createCard(details);
cards_list += card;
}
}
// Update lists, clear the queue, and send the cards.
await ContainerLists.update({ containers: JSON.stringify(containers), sent: JSON.stringify(containers), new: '[]' }, { where: { userID: req.session.userID } });
res.send(cards_list);
return;
}
<div>
  ${message}
</div>
</div>
<button class="btn-close" data-hx-post="/dashboard/alert" data-hx-trigger="click" data-hx-target="#alert" data-hx-swap="outerHTML" style="padding-top: 0.5rem;" ></button>
</div>`;
}
// Container actions (start, stop, pause, restart, hide)
export const DashboardAction = async (req, res) => {
// let trigger_id = req.header('hx-trigger');
let container_name = req.header('hx-trigger-name');
let action = req.params.action;
let containerID = req.params.id;
// console.log(`[container_name] ${container_name} [action] ${action} [containerID] ${containerID}`);
if (action == 'reset') {
await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
res.redirect('/dashboard');
return;
} else if (action == 'update_link') {
let url = req.body.url;
let container = await Container.findOne({ where: { containerID: containerID } });
container.update({ link: url });
res.redirect('/dashboard');
return;
} else if (action == 'update_permissions') {
let { userID, username, reset_permissions, select } = req.body;
let button_id = req.header('hx-trigger');
// Replaces the update button if it's been pressed.
if (button_id == 'confirmed') { res.send(`<button class="btn" type="button" id="submit" hx-post="/dashboard/action/update_permissions/${containerID}" hx-swap="outerHTML">Update </button>`); return; }
// Reset all permissions for the container.
if (reset_permissions == '') { await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerID: containerID } }); trigger_docker_event(); return; }
// Make sure req.body[select] is an array
if (typeof req.body[select] == 'string') { req.body[select] = [req.body[select]]; }
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerID: containerID, userID: userID } });
if (req.body[select]) {
for (let i = 0; i < req.body[select].length; i++) {
let permissions = req.body[select][i];
if (permissions == 'uninstall') { await Permission.update({ uninstall: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'edit') { await Permission.update({ edit: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'upgrade') { await Permission.update({ upgrade: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'start') { await Permission.update({ start: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'stop') { await Permission.update({ stop: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'pause') { await Permission.update({ pause: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'restart') { await Permission.update({ restart: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'logs') { await Permission.update({ logs: true }, { where: {containerID: containerID, userID: userID}}); }
if (permissions == 'view') { await Permission.update({ view: true }, { where: {containerID: containerID, userID: userID}}); }
}
}
trigger_docker_event();
res.send(`<button class="btn" type="button" id="confirmed" hx-post="/dashboard/action/update_permissions/${containerID}" hx-swap="outerHTML" hx-trigger="load delay:1s">Update ✔️</button>`);
return;
} else if (action == 'switch_host') {
req.session.host = req.body.host;
console.log(`Switched to host ${req.session.host}`);
res.redirect('/dashboard');
export const UpdatePermissions = async (req, res) => {
let { user, container, reset_permissions } = req.body;
let id = req.header('hx-trigger');
if (reset_permissions) {
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerName: container} });
return;
}
// Inspect the container
let info = docker.getContainer(containerID);
let container = await info.inspect();
let containerState = container.State.Status;
// Displays container state (starting, stopping, restarting, pausing)
function status (state) {
return(`<div class="text-yellow d-inline-flex align-items-center lh-1 ms-auto" id="AltIDState">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
<strong>${state}</strong>
</div>`);
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false }, { where: { containerName: container, user: user } });
Object.keys(req.body).forEach(async function(key) {
if (key != 'user' && key != 'container') {
let permissions = req.body[key];
if (permissions.includes('uninstall')) { await Permission.update({ uninstall: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('edit')) { await Permission.update({ edit: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('upgrade')) { await Permission.update({ upgrade: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('start')) { await Permission.update({ start: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('stop')) { await Permission.update({ stop: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('pause')) { await Permission.update({ pause: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('restart')) { await Permission.update({ restart: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('logs')) { await Permission.update({ logs: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('view')) { await Permission.update({ view: true }, { where: {containerName: container, user: user}}); }
}
});
if (id == 'submit') {
res.send('<button class="btn" type="button" id="confirmed" hx-post="/updatePermissions" hx-swap="outerHTML" hx-trigger="load delay:2s">Update ✔️</button>');
return;
} else if (id == 'confirmed') {
res.send('<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissions" hx-swap="outerHTML">Update </button>');
return;
}
}
if ((action == 'start') && (containerState == 'exited')) {
info.start();
res.send(status('starting'));
} else if ((action == 'start') && (containerState == 'paused')) {
info.unpause();
res.send(status('starting'));
} else if ((action == 'stop') && (containerState != 'exited')) {
info.stop();
res.send(status('stopping'));
} else if ((action == 'pause') && (containerState == 'paused')) {
info.unpause();
res.send(status('starting'));
} else if ((action == 'pause') && (containerState == 'running')) {
info.pause();
res.send(status('pausing'));
} else if (action == 'restart') {
info.restart();
res.send(status('restarting'));
} else if (action == 'hide') {
let exists = await Permission.findOne({ where: { containerID: containerID, userID: req.session.userID }});
if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: containerID, username: req.session.username, userID: req.session.userID, hide: true }); }
else { exists.update({ hide: true }); }
res.send('ok');
}
// Container charts
export const Chart = async (req, res) => {
let name = req.header('hx-trigger-name');
if (!stats[name]) { stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) }; }
const info = await dockerContainerStats(name);
stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
stats[name].ramArray.push(Math.round(info[0].memPercent));
stats[name].cpuArray = stats[name].cpuArray.slice(-15);
stats[name].ramArray = stats[name].ramArray.slice(-15);
let chart = `
<script>
${name}chart.updateSeries([{
data: [${stats[name].cpuArray}]
}, {
data: [${stats[name].ramArray}]
}])
</script>`
res.send(chart);
}

View file

@ -1,31 +1,75 @@
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { imageList, GetContainerLists } from '../utils/docker.js';
import { docker } from '../server.js';
import { addAlert } from './dashboard.js';
export const Images = async function(req,res){
export const Images = async function(req, res) {
req.session.host = `${req.params.host || 1}`;
let action = req.params.action;
if (action == "remove") {
let images = req.body.select;
if (typeof(images) == 'string') {
images = [images];
}
for (let i = 0; i < images.length; i++) {
if (images[i] != 'on') {
try {
console.log(`Removing image: ${images[i]}`);
let image = docker.getImage(images[i]);
await image.remove();
} catch (error) {
console.log(`Unable to remove image: ${images[i]}`);
}
}
}
res.redirect("/images");
return;
} else if (action == "add") {
let image = req.body.image;
let tag = req.body.tag || 'latest';
try {
console.log(`Pulling image: ${image}:${tag}`);
await docker.pull(`${image}:${tag}`);
} catch (error) {
console.log(`Unable to pull image: ${image}:${tag}`);
}
res.redirect("/images");
return;
}
let containers = await docker.listContainers({ all: true });
let container_images = [];
let image_list = '';
let containers = await GetContainerLists();
for (let i = 0; i < containers.length; i++) {
container_images.push(containers[i].Image);
}
let images = await imageList();
let images = await docker.listImages({ all: true });
let image_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-type">Tag</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < images.length; i++) {
let name = '';
let tag = '';
try { name = images[i].RepoTags[0].split(':')[0]; } catch {}
try { tag = images[i].RepoTags[0].split(':')[1]; } catch {}
// let image_id = images[i].Id.split(':')[1].substring(0, 12);
let image_id = images[i].Id.split(':')[1];
let date = new Date(images[i].Created * 1000);
let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
@ -33,60 +77,34 @@ export const Images = async function(req,res){
size = size.toFixed(2);
let status = '';
try {
if (container_images.includes(images[i].RepoTags[0])) {
status = 'In use';
}
} catch {}
if (container_images.includes(images[i].RepoTags[0])) {
status = 'In use';
}
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="${images[i].Id}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${name}</td>
<td class="sort-type">${tag}</td>
<td class="sort-city">${image_id}</td>
<td class="sort-city">${images[i].Id}</td>
<td class="sort-score text-green">${status}</td>
<td class="sort-quantity">${size} MB</td>
<td class="sort-date" data-date="1628122643">${created}</td>
<td class=""><a class="container-action" href="#"><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></a></td>
<td class="sort-quantity">${size} MB</td>
<td class="text-end"><a class="btn" href="#"><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></a></td>
</tr>`
image_list += details;
}
image_list += `</tbody>`
res.render("images",{
alert: '',
username: req.session.username,
res.render("images", {
name: req.session.user,
role: req.session.role,
image_count: '',
avatar: req.session.user.charAt(0).toUpperCase(),
image_list: image_list,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const searchImages = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}
export const submitImages = async function(req,res){
// console.log(req.body);
let trigger_name = req.header('hx-trigger-name');
let trigger_id = req.header('hx-trigger');
console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`);
res.render("images",{
image_count: images.length,
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
footer: await Footer(req),
});
}

View file

@ -1,74 +1,70 @@
import { User, Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
import { User, Syslog, ServerSettings } from '../db/config.js';
export const Login = async function (req, res) {
if (req.session.userID) { res.redirect("/dashboard"); return; }
// Check authentication settings
let authentication = await ServerSettings.findOne({ where: { key: 'authentication' }});
if (!authentication) { await ServerSettings.create({ key: 'authentication', value: 'default' }); }
authentication = await ServerSettings.findOne({ where: { key: 'authentication' }});
// Create an empty session and redirect if authentication is disabled
if (authentication.value == 'localhost' && req.hostname == 'localhost') {
req.session.username = 'Localhost';
req.session.userID = '00000000-0000-0000-0000-000000000000';
req.session.role = 'admin';
await Syslog.create({ username: 'Localhost', uniqueID: 'localhost', event: "Login", message: "User logged in", ip: req.ip });
res.redirect("/dashboard");
return;
} else if (authentication.value == 'no_auth') {
req.session.username = 'No Auth';
req.session.userID = '00000000-0000-0000-0000-000000000000';
req.session.role = 'admin';
await Syslog.create({ username: 'No Auth', uniqueID: 'no_auth', event: "Login", message: "User logged in", ip: req.ip });
res.redirect("/dashboard");
return;
}
res.render("login",{
"error":"",
});
export const Login = function(req,res){
if (req.session.user) { res.redirect("/logout"); }
else { res.render("login",{ "error":"", }); }
}
export const submitLogin = async function(req,res){
let { email, password } = req.body;
email = email.toLowerCase();
export const submitLogin = async function (req, res) {
if (email && password) {
let existingUser = await User.findOne({ where: {email:email}});
if (existingUser) {
const { password } = req.body;
let email = req.body.email.toLowerCase();
let match = await bcrypt.compare(password,existingUser.password);
// If one of the fields is empty.
if (!email || !password) { res.render("login",{ "error": "Invalid credentials." }); return; }
if (match) {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
let user = await User.findOne({ where: { email: email }});
req.session.user = existingUser.username;
req.session.UUID = existingUser.UUID;
req.session.role = existingUser.role;
req.session.avatar = existingUser.avatar;
// If there is no users with that email or the password is incorrect.
if (!user || !await bcrypt.compare(password, user.password)) {
await Syslog.create({ username: '', uniqueID: email, event: "Login Attempt", message: "User login failed", ip: req.ip });
res.render("login",{ "error": "Invalid credentials." });
return;
}
// Log the user in.
else {
req.session.username = user.username;
req.session.userID = user.userID;
req.session.role = user.role;
let newLogin = new Date().toLocaleString();
await User.update({ lastLogin: newLogin }, { where: { email: email } });
const syslog = await Syslog.create({
user: req.session.user,
email: email,
event: "Successful Login",
message: "User logged in successfully",
ip: req.socket.remoteAddress
});
res.redirect("/dashboard");
} else {
console.log(`${req.session.username} logged in`);
const syslog = await Syslog.create({
user: null,
email: email,
event: "Bad Login",
message: "Invalid password",
ip: req.socket.remoteAddress
});
await Syslog.create({ username: user.username, uniqueID: email, event: "Login", message: "User logged in", ip: req.ip });
res.redirect("/dashboard");
return;
res.render("login",{
"error":"Invalid password",
});
}
} else {
res.render("login",{
"error":"User with that email does not exist.",
});
}
} else {
res.status(400);
res.render("login",{
"error":"Please fill in all the fields.",
});
}
}
export const Logout = async function(req,res){
console.log(`User ${req.session.username} logged out \n`);
await Syslog.create({ username: req.session.username, uniqueID: req.session.userID, event: "Logout", message: "User logged out", ip: req.ip });
export const Logout = function(req,res){
req.session.destroy(() => {
res.redirect("/login");
});

View file

@ -1,30 +1,44 @@
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { networkList, GetContainerLists, removeNetwork } from '../utils/docker.js';
import { docker } from '../server.js';
export const Networks = async function(req, res) {
req.session.host = `${req.params.host || 1}`;
let container_networks = [];
let network_name = '';
let containers = await GetContainerLists();
// List all containers
let containers = await docker.listContainers({ all: true });
for (let i = 0; i < containers.length; i++) {
try { network_name += containers[i].HostConfig.NetworkMode; } catch {}
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID); } catch {}
let network_name = containers[i].HostConfig.NetworkMode;
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID) } catch {}
}
let networks = await networkList();
let network_list = '';
let networks = await docker.listNetworks({ all: true });
let network_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < networks.length; i++) {
// let date = new Date(images[i].Created * 1000);
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let status = '';
// Check if the network is in use
try { if (container_networks.includes(networks[i].Id)) { status = `In use`; } } catch {}
// Create the row for the network entry
if (container_networks.includes(networks[i].Id)) {
status = `In use`;
}
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
@ -32,57 +46,44 @@ export const Networks = async function(req, res) {
<td class="sort-city">${networks[i].Id}</td>
<td class="sort-score text-green">${status}</td>
<td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
<td class=""><button class="badge badge-outline text-grey" id="" data-hx-get="/users/usersModals/user/" hx-target="#modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Details</button></td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
// Add the row to the network list
network_list += details;
}
network_list += `</tbody>`
res.render("networks",{
alert: '',
username: req.session.username,
res.render("networks", {
name: req.session.user,
role: req.session.role,
network_count: '',
avatar: req.session.user.charAt(0).toUpperCase(),
network_list: network_list,
navbar: await Navbar(req),
footer: await Footer(req),
network_count: networks.length,
alert: '',
});
}
export const NetworkAction = async function(req,res){
// let trigger_name = req.header('hx-trigger-name');
// let trigger_id = req.header('hx-trigger');
// console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`);
// console.log(req.body);
// Grab the list of networks
export const removeNetwork = async function(req, res) {
let networks = req.body.select;
// Make sure the value is an array
if (typeof(networks) == 'string') { networks = [networks]; }
// Loop through the array
if (typeof(networks) == 'string') {
networks = [networks];
}
for (let i = 0; i < networks.length; i++) {
if (networks[i] != 'on') {
try {
await removeNetwork(networks[i]);
console.log(`Network removed: ${networks[i]}`);
}
catch {
console.log(`Removing network: ${networks[i]}`);
let network = docker.getNetwork(networks[i]);
await network.remove();
} catch (error) {
console.log(`Unable to remove network: ${networks[i]}`);
}
}
}
res.redirect("/networks");
}
export const searchNetworks = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}

389
controllers/portal.js Normal file
View file

@ -0,0 +1,389 @@
import { Readable } from 'stream';
import { Permission, Container, User } from '../database/models.js';
import { docker } from '../server.js';
import { readFileSync } from 'fs';
let hidden = '';
// The actual page
export const Portal = (req, res) => {
let name = req.session.user;
let role = req.session.role;
let avatar = name.charAt(0).toUpperCase();
res.render("portal", {
name: name,
avatar: avatar,
role: role,
alert: '',
});
}
async function CardList () {
let name = req.session.user;
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
for (let i = 0; i < containers.length; i++) {
let details = await containerInfo(containers[i].containerName);
let card = await createCard(details);
cardList += card;
}
}
export const UserContainers = async (req, res) => {
let cardList = '';
let name = req.session.user;
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
for (let i = 0; i < containers.length; i++) {
if (containers[i].containerName == null) { continue; }
let details = await containerInfo(containers[i].containerName);
let card = await createCard(details);
cardList += card;
}
res.send(cardList);
}
async function containerInfo (containerName) {
let container = docker.getContainer(containerName);
let info = await container.inspect();
let image = info.Config.Image.split('/');
let ports_list = [];
try {
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {
// no exposed ports
}
let external = 0;
let internal = 0;
try {
external = ports_list[0].external;
internal = ports_list[0].internal;
} catch {
// no exposed ports
}
let details = {
name: containerName,
image: image,
service: image[image.length - 1].split(':')[0],
state: info.State.Status,
external_port: external,
internal_port: internal,
ports: ports_list,
link: 'localhost',
}
return details;
}
async function createCard (details) {
if (hidden.includes(details.name)) { return;}
let shortname = details.name.slice(0, 10) + '...';
let trigger = 'data-hx-trigger="load, every 3s"';
let state = details.state;
let state_color = '';
switch (state) {
case 'running':
state_color = 'green';
break;
case 'exited':
state = 'stopped';
state_color = 'red';
trigger = 'data-hx-trigger="load"';
break;
case 'paused':
state_color = 'orange';
trigger = 'data-hx-trigger="load"';
break;
case 'installing':
state_color = 'blue';
trigger = 'data-hx-trigger="load"';
break;
}
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
let card = readFileSync('./views/partials/containerSimple.html', 'utf8');
card = card.replace(/AppName/g, details.name);
card = card.replace(/AppShortName/g, shortname);
card = card.replace(/AppIcon/g, details.service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ExternalPort/g, details.external_port);
card = card.replace(/InternalPort/g, details.internal_port);
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
card = card.replace(/AppNameState/g, `${details.name}State`);
card = card.replace(/data-trigger=""/, trigger);
return card;
}
let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
export async function addCard (name, state) {
console.log(`Adding card for ${name}: ${state}`);
let details = {
name: name,
image: name,
service: name,
state: 'installing',
external_port: 0,
internal_port: 0,
ports: [],
link: 'localhost',
}
createCard(details).then(card => {
cardList += card;
});
}
// HTMX server-side events
export const SSE = async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
let eventCheck = setInterval(async () => {
// builds array of containers and their states
containersArray = [];
await docker.listContainers({ all: true }).then(containers => {
containers.forEach(container => {
let name = container.Names[0].replace('/', '');
if (!hidden.includes(name)) { // if not hidden
containersArray.push({ container: name, state: container.State });
}
});
});
if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
cardList = '';
newCards = '';
containersArray.forEach(container => {
const { container: containerName, state } = container;
const existingContainer = sentArray.find(c => c.container === containerName);
if (!existingContainer) {
containerInfo(containerName).then(details => {
createCard(details).then(card => {
newCards += card;
});
});
res.write(`event: update\n`);
res.write(`data: 'update cards'\n\n`);
} else if (existingContainer.state !== state) {
updatesArray.push(containerName);
}
containerInfo(containerName).then(details => {
createCard(details).then(card => {
cardList += card;
});
});
});
sentArray.forEach(container => {
const { container: containerName } = container;
const existingContainer = containersArray.find(c => c.container === containerName);
if (!existingContainer) {
updatesArray.push(containerName);
}
});
for (let i = 0; i < updatesArray.length; i++) {
res.write(`event: ${updatesArray[i]}\n`);
res.write(`data: 'update cards'\n\n`);
}
updatesArray = [];
sentArray = containersArray.slice();
}
}, 500);
req.on('close', () => {
clearInterval(eventCheck);
});
};
export const updateCards = async (req, res) => {
console.log('updateCards called');
res.send(newCards);
newCards = '';
}
export const Containers = async (req, res) => {
CardList();
// res.send(cardList);
}
export const Card = async (req, res) => {
let name = req.header('hx-trigger-name');
console.log(`${name} requesting updated card`);
// return nothing if in hidden or not found in containersArray
if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
res.send('');
return;
} else {
let details = await containerInfo(name);
let card = await createCard(details);
res.send(card);
}
}
function status (state) {
let status = `<span class="text-yellow align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
${state}
</span>`;
return status;
}
export const Logs = (req, res) => {
let name = req.header('hx-trigger-name');
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) => {
res.send(`<pre>${data}</pre> `)
});
}
export const Action = async (req, res) => {
let name = req.header('hx-trigger-name');
let state = req.header('hx-trigger');
let action = req.params.action;
// Start
if ((action == 'start') && (state == 'stopped')) {
var containerName = docker.getContainer(name);
containerName.start();
res.send(status('starting'));
} else if ((action == 'start') && (state == 'paused')) {
var containerName = docker.getContainer(name);
containerName.unpause();
res.send(status('starting'));
// Stop
} else if ((action == 'stop') && (state != 'stopped')) {
var containerName = docker.getContainer(name);
containerName.stop();
res.send(status('stopping'));
// Pause
} else if ((action == 'pause') && (state == 'paused')) {
var containerName = docker.getContainer(name);
containerName.unpause();
res.send(status('starting'));
} else if ((action == 'pause') && (state == 'running')) {
var containerName = docker.getContainer(name);
containerName.pause();
res.send(status('pausing'));
// Restart
} else if (action == 'restart') {
var containerName = docker.getContainer(name);
containerName.restart();
res.send(status('restarting'));
// Hide
} else if (action == 'hide') {
let exists = await Container.findOne({ where: {name: name}});
if (!exists) {
const newContainer = await Container.create({ name: name, visibility: false, });
} else {
exists.update({ visibility: false });
}
hidden = await Container.findAll({ where: {visibility:false}});
hidden = hidden.map((container) => container.name);
res.send("ok");
// Reset View
} else if (action == 'reset') {
await Container.update({ visibility: true }, { where: {} });
hidden = await Container.findAll({ where: {visibility:false}});
hidden = hidden.map((container) => container.name);
res.send("ok");
}
}
export const Modals = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
let title = name.charAt(0).toUpperCase() + name.slice(1);
if (id == 'permissions') {
let permissions_list = '';
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
let users = await User.findAll({ attributes: ['username', 'UUID']});
for (let i = 0; i < users.length; i++) {
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (!exists) {
const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
}
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
permissions_list += user_permissions;
}
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
res.send(permissions_modal);
return;
}
if (id == 'uninstall') {
let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
modal = modal.replace(/AppName/g, name);
// let containerPermissions = await Permission.findAll({ where: {containerName: name}});
res.send(modal);
return;
}
let modal = readFileSync('./views/modals/details.html', 'utf8');
let details = await containerInfo(name);
modal = modal.replace(/AppName/g, details.name);
modal = modal.replace(/AppImage/g, details.image);
res.send(modal);
}

View file

@ -1,52 +0,0 @@
import { ServerSettings, User } from '../db/config.js';
import { Alert, getLanguage, Navbar, Sidebar, Footer, Capitalize } from '../utils/system.js';
export const Preferences = async function(req,res){
let language = await getLanguage(req.session.userID);
let Language = Capitalize(language);
let selected = `<option value="${language}" selected hidden>${Language}</option>`;
let user = await User.findOne({ where: { userID: req.session.userID }});
let preferences = JSON.parse(user.preferences);
let hide_profile = preferences.hide_profile;
let checked = ''; if (hide_profile == true) { checked = 'checked'; }
res.render("preferences",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
selected: selected,
hide_profile: checked,
});
}
export const submitPreferences = async function(req,res){
let { language_input, hidden_input, check_languages } = req.body;
if (hidden_input == 'on') { hidden_input = true; } else { hidden_input = false; }
let user_preferences = {
hide_profile: hidden_input,
};
if (language_input != undefined && hidden_input != undefined) {
await User.update({ preferences: JSON.stringify(user_preferences), language: language_input }, { where: { userID: req.session.userID }});
}
res.redirect('/preferences');
}
export const searchPreferences = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}

View file

@ -1,113 +1,104 @@
import bcrypt from "bcrypt";
import { Op } from "sequelize";
import { User, ServerSettings, Permission, Syslog } from "../db/config.js";
import { User, Syslog, Permission } from '../database/models.js';
import bcrypt from 'bcrypt';
let SECRET = process.env.SECRET || "MrWiskers"
export const Register = async function(req,res){
if (req.session.username) { res.redirect("/dashboard"); }
let secret_input = '';
let user_registration = await ServerSettings.findOne({ where: { key: 'user_registration' }});
if (user_registration == null ) { user_registration = false; }
else { user_registration = user_registration.value; }
if (user_registration) {
secret_input = `<div class="mb-3"><label class="form-label">Secret</label>
<div class="input-group input-group-flat">
<input type="text" class="form-control" autocomplete="off" name="registration_secret">
</div>
</div>`}
// If there are no users, or registration has been enabled, display the registration page.
if ((await User.count() == 0) || (user_registration)) {
res.render("register",{
"error": "",
"reg_secret": secret_input,
});
export const Register = function(req,res){
if(req.session.user){
res.redirect("/logout");
} else {
res.render("login", {
"error": "User registration is disabled."
res.render("register",{
"error":"",
});
}
}
export const submitRegister = async function(req,res){
const { name, username, password, confirm, secret } = req.body;
let email = req.body.email.toLowerCase();
let { name, username, email, password, confirmPassword, secret } = req.body;
email = email.toLowerCase();
let registration_secret = await ServerSettings.findOne({ where: { key: 'registration_secret' }}).value;
let error = '';
if (!name || !username || !email || !password || !confirm) { error = "All fields are required"; }
else if (password !== confirm) { error = "Passwords do not match"; }
else if (registration_secret && secret !== registration_secret) {
error = "Invalid secret";
await Syslog.create({ username: user.username, uniqueID: email, event: "Failed Registration", message: "Invalid Secret", ip: req.ip });
if (secret != SECRET) {
const syslog = await Syslog.create({
user: username,
email: email,
event: "Failed Registration",
message: "Invalid secret",
ip: req.socket.remoteAddress
});
}
else if (await User.findOne({ where: { [Op.or]: [{ username: username }, { email: email }] }})) {
error = "Username or email already exists";
await Syslog.create({ username: username, uniqueID: email, event: "Failed Registration", message: "Username or email already exists", ip: req.ip });
}
if((name && email && password && confirmPassword && username) && (secret == SECRET) && (password == confirmPassword)){
if (error != '') {
let secret_input = '';
let user_registration = await ServerSettings.findOne({ where: { key: 'user_registration' }});
if (user_registration == null ) { user_registration = false; }
else { user_registration = user_registration.value; }
if (user_registration) {
secret_input = `<div class="mb-3"><label class="form-label">Secret</label>
<div class="input-group input-group-flat">
<input type="text" class="form-control" autocomplete="off" name="registration_secret">
</div>
</div>`}
async function userRole () {
let userCount = await User.count();
if(userCount == 0){
return "admin";
}else{
return "user";
}
}
res.render("register", {
"error": error,
"reg_secret": secret_input,
});
return;
}
let existingUser = await User.findOne({ where: {email:email}});
if(!existingUser){
try {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
// Returns 'admin' if no users have been created.
async function Role() {
if (await User.count() == 0) { return "admin"; }
else { return "user"; }
}
const user = await User.create({
name: name,
username: username,
email: email,
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
lastLogin: newLogin,
});
// Create the user.
await User.create({
name: name,
username: username,
email: email,
password: bcrypt.hashSync(password, 10),
role: await Role(),
preferences: JSON.stringify({ hidden_profile: false }),
lastLogin: new Date().toLocaleString(),
});
// make sure the user was created and get the UUID.
let newUser = await User.findOne({ where: {email:email}});
let match = await bcrypt.compare(password,newUser.password);
// Make sure the user was created and get the UUID.
let user = await User.findOne({ where: { email: email }});
let match = await bcrypt.compare(password, user.password);
if (match) {
req.session.username = user.username;
req.session.userID = user.userID;
req.session.role = user.role;
await Syslog.create({ username: user.username, uniqueID: user.email, event: "Registration", message: "User created", ip: req.ip });
if(match){
req.session.user = newUser.username;
req.session.UUID = newUser.UUID;
req.session.role = newUser.role;
console.log(`User ${username} created`);
const permission = await Permission.create({
user: newUser.username,
userID: newUser.UUID
});
res.redirect("/dashboard");
const syslog = await Syslog.create({
user: req.session.user,
email: email,
event: "Successful Registration",
message: "User registered successfully",
ip: req.socket.remoteAddress
});
res.redirect("/dashboard");
}
} catch(err) {
res.render("register",{
"error":"Something went wrong when creating account.",
});
}
} else {
// return an error.
res.render("register",{
"error":"User with that email already exists.",
});
}
} else {
await Syslog.create({ username: user.username, uniqueID: user.email, event: "Failed Registration", message: "Error. User not created", ip: req.ip });
res.render("register", { "error": "Error. User not created" });
// Redirect to the signup page.
res.render("register",{
"error":"Please fill in all the fields.",
});
}
}
}

View file

@ -1,252 +1,10 @@
import { ServerSettings } from '../db/config.js';
import { configureHost } from '../utils/docker.js';
import { Alert, Navbar, Sidebar, Footer } from '../utils/system.js';
import { readFileSync, writeFileSync } from 'fs';
export const Settings = async function(req,res){
export const Settings = (req, res) => {
req.session.host = `${req.params.host || 1}`;
let user_registration = await ServerSettings.findOne({ where: {key: 'user_registration'}});
let registration_secret = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
let authentication = await ServerSettings.findOne({ where: {key: 'authentication'}}) || { value: 'default' };
let user_registration_enabled = '';
try { if (user_registration.value == true) { user_registration_enabled = 'checked'; } } catch { }
let registration_secret_value = '';
try { registration_secret_value = registration_secret.value; } catch { }
let custom_link = await ServerSettings.findOne({ where: {key: 'custom_link'}});
let link_url = await ServerSettings.findOne({ where: {key: 'link_url'}});
let custom_link_enabled = '';
try { if (custom_link.value == true) { custom_link_enabled = 'checked'; } } catch { }
let link_url_value = '';
try { link_url_value = link_url.value; } catch { }
let host2 = await ServerSettings.findOne({ where: {key: 'host2'}});
let host3 = await ServerSettings.findOne({ where: {key: 'host3'}});
let host4 = await ServerSettings.findOne({ where: {key: 'host4'}});
let [host2_toggle, host2_tag, host2_ip, host2_port] = ['', '', '', ''];
let [host3_toggle, host3_tag, host3_ip, host3_port] = ['', '', '', ''];
let [host4_toggle, host4_tag, host4_ip, host4_port] = ['', '', '', ''];
if (host2.value) { host2_toggle = 'checked'; [host2_tag, host2_ip, host2_port] = host2.value.split(','); }
if (host3.value) { host3_toggle = 'checked'; [host3_tag, host3_ip, host3_port] = host3.value.split(','); }
if (host4.value) { host4_toggle = 'checked'; [host4_tag, host4_ip, host4_port] = host4.value.split(','); }
res.render("settings",{
alert: '',
username: req.session.username,
res.render("settings", {
name: req.session.user,
role: req.session.role,
user_registration: user_registration_enabled,
registration_secret: registration_secret_value,
custom_link: custom_link_enabled,
link_url: link_url_value,
authentication: authentication.value,
host2_toggle: host2_toggle,
host2_tag: host2_tag,
host2_ip: host2_ip,
host2_port: host2_port,
host3_toggle: host3_toggle,
host3_tag: host3_tag,
host3_ip: host3_ip,
host3_port: host3_port,
host4_toggle: host4_toggle,
host4_tag: host4_tag,
host4_ip: host4_ip,
host4_port: host4_port,
selected: 'english',
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});
}
export const SettingsAction = async function (req, res) {
let action = req.params.action;
let id = req.params.id;
let { user_registration, registration_secret, custom_link, link_url, authentication } = req.body;
let { host2, tag2, ip2, port2 } = req.body;
let { host3, tag3, ip3, port3 } = req.body;
let { host4, tag4, ip4, port4 } = req.body;
if (tag2 == '') { tag2 = 'Host 2'; }
if (tag3 == '') { tag3 = 'Host 3'; }
if (tag4 == '') { tag4 = 'Host 4'; }
let trigger_name = req.header('hx-trigger-name');
let trigger_id = req.header('hx-trigger');
// If the trigger is 'submit', return the button
if (trigger_id == 'submit'){
res.send(`<button class="btn btn-primary" id="submit" form="settings">Update</button>`);
return;
}
// Continues on if the trigger is 'settings'
// Custom link
if (custom_link) {
let exists = await ServerSettings.findOne({ where: {key: 'custom_link'}});
if (exists) { await ServerSettings.update({value: true}, {where: {key: 'custom_link'}}); }
else { await ServerSettings.create({ key: 'custom_link', value: true}); }
let exists2 = await ServerSettings.findOne({ where: {key: 'link_url'}});
if (exists2) { await ServerSettings.update({value: link_url}, {where: {key: 'link_url'}}); }
else { await ServerSettings.create({ key: 'link_url', value: link_url}); }
} else if (!custom_link) {
let exists = await ServerSettings.findOne({ where: {key: 'custom_link'}});
if (exists) { await ServerSettings.update({value: false}, {where: {key: 'custom_link'}}); }
else { await ServerSettings.create({ key: 'custom_link', value: false}); }
let exists2 = await ServerSettings.findOne({ where: {key: 'link_url'}});
if (exists2) { await ServerSettings.update({value: 'http://localhost'}, {where: {key: 'link_url'}}); }
else { await ServerSettings.create({ key: 'link_url', value: 'http://localhost'}); }
}
// User registration
if (user_registration) {
let exists = await ServerSettings.findOne({ where: {key: 'user_registration'}});
if (exists) { const setting = await ServerSettings.update({value: true}, {where: {key: 'user_registration'}}); }
else { const newSetting = await ServerSettings.create({ key: 'user_registration', value: true}); }
let exists2 = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
if (exists2) { await ServerSettings.update({value: registration_secret}, {where: {key: 'registration_secret'}}); }
else { await ServerSettings.create({ key: 'registration_secret', value: registration_secret}); }
} else if (!user_registration) {
let exists = await ServerSettings.findOne({ where: {key: 'user_registration'}});
if (exists) { await ServerSettings.update({value: false}, {where: {key: 'user_registration'}}); }
else { await ServerSettings.create({ key: 'user_registration', value: false}); }
let exists2 = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
if (exists2) { await ServerSettings.update({value: ''}, {where: {key: 'registration_secret'}}); }
else { await ServerSettings.create({ key: 'registration_secret', value: ''}); }
}
// Authentication
if (authentication) {
let exists = await ServerSettings.findOne({ where: {key: 'authentication'}});
if (exists) { await ServerSettings.update({value: authentication}, {where: {key: 'authentication'}}); }
else { await ServerSettings.create({ key: 'authentication', value: authentication}); }
} else if (!authentication) {
let exists = await ServerSettings.findOne({ where: {key: 'authentication'}});
if (exists) { await ServerSettings.update({value: 'default'}, {where: {key: 'authentication'}}); }
else { await ServerSettings.create({ key: 'authentication', value: 'off'}); }
}
// Host 2
if (host2) {
let exists = await ServerSettings.findOne({ where: {key: 'host2'}});
if (exists) { await ServerSettings.update({value: `${tag2},${ip2},${port2}`}, {where: {key: 'host2'}}); }
else { await ServerSettings.create({ key: 'host2', value: `${tag2},${ip2},${port2}`}); }
configureHost(2, ip2, port2);
} else if (!host2) {
let exists = await ServerSettings.findOne({ where: {key: 'host2'}});
if (exists) { await ServerSettings.update({value: ''}, {where: {key: 'host2'}}); }
else { await ServerSettings.create({ key: 'host2', value: ''}); }
}
// Host 3
if (host3) {
let exists = await ServerSettings.findOne({ where: {key: 'host3'}});
if (exists) { await ServerSettings.update({value: `${tag3},${ip3},${port3}`}, {where: {key: 'host3'}}); }
else { await ServerSettings.create({ key: 'host3', value: `${tag3},${ip3},${port3}`}); }
configureHost(3, ip3, port3);
} else if (!host3) {
let exists = await ServerSettings.findOne({ where: {key: 'host3'}});
if (exists) { await ServerSettings.update({value: ''}, {where: {key: 'host3'}}); }
else { await ServerSettings.create({ key: 'host3', value: ''}); }
}
// Host 4
if (host4) {
let exists = await ServerSettings.findOne({ where: {key: 'host4'}});
if (exists) { await ServerSettings.update({value: `${tag4},${ip4},${port4}`}, {where: {key: 'host4'}}); }
else { await ServerSettings.create({ key: 'host4', value: `${tag4},${ip4},${port4}`}); }
configureHost(4, ip4, port4);
} else if (!host4) {
let exists = await ServerSettings.findOne({ where: {key: 'host4'}});
if (exists) { await ServerSettings.update({value: ''}, {where: {key: 'host4'}}); }
else { await ServerSettings.create({ key: 'host4', value: ''}); }
}
console.log('Settings updated');
res.send(`<button class="btn btn-success" hx-post="/settings/action/update_settings/0" hx-trigger="load delay:2s" hx-swap="outerHTML" id="submit" hx-target="#submit">Updated</button>`);
}
let inProgress = false;
export const updateLanguages = async function(req,res){
let trigger_id = req.header('hx-trigger');
if (inProgress == true) {
console.log('Language update still in progress');
res.send('<button class="btn" aria-label="button" id="checking" hx-post="/update_languages" hx-swap="outerHTML" hx-target="#checking" hx-trigger="every 2s">Checking For Updates<div class="mx-2 spinner-border spinner-border-sm"></div></button>');
return;
}
if (trigger_id == 'check_languages') {
inProgress = true;
res.send('<button class="btn" aria-label="button" id="checking" hx-post="/update_languages" hx-swap="outerHTML" hx-target="#checking" hx-trigger="every 2s">Checking For Updates<div class="mx-2 spinner-border spinner-border-sm"></div></button>');
const resp = await fetch(`https://api.github.com/repos/lllllllillllllillll/DweebUI/contents/languages?ref=dev`);
const data = await resp.json();
let languages = [];
data.forEach((lang) => {
languages.push({ language: lang.name, download_url: lang.download_url });
});
for (let i = 0; i < languages.length; i++) {
let language_dev = await fetch(languages[i].download_url);
language_dev = await language_dev.text();
let language_local = readFileSync(`./languages/${languages[i].language}`, 'utf8');
if (language_dev != language_local) {
console.log(`\x1b[31mLanguage: ${languages[i].language} is out of date.\x1b[0m`);
console.log(`\x1b[31mUpdating ${languages[i].language}...\x1b[0m`);
writeFileSync(`./languages/${languages[i].language}`, language_dev);
console.log(`\x1b[32mLanguage: ${languages[i].language} has been updated.\x1b[0m`);
} else {
console.log(`\x1b[32mLanguage: ${languages[i].language} is up to date.\x1b[0m`);
}
}
inProgress = false;
console.log('Language update complete');
return;
} else {
if ((trigger_id == "checking") && (inProgress == false)) {
res.send('<button class="btn" aria-label="button" name="check_languages" id="check_languages" value="true" hx-post="/update_languages" hx-swap="outerHTML" hx-target="#check_languages">Update Language Files</button>');
return;
}
}
}
export const searchSettings = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}

View file

@ -1,50 +0,0 @@
import { ServerSettings, User } from '../db/config.js';
import { Alert, getLanguage, Navbar, Sidebar, Footer, Capitalize } from '../utils/system.js';
import { readdirSync, readFileSync } from 'fs';
import bcrypt from 'bcrypt';
export const Sponsors = async function (req, res) {
let language = await getLanguage(req.session.userID);
let Language = Capitalize(language);
let selected = `<option value="${language}" selected hidden>${Language}</option>`;
let user = await User.findOne({ where: { userID: req.session.userID }});
let preferences = JSON.parse(user.preferences);
let hide_profile = preferences.hide_profile;
let checked = ''; if (hide_profile == true) { checked = 'checked'; }
res.render("sponsors",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
selected: selected,
hide_profile: checked,
});
}
export const searchSponsors = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
let sponsored = await ServerSettings.findOne({ where: { key: 'sponsored' }});
if (!sponsored) {
let secret_hash = '$2b$10$2EDoqM10LbNMmSVdbrOV/.eLFlYrxBk4An02prZeqRSqRVktNi3m.';
let correct_key = bcrypt.compareSync(req.body.search, secret_hash);
if (correct_key) {
await ServerSettings.create({ key: 'sponsored', value: 'true' });
console.log('Sponsored. Thank you for your support!');
}
}
res.send('ok');
return;
}

31
controllers/supporters.js Normal file
View file

@ -0,0 +1,31 @@
import { User } from "../database/models.js";
export const Supporters = async (req, res) => {
let user = await User.findOne({ where: { UUID: req.session.UUID }});
res.render("supporters", {
first_name: user.name,
last_name: user.name,
name: user.name,
id: user.id,
email: user.email,
role: user.role,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});
}
let thanks = 0;
export const Thanks = async (req, res) => {
thanks++;
let data = thanks.toString();
if (thanks > 999) {
data = 'Did you really click 1000 times?!';
}
res.send(data);
}

View file

@ -1,10 +1,7 @@
import { Syslog } from '../db/config.js';
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { Syslog } from '../database/models.js';
export const Syslogs = async function(req, res) {
req.session.host = `${req.params.host || 1}`;
let logs = '';
const syslogs = await Syslog.findAll({
@ -13,74 +10,28 @@ export const Syslogs = async function(req, res) {
]
});
for (const log of syslogs) {
let date = (log.createdAt).toDateString();
let time = (log.createdAt).toLocaleTimeString();
let datetime = `${time} ${date}`;
// get the last 12 characters of the uniqueID
let uniqueID = log.uniqueID;
// if (uniqueID.length > 12) {
// uniqueID = uniqueID.substring(uniqueID.length - 12);
// }
let message = log.message;
// if (message.length > 50) {
// message = message.substring(0, 50) + '...';
// }
logs += `<tr>
<td><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select"></td>
<td class="sort-id">${log.id}</td>
<td class="sort-username">${log.username}</td>
<td class="sort-uniqueid">${uniqueID}</td>
<td class="sort-user">${log.user}</td>
<td class="sort-email">${log.email}</td>
<td class="sort-event">${log.event}</td>
<td class="sort-message">${message}</td>
<td class="sort-message">${log.message}</td>
<td class="sort-ip">${log.ip}</td>
<td class="sort-timestamp">${datetime}</td>
<td class=""><a class="" href="#"><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></a></td>
<td class="sort-datetime">${datetime}</td>
</tr>`
}
res.render("syslogs",{
alert: '',
username: req.session.username,
role: req.session.role,
res.render("syslogs", {
name: req.session.user || 'Dev',
role: req.session.role || 'Dev',
avatar: req.session.user.charAt(0).toUpperCase(),
logs: logs,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const submitSyslogs = async function(req,res){
// console.log(req.body);
let trigger_name = req.header('hx-trigger-name');
let trigger_id = req.header('hx-trigger');
console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`);
res.render("syslogs",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const searchSyslogs = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}

View file

@ -1,12 +1,21 @@
import { User, Permission, ContainerLists, Container, ServerSettings } from '../db/config.js';
import { Alert, Navbar, Footer } from '../utils/system.js';
import { readFileSync } from 'fs';
import { User } from '../database/models.js';
export const Users = async function(req,res){
req.session.host = `${req.params.host || 1}`;
let user_list = '';
export const Users = async (req, res) => {
let user_list = `
<tr>
<th><input class="form-check-input" type="checkbox"></th>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>UUID</th>
<th>Role</th>
<th>Last Login</th>
<th>Status</th>
<th>Actions</th>
</tr>`
let allUsers = await User.findAll();
allUsers.forEach((account) => {
@ -21,128 +30,33 @@ export const Users = async function(req,res){
active = '<span class="badge badge-outline text-grey" title="User has not logged-in within the last 30 days.">Inactive</span>';
}
let info = `
<tr>
<td><input class="form-check-input" type="checkbox" name="select"></td>
<td class="sort-id">${account.id}</td>
<td class="sort-avatar p-1"><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
<td class="sort-name">${account.name}</td>
<td class="sort-username">${account.username}</td>
<td class="sort-email">${account.email}</td>
<td class="sort-userid">${account.userID}</td>
<td class="sort-role">${account.role}</td>
<td class="sort-lastlogin">${account.lastLogin}</td>
<td class="sort-active">${active}</td>
<td class="sort-action"><button class="badge badge-outline text-grey" id="${account.username}" data-hx-get="/users/view/user/${account.userID}" hx-target="#modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">View</button></td>
<td><input class="form-check-input" type="checkbox"></td>
<td>${account.id}</td>
<td><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
<td>${account.name}</td>
<td>${account.username}</td>
<td>${account.email}</td>
<td>${account.UUID}</td>
<td>${account.role}</td>
<td>${account.lastLogin}</td>
<td>${active}</td>
<td><a href="#" class="btn">View</a></td>
</tr>`
user_list += info;
});
res.render("users",{
alert: '',
username: req.session.username,
res.render("users", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
user_list: user_list,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const submitUsers = async function(req,res){
// console.log(req.body);
let trigger_name = req.header('hx-trigger-name');
let trigger_id = req.header('hx-trigger');
console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`);
// [HTMX Triggered] Changes the update button.
if(trigger_id == 'settings'){
res.send(`<button class="btn btn-success" hx-post="/settings" hx-trigger="load delay:2s" hx-swap="outerHTML" id="submit" hx-target="#submit">Updated</button>`);
return;
} else if (trigger_id == 'submit'){
res.send(`<button class="btn btn-primary" id="submit" form="settings">Update</button>`);
return;
}
res.render("users",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
footer: await Footer(req),
alert: ''
});
}
export const searchUsers = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}
export const UsersView = async (req, res) => {
let view = req.params.view;
let userID = req.params.id;
let username = req.header('hx-trigger');
// console.log(`[view] ${view} - [userID] ${userID} - [username] ${username}`);
if (view == 'user') {
let user = await User.findOne({ where: { userID: userID } });
let modal = readFileSync('./views/partials/user.html', 'utf8');
modal = modal.replace(/Username/g, username);
modal = modal.replace(/USERID/g, user.userID);
modal = modal.replace(/FullName/g, user.name);
modal = modal.replace(/EmailAddress/g, user.email);
modal = modal.replace(/LastLogin/g, user.lastLogin);
modal = modal.replace(/CreatedAt/g, user.createdAt);
res.send(modal);
return;
}
};
export const UsersAction = async (req, res) => {
let action = req.params.action;
let userID = req.params.id;
let change = req.body.change;
console.log(`[action] ${action} [change] ${change} - [userID] ${userID}`);
if (change == 'remove') {
let container_lists = await ContainerLists.findAll({ where: { userID: userID } });
container_lists.destroy();
let permissions = await Permission.findAll({ where: { userID: userID } });
permissions.forEach(async (permission) => {
await permission.destroy();
});
let user = await User.findOne({ where: { userID: userID } });
await user.destroy();
console.log(`User removed.`);
}
res.redirect('/users');
};
}

9
controllers/variables.js Normal file
View file

@ -0,0 +1,9 @@
export const Variables = (req, res) => {
res.render("variables", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
}

View file

@ -1,15 +1,26 @@
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { volumeList, GetContainerLists } from '../utils/docker.js';
import { docker } from '../server.js';
export const Volumes = async function(req, res) {
req.session.host = `${req.params.host || 1}`;
let container_volumes = [];
let volume_list = '';
// Table header
volume_list = `<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><label class="table-sort" data-sort="sort-type">Type</label></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-city">Mount point</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
// List all containers
let containers = await GetContainerLists();
let containers = await docker.listContainers({ all: true });
// Get the first 6 volumes from each container
for (let i = 0; i < containers.length; i++) {
@ -22,7 +33,7 @@ export const Volumes = async function(req, res) {
}
// List ALL volumes
let list = await volumeList();
let list = await docker.listVolumes({ all: true });
let volumes = list.Volumes;
// Create a table row for each volume
@ -46,50 +57,63 @@ export const Volumes = async function(req, res) {
<td class="sort-score text-green">${status}</td>
<td class="sort-date" data-date="1628122643">${volume.CreatedAt}</td>
<td class="sort-quantity">MB</td>
<td class=""><button class="badge badge-outline text-grey" id="" data-hx-get="/users/usersModals/user/" hx-target="#modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Details</button></td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
volume_list += row;
}
res.render("volumes",{
alert: '',
username: req.session.username,
volume_list += `</tbody>`
res.render("volumes", {
name: req.session.user,
role: req.session.role,
volume_count: '',
avatar: req.session.user.charAt(0).toUpperCase(),
volume_list: volume_list,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const submitVolumes = async function(req,res){
// console.log(req.body);
let trigger_name = req.header('hx-trigger-name');
let trigger_id = req.header('hx-trigger');
console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`);
res.render("volumes",{
volume_count: volumes.length,
alert: '',
username: req.session.username,
role: req.session.role,
volume_count: '',
volume_list: '',
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const addVolume = async function(req, res) {
let volume = req.body.volume;
export const searchVolumes = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}
docker.createVolume({
Name: volume
});
res.redirect("/volumes");
}
export const removeVolume = async function(req, res) {
let volumes = req.body.select;
if (typeof(volumes) == 'string') {
volumes = [volumes];
}
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] != 'on') {
try {
console.log(`Removing volume: ${volumes[i]}`);
let volume = docker.getVolume(volumes[i]);
await volume.remove();
} catch (error) {
console.log(`Unable to remove volume: ${volumes[i]}`);
}
}
}
res.redirect("/volumes");
}
// docker.df(volume.Name).then((data) => {
// for (let key in data) {
// console.log(data[key]);
// }
// });

256
database/models.js Normal file
View file

@ -0,0 +1,256 @@
import { Sequelize, DataTypes } from 'sequelize';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false,
});
export const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
username: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false
},
password: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
},
avatar: {
type: DataTypes.STRING
},
lastLogin: {
type: DataTypes.STRING
},
UUID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
}
});
export const Container = sequelize.define('Container', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
},
service: {
type: DataTypes.STRING
},
state: {
type: DataTypes.STRING
},
image: {
type: DataTypes.STRING
},
external_port: {
type: DataTypes.STRING
},
internal_port: {
type: DataTypes.STRING
},
ports: {
type: DataTypes.STRING
},
volumes: {
type: DataTypes.STRING
},
environment_variables: {
type: DataTypes.STRING
},
labels: {
type: DataTypes.STRING
},
IPv4: {
type: DataTypes.STRING
},
style: {
type: DataTypes.STRING
},
cpu: {
// store the last 15 values from dockerContainerStats
type: DataTypes.STRING
},
ram: {
// store the last 15 values from dockerContainerStats
type: DataTypes.STRING
},
});
export const Permission = sequelize.define('Permission', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
containerName: {
type: DataTypes.STRING,
},
containerID: {
type: DataTypes.STRING,
},
user: {
type: DataTypes.STRING,
allowNull: false
},
userID: {
type: DataTypes.STRING,
allowNull: false
},
install: {
type: DataTypes.STRING,
defaultValue: false
},
uninstall: {
type: DataTypes.STRING,
defaultValue: false
},
edit: {
type: DataTypes.STRING,
defaultValue: false
},
upgrade: {
type: DataTypes.STRING,
defaultValue: false
},
start: {
type: DataTypes.STRING,
defaultValue: false
},
stop: {
type: DataTypes.STRING,
defaultValue: false
},
restart: {
type: DataTypes.STRING,
defaultValue: false
},
pause: {
type: DataTypes.STRING,
defaultValue: false
},
logs: {
type: DataTypes.STRING,
defaultValue: false
},
hide: {
type: DataTypes.STRING,
defaultValue: false
},
reset_view: {
type: DataTypes.STRING,
defaultValue: false
},
view: {
type: DataTypes.STRING,
defaultValue: false
},
});
export const Syslog = sequelize.define('Syslog', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
user: {
type: DataTypes.STRING
},
email: {
type: DataTypes.STRING
},
event: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.STRING,
allowNull: false
},
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,
},
read: {
type: DataTypes.STRING,
},
createdAt : {
type: DataTypes.STRING
},
createdBy : {
type: DataTypes.STRING
},
});
export const Settings = sequelize.define('Settings', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
key: {
type: DataTypes.STRING,
allowNull: false
},
value: {
type: DataTypes.STRING,
allowNull: false
}
});
export const Variables = sequelize.define('Variables', {
find: {
type: DataTypes.STRING,
},
replace: {
type: DataTypes.STRING,
}
});

View file

@ -1,382 +0,0 @@
import session from 'express-session';
import SessionSequelize from 'connect-session-sequelize';
import { Sequelize, DataTypes} from 'sequelize';
import { readFileSync } from 'fs';
import { check_configured_hosts } from '../utils/docker.js';
const SECURE = process.env.HTTPS || false;
// Session store
const SequelizeStore = SessionSequelize(session.Store);
const sessionData = new Sequelize('database', 'username', 'password', {
dialect: 'sqlite',
storage: 'data/sessions.sqlite',
logging: false,
});
const SessionStore = new SequelizeStore({ db: sessionData });
export const sessionMiddleware = session({
secret: 'not keyboard cat',
store: SessionStore,
resave: false,
saveUninitialized: false,
cookie: {
secure: SECURE,
httpOnly: SECURE,
maxAge: 3600000 * 8,
},
});
// Server settings
const settings = new Sequelize('database', 'username', 'password', {
dialect: 'sqlite',
storage: 'data/settings.sqlite',
logging: false,
});
const SettingsDB = new SequelizeStore({ db: settings });
// Display package information
let package_info = readFileSync(`package.json`, 'utf8');
package_info = JSON.parse(package_info);
console.log('\n');
console.log(`\x1b[33mDweebUI v${package_info.version}\x1b[0m`);
console.log(`\x1b[33mAuthor: ${package_info.author}\x1b[0m`);
console.log(`\x1b[33mLicense: ${package_info.license}\x1b[0m`);
console.log(`\x1b[33mDescription: ${package_info.description}\x1b[0m`);
console.log('');
// console.log in red
console.log('\x1b[31m * Only Docker volumes are supported. No bind mounts.\n \x1b[0m');
console.log('\x1b[31m * Breaking changes may require you to remove the DweebUI volume and start fresh. \n \x1b[0m');
// Test database connection
try {
await sessionData.authenticate();
await settings.authenticate();
sessionData.sync();
settings.sync().then(() => {
check_configured_hosts();
});
console.log(`\x1b[32mDatabase connection established.\x1b[0m`);
} catch (error) {
console.error('\x1b[31mDatabase connection failed:', error, '\x1b[0m');
}
// Models
export const User = settings.define('User', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
userID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
username: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false
},
password: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.STRING
},
role: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
},
avatar: {
type: DataTypes.STRING
},
lastLogin: {
type: DataTypes.STRING
},
language: {
type: DataTypes.STRING,
defaultValue: 'english'
},
preferences : {
type: DataTypes.STRING
},
});
export const Permission = settings.define('Permission', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
containerName: {
type: DataTypes.STRING,
},
containerID: {
type: DataTypes.STRING,
},
username: {
type: DataTypes.STRING,
allowNull: false
},
userID: {
type: DataTypes.STRING,
allowNull: false
},
install: {
type: DataTypes.STRING,
defaultValue: false
},
uninstall: {
type: DataTypes.STRING,
defaultValue: false
},
edit: {
type: DataTypes.STRING,
defaultValue: false
},
upgrade: {
type: DataTypes.STRING,
defaultValue: false
},
start: {
type: DataTypes.STRING,
defaultValue: false
},
stop: {
type: DataTypes.STRING,
defaultValue: false
},
restart: {
type: DataTypes.STRING,
defaultValue: false
},
pause: {
type: DataTypes.STRING,
defaultValue: false
},
logs: {
type: DataTypes.STRING,
defaultValue: false
},
hide: {
type: DataTypes.STRING,
defaultValue: false
},
view: {
type: DataTypes.STRING,
defaultValue: false
},
options: {
type: DataTypes.STRING,
defaultValue: false
},
});
export const Syslog = settings.define('Syslog', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
username: {
type: DataTypes.STRING
},
uniqueID: {
type: DataTypes.STRING
},
event: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.STRING,
allowNull: false
},
ip : {
type: DataTypes.STRING
},
options : {
type: DataTypes.STRING
},
});
export const Notification = settings.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,
},
read: {
type: DataTypes.STRING,
},
createdAt : {
type: DataTypes.STRING
},
createdBy : {
type: DataTypes.STRING
},
options : {
type: DataTypes.STRING
},
});
export const ServerSettings = settings.define('ServerSettings', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
key: {
type: DataTypes.STRING,
allowNull: false
},
value: {
type: DataTypes.STRING,
allowNull: true
}
});
export const Container = settings.define('Container', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
containerName: {
type: DataTypes.STRING,
allowNull: false
},
containerID: {
type: DataTypes.STRING,
allowNull: false
},
service: {
type: DataTypes.STRING
},
state: {
type: DataTypes.STRING
},
image: {
type: DataTypes.STRING
},
external_port: {
type: DataTypes.STRING
},
internal_port: {
type: DataTypes.STRING
},
ports: {
type: DataTypes.STRING
},
volumes: {
type: DataTypes.STRING
},
environment_variables: {
type: DataTypes.STRING
},
labels: {
type: DataTypes.STRING
},
IPv4: {
type: DataTypes.STRING
},
style: {
type: DataTypes.STRING
},
cpu: {
type: DataTypes.STRING
},
ram: {
type: DataTypes.STRING
},
link: {
type: DataTypes.STRING
},
update: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
},
options: {
type: DataTypes.STRING
},
host: {
type: DataTypes.STRING
},
});
export const Variables = settings.define('Variables', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
key: {
type: DataTypes.STRING
},
value: {
type: DataTypes.STRING
},
options: {
type: DataTypes.STRING
}
});
export const ContainerLists = settings.define('ContainerLists', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
username: {
type: DataTypes.STRING,
allowNull: false
},
userID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
allowNull: false
},
containers: {
type: DataTypes.STRING,
},
hidden: {
type: DataTypes.STRING,
},
visable: {
type: DataTypes.STRING,
},
new: {
type: DataTypes.STRING,
},
updates: {
type: DataTypes.STRING,
},
sent: {
type: DataTypes.STRING,
},
});

View file

@ -1,43 +0,0 @@
{
"Dashboard": "仪表盘",
"Images": "镜像",
"Volumes": "存储卷",
"Networks": "网络",
"Apps": "应用商店",
"Users": "用户",
"Syslogs": "系统日志",
"Search": "搜索",
"Account": "账户",
"Notifications": "通知",
"Preferences": "偏好设置",
"Settings": "设置",
"Logout": "登出",
"Sponsors": "赞助商",
"Credits": "积分",
"admin": "管理员",
"user": "",
"Start": "",
"Stop": "",
"Pause": "",
"Restart": "",
"Starting": "",
"Stopping": "",
"Pausing": "",
"Restarting": "",
"Running": "",
"Stopped": "",
"Paused": "",
"Details": "",
"Logs": "",
"Edit": "",
"Update": "",
"Uninstall": "",
"Hide": "",
"Reset_View": "",
"Permissions": "",
"Copyright": "",
"Documentation": "文档",
"License": "",
"Source_Code": "",
"Support": ""
}

366
package-lock.json generated
View file

@ -1,27 +1,27 @@
{
"name": "dweebui",
"version": "0.70.474",
"version": "0.60",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dweebui",
"version": "0.70.474",
"version": "0.60",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.16",
"adm-zip": "^0.5.12",
"bcrypt": "^5.1.1",
"connect-session-sequelize": "^7.1.7",
"dockerode": "^4.0.2",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.10",
"express": "^4.21.1",
"express-session": "^1.18.1",
"express": "^4.19.2",
"express-session": "^1.18.0",
"memorystore": "^1.6.7",
"multer": "^1.4.5-lts.1",
"sequelize": "^6.37.5",
"sequelize": "^6.37.3",
"sqlite3": "^5.1.7",
"systeminformation": "^5.23.5",
"yaml": "^2.6.0"
"systeminformation": "^5.22.9",
"yaml": "^2.4.2"
}
},
"node_modules/@balena/dockerignore": {
@ -48,7 +48,7 @@
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
"tar": "^6.2.1"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
@ -113,17 +113,17 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "22.5.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
"integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dependencies": {
"undici-types": "~6.19.2"
"undici-types": "~5.26.4"
}
},
"node_modules/@types/validator": {
"version": "13.12.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA=="
"version": "13.11.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz",
"integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg=="
},
"node_modules/abbrev": {
"version": "1.1.1",
@ -143,11 +143,11 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"version": "0.5.12",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
"integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
"engines": {
"node": ">=12.0"
"node": ">=6.0"
}
},
"node_modules/agent-base": {
@ -250,9 +250,9 @@
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
@ -318,9 +318,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@ -330,7 +330,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@ -447,6 +447,18 @@
"node": ">= 10"
}
},
"node_modules/cacache/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cacache/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@ -459,6 +471,12 @@
"node": ">=10"
}
},
"node_modules/cacache/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"optional": true
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@ -579,20 +597,6 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/connect-session-sequelize": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-7.1.7.tgz",
"integrity": "sha512-Wqq7rg0w+9bOVs6jC0nhZnssXJ3+iKNlDVWn2JfBuBPoY7oYaxzxfBKeUYrX6dHt3OWEWbZV6LJvapwi76iBQQ==",
"dependencies": {
"debug": "^4.1.1"
},
"engines": {
"node": ">= 10"
},
"peerDependencies": {
"sequelize": ">= 6.1.0"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -618,9 +622,9 @@
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
@ -650,11 +654,11 @@
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "^2.1.3"
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
@ -816,9 +820,9 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
@ -908,36 +912,36 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"send": "0.18.0",
"serve-static": "1.15.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@ -949,11 +953,11 @@
}
},
"node_modules/express-session": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
"dependencies": {
"cookie": "0.7.2",
"cookie": "0.6.0",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
@ -966,14 +970,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@ -1038,12 +1034,12 @@
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@ -1159,7 +1155,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -1371,7 +1366,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -1434,9 +1428,9 @@
"optional": true
},
"node_modules/jake": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
"integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
@ -1473,15 +1467,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"optional": true,
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"node_modules/make-dir": {
@ -1533,6 +1524,24 @@
"node": ">= 10"
}
},
"node_modules/make-fetch-happen/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/make-fetch-happen/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"optional": true
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -1541,14 +1550,23 @@
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node_modules/memorystore": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz",
"integrity": "sha512-OZnmNY/NDrKohPQ+hxp0muBcBKrzKNtHr55DbqSx9hLsYVNnomSAMRAtI7R64t3gf3ID7tHQA7mG4oL3Hu9hdw==",
"dependencies": {
"debug": "^4.3.0",
"lru-cache": "^4.0.3"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@ -1693,6 +1711,11 @@
"node": ">=8"
}
},
"node_modules/minipass/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
@ -1705,6 +1728,11 @@
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@ -1741,9 +1769,9 @@
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/multer": {
"version": "1.4.5-lts.1",
@ -1763,9 +1791,9 @@
}
},
"node_modules/nan": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
"integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
"optional": true
},
"node_modules/napi-build-utils": {
@ -1782,9 +1810,9 @@
}
},
"node_modules/node-abi": {
"version": "3.67.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz",
"integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==",
"version": "3.62.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz",
"integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==",
"dependencies": {
"semver": "^7.3.5"
},
@ -1925,12 +1953,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": {
"node": ">= 0.4"
},
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -1994,9 +2019,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/pg-connection-string": {
"version": "2.6.4",
@ -2064,21 +2089,26 @@
"node": ">= 0.10"
}
},
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.6"
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
@ -2162,7 +2192,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
@ -2198,9 +2227,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"bin": {
"semver": "bin/semver.js"
},
@ -2209,9 +2238,9 @@
}
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@ -2244,18 +2273,15 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/sequelize": {
"version": "6.37.5",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.5.tgz",
"integrity": "sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==",
"version": "6.37.3",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz",
"integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==",
"funding": [
{
"type": "opencollective",
@ -2322,14 +2348,14 @@
}
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
"send": "0.18.0"
},
"engines": {
"node": ">= 0.8.0"
@ -2499,14 +2525,17 @@
}
},
"node_modules/sqlite3/node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz",
"integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==",
"engines": {
"node": "^16 || ^18 || >= 20"
}
},
"node_modules/ssh2": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
"integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
@ -2516,8 +2545,8 @@
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.10",
"nan": "^2.20.0"
"cpu-features": "~0.0.9",
"nan": "^2.18.0"
}
},
"node_modules/ssri": {
@ -2600,9 +2629,9 @@
}
},
"node_modules/systeminformation": {
"version": "5.23.5",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.5.tgz",
"integrity": "sha512-PEpJwhRYxZgBCAlWZhWIgfMTjXLqfcaZ1pJsJn9snWNfBW/Z1YQg1mbIUSWrEV3ErAHF7l/OoVLQeaZDlPzkpA==",
"version": "5.22.9",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.9.tgz",
"integrity": "sha512-qUWJhQ9JSBhdjzNUQywpvc0icxUAjMY3sZqUoS0GOtaJV9Ijq8s9zEP8Gaqmymn1dOefcICyPXK1L3kgKxlUpg==",
"os": [
"darwin",
"linux",
@ -2690,6 +2719,11 @@
"node": ">=10"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -2753,9 +2787,9 @@
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unique-filename": {
"version": "1.1.1",
@ -2879,14 +2913,14 @@
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
},
"node_modules/yaml": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"bin": {
"yaml": "bin.mjs"
},

View file

@ -1,6 +1,7 @@
{
"name": "dweebui",
"version": "0.70.474",
"version": "0.60",
"description": "Free and Open-Source WebUI For Managing Your Containers.",
"main": "server.js",
"type": "module",
"scripts": {
@ -10,20 +11,19 @@
"keywords": [],
"author": "lllllllillllllillll",
"license": "MIT",
"description": "DweebUI is a WebUI for managing your containers. https://dweebui.com",
"dependencies": {
"adm-zip": "^0.5.16",
"adm-zip": "^0.5.12",
"bcrypt": "^5.1.1",
"connect-session-sequelize": "^7.1.7",
"dockerode": "^4.0.2",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.10",
"express": "^4.21.1",
"express-session": "^1.18.1",
"express": "^4.19.2",
"express-session": "^1.18.0",
"memorystore": "^1.6.7",
"multer": "^1.4.5-lts.1",
"sequelize": "^6.37.5",
"sequelize": "^6.37.3",
"sqlite3": "^5.1.7",
"systeminformation": "^5.23.5",
"yaml": "^2.6.0"
"systeminformation": "^5.22.9",
"yaml": "^2.4.2"
}
}

View file

@ -1,276 +0,0 @@
/*!
* Tabler v1.0.0-beta20 (https://tabler.io)
* @version 1.0.0-beta20
* @link https://tabler.io
* Copyright 2018-2023 The Tabler Authors
* Copyright 2018-2023 codecalm.net Paweł Kuna
* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)
*/
/* prettier-ignore */
/* prettier-ignore */
pre.highlight,
.highlight pre {
max-height: 30rem;
margin: 1.5rem 0;
overflow: auto;
border-radius: var(--tblr-border-radius);
}
pre.highlight,
.highlight pre {
scrollbar-color: rgba(var(--tblr-scrollbar-color, var(--tblr-body-color-rgb)), 0.16) transparent;
}
pre.highlight::-webkit-scrollbar,
.highlight pre::-webkit-scrollbar {
width: 1rem;
height: 1rem;
-webkit-transition: background 0.3s;
transition: background 0.3s;
}
@media (prefers-reduced-motion: reduce) {
pre.highlight::-webkit-scrollbar,
.highlight pre::-webkit-scrollbar {
-webkit-transition: none;
transition: none;
}
}
pre.highlight::-webkit-scrollbar-thumb,
.highlight pre::-webkit-scrollbar-thumb {
border-radius: 1rem;
border: 5px solid transparent;
box-shadow: inset 0 0 0 1rem rgba(var(--tblr-scrollbar-color, var(--tblr-body-color-rgb)), 0.16);
}
pre.highlight::-webkit-scrollbar-track,
.highlight pre::-webkit-scrollbar-track {
background: transparent;
}
pre.highlight:hover::-webkit-scrollbar-thumb,
.highlight pre:hover::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 0 1rem rgba(var(--tblr-scrollbar-color, var(--tblr-body-color-rgb)), 0.32);
}
pre.highlight::-webkit-scrollbar-corner,
.highlight pre::-webkit-scrollbar-corner {
background: transparent;
}
.highlight {
margin: 0;
}
.highlight code > * {
margin: 0 !important;
padding: 0 !important;
}
.highlight .c, .highlight .c1 {
color: #a0aec0;
}
.highlight .nt, .highlight .nc, .highlight .nx {
color: #ff8383;
}
.highlight .na, .highlight .p {
color: #ffe484;
}
.highlight .s, .highlight .dl, .highlight .s2 {
color: #b5f4a5;
}
.highlight .k {
color: #93ddfd;
}
.highlight .s1, .highlight .mi {
color: #d9a9ff;
}
.example {
padding: 2rem;
margin: 1rem 0 2rem;
border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
border-radius: 3px 3px 0 0;
position: relative;
min-height: 12rem;
display: flex;
align-items: center;
overflow-x: auto;
}
.example-centered {
justify-content: center;
}
.example-centered .example-content {
flex: 0 auto;
}
.example-content {
font-size: 0.875rem;
line-height: 1.4285714286;
color: var(--tblr-body-color);
flex: 1;
max-width: 100%;
}
.example-content .page-header {
margin-bottom: 0;
}
.example-bg {
background: #f6f8fb;
}
.example-code {
margin: 2rem 0;
border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
border-top: none;
}
.example-code pre {
margin: 0;
border: 0;
border-radius: 0 0 3px 3px;
}
.example + .example-code {
margin-top: -2rem;
}
.example-column {
margin: 0 auto;
}
.example-column > .card:last-of-type {
margin-bottom: 0;
}
.example-column-1 {
max-width: 26rem;
}
.example-column-2 {
max-width: 52rem;
}
.example-modal-backdrop {
background: #182433;
opacity: 0.24;
position: absolute;
width: 100%;
left: 0;
top: 0;
height: 100%;
border-radius: 2px 2px 0 0;
}
.card-sponsor {
background: var(--tblr-primary-lt) no-repeat center/100% 100%;
border-color: var(--tblr-primary);
min-height: 316px;
}
.dropdown-menu-demo {
display: inline-block;
width: 100%;
position: relative;
top: 0;
margin-bottom: 1rem !important;
}
.demo-icon-preview {
position: -webkit-sticky;
position: sticky;
top: 0;
}
.demo-icon-preview svg,
.demo-icon-preview i {
width: 15rem;
height: 15rem;
font-size: 15rem;
stroke-width: 1.5;
margin: 0 auto;
display: block;
}
@media (max-width: 575.98px) {
.demo-icon-preview svg,
.demo-icon-preview i {
width: 10rem;
height: 10rem;
font-size: 10rem;
}
}
.demo-icon-preview-icon pre {
margin: 0;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
}
.demo-dividers > p {
opacity: 0.2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.demo-icons-list {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 0 -2px -1px 0;
list-style: none;
}
.demo-icons-list > * {
flex: 1 0 4rem;
}
.demo-icons-list-wrap {
overflow: hidden;
}
.demo-icons-list-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
aspect-ratio: 1;
text-align: center;
padding: 0.5rem;
border-right: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
border-bottom: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
color: inherit;
cursor: pointer;
}
.demo-icons-list-item .icon {
width: 1.5rem;
height: 1.5rem;
font-size: 1.5rem;
}
.demo-icons-list-item:hover {
text-decoration: none;
}
.settings-btn {
position: fixed;
right: -1px;
top: 10rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: rgba(var(--tblr-body-color-rgb), 0.04) 0 2px 4px 0;
}
.settings-scheme {
display: inline-block;
border-radius: 50%;
height: 3rem;
width: 3rem;
position: relative;
border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
box-shadow: rgba(var(--tblr-body-color-rgb), 0.04) 0 2px 4px 0;
}
.settings-scheme-light {
background: linear-gradient(135deg, #ffffff 50%, #fcfdfe 50%);
}
.settings-scheme-mixed {
background-image: linear-gradient(135deg, #182433 50%, #fff 50%);
}
.settings-scheme-transparent {
background: #fcfdfe;
}
.settings-scheme-dark {
background: #182433;
}
.settings-scheme-colored {
background-image: linear-gradient(135deg, var(--tblr-primary) 50%, #fcfdfe 50%);
}

View file

@ -1,6 +1,6 @@
/*!
* Tabler v1.0.0-beta20 (https://tabler.io)
* @version 1.0.0-beta20
* Tabler v1.0.0-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* @link https://tabler.io
* Copyright 2018-2023 The Tabler Authors
* Copyright 2018-2023 codecalm.net Paweł Kuna

View file

@ -1,192 +0,0 @@
@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";
}
.meter {
box-sizing: content-box;
height: 15px;
margin-left: auto;
margin-right: auto;
position: relative;
background: #a7a7a752;
border-radius: 25px;
padding: 3px;
box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3);
}
.meter > span {
display: block;
height: 100%;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
background-color: rgb(43, 194, 83);
background-image: linear-gradient(
center bottom,
rgb(43, 194, 83) 37%,
rgb(84, 240, 84) 69%
);
box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3),
inset 0 -2px 6px rgba(0, 0, 0, 0.4);
position: relative;
overflow: hidden;
}
.meter > span:after,
.animate > span > span {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-image: linear-gradient(
-45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
z-index: 1;
background-size: 50px 50px;
animation: move 2s linear infinite;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
overflow: hidden;
}
.animate > span:after {
display: none;
}
@keyframes move {
0% {
background-position: 0 0;
}
100% {
background-position: 50px 50px;
}
}
.orange > span {
background-image: linear-gradient(#f1a165, #f36d0a);
}
.red > span {
background-image: linear-gradient(#f0a3a3, #f42323);
}
.blue > span {
background-image: linear-gradient(#2478f5, #22017e);
}
.purple > span {
background-image: linear-gradient(#bd14d3, #670370);
}
.nostripes > span > span,
.nostripes > span::after {
background-image: none;
}
.container-stamp {
--tblr-stamp-size: 8rem;
position: absolute;
bottom: 0;
left: 0;
width: calc(var(--tblr-stamp-size) * 1);
height: calc(var(--tblr-stamp-size) * 1);
max-height: 100%;
border-top-right-radius: 4px;
opacity: 0.2;
overflow: hidden;
pointer-events: none;
}
.container-action {
padding: 0;
border: 0;
color: var(--tblr-secondary);
display: inline-flex;
width: 1.5rem;
height: 1.5rem;
align-items: center;
justify-content: center;
border-radius: var(--tblr-border-radius);
background: transparent;
}
.container-action:after {
content: none;
}
.container-action:focus {
outline: none;
box-shadow: none;
}
.container-action:hover, .container-action.show {
color: var(--tblr-body-color);
background: var(--tblr-active-bg);
}
.container-action.show {
color: var(--tblr-primary);
}
.container-action .icon {
margin: 0;
width: 1.25rem;
height: 1.25rem;
font-size: 1.25rem;
stroke-width: 1;
}
.container-actions {
display: flex;
}
.modal-content {
border: 1px solid grey;
}
.accordion-user {
border: 1px solid grey;
}
.slim-modal {
--tblr-modal-width: 450px;
}
.medium-modal {
--tblr-modal-width: 850px;
}
.wide-modal {
--tblr-modal-width: 1450px;
}
.avatar-3xl {
--tblr-avatar-size: 9rem;
--tblr-avatar-status-size: 1rem;
--tblr-avatar-font-size: 3rem;
--tblr-avatar-icon-size: 5rem;
}
.description {
max-height: 6em; /* Adjust based on font size and line height */
overflow: hidden;
text-overflow: ellipsis " [..]";
line-height: 1.5em; /* Adjust based on font size */
}

124
public/css/meters.css Normal file
View file

@ -0,0 +1,124 @@
.meter {
box-sizing: content-box;
height: 15px;
margin-left: auto;
margin-right: auto;
position: relative;
background: #a7a7a752;
border-radius: 25px;
padding: 3px;
box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3);
}
.meter > span {
display: block;
height: 100%;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
background-color: rgb(43, 194, 83);
background-image: linear-gradient(
center bottom,
rgb(43, 194, 83) 37%,
rgb(84, 240, 84) 69%
);
box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3),
inset 0 -2px 6px rgba(0, 0, 0, 0.4);
position: relative;
overflow: hidden;
}
.meter > span:after,
.animate > span > span {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-image: linear-gradient(
-45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
z-index: 1;
background-size: 50px 50px;
animation: move 2s linear infinite;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
overflow: hidden;
}
.animate > span:after {
display: none;
}
@keyframes move {
0% {
background-position: 0 0;
}
100% {
background-position: 50px 50px;
}
}
.orange > span {
background-image: linear-gradient(#f1a165, #f36d0a);
}
.red > span {
background-image: linear-gradient(#f0a3a3, #f42323);
}
.blue > span {
background-image: linear-gradient(#2478f5, #22017e);
}
.purple > span {
background-image: linear-gradient(#bd14d3, #670370);
}
.nostripes > span > span,
.nostripes > span::after {
background-image: none;
}
.border {
--tblr-card-spacer-y: 1rem;
--tblr-card-spacer-x: 1.5rem;
--tblr-card-title-spacer-y: 1.25rem;
--tblr-card-border-width: var(--tblr-border-width);
--tblr-card-border-color: var(--tblr-border-color);
--tblr-card-border-radius: var(--tblr-border-radius);
--tblr-card-box-shadow: var(--tblr-shadow-card);
--tblr-card-inner-border-radius: calc(var(--tblr-border-radius) - (var(--tblr-border-width)));
--tblr-card-cap-padding-y: 1rem;
--tblr-card-cap-padding-x: 1.5rem;
--tblr-card-cap-bg: var(--tblr-bg-surface-tertiary);
--tblr-card-cap-color: inherit;
--tblr-card-color: inherit;
--tblr-card-bg: var(--tblr-bg-surface);
--tblr-card-img-overlay-padding: 1rem;
--tblr-card-group-margin: 1.5rem;
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
height: var(--tblr-card-height);
word-wrap: break-word;
background-color: var(--tblr-card-bg);
background-clip: border-box;
border: var(--tblr-card-border-width) solid var(--tblr-card-border-color);
border-radius: var(--tblr-card-border-radius);
}

File diff suppressed because it is too large Load diff

28590
public/css/tabler.min.css vendored

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View file

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

35
public/js/demo-theme.js Normal file
View file

@ -0,0 +1,35 @@
/*!
* Tabler v1.0.0-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* @link https://tabler.io
* Copyright 2018-2023 The Tabler Authors
* Copyright 2018-2023 codecalm.net Paweł Kuna
* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)
*/
(function (factory) {
typeof define === 'function' && define.amd ? define(factory) :
factory();
})((function () { 'use strict';
var themeStorageKey = "tablerTheme";
var defaultTheme = "dark";
var selectedTheme;
var params = new Proxy(new URLSearchParams(window.location.search), {
get: function get(searchParams, prop) {
return searchParams.get(prop);
}
});
if (!!params.theme) {
localStorage.setItem(themeStorageKey, params.theme);
selectedTheme = params.theme;
} else {
var storedTheme = localStorage.getItem(themeStorageKey);
selectedTheme = storedTheme ? storedTheme : defaultTheme;
}
if (selectedTheme === 'dark') {
document.body.setAttribute("data-bs-theme", selectedTheme);
} else {
document.body.removeAttribute("data-bs-theme");
}
}));

View file

@ -1,6 +1,6 @@
/*!
* Tabler v1.0.0-beta20 (https://tabler.io)
* @version 1.0.0-beta20
* Tabler v1.0.0-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* @link https://tabler.io
* Copyright 2018-2023 The Tabler Authors
* Copyright 2018-2023 codecalm.net Paweł Kuna

View file

@ -1,6 +1,6 @@
/*!
* Tabler v1.0.0-beta20 (https://tabler.io)
* @version 1.0.0-beta20
* Tabler v1.0.0-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* @link https://tabler.io
* Copyright 2018-2023 The Tabler Authors
* Copyright 2018-2023 codecalm.net Paweł Kuna

View file

@ -1,53 +0,0 @@
var themeStorageKey = "tablerTheme";
var defaultTheme = "dark";
var selectedTheme;
(function () {
'use strict';
var storedTheme = localStorage.getItem(themeStorageKey);
selectedTheme = storedTheme ? storedTheme : defaultTheme;
if (selectedTheme === 'dark') {
document.body.setAttribute("data-bs-theme", selectedTheme);
} else {
document.body.removeAttribute("data-bs-theme");
}
})();
function toggleTheme(button) {
if (button.value == 'dark-theme') {
document.body.setAttribute("data-bs-theme", 'dark');
localStorage.setItem(themeStorageKey, 'dark');
}
else if (button.value == 'light-theme') {
document.body.removeAttribute("data-bs-theme");
localStorage.setItem(themeStorageKey, 'light');
}
}
function selectAll(group) {
let checkboxes = document.getElementsByName(group);
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
function topScroll() {
window.scrollTo(0, 0);
}
function bottomScroll() {
window.scrollTo(0, document.body.scrollHeight);
}

View file

@ -6,285 +6,350 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
htmx.defineExtension('sse', {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef
htmx.defineExtension("sse", {
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource
}
},
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
getSelectors: function() {
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
},
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
var parent = evt.target || evt.detail.elt
switch (name) {
case 'htmx:beforeCleanupElement':
var internalData = api.getInternalData(parent)
// Try to remove remove an EventSource when elements are removed
var source = internalData.sseEventSource
if (source) {
api.triggerEvent(parent, 'htmx:sseClose', {
source,
type: 'nodeReplaced',
})
internalData.sseEventSource.close()
}
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
return
switch (name) {
// Try to create EventSources when elements are processed
case 'htmx:afterProcessNode':
ensureEventSourceOnElement(parent)
}
}
})
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(evt.target)
// Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) {
internalData.sseEventSource.close();
}
/// ////////////////////////////////////////////
// HELPER FUNCTIONS
/// ////////////////////////////////////////////
return;
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, { withCredentials: true })
}
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(evt.target);
registerSSE(evt.target);
}
}
});
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
if (api.getAttributeValue(elt, 'sse-swap')) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource)
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement)
var source = internalData.sseEventSource
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
var sseEventNames = sseSwapAttr.split(',')
for (var i = 0; i < sseEventNames.length; i++) {
const sseEventName = sseEventNames[i].trim()
const listener = function(event) {
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
return
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(elt)) {
source.removeEventListener(sseEventName, listener)
return
}
// swap the response into the DOM and trigger a notification
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
return
}
swap(elt, event.data)
api.triggerEvent(elt, 'htmx:sseMessage', event)
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(sseEventName, listener)
}
}
// Add message handlers for every `hx-trigger="sse:*"` attribute
if (api.getAttributeValue(elt, 'hx-trigger')) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource)
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement)
var source = internalData.sseEventSource
var triggerSpecs = api.getTriggerSpecs(elt)
triggerSpecs.forEach(function(ts) {
if (ts.trigger.slice(0, 4) !== 'sse:') {
return
}
var listener = function (event) {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(elt)) {
source.removeEventListener(ts.trigger.slice(4), listener)
}
// Trigger events to be handled by the rest of htmx
htmx.trigger(elt, ts.trigger, event)
htmx.trigger(elt, 'htmx:sseMessage', event)
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(ts.trigger.slice(4), listener)
})
}
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null
}
// handle extension source creation attribute
if (api.getAttributeValue(elt, 'sse-connect')) {
var sseURL = api.getAttributeValue(elt, 'sse-connect')
if (sseURL == null) {
return
}
ensureEventSource(elt, sseURL, retryCount)
}
registerSSE(elt)
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url)
source.onerror = function(err) {
// Log an error event
api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source })
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
return
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0
retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
var timeout = retryCount * 500
window.setTimeout(function() {
ensureEventSourceOnElement(elt, retryCount)
}, timeout)
}
}
source.onopen = function(evt) {
api.triggerEvent(elt, 'htmx:sseOpen', { source })
if (retryCount && retryCount > 0) {
const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]")
for (let i = 0; i < childrenToFix.length; i++) {
registerSSE(childrenToFix[i])
}
// We want to increase the reconnection delay for consecutive failed attempts only
retryCount = 0
}
}
api.getInternalData(elt).sseEventSource = source
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
var closeAttribute = api.getAttributeValue(elt, "sse-close");
if (closeAttribute) {
// close eventsource when this message is received
source.addEventListener(closeAttribute, function() {
api.triggerEvent(elt, 'htmx:sseClose', {
source,
type: 'message',
})
source.close()
});
}
}
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, { withCredentials: true });
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource
if (source != undefined) {
api.triggerEvent(elt, 'htmx:sseClose', {
source,
type: 'nodeMissing',
})
source.close()
// source = null
return true
}
}
return false
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacySSEURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt)
})
function getLegacySSESwaps(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
var returnArr = [];
if (legacySSEValue != null) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "swap") {
returnArr.push(value[1]);
}
}
}
return returnArr;
}
var swapSpec = api.getSwapSpecification(elt)
var target = api.getTarget(elt)
api.swap(target, content, swapSpec)
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource);
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null; // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null
}
})()
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
if (sseSwapAttr) {
var sseEventNames = sseSwapAttr.split(",");
} else {
var sseEventNames = getLegacySSESwaps(child);
}
for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim();
var listener = function(event) {
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
return;
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
// swap the response into the DOM and trigger a notification
swap(child, event.data);
api.triggerEvent(elt, "htmx:sseMessage", event);
};
// Register the new listener
api.getInternalData(child).sseEventListener = listener;
source.addEventListener(sseEventName, listener);
}
});
// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
var sseEventName = api.getAttributeValue(child, "hx-trigger");
if (sseEventName == null) {
return;
}
// Only process hx-triggers for events with the "sse:" prefix
if (sseEventName.slice(0, 4) != "sse:") {
return;
}
// remove the sse: prefix from here on out
sseEventName = sseEventName.substr(4);
var listener = function() {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
}
});
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null;
}
// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
var sseURL = api.getAttributeValue(child, "sse-connect");
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
// handle legacy sse, remove for HTMX2
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
var sseURL = getLegacySSEURL(child);
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url);
source.onerror = function(err) {
// Log an error event
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
return;
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0;
var timeout = Math.random() * (2 ^ retryCount) * 500;
window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
}, timeout);
}
};
source.onopen = function(evt) {
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
}
api.getInternalData(elt).sseEventSource = source;
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource;
if (source != undefined) {
source.close();
// source = null
return true;
}
}
return false;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = [];
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName)) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
result.push(node);
});
return result;
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt);
});
var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt);
var settleInfo = api.makeSettleInfo(elt);
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:beforeSettle');
});
// Handle settle tasks (with delay if requested)
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
} else {
doSettle(settleInfo)();
}
}
/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function(task) {
task.call();
});
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:afterSettle');
});
}
}
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null;
}
})();

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

122
router.js
View file

@ -1,122 +0,0 @@
import express from 'express';
export const router = express.Router();
import { Login, submitLogin, Logout } from './controllers/login.js';
import { Register, submitRegister } from './controllers/register.js';
import { Dashboard, searchDashboard, ServerMetrics, SSE, DashboardView, DashboardAction } from './controllers/dashboard.js';
import { Images, submitImages, searchImages } from './controllers/images.js';
import { Volumes, submitVolumes, searchVolumes } from './controllers/volumes.js';
import { Networks, NetworkAction, searchNetworks } from './controllers/networks.js';
import { Apps, submitApps, searchApps, appsModals, } from './controllers/apps.js';
import { Users, submitUsers, searchUsers, UsersView, UsersAction } from './controllers/users.js';
import { Syslogs, searchSyslogs } from './controllers/syslogs.js';
import { Account, searchAccount } from './controllers/account.js';
import { Preferences, submitPreferences, searchPreferences } from './controllers/preferences.js';
import { Settings, SettingsAction, updateLanguages, searchSettings } from './controllers/settings.js';
import { Sponsors, searchSponsors } from './controllers/sponsors.js';
import { Credits } from './controllers/credits.js';
import { Install } from './utils/install.js';
import { Uninstall } from './utils/uninstall.js';
import { sessionCheck, adminOnly, permissionCheck } from './utils/permissions.js';
// router.get('*', (req, res, next) => { console.log(`[GET] ${req.url}`); next(); });
// router.post('*', (req, res, next) => { console.log(`[POST] ${req.url}`); next(); });
router.get('/login', Login);
router.post('/login', submitLogin);
router.get('/logout', Logout);
router.get('/register', Register);
router.post('/register', submitRegister);
router.get("/", sessionCheck, Dashboard);
router.get("/dashboard", sessionCheck, Dashboard);
router.get("/dashboard/view/:view/:id?", sessionCheck, DashboardView);
router.post("/dashboard/action/:action/:id?", sessionCheck, DashboardAction);
router.get("/server_metrics", sessionCheck, ServerMetrics);
router.get("/sse", permissionCheck, SSE);
router.get("/images", adminOnly, Images);
router.post('/images', adminOnly, submitImages);
router.get("/volumes", adminOnly, Volumes);
router.post('/volumes', adminOnly, submitVolumes);
router.get("/networks", adminOnly, Networks);
router.post('/network/:action/:containerid?', adminOnly, NetworkAction);
router.get("/apps/:page?/:template?", adminOnly, Apps);
router.post("/apps/:action?", adminOnly, submitApps);
router.get("/users", adminOnly, Users);
router.get("/users/view/:view/:id?", adminOnly, UsersView);
router.post("/users/action/:action/:id?", adminOnly, UsersAction);
router.get('/syslogs', adminOnly, Syslogs);
router.get('/settings', adminOnly, Settings);
router.post('/settings/action/:action?/:id?', adminOnly, SettingsAction);
router.get('/preferences', sessionCheck, Preferences);
router.post('/preferences', sessionCheck, submitPreferences);
router.get('/account', sessionCheck, Account);
router.get('/sponsors', sessionCheck, Sponsors);
router.get('/credits', sessionCheck, Credits);
router.get("/appsModals/:modal?", adminOnly, appsModals);
router.post("/install", adminOnly, Install);
router.post("/uninstall", adminOnly, Uninstall);
router.post('/update_languages', adminOnly, updateLanguages);
router.post("/search", function (req, res) {
// req.header('hx-current-url') == http://localhost:8000/dashboard
let page = (req.header('hx-current-url')).split("/").pop();
switch(page) {
case "dashboard":
searchDashboard(req, res);
break;
case "images":
searchImages(req, res);
break;
case "volumes":
searchVolumes(req, res);
break;
case "networks":
searchNetworks(req, res);
break;
case "apps":
searchApps(req, res);
break;
case "users":
searchUsers(req, res);
break;
case "syslogs":
searchSyslogs(req, res);
break;
case "preferences":
searchPreferences(req, res);
break;
case "settings":
searchSettings(req, res);
break;
case "account":
searchAccount(req, res);
case "sponsors":
searchSponsors(req, res);
break;
default:
console.log(`[Search] ${req.body.search}`);
res.send('ok');
}
});

106
router/index.js Normal file
View file

@ -0,0 +1,106 @@
import express from "express";
import { Permission } from '../database/models.js';
export const router = express.Router();
// Controllers
import { Login, submitLogin, Logout } from "../controllers/login.js";
import { Register, submitRegister } from "../controllers/register.js";
import { Dashboard, DashboardAction, Stats, Chart, SSE, UpdatePermissions } from "../controllers/dashboard.js";
import { Apps, appSearch, InstallModal, ImportModal, LearnMore, Upload, removeTemplate } from "../controllers/apps.js";
import { Users } from "../controllers/users.js";
import { Images } from "../controllers/images.js";
import { Networks, removeNetwork } from "../controllers/networks.js";
import { Volumes, addVolume, removeVolume } from "../controllers/volumes.js";
import { Account } from "../controllers/account.js";
import { Variables } from "../controllers/variables.js";
import { Settings } from "../controllers/settings.js";
import { Supporters, Thanks } from "../controllers/supporters.js";
import { Syslogs } from "../controllers/syslogs.js";
import { Install } from "../utils/install.js"
import { Uninstall } from "../utils/uninstall.js"
// Permission Middleware
const adminOnly = async (req, res, next) => {
if (req.session.role == 'admin') { next(); }
else { res.redirect('/dashboard'); }
}
const sessionCheck = async (req, res, next) => {
if (req.session.user) { next(); }
else { res.redirect('/login'); }
}
const permissionCheck = async (req, res, next) => {
if (req.session.role == 'admin') { next(); return; }
let user = req.session.user;
let action = req.path.split("/")[2];
let trigger = req.header('hx-trigger-name');
const userAction = ['start', 'stop', 'restart', 'pause', 'uninstall', 'upgrade', 'edit', 'logs', 'view'];
const userPaths = ['card', 'updates', 'hide', 'reset', 'alert'];
if (userAction.includes(action)) {
let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
if (permission) {
if (permission[action] == true) {
console.log(`User ${user} has permission to ${action} ${trigger}`);
next();
return;
}
else {
console.log(`User ${user} does not have permission to ${action} ${trigger}`);
}
}
} else if (userPaths.includes(action)) {
next();
return;
}
}
// Utils
router.post("/install", adminOnly, Install);
router.post("/uninstall", adminOnly, Uninstall);
// Routes
router.get("/login", Login);
router.post("/login", submitLogin);
router.get("/logout", Logout);
router.get("/register", Register);
router.post("/register", submitRegister);
router.get("/", sessionCheck, Dashboard);
router.get("/dashboard", sessionCheck, Dashboard);
router.post("/dashboard/:action", sessionCheck, permissionCheck, DashboardAction);
router.get("/sse", sessionCheck, SSE);
router.post("/updatePermissions", adminOnly, UpdatePermissions);
router.get("/stats", sessionCheck, Stats);
router.get("/chart", sessionCheck, Chart);
router.get("/images", adminOnly, Images);
router.post("/images/:action", adminOnly, Images);
router.get("/volumes", adminOnly, Volumes);
router.post("/addVolume", adminOnly, addVolume);
router.post("/removeVolume", adminOnly, removeVolume);
router.get("/networks", adminOnly, Networks);
router.post("/removeNetwork", adminOnly, removeNetwork);
router.get("/apps/:page?/:template?", adminOnly, Apps);
router.post("/apps", adminOnly, appSearch);
router.get("/remove_template/:template", adminOnly, removeTemplate);
router.get("/install_modal", adminOnly, InstallModal)
router.get("/import_modal", adminOnly, ImportModal)
router.get("/learn_more", adminOnly, LearnMore)
router.post("/upload", adminOnly, Upload);
router.get("/users", adminOnly, Users);
router.get("/syslogs", adminOnly, Syslogs);
router.get("/variables", adminOnly, Variables);
router.get("/settings", adminOnly, Settings);
router.get("/account", sessionCheck, Account);
router.get("/supporters", sessionCheck, Supporters);
router.post("/thank", sessionCheck, Thanks);

View file

@ -1,22 +1,48 @@
import express from 'express';
import session from 'express-session';
import memorystore from 'memorystore';
import ejs from 'ejs';
import { router } from './router.js';
import { sessionMiddleware } from './db/config.js';
import Docker from 'dockerode';
import { router } from './router/index.js';
import { sequelize } from './database/models.js';
export const docker = new Docker();
// Session middleware
const MemoryStore = memorystore(session);
const sessionMiddleware = session({
store: new MemoryStore({ checkPeriod: 86400000 }), // Prune expired entries every 24h
secret: "keyboard cat",
resave: false,
saveUninitialized: false,
cookie:{
secure: false,
httpOnly: false,
maxAge: 3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
}
});
// Express middleware
const app = express();
const PORT = process.env.PORT || 8000;
app.set('view engine', 'html');
app.set('trust proxy', true);
app.engine('html', ejs.renderFile);
app.use([
express.static('public'),
express.urlencoded({ extended: true }),
sessionMiddleware,
router,
router
]);
// Initialize server
app.listen(PORT, async () => {
console.log(`\x1b[32mListening on http://localhost:${PORT}\x1b[0m`);
console.log('');
async function init() {// I made sure the console.logs and emojis lined up
try { await sequelize.authenticate().then(
() => { console.log('DB Connection: ✔️') }); }
catch { console.log('DB Connection: ❌'); }
try { await sequelize.sync().then(
() => { console.log('Synced Models: ✔️') }); }
catch { console.log('Synced Models: ❌'); } }
await init().then(() => {
console.log(`Listening on http://localhost:${PORT}`);
});
});

2
templates/compose/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -40,24 +40,6 @@
"label": "TZ",
"default": "America/Los_Angeles"
}
],
"labels": [
{
"name": "traefik.enable",
"value": "true"
},
{
"name": "traefik.http.services.heimdall.loadbalancer.server.port",
"value": "80"
},
{
"name": "traefik.http.routers.heimdall.entrypoints",
"value": "websecure"
},
{
"name": "traefik.http.routers.heimdall.tls.certresolver",
"value": "mydnschallenge"
}
]
},
{
@ -1952,7 +1934,7 @@
"title": "Fail2ban",
"name": "fail2ban",
"note": "",
"description": "Fail2Ban is an intrusion prevention software framework, designed to prevent brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as iptables or TCP Wrapper.",
"description": "Fail2ban is a daemon to ban hosts that cause multiple authentication errors.",
"platform": "linux",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/fail2ban.png",
"image": "linuxserver/fail2ban:latest",
@ -3141,7 +3123,7 @@
"Networking",
"Tools"
],
"description": "Guacamole is a clientless remote desktop gateway. It supports standard protocols like VNC and RDP. It is called clientless because no plugins or client software are required.",
"description": "A clientless remote desktop gateway.",
"image": "oznu/guacamole:latest",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/guacamole.png",
"name": "guacamole",
@ -5039,6 +5021,67 @@
],
"privileged": true
},
{
"name": "nocodb",
"title": "NocoDB",
"note": "",
"description": "NocoDB is a free, open-source, self-hosted, no-code platform to make database driven application. <a href='https://www.nocodb.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nocodb/nocodb' target='_blank'>Docker Hub</a>",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/nocodb.png",
"image": "nocodb/nocodb",
"categories": [
"Database"
],
"volumes": [
{
"bind": "/home/docker/nocodb",
"container": "/var/lib/nocodb",
"mode": "rw"
}
],
"ports" : [
{
"host": "8080",
"container": "8080"
}
],
"env": [
{
"name": "NOCODB_DB_HOST",
"label": "Database Host",
"description": "Database host",
"type": "text",
"default": "mysql"
},
{
"name": "NOCODB_DB_PORT",
"label": "Database Port",
"description": "Database port",
"type": "text",
"default": "3306"
},
{
"name": "NOCODB_DB_USER",
"label": "Database User",
"description": "Database user",
"type": "text",
"default": "root"
},
{
"name": "NOCODB_DB_PASSWORD",
"label": "Database Password",
"description": "Database password",
"type": "password",
"default": "password"
},
{
"name": "NOCODB_DB_NAME",
"label": "Database Name",
"description": "Database name",
"type": "text",
"default": "nocodb"
}
]
},
{
"categories": [
"Tools"
@ -5063,7 +5106,7 @@
],
"note": "DNS-over-HTTPS: [80:80/TCP] [443:443/TCP] [443:443/UDP] [3000:3000/TCP] [DEFAULT]. DNS: [53:53/TCP] [53:53/UDP]. Admin Panel: [3000:3000/TCP]. DHCP: [67:67/UDP] [68:68/TCP] [68:68/UDP]. DNS-over-TLS: [853:853/TCP]. DNS-over-QUIC: [784:784/UDP] [853:853/UDP] [8853:8853/UDP]. DNSCrypt: [5443:5443/TCP] [5443:5443/UDP]",
"image": "adguard/adguardhome:latest",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/Dashboard-Icons/main/png/adguard.png",
"logo": "https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Images/adguard.png",
"name": "adguard",
"platform": "linux",
"ports": [
@ -5314,7 +5357,7 @@
"CMS"
],
"platform": "linux",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/drupal.png",
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/drupal.png",
"image": "drupal:latest",
"ports": [
"80/tcp"
@ -5476,7 +5519,7 @@
{
"type": 1,
"name": "nvidia-hwa-test",
"title": "NVIDIA-HWA-Test",
"title": "NVIDIA HWA Test",
"note": "",
"description": "Nvidia HWA Test is a test container for NVIDIA hardware acceleration. Start the container then check the logs to confirm your output matches the example from this page: <a href='https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/sample-workload.html'>Running a Sample Workload</a>. <a href='https://www.nvidia.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nvidia/cuda' target='_blank'>Docker Hub</a>",
"platform": "linux",

2
templates/tmp/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -1,241 +0,0 @@
import Docker from 'dockerode';
import { dockerContainerStats } from 'systeminformation';
import { Container, ServerSettings } from '../db/config.js'
import stream from 'stream';
export var docker;
var docker2;
var docker3;
var docker4;
if (process.env.DOCKER_HOST && process.env.DOCKER_PORT) {
console.log('Connecting to Docker with environment variables.');
docker = new Docker({ host: process.env.DOCKER_HOST, port: process.env.DOCKER_PORT });
console.log('Docker host connected.');
} else {
console.log('Connecting to default Docker host.');
docker = new Docker();
console.log('Docker host connected.');
}
export async function GetContainerLists(hostid) {
let host = hostid || 1;
let containers;
if (host == 0) {
containers = await docker.listContainers({ all: true });
}
if ((host == 0) && docker2) {
let containers2 = await docker2.listContainers({ all: true });
containers = containers.concat(containers2);
}
if ((host == 0) && docker3) {
let containers3 = await docker3.listContainers({ all: true });
containers = containers.concat(containers3);
}
if ((host == 0) && docker4) {
let containers4 = await docker4.listContainers({ all: true });
containers = containers.concat(containers4);
}
if (host == 1) {
containers = await docker.listContainers({ all: true });
}
if (host == 2 && docker2) {
containers = await docker2.listContainers({ all: true });
}
if (host == 3 && docker3) {
containers = await docker3.listContainers({ all: true });
}
if (host == 4 && docker4) {
containers = await docker4.listContainers({ all: true });
}
return containers;
}
export async function configureHost(hostid, ip, port) {
if (hostid == 2) {
docker2 = new Docker({ host: ip, port: port });
try {
let containers = await docker2.listContainers({ all: true });
console.log(`Host 2 connected. ${containers.length} containers found.`);
}
catch {
console.log('Host 2 connection failed.');
docker2;
}
} else if (hostid == 3) {
docker3 = new Docker({ host: ip, port: port });
try {
let containers = await docker3.listContainers({ all: true });
console.log(`Host 3 connected. ${containers.length} containers found.`);
}
catch {
console.log('Host 3 connection failed.');
docker3;
}
} else if (hostid == 4) {
docker4 = new Docker({ host: ip, port: port });
try {
let containers = await docker4.listContainers({ all: true });
console.log(`Host 4 connected. ${containers.length} containers found.`);
}
catch {
console.log('Host 4 connection failed.');
docker4;
}
}
}
export async function imageList() {
let images = await docker.listImages({ all: true });
return images;
}
export async function volumeList() {
let volumes = await docker.listVolumes();
return volumes;
}
export async function networkList() {
let networks = await docker.listNetworks();
return networks;
}
export async function GetContainer(containerID) {
let container = docker.getContainer(containerID);
return container;
}
export async function containerInfo (containerID) {
// get the container info
let info = docker.getContainer(containerID);
let container = await info.inspect();
let container_name = container.Name.slice(1);
let container_image = container.Config.Image;
let container_service = container.Config.Labels['com.docker.compose.service'];
let ports_list = [];
let external = 0;
let internal = 0;
try {
for (const [key, value] of Object.entries(container.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
try { external = ports_list[0].external; internal = ports_list[0].internal; } catch { }
let container_info = {
containerName: container_name,
containerID: containerID,
containerImage: container_image,
containerService: container_service,
containerState: container.State.Status,
external_port: external,
internal_port: internal,
ports: ports_list,
volumes: container.Mounts,
env: container.Config.Env,
labels: container.Config.Labels,
link: '',
}
return container_info;
}
export async function containerLogs(containerID) {
let container = docker.getContainer(containerID);
const logs = await container.logs({ stdout: true, stderr: true, tail: 'all', });
const logsString = logs.toString('utf8');
return logsString;
}
let available_versions = '';
async function version_check () {
const resp = await fetch('https://registry.hub.docker.com/v2/namespaces/lllllllillllllillll/repositories/dweebui/tags/?page_size=10000');
let hub = await resp.json();
for (let i = 0; i < hub.results.length; i++) {
available_versions += '| ' + hub.results[i].name + ' ';
}
console.log('Available versions:');
console.log(available_versions);
}
version_check();
// Creates then destroys a docker volume to trigger a docker event.
export async function trigger_docker_event () {
let volume = await docker.createVolume({ Name: 'dweebui_test_volume' });
setTimeout(async() => {
await volume.remove();
}, 200);
}
export async function containerStats (containerID) {
const stats = await dockerContainerStats(containerID);
let info = {
containerID: containerID,
cpu: Math.round(stats[0].cpuPercent),
ram: Math.round(stats[0].memPercent)
}
return info;
}
export async function removeNetwork(networkID) {
let network = docker.getNetwork(networkID);
await network.remove();
console.log(`Network ${networkID} removed.`);
}
export async function check_configured_hosts () {
let [host2, created] = await ServerSettings.findOrCreate({ where: {key: 'host2'}, defaults: { key: 'host2', value: '' } });
if (host2.value != '') {
let [tag2, ip2, port2] = host2.value.split(',');
configureHost(2, ip2, port2);
console.log('Host 2 configured.');
}
let [host3, created3] = await ServerSettings.findOrCreate({ where: {key: 'host3'}, defaults: { key: 'host3', value: '' } });
if (host3.value != '') {
let [tag3, ip3, port3] = host3.value.split(',');
configureHost(3, ip3, port3);
console.log('Host 3 configured.');
}
let [host4, created4] = await ServerSettings.findOrCreate({ where: {key: 'host4'}, defaults: { key: 'host4', value: '' } });
if (host4.value != '') {
let [tag4, ip4, port4] = host4.value.split(',');
configureHost(4, ip4, port4);
console.log('Host 4 configured.');
}
}

View file

@ -1,239 +1,250 @@
import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "fs";
import { execSync } from "child_process";
import { Syslog } from "../db/config.js";
import { docker } from "./docker.js";
import DockerodeCompose from "dockerode-compose";
import yaml from 'js-yaml';
import { Alert } from "./system.js";
import { execSync } from "child_process";
import { docker } from "../server.js";
import DockerodeCompose from "dockerode-compose";
import { Syslog } from "../database/models.js";
import { addAlert } from "../controllers/dashboard.js";
// This entire page hurts to look at.
export const Install = async (req, res) => {
let data = req.body;
let { name, service_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 data = req.body;
let name = data.name;
let ports = [ port0, port1, port2, port3, port4, port5 ];
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5];
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11];
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11];
let docker_volumes = [];
// Make sure there isn't a container already running that has the same name
let containers = await docker.listContainers({ all: true });
for (let i = 0; i < containers.length; i++) {
if (containers[i].Names[0].includes(name)) {
console.log(`App '${name}' already exists. Please choose a different name.`);
let alert = Alert('danger', `App '${name}' already exists. Please choose a different name.`);
res.send(alert);
return;
}
}
// async function composeInstall (name, compose, req) {
// console.log('[composeInstall]');
// // try {
// // await compose.pull().then(() => {
// // compose.up();
// // Syslog.create({
// // user: req.session.user,
// // email: null,
// // event: "App Installation",
// // message: `${name} installed successfully`,
// // ip: req.socket.remoteAddress
// // });
// // });
// // } catch (err) {
// // await Syslog.create({
// // user: req.session.user,
// // email: null,
// // event: "App Installation",
// // message: `${name} installation failed: ${err}`,
// // ip: req.socket.remoteAddress
// // });
// // }
// await compose.pull();
// await compose.up();
// console.log('compose.up');
// }
// Compose file installation
if (req.body.compose) {
// Create the directory
mkdirSync(`./appdata/${name}`, { recursive: true });
// Write the form data to the compose file
writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) });
var compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`);
composeInstall(name, compose, req);
res.redirect('/');
return;
}
// Convert a JSON template into a compose file
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
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
break;
}
}
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
// Volumes
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] == 'on') {
compose_file += `\n volumes:`
break;
}
}
for (let i = 0; i < volumes.length; 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
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n environment:`
break;
}
}
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Labels
for (let i = 0; i < labels.length; i++) {
if (labels[i] == 'on') {
compose_file += `\n labels:`
break;
}
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Privileged mode
if (data.privileged == 'on') { compose_file += `\n privileged: true` }
// Hardware acceleration
for (let i = 0; i < env_vars.length; i++) {
if ((env_vars[i] == 'on') && (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]`
break;
}
}
// add volumes to the compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// Removed any duplicates from docker_volumes
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]}:`
let containers = await docker.listContainers({ all: true });
for (let i = 0; i < containers.length; i++) {
if (containers[i].Names[0].includes(name)) {
addAlert(req.session, 'danger', `App ${name} already exists. Please remove it first.`);
res.redirect('/');
return;
}
}
}
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/compose.yaml`, compose_file, function (err) { console.log(err) });
if (req.body.compose) {
console.log(`Installing ${name}. It should appear on the dashboard shortly.`);
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) });
let compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`);
addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
try {
(async () => {
await compose.pull();
await compose.up();
// composeInstall(name, compose, req);
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${app} installed successfully`,
ip: req.socket.remoteAddress
});
})();
} catch (err) {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${app} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
} else {
var compose = new DockerodeCompose(docker, `./appdata/${name}/compose.yaml`, `${name}`);
let { service_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;
(async () => {
console.log('Pulling image');
await compose.pull();
console.log('Starting container');
await compose.up();
})();
let ports = [port0, port1, port2, port3, port4, port5]
let alert = Alert('success', `Installing ${name}. It should appear on the dashboard shortly.`);
let docker_volumes = [];
res.send(alert);
addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
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
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
break;
}
}
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
// Volumes
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5]
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] == 'on') {
compose_file += `\n volumes:`
break;
}
}
for (let i = 0; i < volumes.length; 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
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n environment:`
break;
}
}
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Labels
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]
for (let i = 0; i < labels.length; i++) {
if (labels[i] == 'on') {
compose_file += `\n labels:`
break;
}
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Hardware acceleration
for (let i = 0; i < env_vars.length; i++) {
if ((env_vars[i] == 'on') && (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]`
break;
}
}
// 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) });
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
} catch {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed - error creating directory or compose file`,
ip: req.socket.remoteAddress
});
}
try {
(async () => {
await compose.pull();
await compose.up();
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
})();
} catch (err) {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
}
}
res.redirect('/');
}
// im just going to leave this old stackfile snippet here for now
// 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') }
// }
// }

View file

@ -1,60 +0,0 @@
import { Permission, User, Syslog } from "../db/config.js";
import { readFileSync } from 'fs';
import { Capitalize } from '../utils/system.js';
export const adminOnly = async (req, res, next) => {
let path = req.path;
// console.log(`\x1b[90m ${req.session.username} ${path} \x1b[0m`);
if (req.session.role == 'admin') { next(); return; }
console.log(`User ${req.session.username} does not have permission to access ${path}`);
res.redirect('/dashboard');
return;
}
export const sessionCheck = async (req, res, next) => {
if (req.session.userID) { next(); }
else { res.redirect('/login'); }
}
export const permissionCheck = async (req, res, next) => {
if (req.session.role == 'admin') { next(); return; }
let path = req.path;
let containerID = req.params.containerid;
let action = req.params.action;
let AltIDState = 'a' + containerID + 'State';
const userAction = ['start', 'stop', 'pause', 'restart', 'uninstall', 'upgrade', 'edit', 'logs', 'view'];
const userPaths = ['/card_list', '/update_card', 'hide', 'reset', 'alert', '/sse', `/update_card/${containerID}` ];
if (userAction.includes(action)) {
let permission = await Permission.findOne({ where: { containerID: containerID, userID: req.session.userID }, attributes: [`${action}`] });
if (permission) {
if (permission[action] == true) {
// console.log(`User ${req.session.username} has permission for ${path}`);
await Syslog.create({ username: req.session.username, uniqueID: req.session.userID, event: "User Action", message: `User ${req.session.username} has permission to ${action} ${containerID}`, ip: req.socket.remoteAddress });
next();
return;
}
else {
console.log(`User ${req.session.username} does NOT have permission for ${path}`);
await Syslog.create({ username: req.session.username, uniqueID: req.session.userID, event: "User Action", message: `User ${req.session.username} does not have permission to ${action} ${containerID}`, ip: req.socket.remoteAddress });
let denied =`<div class="text-yellow d-inline-flex align-items-center lh-1 ms-auto" id="${AltIDState}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
<strong>Denied</strong>
</div>`;
res.send(denied);
return;
}
}
} else if (userPaths.includes(path)) {
// console.log(`User ${req.session.username} has permission for ${path}`);
next();
return;
} else {
console.log(`User ${req.session.username} does NOT have permission for ${path}`);
}
}

View file

@ -1,174 +0,0 @@
import { User, ServerSettings } from '../db/config.js';
import { readFileSync } from 'fs';
// Navbar
export async function Navbar (req) {
let userID = req.session.userID;
let username = req.session.username;
let role = req.session.role;
let host = req.session.host;
let language = await getLanguage(userID);
// Check if the user wants to hide their profile name.
if (userID != '00000000-0000-0000-0000-000000000000') {
let user = await User.findOne({ where: { userID: userID }});
let preferences = JSON.parse(user.preferences);
if (preferences.hide_profile == true) { username = 'Anon'; }
}
let sponsored = await ServerSettings.findOne({ where: { key: 'sponsored' }});
if (sponsored) { username = `<label class="text-yellow">${username}</label>`; }
let [host0_active, host0_toggle, host0_tag, host0_ip, host0_port] = ['', '', '', '', ''];
let [host1_active, host1_toggle, host1_tag, host1_ip, host1_port] = ['', '', '', '', ''];
let [host2_active, host2_toggle, host2_tag, host2_ip, host2_port] = ['', '', '', '', ''];
let [host3_active, host3_toggle, host3_tag, host3_ip, host3_port] = ['', '', '', '', ''];
let [host4_active, host4_toggle, host4_tag, host4_ip, host4_port] = ['', '', '', '', ''];
const [host2, created2] = await ServerSettings.findOrCreate({ where: { key: 'host2' }, defaults: { key: 'host2', value: '' }});
const [host3, created3] = await ServerSettings.findOrCreate({ where: { key: 'host3' }, defaults: { key: 'host3', value: '' }});
const [host4, created4] = await ServerSettings.findOrCreate({ where: { key: 'host4' }, defaults: { key: 'host4', value: '' }});
if (host2.value) { host2_toggle = 'checked'; [host2_tag, host2_ip, host2_port] = host2.value.split(','); }
if (host3.value) { host3_toggle = 'checked'; [host3_tag, host3_ip, host3_port] = host3.value.split(','); }
if (host4.value) { host4_toggle = 'checked'; [host4_tag, host4_ip, host4_port] = host4.value.split(','); }
let host_buttons = '<form action="/dashboard/action/switch_host/hostid" method="post">';
let nav_link = '';
if (host == '0') { host0_active = 'text-yellow'; nav_link = '/0'; }
if (host == '1') { host1_active = 'text-yellow'; }
if (host == '2') { host2_active = 'text-yellow'; nav_link = '/2'; }
if (host == '3') { host3_active = 'text-yellow'; nav_link = '/3'; }
if (host == '4') { host4_active = 'text-yellow'; nav_link = '/4'; }
if (host2_toggle || host3_toggle || host4_toggle) { host_buttons += `<button type="submit" name="host" value="0" class="btn ${host0_active}" title="All">All</button> <button type="submit" name="host" value="1" hx-swap="none" class="btn ${host1_active}" title="Host 1">Host 1</button>`; }
if (host2_toggle) { host_buttons += `<button type="submit" name="host" value="2" class="btn ${host2_active}" title="${host2_tag}">${host2_tag}</button>`; }
if (host3_toggle) { host_buttons += `<button type="submit" name="host" value="3" hx-swap="none" class="btn ${host3_active}" title="${host3_tag}">${host3_tag}</button>`; }
if (host4_toggle) { host_buttons += `<button type="submit" name="host" value="4" hx-swap="none" class="btn ${host4_active}" title="${host4_tag}">${host4_tag}</button>`; }
host_buttons += '</form>';
let navbar = readFileSync('./views/partials/navbar.html', 'utf8');
if (language == 'english') {
navbar = navbar.replace(/Username/g, username);
navbar = navbar.replace(/Userrole/g, role);
navbar = navbar.replace(/HostButtons/g, host_buttons);
navbar = navbar.replace(/HOSTID/g, nav_link);
return navbar;
} else {
let lang = readFileSync(`./languages/${language}.json`, 'utf8');
lang = JSON.parse(lang);
navbar = navbar.replace(/Dashboard/g, lang.Dashboard);
navbar = navbar.replace(/Images/g, lang.Images);
navbar = navbar.replace(/Volumes/g, lang.Volumes);
navbar = navbar.replace(/Networks/g, lang.Networks);
navbar = navbar.replace(/Apps/g, lang.Apps);
navbar = navbar.replace(/Users/g, lang.Users);
navbar = navbar.replace(/Syslogs/g, lang.Syslogs);
navbar = navbar.replace(/HOSTID/g, nav_link);
navbar = navbar.replace(/Search/g, lang.Search);
navbar = navbar.replace(/Account/g, lang.Account);
navbar = navbar.replace(/Notifications/g, lang.Notifications);
navbar = navbar.replace(/Preferences/g, lang.Preferences);
navbar = navbar.replace(/Settings/g, lang.Settings);
navbar = navbar.replace(/Logout/g, lang.Logout);
navbar = navbar.replace(/Username/g, username);
navbar = navbar.replace(/Userrole/g, role);
navbar = navbar.replace(/HostButtons/g, host_buttons);
return navbar;
}
}
// Sidebar
export async function Sidebar (req) {
let language = await getLanguage(req.session.userID);
let sidebar = readFileSync('./views/partials/sidebar.html', 'utf8');
if (language == 'english') {
return sidebar;
} else {
let lang = readFileSync(`./languages/${language}.json`, 'utf8');
lang = JSON.parse(lang);
sidebar = sidebar.replace(/Account/g, lang.Account);
sidebar = sidebar.replace(/Notifications/g, lang.Notifications);
sidebar = sidebar.replace(/Preferences/g, lang.Preferences);
sidebar = sidebar.replace(/Settings/g, lang.Settings);
sidebar = sidebar.replace(/Sponsors/g, lang.Sponsors);
sidebar = sidebar.replace(/Credits/g, lang.Credits);
return sidebar;
}
}
// Footer
export async function Footer (req) {
let language = await getLanguage(req.session.userID);
let footer = readFileSync('./views/partials/footer.html', 'utf8');
let package_info = readFileSync(`package.json`, 'utf8');
package_info = JSON.parse(package_info);
let build_version = package_info.version.split('.').pop();
footer = footer.replace(/BuildVersion/g, build_version);
if (language == 'english') {
return footer;
} else {
let lang = readFileSync(`./languages/${language}.json`, 'utf8');
lang = JSON.parse(lang);
footer = footer.replace(/Documentation/g, lang.Documentation);
return footer;
}
}
// Header Alert
export function Alert (type, message) {
return `
<div class="alert alert-${type} alert-dismissible" role="alert" style="margin-bottom: 0;">
<div class="d-flex">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon alert-icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg>
</div>
<div>
${message}
</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
</div>`;
}
export async function getLanguage (userID) {
// Use the admin's language if authentication is disabled.
if (userID == '00000000-0000-0000-0000-000000000000') {
let user = await User.findOne({ where: { role: 'admin' }});
return user.language;
} else {
let user = await User.findOne({ where: { userID: userID }});
return user.language;
}
}
export function Capitalize (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View file

@ -1,34 +1,32 @@
import { docker } from "../utils/docker.js";
import { Syslog } from "../db/config.js";
import { docker } from "../server.js";
import { Syslog } from "../database/models.js";
export const Uninstall = async (req, res) => {
let { confirm, service_id } = req.body;
let { confirm, service_name } = req.body;
console.log(req.body);
console.log(`Uninstalling ${service_id}...`);
console.log(`Uninstalling ${service_name}...`);
if (confirm == 'Yes') {
let containerName = docker.getContainer(service_id);
console.log(`Stopping ${service_id}...`)
let containerName = docker.getContainer(service_name);
console.log(`Stopping ${service_name}...`)
try {
await containerName.stop();
} catch {
console.log(`Error stopping ${service_id} container`);
console.log(`Error stopping ${service_name} container`);
}
try {
console.log(`Removing ${service_id}...`);
console.log(`Removing ${service_name}...`);
containerName.remove();
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Removal",
message: `${service_id} uninstalled successfully`,
message: `${service_name} uninstalled successfully`,
ip: req.socket.remoteAddress
});
@ -38,12 +36,12 @@ export const Uninstall = async (req, res) => {
user: req.session.user,
email: null,
event: "App Removal",
message: `${service_id} uninstallation failed`,
message: `${service_name} uninstallation failed`,
ip: req.socket.remoteAddress
});
}
} else {
console.log(`Didn't confirm uninstallation of ${service_id}...`);
console.log(`Didn't confirm uninstallation of ${service_name}...`);
}
res.redirect('/');

View file

@ -1,108 +1,131 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Account - DweebUI</title>
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
</head>
<body >
<div class="page">
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<!-- EJS -->
<%- sidebar %>
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<h1 class="mb-4">My Account</h1>
<h3 class="card-title">Profile Details</h3>
<div class="row align-items-center">
<div class="col-auto"><span class="avatar avatar-xl bg-green-lt">A</span>
</div>
<div class="col-auto"><a href="#" class="btn">
Change avatar
</a></div>
<div class="col-auto"><a href="#" class="btn btn-ghost-danger">
Delete avatar
</a></div>
</div>
<h3 class="card-title mt-4"> </h3>
<div class="row g-3">
<div class="col-md">
<div class="form-label">Name</div>
<input type="text" class="form-control" value="<%= name %>">
</div>
<div class="col-md">
<div class="form-label">Username</div>
<input type="text" class="form-control" value="<%= username %>">
</div>
<div class="col-md">
<div class="form-label">Role</div>
<input type="text" class="form-control" value="<%= role %>">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle"> </p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="<%= email %>">
</div>
<div class="col-auto"><a href="#" class="btn">
Change
</a></div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle"> </p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<a href="#" class="btn btn-primary">
Submit
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
<!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 - Account</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-4">My Account</h2>
<h3 class="card-title">Profile Details</h3>
<div class="row align-items-center">
<div class="col-auto"><span class="avatar avatar-xl"><%- avatar %></span>
</div>
<div class="col-auto"><a href="#" class="btn">
Change avatar
</a>
</div>
<div class="col-auto"><a href="#" class="btn btn-ghost-danger">
Delete avatar
</a>
</div>
</div>
<h3 class="card-title mt-4">Profile</h3>
<div class="row g-3">
<div class="col-md">
<div class="form-label">Full Name</div>
<input type="text" class="form-control" value="<%= name %>" readonly="<%= name %>">
</div>
<div class="col-md">
<div class="form-label">First Name</div>
<input type="text" class="form-control" value="<%= first_name %>" readonly="<%= first_name %>">
</div>
<div class="col-md">
<div class="form-label">Last Name</div>
<input type="text" class="form-control" value="<%= last_name %>" readonly="<%= last_name %>">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="<%= email %>" readonly="<%= email %>">
</div>
<div class="col-auto">
<a href="#" class="btn">Change</a>
</div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
<h3 class="card-title mt-4">Public profile</h3>
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
you.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" >
<span class="form-check-label form-check-label-on">You're currently visible</span>
<span class="form-check-label form-check-label-off">You're
currently invisible</span>
</label>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<a href="#" class="btn btn-primary">
Submit
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,21 +1,29 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Apps - DweebUI</title>
<title>DweebUI - Apps</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<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">
<!-- EJS -->
<%- navbar %>
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
@ -27,7 +35,7 @@
<div class="card">
<div class="card-body text-center">
<div class="d-flex align-items-center">
<div class="me-auto btn"><%= app_count %></div>
<div class="me-auto btn"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps</div>
<%- remove_button %>
</div>
</div>
@ -85,7 +93,7 @@
<%- json_templates %>
</ul>
</dropdown>
<button class="btn" name="import" data-bs-toggle="modal" data-bs-target="#import_modal">Import</button>
<button class="btn" name="Import" id="Import" data-hx-get="/import_modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Import</button>
</div>
</div>
</div>
@ -113,37 +121,28 @@
<div class="container-xl">
<div class="row row-cards">
<%- apps_list %>
<div id="import_modal" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="modal-title">Import Template(s)</div>
<div class="text-muted mb-2">Individual template(s) can be *.json, *.yml, or *.yaml.</div>
<div class="text-muted mb-3">Multiple compose files can be imported from a zip file. Each folder should contain a single compose.yaml.</div>
<img src = "/img/add to zip.jpg" alt = "Add to zip" class = "img-fluid" />
<div class="mt-3">
<form method="post" action="/upload" enctype="multipart/form-data" id="upload">
<input class="form-control" type="file" name="files" multiple />
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal" form="upload">Upload</button>
</div>
<!-- HTMX Target-->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Loading</h5>
</div>
<div class="modal-body text-center">
<div class="spinner-border"></div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex mt-4">
<ul class="pagination ms-auto">
<li class="page-item">
<a class="page-link" href="<%- prev %>" tabindex="-1" aria-disabled="true">
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
<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="M15 6l-6 6l6 6" /></svg>
prev
</a>
@ -153,7 +152,7 @@
<li class="page-item">
<a class="page-link" href="<%- next %>">
next
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
<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="M9 6l6 6l-6 6" /></svg>
</a>
</li>
@ -162,34 +161,13 @@
</div>
</div>
<!-- EJS -->
<%- footer %>
<%- include('partials/footer.html') %>
</div>
</div>
<div class="modal medium-modal modal-blur fade" id="wide_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content" id="wide_modal_content">
<!-- modal content inserted with htmx -->
</div>
</div>
</div>
<div class="modal slim-modal modal-blur fade" id="info_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content" id="info_modal_content">
</div>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,89 +0,0 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Credits - DweebUI.</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
</head>
<body >
<div class="page">
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<!-- EJS -->
<%- sidebar %>
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<!-- HTMX - Submits the form and replaces the target with the response. Replaces the submit button with "Updated" -->
<form id="preferences" hx-post="/preferences" hx-target="#submit" hx-swap="outerHTML">
<h1 class="">Credits</h1>
<label class="text-muted">DweebUI wouldn't be possible without:</label>
<div class="table-group-divider mt-3"></div>
<ul>
<li class="m-2">Dockerode and Dockerode-Compose by Apocas </li>
<li class="m-2">Tabler</li>
<li class="m-2">Walkxcode</li>
</ul>
</div>
<!-- <div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<button class="btn btn-primary" id="submit">
Update
</button>
</div>
</div> -->
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
</html>

View file

@ -1,188 +1,280 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Dashboard - DweebUI.</title>
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
</head>
<body>
<div class="page">
<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>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/meters.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<script src="/js/htmx-sse.js"></script>
<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">
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper" hx-ext="sse" sse-connect="/sse">
<div class="page-body" style="margin-top: 16px;">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="CPU" id="green" data-hx-get="/server_metrics" data-hx-trigger="load, every 1s" hx-swap="innerHTML">
<div class="font-weight-medium">
<label class="cpu-text mb-1">CPU 0%</label>
</div>
<div class="cpu-bar meter animate green">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="RAM" id="blue" data-hx-get="/server_metrics" data-hx-trigger="load, every 2s" hx-swap="innerHTML">
<div class="font-weight-medium">
<label class="ram-text mb-1">RAM 0%</label>
</div>
<div class="ram-bar meter animate blue">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-purple text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" 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="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
</span>
</div>
<div class="col" name="NET" id="purple" data-hx-get="/server_metrics" data-hx-trigger="load, every 3s">
<div class="font-weight-medium">NET 0%</div>
<div class="cpu-bar meter animate purple mt-1">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-orange text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="DISK" id="orange" data-hx-get="/server_metrics" data-hx-trigger="load, every 3s">
<div class="font-weight-medium">
<label class="disk-text mb-1">DISK 0%</label>
</div>
<div class="meter animate orange">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="row row-cards" id="containers">
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" name="card_list" hx-get="/dashboard/view/card_list" data-hx-trigger="load, sse:update" data-hx-swap="afterbegin" hx-target="#containers"></div>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
</div>
</div>
<div class="modal slim-modal modal-blur fade" id="scrolling_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content" id="modal_content">
<!-- modal content inserted with htmx -->
</div>
</div>
</div>
<div class="modal medium-modal modal-blur fade" id="medium_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content" id="medium_modal_content">
<!-- modal content inserted with htmx -->
</div>
</div>
</div>
<div class="modal wide-modal modal-blur fade" id="wide_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content" id="wide_modal_content">
<!-- modal content inserted with htmx -->
</div>
</div>
</div>
<script defer>
var modalScrollable = document.getElementById('wide_modal');
modalScrollable.addEventListener('shown.bs.modal', function () {
modalScrollable.querySelector('.modal-body').scrollTop = modalScrollable.querySelector('.modal-body').scrollHeight;
});
</script>
<script src="/libs/apexcharts/dist/apexcharts.min.js?1692870487"></script>
<script src="/js/dweebui.js"></script>
<script src="/js/htmx.min.js"></script>
<script src="/js/htmx-sse.js"></script>
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<%- include('partials/navbar.html') %>
</body>
</html>
<div class="page-wrapper">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards" hx-ext="sse" sse-connect="/sse">
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="CPU" id="green" data-hx-get="/stats" data-hx-trigger="load, every 1s">
<div class="font-weight-medium">
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
</div>
<div class="cpu-bar meter animate green">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="RAM" id="blue" data-hx-get="/stats" data-hx-trigger="load, every 2s">
<div class="font-weight-medium">
<label class="ram-text mb-1" for="ram">RAM 0%</label>
</div>
<div class="ram-bar meter animate blue">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-purple text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" 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="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="NET" id="purple" data-hx-get="/stats" data-hx-trigger="load, every 2s">
<div class="font-weight-medium">
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
</div>
<div class="ram-bar meter animate purple">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-orange text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="DISK" id="orange" data-hx-get="/stats" data-hx-trigger="load, every 3s">
<div class="font-weight-medium">
<label class="disk-text mb-1" for="disk">DISK 0%</label>
</div>
<div class="meter animate orange">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="row row-cards" id="containers">
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" data-hx-post="/dashboard/updates" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
</div>
</div>
<!-- HTMX Modal Target -->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Loading</h5>
</div>
<div class="modal-body text-center">
<div class="spinner-border"></div>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card-body">
<h4>Logs:</h4>
<div id="logView">
<pre>No logs available</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" 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 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
<script src="/js/tabler.min.js"></script>
<script>
var options = {
chart: {
type: "line",
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [3, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: []
}, {
name: "RAM",
data: []
}],
tooltip: {
enabled: false
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
}
},
yaxis: {
min: 0,
max: 100,
labels: {
padding: 4
}
},
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}
</script>
<!-- SelectAll for the permissions modal -->
<script>
function selectAll(group) {
let checkboxes = document.getElementsByName(group);
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -1,26 +1,31 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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 - Images</title>
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- EJS -->
<%- navbar %>
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body" style="margin-top: 16px;">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
@ -44,23 +49,9 @@
<div id="table-default" class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll('select')"></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-type">Tag</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">
<%- image_list %>
</tbody>
</table>
</div>
@ -124,21 +115,16 @@
</div>
</div>
<!-- EJS -->
<%- footer %>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
@ -154,5 +140,20 @@
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -1,117 +1,89 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Login - DweebUI</title>
<title>DweebUI - Login</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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 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.png" height="100" alt="Dweeb">
</a>
<div class="text-center">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="100px">
</h1>
</div>
<div class="text-center mb-4">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/img/dweebui.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>
<% } %>
<form action="/login" method="post" autocomplete="off" novalidate>
<div class="mb-3">
<div class="mb-2">
<label class="form-label">Email address</label>
<input name="email" type="email" class="form-control" autocomplete="off">
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-2">
<label class="form-label">
Password
<span class="form-label-description">
<!-- <a href="./forgot-password.html">I forgot password</a> -->
</span>
</label>
<div class="input-group input-group-flat">
<input type="password" name="password" class="form-control" autocomplete="off">
<!-- <span class="input-group-text">
<a href="#" class="link-secondary" title="Show password" data-bs-toggle="tooltip">
<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="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>
</span> -->
<input type="password" class="form-control" id="password" name="password" autocomplete="off">
</div>
</div>
<!-- <div class="mb-2">
<div class="mb-2 d-flex justify-content-between">
<label class="form-check">
<input type="checkbox" class="form-check-input"/>
<span class="form-check-label">Remember me on this device</span>
<input type="checkbox" class="form-check-input" checked=""/>
<span class="form-check-label">
Remember me
</span>
</label>
</div> -->
<span>
<a href="#">Forgot password</a>
</span>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">Login</button>
<button type="submit" class="btn btn-primary w-100">Sign in</button>
</div>
</form>
</div>
<!-- <div class="hr-text">or</div>
<div class="card-body">
<div class="row">
<div class="col">
<a href="#" class="btn w-100 text-secondary" title="not available">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-github" 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="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" /></svg>
Login with Github
</a>
</div>
<div class="col">
<a href="#" class="btn w-100 text-secondary" title="not available">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-twitter" 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="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c0 -.249 1.51 -2.772 1.818 -4.013z" /></svg>
Login with Twitter
</a>
</div>
</div>
</div> -->
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<button class="nav-link hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom" value="dark-theme" onclick="toggleTheme(this)">
<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>
</button>
<button class="nav-link hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom" value="light-theme" onclick="toggleTheme(this)">
<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>
</button>
</div>
<div class="text-center text-secondary">
Don't have an account? <a href="/register" tabindex="-1">Register</a>
</div>
<div class="text-center text-muted mt-2">
Don't have account? <a href="/register" tabindex="-1">Register</a>
</div>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,83 +1,75 @@
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="details">
<div class="row mb-3 align-items-end">
<div class="col-lg-5">
<label class="form-label">Container Name: </label>
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
<input type="text" class="form-control" name="name" value="AppName"/>
</div>
<div class="col-lg-4">
<label class="form-label">Image: </label>
<input type="text" class="form-control" name="image" value="AppImage"/>
</div>
<div class="col-lg-3">
<label class="form-label">Restart Policy: </label>
<select class="form-select" name="restart_policy">
<option value="RestartPolicy" selected hidden>RestartPolicy</option>
<option value="unless-stopped">unless-stopped</option>
<option value="on-failure">on-failure</option>
<option value="no">never</option>
<option value="always">always</option>
</select>
</div>
<form action="" id="details_modal" method="POST">
<div class="row mb-3 align-items-end">
<div class="col-lg-5">
<label class="form-label">Container Name: </label>
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
<input type="text" class="form-control" name="name" value="AppName"/>
</div>
<div class="col-lg-4">
<label class="form-label">Image: </label>
<input type="text" class="form-control" name="image" value="AppImage"/>
</div>
<div class="col-lg-3">
<label class="form-label">Restart Policy: </label>
<select class="form-select" name="restart_policy" value="">
<option value="1">unless-stopped</option>
<option value="2">on-failure</option>
<option value="3">never</option>
<option value="4">always</option>
</select>
</div>
</div>
<label class="form-label">Network Mode</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" NetHost>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Host Network</span>
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="NetName" class="form-selectgroup-input" NetBridge>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" NetDocker>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="report-type" value="1" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
<span class="form-selectgroup-title strong mb-1">Host Network</span>
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
</span>
</span>
</label>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="report-type" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="report-type" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
</span>
</span>
</label>
</div>
</div>
<div class="accordion" id="modal-accordion">
@ -90,21 +82,20 @@
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port0" type="checkbox" Port0Check>
<input class="form-check-input" name="port_0_check" type="checkbox" Port0Check>
</div>
<div class="col">
<label class="form-label">External Port</label>
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="port_0_external" value="Port0External"/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="port_0_internal" value="Port0Internal"/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<label class="form-label">Protocol</label>
<select class="form-select" name="port_0_protocol">
<option value="Port0Protocol" selected hidden>Port0Protocol</option>
<option value="tcp">tcp</option>
@ -115,7 +106,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port1" type="checkbox" Port1Check>
<input class="form-check-input" name="port_1_check" type="checkbox" Port1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_external" value="Port1External"/>
@ -134,7 +125,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port2" type="checkbox" Port2Check>
<input class="form-check-input" name="port_2_check" type="checkbox" Port2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_external" value="Port2External"/>
@ -154,7 +145,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port3" type="checkbox" Port3Check>
<input class="form-check-input" name="port_3_check" type="checkbox" Port3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_external" value="Port3External"/>
@ -174,7 +165,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port4" type="checkbox" Port4Check>
<input class="form-check-input" name="port_4_check" type="checkbox" Port4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_external" value="Port4External"/>
@ -193,7 +184,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port5" type="checkbox" Port5Check>
<input class="form-check-input" name="port_5_check" type="checkbox" Port5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_external" value="Port5External"/>
@ -225,7 +216,7 @@
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume0" type="checkbox" Vol0Check>
<input class="form-check-input" name="volume_0_check" type="checkbox" Vol0Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_bind" value="Vol0Source"/>
@ -244,7 +235,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume1" type="checkbox" Vol1Check>
<input class="form-check-input" name="volume_1_check" type="checkbox" Vol1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_bind" value="Vol1Source"/>
@ -263,7 +254,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume2" type="checkbox" Vol2Check>
<input class="form-check-input" name="volume_2_check" type="checkbox" Vol2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_bind" value="Vol2Source"/>
@ -282,7 +273,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume3" type="checkbox" Vol3Check>
<input class="form-check-input" name="volume_3_check" type="checkbox" Vol3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_bind" value="Vol3Source"/>
@ -301,7 +292,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume4" type="checkbox" Vol4Check>
<input class="form-check-input" name="volume_4_check" type="checkbox" Vol4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_bind" value="Vol4Source"/>
@ -320,7 +311,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume5" type="checkbox" Vol5Check>
<input class="form-check-input" name="volume_5_check" type="checkbox" Vol5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_bind" value="Vol5Source"/>
@ -579,52 +570,52 @@
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_1_check" Label1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_name" value="Label1Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
</div>
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_1_check" Label1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_name" value="Label1Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_2_check" Label2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_name" value="Label2Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
</div>
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_2_check" Label2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_name" value="Label2Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_3_check" Label3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_name" value="Label3Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
</div>
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_3_check" Label3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_name" value="Label3Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_4_check" Label4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_name" value="Label4Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
</div>
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_4_check" Label4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_name" value="Label4Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
@ -808,35 +799,158 @@
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-5">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
Extras
</button>
</h2>
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-5">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
Extras
</button>
</h2>
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<!-- <div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_0_check" type="checkbox" ${ports_data[0].check}>
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="port_0_external" value="${ports_data[0].external}"/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="port_0_internal" value="${ports_data[0].internal}"/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="port_0_protocol">
<option value="${ports_data[0].protocol}" selected hidden>${ports_data[0].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_1_check" type="checkbox" ${ports_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_external" value="${ports_data[1].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_internal" value="${ports_data[1].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_1_protocol">
<option value="${ports_data[1].protocol}" selected hidden>${ports_data[1].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_2_check" type="checkbox" ${ports_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_external" value="${ports_data[2].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_internal" value="${ports_data[2].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_2_protocol">
<option value="${ports_data[2].protocol}" selected hidden>${ports_data[2].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_3_check" type="checkbox" ${ports_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_external" value="${ports_data[3].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_internal" value="${ports_data[3].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_3_protocol">
<option value="${ports_data[3].protocol}" selected hidden>${ports_data[3].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_4_check" type="checkbox" ${ports_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_external" value="${ports_data[4].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_internal" value="${ports_data[4].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_4_protocol">
<option value="${ports_data[4].protocol}" selected hidden>${ports_data[4].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="" type="checkbox" >
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="" value=""/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="" value=""/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="">
<option value="" selected hidden></option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div> -->
</div>
</div>
</div>
</div>
</form>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" form="install_info">Install</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>

20
views/modals/import.html Normal file
View file

@ -0,0 +1,20 @@
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="modal-title">Import Template(s)</div>
<div class="text-muted mb-2">Individual template(s) can be *.json, *.yml, or *.yaml.</div>
<div class="text-muted mb-3">Multiple compose files can be imported from a zip file. Each folder should contain a single compose.yaml.</div>
<img src = "/img/add to zip.jpg" alt = "Add to zip" class = "img-fluid" />
<div class="mt-3">
<form method="post" action="/upload" enctype="multipart/form-data" id="upload">
<input class="form-control" type="file" name="files" multiple />
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal" form="upload">Upload</button>
</div>
</div>
</div>

728
views/modals/json.html Normal file
View file

@ -0,0 +1,728 @@
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Install AppName</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre class="text-secondary">AppNote</pre>
<form action="/install" name="FormId_install" id="FormId_install" method="POST">
<div class="row mb-3 align-items-end">
<div class="col-lg-6">
<label class="form-label">Container Name: </label>
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
<input type="text" class="form-control" name="name" value="AppName"/>
</div>
<div class="col-lg-3">
<label class="form-label">Image: </label>
<input type="text" class="form-control" name="image" value="AppImage"/>
</div>
<div class="col-lg-3">
<label class="form-label">Restart Policy: </label>
<select class="form-select" name="restart_policy">
<option value="RestartPolicy" selected hidden>RestartPolicy</option>
<option value="unless-stopped">unless-stopped</option>
<option value="on-failure">on-failure</option>
<option value="no">never</option>
<option value="always">always</option>
</select>
</div>
</div>
<label class="form-label">Network Mode</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" NetHost>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Host Network</span>
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="NetName" class="form-selectgroup-input" NetBridge>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" NetDocker>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
</span>
</span>
</label>
</div>
</div>
<div class="accordion" id="ModalName-accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="heading-1">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
Ports
</button>
</h2>
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port0" type="checkbox" Port0Check>
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="port_0_external" value="Port0External"/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="port_0_internal" value="Port0Internal"/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="port_0_protocol">
<option value="Port0Protocol" selected hidden>Port0Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port1" type="checkbox" Port1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_external" value="Port1External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_internal" value="Port1Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_1_protocol">
<option value="Port1Protocol" selected hidden>Port1Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port2" type="checkbox" Port2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_external" value="Port2External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_internal" value="Port2Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_2_protocol">
<option value="Port2Protocol" selected hidden>Port2Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port3" type="checkbox" Port3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_external" value="Port3External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_internal" value="Port3Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_3_protocol">
<option value="Port3Protocol" selected hidden>Port3Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port4" type="checkbox" Port4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_external" value="Port4External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_internal" value="Port4Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_4_protocol">
<option value="Port4Protocol" selected hidden>Port4Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port5" type="checkbox" Port5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_external" value="Port5External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_internal" value="Port5Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_5_protocol">
<option value="Port5Protocol" selected hidden>Port5Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-2">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
Volumes
</button>
</h2>
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume0" type="checkbox" Volume0Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_bind" value="Volume0Bind"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_container" value="Volume0Container"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_0_readwrite">
<option value="Volume0RW" selected hidden>Volume0RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume1" type="checkbox" Volume1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_bind" value="Volume1Bind"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_container" value="Volume1Container"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_1_readwrite">
<option value="Volume1RW" selected hidden>Volume1RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume2" type="checkbox" Volume2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_bind" value="Volume2Bind"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_container" value="Volume2Container"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_2_readwrite">
<option value="Volume2RW" selected hidden>Volume2RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume3" type="checkbox" Volume3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_bind" value="Volume3Bind"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_container" value="Volume3Container"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_3_readwrite">
<option value="Volume3RW" selected hidden>Volume3RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume4" type="checkbox" Volume4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_bind" value="Volume4Bind"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_container" value="Volume4Container"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_4_readwrite">
<option value="Volume4RW" selected hidden>Volume4RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume5" type="checkbox" Volume5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_bind" value="Volume5Bind"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_container" value="Volume5Container"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_5_readwrite">
<option value="Volume5RW" selected hidden>Volume5RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-3">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
Environment Variables
</button>
</h2>
<div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env0" Env0Check>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="env_0_name" value="Env0Name"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="env_0_default" value="Env0Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env1" Env1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_name" value="Env1Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_default" value="Env1Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env2" Env2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_name" value="Env2Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_default" value="Env2Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env3" Env3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_name" value="Env3Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_default" value="Env3Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env4" Env4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_name" value="Env4Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_default" value="Env4Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env5" Env5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_name" value="Env5Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_default" value="Env5Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env6" Env6Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_name" value="Env6Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_default" value="Env6Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env7" Env7Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_name" value="Env7Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_default" value="Env7Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env8" Env8Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_name" value="Env8Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_default" value="Env8Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env9" Env9Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_name" value="Env9Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_default" value="Env9Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env10" Env10Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_name" value="Env10Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_default" value="Env10Default"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env11" Env11Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_name" value="Env11Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_default" value="Env11Default"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-4">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
Labels
</button>
</h2>
<div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label0" Label0Check>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="label_0_name" value="Label0Name"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="label_0_value" value="Label0Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label1" Label1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_name" value="Label1Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label2" Label2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_name" value="Label2Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label3" Label3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_name" value="Label3Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label4" Label4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_name" value="Label4Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label5" Label5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_name" value="Label5Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_value" value="Label5Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label6" Label6Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_name" value="Label6Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_value" value="Label6Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label7" Label7Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_name" value="Label7Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_value" value="Label7Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label8" Label8Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_name" value="Label8Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_value" value="Label8Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label9" Label9Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_name" value="Label9Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_value" value="Label9Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label10" Label10Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_name" value="Label10Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_value" value="Label10Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label11" Label11Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label11Name"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label11Value"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-5">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
Extras
</button>
</h2>
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="command_check" type="checkbox" CommandCheck>
</div>
<div class="col">
<label class="form-label">Command</label>
<input type="text" class="form-control" name="command" value="CommandValue"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="privileged" type="checkbox" PrivilegedCheck>
</div>
<div class="col">
<label class="form-label">Privileged Mode</label>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<input type="submit" form="FormId_install" class="btn btn-success" value="Install"/>
</div>
</div>
</div>

View file

@ -0,0 +1,13 @@
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="modal-title">AppName</div>
<div>AppDesc</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
</div>
</div>
</div>

View file

@ -0,0 +1,22 @@
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">PermissionsTitle Permissions</h5>
</div>
<div class="modal-body">
<div class="accordion" id="modal-accordion">
PermissionsList
</div>
</div>
<div class="modal-footer">
<div class="row">
<div class="col">
<form id="reset_permissions">
<input type="hidden" name="container" value="PermissionsContainer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" value="reset_permissions" id="submit" hx-post="/updatePermissions" hx-trigger="click" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
</form>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,77 @@
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-danger"></div>
<div class="modal-body text-center py-3">
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
<h3>Uninstall AppName?</h3>
<form action="/uninstall" id="AppName_uninstall" method="POST">
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
<div class="mb-3"> </div>
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">Remove Volumes</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Image
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_image" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Backups
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
</label>
</div>
</div>
</div>
</div>
<div class="mt-1"> </div>
<div class="text-muted">Enter "Yes" below to remove the container.</div>
<input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
</form>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" form="AppName_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,27 +1,31 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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 - Networks</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- EJS -->
<%- navbar %>
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body" style="margin-top: 16px;">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
@ -44,28 +48,16 @@
<div id="table-default" class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll('select')"></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">
<%- network_list %>
</tbody>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/network/remove/">Remove</button>
<button class="btn" type="submit" formaction="/removeNetwork">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
@ -95,21 +87,16 @@
</div>
</div>
<!-- EJS -->
<%- footer %>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
@ -125,6 +112,20 @@
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,22 @@
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body text-center">
<span class="avatar avatar-xlplus mb-3 rounded"><img src='AppLogo' width="144px" height="144px" loading="lazy"/></span>
<h3 class="m-0 mb-1"><a href="#">AppShortName</a></h3>
<div class="text-secondary">AppDesc</div>
<div class="mt-3">
AppCategories
</div>
</div>
<div class="d-flex">
<a href="#" class="card-btn" name="AppName" id="AppType" data-hx-get="/learn_more" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article" 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="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
  Learn More
</a>
<a href="" class="card-btn" name="AppName" id="AppType" data-hx-get="/install_modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down" 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="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
  Install
</a>
</div>
</div>
</div>

View file

@ -1,46 +0,0 @@
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body py-2">
<div class="row">
<h1 class="m-0 p-0">AppTitle</h1>
<div class="col-auto p-0">
<span class="avatar avatar-xl mt-1" style="background-image: url(AppIcon)"></span>
</div>
<div class="col">
<div class="card-subtitle description m-1">AppDescription</div>
</div>
<div class="py-1 mb-1">
<div class="row align-items-center">
<div class="col-auto m-0 p-0">
AppCategories
</div>
</div>
</div>
<div class="px-0">
<div class="row align-items-center">
<div class="col-auto">
<a href="" class="btn" name="AppName" id="AppType" hx-get="/appsModals/info" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#info_modal_content" data-bs-toggle="modal" data-bs-target="#info_modal">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article mx-2" 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="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
  More Info
</a>
</div>
<div class="col-auto ms-auto">
<button class="btn" name="AppName" id="AppType" hx-get="/appsModals/view_install" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#wide_modal_content" data-bs-toggle="modal" data-bs-target="#wide_modal">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down mx-2" 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="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
 Install  
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,80 @@
<div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<div class="subheader text-yellow">ExternalPort:InternalPort</div>
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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 class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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 class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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 class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" 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><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-post="/dashboard/details" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-post="/dashboard/logs" hx-swap="innerHTML" hx-trigger="mousedown" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
<button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
<button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="mousedown" data-hx-post="/dashboard/uninstall" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
</div>
</div>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<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" data-hx-post="/dashboard/hide" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/permissions" name="AppName" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="mousedown" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="AppName" style="margin-bottom: 0;">
<a href="http://${link}:${external_port}" target="_blank">
AppShortName
</a>
</div>
<div class="ms-auto">
<label id="AppNameState">
<span class="text-StateColor align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
AppState
</span>
</label>
</div>
</div>
<script>
var ChartNamechart = new ApexCharts(document.querySelector("#ChartName_chart"), options);
</script>
<div class="chart-sm">
<div id="ChartName_chart" data-trigger="" data-hx-get="/chart" name="ChartName" hx-swap="innerHTML">
<script>
ChartNamechart.render();
</script>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,71 @@
<div class="col-sm-6 col-lg-3 pt-1" hx-get="" hx-trigger="" hx-swap="none" name="AppName">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<div class="subheader text-yellow">ExternalPort:InternalPort</div>
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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 class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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 class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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 class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<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">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" 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><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-get="/dashboard/modals" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-get="/dashboard/logs" hx-swap="innerHTML" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
<button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
<button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="click" data-hx-get="/modals" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
</div>
</div>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<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" data-hx-post="/dashboard/hide" data-hx-trigger="click" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="click" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
<button class="dropdown-item text-secondary" name="AppName" id="permissions" data-hx-get="/modals" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="AppName">
<a href="http://${link}:${external_port}" target="_blank">
AppShortName
</a>
</div>
<div class="ms-auto">
<label id="AppNameState">
<span class="text-StateColor align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
AppState
</span>
</label>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,127 +0,0 @@
<div class="col-sm-6 col-lg-3" hx-get="/dashboard/view/update_card/ContainerID" hx-trigger="sse:ContainerID" id="AltID" hx-swap="outerHTML" hx-target="#AltID" name="AppName">
<div class="card p-2">
<div class="container-stamp">
<img class="rounded-4 pt-1 px-1" width="95px" src="https://raw.githubusercontent.com/lllllllillllllillll/Dashboard-Icons/main/png/AppService.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<label style="font-size: smaller; font-weight: bold;" class="text-yellow">AppPorts</label>
<div class="ms-auto lh-1">
<button class="container-action" title="Start" data-hx-post="/dashboard/action/start/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Start" id="AppName" hx-swap="outerHTML">
<svg xmlns="http://www.w3.org/2000/svg" class="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 class="container-action" title="Stop" data-hx-post="/dashboard/action/stop/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Stop" id="AppName" hx-swap="outerHTML">
<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 class="container-action" title="Pause" data-hx-post="/dashboard/action/pause/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Pause" id="AppName" hx-swap="outerHTML">
<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 class="container-action" title="Restart" data-hx-post="/dashboard/action/restart/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Restart" id="AppName" hx-swap="outerHTML">
<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>
<a href="#" class="container-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" 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><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/details/ContainerID" data-hx-trigger="mousedown" hx-target="#medium_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#medium_modal">Details</button>
<button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/logs/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#wide_modal">Logs</button>
<button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/edit/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#wide_modal">Edit</button>
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
<button class="dropdown-item text-danger" name="AppName" hx-trigger="mousedown" data-hx-get="/dashboard/view/uninstall/ContainerID" hx-swap="innerHTML" data-hx-target="#modal_content" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Uninstall</button>
</div>
<a href="#" class="container-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<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" name="AppName" data-hx-post="/dashboard/action/hide/ContainerID" data-hx-trigger="mousedown" data-hx-swap="delete" data-hx-target="#AltID">Hide</button>
<button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/link_modal/ContainerID" hx-trigger="mousedown" hx-swap="innerHTML" data-hx-target="#modal_content" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Edit Link</button>
<button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/permissions/ContainerID" hx-target="#modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Permissions</button>
</div>
</div>
</div>
<div class="d-flex align-items-center">
<label style="font-size: x-large; font-weight: bold; line-height: 1;">TitleLink</label>
<div class="text-StateColor d-inline-flex align-items-center ms-auto" id="AltIDState">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="20" height="24" viewBox="0 0 20 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 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
<strong>AppState</strong>
</div>
</div>
<div id="AltIDchart" class="chart-sm"></div>
<script>
var options = {
chart: {
type: "line",
fontFamily: 'inherit',
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
},
},
fill: {
opacity: 1,
},
stroke: {
width: [3, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth",
},
series: [{
name: "CPU",
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},{
name: "RAM",
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}],
tooltip: {
enabled: false,
theme: 'dark'
},
grid: {
strokeDashArray: 4,
},
xaxis: {
labels: {
padding: 0,
},
tooltip: {
enabled: false
},
type: 'datetime',
},
yaxis: {
min: 0,
max: 100,
labels: {
padding: 0,
},
},
labels: [
'2020-06-20', '2020-06-21', '2020-06-22', '2020-06-23', '2020-06-24', '2020-06-25', '2020-06-26', '2020-06-27', '2020-06-28', '2020-06-29'
],
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false,
},
};
var AltIDchart = new ApexCharts(document.querySelector("#AltIDchart"), options);
AltIDchart.render();
</script>
ChartTrigger
</div>
</div>

View file

@ -3,11 +3,11 @@
<div class="row text-center align-items-center flex-row-reverse">
<div class="col-lg-auto ms-lg-auto">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/wiki" target="_blank" class="link-secondary" rel="noopener">Documentation</a></li>
<li class="list-inline-item"><a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/LICENSE" class="link-secondary">License</a></li>
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI" target="_blank" class="link-secondary" rel="noopener">Source code</a></li>
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/README.md" target="_blank" class="link-secondary" rel="noopener">Documentation</a></li>
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE" class="link-secondary">License</a></li>
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/tree/main" target="_blank" class="link-secondary" rel="noopener">Source code</a></li>
<li class="list-inline-item">
<a href="https://buymeacoffee.com/lllllllillllllillll" target="_blank" class="link-secondary" rel="noopener">
<a href="https://www.buymeacoffee.com/lllllllillllllillll" target="_blank" class="link-secondary" rel="noopener">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-pink icon-filled icon-inline" 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="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>
Sponsor
</a>
@ -17,13 +17,13 @@
<div class="col-12 col-lg-auto mt-3 mt-lg-0">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item">
Copyright &copy; 2024
Copyright &copy; 2023 - 2024
<a href="https://dweebui.com" class="link-secondary">DweebUI</a>.
All rights reserved.
</li>
<li class="list-inline-item">
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/CHANGELOG.md" class="link-secondary" rel="noopener">
v0.7X (Extra Broken) [Build BuildVersion]
<a href="https://github.com/lllllllillllllillll/DweebUI/releases" class="link-secondary" rel="noopener">
v0.60
</a>
</li>
</ul>

View file

@ -1,9 +0,0 @@
<div class="modal-body">
<div class="modal-title">AppTitle</div>
<div>AppDescription</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
</div>

View file

@ -1,842 +0,0 @@
<div class="modal-header">
<h5 class="modal-title">Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="install_info" hx-post="/install" hx-swap="innerHTML" hx-target="#alert">
<div class="row mb-3 align-items-end">
<div class="col-lg-5">
<label class="form-label">Container Name: </label>
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
<input type="text" class="form-control" name="name" value="AppName"/>
</div>
<div class="col-lg-4">
<label class="form-label">Image: </label>
<input type="text" class="form-control" name="image" value="AppImage"/>
</div>
<div class="col-lg-3">
<label class="form-label">Restart Policy: </label>
<select class="form-select" name="restart_policy">
<option value="RestartPolicy" selected hidden>RestartPolicy</option>
<option value="unless-stopped">unless-stopped</option>
<option value="on-failure">on-failure</option>
<option value="no">never</option>
<option value="always">always</option>
</select>
</div>
</div>
<label class="form-label">Network Mode</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" NetHost>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Host Network</span>
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="NetName" class="form-selectgroup-input" NetBridge>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" NetDocker>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
</span>
</span>
</label>
</div>
</div>
<div class="accordion" id="modal-accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="heading-1">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
Ports
</button>
</h2>
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port0" type="checkbox" Port0Check>
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="port_0_external" value="Port0External"/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="port_0_internal" value="Port0Internal"/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="port_0_protocol">
<option value="Port0Protocol" selected hidden>Port0Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port1" type="checkbox" Port1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_external" value="Port1External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_internal" value="Port1Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_1_protocol">
<option value="Port1Protocol" selected hidden>Port1Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port2" type="checkbox" Port2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_external" value="Port2External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_internal" value="Port2Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_2_protocol">
<option value="Port2Protocol" selected hidden>Port2Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port3" type="checkbox" Port3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_external" value="Port3External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_internal" value="Port3Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_3_protocol">
<option value="Port3Protocol" selected hidden>Port3Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port4" type="checkbox" Port4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_external" value="Port4External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_internal" value="Port4Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_4_protocol">
<option value="Port4Protocol" selected hidden>Port4Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port5" type="checkbox" Port5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_external" value="Port5External"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_internal" value="Port5Internal"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_5_protocol">
<option value="Port5Protocol" selected hidden>Port5Protocol</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-2">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
Volumes
</button>
</h2>
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume0" type="checkbox" Vol0Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_bind" value="Vol0Source"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_container" value="Vol0Destination"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_0_readwrite">
<option value="Vol0RW" selected hidden>Vol0RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume1" type="checkbox" Vol1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_bind" value="Vol1Source"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_container" value="Vol1Destination"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_1_readwrite">
<option value="Vol1RW" selected hidden>Vol1RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume2" type="checkbox" Vol2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_bind" value="Vol2Source"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_container" value="Vol2Destination"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_2_readwrite">
<option value="Vol2RW" selected hidden>Vol2RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume3" type="checkbox" Vol3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_bind" value="Vol3Source"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_container" value="Vol3Destination"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_3_readwrite">
<option value="Vol3RW" selected hidden>Vol3RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume4" type="checkbox" Vol4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_bind" value="Vol4Source"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_container" value="Vol4Destination"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_4_readwrite">
<option value="Vol4RW" selected hidden>Vol4RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume5" type="checkbox" Vol5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_bind" value="Vol5Source"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_container" value="Vol5Destination"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_5_readwrite">
<option value="Vol5RW" selected hidden>Vol5RW</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-3">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
Environment Variables
</button>
</h2>
<div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_0_check" Env0Check>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="env_0_name" value="Env0Key"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="env_0_default" value="Env0Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_1_check" Env1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_name" value="Env1Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_default" value="Env1Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_2_check" Env2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_name" value="Env2Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_default" value="Env2Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_3_check" Env3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_name" value="Env3Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_default" value="Env3Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_4_check" Env4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_name" value="Env4Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_default" value="Env4Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_5_check" Env5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_name" value="Env5Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_default" value="Env5Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_6_check" Env6Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_name" value="Env6Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_default" value="Env6Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_7_check" Env7Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_name" value="Env7Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_default" value="Env7Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_8_check" Env8Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_name" value="Env8Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_default" value="Env8Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_9_check" Env9Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_name" value="Env9Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_default" value="Env9Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_10_check" Env10Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_name" value="Env10Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_default" value="Env10Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_11_check" Env11Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_name" value="Env11Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_default" value="Env11Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_12_check" Env12Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_name" value="Env12Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_default" value="Env12Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_12_check" Env13Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_name" value="Env13Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_default" value="Env13Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_12_check" Env14Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_name" value="Env14Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_default" value="Env14Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env_12_check" Env15Check>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_name" value="Env15Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_12_default" value="Env15Value"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-4">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
Labels
</button>
</h2>
<div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_0_check" Label0Check>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="label_0_name" value="Label0Key"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="label_0_value" value="Label0Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_1_check" Label1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_name" value="Label1Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_2_check" Label2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_name" value="Label2Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_3_check" Label3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_name" value="Label3Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_4_check" Label4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_name" value="Label4Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_5_check" Label5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_name" value="Label5Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_value" value="Label5Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_6_check" Label6Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_name" value="Label6Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_value" value="Label6Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_7_check" Label7Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_name" value="Label7Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_value" value="Label7Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_8_check" Label8Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_name" value="Label8Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_value" value="Label8Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_9_check" Label9Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_name" value="Label9Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_value" value="Label9Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_10_check" Label10Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_name" value="Label10Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_value" value="Label10Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label11Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label11Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label11Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label12Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label12Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label12Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label13Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label13Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label13Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label14Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label14Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label14Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label15Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label15Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label15Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label16Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label16Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label16Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label17Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label17Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label17Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label18Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label18Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label18Value"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label_11_check" Label19Check>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="Label19Key"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="Label19Value"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-5">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
Extras
</button>
</h2>
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" form="install_info" data-bs-dismiss="modal" onclick="topScroll()">Install</button>
</div>

View file

@ -1,31 +0,0 @@
<div class="modal-content" id="modal_content">
<form action="/container/update_link/ContainerID" method="post">
<input type="hidden" name="service_id" value="ContainerID">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-body text-center py-3">
<h3 class="mb-3">AppName Link</h3>
<div class="text-muted mb-2">URL needs to start with http:// or https://</div>
<input type="text" class="form-control mb-2" name="url" value="AppLink">
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" value="Update" class="btn btn-primary w-100">
</div>
</div>
</div>
</div>
</form>
</div>

View file

@ -1,43 +0,0 @@
<div class="modal-header">
<h5 class="modal-title">Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="modalBody">
<div class="row mb-3 align-items-end">
<div class="col-lg-5">
<label class="form-label mb-1">Container Name: </label>
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
<input type="text" class="form-control" name="name" value="AppName"/>
</div>
<!-- <div class="col-lg-4">
<label class="form-label mb-1">Image: </label>
<input type="text" class="form-control" name="image" value="AppImage"/>
</div> -->
<div class="col-lg-3">
<label class="form-label mb-1">Filter: </label>
<select class="form-select" name="restart_policy">
<option value="All" selected hidden>All</option>
<option value="All">All</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-12">
<textarea class="form-control" id="logs" style="height: 65vh; resize: none;" readonly>ContainerLogs</textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-success" name="AppName" data-hx-get="/dashboard/view/logs/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#wide_modal">Refresh</button>
</div>

View file

@ -1,251 +1,278 @@
<header class="navbar navbar-expand-md d-print-none" >
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal p-0">
<a href="#">
<img src="/static/logo.png" alt="DweebUI" title="DweebUI" height="40px">
</a>
<a href="#">
<img src="/static/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</a>
</h1>
<script>
var themeStorageKey = "tablerTheme";
var defaultTheme = "dark";
var selectedTheme;
(function () {
'use strict';
var storedTheme = localStorage.getItem(themeStorageKey);
selectedTheme = storedTheme ? storedTheme : defaultTheme;
if (selectedTheme === 'dark') {
document.body.setAttribute("data-bs-theme", selectedTheme);
} else {
document.body.removeAttribute("data-bs-theme");
}
})();
function toggleTheme(button) {
if (button.value == 'dark-theme') {
document.body.setAttribute("data-bs-theme", 'dark');
localStorage.setItem(themeStorageKey, 'dark');
}
else if (button.value == 'light-theme') {
document.body.removeAttribute("data-bs-theme");
localStorage.setItem(themeStorageKey, 'light');
}
}
</script>
<header class="navbar navbar-expand-md d-print-none py-0">
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu"
aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
<a href="#">
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="40px">
</a>
<a href="#">
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</a>
</h1>
<% if(alert) { %>
<%- alert %>
<% } %>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item d-none d-md-flex me-3">
<!-- <div class="btn-list">
<a href="#" class="btn text-green">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" 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 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path> <path d="M8 11v-4a4 4 0 1 1 8 0v4"></path> </svg>
VPN
</a>
<a href="#" class="btn text-green">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-shield" 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 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"></path> </svg>
Firewall
</a>
<a href="#" class="btn text-green">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-screen-share" 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="M21 12v3a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10a1 1 0 0 1 1 -1h9"></path> <path d="M7 20l10 0"></path> <path d="M9 16l0 4"></path> <path d="M15 16l0 4"></path> <path d="M17 4h4v4"></path> <path d="M16 9l5 -5"></path> </svg>
VNC
</a>
</div> -->
<!-- <% if(role == 'admin') { %>
<div class="btn-list">
<a href="#" class="btn text-red">
Admin
</a>
</div>
<% } %> -->
</div>
<div class="d-none d-md-flex">
<button class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip"
data-bs-placement="bottom" value="dark-theme" onclick="toggleTheme(this)">
<!-- 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>
</button>
<button class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip"
data-bs-placement="bottom" value="light-theme" onclick="toggleTheme(this)">
<!-- 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>
</button>
<div class="nav-item dropdown d-none d-md-flex me-2">
<a href="#" class="nav-link px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="Show notifications">
<!-- Download SVG icon from http://tabler-icons.io/i/bell -->
<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="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" /> <path d="M9 17v1a3 3 0 0 0 6 0v-1" /> </svg>
<!-- <span class="badge bg-red"></span> -->
</a>
<div id="alert"></div>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item d-none d-md-flex">
<div class="btn-list">
HostButtons
<div class="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
<div class="card">
<div class="card-header">
<h3 class="card-title">Notifications</h3>
</div>
</div>
<div class="d-none d-md-flex mx-2">
<button class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom" value="dark-theme" onclick="toggleTheme(this)">
<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>
</button>
<button class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom" value="light-theme" onclick="toggleTheme(this)">
<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>
</button>
<div class="nav-item dropdown d-none d-md-flex">
<a class="nav-link px-0 text-muted" data-bs-toggle="dropdown" tabindex="-1" aria-label="Show notifications">
<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="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" /><path d="M9 17v1a3 3 0 0 0 6 0v-1" /></svg>
<!-- <span class="badge bg-red"></span> -->
</a>
<div class="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
<div class="card">
<div class="card-header">
<h3 class="card-title">Last updates</h3>
<div class="list-group list-group-flush list-group-hoverable">
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto"><span class="status-dot status-dot-animated bg-green d-block"></span></div>
<div class="col text-truncate">
<a href="#" class="text-body d-block">App Installed</a>
<div class="d-block text-muted text-truncate mt-n1">
Just an example of an app install notification.
</div>
</div>
<div class="list-group list-group-flush list-group-hoverable">
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto"><span class="status-dot status-dot-animated bg-red d-block"></span></div>
<div class="col text-truncate">
<a href="#" class="text-body d-block">Example 1</a>
<div class="d-block text-secondary text-truncate mt-n1">
Change deprecated html tags to text decoration classes (#29604)
</div>
</div>
<div class="col-auto">
<a href="#" class="list-group-item-actions">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted" 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>
</a>
</div>
</div>
</div>
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto"><span class="status-dot d-block"></span></div>
<div class="col text-truncate">
<a href="#" class="text-body d-block">Example 2</a>
<div class="d-block text-secondary text-truncate mt-n1">
justify-content:between ⇒ justify-content:space-between (#29734)
</div>
</div>
<div class="col-auto">
<a href="#" class="list-group-item-actions show">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-yellow" 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>
</a>
</div>
</div>
</div>
<div class="col-auto">
<a href="#" class="list-group-item-actions">
<!-- Download SVG icon from http://tabler-icons.io/i/star -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted" 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>
</a>
</div>
</div>
</div>
</div>
<!-- <button class="nav-link text-orange px-0" title="">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" fill="orange" stroke="currentColor" stroke-width="2" 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>
</button> -->
<!-- <button class="nav-link text-pink px-0" title="">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="pink" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-heart"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>
</button> -->
</div>
<div class="nav-item dropdown">
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar avatar-sm bg-green-lt">A</span>
<div class="d-none d-xl-block ps-2">
<label class="">Username</label>
<div class="mt-1 small text-secondary">Userrole</div>
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a href="/account" class="dropdown-item">Account</a>
<a class="dropdown-item text-muted">Notifications</a>
<!-- <div class="dropdown-divider"></div> -->
<a href="/preferences" class="dropdown-item">Preferences</a>
<a href="/settings" class="dropdown-item">Settings</a>
<a href="/logout" class="dropdown-item">Logout</a>
</div>
</div>
</div>
</div>
</header>
<header class="navbar-expand-md">
<div class="collapse navbar-collapse" id="navbar-menu">
<div class="navbar">
<div class="container-xl">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/dashboard -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dashboard" 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 13m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path> <path d="M13.45 11.55l2.05 -2.05"></path> <path d="M6.4 20a9 9 0 1 1 11.2 0z"></path> </svg>
</span>
<span class="nav-link-title">
Dashboard
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/images">
<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-augmented-reality" 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="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M12 12.5l4 -2.5" /><path d="M8 10l4 2.5v4.5l4 -2.5v-4.5l-4 -2.5z" /><path d="M8 10v4.5l4 2.5" /></svg>
</span>
<span class="nav-link-title">
Images
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/volumes">
<span
class="nav-link-icon d-md-none d-lg-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
</span>
<span class="nav-link-title">
Volumes
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/networks">
<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-world" 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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M3.6 9h16.8" /><path d="M3.6 15h16.8" /><path d="M11.5 3a17 17 0 0 0 0 18" /><path d="M12.5 3a17 17 0 0 1 0 18" /></svg>
</span>
<span class="nav-link-title">
Networks
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/apps">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/apps -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-apps" 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="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path> <path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path> <path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path> <path d="M14 7l6 0"></path> <path d="M17 4l0 6"></path> </svg>
</span>
<span class="nav-link-title">
Apps
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/users">
<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-user" 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="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path> <path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path> </svg>
</span>
<span class="nav-link-title">
Users
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/syslogs">
<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-file-text" 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="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /><path d="M9 9l1 0" /><path d="M9 13l6 0" /><path d="M9 17l6 0" /></svg>
</span>
<span class="nav-link-title">
Syslogs
</span>
</a>
</li>
</ul>
<div class="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">
<div class="card-actions btn-actions">
<input type="search" class="form-control mx-2" placeholder="Search..." aria-label="search" name="search" hx-post="/search" hx-trigger="input changed delay:500ms, search" hx-swap="none">
<button class="btn-action mx-1 text-muted" title="Grid View" data-hx-post="/dashboard/view" data-hx-trigger="mousedown" data-hx-target="#" name="grid" id="AppState" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler icons-tabler-outline icon-tabler-layout-grid"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /></svg>
</button>
<button class="btn-action mx-1 text-muted" title="List View" data-hx-post="/dashboard/view" data-hx-trigger="mousedown" data-hx-target="#" name="list" id="AppState" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler icons-tabler-outline icon-tabler-layout-grid"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l11 0" /><path d="M9 12l11 0" /><path d="M9 18l11 0" /><path d="M5 6l0 .01" /><path d="M5 12l0 .01" /><path d="M5 18l0 .01" /></svg>
</button>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" 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><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<form action="/dashboard/action/reset/000" method="post">
<button class="dropdown-item text-secondary" name="reset">Reset View</button>
</form>
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto"><span class="status-dot status-dot-animated bg-red d-block"></span></div>
<div class="col text-truncate">
<a href="#" class="text-body d-block">App Uninstalled</a>
<div class="d-block text-muted text-truncate mt-n1">
Just an example of an app uninstall notification.
</div>
</div>
<div class="col-auto">
<a href="#" class="list-group-item-actions">
<!-- Download SVG icon from http://tabler-icons.io/i/star -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted" 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>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
</div>
<div class="nav-item dropdown">
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar avatar-sm bg-green-lt"><%= avatar %></span></span>
<div class="d-none d-xl-block ps-2">
<div>
<%= name %>
</div>
<div class="mt-1 small text-muted">
<%= role %>
</div>
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a href="/account" class="dropdown-item">Account</a>
<a href="/settings" class="dropdown-item">Settings</a>
<!-- <div class="dropdown-divider"></div> -->
<a href="/logout" class="dropdown-item">Logout</a>
</div>
</div>
</div>
</div>
</header>
<header class="navbar-expand-md">
<div class="collapse navbar-collapse" id="navbar-menu">
<div class="navbar">
<div class="container-xl">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/dashboard -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dashboard" 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 13m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path> <path d="M13.45 11.55l2.05 -2.05"></path> <path d="M6.4 20a9 9 0 1 1 11.2 0z"></path> </svg>
</span>
<span class="nav-link-title">
Dashboard
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/images">
<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-augmented-reality" 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="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M12 12.5l4 -2.5" /><path d="M8 10l4 2.5v4.5l4 -2.5v-4.5l-4 -2.5z" /><path d="M8 10v4.5l4 2.5" /></svg>
</span>
<span class="nav-link-title">
Images
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/volumes">
<span
class="nav-link-icon d-md-none d-lg-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
</span>
<span class="nav-link-title">
Volumes
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/networks">
<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-world" 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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M3.6 9h16.8" /><path d="M3.6 15h16.8" /><path d="M11.5 3a17 17 0 0 0 0 18" /><path d="M12.5 3a17 17 0 0 1 0 18" /></svg>
</span>
<span class="nav-link-title">
Networks
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/apps">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/apps -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-apps" 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="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path> <path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path> <path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path> <path d="M14 7l6 0"></path> <path d="M17 4l0 6"></path> </svg>
</span>
<span class="nav-link-title">
Apps
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/users">
<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-user" 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="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path> <path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path> </svg>
</span>
<span class="nav-link-title">
Users
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/syslogs">
<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-file-text" 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="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /><path d="M9 9l1 0" /><path d="M9 13l6 0" /><path d="M9 17l6 0" /></svg>
</span>
<span class="nav-link-title">
Syslogs
</span>
</a>
</li>
</ul>
<div class="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">
<ul class="navbar-nav">
</ul>
</div>
</div>
</div>
</div>
</header>

View file

@ -1,155 +0,0 @@
<div class="accordion-user mb-3">
<h2 class="accordion-header" id="heading-Entry">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-Entry" aria-expanded="false">
<span class="avatar avatar-sm bg-green-lt col-3 text-start">JD</span>
<div class="col text-end" style="margin-right: 10px;">Username</div>
</button>
</h2>
<div id="collapse-Entry" class="accordion-collapse collapse" data-bs-parent="#accordion-example">
<div class="accordion-body pt-0">
<form id="updatePermissionsEntry">
<div class="col">
<input type="hidden" name="containerName" value="container_name">
<input type="hidden" name="containerID" value="container_id">
<input type="hidden" name="userID" value="user_id">
<input type="hidden" name="username" value="Username">
<input type="hidden" name="select" value="selectEntry">
<div class="mb-3">
<div class="mb-3">
<label class="row">
<span class="col">All</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" onclick="selectAll('selectEntry')" data-AllCheck>
</label>
</span>
</label>
</div>
<div class="divide-y-2">
<div>
</div>
<div>
<label class="row">
<span class="col">Uninstall</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="uninstall" data-UninstallCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Edit</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="edit" data-EditCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Upgrade</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="upgrade" data-UpgradeCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Start</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="start" data-StartCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Stop</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="stop" data-StopCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Pause</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="pause" data-PauseCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Restart</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="restart" data-RestartCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">Logs</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="logs" data-LogsCheck>
</label>
</span>
</label>
</div>
<div>
<label class="row">
<span class="col">View</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="selectEntry" value="view" data-ViewCheck>
</label>
</span>
</label>
</div>
<div>
</div>
</div>
</div>
<div class="row mb-2 pt-2">
<button class="btn" type="button" id="submit" hx-post="/dashboard/action/update_permissions/container_id" hx-swap="outerHTML">Update&nbsp;&nbsp;</button>
</div>
</div>
</form>
</div>
</div>
</div>

View file

@ -1,16 +1,13 @@
<div class="col-12 col-md-3 border-end">
<div class="card-body">
<h4 class="subheader">Menu</h4>
<div class="list-group list-group-transparent">
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Account</a>
<a class="list-group-item list-group-item-action d-flex align-items-center text-muted">Notifications</a>
<a href="/preferences" class="list-group-item list-group-item-action d-flex align-items-center">Preferences</a>
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
</div>
<h4 class="subheader mt-4">Special Thanks</h4>
<div class="list-group list-group-transparent">
<a href="/sponsors" class="list-group-item list-group-item-action">Sponsors</a>
<a href="/credits" class="list-group-item list-group-item-action">Credits</a>
</div>
</div>
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<h4 class="subheader">Menu</h4>
<div class="list-group list-group-transparent">
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Accounts</a>
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
</div>
<h4 class="subheader mt-4">Other</h4>
<div class="list-group list-group-transparent">
<a href="/supporters" class="list-group-item list-group-item-action">Supporters</a>
</div>
</div>
</div>

View file

@ -1,100 +0,0 @@
<div class="modal-content" id="modal_content">
<form action="/uninstall" method="POST" id="uninstall">
<input type="hidden" name="service_id" value="ContainerID">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-danger">
</div>
<div class="modal-body text-center py-3">
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
<h3 class="mb-3">Uninstall AppName?</h3>
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Volumes
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Image
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_image" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Networks
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Backups
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
</label>
</div>
</div>
</div>
</div>
<div class="mt-1"> </div>
<div class="text-muted">Enter "Yes" below to remove the container.</div>
<input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" form="uninstall" class="btn btn-danger w-100" value="Uninstall"/>
</div>
</div>
</div>
</div>
</form>
</div>

View file

@ -1,84 +0,0 @@
<form action="/users/action/change/USERID" method="post">
<div class="modal-header">
<h5 class="modal-title px-2">Username</h5>
<div class="me-auto badge badge-outline text-green">Active</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body py-2">
<div class="row mb-3 align-items-end">
<div class="col-auto">
<!-- <a href="#" class="avatar avatar-upload rounded">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 5l0 14"></path><path d="M5 12l14 0"></path></svg>
<span class="avatar-upload-text">Add</span>
</a> -->
<span class="avatar avatar-upload rounded bg-green-lt">J</span>
</div>
<div class="col">
<label class="form-label">Full Name</label>
<input type="text" class="form-control" name="name" value="FullName">
</div>
</div>
<div class="col-12 mb-3">
<label class="mb-1">Email: </label>
<input type="text" class="form-control" name="email" value="EmailAddress"/>
</div>
<div class="col-12 mb-3">
<label class="mb-1">UserID: </label>
<input type="text" class="form-control text-secondary" name="userID" value="USERID" readonly/>
</div>
<div class="row mb-3">
<div class="col-lg-6">
<label class="mb-1">Last Login: </label>
<input type="text" class="form-control" name="last_login" value="LastLogin"/>
</div>
<div class="col-lg-6">
<label class="mb-1">Created: </label>
<input type="text" class="form-control" name="created" value="CreatedAt"/>
</div>
</div>
<div class="col mb-2">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Reset Permissions
</a>
</div>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Close
</a>
</div>
<div class="col">
<button type="submit" name="change" value="remove" class="btn btn-danger w-100" hx-confirm="Are you sure you want to remove this account?">Remove</button>
</div>
<div class="col">
<button type="submit" name="change" value="disable" class="btn btn-secondary w-100" hx-confirm="Are you sure you want to disable this account?">Disable</button>
</div>
<div class="col">
<button type="submit" name="change" value="update" class="btn btn-primary w-100" hx-confirm="Update account?">Update</button>
</div>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,182 @@
<div class="accordion-item mb-3" style="border: 1px solid grey;">
<h2 class="accordion-header" id="heading-EntryNumber">
<button class="accordion-button collapsed row" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-EntryNumber" aria-expanded="false">
<span class="avatar avatar-sm bg-green-lt col-3 text-start">JD</span>
<div class="col text-end" style="margin-right: 10px;">PermissionsUsername</div>
</button>
</h2>
<div id="collapse-EntryNumber" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="">
<div class="">
<form id="updatePermissionsEntryNumber">
<div class="row mb-3">
<div class="col-9">
<label class="row text-start">
<span class="col">
All
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" onclick="selectAll('selectEntryNumber')">
</label>
</div>
</div>
<input type="hidden" name="user" value="PermissionsUsername">
<input type="hidden" name="container" value="PermissionsContainer">
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Uninstall
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="uninstall" data-UninstallCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Edit
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="edit" data-EditCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Upgrade
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="upgrade" data-UpgradeCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Start
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="start" data-StartCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Stop
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="stop" data-StopCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Pause
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="pause" data-PauseCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Restart
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="restart" data-RestartCheck>
</label>
</div>
</div>
<div class="row mb-2">
<div class="col-9">
<label class="row text-start">
<span class="col">
Logs
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="logs" data-LogsCheck>
</label>
</div>
</div>
<div class="row mb-4">
<div class="col-9">
<label class="row text-start">
<span class="col">
View
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="view" data-ViewCheck>
</label>
</div>
</div>
<div class="row mb-2">
<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissionsEntryNumber" hx-swap="outerHTML">Update  </button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

275
views/portal.html Normal file
View file

@ -0,0 +1,275 @@
<!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"/>
<script src="/js/htmx.min.js"></script>
<script src="/js/htmx-sse.js"></script>
<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('partials/navbar.html') %>
<div class="page-wrapper">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards" hx-ext="sse" sse-connect="/sse_event">
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="CPU" id="green">
<div class="font-weight-medium">
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
</div>
<div class="cpu-bar meter animate green">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="RAM" id="blue">
<div class="font-weight-medium">
<label class="ram-text mb-1" for="ram">RAM 0%</label>
</div>
<div class="ram-bar meter animate blue">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-purple text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" 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="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="NET" id="purple">
<div class="font-weight-medium">
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
</div>
<div class="ram-bar meter animate purple">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-orange text-white avatar">
<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>
<!-- HTMX -->
<div class="col" name="DISK" id="orange">
<div class="font-weight-medium">
<label class="disk-text mb-1" for="disk">DISK 0%</label>
</div>
<div class="meter animate orange">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" id="containers" data-hx-get="/user_containers" data-hx-trigger="load" data-hx-swap="innerHTML">
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" data-hx-get="/new_user_cards" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
</div>
</div>
<!-- HTMX Target-->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Loading</h5>
</div>
<div class="modal-body text-center">
<div class="spinner-border"></div>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card-body">
<h4>Logs:</h4>
<div id="logView">
<pre>No logs available</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" 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 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
<script src="/js/tabler.min.js"></script>
<script>
var options = {
chart: {
type: "line",
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [3, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: []
}, {
name: "RAM",
data: []
}],
tooltip: {
enabled: false
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
}
},
yaxis: {
min: 0,
max: 100,
labels: {
padding: 4
}
},
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}
</script>
<!-- SelectAll for the permissions modal -->
<script>
function selectAll(group) {
let checkboxes = document.getElementsByName(group);
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -1,102 +0,0 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Preferences - DweebUI.</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
</head>
<body >
<div class="page">
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<!-- EJS -->
<%- sidebar %>
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<!-- HTMX - Submits the form and replaces the target with the response. Replaces the submit button with "Updated" -->
<form id="preferences" action="/preferences" method="POST">
<h1 class="">Preferences</h1>
<label class="text-muted mb-3">User Preferences.</label>
<div class="table-group-divider mt-6"></div>
<h3 class="card-title mt-4">Profile Visibility</h3>
<p class="card-subtitle">Hide your username from the dashboard.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="hidden_input" <%= hide_profile %>>
<span class="form-check-label form-check-label-on">Hidden</span>
<span class="form-check-label form-check-label-off"><%= username %></span>
</label>
</div>
<div class="table-group-divider mt-4"></div>
<h3 class="card-title mt-4">Language</h3>
<p class="card-subtitle">In early development.</p>
<div class="row g-2">
<div class="col-3">
<select class="form-control form-select" name="language_input">
<option value="english" selected="" hidden="">English</option>
<%- selected %>
<option value="english">English</option>
<option value="chinese">Chinese</option>
</select>
</div>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<button class="btn btn-primary" id="submit" type="submit">
Update
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
</html>

View file

@ -1,27 +1,47 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Register - DweebUI</title>
<title>DweebUI - Register</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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 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.png" height="100" alt="Dweeb">
</a>
<form class="container container-tight py-4" action="/register" method="POST" novalidate>
<div class="text-center">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="100px">
</h1>
</div>
<form class="card card-md" action="/register" method="post" autocomplete="off" novalidate>
<div class="card-body">
<h2 class="card-title text-center mb-4">Create new account</h2>
<div class="text-center mb-4">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</h1>
</div>
<div class="card">
<div class="card-body text-center py-4">
<h1 class="mt-1">Welcome to DweebUI</h1>
<p class="text-muted">Account information is stored in a local sqlite database.</p>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
@ -29,73 +49,91 @@
</div>
<% } %>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name">
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" name="username">
</div>
<div class="mb-3">
<label class="form-label">Email address</label>
<input type="email" class="form-control" name="email">
</div>
</div>
<div class="card-body">
<div class="mb-3">
<div class="row row-cards">
<div class="col-sm-6 col-md-6">
<div class="mb-2">
<label class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name">
</div>
</div>
<div class="col-sm-6 col-md-6">
<div class="mb-2">
<label class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username">
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-2">
<label class="form-label">Password</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" autocomplete="off" name="password">
<input type="password" class="form-control" id="password" name="password" autocomplete="off">
</div>
</div>
<div class="mb-3">
<div class="mb-2">
<label class="form-label">Confirm Password</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" autocomplete="off" name="confirm">
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" autocomplete="off">
</div>
</div>
<div class="mb-2">
<label class="form-label" title="Enter the value of 'SECRET' from the DweebUI docker-compose.yaml">SECRET</label>
<input type="text" class="form-control" id="secret" name="secret">
</div>
<%- reg_secret %>
<!-- <div class="mb-3">
<!-- <div class="mb-2">
<label class="form-check">
<input type="checkbox" class="form-check-input"/>
<span class="form-check-label">Agree the <a href="./terms-of-service.html" tabindex="-1">terms and policy</a>.</span>
<input type="checkbox" class="form-check-input" name="warning"/>
<span class="form-check-label">
I understand that<a href="https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet"> exposing DweebUI directly to the internet</a> is a bad idea.
</span>
</label>
</div> -->
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">Create new account</button>
</div>
</div>
</form>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<button class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom" value="dark-theme" onclick="toggleTheme(this)">
<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>
</button>
<button class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom" value="light-theme" onclick="toggleTheme(this)">
<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>
</button>
</div>
<div class="text-center text-secondary">
Already have an account? <a href="/login" tabindex="-1">Login</a>
</div>
</div>
</div>
<div class="row align-items-center mt-2">
<div class="col">
<a href="/login">Login</a>
</div>
<div class="col">
<div class="btn-list justify-content-end">
<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">
<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">
<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>
<button type="submit" class="btn btn-primary">Register</button>
</div>
</div>
</div>
</form>
</div>
<script src="/js/dweebui.js" defer></script>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,292 +1,120 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Settings - DweebUI.</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
</head>
<body >
<div class="page">
<!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 - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Settings</h2>
<p class="text-muted mb-4">Configure server below</p>
<div class="row align-items-center">
<div class="col">
<!-- <a href="./QuickConnect.bat" class="btn" download="QuickConnect.bat">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-windows" 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="M17.8 20l-12 -1.5c-1 -.1 -1.8 -.9 -1.8 -1.9v-9.2c0 -1 .8 -1.8 1.8 -1.9l12 -1.5c1.2 -.1 2.2 .8 2.2 1.9v12.1c0 1.2 -1.1 2.1 -2.2 1.9z"></path> <path d="M12 5l0 14"></path> <path d="M4 12l16 0"></path> </svg>
Windows QuickConnect
</a> -->
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Full Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">First Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Last Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="" readonly="">
</div>
<div class="col-auto">
<a href="#" class="btn">Change</a>
</div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
<h3 class="card-title mt-4">Public profile</h3>
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
you.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" >
<span class="form-check-label form-check-label-on">You're currently visible</span>
<span class="form-check-label form-check-label-off">You're
currently invisible</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<!-- EJS -->
<%- sidebar %>
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<!-- HTMX - Submits the form and replaces the target with the response. Replaces the submit button with "Updated" -->
<form id="settings" hx-post="/settings/action/update" hx-target="#submit" hx-swap="outerHTML">
<h1 class="">Settings</h1>
<label class="text-muted mb-3">Configure server settings. Admin only.</label>
<div class="table-group-divider mt-6"></div>
<h3 class="card-title mt-4">User Registration</h3>
<p class="card-subtitle mb-3">Enable registration and choose a secret.</p>
<div class="row align-items-center">
<div class="col-auto">
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="user_registration" <%= user_registration %>>
<span class="form-check-label form-check-label-on text-success">
Enabled
</span>
<span class="form-check-label form-check-label-off text-danger">
Disabled
</span>
</label>
</div>
<div class="col-5">
<input type="text" class="form-control" name="registration_secret" placeholder="multiple-words-strong-passphrase" value="<%= registration_secret %>">
</div>
</div>
<div class="table-group-divider mt-3"></div>
<h3 class="card-title mt-4">Container Port Links</h3>
<p class="card-subtitle mb-3">Choose the base URL for the container card port links. Link can include 'http://' or 'https://'.</p>
<div class="row align-items-center">
<div class="col-auto">
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="custom_link" <%= custom_link %>>
<span class="form-check-label form-check-label-on text-warning">
Custom
</span>
<span class="form-check-label form-check-label-off text-success">
Localhost
</span>
</label>
</div>
<div class="col-5">
<input type="text" class="form-control" name="link_url" placeholder="IP Address or Domain" value="<%= link_url %>">
</div>
</div>
<div class="table-group-divider mt-3"></div>
<h3 class="card-title mt-4">Authentication</h3>
<p class="card-subtitle mb-3">Change authentication settings. Only the default, Username and Password, supports multiple users.</p>
<div class="row align-items-center">
<div class="col-auto">
<select class="form-select" name="authentication">
<option value="default">Username and Password - Default</option>
<option value="localhost">Localhost</option>
<option value="no_auth">Disabled - No Authentication</option>
</select>
</div>
</div>
<div class="table-group-divider mt-3"></div>
<h3 class="card-title mt-4">Default Language</h3>
<p class="card-subtitle">Default language for the server.</p>
<div class="row g-2">
<div class="col-3">
<select class="form-control form-select" name="language_input">
<option value="english" selected="" hidden="">English</option>
<%- selected %>
<option value="english">English</option>
<option value="chinese">Chinese</option>
</select>
</div>
<div class="col-auto">
<button class="btn" aria-label="button" name="check_languages" id="check_languages" value="true" hx-post="/update_languages" hx-swap="outerHTML" hx-target="#check_languages">Update Language Files</button>
</div>
</div>
<!-- <div class="table-group-divider mt-3"></div>
<h3 class="card-title mt-4">HTTP / HTTPS</h3>
<p class="card-subtitle">Requires restarting DweebUI.</p>
<div class="row g-2">
<div class="col-3">
<select class="form-control form-select" name="language_input">
<option value="HTTP" selected="" hidden="">HTTP</option>
<option value="HTTP">HTTP</option>
<option value="HTTPS">HTTPS</option>
</select>
</div>
<div class="col-auto">
<button class="btn" aria-label="button">Restart DweebUI</button>
</div>
</div>
<div class="table-group-divider mt-3"></div>
<h3 class="card-title mt-4">Port</h3>
<p class="card-subtitle">Requires restarting DweebUI.</p>
<div class="row g-2">
<div class="col-3">
<input type="text" class="form-control" name="port" placeholder="Port" value="8000">
</div>
<div class="col-auto">
<button class="btn" aria-label="button">Restart DweebUI</button>
</div>
</div> -->
<div class="table-group-divider mt-4"></div>
<h3 class="mt-4">Hosts</h3>
<label class="text-muted mb-2">Host #1</label>
<div class="row align-items-center">
<div class="col-auto">
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="host1" checked disabled>
<span class="form-check-label form-check-label-on text-success">
Enabled
</span>
<span class="form-check-label form-check-label-off text-danger">
Disabled
</span>
</label>
</div>
<div class="col-2">
<input type="text" class="form-control" placeholder="Host 1" readonly>
</div>
<div class="col-4">
<input type="text" class="form-control" placeholder="/var/run/docker.sock" readonly>
</div>
<div class="col-2">
<input type="text" class="form-control" readonly>
</div>
</div>
<label class="text-muted mb-2">Host #2</label>
<div class="row align-items-center">
<div class="col-auto">
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="host2" <%= host2_toggle %> >
<span class="form-check-label form-check-label-on text-success">
Enabled
</span>
<span class="form-check-label form-check-label-off text-danger">
Disabled
</span>
</label>
</div>
<div class="col-2">
<input type="text" class="form-control" name="tag2" value="<%= host2_tag %>" placeholder="Tag">
</div>
<div class="col-4">
<input type="text" class="form-control" name="ip2" value="<%= host2_ip %>" placeholder="Host IP">
</div>
<div class="col-2">
<input type="text" class="form-control" name="port2" value="<%= host2_port %>" placeholder="PORT">
</div>
</div>
<label class="text-muted mb-2">Host #3</label>
<div class="row align-items-center">
<div class="col-auto">
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="host3" <%= host3_toggle %> >
<span class="form-check-label form-check-label-on text-success">
Enabled
</span>
<span class="form-check-label form-check-label-off text-danger">
Disabled
</span>
</label>
</div>
<div class="col-2">
<input type="text" class="form-control" name="tag3" value="<%= host3_tag %>" placeholder="Tag">
</div>
<div class="col-4">
<input type="text" class="form-control" name="ip3" value="<%= host3_ip %>" placeholder="Host IP">
</div>
<div class="col-2">
<input type="text" class="form-control" name="port3" value="<%= host3_port %>" placeholder="PORT">
</div>
</div>
<label class="text-muted mb-2">Host #4</label>
<div class="row align-items-center">
<div class="col-auto">
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" name="host4" <%= host4_toggle %> >
<span class="form-check-label form-check-label-on text-success">
Enabled
</span>
<span class="form-check-label form-check-label-off text-danger">
Disabled
</span>
</label>
</div>
<div class="col-2">
<input type="text" class="form-control" name="tag4" value="<%= host4_tag %>" placeholder="Tag">
</div>
<div class="col-4">
<input type="text" class="form-control" name="ip4" value="<%= host4_ip %>" placeholder="Host IP">
</div>
<div class="col-2">
<input type="text" class="form-control" name="port4" value="<%= host4_port %>" placeholder="PORT">
</div>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<button class="btn btn-primary" id="submit">
Update
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
</html>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,87 +0,0 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Sponsors - DweebUI.</title>
<!-- CSS files -->
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
</head>
<body >
<div class="page">
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<!-- EJS -->
<%- sidebar %>
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<!-- HTMX - Submits the form and replaces the target with the response. Replaces the submit button with "Updated" -->
<form id="preferences" hx-post="/preferences" hx-target="#submit" hx-swap="outerHTML">
<h1 class="">Sponsors</h1>
<label class="text-muted mb-3">Some awesome people who have donated to DweebUI.</label>
<div class="table-group-divider mb-3"></div>
<span type="button" class="btn avatar avatar-xl bg-green-lt" hx-trigger="load, click" hx-post="/thank" hx-target="#count" name="MM" title="MM" style="margin-right: 5px;">MM</span>
<span type="button" class="btn avatar avatar-xl bg-azure-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="PD" title="PD" style="margin-right: 5px;">PD</span>
<span type="button" class="btn avatar avatar-xl bg-teal-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="C" title="C" style="margin-right: 5px;">C</span>
<span type="button" class="btn avatar avatar-xl bg-indigo-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="AD" title="AD" style="margin-right: 5px;">AD</span>
</div>
<!-- <div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<button class="btn btn-primary" id="submit">
Update
</button>
</div>
</div> -->
</div>
</form>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
</html>

85
views/supporters.html Normal file
View file

@ -0,0 +1,85 @@
<!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 - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Supporters</h2>
<p class="text-muted mb-4">[Click to Thank]</p>
<div class="row align-items-center">
<div class="col">
<span type="button" class="avatar avatar-md bg-green-lt" hx-trigger="load, click" hx-post="/thank" hx-target="#count" name="MM" title="MM" style="margin-right: 5px;">mm</span>
<span type="button" class="avatar avatar-md bg-cyan-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="PD" title="PD" style="margin-right: 5px;">pd</span>
</div>
</div>
</div>
<div class="card-body">
<p class="text-muted mb-4">Thanks counter:</p>
<div class="row align-items-center">
<div class="col">
<span class="avatar avatar-md bg-yellow-lt" id="count" style="margin-right: 5px;">0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,159 +1,93 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Syslogs - DweebUI</title>
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<title>DweebUI - Syslogs</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- EJS -->
<%- navbar %>
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body" style="margin-top: 16px;">
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Syslogs</h3>
<div class="card-options btn-list">
<!-- <a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" 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 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a> -->
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" 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 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll('select')"></th>
<th><label class="table-sort" data-sort="sort-id">ID</label></th>
<th><label class="table-sort" data-sort="sort-username">Username</label></th>
<th><label class="table-sort" data-sort="sort-uniqueid">Unique ID</label></th>
<th><label class="table-sort" data-sort="sort-event">Event</label></th>
<th><label class="table-sort" data-sort="sort-message">Message</label></th>
<th><label class="table-sort" data-sort="sort-ip">IP</label></th>
<th><label class="table-sort" data-sort="sort-timestamp">Timestamp</label></th>
<th><label class="table-sort" data-sort="sort-action">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">
<%- logs %>
</tbody>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/removeVolume">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Delete
</button>
</div>
</span> -->
<p class="m-0 text-muted ms-auto">Events</p>
</div>
</form>
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Volume</h5>
</div>
<div class="modal-body text-center">
<form method="post" action="/">
<div class="row g-2 align-items-end">
<div class="col-9">
<label class="form-label text-muted">Volume Name</label>
<input type="text" class="form-control" name="volume">
</div>
<div class="col-2">
<button type="submit" class="btn mt-2">Create</button>
</div>
</div>
<label class="mt-3 text-muted"><label class="text-danger">*</label>Name cannot contain spaces or special characters.</label>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Syslogs
</h2>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="card-body">
<div id="table-default" class="table-responsive">
<table class="table">
<thead>
<tr>
<th><button class="table-sort" data-sort="sort-id">id</button></th>
<th><button class="table-sort" data-sort="sort-user">user</button></th>
<th><button class="table-sort" data-sort="sort-email">email</button></th>
<th><button class="table-sort" data-sort="sort-event">event</button></th>
<th><button class="table-sort" data-sort="sort-message">message</button></th>
<th><button class="table-sort" data-sort="sort-ip">ip</button></th>
<th><button class="table-sort" data-sort="sort-datetime">date/time</button></th>
</tr>
</thead>
<tbody class="table-tbody">
<%- logs %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<script src="/js/dweebui.js" ></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-id', 'sort-username', 'sort-uniqueid', 'sort-event', 'sort-message', 'sort-ip', { attr: 'data-date', name: 'sort-timestamp' }, 'sort-quantity' ]
});
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-id', 'sort-email', 'sort-user', 'sort-event',
{ attr: 'data-date', name: 'sort-message' },
{ attr: 'data-progress', name: 'sort-datetime' },
'sort-ip'
]
});
})
</script>

View file

@ -1,164 +1,70 @@
<!doctype html>
<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
<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>Users - DweebUI</title>
<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
<link href="/css/dweebui.css" rel="stylesheet"/>
<title>DweebUI - Users</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- EJS -->
<%- navbar %>
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body" style="margin-top: 16px;">
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Users</h3>
<div class="card-options btn-list">
<!-- <a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" 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 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a> -->
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" 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 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll('select')"></th>
<th><label class="table-sort" data-sort="sort-id">ID</label></th>
<th><label class="table-sort" data-sort="sort-avatar">Avatar</label></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-username">Username</label></th>
<th><label class="table-sort" data-sort="sort-email">Email</label></th>
<th><label class="table-sort" data-sort="sort-userid">UserID</label></th>
<th><label class="table-sort" data-sort="sort-role">Role</label></th>
<th><label class="table-sort" data-sort="sort-lastlogin">Last Login</label></th>
<th><label class="table-sort" data-sort="sort-status">Status</label></th>
<th><label class="table-sort" data-sort="sort-action">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">
<%- user_list %>
</tbody>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Delete
</button>
</div>
</span> -->
<p class="m-0 text-muted ms-auto">Users</p>
</div>
<!-- New User Modal -->
<!-- <div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New User</h5>
</div>
<div class="modal-body text-center">
<div class="row g-2 align-items-end">
<div class="col-9">
<label class="form-label text-muted">User Name</label>
<input type="text" class="form-control" name="name">
</div>
<div class="col-2">
<button type="submit" class="btn mt-2">Create</button>
</div>
</div>
<label class="mt-3 text-muted"><label class="text-danger">*</label>Name cannot contain spaces or special characters.</label>
</div>
</div>
</div>
</div> -->
<div class="modal slim-modal modal-blur fade" id="scrolling_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content" id="modal_content">
<!-- modal content inserted with htmx -->
</div>
</div>
</div>
</div>
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Users
</h2>
</div>
</div>
</div>
</div>
<!-- EJS -->
<%- footer %>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<tbody>
<%- user_list %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js"></script>
<script src="/js/dweebui.js"></script>
<script src="/js/htmx.min.js"></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-id', 'sort-avatar', 'sort-name', 'sort-username', 'sort-email', 'sort-userid', 'sort-role', 'sort-lastlogin', 'sort-status']
});
})
</script>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

120
views/variables.html Normal file
View file

@ -0,0 +1,120 @@
<!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 - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Variables</h2>
<p class="text-muted mb-4">Configure default variables below.</p>
<!-- <div class="row align-items-center">
<div class="col">
</div>
</div> -->
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Match 1</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Match 2</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Default</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Match 1</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Match 2</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Default</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Match 1</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Match 2</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Default</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more