Merge pull request #60 from lllllllillllllillll/dev

v0.40
This commit is contained in:
lllllllillllllillll 2024-02-26 15:59:24 -08:00 committed by GitHub
commit c7d79b296c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 14715 additions and 4996 deletions

5
.dockerignore Normal file
View file

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

11
.gitignore vendored
View file

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

View file

@ -1,3 +1,27 @@
## v0.40 (Feb 26th 2024) - HTMX rewrite
* Pages rewritten to use HTMX.
* Removed Socket.io.
* Changed view files to *.HTML instead of *.EJS.
* Removed "USER root" from Dockerfile.
* Express sessions configured to use memorystore.
* Improved chart rendering.
* Improvements to container charts.
* Created Variables page.
* Created Supporters page.
* Ability to remove images, volumes, or networks.
* Fixed list.js sorting.
* Fixed apps.js page navigation.
* Removed stackfiles from templates.json and updated some icons.
* New logo.
* Improved handling of Docker events.
* Improved dashboard responsiveness.
* Updated server metrics styles.
* Container cards display pending action.
* Container charts only rendered if container running.
* Created permissions modal.
* Podman support (untested).
* Started a new template for FOSS apps.
## v0.20 (Jan 20th 2024) - The rewrite. Jumping all the way to v0.20.
* Changed to ES6 imports.
* Cleaned up file structure and code layout.

View file

@ -1,19 +1,7 @@
FROM node:21-alpine
ENV NODE_ENV=production
WORKDIR /app
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev
USER root
COPY . .
COPY . /app
RUN npm install
EXPOSE 8000
CMD ["node", "server.js"]
CMD node server.js

View file

@ -1,7 +1,7 @@
# DweebUI
DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
Alpha v0.20 ( :fire: Experimental :fire: )
Alpha v0.40 ( :fire: Experimental :fire: )
[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
@ -10,7 +10,7 @@ Alpha v0.20 ( :fire: Experimental :fire: )
[![GitHub Activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![Docker Pulls](https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui)](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
[![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
[![GitHub License](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
[![Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
* This is a personal project I started to get more familiar with Javascript and Node.js.
* Some UI elements are placeholders and every version may have breaking changes.
@ -31,8 +31,9 @@ Alpha v0.20 ( :fire: Experimental :fire: )
* [x] Dashboard provides server metrics, container metrics, and container controls, on a single page.
* [x] View container logs.
* [ ] Update containers (planned).
* [ ] Manage your Docker networks, images, and volumes (in development).
* [x] Manage your Docker networks, images, and volumes.
* [x] Light/Dark Mode.
* [x] Mobile Friendly.
* [x] Easy to install app templates.
* [x] Multi-User built-in.
* [ ] Permissions system (in development).
@ -41,6 +42,7 @@ Alpha v0.20 ( :fire: Experimental :fire: )
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [x] Automatically persists data in docker volumes if bind mount isn't used.
* [ ] Preset variables (planned).
* [ ] Themes (planned).
## Setup
@ -51,9 +53,8 @@ version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.20
image: lllllllillllllillll/dweebui:v0.40
environment:
NODE_ENV: production
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
@ -61,7 +62,11 @@ services:
- 8000:8000
volumes:
- dweebui:/app
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket
#- /run/podman/podman.sock:/var/run/docker.sock
networks:
- dweebui_net
@ -91,4 +96,5 @@ Compose setup:
## Supporters
* MM (Patreon)
* MM (Patreon)
* PD (Buymeacoffee)

View file

@ -1,32 +1,29 @@
// export for app.js
export const containerCard = (data) => {
let { name, service, state, external_port, internal_port, ports, link } = data;
let wrapped = name;
let chart = name;
let disable = "";
let chartName = name.replace(/-/g, '');
if (name.length > 13) {
wrapped = name.slice(0, 10) + '...';
}
//disable controls for a docker container depending on its name
let actions = "";
if (name.startsWith('dweebui')) {
actions = 'disabled=""';
}
// shorten long names
if (name.length > 13) { wrapped = name.slice(0, 10) + '...'; }
// disable buttons for dweebui
if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
if ( external_port == undefined ) { external_port = 0; }
if ( internal_port == undefined ) { internal_port = 0; }
let state_indicator = 'green';
if (state == 'exited') {
state = 'stopped';
state_indicator = 'red';
state = 'stopped';
state_indicator = 'red';
} else if (state == 'paused') {
state_indicator = 'orange';
state_indicator = 'orange';
}
let noChart = 'hx-swap="none"';
if (state == 'running') { noChart = ''; }
let ports_data = [];
if (ports) {
ports_data = ports;
@ -49,7 +46,7 @@ export const containerCard = (data) => {
return `
<div class="col-sm-6 col-lg-3 deleteme">
<div class="col-sm-6 col-lg-3 pt-1">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
@ -60,16 +57,16 @@ export const containerCard = (data) => {
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button onclick="clicked(this)" name="${name}" value="${state}" id="start" class="btn-action" title="Start" ${actions}><!-- player-play -->
<button class="btn-action" title="Start" data-hx-post="/start" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- player-play -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="stop" class="btn-action" title="Stop" ${actions}><!-- player-stop -->
<button class="btn-action" title="Stop" data-hx-post="/stop" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- player-stop -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="pause" class="btn-action" title="Pause" ${actions}><!-- player-pause -->
<button class="btn-action" title="Pause" data-hx-post="/pause" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- player-pause -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="restart" class="btn-action" title="Restart" ${actions}><!-- reload -->
<button class="btn-action" title="Restart" data-hx-post="/restart" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- reload -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
</button>
<div class="dropdown">
@ -77,11 +74,11 @@ export const containerCard = (data) => {
<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" onclick="clicked(this)" id="details" data-bs-toggle="modal" data-bs-target="#details_modal" href="#">Details</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="logs" data-bs-toggle="modal" data-bs-target="#log_view" href="#">Logs</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="edit" href="#">Edit</button>
<button class="dropdown-item text-primary" onclick="clicked(this)" name="${name}" id="update" href="#">Update</button>
<button class="dropdown-item text-danger" onclick="clicked(this)" name="${name}" id="remove" data-bs-toggle="modal" data-bs-target="#${name}_uninstall_modal" href="#">Remove</button>
<button class="dropdown-item text-secondary" name="${name}" data-hx-get="/modal" data-hx-target="#modals-here" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
<button class="dropdown-item text-secondary" name="${name}" id="logs" data-hx-get="/logs" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
<button class="dropdown-item text-secondary" name="${name}" id="edit">Edit</button>
<button class="dropdown-item text-primary" name="${name}" id="update" disabled="">Update</button>
<button class="dropdown-item text-danger" name="${name}" id="remove" data-bs-toggle="modal" data-bs-target="#${name}_uninstall_modal" href="#">Remove</button>
</div>
</div>
<div class="dropdown">
@ -89,9 +86,9 @@ export const containerCard = (data) => {
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="resetView" value="resetView">Reset View</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="permissions" value="permissions" data-bs-toggle="modal" data-bs-target="#${name}_permissions">Permissions</button>
<button class="dropdown-item text-secondary" data-hx-post="/hide" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" data-hx-post="/reset" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="reset" value="reset">Reset View</button>
<button class="dropdown-item text-secondary" name="${name}" id="permissions" data-hx-get="/modal" data-hx-target="#modals-here" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
</div>
</div>
</div>
@ -105,13 +102,27 @@ export const containerCard = (data) => {
</a>
</div>
<div class="ms-auto">
<span class="text-${state_indicator} 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>
<label id="${name}state">
<span class="text-${state_indicator} 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>
</label>
</div>
</div>
<div id="${chart}_chart" class="chart-sm"></div>
<script>
var ${chartName}chart = new ApexCharts(document.querySelector("#${chartName}_chart"), options);
</script>
<div class="chart-sm">
<div id="${chartName}_chart" data-hx-trigger="load, every 2s" data-hx-get="/chart" name="${chartName}" ${noChart}>
<script>
${chartName}chart.render();
</script>
</div>
</div>
</div>
</div>
</div>
@ -206,18 +217,35 @@ export const containerCard = (data) => {
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<div class="d-flex align-items-center">
User:
<select class="form-select ms-2">
<option value="john_doe" selected hidden>John Doe</option>
<option value="john_doe">John Doe</option>
<option value="jane_doe">Jane Doe</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">Install</span>
<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="remove_volumes">
<input class="form-check-input" type="checkbox" name="remove_image">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
@ -232,6 +260,7 @@ export const containerCard = (data) => {
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">

893
components/modal.js Normal file
View file

@ -0,0 +1,893 @@
export const modal = (data) => {
let { name, state, image } = data;
let ports_data = [];
for (let i = 0; i < 12; i++) {
let port_check = "checked";
let external = i;
let internal = i;
let protocol = "tcp";
ports_data.push({
check: port_check,
external: external,
internal: internal,
protocol: protocol
});
}
let volumes_data = [];
for (let i = 0; i < 12; i++) {
let vol_check = "checked";
let bind = i;
let container = i;
let readwrite = "rw";
volumes_data.push({
check: vol_check,
bind: bind,
container: container,
readwrite: readwrite
});
}
let env_data = [];
for (let i = 0; i < 12; i++) {
let env_check = "checked";
let env_name = i;
let env_default = i;
env_data.push({
check: env_check,
name: env_name,
default: env_default
});
}
let label_data = [];
for (let i = 0; i < 12; i++) {
let label_check = "checked";
let label_name = i;
let label_default = i;
label_data.push({
check: label_check,
name: label_name,
value: label_default
});
}
let modal = `
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Details</h5>
</div>
<div class="modal-body">
<pre class="text-secondary">note</pre>
<form action="" id="details_modal" 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="${name}" hidden/>
<input type="text" class="form-control" name="name" value="${name}"/>
</div>
<div class="col-lg-3">
<label class="form-label">Image: </label>
<input type="text" class="form-control" name="image" value="${image}"/>
</div>
<div class="col-lg-3">
<label class="form-label">Restart Policy: </label>
<select class="form-select" name="restart_policy" value="">
<option value="1">unless-stopped</option>
<option value="2">on-failure</option>
<option value="3">never</option>
<option value="4">always</option>
</select>
</div>
</div>
<label class="form-label">Network Mode</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="report-type" value="1" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">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="report-type" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="report-type" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
</span>
</span>
</label>
</div>
</div>
<div class="accordion" id="modal-accordion">
<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="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="port_5_check" type="checkbox" ${ports_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_external" value="${ports_data[5].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_internal" value="${ports_data[5].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_5_protocol">
<option value="${ports_data[5].protocol}" selected hidden>${ports_data[5].protocol}</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="volume_0_check" type="checkbox" ${volumes_data[0].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_bind" value="${volumes_data[0].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_container" value="${volumes_data[0].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_0_readwrite">
<option value="${volumes_data[0].readwrite}" selected hidden>${volumes_data[0].readwrite}</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="volume_1_check" type="checkbox" ${volumes_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_bind" value="${volumes_data[1].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_container" value="${volumes_data[1].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_1_readwrite">
<option value="${volumes_data[1].readwrite}" selected hidden>${volumes_data[1].readwrite}</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="volume_2_check" type="checkbox" ${volumes_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_bind" value="${volumes_data[2].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_container" value="${volumes_data[2].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_2_readwrite">
<option value="${volumes_data[2].readwrite}" selected hidden>${volumes_data[2].readwrite}</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="volume_3_check" type="checkbox" ${volumes_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_bind" value="${volumes_data[3].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_container" value="${volumes_data[3].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_3_readwrite">
<option value="${volumes_data[3].readwrite}" selected hidden>${volumes_data[3].readwrite}</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="volume_4_check" type="checkbox" ${volumes_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_bind" value="${volumes_data[4].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_container" value="${volumes_data[4].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_4_readwrite">
<option value="${volumes_data[4].readwrite}" selected hidden>${volumes_data[4].readwrite}</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="volume_5_check" type="checkbox" ${volumes_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_bind" value="${volumes_data[5].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_container" value="${volumes_data[5].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_5_readwrite">
<option value="${volumes_data[5].readwrite}" selected hidden>${volumes_data[5].readwrite}</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" ${env_data[0].check}>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="env_0_name" value="${env_data[0].name}"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="env_0_default" value="${env_data[0].default}"/>
</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" ${env_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_name" value="${env_data[1].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_default" value="${env_data[1].default}"/>
</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" ${env_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_name" value="${env_data[2].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_default" value="${env_data[2].default}"/>
</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" ${env_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_name" value="${env_data[3].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_default" value="${env_data[3].default}"/>
</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" ${env_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_name" value="${env_data[4].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_default" value="${env_data[4].default}"/>
</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" ${env_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_name" value="${env_data[5].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_default" value="${env_data[5].default}"/>
</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" ${env_data[6].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_name" value="${env_data[6].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_default" value="${env_data[6].default}"/>
</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" ${env_data[7].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_name" value="${env_data[7].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_default" value="${env_data[7].default}"/>
</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" ${env_data[8].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_name" value="${env_data[8].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_default" value="${env_data[8].default}"/>
</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" ${env_data[9].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_name" value="${env_data[9].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_default" value="${env_data[9].default}"/>
</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" ${env_data[10].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_name" value="${env_data[10].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_default" value="${env_data[10].default}"/>
</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" ${env_data[11].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_name" value="${env_data[11].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_default" value="${env_data[11].default}"/>
</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" ${label_data[0].check}>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="label_0_name" value="${label_data[0].name}"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="label_0_value" value="${label_data[0].value}"/>
</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" ${label_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_name" value="${label_data[1].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_value" value="${label_data[1].value}"/>
</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" ${label_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_name" value="${label_data[2].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_value" value="${label_data[2].value}"/>
</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" ${label_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_name" value="${label_data[3].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_value" value="${label_data[3].value}"/>
</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" ${label_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_name" value="${label_data[4].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_value" value="${label_data[4].value}"/>
</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" ${label_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_name" value="${label_data[5].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_value" value="${label_data[5].value}"/>
</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" ${label_data[6].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_name" value="${label_data[6].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_value" value="${label_data[6].value}"/>
</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" ${label_data[7].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_name" value="${label_data[7].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_value" value="${label_data[7].value}"/>
</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" ${label_data[8].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_name" value="${label_data[8].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_value" value="${label_data[8].value}"/>
</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" ${label_data[9].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_name" value="${label_data[9].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_value" value="${label_data[9].value}"/>
</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" ${label_data[10].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_name" value="${label_data[10].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_value" value="${label_data[10].value}"/>
</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" ${label_data[11].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="${label_data[11].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="${label_data[11].value}"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-5">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
Extras
</button>
</h2>
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_0_check" type="checkbox" ${ports_data[0].check}>
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="port_0_external" value="${ports_data[0].external}"/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="port_0_internal" value="${ports_data[0].internal}"/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="port_0_protocol">
<option value="${ports_data[0].protocol}" selected hidden>${ports_data[0].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_1_check" type="checkbox" ${ports_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_external" value="${ports_data[1].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_internal" value="${ports_data[1].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_1_protocol">
<option value="${ports_data[1].protocol}" selected hidden>${ports_data[1].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_2_check" type="checkbox" ${ports_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_external" value="${ports_data[2].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_internal" value="${ports_data[2].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_2_protocol">
<option value="${ports_data[2].protocol}" selected hidden>${ports_data[2].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_3_check" type="checkbox" ${ports_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_external" value="${ports_data[3].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_internal" value="${ports_data[3].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_3_protocol">
<option value="${ports_data[3].protocol}" selected hidden>${ports_data[3].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port_4_check" type="checkbox" ${ports_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_external" value="${ports_data[4].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_internal" value="${ports_data[4].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_4_protocol">
<option value="${ports_data[4].protocol}" selected hidden>${ports_data[4].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="" type="checkbox" >
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="" value=""/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="" value=""/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="">
<option value="" selected hidden></option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>`;
return modal;
}

View file

@ -0,0 +1,371 @@
export const permissionsModal = (data) => {
let modal = `
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Permissions</h5>
</div>
<div class="modal-body">
<form action="" id="permissions_modal" method="POST">
<div class="accordion" id="modal-accordion">
<div class="accordion-item mb-3" style="border: 1px solid grey;">
<h2 class="accordion-header" id="heading-1">
<button class="accordion-button collapsed row" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" 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;">Jane Doe</div>
</button>
</h2>
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="">
<div class="">
<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="select" onclick="selectAll('select')">
</label>
</div>
</div>
<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="select">
</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="select">
</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="select">
</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="select">
</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="select">
</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="select">
</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="select">
</label>
</div>
</div>
<div class="row mb-4">
<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="select">
</label>
</div>
</div>
<div class="row mb-2">
<button class="btn" type="submit" formaction="/updatePermissions">Update</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item mb-3" style="border: 1px solid grey;">
<h2 class="accordion-header" id="heading-2">
<button class="accordion-button collapsed row" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
<span class="avatar avatar-sm bg-cyan-lt col-3 text-start">JD</span>
<div class="col text-end" style="margin-right: 10px;">John Doe</div>
</button>
</h2>
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
<div class="accordion-body pt-0">
<div class="">
<div class="">
<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="remove_image">
</label>
</div>
</div>
<div class="row mb-2">
<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="remove_image">
</label>
</div>
</div>
<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="remove_image">
</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="remove_backups">
</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="remove_backups">
</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="remove_backups">
</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="remove_backups">
</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="remove_backups">
</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="remove_backups">
</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="remove_backups">
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<div class="row">
<div class="col">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" disabled="">Reset</button>
</div>
<div class="col">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Update</button>
</div>
</div>
</div>
</div>
</div>`;
return modal;
}

View file

@ -1,7 +1,7 @@
import { readFileSync } from 'fs';
import { appCard } from '../components/appCard.js';
let templatesJSON = readFileSync('./templates.json');
let templatesJSON = readFileSync('./templates/templates.json');
let templates = JSON.parse(templatesJSON).templates;
templates = templates.sort((a, b) => {

View file

@ -1,23 +1,298 @@
import { Readable } from 'stream';
import { Permission, Container } from '../database/models.js';
import { modal } from '../components/modal.js';
import { permissionsModal } from '../components/permissions_modal.js';
import { setEvent, cpu, ram, tx, rx, disk, docker } from '../server.js';
import { dockerContainerStats } from 'systeminformation';
import { containerCard } from '../components/containerCard.js';
let [ hidden, cardList ] = [ '', '' ];
export const Dashboard = (req, res) => {
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
}
export const searchDashboard = (req, res) => {
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> `)
});
}
console.log(req.params);
export const Modal = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
if (id == 'permissions') {
let containerPermissions = await Permission.findAll({ where: {containerName: name}});
let form = permissionsModal();
res.send(form);
return;
}
let containerId = docker.getContainer(name);
let containerInfo = await containerId.inspect();
let ports_list = [];
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
let external_port = ports_list[0]?.external || 0;
let internal_port = ports_list[0]?.internal || 0;
let container_info = {
name: containerInfo.Name.slice(1),
state: containerInfo.State.Status,
image: containerInfo.Config.Image,
external_port: external_port,
internal_port: internal_port,
ports: ports_list,
link: 'localhost',
}
let form = modal(container_info);
res.send(form);
}
export const Stats = async (req, res) => {
let name = req.header('hx-trigger-name');
let color = req.header('hx-trigger');
let value = 0;
switch (name) {
case 'CPU': value = cpu;
break;
case 'RAM': value = ram;
break;
case 'TX': value = tx;
break;
case 'RX': value = rx;
break;
case 'DISK': value = disk;
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>`;
res.send(info);
}
export const Hide = async (req, res) => {
let name = req.header('hx-trigger-name');
let exists = await Container.findOne({ where: {name: name}});
if (!exists) {
const newContainer = await Container.create({ name: name, visibility: false, });
} else {
exists.update({ visibility: false });
}
setEvent(true, 'docker');
res.send("ok");
}
export const Reset = async (req, res) => {
Container.update({ visibility: true }, { where: {} });
setEvent(true, 'docker');
res.send("ok");
}
let stats = {};
export const Chart = async (req, res) => {
let name = req.header('hx-trigger-name');
// create an empty array if it doesn't exist
if (!stats[name]) {
stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) };
}
// get the stats
const info = await dockerContainerStats(name);
// update the arrays
stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
stats[name].ramArray.push(Math.round(info[0].memPercent));
// slice them down to the last 15 values
stats[name].cpuArray = stats[name].cpuArray.slice(-15);
stats[name].ramArray = stats[name].ramArray.slice(-15);
// replace the chart with the new data
let chart = `
<script>
${name}chart.updateSeries([{
data: [${stats[name].cpuArray}]
}, {
data: [${stats[name].ramArray}]
}])
</script>`
res.send(chart);
}
// Get hidden containers
async function getHidden() {
hidden = await Container.findAll({ where: {visibility:false}});
hidden = hidden.map((container) => container.name);
}
// Create list of docker containers cards
async function containerCards() {
let list = '';
const allContainers = await docker.listContainers({ all: true });
for (const container of allContainers) {
if (!hidden.includes(container.Names[0].slice(1))) {
let imageVersion = container.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
let containerId = docker.getContainer(container.Id);
let containerInfo = await containerId.inspect();
let ports_list = [];
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
let external_port = ports_list[0]?.external || 0;
let internal_port = ports_list[0]?.internal || 0;
let container_info = {
name: container.Names[0].slice(1),
service: service,
id: container.Id,
state: container.State,
image: container.Image,
external_port: external_port,
internal_port: internal_port,
ports: ports_list,
link: 'localhost',
}
let card = containerCard(container_info);
list += card;
}
}
cardList = list;
}
export const Containers = async (req, res) => {
await getHidden();
await containerCards();
res.send(cardList);
}
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 Start = (req, res) => {
let name = req.header('hx-trigger-name');
let state = req.header('hx-trigger');
if (state == 'stopped') {
var containerName = docker.getContainer(name);
containerName.start();
} else if (state == 'paused') {
var containerName = docker.getContainer(name);
containerName.unpause();
}
res.send(status('starting'));
}
export const Stop = (req, res) => {
let name = req.header('hx-trigger-name');
let state = req.header('hx-trigger');
if (state != 'stopped') {
var containerName = docker.getContainer(name);
containerName.stop();
}
res.send(status('stopping'));
}
export const Pause = (req, res) => {
let name = req.header('hx-trigger-name');
let state = req.header('hx-trigger');
if (state == 'running') {
var containerName = docker.getContainer(name);
containerName.pause();
} else if (state == 'paused') {
var containerName = docker.getContainer(name);
containerName.unpause();
}
res.send(status('pausing'));
}
export const Restart = (req, res) => {
let name = req.header('hx-trigger-name');
var containerName = docker.getContainer(name);
containerName.restart();
res.send(status('restarting'));
}
export const Installs = async (req, res) => {
let name = req.header('hx-trigger-name');
let all_containers = '';
await docker.listContainers({ all: true }).then(containers => {
containers.forEach(container => {
if (container.Names[0].slice(1) == name) {
return;
}
});
let install_info = {
name: name,
service: 'Service Name',
id: '',
state: '',
image: '',
external_port: 0,
internal_port: 0,
ports: '',
link: 'localhost',
}
let card = containerCard(install_info);
res.send(card);
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
}

View file

@ -8,13 +8,13 @@ export const Images = async function(req, res) {
<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><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
<th><button class="table-sort" data-sort="sort-type">Tag</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></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-type">Tag</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">`
@ -30,11 +30,11 @@ export const Images = async function(req, res) {
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<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">${images[i].RepoTags}</td>
<td class="sort-city">${images[i].Id}</td>
<td class="sort-type">Latest</td>
<td class="sort-score text-green">In use</td>
<td class="sort-type"> - </td>
<td class="sort-score text-green"> - </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="#">Details</a></td>
@ -53,4 +53,28 @@ export const Images = async function(req, res) {
image_count: images.length
});
}
export const removeImage = async function(req, res) {
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");
}

View file

@ -1,6 +1,8 @@
import { User, Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
export const Login = function(req,res){
if(req.session.user){
res.redirect("/logout");

View file

@ -9,29 +9,27 @@ export const Networks = async function(req, res) {
<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><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></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">`
<tbody class="table-tbody">`
for (let i = 0; i < networks.length; i++) {
// let date = new Date(images[i].Created * 1000);
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${networks[i].Name}</td>
<td class="sort-city">${networks[i].Id}</td>
<td class="sort-score text-green">In use</td>
<td class="sort-score text-green"> - </td>
<td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
@ -40,7 +38,6 @@ export const Networks = async function(req, res) {
network_list += `</tbody>`
res.render("networks", {
name: req.session.user,
role: req.session.role,
@ -48,5 +45,29 @@ export const Networks = async function(req, res) {
network_list: network_list,
network_count: networks.length
});
}
export const removeNetwork = async function(req, res) {
let networks = req.body.select;
if (typeof(networks) == 'string') {
networks = [networks];
}
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) {
console.log(`Unable to remove network: ${networks[i]}`);
}
}
}
res.redirect("/networks");
}

View file

@ -55,7 +55,7 @@ export const submitRegister = async function(req,res){
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
avatar: `<img src="img/avatars/${avatar}">`,
avatar: `<img src="/images/avatars/${avatar}">`,
lastLogin: newLogin,
});

32
controllers/supporters.js Normal file
View file

@ -0,0 +1,32 @@
import { User } from "../database/models.js";
export const Supporters = async (req, res) => {
if (!req.session.UUID) return res.redirect("/login");
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: user.avatar,
});
}
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);
}

9
controllers/variables.js Normal file
View file

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

View file

@ -10,12 +10,12 @@ export const Volumes = async function(req, res) {
<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><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">Mount point</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></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">`
@ -44,10 +44,10 @@ export const Volumes = async function(req, res) {
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<td><input class="form-check-input m-0 align-middle" name="select" value="${name}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${name}</td>
<td class="sort-city">${mount}</td>
<td class="sort-score text-green">In use</td>
<td class="sort-score text-green"> - </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>
@ -67,4 +67,38 @@ export const Volumes = async function(req, res) {
volume_count: volumes.length
});
}
export const createVolume = async function(req, res) {
let name = req.body.name;
docker.createVolume({
Name: name
});
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");
}

View file

@ -1,17 +1,9 @@
import { Sequelize, DataTypes } from 'sequelize';
// let SQLITE_PASS = process.env.SQLITE_PASS || 'some_long_elaborate_password';
// export const sequelize = new Sequelize('dweebui', 'dweebui', SQLITE_PASS, {
// dialect: 'sqlite',
// dialectModulePath: '@journeyapps/sqlcipher',
// storage: './database/database.sqlite',
// logging: false,
// });
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/database.sqlite',
storage: './database/db.sqlite',
logging: false,
});
@ -55,25 +47,60 @@ export const User = sequelize.define('User', {
});
export const Container = sequelize.define('Container', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
},
size: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
}
});
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: {

View file

@ -2,9 +2,8 @@ version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.20
image: lllllllillllllillll/dweebui:v0.40
environment:
NODE_ENV: production
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
@ -12,7 +11,11 @@ services:
- 8000:8000
volumes:
- dweebui:/app
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket
#- /run/podman/podman.sock:/var/run/docker.sock
networks:
- dweebui_net
@ -21,4 +24,4 @@ volumes:
networks:
dweebui_net:
driver: bridge
driver: bridge

View file

@ -9,8 +9,6 @@ import { containerCard } from "../components/containerCard.js";
// This entire page hurts to look at.
export const Install = async (req, res) => {
console.log(req.app.locals.installCard);
let data = req.body;
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
@ -170,39 +168,40 @@ export const Install = async (req, res) => {
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 { console.log('error creating directory or compose file') }
} catch {
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed - error creating directory or compose file : ${err}`,
ip: req.socket.remoteAddress
});
}
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
try {
try {
(async () => {
await compose.pull();
await compose.up().then(() => {
const syslog = Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
});
} catch (err) {
console.error(err);
await compose.up();
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
}
})();
});
})();
} catch (err) {
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
}
res.redirect('/');
}

View file

@ -6,17 +6,20 @@ export const Uninstall = async (req, res) => {
let { confirm, service_name } = req.body;
console.log(`Uninstalling ${service_name}...`);
if (confirm == 'Yes') {
var containerName = docker.getContainer(`${service_name}`);
try {
await containerName.stop();
} catch {
console.log(`Error stopping ${service_name} container`);
}
try {
await containerName.remove();
try {
containerName.remove();
const syslog = await Syslog.create({
user: req.session.user,
email: null,

2723
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,27 @@
{
"name": "dweebui",
"version": "1.0.0",
"description": "A web UI for Docker",
"description": "",
"main": "server.js",
"type": "module",
"scripts": {
"test": "mocha --require @babel/register"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/register": "^7.23.7",
"@socket.io/admin-ui": "^0.5.1",
"bcrypt": "^5.1.1",
"chai": "^5.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dockerode": "^4.0.2",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-session": "^1.17.3",
"helmet": "^7.1.0",
"express-session": "^1.18.0",
"js-yaml": "^4.1.0",
"mocha": "^10.2.0",
"sequelize": "^6.35.2",
"sinon": "^17.0.1",
"socket.io": "^4.7.4",
"memorystore": "^1.6.7",
"sequelize": "^6.37.1",
"sqlite3": "^5.1.7",
"stream": "^0.0.2",
"supertest": "^6.3.3",
"systeminformation": "^5.21.22"
"systeminformation": "^5.22.0"
}
}

View file

@ -83,6 +83,10 @@
.blue > span {
background-image: linear-gradient(#2478f5, #22017e);
}
.purple > span {
background-image: linear-gradient(#bd14d3, #670370);
}
.nostripes > span > span,
.nostripes > span::after {

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

355
public/js/htmx-sse.js Normal file
View file

@ -0,0 +1,355 @@
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
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;
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
switch (name) {
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();
}
return;
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(evt.target);
registerSSE(evt.target);
}
}
});
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
/**
* 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 });
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacySSEURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
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;
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource);
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null; // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
// 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;
}
})();

1
public/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,141 +0,0 @@
socket.on('connect', () => {
console.log('connected');
//clear localStorage (because of code in old versions)
localStorage.clear();
});
// Server metrics
const cpuText = document.getElementById('cpu-text');
const cpuBar = document.getElementById('cpu-bar');
const ramText = document.getElementById('ram-text');
const ramBar = document.getElementById('ram-bar');
const netText = document.getElementById('net-text');
const netBar = document.getElementById('net-bar');
const diskText = document.getElementById('disk-text');
const diskBar = document.getElementById('disk-bar');
// Container cards
const dockerCards = document.getElementById('cards');
// Container logs
const logViewer = document.getElementById('logView');
// Server metrics
socket.on('metrics', (data) => {
let [cpu, ram, tx, rx, disk] = data;
cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
if (cpu < 7 ) { cpu = 7; }
cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
ramText.innerHTML = `<span>RAM ${ram} %</span>`;
if (ram < 7 ) { ram = 7; }
ramBar.innerHTML = `<span style="width: ${ram}%"><span></span></span>`;
tx = Math.round(tx / 1024 / 1024);
rx = Math.round(rx / 1024 / 1024);
netText.innerHTML = `<span>Down: ${rx}MB</span><span> Up: ${tx}MB</span>`;
netBar.innerHTML = `<span style="width: 50%"><span></span></span>`;
diskText.innerHTML = `<span>DISK ${disk} %</span>`;
if (disk < 7 ) { disk = 7; }
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
});
// Container cards
socket.on('containers', (data) => {
let deleteMeElements = document.querySelectorAll('.deleteme');
deleteMeElements.forEach((element) => {
element.parentNode.removeChild(element);
});
dockerCards.insertAdjacentHTML("afterend", data);
});
function drawCharts(name, cpuArray, ramArray) {
let element = document.querySelector(`${name}`);
let chart = new ApexCharts(element, {
chart: {
type: "line",
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [2, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: cpuArray
}, {
name: "RAM",
data: ramArray
}],
tooltip: {
enabled: false
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
padding: 4
}
},
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
})
chart.render();
}
// Buttons functions
function clicked(button) {
socket.emit('clicked', {name: button.name, id: button.id, value: button.value});
}
socket.on('containerStats', (data) => {
let containerStats = data;
for (const [name, statsArray] of Object.entries(containerStats)) {
let cpuArray = statsArray.cpuArray;
let ramArray = statsArray.ramArray;
let chart = document.getElementById(`${name}_chart`);
if (chart) {
chart.innerHTML = '';
drawCharts(`#${name}_chart`, cpuArray, ramArray);
} else {
console.log(`Chart element with id ${name}_chart not found in the DOM`);
}
}
});
socket.on('logs', (data) => {
logViewer.innerHTML = `<pre>${data}</pre>`;
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -4,21 +4,19 @@ export const router = express.Router();
// Controllers
import { Login, submitLogin, Logout } from "../controllers/login.js";
import { Register, submitRegister } from "../controllers/register.js";
import { Dashboard, searchDashboard } from "../controllers/dashboard.js";
import { Dashboard, Start, Stop, Pause, Restart, Logs, Modal, Stats, Hide, Reset, Chart, Containers, Installs } from "../controllers/dashboard.js";
import { Apps, appSearch } from "../controllers/apps.js";
import { Users } from "../controllers/users.js";
import { Images } from "../controllers/images.js";
import { Images, removeImage } from "../controllers/images.js";
import { Networks, removeNetwork } from "../controllers/networks.js";
import { Volumes, removeVolume } from "../controllers/volumes.js";
import { Account } from "../controllers/account.js";
import { Variables } from "../controllers/variables.js";
import { Settings } from "../controllers/settings.js";
import { Networks } from "../controllers/networks.js";
import { Volumes } from "../controllers/volumes.js";
import { Supporters, Thanks } from "../controllers/supporters.js";
import { Syslogs } from "../controllers/syslogs.js";
import { Portal } from "../controllers/portal.js"
/// Functions
import { Install } from "../functions/install.js"
import { Uninstall } from "../functions/uninstall.js"
// Auth middleware
const auth = (req, res, next) => {
if (req.session.role == "admin") {
@ -28,22 +26,29 @@ const auth = (req, res, next) => {
}
};
// Routes
router.get("/login", Login);
router.post("/login", submitLogin);
router.get("/logout", Logout);
router.get("/register", Register);
router.post("/register", submitRegister);
// Admin routes
router.get("/", auth, Dashboard);
router.post("/", auth, searchDashboard);
router.get("/containers", auth, Containers);
router.post("/start", auth, Start);
router.post("/stop", auth, Stop);
router.post("/pause", auth, Pause);
router.post("/restart", auth, Restart);
router.get("/logs", auth, Logs);
router.get ("/modal", auth, Modal);
router.get("/stats", auth, Stats);
router.post("/hide", auth, Hide);
router.post("/reset", auth, Reset);
router.get("/chart", auth, Chart);
router.get("/installs", auth, Installs);
router.get("/images", auth, Images);
router.post("/removeImage", auth, removeImage);
router.get("/volumes", auth, Volumes);
router.post("/removeVolume", auth, removeVolume);
router.get("/networks", auth, Networks);
router.get("/portal", Portal)
router.post("/removeNetwork", auth, removeNetwork);
router.get("/apps", auth, Apps);
router.get("/apps/:page", auth, Apps);
@ -52,10 +57,25 @@ router.post("/apps", auth, appSearch);
router.get("/users", auth, Users);
router.get("/syslogs", auth, Syslogs);
router.get("/account", Account);
router.get("/variables", auth, Variables);
router.get("/settings", auth, Settings);
// User routes
router.get("/portal", Portal);
router.get("/account", Account);
router.get("/supporters", Supporters);
router.post("/thank", Thanks);
router.get("/login", Login);
router.post("/login", submitLogin);
router.get("/register", Register);
router.post("/register", submitRegister);
router.get("/logout", Logout);
// Functions
import { Install } from "../functions/install.js"
import { Uninstall } from "../functions/uninstall.js"
router.post("/install", auth, Install);
router.post("/uninstall", auth, Uninstall);
router.post("/uninstall", auth, Uninstall);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

343
server.js
View file

@ -1,44 +1,24 @@
import express from 'express';
import session from 'express-session';
import compression from 'compression';
import helmet from 'helmet';
import memorystore from 'memorystore';
import ejs from 'ejs';
import Docker from 'dockerode';
import cors from 'cors';
import { Readable } from 'stream';
import { rateLimit } from 'express-rate-limit';
import { instrument } from '@socket.io/admin-ui'
import { router } from './router/index.js';
import { createServer } from 'node:http';
import { Server } from 'socket.io';
import { sequelize, Container } from './database/models.js';
import { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces } from 'systeminformation';
import { containerCard } from './components/containerCard.js';
import { sequelize } from './database/models.js';
import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
export const app = express();
const server = createServer(app);
const port = process.env.PORT || 8000;
export var docker = new Docker();
let [cpu, ram, tx, rx, disk] = [0, 0, 0, 0, 0];
let [hidden, clicked, dockerEvents] = ['', false, ''];
let metricsInterval, cardsInterval, graphsInterval;
let cardList = '';
const statsArray = {};
export { setEvent, cpu, ram, tx, rx, disk }
// Socket.io admin ui
export const io = new Server(server, {
cors: {
origin: ['http://localhost:8000', 'https://admin.socket.io'],
methods: ['GET', 'POST'],
credentials: true
}
});
instrument(io, {
auth: false,
readonly: true
});
const app = express();
const MemoryStore = memorystore(session);
const port = process.env.PORT || 8000;
let [ cpu, ram, tx, rx, disk ] = [0, 0, 0, 0, 0];
let [ event, eventType ] = [false, 'docker'];
// Session middleware
const sessionMiddleware = session({
store: new MemoryStore({ checkPeriod: 86400000 }), // Prune expired entries every 24h
secret: "keyboard cat",
resave: false,
saveUninitialized: false,
@ -49,46 +29,36 @@ const sessionMiddleware = session({
}
});
// Make session data available to socket.io
io.engine.use(sessionMiddleware);
// Rate limiter
const limiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
limit: 50, // Limit each IP to 50 requests per `window`.
standardHeaders: 'draft-7',
legacyHeaders: false,
})
// Express middleware
app.set('view engine', 'ejs');
app.set('view engine', 'html');
app.engine('html', ejs.renderFile);
app.use([
compression(),
cors(),
helmet({contentSecurityPolicy: false}),
express.static("public"),
express.static('public'),
express.json(),
express.urlencoded({ extended: true }),
sessionMiddleware,
router,
limiter
router
]);
// Initialize server
server.listen(port, async () => {
app.listen(port, async () => {
async function init() {
try { await sequelize.authenticate().then(() => { console.log('[Connected to DB]') }); }
catch { console.log('[Could not connect to DB]'); }
try { await sequelize.sync().then(() => { console.log('[Models Synced]') }); }
catch { console.log('[Could not Sync Models]', error); }
await getHidden();
containerCards();
}
await init();
app.emit("appStarted");
console.log(`\nServer listening on http://localhost:${port}`);
try { await sequelize.authenticate().then(
() => { console.log('DB Connection: ✔️') }); }
catch { console.log('DB Connection: ❌'); }
try { await sequelize.sync().then( // check out that formatting
() => { console.log('Synced Models: ✔️') }); }
catch { console.log('Synced Models: ❌'); } }
await init().then(() => {
console.log(`Listening on http://localhost:${port}`);
});
});
function setEvent(value, type) {
event = value;
eventType = type;
}
// Server metrics
let serverMetrics = async () => {
currentLoad().then(data => {
@ -105,239 +75,26 @@ let serverMetrics = async () => {
disk = data[0].use;
});
}
setInterval(serverMetrics, 1000);
// List docker containers
let containerCards = async () => {
let list = '';
const allContainers = await docker.listContainers({ all: true });
for (const container of allContainers) {
if (!hidden.includes(container.Names[0].slice(1))) {
let imageVersion = container.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
let containerId = docker.getContainer(container.Id);
let containerInfo = await containerId.inspect();
let ports_list = [];
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
let external_port = ports_list[0]?.external || 0;
let internal_port = ports_list[0]?.internal || 0;
let container_info = {
name: container.Names[0].slice(1),
service: service,
id: container.Id,
state: container.State,
image: container.Image,
external_port: external_port,
internal_port: internal_port,
ports: ports_list,
link: 'localhost',
}
let card = containerCard(container_info);
list += card;
let sent_list = '';
router.get('/sse_event', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', });
let eventCheck = setInterval(async() => {
let all_containers = '';
await docker.listContainers({ all: true }).then(containers => {
containers.forEach(container => {
all_containers += `${container.Names}: ${container.State}\n`;
});
});
if ((all_containers != sent_list) || (event == true)) {
sent_list = all_containers;
event = false;
res.write(`event: ${eventType}\n`);
res.write(`data: there was an event!\n\n`);
}
}
cardList = list;
}
// Container metrics
let containerStats = async () => {
const data = await docker.listContainers({ all: true });
for (const container of data) {
if (!hidden.includes(container.Names[0].slice(1))) {
const stats = await dockerContainerStats(container.Id);
const name = container.Names[0].slice(1);
if (!statsArray[name]) {
statsArray[name] = {
cpuArray: Array(15).fill(0),
ramArray: Array(15).fill(0)
};
}
statsArray[name].cpuArray.push(Math.round(stats[0].cpuPercent));
statsArray[name].ramArray.push(Math.round(stats[0].memPercent));
statsArray[name].cpuArray = statsArray[name].cpuArray.slice(-15);
statsArray[name].ramArray = statsArray[name].ramArray.slice(-15);
}
}
}
// Store docker events
docker.getEvents((err, stream) => {
if (err) throw err;
stream.on('data', (chunk) => {
dockerEvents += chunk.toString('utf8');
}, 1000);
req.on('close', () => {
clearInterval(eventCheck);
});
});
// Check for docker events
setInterval(async () => {
if (dockerEvents != '') {
await getHidden();
await containerCards();
dockerEvents = '';
}
}, 1000);
// Get hidden containers
async function getHidden() {
hidden = await Container.findAll({ where: {visibility:false}});
hidden = hidden.map((container) => container.name);
}
// Socket.io
io.on('connection', (socket) => {
let sessionData = socket.request.session;
let sent = '';
if (sessionData.user != undefined) {
console.log(`${sessionData.user} connected from ${socket.handshake.headers.host}`);
// Start intervals if not already started
if (!metricsInterval) {
serverMetrics();
metricsInterval = setInterval(serverMetrics, 1000);
console.log('Metrics interval started');
}
if (!cardsInterval) {
containerCards();
cardsInterval = setInterval(containerCards, 1000);
console.log('Cards interval started');
}
if (!graphsInterval) {
containerStats();
graphsInterval = setInterval(containerStats, 1000);
console.log('Graphs interval started');
}
setInterval(() => {
socket.emit('metrics', [cpu, ram, tx, rx, disk]);
if (sent != cardList) {
sent = cardList;
socket.emit('containers', cardList);
}
socket.emit('containerStats', statsArray);
}, 1000);
// Client input
socket.on('clicked', (data) => {
let { name, id, value } = data;
console.log(`${sessionData.user} clicked: ${id} ${value} ${name}`);
if (clicked == true) { return; } clicked = true;
// View container logs
if (id == 'logs'){
function containerLogs (data) {
return new Promise((resolve, reject) => {
let logString = '';
var options = {
follow: false,
stdout: true,
stderr: false,
timestamps: false
};
var containerName = docker.getContainer(data);
containerName.logs(options, function (err, stream) {
if (err) { reject(err); return; }
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
resolve(logString);
});
});
});
};
containerLogs(name).then((data) => {
socket.emit('logs', data);
});
}
// start, stop, pause, restart container
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
var containerName = docker.getContainer(name);
if ((id == 'start') && (value == 'stopped')) {
containerName.start();
} else if ((id == 'start') && (value == 'paused')) {
containerName.unpause();
} else if ((id == 'stop') && (value != 'stopped')) {
containerName.stop();
} else if ((id == 'pause') && (value == 'running')) {
containerName.pause();
} else if ((id == 'pause') && (value == 'paused')) {
containerName.unpause();
} else if (id == 'restart') {
containerName.restart();
}
}
// hide container
if (id == 'hide') {
async function hideContainer() {
let containerExists = await Container.findOne({ where: {name: name}});
if(!containerExists) {
const newContainer = await Container.create({ name: name, visibility: false, });
getHidden();
} else {
containerExists.update({ visibility: false });
getHidden();
}
}
hideContainer();
}
// unhide containers
if (id == 'resetView') {
Container.update({ visibility: true }, { where: {} });
getHidden();
}
clicked = false;
});
socket.on('disconnect', () => {
console.log(`${sessionData.user} disconnected`);
socket.disconnect();
// clear intervals if no users are connected
if (io.engine.clientsCount == 0) {
clearInterval(metricsInterval);
clearInterval(cardsInterval);
clearInterval(graphsInterval);
metricsInterval = null;
cardsInterval = null;
graphsInterval = null;
console.log('All intervals cleared');
}
});
} else {
console.log('Missing session data');
}
});
// let link = '';
// networkInterfaces().then(data => {
// link = data[0].ip4;
// });
});

5357
templates/foss.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -5914,7 +5914,7 @@
"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",
"logo": "https://avatars.githubusercontent.com/u/1728152",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nvidia.png",
"image": "nvidia/cuda:12.2.0-base-ubuntu20.04",
"env": [
{

5539
templates/templates.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,144 +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 - 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('navbar.ejs') %>
<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">
Account Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<h4 class="subheader">Business settings</h4>
<div class="list-group list-group-transparent">
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center active">Accounts</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">My Notifications</a>
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Connected Apps</a> -->
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Billing & Invoices</a> -->
</div>
<h4 class="subheader mt-4">Experience</h4>
<div class="list-group list-group-transparent">
<a href="#" class="list-group-item list-group-item-action">Credits</a>
</div>
</div>
</div>
<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('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

131
views/account.html Normal file
View file

@ -0,0 +1,131 @@
<!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('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('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('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

@ -22,7 +22,7 @@
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<%- include('navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
@ -82,11 +82,11 @@
prev
</a>
</li>
<li class="page-item"><a class="page-link" href="/apps?page=1">1</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=2">2</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=3">3</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=4">4</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=5">5</a></li>
<li class="page-item"><a class="page-link" href="/apps/1">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5">5</a></li>
<li class="page-item">
<a class="page-link" href="<%- next %>">
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
@ -98,7 +98,7 @@
</div>
</div>
<%- include('footer.ejs') %>
<%- include('footer.html') %>
</div>
</div>

File diff suppressed because it is too large Load diff

282
views/dashboard.html Normal file
View file

@ -0,0 +1,282 @@
<!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('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" 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">
<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>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" id="containerCards" data-hx-get="/containers" data-hx-trigger="load, sse:docker" data-hx-swap="innerHTML">
</div>
</div>
<!-- HTMX -->
<!-- <div class="col-12">
<div class="row row-cards" data-hx-get="/installs" name="jellyfin" data-hx-trigger="load delay:2s, sse:install" data-hx-swap="beforeend" hx-target="#containerCards">
</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-lg modal-dialog-centered" role="document">
<div class="modal-content">
</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('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: [2, 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>
<script>
function selectAll(group) {
let checkboxes = document.getElementsByName(group);
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -23,8 +23,8 @@
All rights reserved.
</li>
<li class="list-inline-item">
<a href="#" class="link-secondary" rel="noopener">
v0.10
<a href="https://github.com/lllllllillllllillll/DweebUI/releases" class="link-secondary" rel="noopener">
v0.40
</a>
</li>
</ul>

View file

@ -1,188 +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 - 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>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Docker Images</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="#not_add-site">
<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>
New Image
</a>
</div>
</div>
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<form action="/addsite" id="addsite" method="POST">
<div class="mb-3">
<div class="form-label">Type</div>
<select class="form-select" name="type">
<option value="reverse_proxy">Reverse Proxy</option>
<option value="proxy">Proxy</option>
<option value="file_server">File Server</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Domain / Subdomain</label>
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
</div>
<div class="mb-4">
<div class="row g-2">
<div class="col-8">
<label class="form-label">Hostname / Host IP</label>
<input type="text" class="form-control" name="host" placeholder="localhost">
</div>
<div class="col-4">
<label class="form-label">Port</label>
<input type="text" class="form-control" name="port" placeholder="8000">
</div>
</div>
</div>
<div class="mb-3">
<div class="divide-y">
<div>
<label class="row">
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
</label>
</span>
</label>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-label">Container</div>
<select class="form-select" name="container" disabled="">
<option value="0" selected></option>
<option value="1">Jellyfin</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
</div>
</div>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- image_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<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="/enablesite">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/disablesite">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/removesite">
Delete
</button>
</div>
</span>
</form>
<p class="m-0 text-muted ms-auto"><%- image_count %> Images</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</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>

116
views/images.html Normal file
View file

@ -0,0 +1,116 @@
<!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 - 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>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Docker Images</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="#not_add-site">
<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>
New Image
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- image_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/removeImage">Remove</button>
</form>
<p class="m-0 text-muted ms-auto"><%- image_count %> Images</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</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

@ -9,7 +9,7 @@
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('fonts/inter.css');
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
@ -22,19 +22,27 @@
<script src="/js/demo-theme.js"></script>
<div class="page page-center">
<div class="container container-tight py-4">
<div class="text-center mb-4">
<a href="." class="navbar-brand navbar-brand-autodark"><img src="/static/logo.svg" height="50" alt=""></a>
<div class="text-center">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/images/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="/images/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</h1>
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="h2 text-center mb-4">Login to your account</h2>
<form action="/login" method="POST" novalidate>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<% if(error) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<div class="mb-2">
<label class="form-label">Email address</label>
@ -70,9 +78,12 @@
</div>
</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

@ -33,10 +33,15 @@
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 pe-md-3">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
<a href="#">
<img src="/static/logo.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
<img src="/images/logo.png" alt="DweebUI" title="DweebUI" height="40px">
</a>
<a href="#">
<img src="/images/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</a>
</h1>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item d-none d-md-flex me-3">
@ -79,11 +84,15 @@
<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>
<div class="list-group list-group-flush list-group-hoverable">
<div class="list-group-item">
<div class="row align-items-center">
@ -120,7 +129,9 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -138,11 +149,11 @@
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<!-- <a href="#" class="dropdown-item">Status</a> -->
<a href="/account" class="dropdown-item">Account</a>
<!-- <a href="#" class="dropdown-item">Feedback</a> -->
<!-- <div class="dropdown-divider"></div> -->
<a href="/variables" class="dropdown-item">Variables</a>
<a href="/settings" class="dropdown-item">Settings</a>
<!-- <div class="dropdown-divider"></div> -->
<a href="/logout" class="dropdown-item">Logout</a>
</div>
</div>

View file

@ -1,188 +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 - 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>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Docker Networks</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="#not_add-site">
<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>
New Network
</a>
</div>
</div>
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<form action="/addsite" id="addsite" method="POST">
<div class="mb-3">
<div class="form-label">Type</div>
<select class="form-select" name="type">
<option value="reverse_proxy">Reverse Proxy</option>
<option value="proxy">Proxy</option>
<option value="file_server">File Server</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Domain / Subdomain</label>
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
</div>
<div class="mb-4">
<div class="row g-2">
<div class="col-8">
<label class="form-label">Hostname / Host IP</label>
<input type="text" class="form-control" name="host" placeholder="localhost">
</div>
<div class="col-4">
<label class="form-label">Port</label>
<input type="text" class="form-control" name="port" placeholder="8000">
</div>
</div>
</div>
<div class="mb-3">
<div class="divide-y">
<div>
<label class="row">
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
</label>
</span>
</label>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-label">Container</div>
<select class="form-select" name="container" disabled="">
<option value="0" selected></option>
<option value="1">Jellyfin</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
</div>
</div>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- network_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<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="/enablesite">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/disablesite">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/removesite">
Delete
</button>
</div>
</span>
</form>
<p class="m-0 text-muted ms-auto"><%- network_count %> Networks</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</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>

131
views/networks.html Normal file
View file

@ -0,0 +1,131 @@
<!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 - 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>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Docker Networks</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="#not_add-site">
<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>
New Network
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- network_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/removeNetwork">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
Delete
</button>
</div>
</span> -->
<p class="m-0 text-muted ms-auto"><%- network_count %> Networks</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</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

@ -21,7 +21,7 @@
<body >
<div class="page">
<%- include('navbar.ejs') %>
<%- include('navbar.html') %>
<div class="page-wrapper">
<div class="page-body">
@ -131,7 +131,7 @@
</div>
</div>
<%- include('footer.ejs') %>
<%- include('footer.html') %>
</div>
</div>

View file

@ -9,7 +9,7 @@
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('fonts/inter.css');
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
@ -26,7 +26,7 @@
<form class="container container-tight py-4" action="/register" method="POST" novalidate>
<div class="text-center mb-4">
<a href="#" class="navbar-brand navbar-brand-autodark d-none"><img src="/static/logo.svg" height="36" alt=""></a>
<a href="#" class="navbar-brand navbar-brand-autodark d-none"><img src="/images/logo.svg" height="36" alt=""></a>
</div>
<div class="card">
@ -88,7 +88,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="rus.jpg" class="form-imagecheck-input" checked/>
<span class="form-imagecheck-figure">
<img src="img/avatars/rus.jpg" alt="Rich Uncle Skeleton" title="Rich Uncle Skeleton" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/rus.jpg" alt="Rich Uncle Skeleton" title="Rich Uncle Skeleton" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -96,7 +96,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="burns.jpg" class="form-imagecheck-input"/>
<span class="form-imagecheck-figure">
<img src="img/avatars/burns.jpg" alt="Montgomery Burns" title="Montgomery Burns" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/burns.jpg" alt="Montgomery Burns" title="Montgomery Burns" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -104,7 +104,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="frank.jpg" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/frank.jpg" alt="Frank Grimes" title= "Frank Grimes" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/frank.jpg" alt="Frank Grimes" title= "Frank Grimes" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -112,7 +112,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="moe.jpg" class="form-imagecheck-input"/>
<span class="form-imagecheck-figure">
<img src="img/avatars/moe.jpg" alt="Moe Szyslak" title="Moe Szyslak" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/moe.jpg" alt="Moe Szyslak" title="Moe Szyslak" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -120,7 +120,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="poochie.jpg" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/poochie.jpg" alt="Poochie" title="Poochie" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/poochie.jpg" alt="Poochie" title="Poochie" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -128,7 +128,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="skinner.jpg" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/skinner.jpg" alt="Seymour Skinner" title="Seymour Skinner" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/skinner.jpg" alt="Seymour Skinner" title="Seymour Skinner" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -136,7 +136,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="moleman.png" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/moleman.png" alt="Hans Moleman" title="Hans Moleman" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/moleman.png" alt="Hans Moleman" title="Hans Moleman" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -144,7 +144,7 @@
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="duffman.png" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/duffman.png" alt="Duffman" title="Duffman" class="form-imagecheck-image" width="100px">
<img src="/images/avatars/duffman.png" alt="Duffman" title="Duffman" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
@ -176,11 +176,9 @@
<div class="d-none d-md-flex">
<a href="?theme=dark" class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
<!-- Download SVG icon from http://tabler-icons.io/i/moon -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>
</a>
<a href="?theme=light" class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
<!-- Download SVG icon from http://tabler-icons.io/i/sun -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>
</a>
</div>

View file

@ -1,136 +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('navbar.ejs') %>
<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">
Account Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<h4 class="subheader">Business settings</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 href="#" class="list-group-item list-group-item-action d-flex align-items-center">My Notifications</a>
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Connected Apps</a> -->
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center active">Settings</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Billing & Invoices</a> -->
</div>
<h4 class="subheader mt-4">Experience</h4>
<div class="list-group list-group-transparent">
<a href="#" class="list-group-item list-group-item-action">Credits</a>
</div>
</div>
</div>
<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">
<!-- Download SVG icon from https://tabler-icons.io/i/brand-tabler-->
<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>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

120
views/settings.html Normal file
View file

@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('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('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>
<%- include('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>

14
views/sidebar.html Normal file
View file

@ -0,0 +1,14 @@
<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="/variables" class="list-group-item list-group-item-action d-flex align-items-center" disabled="">Variables</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>

85
views/supporters.html Normal file
View file

@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('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('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('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

@ -21,7 +21,7 @@
<div class="page">
<%- include('navbar.ejs') %>
<%- include('navbar.html') %>
<div class="page-wrapper">
@ -68,7 +68,7 @@
</div>
</div>
<%- include('footer.ejs') %>
<%- include('footer.html') %>
</div>
</div>
<!-- Libs JS -->

View file

@ -21,7 +21,7 @@
<div class="page">
<%- include('navbar.ejs') %>
<%- include('navbar.html') %>
<div class="page-wrapper">
@ -59,7 +59,7 @@
</div>
</div>
</div>
<%- include('footer.ejs') %>
<%- include('footer.html') %>
</div>
</div>
<!-- Libs JS -->

120
views/variables.html Normal file
View file

@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('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('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('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,188 +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 - Volumes</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Docker Volumes</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="#not_add-site">
<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>
New Volume
</a>
</div>
</div>
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<form action="/addsite" id="addsite" method="POST">
<div class="mb-3">
<div class="form-label">Type</div>
<select class="form-select" name="type">
<option value="reverse_proxy">Reverse Proxy</option>
<option value="proxy">Proxy</option>
<option value="file_server">File Server</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Domain / Subdomain</label>
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
</div>
<div class="mb-4">
<div class="row g-2">
<div class="col-8">
<label class="form-label">Hostname / Host IP</label>
<input type="text" class="form-control" name="host" placeholder="localhost">
</div>
<div class="col-4">
<label class="form-label">Port</label>
<input type="text" class="form-control" name="port" placeholder="8000">
</div>
</div>
</div>
<div class="mb-3">
<div class="divide-y">
<div>
<label class="row">
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
</label>
</span>
</label>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-label">Container</div>
<select class="form-select" name="container" disabled="">
<option value="0" selected></option>
<option value="1">Jellyfin</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
</div>
</div>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- volume_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<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="/enablesite">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/disablesite">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/removesite">
Delete
</button>
</div>
</span>
</form>
<p class="m-0 text-muted ms-auto"><%- volume_count %> Volumes</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</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>

130
views/volumes.html Normal file
View file

@ -0,0 +1,130 @@
<!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 - Volumes</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Docker Volumes</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="#not_add-site">
<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>
New Volume
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- volume_list %>
</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"><%- volume_count %> Volumes</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</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>