Compare commits

..

33 commits
main ... dev

Author SHA1 Message Date
lllllllillllllillll
4bdf1e3148 Almost a complete rewrite, part 5. Almost ready. 2024-10-27 17:55:40 -07:00
lllllllillllllillll
ec4746ebd3 Updated dependencies and CHANGELOG. Navbar tweaks. 2024-09-18 00:47:50 -07:00
lllllllillllllillll
e16492e462 Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2024-09-17 01:26:26 -07:00
lllllllillllllillll
5fb7878f59 Almost a complete rewrite, part 4. 2024-09-16 01:05:46 -07:00
lllllllillllllillll
596e8f1b7c Almost a complete rewrite, part 3. 2024-08-14 01:12:36 -07:00
lllllllillllllillll
00c31f0fb6 Almost a complete rewrite, part 2. 2024-08-10 00:57:22 -07:00
lllllllillllllillll
f113fa546b Fixed version information and footer 2024-07-29 21:00:40 -07:00
lllllllillllllillll
35255442a5 update ignore 2024-07-29 17:55:20 -07:00
lllllllillllllillll
622318c461 Almost a complete rewrite, part 1. 2024-07-29 17:49:04 -07:00
lllllllillllllillll
33e45a8bbf Refactored to stop spamming Docker API 2024-07-17 18:37:40 -07:00
lllllllillllllillll
c1d0a306a8 Hide ports if nothing exposed 2024-07-10 00:43:30 -07:00
lllllllillllllillll
b4f2b1f64f Improvements to permissions system 2024-07-10 00:16:01 -07:00
lllllllillllllillll
35e72e1b0d Remote hosts UI. Added search to router. 2024-06-29 01:22:35 -07:00
lllllllillllllillll
87fe9d4c65 App icons are now determined by service label. 2024-06-27 23:33:30 -07:00
lllllllillllllillll
31f5230f50 router cleanup and search test 2024-06-27 17:01:15 -07:00
lllllllillllllillll
32e5be4106 Fixed issue viewing logs on running containers 2024-06-27 15:33:03 -07:00
lllllllillllllillll
4e46052770 Added buttons to the navbar. some file cleanup. 2024-06-26 01:02:56 -07:00
lllllllillllllillll
8082b3e4e7 permissions fix for view. 2024-06-24 01:39:25 -07:00
lllllllillllllillll
926218ee0e Custom container links 2024-06-23 16:35:01 -07:00
lllllllillllllillll
cef9f7e7d7 User registration toggle in Settings page. 2024-06-23 16:08:33 -07:00
lllllllillllllillll
d30b2eefb8 Working settings page. 2024-06-23 01:13:09 -07:00
lllllllillllllillll
935cf1e995 disabled UI placeholders. working NO_AUTH. 2024-06-21 00:41:08 -07:00
lllllllillllllillll
aacc25cd15 functioning NO_AUTH env 2024-06-20 01:00:48 -07:00
lllllllillllllillll
6c15c28c2e Added pm2 to dockerfile. NO_AUTH env. 2024-06-19 18:28:48 -07:00
lllllllillllllillll
5e13288fc1 Added NO_AUTH env variable for localhost 2024-06-19 14:16:14 -07:00
lllllllillllllillll
6ed9360fa7 code clean up - networks.js 2024-06-18 23:19:23 -07:00
lllllllillllllillll
fc963b662d more try blocks - issue #87 2024-06-18 17:40:27 -07:00
lllllllillllllillll
c31131fef5 container link and registration switch saves to db 2024-06-18 00:57:04 -07:00
lllllllillllllillll
da9692bdb1 Added try block for images without RepoTags
Issue #85
2024-06-17 01:52:42 -07:00
lllllllillllllillll
874615502d New UI elements Grid view and List view. 2024-06-16 16:34:44 -07:00
lllllllillllllillll
53f0e2c5e9 fixed compose file volume, updated dependencies 2024-06-15 02:39:34 -07:00
lllllllillllllillll
61d3d54126 Fixes to app installer and new UI elements 2024-06-13 22:51:11 -07:00
lllllllillllllillll
fb38d3e4cd Bump systeminformation from 5.22.0 to 5.22.10 2024-06-09 14:28:05 -07:00
101 changed files with 73589 additions and 34848 deletions

View file

@ -1,8 +1,9 @@
**/db.sqlite
**/*.sqlite
**/node_modules
**/screenshots
.gitignore
.github
.git
Dockerfile
docker-compose.yaml
docker-compose.yaml
**/*.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@v6
uses: docker/build-push-action@v5
timeout-minutes: 30
with:
context: .

8
.gitignore vendored
View file

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

View file

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

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 lllllllillllllillll
Copyright (c) 2023-2024 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 Beta v0.60 ( :fire: Experimental :fire: )</h4>
<h4 align="center">DweebUI v0.70 ( :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,18 +15,20 @@
## Features
* [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
* [x] Multi-user support with permissions system.
* [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] Windows, Linux, and MacOS compatable.
* [x] Light/Dark Mode.
* [x] Mobile Friendly.
* [x] Manage your Docker networks, images, and volumes.
* [x] Easy to install app templates.
* [x] Docker Compose Support.
* [x] Easy to install app templates (Compatible with Portainer).
* [x] Docker Compose.
* [ ] Available updates without image pull (in development).
* [ ] 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
@ -37,7 +39,13 @@
## Setup
Docker Compose:
### 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:
```
version: "3.9"
services:
@ -46,7 +54,8 @@ services:
image: lllllllillllllillll/dweebui
environment:
PORT: 8000
SECRET: MrWiskers
HTTPS: false
NO_AUTH: false
restart: unless-stopped
ports:
- 8000:8000
@ -67,19 +76,15 @@ 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```.
Configuration:
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
[Troubleshooting](https://github.com/lllllllillllllillll/DweebUI/wiki/Troubleshooting)
* `PORT` - Specifies which port the service binds to on startup. Default is `8000`.
* `SECRET` - A shared secret used by the registration page.
## Credits
@ -92,4 +97,4 @@ Configuration:
## Supporters
* MM (Patreon)
* PD (Buymeacoffee)
* PD (Buymeacoffee)

1
appdata/compose/.gitignore vendored Normal file
View file

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

View file

@ -40,6 +40,24 @@
"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"
}
]
},
{
@ -1934,7 +1952,7 @@
"title": "Fail2ban",
"name": "fail2ban",
"note": "",
"description": "Fail2ban is a daemon to ban hosts that cause multiple authentication errors.",
"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.",
"platform": "linux",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/fail2ban.png",
"image": "linuxserver/fail2ban:latest",
@ -3123,7 +3141,7 @@
"Networking",
"Tools"
],
"description": "A clientless remote desktop gateway.",
"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.",
"image": "oznu/guacamole:latest",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/guacamole.png",
"name": "guacamole",
@ -5021,67 +5039,6 @@
],
"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"
@ -5106,7 +5063,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/Qballjos/portainer_templates/master/Images/adguard.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/Dashboard-Icons/main/png/adguard.png",
"name": "adguard",
"platform": "linux",
"ports": [
@ -5357,7 +5314,7 @@
"CMS"
],
"platform": "linux",
"logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/drupal.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/drupal.png",
"image": "drupal:latest",
"ports": [
"80/tcp"
@ -5519,7 +5476,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",

1
appdata/tmp/.gitignore vendored Normal file
View file

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

View file

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

View file

@ -1,20 +1,47 @@
import { User } from "../database/models.js";
import { User, ServerSettings } from '../db/config.js';
import { Alert, getLanguage, Navbar, Sidebar, Footer } from '../utils/system.js';
export const Account = async (req, res) => {
export const Account = async function(req,res){
let user = await User.findOne({ where: { UUID: req.session.UUID }});
req.session.host = `${req.params.host || 1}`;
res.render("account", {
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(),
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: '',
name: user.name,
username: req.session.username,
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",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
sidebar: await Sidebar(req),
footer: await Footer(req),
});
}
}

View file

@ -1,86 +1,100 @@
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 = '';
export const Apps = async (req, res) => {
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}`;
let [apps_list, app_count] = ['', ''];
let page = Number(req.params.page) || 1;
let template_param = req.params.template || 'default';
let template = 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 = '';
}
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;
// 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 = '';
}
}
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/');
app_count = compose_files.length;
last_page = Math.ceil(compose_files.length/28);
// 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;
}
}
compose_files.forEach(file => {
if (file == '.gitignore') { return; }
// 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 {
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 template_file = readFileSync(`appdata/templates/${template}.json`);
let templates = JSON.parse(template_file).templates;
templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
app_count = templates.length;
@ -88,11 +102,13 @@ export const Apps = async (req, res) => {
templates_global = templates;
apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
for (let i = list_start; i < list_end && i < templates_global.length; i++) {
let appCard = readFileSync('views/partials/app_card.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, 60) + "...";
// let desc = templates[i].description.slice(0, 75) + "...";
let desc = templates[i].description || "no description available";
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;
@ -109,143 +125,41 @@ export const Apps = async (req, res) => {
appCard = appCard.replace(/AppName/g, name);
appCard = appCard.replace(/AppTitle/g, title);
appCard = appCard.replace(/AppShortName/g, name);
appCard = appCard.replace(/AppDesc/g, desc);
appCard = appCard.replace(/AppLogo/g, logo);
appCard = appCard.replace(/AppDescription/g, desc);
appCard = appCard.replace(/AppIcon/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) {
@ -293,295 +207,231 @@ function CatagoryColor(category) {
}
}
export const InstallModal = async (req, res) => {
let input = req.header('hx-trigger-name');
let type = req.header('hx-trigger');
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}`);
}
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;
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
// console.log(`[submitApps] app_name: ${app_name} app_type: ${app_type} modal: ${modal}`);
for (let i = 0; i < 12; i++) {
// 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';
// 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] : "";
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] = [[], [], [], []];
// remove /tcp or /udp from port_external if it exists
if (port_external.includes("/")) {
port_external = port_external.split("/")[0];
}
for (let i = 0; i < 20; i++) {
ports_data.push({
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_protocol
});
} catch {
// 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: "",
external: "",
internal: "",
protocol: ""
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_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";
} catch {
ports_data.push({
check: "",
external: "",
internal: "",
protocol: ""
});
}
volumes_data.push({
check: volume_check,
bind: volume_bind,
container: volume_container,
readwrite: volume_readwrite
});
} catch {
volumes_data.push({
check: "",
bind: "",
container: "",
readwrite: ""
});
// 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: ""
});
}
}
// 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);
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(`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);
}
}
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);
}
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');
});
};
}

35
controllers/credits.js Normal file
View file

@ -0,0 +1,35 @@
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,360 +1,48 @@
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 { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
import { docker, containerInfo, containerLogs, GetContainerLists, containerStats, trigger_docker_event } from '../utils/docker.js';
import { readFileSync } from 'fs';
import { User, Permission, ServerSettings, ContainerLists, Container } from '../db/config.js';
import { Alert, Navbar, Footer, Capitalize } from '../utils/system.js';
import { Op } from 'sequelize';
let hidden = '';
let alert = '';
let [ cardList, newCards, stats ] = [ '', '', {}];
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
// The page
export const Dashboard = (req, res) => {
// Dashboard
export const Dashboard = async function (req, res) {
let name = req.session.user;
console.log(`[Dashboard] ${req.session.username}`);
let username = req.session.username;
let userID = req.session.userID;
let role = req.session.role;
alert = req.session.alert;
let host = req.session.host;
res.render("dashboard", {
name: name,
avatar: name.charAt(0).toUpperCase(),
// 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,
role: role,
alert: alert,
});
navbar: await Navbar(req),
footer: await Footer(req),
});
}
// 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 = '';
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'));
}
// Dashboard search
export const searchDashboard = async function (req, res) {
// console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}
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 Stats = async (req, res) => {
export const ServerMetrics = async (req, res) => {
let name = req.header('hx-trigger-name');
let color = req.header('hx-trigger');
let value = 0;
@ -377,72 +65,448 @@ export const Stats = 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);
}
// 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>
  ${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>`;
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;
}
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;
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;
}
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}}); }
}
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'
});
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;
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 () => {
});
}
// 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);
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>
</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;
}
}
// 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');
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>`);
}
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');
}
}

View file

@ -1,75 +1,31 @@
import { docker } from '../server.js';
import { addAlert } from './dashboard.js';
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { imageList, GetContainerLists } from '../utils/docker.js';
export const Images = async function(req, res) {
export const Images = async function(req,res){
let action = req.params.action;
req.session.host = `${req.params.host || 1}`;
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 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">`
let images = await imageList();
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' });
@ -77,34 +33,60 @@ export const Images = async function(req, res) {
size = size.toFixed(2);
let status = '';
if (container_images.includes(images[i].RepoTags[0])) {
status = 'In use';
}
try {
if (container_images.includes(images[i].RepoTags[0])) {
status = 'In use';
}
} catch {}
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">${images[i].Id}</td>
<td class="sort-city">${image_id}</td>
<td class="sort-score text-green">${status}</td>
<td class="sort-date" data-date="1628122643">${created}</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>
<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>
</tr>`
image_list += details;
}
image_list += `</tbody>`
res.render("images", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
image_list: image_list,
image_count: images.length,
res.render("images",{
alert: '',
username: req.session.username,
role: req.session.role,
image_count: '',
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",{
alert: '',
username: req.session.username,
role: req.session.role,
navbar: await Navbar(req),
footer: await Footer(req),
});
}

View file

@ -1,70 +1,74 @@
import { User, Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
import { User, Syslog, ServerSettings } from '../db/config.js';
export const Login = function(req,res){
if (req.session.user) { res.redirect("/logout"); }
else { res.render("login",{ "error":"", }); }
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 submitLogin = async function(req,res){
let { email, password } = req.body;
email = email.toLowerCase();
if (email && password) {
let existingUser = await User.findOne({ where: {email:email}});
if (existingUser) {
export const submitLogin = async function (req, res) {
let match = await bcrypt.compare(password,existingUser.password);
const { password } = req.body;
let email = req.body.email.toLowerCase();
if (match) {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
// If one of the fields is empty.
if (!email || !password) { res.render("login",{ "error": "Invalid credentials." }); return; }
req.session.user = existingUser.username;
req.session.UUID = existingUser.UUID;
req.session.role = existingUser.role;
req.session.avatar = existingUser.avatar;
let user = await User.findOne({ 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 {
// 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: null,
email: email,
event: "Bad Login",
message: "Invalid password",
ip: req.socket.remoteAddress
});
console.log(`${req.session.username} logged in`);
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.",
});
await Syslog.create({ username: user.username, uniqueID: email, event: "Login", message: "User logged in", ip: req.ip });
res.redirect("/dashboard");
return;
}
}
export const Logout = function(req,res){
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 });
req.session.destroy(() => {
res.redirect("/login");
});

View file

@ -1,44 +1,30 @@
import { docker } from '../server.js';
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { networkList, GetContainerLists, removeNetwork } from '../utils/docker.js';
export const Networks = async function(req, res) {
req.session.host = `${req.params.host || 1}`;
let container_networks = [];
// List all containers
let containers = await docker.listContainers({ all: true });
let network_name = '';
let containers = await GetContainerLists();
for (let i = 0; i < containers.length; i++) {
let network_name = containers[i].HostConfig.NetworkMode;
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID) } catch {}
try { network_name += containers[i].HostConfig.NetworkMode; } catch {}
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID); } catch {}
}
let networks = await networkList();
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">`
let network_list = '';
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 = '';
if (container_networks.includes(networks[i].Id)) {
status = `In use`;
}
// 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
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>
@ -46,44 +32,57 @@ 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="text-end"><a class="btn" href="#">Details</a></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>
</tr>`
// Add the row to the network list
network_list += details;
}
network_list += `</tbody>`
res.render("networks", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
network_list: network_list,
network_count: networks.length,
res.render("networks",{
alert: '',
username: req.session.username,
role: req.session.role,
network_count: '',
network_list: network_list,
navbar: await Navbar(req),
footer: await Footer(req),
});
}
export const NetworkAction = async function(req,res){
export const removeNetwork = 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
let networks = req.body.select;
if (typeof(networks) == 'string') {
networks = [networks];
}
// Make sure the value is an array
if (typeof(networks) == 'string') { networks = [networks]; }
// Loop through the array
for (let i = 0; i < networks.length; i++) {
if (networks[i] != 'on') {
try {
console.log(`Removing network: ${networks[i]}`);
let network = docker.getNetwork(networks[i]);
await network.remove();
} catch (error) {
await removeNetwork(networks[i]);
console.log(`Network removed: ${networks[i]}`);
}
catch {
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;
}

View file

@ -1,389 +0,0 @@
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

@ -0,0 +1,52 @@
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,104 +1,113 @@
import { User, Syslog, Permission } from '../database/models.js';
import bcrypt from 'bcrypt';
import bcrypt from "bcrypt";
import { Op } from "sequelize";
import { User, ServerSettings, Permission, Syslog } from "../db/config.js";
let SECRET = process.env.SECRET || "MrWiskers"
export const Register = function(req,res){
if(req.session.user){
res.redirect("/logout");
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,
});
} else {
res.render("register",{
"error":"",
res.render("login", {
"error": "User registration is disabled."
});
}
}
export const submitRegister = async function(req,res){
let { name, username, email, password, confirmPassword, secret } = req.body;
email = email.toLowerCase();
const { name, username, password, confirm, secret } = req.body;
let email = req.body.email.toLowerCase();
let registration_secret = await ServerSettings.findOne({ where: { key: 'registration_secret' }}).value;
if (secret != SECRET) {
const syslog = await Syslog.create({
user: username,
email: email,
event: "Failed Registration",
message: "Invalid secret",
ip: req.socket.remoteAddress
});
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((name && email && password && confirmPassword && username) && (secret == SECRET) && (password == confirmPassword)){
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 });
}
async function userRole () {
let userCount = await User.count();
if(userCount == 0){
return "admin";
}else{
return "user";
}
}
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>`}
let existingUser = await User.findOne({ where: {email:email}});
if(!existingUser){
res.render("register", {
"error": error,
"reg_secret": secret_input,
});
return;
}
try {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
const user = await User.create({
name: name,
username: username,
email: email,
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
lastLogin: newLogin,
});
// Returns 'admin' if no users have been created.
async function Role() {
if (await User.count() == 0) { return "admin"; }
else { return "user"; }
}
// 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);
// 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(),
});
if(match){
req.session.user = newUser.username;
req.session.UUID = newUser.UUID;
req.session.role = newUser.role;
// 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 });
const permission = await Permission.create({
user: newUser.username,
userID: newUser.UUID
});
console.log(`User ${username} created`);
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.",
});
}
res.redirect("/dashboard");
} else {
// Redirect to the signup page.
res.render("register",{
"error":"Please fill in all the fields.",
});
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" });
}
}
}

View file

@ -1,10 +1,252 @@
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 = (req, res) => {
export const Settings = async function(req,res){
res.render("settings", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
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,
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),
});
}
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;
}

50
controllers/sponsors.js Normal file
View file

@ -0,0 +1,50 @@
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;
}

View file

@ -1,31 +0,0 @@
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,7 +1,10 @@
import { Syslog } from '../database/models.js';
import { Syslog } from '../db/config.js';
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
export const Syslogs = async function(req, res) {
req.session.host = `${req.params.host || 1}`;
let logs = '';
const syslogs = await Syslog.findAll({
@ -10,28 +13,74 @@ 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-user">${log.user}</td>
<td class="sort-email">${log.email}</td>
<td class="sort-username">${log.username}</td>
<td class="sort-uniqueid">${uniqueID}</td>
<td class="sort-event">${log.event}</td>
<td class="sort-message">${log.message}</td>
<td class="sort-message">${message}</td>
<td class="sort-ip">${log.ip}</td>
<td class="sort-datetime">${datetime}</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>
</tr>`
}
res.render("syslogs", {
name: req.session.user || 'Dev',
role: req.session.role || 'Dev',
avatar: req.session.user.charAt(0).toUpperCase(),
logs: logs,
res.render("syslogs",{
alert: '',
username: req.session.username,
role: req.session.role,
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,21 +1,12 @@
import { User } from '../database/models.js';
import { User, Permission, ContainerLists, Container, ServerSettings } from '../db/config.js';
import { Alert, Navbar, Footer } from '../utils/system.js';
import { readFileSync } from 'fs';
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>`
export const Users = async function(req,res){
req.session.host = `${req.params.host || 1}`;
let user_list = '';
let allUsers = await User.findAll();
allUsers.forEach((account) => {
@ -30,33 +21,128 @@ export const Users = async (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"></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>
<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>
</tr>`
user_list += info;
});
res.render("users", {
name: req.session.user,
res.render("users",{
alert: '',
username: req.session.username,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
user_list: user_list,
alert: ''
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),
});
}
}
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');
};

View file

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

View file

@ -1,26 +1,15 @@
import { docker } from '../server.js';
import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
import { volumeList, GetContainerLists } from '../utils/docker.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 docker.listContainers({ all: true });
let containers = await GetContainerLists();
// Get the first 6 volumes from each container
for (let i = 0; i < containers.length; i++) {
@ -33,7 +22,7 @@ export const Volumes = async function(req, res) {
}
// List ALL volumes
let list = await docker.listVolumes({ all: true });
let list = await volumeList();
let volumes = list.Volumes;
// Create a table row for each volume
@ -57,63 +46,50 @@ 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="text-end"><a class="btn" href="#">Details</a></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>
</tr>`
volume_list += row;
}
volume_list += `</tbody>`
res.render("volumes", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
volume_list: volume_list,
volume_count: volumes.length,
res.render("volumes",{
alert: '',
username: req.session.username,
role: req.session.role,
volume_count: '',
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",{
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;
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]);
// }
// });
export const searchVolumes = async function (req, res) {
console.log(`[Search] ${req.body.search}`);
res.send('ok');
return;
}

View file

@ -1,256 +0,0 @@
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,
}
});

382
db/config.js Normal file
View file

@ -0,0 +1,382 @@
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,
},
});

43
languages/chinese.json Normal file
View file

@ -0,0 +1,43 @@
{
"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.60",
"version": "0.70.474",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dweebui",
"version": "0.60",
"version": "0.70.474",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.12",
"adm-zip": "^0.5.16",
"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.19.2",
"express-session": "^1.18.0",
"memorystore": "^1.6.7",
"express": "^4.21.1",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.1",
"sequelize": "^6.37.3",
"sequelize": "^6.37.5",
"sqlite3": "^5.1.7",
"systeminformation": "^5.22.9",
"yaml": "^2.4.2"
"systeminformation": "^5.23.5",
"yaml": "^2.6.0"
}
},
"node_modules/@balena/dockerignore": {
@ -48,7 +48,7 @@
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.2.1"
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
@ -113,17 +113,17 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"version": "22.5.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
"integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~6.19.2"
}
},
"node_modules/@types/validator": {
"version": "13.11.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz",
"integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg=="
"version": "13.12.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA=="
},
"node_modules/abbrev": {
"version": "1.1.1",
@ -143,11 +143,11 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.12",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
"integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"engines": {
"node": ">=6.0"
"node": ">=12.0"
}
},
"node_modules/agent-base": {
@ -250,9 +250,9 @@
}
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
@ -318,9 +318,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"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==",
"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.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@ -447,18 +447,6 @@
"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",
@ -471,12 +459,6 @@
"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",
@ -597,6 +579,20 @@
"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",
@ -622,9 +618,9 @@
}
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
@ -654,11 +650,11 @@
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "2.1.2"
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@ -820,9 +816,9 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"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==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
@ -912,36 +908,36 @@
}
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@ -953,11 +949,11 @@
}
},
"node_modules/express-session": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
"dependencies": {
"cookie": "0.6.0",
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
@ -970,6 +966,14 @@
"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",
@ -1034,12 +1038,12 @@
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@ -1155,6 +1159,7 @@
"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",
@ -1366,6 +1371,7 @@
"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"
@ -1428,9 +1434,9 @@
"optional": true
},
"node_modules/jake": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
"integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==",
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
@ -1467,12 +1473,15 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"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": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/make-dir": {
@ -1524,24 +1533,6 @@
"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",
@ -1550,22 +1541,13 @@
"node": ">= 0.6"
}
},
"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=="
"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/methods": {
"version": "1.1.2",
@ -1711,11 +1693,6 @@
"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",
@ -1728,11 +1705,6 @@
"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",
@ -1769,9 +1741,9 @@
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/multer": {
"version": "1.4.5-lts.1",
@ -1791,9 +1763,9 @@
}
},
"node_modules/nan": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
"integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
"optional": true
},
"node_modules/napi-build-utils": {
@ -1810,9 +1782,9 @@
}
},
"node_modules/node-abi": {
"version": "3.62.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz",
"integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==",
"version": "3.67.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz",
"integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==",
"dependencies": {
"semver": "^7.3.5"
},
@ -1953,9 +1925,12 @@
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"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"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -2019,9 +1994,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
},
"node_modules/pg-connection-string": {
"version": "2.6.4",
@ -2089,26 +2064,21 @@
"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.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
@ -2192,6 +2162,7 @@
"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"
},
@ -2227,9 +2198,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"bin": {
"semver": "bin/semver.js"
},
@ -2238,9 +2209,9 @@
}
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@ -2273,15 +2244,18 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"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/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/sequelize": {
"version": "6.37.3",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz",
"integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==",
"version": "6.37.5",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.5.tgz",
"integrity": "sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==",
"funding": [
{
"type": "opencollective",
@ -2348,14 +2322,14 @@
}
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
@ -2525,17 +2499,14 @@
}
},
"node_modules/sqlite3/node_modules/node-addon-api": {
"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"
}
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
},
"node_modules/ssh2": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
"integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
@ -2545,8 +2516,8 @@
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.9",
"nan": "^2.18.0"
"cpu-features": "~0.0.10",
"nan": "^2.20.0"
}
},
"node_modules/ssri": {
@ -2629,9 +2600,9 @@
}
},
"node_modules/systeminformation": {
"version": "5.22.9",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.9.tgz",
"integrity": "sha512-qUWJhQ9JSBhdjzNUQywpvc0icxUAjMY3sZqUoS0GOtaJV9Ijq8s9zEP8Gaqmymn1dOefcICyPXK1L3kgKxlUpg==",
"version": "5.23.5",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.5.tgz",
"integrity": "sha512-PEpJwhRYxZgBCAlWZhWIgfMTjXLqfcaZ1pJsJn9snWNfBW/Z1YQg1mbIUSWrEV3ErAHF7l/OoVLQeaZDlPzkpA==",
"os": [
"darwin",
"linux",
@ -2719,11 +2690,6 @@
"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",
@ -2787,9 +2753,9 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/unique-filename": {
"version": "1.1.1",
@ -2913,14 +2879,14 @@
}
},
"node_modules/yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"bin": {
"yaml": "bin.mjs"
},

View file

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

276
public/css/demo.css Normal file
View file

@ -0,0 +1,276 @@
/*!
* 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-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* 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

192
public/css/dweebui.css Normal file
View file

@ -0,0 +1,192 @@
@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 */
}

View file

@ -1,124 +0,0 @@
.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);
}

25813
public/css/tabler.css Normal file

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

@ -1,35 +0,0 @@
/*!
* 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-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* 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

View file

@ -1,6 +1,6 @@
/*!
* Tabler v1.0.0-beta19 (https://tabler.io)
* @version 1.0.0-beta19
* 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

53
public/js/dweebui.js Normal file
View file

@ -0,0 +1,53 @@
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,350 +6,285 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension('sse', {
htmx.defineExtension("sse", {
/**
* 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
/**
* 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;
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource
}
},
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
getSelectors: function() {
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
/**
* 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()
}
switch (name) {
return
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();
}
// Try to create EventSources when elements are processed
case 'htmx:afterProcessNode':
ensureEventSourceOnElement(parent)
}
}
})
return;
/// ////////////////////////////////////////////
// HELPER FUNCTIONS
/// ////////////////////////////////////////////
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(evt.target);
registerSSE(evt.target);
}
}
});
/**
* 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 })
}
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
/**
* 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
/**
* 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 });
}
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()
});
}
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
/**
* 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 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];
}
}
}
}
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;
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt)
})
/**
* 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
}
var swapSpec = api.getSwapSpecification(elt)
var target = api.getTarget(elt)
api.swap(target, content, swapSpec)
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
// 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;
}
})();
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null
}
})()

File diff suppressed because one or more lines are too long

7543
public/js/tabler.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

32956
public/libs/apexcharts/dist/apexcharts.js vendored Normal file

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

122
router.js Normal file
View file

@ -0,0 +1,122 @@
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');
}
});

View file

@ -1,106 +0,0 @@
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,48 +1,22 @@
import express from 'express';
import session from 'express-session';
import memorystore from 'memorystore';
import ejs from 'ejs';
import Docker from 'dockerode';
import { router } from './router/index.js';
import { sequelize } from './database/models.js';
export const docker = new Docker();
import { router } from './router.js';
import { sessionMiddleware } from './db/config.js';
// 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 () => {
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}`);
});
console.log(`\x1b[32mListening on http://localhost:${PORT}\x1b[0m`);
console.log('');
});

View file

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

View file

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

241
utils/docker.js Normal file
View file

@ -0,0 +1,241 @@
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,250 +1,239 @@
import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "fs";
import yaml from 'js-yaml';
import { execSync } from "child_process";
import { docker } from "../server.js";
import { Syslog } from "../db/config.js";
import { docker } from "./docker.js";
import DockerodeCompose from "dockerode-compose";
import { Syslog } from "../database/models.js";
import { addAlert } from "../controllers/dashboard.js";
import yaml from 'js-yaml';
import { Alert } from "./system.js";
// This entire page hurts to look at.
export const Install = async (req, res) => {
let data = req.body;
let name = data.name;
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 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;
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]}:`
}
}
}
mkdirSync(`./appdata/${name}`, { recursive: true });
if (req.body.compose) {
writeFileSync(`./appdata/${name}/compose.yaml`, compose_file, function (err) { console.log(err) });
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();
console.log(`Installing ${name}. It should appear on the dashboard shortly.`);
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 {
// composeInstall(name, compose, req);
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;
var compose = new DockerodeCompose(docker, `./appdata/${name}/compose.yaml`, `${name}`);
let ports = [port0, port1, port2, port3, port4, port5]
(async () => {
console.log('Pulling image');
await compose.pull();
console.log('Starting container');
await compose.up();
})();
let docker_volumes = [];
let alert = Alert('success', `Installing ${name}. It should appear on the dashboard shortly.`);
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('/');
res.send(alert);
}
// 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') }
// }
// }

60
utils/permissions.js Normal file
View file

@ -0,0 +1,60 @@
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}`);
}
}

174
utils/system.js Normal file
View file

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

View file

@ -1,131 +1,108 @@
<!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>
<!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>
</html>

View file

@ -1,29 +1,21 @@
<!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 - Apps</title>
<title>Apps - DweebUI</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>
<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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page header -->
@ -35,7 +27,7 @@
<div class="card">
<div class="card-body text-center">
<div class="d-flex align-items-center">
<div class="me-auto btn"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps</div>
<div class="me-auto btn"><%= app_count %></div>
<%- remove_button %>
</div>
</div>
@ -93,7 +85,7 @@
<%- json_templates %>
</ul>
</dropdown>
<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>
<button class="btn" name="import" data-bs-toggle="modal" data-bs-target="#import_modal">Import</button>
</div>
</div>
</div>
@ -121,28 +113,37 @@
<div class="container-xl">
<div class="row row-cards">
<%- apps_list %>
<!-- 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 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>
</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>
@ -152,7 +153,7 @@
<li class="page-item">
<a class="page-link" href="<%- next %>">
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
next
<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>
@ -161,13 +162,34 @@
</div>
</div>
<%- include('partials/footer.html') %>
<!-- EJS -->
<%- footer %>
</div>
</div>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<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>
</body>
</html>

89
views/credits.html Normal file
View file

@ -0,0 +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>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,280 +1,188 @@
<!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 - 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">
<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">
<%- include('partials/navbar.html') %>
<!-- 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>
<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>
</body>
</html>

View file

@ -1,31 +1,26 @@
<!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" 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>
<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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="page-body" style="margin-top: 16px;">
<div class="container-xl">
<div class="row row-deck row-cards">
@ -49,9 +44,23 @@
<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>
@ -115,16 +124,21 @@
</div>
</div>
<%- include('partials/footer.html') %>
<!-- EJS -->
<%- footer %>
</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" defer></script>
<script src="/js/demo.min.js" defer></script>
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
@ -140,20 +154,5 @@
})
</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,89 +1,117 @@
<!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 - Login</title>
<title>Login - DweebUI</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>
<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 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">
<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>
<a href="." class="navbar-brand navbar-brand-autodark">
<img src="/static/logo.png" height="100" alt="Dweeb">
</a>
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="h2 text-center mb-4">Login to your account</h2>
<form action="/login" method="POST" novalidate>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<div class="mb-2">
<form action="/login" method="post" autocomplete="off" novalidate>
<div class="mb-3">
<label class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email">
<input name="email" type="email" class="form-control" autocomplete="off">
</div>
<div class="mb-2">
<label class="form-label">
Password
</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" id="password" name="password" autocomplete="off">
</div>
</div>
<div class="mb-2 d-flex justify-content-between">
<label class="form-check">
<input type="checkbox" class="form-check-input" checked=""/>
<span class="form-check-label">
Remember me
<span class="form-label-description">
<!-- <a href="./forgot-password.html">I forgot password</a> -->
</span>
</label>
<span>
<a href="#">Forgot password</a>
</span>
<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> -->
</div>
</div>
<!-- <div class="mb-2">
<label class="form-check">
<input type="checkbox" class="form-check-input"/>
<span class="form-check-label">Remember me on this device</span>
</label>
</div> -->
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">Sign in</button>
<button type="submit" class="btn btn-primary w-100">Login</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="text-center text-muted mt-2">
Don't have account? <a href="/register" tabindex="-1">Register</a>
<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>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
</html>

View file

@ -1,20 +0,0 @@
<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>

View file

@ -1,728 +0,0 @@
<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

@ -1,13 +0,0 @@
<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

@ -1,22 +0,0 @@
<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

@ -1,77 +0,0 @@
<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,31 +1,27 @@
<!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>
<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>
<!-- 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">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="page-body" style="margin-top: 16px;">
<div class="container-xl">
<div class="row row-deck row-cards">
@ -48,16 +44,28 @@
<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="/removeNetwork">Remove</button>
<button class="btn" type="submit" formaction="/network/remove/">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
@ -87,16 +95,21 @@
</div>
</div>
<%- include('partials/footer.html') %>
<!-- EJS -->
<%- footer %>
</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" defer></script>
<script src="/js/demo.min.js" defer></script>
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
@ -112,20 +125,6 @@
})
</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,22 +0,0 @@
<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

@ -0,0 +1,46 @@
<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

@ -1,80 +0,0 @@
<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

@ -1,71 +0,0 @@
<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

@ -0,0 +1,127 @@
<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

@ -1,75 +1,83 @@
<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 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>
<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>
</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="report-type" value="1" class="form-selectgroup-input">
<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="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
</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>
</span>
</label>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="report-type" class="form-selectgroup-input">
<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">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
<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>
</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">
@ -82,20 +90,21 @@
<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="port_0_check" type="checkbox" Port0Check>
<input class="form-check-input" name="port0" 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>
@ -106,7 +115,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_1_check" type="checkbox" Port1Check>
<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"/>
@ -125,7 +134,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_2_check" type="checkbox" Port2Check>
<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"/>
@ -145,7 +154,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_3_check" type="checkbox" Port3Check>
<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"/>
@ -165,7 +174,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_4_check" type="checkbox" Port4Check>
<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"/>
@ -184,7 +193,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_5_check" type="checkbox" Port5Check>
<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"/>
@ -216,7 +225,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="volume_0_check" type="checkbox" Vol0Check>
<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"/>
@ -235,7 +244,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_1_check" type="checkbox" Vol1Check>
<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"/>
@ -254,7 +263,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_2_check" type="checkbox" Vol2Check>
<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"/>
@ -273,7 +282,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_3_check" type="checkbox" Vol3Check>
<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"/>
@ -292,7 +301,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_4_check" type="checkbox" Vol4Check>
<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"/>
@ -311,7 +320,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_5_check" type="checkbox" Vol5Check>
<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"/>
@ -570,52 +579,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">
@ -799,158 +808,35 @@
</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>
</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>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<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>
</div>
</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/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://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://www.buymeacoffee.com/lllllllillllllillll" target="_blank" class="link-secondary" rel="noopener">
<a href="https://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; 2023 - 2024
Copyright &copy; 2024
<a href="https://dweebui.com" class="link-secondary">DweebUI</a>.
All rights reserved.
</li>
<li class="list-inline-item">
<a href="https://github.com/lllllllillllllillll/DweebUI/releases" class="link-secondary" rel="noopener">
v0.60
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/CHANGELOG.md" class="link-secondary" rel="noopener">
v0.7X (Extra Broken) [Build BuildVersion]
</a>
</li>
</ul>

9
views/partials/info.html Normal file
View file

@ -0,0 +1,9 @@
<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>

842
views/partials/install.html Normal file
View file

@ -0,0 +1,842 @@
<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>

31
views/partials/link.html Normal file
View file

@ -0,0 +1,31 @@
<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>

43
views/partials/logs.html Normal file
View file

@ -0,0 +1,43 @@
<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,278 +1,251 @@
<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
<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>
</div>
<% } %> -->
<a href="#">
<img src="/static/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</a>
</h1>
</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 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 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>
<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="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>
<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 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>
</div>
</div>
<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>
<!-- <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>
</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 %>
</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>
</div>
</div>
</div>
</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>
</header>

View file

@ -0,0 +1,155 @@
<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,13 +1,16 @@
<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 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>

View file

@ -0,0 +1,100 @@
<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>

84
views/partials/user.html Normal file
View file

@ -0,0 +1,84 @@
<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

@ -1,182 +0,0 @@
<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>

View file

@ -1,275 +0,0 @@
<!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>

102
views/preferences.html Normal file
View file

@ -0,0 +1,102 @@
<!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,47 +1,27 @@
<!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 - Register</title>
<title>Register - DweebUI</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>
<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 class="d-flex flex-column">
<script src="/js/demo-theme.js"></script>
<div class="page page-center">
<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>
<div class="container container-tight py-4">
<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>
<a href="." class="navbar-brand navbar-brand-autodark">
<img src="/static/logo.png" height="100" alt="Dweeb">
</a>
</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>
<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>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
@ -49,91 +29,73 @@
</div>
<% } %>
</div>
<div class="card-body">
<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 class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name">
</div>
<div class="mb-2">
<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" id="email" name="email">
<input type="email" class="form-control" name="email">
</div>
<div class="mb-2">
<div class="mb-3">
<label class="form-label">Password</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" id="password" name="password" autocomplete="off">
<input type="password" class="form-control" autocomplete="off" name="password">
</div>
</div>
<div class="mb-2">
<div class="mb-3">
<label class="form-label">Confirm Password</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" autocomplete="off">
<input type="password" class="form-control" autocomplete="off" name="confirm">
</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>
<!-- <div class="mb-2">
<%- reg_secret %>
<!-- <div class="mb-3">
<label class="form-check">
<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>
<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>
</label>
</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 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>
</form>
</div>
</div>
<script src="/js/dweebui.js" defer></script>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script src="/js/tabler.min.js?1692870487" defer></script>
<script src="/js/demo.min.js?1692870487" defer></script>
</body>
</html>

View file

@ -1,120 +1,292 @@
<!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>
<!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">
<%- 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>
<!-- 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>

87
views/sponsors.html Normal file
View file

@ -0,0 +1,87 @@
<!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>

View file

@ -1,85 +0,0 @@
<!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,93 +1,159 @@
<!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 - 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>
<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"/>
</head>
<body >
<div class="page">
<%- include('partials/navbar.html') %>
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<div class="page-header d-print-none">
<!-- Page header -->
<!-- Page body -->
<div class="page-body" style="margin-top: 16px;">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Syslogs
</h2>
</div>
</div>
</div>
</div>
<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 %>
<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>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
<!-- EJS -->
<%- footer %>
</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" defer></script>
<script src="/js/demo.min.js" defer></script>
<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-email', 'sort-user', 'sort-event',
{ attr: 'data-date', name: 'sort-message' },
{ attr: 'data-progress', name: 'sort-datetime' },
'sort-ip'
]
});
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' ]
});
})
</script>

View file

@ -1,70 +1,164 @@
<!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 - 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>
<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"/>
</head>
<body >
<div class="page">
<%- include('partials/navbar.html') %>
<!-- EJS -->
<%- navbar %>
<div class="page-wrapper">
<div class="page-header d-print-none">
<!-- Page header -->
<!-- Page body -->
<div class="page-body" style="margin-top: 16px;">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Users
</h2>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<tbody>
<%- user_list %>
</tbody>
</table>
<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>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
<!-- EJS -->
<%- footer %>
</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" defer></script>
<script src="/js/demo.min.js" defer></script>
<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>
</body>
</html>

View file

@ -1,120 +0,0 @@
<!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