Compare commits
186 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d0bbc27fa | ||
![]() |
47c9ab68ce | ||
![]() |
518bc46dab | ||
![]() |
00216663a5 | ||
![]() |
f46648e598 | ||
![]() |
d905a95764 | ||
![]() |
a67f65996e | ||
![]() |
4b5ae32a97 | ||
![]() |
ee9870f554 | ||
![]() |
8f97e17765 | ||
![]() |
b4d472f414 | ||
![]() |
109d9bc171 | ||
![]() |
842f83ea91 | ||
![]() |
0dae02c382 | ||
![]() |
3080516d93 | ||
![]() |
928855d2f7 | ||
![]() |
29d630f1dd | ||
![]() |
a8bf38eedb | ||
![]() |
7cf8b84169 | ||
![]() |
83180e0a62 | ||
![]() |
869b7b30e7 | ||
![]() |
d0db603efe | ||
![]() |
774d5f4f62 | ||
![]() |
03be1187ef | ||
![]() |
57f080ec0d | ||
![]() |
04dbeefeb9 | ||
![]() |
d78cd645af | ||
![]() |
e1367b58f1 | ||
![]() |
37fd6f320d | ||
![]() |
6aa325ed8e | ||
![]() |
5081100f71 | ||
![]() |
cec389702c | ||
![]() |
82273d1fc1 | ||
![]() |
fbe26cd0ed | ||
![]() |
1558d61dcd | ||
![]() |
2d08e9fa3b | ||
![]() |
eda852b89e | ||
![]() |
c795cac009 | ||
![]() |
c71f330b49 | ||
![]() |
319aab60f5 | ||
![]() |
5581710e75 | ||
![]() |
82c134158d | ||
![]() |
2d9914c458 | ||
![]() |
cf41f07bbd | ||
![]() |
15722b1687 | ||
![]() |
62b7e73aac | ||
![]() |
64ec287286 | ||
![]() |
74cf69b3d3 | ||
![]() |
c9d7dea132 | ||
![]() |
42ca573b51 | ||
![]() |
9c41839852 | ||
![]() |
a73a89b250 | ||
![]() |
365cdde0cc | ||
![]() |
167dd8917e | ||
![]() |
8d9eb9981a | ||
![]() |
66f273e22e | ||
![]() |
6a352281ab | ||
![]() |
5c6e2a9eaa | ||
![]() |
8b8e30772f | ||
![]() |
7c5670e92b | ||
![]() |
c2f06639f5 | ||
![]() |
f04f08d44d | ||
![]() |
974d32e350 | ||
![]() |
b395de3445 | ||
![]() |
e78afb90ca | ||
![]() |
c9da3bd30b | ||
![]() |
bb84828ffe | ||
![]() |
12e75af9b0 | ||
![]() |
a841fed064 | ||
![]() |
308538f579 | ||
![]() |
f615a492e8 | ||
![]() |
6213c54165 | ||
![]() |
ff78e24913 | ||
![]() |
705779ec29 | ||
![]() |
c9c270fd81 | ||
![]() |
eb952c0a50 | ||
![]() |
7bf1739c52 | ||
![]() |
17a479be21 | ||
![]() |
7e3617f967 | ||
![]() |
785b54d5aa | ||
![]() |
2dc22fd75a | ||
![]() |
9a994bfbf1 | ||
![]() |
1d7b56907c | ||
![]() |
cfe9660ac2 | ||
![]() |
c7d79b296c | ||
![]() |
0596793c89 | ||
![]() |
575a689406 | ||
![]() |
d288cdb205 | ||
![]() |
f97628e9cd | ||
![]() |
32c2301873 | ||
![]() |
e294ca7089 | ||
![]() |
ea9ead5709 | ||
![]() |
eb992f706e | ||
![]() |
b62e209e6f | ||
![]() |
25280ae174 | ||
![]() |
c27f64f308 | ||
![]() |
a95b042960 | ||
![]() |
666f820a1f | ||
![]() |
97481b0b75 | ||
![]() |
04cc1c1df3 | ||
![]() |
003db6d7d1 | ||
![]() |
95dcedbdc1 | ||
![]() |
24941d5f32 | ||
![]() |
71bbb574d1 | ||
![]() |
13ee350bb2 | ||
![]() |
377ba6ae67 | ||
![]() |
e786b32161 | ||
![]() |
1938d7b2fc | ||
![]() |
70ec201924 | ||
![]() |
883a65faae | ||
![]() |
8feb88a2a0 | ||
![]() |
f94bd91898 | ||
![]() |
f058360b19 | ||
![]() |
5e45e084d0 | ||
![]() |
0f5575075e | ||
![]() |
c3f10fbb7c | ||
![]() |
9c79290560 | ||
![]() |
562997826f | ||
![]() |
109e77c56c | ||
![]() |
0911a15ac4 | ||
![]() |
df242f250f | ||
![]() |
e6ad8dff72 | ||
![]() |
81a98d3f5d | ||
![]() |
b6faa9786f | ||
![]() |
5b8157a7fe | ||
![]() |
093b27d016 | ||
![]() |
3fac67621a | ||
![]() |
40bd0b693c | ||
![]() |
4d78177951 | ||
![]() |
b22894f366 | ||
![]() |
6d8a919d18 | ||
![]() |
a105e5fbb6 | ||
![]() |
3e7f714115 | ||
![]() |
bd34d78648 | ||
![]() |
68cc67d5aa | ||
![]() |
d4fed30db6 | ||
![]() |
190b902090 | ||
![]() |
ec3ccc110e | ||
![]() |
20c987f7ba | ||
![]() |
569df8fa1e | ||
![]() |
7f77605406 | ||
![]() |
0cbf9226e5 | ||
![]() |
2c8d9993c6 | ||
![]() |
56b18cdbba | ||
![]() |
e99dc8470b | ||
![]() |
837c21fdb8 | ||
![]() |
9a065a8883 | ||
![]() |
385e2e80ee | ||
![]() |
d49ab1a53e | ||
![]() |
ac1356d4b9 | ||
![]() |
458fe2940f | ||
![]() |
bc10e26452 | ||
![]() |
d4211f72c6 | ||
![]() |
f3c6e6f155 | ||
![]() |
9d9d5dac2e | ||
![]() |
8c90b496c9 | ||
![]() |
1e4ff17a37 | ||
![]() |
23be19c4d3 | ||
![]() |
058eadd3cd | ||
![]() |
a10371d0e1 | ||
![]() |
22d769bcc3 | ||
![]() |
23ec95c6a6 | ||
![]() |
a396764880 | ||
![]() |
7515592564 | ||
![]() |
54381968a5 | ||
![]() |
6ad89b9914 | ||
![]() |
b7e2d6c7ca | ||
![]() |
3bf91e20c5 | ||
![]() |
821ece2e88 | ||
![]() |
b5114ace4f | ||
![]() |
4a626d4aab | ||
![]() |
96d8ea7850 | ||
![]() |
b17af5804e | ||
![]() |
be67ed96fb | ||
![]() |
f3e32765dd | ||
![]() |
633c9779c9 | ||
![]() |
c2ba8aaa89 | ||
![]() |
dff7e384db | ||
![]() |
d50d0f85ab | ||
![]() |
7f6370c891 | ||
![]() |
1f94d6154b | ||
![]() |
76f31e3b90 | ||
![]() |
e06932a6f4 | ||
![]() |
632bcc63f7 | ||
![]() |
8dcfc16246 | ||
![]() |
3acb605685 |
8
.dockerignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
**/db.sqlite
|
||||
**/node_modules
|
||||
**/screenshots
|
||||
.gitignore
|
||||
.github
|
||||
.git
|
||||
Dockerfile
|
||||
docker-compose.yaml
|
3
.github/FUNDING.yml
vendored
|
@ -1,2 +1 @@
|
|||
github: [lllllllillllllillll]
|
||||
patreon: DweebUI
|
||||
patreon: DweebUI
|
2
.github/dependabot.yml
vendored
|
@ -15,6 +15,6 @@ updates:
|
|||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "🤖 Dependencies"
|
||||
|
|
2
.github/workflows/docker.yml
vendored
|
@ -55,7 +55,7 @@ jobs:
|
|||
|
||||
# Build image and only publish if not a Pull Request
|
||||
- name: Build and Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
timeout-minutes: 30
|
||||
with:
|
||||
context: .
|
||||
|
|
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
**/db.sqlite
|
||||
**/node_modules
|
||||
**/appdata
|
||||
.github
|
||||
.git
|
94
CHANGELOG.md
|
@ -1,3 +1,97 @@
|
|||
## v0.60 (June 9th 2024) - Permissions system and import templates
|
||||
* Converted JS template literals into HTML.
|
||||
* Converted modals into HTML/HTMX.
|
||||
* Moved functions into dashboard controller.
|
||||
* New - Modal placeholder with loading spinner.
|
||||
* Container cards now update independently.
|
||||
* Container cards now display pending action (starting, stopping, pausing, restarting).
|
||||
* User avatars are now automatically generated.
|
||||
* Updated database models.
|
||||
* New - Multi-user permission system.
|
||||
* Refactored dashboard to support multiple users.
|
||||
* New - Banner alerts.
|
||||
* New - Template importing (*.yml, *.yaml, *.json).
|
||||
* Improved app search.
|
||||
* New - Search by category.
|
||||
* Updated dependencies.
|
||||
* Removed warning from the bottom of the registration page. Will be added back in a different location.
|
||||
* New - admin checks, session checks, and permission checks for router.
|
||||
* Added titles to activity indicators.
|
||||
* Created Github Wiki.
|
||||
* Added image pull to images page.
|
||||
* Images and volumes display 'In use'.
|
||||
* Images display tag.
|
||||
* Image pull gets latest if not set.
|
||||
* Updated buttons to trigger from 'mousedown' (John Carmack + Theo told me to).
|
||||
* Volumes page displays type (Volume or Bind).
|
||||
* Volume button is now functional.
|
||||
|
||||
## 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.
|
||||
* Updated DweebUI logo.
|
||||
* Visual tweaks to login and registration pages.
|
||||
* Added .gitignore and .dockerignore files.
|
||||
* Syslogs - View logs for sign-in and registration attempts. :new:
|
||||
* Docker socket now uses default connection.
|
||||
* Updated Users page displays 'inactive' if no sign-ins within 30 days.
|
||||
* Dashboard updates now triggered by Docker events.
|
||||
* Massive reduction in the amount of HTML, CSS, and JS on client side.
|
||||
* Container graphs are significantly more efficent and no longer use localStorage.
|
||||
* Made dark mode the default theme.
|
||||
* Created intervals to allow application to idle or scale with more users.
|
||||
* Pages for images, volumes, and networks. :new:
|
||||
* Localized fonts.
|
||||
* CORS.
|
||||
* Testing with Mocha and Supertest.
|
||||
* Created Portal page. :new:
|
||||
|
||||
|
||||
## <del>v0.09 (dev)</del> dead. (It had so many problems that I essentially rewrote everything)
|
||||
* Added authentication middleware to router.
|
||||
* Added gzip compression.
|
||||
* Added PM2.
|
||||
* Added Helmet.
|
||||
* Fixed missing session data.
|
||||
* Reduced sqlite queries.
|
||||
|
||||
## v0.08 (Dec 15th 2023)
|
||||
* Updates to compose file and instructions from [steveiliop56](https://github.com/steveiliop56)
|
||||
* Added SECRET field to compose file as a basic security measure.
|
||||
* Visibility button to hide containers or reset view.
|
||||
* Container link now uses server IP address.
|
||||
* More compact container card, with style options planned.
|
||||
* Improved log view.
|
||||
* Removed VPN, Firewall, and VNC buttons.
|
||||
* Updated dependencies (Sequelize 6.35.2)
|
||||
* Fixed web pages not using the "public" static folder.
|
||||
* Small tweaks to router.
|
||||
* Replaced the default icon shown for missing icons (docker.png).
|
||||
|
||||
## v0.07 (Dec 8th 2023)
|
||||
* View container logs.
|
||||
* Removed Redis.
|
||||
|
|
24
Dockerfile
|
@ -1,21 +1,7 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:21-alpine
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
FROM node:22-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 app.js
|
||||
CMD node server.js
|
BIN
DweebUI.png
Before Width: | Height: | Size: 114 KiB |
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 lllllllillllllillll
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
130
README.md
|
@ -1,75 +1,95 @@
|
|||
# DweebUI
|
||||
DweebUI is a simple Docker web interface created with javascript and node.js
|
||||
|
||||
Pre-Pre-Pre-Pre-Pre Alpha v0.07 ( :fire: Experimental. Don't install on any servers you care about :fire: )
|
||||
|
||||
[](https://github.com/lllllllillllllillll)
|
||||
[](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
|
||||
[](https://github.com/lllllllillllllillll)
|
||||
|
||||
* I haven't used Github very much and I'm still new to javascript.
|
||||
* This is the first project I've ever released and I'm sure it's full of plenty of bugs and mistakes.
|
||||
* I probably should have waited a lot longer to share this :|
|
||||
|
||||
<a href="https://raw.githubusercontent.com//lllllllillllllillll/DweebUI/main/screenshots/dashboard.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard.png" width="50%"/></a>
|
||||
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="50%"/></a>
|
||||
|
||||
<h3 align="center"><img width="150" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/public/img/logo.png"></h3>
|
||||
<h4 align="center">DweebUI Beta v0.60 ( :fire: Experimental :fire: )</h4>
|
||||
<h3 align="center">Free and Open-Source WebUI For Managing Your Containers.</h3>
|
||||
<p align="center">
|
||||
<a href=""><img src="https://img.shields.io/github/stars/lllllllillllllillll/DweebUI?style=flat"/></a>
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI%2Fdev"><img src="https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI%2Fdev"/></a>
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI%2Fdev"><img src="https://img.shields.io/github/last-commit/lllllllillllllillll/DweebUI%2Fdev"/></a>
|
||||
<a href="https://hub.docker.com/r/lllllllillllllillll/dweebui"><img src="https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui"/></a>
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE"><img src="https://img.shields.io/github/license/lllllllillllllillll/DweebUI"/></a>
|
||||
<a href="https://www.reddit.com/r/dweebui"><img src="https://img.shields.io/badge/reddit-orange"/></a>
|
||||
<a href="https://www.buymeacoffee.com/lllllllillllllillll"><img src="https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee"/></a>
|
||||
</p>
|
||||
<h3 align="center"><img width="800" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png"></h3>
|
||||
|
||||
## Features
|
||||
* [x] Dashboard provides server metrics (cpu, ram, network, disk) and container controls on a single page.
|
||||
|
||||
* [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
|
||||
* [x] Multi-user support with permissions system.
|
||||
* [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
|
||||
* [x] Windows, Linux, and MacOS compatable.
|
||||
* [x] Light/Dark Mode.
|
||||
* [x] Mobile Friendly.
|
||||
* [x] Manage your Docker networks, images, and volumes.
|
||||
* [x] Easy to install app templates.
|
||||
* [x] Automatically persists data in docker volumes if bind mount isn't used.
|
||||
* [x] Proxy manager for Caddy. (Optional)
|
||||
* [x] Partial Portainer Template Support (Network Mode, Ports, Volumes, Enviroment Variables, Labels, Commands, Restart Policy, Nvidia Hardware Acceleration).
|
||||
* [x] Multi-User built-in.
|
||||
* [ ] User pages: Shortcuts, Requests, Support. (planned)
|
||||
* [x] Support for Windows, Linux, and MacOS.
|
||||
* [ ] Import compose files. (planned)
|
||||
* [x] Javascript, Node.js, and Express.
|
||||
* [x] Docker Compose Support.
|
||||
* [ ] Update containers (planned).
|
||||
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
|
||||
* [ ] Manage your Docker networks, images, and volumes. (planned)
|
||||
* [ ] Preset variables. (planned)
|
||||
* [ ] VPN, VPS, and Firewall Toggles. (planned)
|
||||
* [ ] Offline Mode. (planned)
|
||||
* [ ] Preset variables (planned).
|
||||
* [ ] Themes (planned).
|
||||
|
||||
## About
|
||||
|
||||
* I started this as a personal project to get more familiar with Javascript and Node.js, so there may be some rough edges and spaghetti code.
|
||||
* I'm open to any contributions but you may want to wait until I reach v1.0 first.
|
||||
* Please post issues and discussions so I know what bugs and features to focus on.
|
||||
* DweebUI is a management interface and should not be directly exposed to the internet.
|
||||
|
||||
## Setup
|
||||
|
||||
* Docker compose.yaml:
|
||||
Docker Compose:
|
||||
```
|
||||
version: "3.9"
|
||||
services:
|
||||
dweebui:
|
||||
container_name: DweebUI
|
||||
image: lllllllillllllillll/dweebui:v0.07
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 8000
|
||||
# Proxy_Manager: enabled
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
- caddyfiles:/app/caddyfiles
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
dweebui:
|
||||
container_name: dweebui
|
||||
image: lllllllillllllillll/dweebui
|
||||
environment:
|
||||
PORT: 8000
|
||||
SECRET: MrWiskers
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 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
|
||||
|
||||
volumes:
|
||||
dweebui:
|
||||
caddyfiles:
|
||||
dweebui:
|
||||
|
||||
networks:
|
||||
dweebui_net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
* Using setup.sh:
|
||||
```
|
||||
Extract DweebUI.zip and navigate to /DweebUI
|
||||
cd DweebUI
|
||||
chmod +x setup.sh
|
||||
sudo ./setup.sh
|
||||
```
|
||||
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
|
||||
|
||||
Compose setup:
|
||||
|
||||
## Credit
|
||||
* Paste the above content into a file named ```docker-compose.yml``` then place it in a folder named ```dweebui```.
|
||||
* Open a terminal in the ```dweebui``` folder, then enter ```docker compose up -d```.
|
||||
* You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
|
||||
|
||||
Configuration:
|
||||
|
||||
* `PORT` - Specifies which port the service binds to on startup. Default is `8000`.
|
||||
* `SECRET` - A shared secret used by the registration page.
|
||||
|
||||
## Credits
|
||||
|
||||
* Dockerode and dockerode-compose by Apocas: https://github.com/apocas/dockerode
|
||||
* UI was built using HTML and CSS elements from https://tabler.io/
|
||||
* Apps template based on Portainer template provided by Lissy93: https://github.com/Lissy93/portainer-templates
|
||||
* Icons from Walkxcode with some renames and additions: https://github.com/walkxcode/dashboard-icons
|
||||
|
||||
|
||||
## Supporters
|
||||
|
||||
* MM (Patreon)
|
||||
* PD (Buymeacoffee)
|
||||
|
|
112
app.js
|
@ -1,112 +0,0 @@
|
|||
// Express
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
const session = require("express-session");
|
||||
const PORT = process.env.PORT || 8000;
|
||||
|
||||
// Router
|
||||
const routes = require("./routes");
|
||||
|
||||
// Functions and variables
|
||||
const { serverStats, containerList, containerStats, containerAction, containerLogs } = require('./functions/system');
|
||||
let sentList, clicked;
|
||||
app.locals.site_list = '';
|
||||
|
||||
// Configure Session
|
||||
const sessionMiddleware = session({
|
||||
secret: "keyboard cat",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie:{
|
||||
secure:false, // Only set to true if you are using HTTPS.
|
||||
httpOnly:false, // Only set to true if you are using HTTPS.
|
||||
maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
|
||||
}
|
||||
})
|
||||
|
||||
// Middleware
|
||||
app.set('view engine', 'ejs');
|
||||
app.use([
|
||||
express.static("public"),
|
||||
express.json(),
|
||||
express.urlencoded({ extended: true }),
|
||||
sessionMiddleware,
|
||||
routes
|
||||
]);
|
||||
|
||||
// Start Express server
|
||||
const server = app.listen(PORT, async () => {
|
||||
console.log(`App listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Start Socket.io
|
||||
const io = require('socket.io')(server);
|
||||
io.engine.use(sessionMiddleware);
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
|
||||
// Set user session
|
||||
const user_session = socket.request.session;
|
||||
console.log(`${user_session.user} connected from ${socket.handshake.headers.host} ${socket.handshake.address}`);
|
||||
|
||||
// Check if a list of containers or an install card needs to be sent
|
||||
if (sentList != null) { socket.emit('cards', sentList); }
|
||||
if((app.locals.install != '') && (app.locals.install != null)){ socket.emit('install', app.locals.install); }
|
||||
|
||||
// Send server metrics
|
||||
let ServerStats = setInterval(async () => {
|
||||
socket.emit('metrics', await serverStats());
|
||||
}, 1000);
|
||||
|
||||
// Send list of containers
|
||||
let ContainerList = setInterval(async () => {
|
||||
let cardList = await containerList();
|
||||
if (sentList !== cardList) {
|
||||
sentList = cardList;
|
||||
app.locals.install = '';
|
||||
socket.emit('cards', cardList);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Send container metrics
|
||||
let ContainerStats = setInterval(async () => {
|
||||
let stats = await containerStats();
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
socket.emit('containerStats', stats[i]);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Container controls
|
||||
socket.on('clicked', (data) => {
|
||||
if (clicked == true) { return; } clicked = true;
|
||||
let buttonPress = {
|
||||
user: socket.request.session.user,
|
||||
role: socket.request.session.role,
|
||||
action: data.action,
|
||||
container: data.container,
|
||||
state: data.state
|
||||
}
|
||||
containerAction(buttonPress);
|
||||
clicked = false;
|
||||
});
|
||||
|
||||
|
||||
// Container logs
|
||||
socket.on('logs', (data) => {
|
||||
containerLogs(data.container)
|
||||
.then(logs => {
|
||||
socket.emit('logString', logs);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
// On disconnect
|
||||
socket.on('disconnect', () => {
|
||||
clearInterval(ServerStats);
|
||||
clearInterval(ContainerList);
|
||||
clearInterval(ContainerStats);
|
||||
});
|
||||
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
import ./sites/*
|
|
@ -1,994 +0,0 @@
|
|||
function appCard(data) {
|
||||
|
||||
// make data.title lowercase
|
||||
let app_name = data.name || data.title.toLowerCase();
|
||||
let shortened_name = "";
|
||||
let shortened_desc = data.description.slice(0, 60) + "...";
|
||||
let modal = app_name.replaceAll(" ", "-");
|
||||
let form_id = app_name.replaceAll("-", "_");
|
||||
let note = data.note ? data.note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let description = data.description.replaceAll(". ", ".\n") || "no description available";
|
||||
let command = data.command ? data.command : "";
|
||||
let command_check = command ? "checked" : "";
|
||||
let privileged = data.privileged || "";
|
||||
let privileged_check = privileged ? "checked" : "";
|
||||
let repository = data.repository || "";
|
||||
let source = data.image || "";
|
||||
|
||||
|
||||
// if data.network is set to host, bridge, or docker set the radio button to checked
|
||||
let net_host, net_bridge, net_docker = '';
|
||||
let net_name = 'AppBridge';
|
||||
|
||||
if (data.network == 'host') {
|
||||
net_host = 'checked';
|
||||
} else if (data.network) {
|
||||
net_bridge = 'checked';
|
||||
net_name = data.network;
|
||||
} else {
|
||||
net_docker = 'checked';
|
||||
}
|
||||
|
||||
|
||||
if (data.title.length > 28) {
|
||||
shortened_name = (data.title).slice(0, 25) + "...";
|
||||
}
|
||||
else {
|
||||
shortened_name = data.title;
|
||||
}
|
||||
|
||||
|
||||
if (repository != "") {
|
||||
source = (`${repository.url}/raw/master/${repository.stackfile}`);
|
||||
}
|
||||
|
||||
|
||||
function CatagoryColor(category) {
|
||||
switch (category) {
|
||||
case 'Other':
|
||||
return '<span class="badge bg-blue-lt">Other</span> ';
|
||||
case 'Productivity':
|
||||
return '<span class="badge bg-blue-lt">Productivity</span> ';
|
||||
case 'Tools':
|
||||
return '<span class="badge bg-blue-lt">Tools</span> ';
|
||||
case 'Dashboard':
|
||||
return '<span class="badge bg-blue-lt">Dashboard</span> ';
|
||||
case 'Communication':
|
||||
return '<span class="badge bg-azure-lt">Communication</span> ';
|
||||
case 'Media':
|
||||
return '<span class="badge bg-azure-lt">Media</span> ';
|
||||
case 'CMS':
|
||||
return '<span class="badge bg-azure-lt">CMS</span> ';
|
||||
case 'Monitoring':
|
||||
return '<span class="badge bg-indigo-lt">Monitoring</span> ';
|
||||
case 'LDAP':
|
||||
return '<span class="badge bg-purple-lt">LDAP</span> ';
|
||||
case 'Arr':
|
||||
return '<span class="badge bg-purple-lt">Arr</span> ';
|
||||
case 'Database':
|
||||
return '<span class="badge bg-red-lt">Database</span> ';
|
||||
case 'Paid':
|
||||
return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
|
||||
case 'Gaming':
|
||||
return '<span class="badge bg-pink-lt">Gaming</span> ';
|
||||
case 'Finance':
|
||||
return '<span class="badge bg-orange-lt">Finance</span> ';
|
||||
case 'Networking':
|
||||
return '<span class="badge bg-yellow-lt">Networking</span> ';
|
||||
case 'Authentication':
|
||||
return '<span class="badge bg-lime-lt">Authentication</span> ';
|
||||
case 'Development':
|
||||
return '<span class="badge bg-green-lt">Development</span> ';
|
||||
case 'Media Server':
|
||||
return '<span class="badge bg-teal-lt">Media Server</span> ';
|
||||
case 'Downloaders':
|
||||
return '<span class="badge bg-cyan-lt">Downloaders</span> ';
|
||||
default:
|
||||
return ''; // default to other if the category is not recognized
|
||||
}
|
||||
}
|
||||
|
||||
// set data.catagories to 'other' if data.catagories is empty or undefined
|
||||
if (data.categories == null || data.categories == undefined || data.categories == '') {
|
||||
data.categories = ['Other'];
|
||||
}
|
||||
|
||||
let categories = '';
|
||||
|
||||
for (let i = 0; i < data.categories.length; i++) {
|
||||
categories += CatagoryColor(data.categories[i]);
|
||||
}
|
||||
|
||||
if (data.restart_policy == null) {
|
||||
data.restart_policy = 'unless-stopped';
|
||||
}
|
||||
|
||||
let ports_data = [], volumes_data = [], env_data = [], label_data = [];
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
// Get port details
|
||||
try {
|
||||
let ports = data.ports[i];
|
||||
let port_check = ports ? "checked" : "";
|
||||
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
|
||||
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
|
||||
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
|
||||
|
||||
// remove /tcp or /udp from port_external if it exists
|
||||
if (port_external.includes("/")) {
|
||||
port_external = port_external.split("/")[0];
|
||||
}
|
||||
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: port_external,
|
||||
internal: port_internal,
|
||||
protocol: port_protocol
|
||||
});
|
||||
} catch {
|
||||
ports_data.push({
|
||||
check: "",
|
||||
external: "",
|
||||
internal: "",
|
||||
protocol: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get volume details
|
||||
try {
|
||||
let volumes = data.volumes[i];
|
||||
let volume_check = volumes ? "checked" : "";
|
||||
let volume_bind = volumes.bind ? volumes.bind : "";
|
||||
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
|
||||
let volume_readwrite = "rw";
|
||||
|
||||
if (volumes.readonly == true) {
|
||||
volume_readwrite = "ro";
|
||||
}
|
||||
|
||||
volumes_data.push({
|
||||
check: volume_check,
|
||||
bind: volume_bind,
|
||||
container: volume_container,
|
||||
readwrite: volume_readwrite
|
||||
});
|
||||
} catch {
|
||||
volumes_data.push({
|
||||
check: "",
|
||||
bind: "",
|
||||
container: "",
|
||||
readwrite: ""
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Get environment details
|
||||
try {
|
||||
let env = data.env[i];
|
||||
let env_check = "";
|
||||
let env_default = env.default ? env.default : "";
|
||||
if (env.set) { env_default = env.set;}
|
||||
let env_description = env.description ? env.description : "";
|
||||
let env_label = env.label ? env.label : "";
|
||||
let env_name = env.name ? env.name : "";
|
||||
|
||||
env_data.push({
|
||||
check: env_check,
|
||||
default: env_default,
|
||||
description: env_description,
|
||||
label: env_label,
|
||||
name: env_name
|
||||
});
|
||||
} catch {
|
||||
env_data.push({
|
||||
check: "",
|
||||
default: "",
|
||||
description: "",
|
||||
label: "",
|
||||
name: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get label details
|
||||
|
||||
try {
|
||||
let label = data.labels[i];
|
||||
let label_check = "";
|
||||
let label_name = label.name ? label.name : "";
|
||||
let label_value = label.value ? label.value : "";
|
||||
|
||||
label_data.push({
|
||||
check: label_check,
|
||||
name: label_name,
|
||||
value: label_value
|
||||
});
|
||||
} catch {
|
||||
label_data.push({
|
||||
check: "",
|
||||
name: "",
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return `
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 text-center">
|
||||
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"></img></span>
|
||||
<h3 class="m-0 mb-1"><a href="#">${shortened_name}</a></h3>
|
||||
<div class="text-secondary">${shortened_desc}</div>
|
||||
<div class="mt-3">
|
||||
${categories}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-info"><!-- Download SVG icon from http://tabler-icons.io/i/mail -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
|
||||
Learn More
|
||||
</a>
|
||||
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-install"><!-- Download SVG icon from http://tabler-icons.io/i/phone -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
|
||||
Install
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="${modal}-info" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="modal-title">${data.title}</div>
|
||||
<div>${description}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="${modal}-install" tabindex="-1" role="dialog" 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">Install ${data.title}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<pre class="text-secondary">${note}</pre>
|
||||
|
||||
<form action="/install" name="${form_id}_install" id="${form_id}_install" method="POST">
|
||||
|
||||
<div class="row mb-3 align-items-end">
|
||||
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label">Container Name: </label>
|
||||
<input type="text" class="form-control" name="service_name" value="${app_name}" hidden/>
|
||||
<input type="text" class="form-control" name="name" value="${app_name}"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Image: </label>
|
||||
<input type="text" class="form-control" name="image" value="${source}"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Restart Policy: </label>
|
||||
<select class="form-select" name="restart_policy">
|
||||
<option value="${data.restart_policy}" selected hidden>${data.restart_policy}</option>
|
||||
<option value="unless-stopped">unless-stopped</option>
|
||||
<option value="on-failure">on-failure</option>
|
||||
<option value="never">never</option>
|
||||
<option value="always">always</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Network Mode</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" ${net_host}>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Host Network</span>
|
||||
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="${net_name}" class="form-selectgroup-input" ${net_bridge}>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
|
||||
<span class="d-block text-secondary">Containers can communicate using names.</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" ${net_docker}>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
|
||||
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="accordion" id="${modal}-accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-1">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
|
||||
Ports
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port0" type="checkbox" ${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="port1" 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="port2" 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="port3" 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="port4" 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="port5" 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="volume0" 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="volume1" 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="volume2" 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="volume3" 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="volume4" 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="volume5" 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="env0" ${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="env1" ${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="env2" ${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="env3" ${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="env4" ${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="env5" ${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="env6" ${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="env7" ${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="env8" ${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="env9" ${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="env10" ${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="env11" ${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="label0" ${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="label1" ${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="label2" ${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="label3" ${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="label4" ${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="label5" ${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="label6" ${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="label7" ${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="label8" ${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="label9" ${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="label10" ${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="label11" ${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="command_check" type="checkbox" ${command_check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Command</label>
|
||||
<input type="text" class="form-control" name="command" value="${command}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="privileged" type="checkbox" ${privileged_check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Privileged Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
|
||||
<input type="submit" form="${form_id}_install" class="btn btn-success" value="Install"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = { appCard };
|
|
@ -1,18 +0,0 @@
|
|||
function siteCard(type, domain, host, port, id) {
|
||||
|
||||
let site = `<tr>`
|
||||
site += `<td><input class="form-check-input m-0 align-middle" name="select${id}" value="${domain}" type="checkbox" aria-label="Select invoice"></td>`
|
||||
site += `<td><span class="text-muted">${id}</span></td>`
|
||||
site += `<td><a href="https://${domain}" class="text-reset" tabindex="-1" target="_blank">${domain}</a></td>`
|
||||
site += `<td>${type}</td>`
|
||||
site += `<td>${host}</td>`
|
||||
site += `<td>${port}</td>`
|
||||
site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
|
||||
site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
|
||||
site += `<td class="text-end"><a class="btn" href="#"> Edit </a></td>`
|
||||
site += `</tr>`
|
||||
|
||||
return site;
|
||||
}
|
||||
|
||||
module.exports = { siteCard };
|
22
compose.yaml
|
@ -1,19 +1,27 @@
|
|||
version: "3.9"
|
||||
services:
|
||||
dweebui:
|
||||
container_name: DweebUI
|
||||
image: lllllllillllllillll/dweebui:v0.07
|
||||
container_name: dweebui
|
||||
image: lllllllillllllillll/dweebui:v0.60
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 8000
|
||||
# Proxy_Manager: enabled
|
||||
SECRET: MrWiskers
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
- caddyfiles:/app/caddyfiles
|
||||
- dweebui:/app/config
|
||||
# Docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Podman socket
|
||||
#- /run/podman/podman.sock:/var/run/docker.sock
|
||||
|
||||
networks:
|
||||
- dweebui_net
|
||||
|
||||
volumes:
|
||||
dweebui:
|
||||
caddyfiles:
|
||||
|
||||
networks:
|
||||
dweebui_net:
|
||||
driver: bridge
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
const User = require('../database/UserModel');
|
||||
import { User } from "../database/models.js";
|
||||
|
||||
export const Account = async (req, res) => {
|
||||
|
||||
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
|
||||
res.render("account", {
|
||||
first_name: user.name,
|
||||
last_name: user.name,
|
||||
name: user.name,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
alert: '',
|
||||
});
|
||||
|
||||
|
||||
exports.Account = async function(req, res) {
|
||||
if (req.session.user) {
|
||||
// Get the user.
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
// Render the home page
|
||||
res.render("pages/account", {
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
isLoggedIn: true
|
||||
});
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,192 +1,587 @@
|
|||
const User = require('../database/UserModel');
|
||||
const { appCard } = require('../components/appCard')
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
|
||||
import { parse } from 'yaml';
|
||||
import multer from 'multer';
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
const { install, uninstall } = require('../functions/package_manager');
|
||||
const upload = multer({storage: multer.diskStorage({
|
||||
destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
|
||||
filename: function (req, file, cb) { cb(null, file.originalname) },
|
||||
})});
|
||||
|
||||
// import { install, uninstall } from '../functions/package_manager';
|
||||
let alert = '';
|
||||
let templates_global = '';
|
||||
let json_templates = '';
|
||||
let remove_button = '';
|
||||
|
||||
const templates_json = require('../templates.json');
|
||||
let templates = templates_json.templates;
|
||||
|
||||
// sort templates alphabetically
|
||||
templates = templates.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
export const Apps = async (req, res) => {
|
||||
|
||||
let page = Number(req.params.page) || 1;
|
||||
let template_param = req.params.template || 'default';
|
||||
|
||||
exports.Apps = async function(req, res) {
|
||||
if (req.session.role == "admin") {
|
||||
if ((template_param != 'default') && (template_param != 'compose')) {
|
||||
remove_button = `<a href="/remove_template/${template_param}" class="btn" hx-confirm="Are you sure you want to remove this template?">Remove</a>`;
|
||||
} else {
|
||||
remove_button = '';
|
||||
}
|
||||
|
||||
// Get the user.
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
|
||||
let page = Number(req.query.page) || 1;
|
||||
let list_start = (page - 1) * 28;
|
||||
let list_end = (page * 28);
|
||||
let last_page = Math.ceil(templates.length / 28);
|
||||
|
||||
let prev = '/apps?page=' + (page - 1);
|
||||
let next = '/apps?page=' + (page + 1);
|
||||
if (page == 1) {
|
||||
prev = '/apps?page=' + (page);
|
||||
}
|
||||
if (page == last_page) {
|
||||
next = '/apps?page=' + (page);
|
||||
}
|
||||
|
||||
let apps_list = '';
|
||||
for (let i = list_start; i < list_end && i < templates.length; i++) {
|
||||
let app_card = appCard(templates[i]);
|
||||
|
||||
apps_list += app_card;
|
||||
}
|
||||
|
||||
// Render the home page
|
||||
res.render("pages/apps", {
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
isLoggedIn: true,
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: templates.length,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list
|
||||
});
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
json_templates = '';
|
||||
let json_files = readdirSync('templates/json/');
|
||||
for (let i = 0; i < json_files.length; i++) {
|
||||
if (json_files[i] != 'default.json') {
|
||||
let filename = json_files[i].split('.')[0];
|
||||
let link = `<li><a class="dropdown-item" href="/apps/1/${filename}">${filename}</a></li>`
|
||||
json_templates += link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let apps_list = '';
|
||||
let app_count = '';
|
||||
|
||||
let list_start = (page - 1) * 28;
|
||||
let list_end = (page * 28);
|
||||
let last_page = '';
|
||||
|
||||
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
|
||||
|
||||
|
||||
|
||||
exports.searchApps = async function(req, res) {
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
// Get the user.
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
|
||||
let page = Number(req.query.page) || 1;
|
||||
let list_start = (page - 1) * 28;
|
||||
let list_end = (page * 28);
|
||||
let last_page = Math.ceil(templates.length / 28);
|
||||
|
||||
let prev = '/apps?page=' + (page - 1);
|
||||
let next = '/apps?page=' + (page + 1);
|
||||
if (page == 1) {
|
||||
prev = '/apps?page=' + (page);
|
||||
}
|
||||
if (page == last_page) {
|
||||
next = '/apps?page=' + (page);
|
||||
}
|
||||
|
||||
let apps_list = '';
|
||||
let search_results = [];
|
||||
|
||||
let search = req.body.search;
|
||||
|
||||
// split value of search into an array of words
|
||||
search = search.split(' ');
|
||||
try {console.log(search[0]);} catch (error) {}
|
||||
try {console.log(search[1]);} catch (error) {}
|
||||
try {console.log(search[2]);} catch (error) {}
|
||||
|
||||
function searchTemplates(word) {
|
||||
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
if ((templates[i].description.includes(word)) || (templates[i].name.includes(word)) || (templates[i].title.includes(word))) {
|
||||
search_results.push(templates[i]);
|
||||
}
|
||||
}
|
||||
// console.log(search_results);
|
||||
}
|
||||
|
||||
searchTemplates(search);
|
||||
|
||||
for (let i = 0; i < search_results.length; i++) {
|
||||
let app_card = appCard(search_results[i]);
|
||||
apps_list += app_card;
|
||||
}
|
||||
|
||||
// Render the home page
|
||||
res.render("pages/apps", {
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
isLoggedIn: true,
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: templates.length,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list
|
||||
});
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
let prev = '/apps/' + (page - 1) + '/' + template_param;
|
||||
let next = '/apps/' + (page + 1) + '/' + template_param;
|
||||
if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
|
||||
if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.Install = async function (req, res) {
|
||||
if (template_param == 'compose') {
|
||||
let compose_files = readdirSync('templates/compose/');
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
app_count = compose_files.length;
|
||||
last_page = Math.ceil(compose_files.length/28);
|
||||
|
||||
console.log(`Starting install for: ${req.body.name}`)
|
||||
compose_files.forEach(file => {
|
||||
if (file == '.gitignore') { return; }
|
||||
|
||||
install(req.body);
|
||||
let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
|
||||
let compose_data = parse(compose);
|
||||
let service_name = Object.keys(compose_data.services)
|
||||
let container = compose_data.services[service_name].container_name;
|
||||
let image = compose_data.services[service_name].image;
|
||||
|
||||
let container_info = {
|
||||
name: req.body.name,
|
||||
service: req.body.service_name,
|
||||
state: 'installing',
|
||||
image: req.body.image,
|
||||
restart_policy: req.body.restart_policy
|
||||
}
|
||||
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
|
||||
appCard = appCard.replace(/AppName/g, service_name);
|
||||
appCard = appCard.replace(/AppShortName/g, service_name);
|
||||
appCard = appCard.replace(/AppDesc/g, 'Compose File');
|
||||
appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
|
||||
appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
|
||||
appCard = appCard.replace(/AppType/g, 'compose');
|
||||
apps_list += appCard;
|
||||
});
|
||||
} else {
|
||||
|
||||
let installCard = dashCard(container_info);
|
||||
let template_file = readFileSync(`./templates/json/${template_param}.json`);
|
||||
let templates = JSON.parse(template_file).templates;
|
||||
templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
|
||||
app_count = templates.length;
|
||||
|
||||
req.app.locals.install = installCard;
|
||||
templates_global = templates;
|
||||
|
||||
|
||||
// Redirect to the home page
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
apps_list = '';
|
||||
for (let i = list_start; i < list_end && i < templates.length; i++) {
|
||||
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
|
||||
let name = templates[i].name || templates[i].title.toLowerCase();
|
||||
let title = templates[i].title || templates[i].name;
|
||||
let desc = templates[i].description.slice(0, 60) + "...";
|
||||
let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
|
||||
let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let image = templates[i].image;
|
||||
let logo = templates[i].logo;
|
||||
let categories = '';
|
||||
// set data.catagories to 'other' if data.catagories is empty or undefined
|
||||
if (templates[i].categories == null || templates[i].categories == undefined || templates[i].categories == '') {
|
||||
templates[i].categories = ['Other'];
|
||||
}
|
||||
// loop through the categories and add the badge to the card
|
||||
for (let j = 0; j < templates[i].categories.length; j++) {
|
||||
categories += CatagoryColor(templates[i].categories[j]);
|
||||
}
|
||||
appCard = appCard.replace(/AppName/g, name);
|
||||
appCard = appCard.replace(/AppTitle/g, title);
|
||||
appCard = appCard.replace(/AppShortName/g, name);
|
||||
appCard = appCard.replace(/AppDesc/g, desc);
|
||||
appCard = appCard.replace(/AppLogo/g, logo);
|
||||
appCard = appCard.replace(/AppCategories/g, categories);
|
||||
appCard = appCard.replace(/AppType/g, 'json');
|
||||
apps_list += appCard;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
res.render("apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: app_count,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list,
|
||||
alert: alert,
|
||||
template_list: '',
|
||||
json_templates: json_templates,
|
||||
pages: pages,
|
||||
remove_button: remove_button
|
||||
});
|
||||
alert = '';
|
||||
}
|
||||
|
||||
export const removeTemplate = async (req, res) => {
|
||||
let template = req.params.template;
|
||||
unlinkSync(`templates/json/${template}.json`);
|
||||
res.redirect('/apps');
|
||||
}
|
||||
|
||||
|
||||
export const appSearch = async (req, res) => {
|
||||
|
||||
exports.Uninstall = async function (req, res) {
|
||||
let search = req.body.search;
|
||||
|
||||
let page = Number(req.params.page) || 1;
|
||||
|
||||
let template_param = req.params.template || 'default';
|
||||
|
||||
let template_file = readFileSync(`./templates/json/${template_param}.json`);
|
||||
|
||||
let templates = JSON.parse(template_file).templates;
|
||||
|
||||
templates = templates.sort((a, b) => {
|
||||
if (a.name < b.name) { return -1; }
|
||||
});
|
||||
|
||||
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
|
||||
|
||||
|
||||
let list_start = (page-1)*28;
|
||||
let list_end = (page*28);
|
||||
let last_page = Math.ceil(templates.length/28);
|
||||
let prev = '/apps/' + (page-1);
|
||||
let next = '/apps/' + (page+1);
|
||||
if (page == 1) { prev = '/apps/' + (page); }
|
||||
if (page == last_page) { next = '/apps/' + (page); }
|
||||
|
||||
|
||||
let apps_list = '';
|
||||
let results = [];
|
||||
let [cat_1, cat_2, cat_3] = ['','',''];
|
||||
|
||||
function searchTemplates(terms) {
|
||||
terms = terms.toLowerCase();
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
if (templates[i].categories) {
|
||||
if (templates[i].categories[0]) {
|
||||
cat_1 = (templates[i].categories[0]).toLowerCase();
|
||||
}
|
||||
if (templates[i].categories[1]) {
|
||||
cat_2 = (templates[i].categories[1]).toLowerCase();
|
||||
}
|
||||
if (templates[i].categories[2]) {
|
||||
cat_3 = (templates[i].categories[2]).toLowerCase();
|
||||
}
|
||||
}
|
||||
if ((templates[i].description.includes(terms)) || (templates[i].name.includes(terms)) || (templates[i].title.includes(terms)) || (cat_1.includes(terms)) || (cat_2.includes(terms)) || (cat_3.includes(terms))){
|
||||
results.push(templates[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
searchTemplates(search);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
|
||||
let name = results[i].name || results[i].title.toLowerCase();
|
||||
let desc = results[i].description.slice(0, 60) + "...";
|
||||
let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
|
||||
let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let image = results[i].image;
|
||||
let logo = results[i].logo;let categories = '';
|
||||
// set data.catagories to 'other' if data.catagories is empty or undefined
|
||||
if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
|
||||
results[i].categories = ['Other'];
|
||||
}
|
||||
// loop through the categories and add the badge to the card
|
||||
for (let j = 0; j < results[i].categories.length; j++) {
|
||||
categories += CatagoryColor(results[i].categories[j]);
|
||||
}
|
||||
appCard = appCard.replace(/AppName/g, name);
|
||||
appCard = appCard.replace(/AppShortName/g, name);
|
||||
appCard = appCard.replace(/AppDesc/g, desc);
|
||||
appCard = appCard.replace(/AppLogo/g, logo);
|
||||
appCard = appCard.replace(/AppCategories/g, categories);
|
||||
appCard = appCard.replace(/AppType/g, 'json');
|
||||
|
||||
apps_list += appCard;
|
||||
}
|
||||
res.render("apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: results.length,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list,
|
||||
alert: alert,
|
||||
template_list: '',
|
||||
json_templates: json_templates,
|
||||
pages: pages,
|
||||
remove_button: remove_button
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function CatagoryColor(category) {
|
||||
switch (category) {
|
||||
case 'Other':
|
||||
return '<span class="badge bg-blue-lt">Other</span> ';
|
||||
case 'Productivity':
|
||||
return '<span class="badge bg-blue-lt">Productivity</span> ';
|
||||
case 'Tools':
|
||||
return '<span class="badge bg-blue-lt">Tools</span> ';
|
||||
case 'Dashboard':
|
||||
return '<span class="badge bg-blue-lt">Dashboard</span> ';
|
||||
case 'Communication':
|
||||
return '<span class="badge bg-azure-lt">Communication</span> ';
|
||||
case 'Media':
|
||||
return '<span class="badge bg-azure-lt">Media</span> ';
|
||||
case 'CMS':
|
||||
return '<span class="badge bg-azure-lt">CMS</span> ';
|
||||
case 'Monitoring':
|
||||
return '<span class="badge bg-indigo-lt">Monitoring</span> ';
|
||||
case 'LDAP':
|
||||
return '<span class="badge bg-purple-lt">LDAP</span> ';
|
||||
case 'Arr':
|
||||
return '<span class="badge bg-purple-lt">Arr</span> ';
|
||||
case 'Database':
|
||||
return '<span class="badge bg-red-lt">Database</span> ';
|
||||
case 'Paid':
|
||||
return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
|
||||
case 'Gaming':
|
||||
return '<span class="badge bg-pink-lt">Gaming</span> ';
|
||||
case 'Finance':
|
||||
return '<span class="badge bg-orange-lt">Finance</span> ';
|
||||
case 'Networking':
|
||||
return '<span class="badge bg-yellow-lt">Networking</span> ';
|
||||
case 'Authentication':
|
||||
return '<span class="badge bg-lime-lt">Authentication</span> ';
|
||||
case 'Development':
|
||||
return '<span class="badge bg-green-lt">Development</span> ';
|
||||
case 'Media Server':
|
||||
return '<span class="badge bg-teal-lt">Media Server</span> ';
|
||||
case 'Downloaders':
|
||||
return '<span class="badge bg-cyan-lt">Downloaders</span> ';
|
||||
default:
|
||||
return ''; // default to other if the category is not recognized
|
||||
}
|
||||
}
|
||||
|
||||
export const InstallModal = async (req, res) => {
|
||||
let input = req.header('hx-trigger-name');
|
||||
let type = req.header('hx-trigger');
|
||||
|
||||
if (type == 'compose') {
|
||||
let compose = readFileSync(`templates/compose/${input}/compose.yaml`, 'utf8');
|
||||
let modal = readFileSync('./views/modals/compose.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, input);
|
||||
modal = modal.replace(/COMPOSE_CONTENT/g, compose);
|
||||
res.send(modal);
|
||||
return;
|
||||
} else {
|
||||
let result = templates_global.find(t => t.name == input);
|
||||
let name = result.name || result.title.toLowerCase();
|
||||
let short_name = name.slice(0, 25) + "...";
|
||||
let desc = result.description.replaceAll(". ", ".\n") || "no description available";
|
||||
let short_desc = desc.slice(0, 60) + "...";
|
||||
let modal_name = name.replaceAll(" ", "-");
|
||||
let form_id = name.replaceAll("-", "_");
|
||||
let note = result.note ? result.note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let command = result.command ? result.command : "";
|
||||
let command_check = command ? "checked" : "";
|
||||
let privileged = result.privileged || "";
|
||||
let privileged_check = privileged ? "checked" : "";
|
||||
let repository = result.repository || "";
|
||||
let image = result.image || "";
|
||||
let net_host, net_bridge, net_docker = '';
|
||||
let net_name = 'AppBridge';
|
||||
let restart_policy = result.restart_policy || 'unless-stopped';
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
switch (result.network) {
|
||||
case 'host':
|
||||
net_host = 'checked';
|
||||
break;
|
||||
case 'bridge':
|
||||
net_bridge = 'checked';
|
||||
net_name = result.network;
|
||||
break;
|
||||
default:
|
||||
net_docker = 'checked';
|
||||
}
|
||||
|
||||
if (repository != "") {
|
||||
image = (`${repository.url}/raw/master/${repository.stackfile}`);
|
||||
}
|
||||
|
||||
if (req.body.confirm == 'Yes') {
|
||||
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
|
||||
|
||||
uninstall(req.body);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
// Get port details
|
||||
try {
|
||||
let ports = result.ports[i];
|
||||
let port_check = ports ? "checked" : "";
|
||||
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
|
||||
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
|
||||
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
|
||||
|
||||
// remove /tcp or /udp from port_external if it exists
|
||||
if (port_external.includes("/")) {
|
||||
port_external = port_external.split("/")[0];
|
||||
}
|
||||
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: port_external,
|
||||
internal: port_internal,
|
||||
protocol: port_protocol
|
||||
});
|
||||
} catch {
|
||||
ports_data.push({
|
||||
check: "",
|
||||
external: "",
|
||||
internal: "",
|
||||
protocol: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get volume details
|
||||
try {
|
||||
let volumes = result.volumes[i];
|
||||
let volume_check = volumes ? "checked" : "";
|
||||
let volume_bind = volumes.bind ? volumes.bind : "";
|
||||
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
|
||||
let volume_readwrite = "rw";
|
||||
|
||||
if (volumes.readonly == true) {
|
||||
volume_readwrite = "ro";
|
||||
}
|
||||
|
||||
volumes_data.push({
|
||||
check: volume_check,
|
||||
bind: volume_bind,
|
||||
container: volume_container,
|
||||
readwrite: volume_readwrite
|
||||
});
|
||||
} catch {
|
||||
volumes_data.push({
|
||||
check: "",
|
||||
bind: "",
|
||||
container: "",
|
||||
readwrite: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get environment details
|
||||
try {
|
||||
let env = result.env[i];
|
||||
let env_check = "";
|
||||
let env_default = env.default ? env.default : "";
|
||||
if (env.set) { env_default = env.set;}
|
||||
let env_description = env.description ? env.description : "";
|
||||
let env_label = env.label ? env.label : "";
|
||||
let env_name = env.name ? env.name : "";
|
||||
|
||||
env_data.push({
|
||||
check: env_check,
|
||||
default: env_default,
|
||||
description: env_description,
|
||||
label: env_label,
|
||||
name: env_name
|
||||
});
|
||||
} catch {
|
||||
env_data.push({
|
||||
check: "",
|
||||
default: "",
|
||||
description: "",
|
||||
label: "",
|
||||
name: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get label details
|
||||
try {
|
||||
let label = result.labels[i];
|
||||
let label_check = "";
|
||||
let label_name = label.name ? label.name : "";
|
||||
let label_value = label.value ? label.value : "";
|
||||
|
||||
label_data.push({
|
||||
check: label_check,
|
||||
name: label_name,
|
||||
value: label_value
|
||||
});
|
||||
} catch {
|
||||
label_data.push({
|
||||
check: "",
|
||||
name: "",
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Redirect to the home page
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
|
||||
let modal = readFileSync('./views/modals/json.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
modal = modal.replace(/AppNote/g, note);
|
||||
modal = modal.replace(/AppImage/g, image);
|
||||
modal = modal.replace(/RestartPolicy/g, restart_policy);
|
||||
modal = modal.replace(/NetHost/g, net_host);
|
||||
modal = modal.replace(/NetBridge/g, net_bridge);
|
||||
modal = modal.replace(/NetDocker/g, net_docker);
|
||||
modal = modal.replace(/NetName/g, net_name);
|
||||
modal = modal.replace(/ModalName/g, modal_name);
|
||||
modal = modal.replace(/FormId/g, form_id);
|
||||
modal = modal.replace(/CommandCheck/g, command_check);
|
||||
modal = modal.replace(/CommandValue/g, command);
|
||||
modal = modal.replace(/PrivilegedCheck/g, privileged_check);
|
||||
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
|
||||
modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
|
||||
modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
|
||||
modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
|
||||
|
||||
modal = modal.replaceAll(`Volume${i}Check`, volumes_data[i].check);
|
||||
modal = modal.replaceAll(`Volume${i}Bind`, volumes_data[i].bind);
|
||||
modal = modal.replaceAll(`Volume${i}Container`, volumes_data[i].container);
|
||||
modal = modal.replaceAll(`Volume${i}RW`, volumes_data[i].readwrite);
|
||||
|
||||
modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
|
||||
modal = modal.replaceAll(`Env${i}Default`, env_data[i].default);
|
||||
modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
|
||||
modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
|
||||
modal = modal.replaceAll(`Env${i}Name`, env_data[i].name);
|
||||
|
||||
modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
|
||||
modal = modal.replaceAll(`Label${i}Name`, label_data[i].name);
|
||||
modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
|
||||
}
|
||||
res.send(modal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const LearnMore = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let id = req.header('hx-trigger');
|
||||
|
||||
if (id == 'compose') {
|
||||
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
modal = modal.replace(/AppDesc/g, 'Compose File');
|
||||
res.send(modal);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = templates_global.find(t => t.name == name);
|
||||
|
||||
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, result.title);
|
||||
modal = modal.replace(/AppDesc/g, result.description);
|
||||
|
||||
res.send(modal);
|
||||
}
|
||||
|
||||
|
||||
export const ImportModal = async (req, res) => {
|
||||
let modal = readFileSync('./views/modals/import.html', 'utf8');
|
||||
res.send(modal);
|
||||
}
|
||||
|
||||
|
||||
export const Upload = (req, res) => {
|
||||
upload.array('files', 10)(req, res, () => {
|
||||
|
||||
alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
|
||||
<div class="d-flex">
|
||||
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
|
||||
<div>Template(s) Uploaded!</div>
|
||||
</div>
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
|
||||
</div>`;
|
||||
|
||||
|
||||
let exists_alert = `<div class="alert alert-danger alert-dismissible mb-0 py-2" role="alert">
|
||||
<div class="d-flex">
|
||||
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
|
||||
<div>Template already exists</div>
|
||||
</div>
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
|
||||
</div>`;
|
||||
|
||||
let files = readdirSync('templates/tmp/');
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
|
||||
if (files[i].endsWith('.zip')) {
|
||||
let zip = new AdmZip(`templates/tmp/${files[i]}`);
|
||||
zip.extractAllTo('templates/compose', true);
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
} else if (files[i].endsWith('.json')) {
|
||||
if (existsSync(`templates/json/${files[i]}`)) {
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
alert = exists_alert;
|
||||
res.redirect('/apps');
|
||||
return;
|
||||
}
|
||||
renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
|
||||
} else if (files[i].endsWith('.yml')) {
|
||||
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
|
||||
let compose_data = parse(compose);
|
||||
let service_name = Object.keys(compose_data.services);
|
||||
if (existsSync(`templates/compose/${service_name}`)) {
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
alert = exists_alert;
|
||||
res.redirect('/apps');
|
||||
return;
|
||||
}
|
||||
mkdirSync(`templates/compose/${service_name}`);
|
||||
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
|
||||
} else if (files[i].endsWith('.yaml')) {
|
||||
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
|
||||
let compose_data = parse(compose);
|
||||
let service_name = Object.keys(compose_data.services);
|
||||
if (existsSync(`templates/compose/${service_name}`)) {
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
alert = exists_alert;
|
||||
res.redirect('/apps');
|
||||
return;
|
||||
}
|
||||
mkdirSync(`templates/compose/${service_name}`);
|
||||
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
|
||||
} else {
|
||||
// unsupported file type
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
}
|
||||
}
|
||||
res.redirect('/apps');
|
||||
});
|
||||
};
|
|
@ -1,150 +0,0 @@
|
|||
const User = require('../database/UserModel');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
|
||||
exports.Login = function(req,res){
|
||||
|
||||
// check whether we have a session
|
||||
if(req.session.user){
|
||||
// Redirect to log out.
|
||||
res.redirect("/logout");
|
||||
}else{
|
||||
// Render the login page.
|
||||
res.render("pages/login",{
|
||||
"error":"",
|
||||
"isLoggedIn": false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.processLogin = async function(req,res){
|
||||
// get the data.
|
||||
let email = req.body.email;
|
||||
let password = req.body.password;
|
||||
// check if we have data.
|
||||
if(email && password){
|
||||
// check if the user exists.
|
||||
let existingUser = await User.findOne({ where: {email:email}});
|
||||
if(existingUser){
|
||||
// compare the password.
|
||||
let match = await bcrypt.compare(password,existingUser.password);
|
||||
if(match){
|
||||
// set the session.
|
||||
req.session.user = existingUser.username;
|
||||
req.session.UUID = existingUser.UUID;
|
||||
req.session.role = existingUser.role;
|
||||
|
||||
// Redirect to the home page.
|
||||
res.redirect("/");
|
||||
}else{
|
||||
// return an error.
|
||||
res.render("pages/login",{
|
||||
"error":"Invalid password",
|
||||
isLoggedIn: false
|
||||
});
|
||||
}
|
||||
}else{
|
||||
// return an error.
|
||||
res.render("pages/login",{
|
||||
"error":"User with that email does not exist.",
|
||||
isLoggedIn:false
|
||||
});
|
||||
}
|
||||
}else{
|
||||
res.status(400);
|
||||
res.render("pages/login",{
|
||||
"error":"Please fill in all the fields.",
|
||||
isLoggedIn:false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
exports.Logout = function(req,res){
|
||||
// clear the session.
|
||||
req.session.destroy();
|
||||
// Redirect to the login page.
|
||||
res.redirect("/login");
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.Register = function(req,res){
|
||||
// Check whether we have a session
|
||||
if(req.session.user){
|
||||
// Redirect to log out.
|
||||
res.redirect("/logout");
|
||||
} else {
|
||||
// Render the signup page.
|
||||
res.render("pages/register",{
|
||||
"error":"",
|
||||
isLoggedIn:false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.processRegister = async function(req,res){
|
||||
|
||||
// Get the data.
|
||||
let { first_name, last_name, username, email, password, avatar, tos } = req.body;
|
||||
let role = "user";
|
||||
|
||||
// Check the data.
|
||||
if(first_name && last_name && email && password && username && tos){
|
||||
|
||||
// Check if there is an existing user with that username.
|
||||
let existingUser = await User.findOne({ where: {username:username}});
|
||||
|
||||
let adminUser = await User.findOne({ where: {role:"admin"}});
|
||||
|
||||
if(!existingUser){
|
||||
// hash the password.
|
||||
let hashedPassword = bcrypt.hashSync(password,10);
|
||||
|
||||
if(!adminUser){
|
||||
console.log('Creating admin User');
|
||||
role = "admin";
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.create({
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
username: username,
|
||||
email: email,
|
||||
password: hashedPassword,
|
||||
role: role,
|
||||
group: 'all',
|
||||
avatar: `<img src="./static/avatars/${avatar}">`
|
||||
});
|
||||
|
||||
// set the session.
|
||||
req.session.user = user.username;
|
||||
req.session.UUID = user.UUID;
|
||||
req.session.role = user.role;
|
||||
// Redirect to the home page.
|
||||
res.redirect("/");
|
||||
}
|
||||
catch (err) {
|
||||
// return an error.
|
||||
res.render("pages/register",{
|
||||
"error":"Something went wrong when creating account.",
|
||||
isLoggedIn:false
|
||||
});
|
||||
}
|
||||
|
||||
}else{
|
||||
// return an error.
|
||||
res.render("pages/register",{
|
||||
"error":"User with that username already exists.",
|
||||
isLoggedIn:false
|
||||
});
|
||||
}
|
||||
}else{
|
||||
// Redirect to the signup page.
|
||||
res.render("pages/register",{
|
||||
"error":"Please fill in all the fields and accept TOS.",
|
||||
isLoggedIn:false
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,247 +1,448 @@
|
|||
const User = require('../database/UserModel');
|
||||
const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
|
||||
const { execSync } = require("child_process");
|
||||
const { siteCard } = require('../components/siteCard');
|
||||
const { containerExec } = require('../functions/system')
|
||||
import { Readable } from 'stream';
|
||||
import { Permission, User } from '../database/models.js';
|
||||
import { docker } from '../server.js';
|
||||
import { dockerContainerStats } from 'systeminformation';
|
||||
import { readFileSync } from 'fs';
|
||||
import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
let hidden = '';
|
||||
let alert = '';
|
||||
let [ cardList, newCards, stats ] = [ '', '', {}];
|
||||
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
|
||||
|
||||
// The page
|
||||
export const Dashboard = (req, res) => {
|
||||
|
||||
exports.Dashboard = async function (req, res) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
// get user data with matching UUID from sqlite database
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID } });
|
||||
|
||||
let caddy = 'd-none';
|
||||
|
||||
if (process.env.Proxy_Manager == 'enabled') {
|
||||
caddy = '';
|
||||
}
|
||||
|
||||
// Render the home page
|
||||
res.render("pages/dashboard", {
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
isLoggedIn: true,
|
||||
site_list: req.app.locals.site_list,
|
||||
caddy: caddy
|
||||
});
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.AddSite = async function (req, res) {
|
||||
|
||||
let { domain, type, host, port } = req.body;
|
||||
|
||||
if ((req.session.role == "admin") && ( domain && type && host && port)) {
|
||||
|
||||
|
||||
let { domain, type, host, port } = req.body;
|
||||
|
||||
// build caddyfile
|
||||
let caddyfile = `${domain} {`
|
||||
caddyfile += `\n\t${type} ${host}:${port}`
|
||||
caddyfile += `\n\theader {`
|
||||
caddyfile += `\n\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains; preload"`
|
||||
caddyfile += `\n\t}`
|
||||
caddyfile += `\n}`
|
||||
|
||||
|
||||
// save caddyfile
|
||||
writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
|
||||
|
||||
|
||||
// format caddyfile
|
||||
let format = {
|
||||
container: 'DweebProxy',
|
||||
command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
|
||||
}
|
||||
await containerExec(format, function(err, data) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.log(`Formatted ${domain}.Caddyfile`);
|
||||
});
|
||||
|
||||
///////////////// convert caddyfile to json
|
||||
let convert = {
|
||||
container: 'DweebProxy',
|
||||
command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
|
||||
}
|
||||
await containerExec(convert, function(err, data) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.log(`Converted ${domain}.Caddyfile to JSON`);
|
||||
});
|
||||
|
||||
////////////// reload caddy
|
||||
let reload = {
|
||||
container: 'DweebProxy',
|
||||
command: `caddy reload --config /etc/caddy/Caddyfile`
|
||||
}
|
||||
await containerExec(reload, function(err, data) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
console.log(`Reloaded Caddy Config`);
|
||||
});
|
||||
|
||||
let site = siteCard(type, domain, host, port, 0);
|
||||
|
||||
req.app.locals.site_list += site;
|
||||
|
||||
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect
|
||||
console.log('not admin or missing info')
|
||||
res.redirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
exports.RemoveSite = async function (req, res) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
for (const [key, value] of Object.entries(req.body)) {
|
||||
|
||||
execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
|
||||
if (err) { console.error(`error: ${err.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`removed ${value}.Caddyfile`);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
let reload = {
|
||||
container: 'DweebProxy',
|
||||
command: `caddy reload --config /etc/caddy/Caddyfile`
|
||||
}
|
||||
await containerExec(reload);
|
||||
|
||||
|
||||
console.log('Removed Site(s)')
|
||||
|
||||
res.redirect("/refreshsites");
|
||||
} else {
|
||||
res.redirect("/");
|
||||
}
|
||||
let name = req.session.user;
|
||||
let role = req.session.role;
|
||||
alert = req.session.alert;
|
||||
|
||||
res.render("dashboard", {
|
||||
name: name,
|
||||
avatar: name.charAt(0).toUpperCase(),
|
||||
role: role,
|
||||
alert: alert,
|
||||
});
|
||||
}
|
||||
|
||||
// The page actions
|
||||
export const DashboardAction = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let value = req.header('hx-trigger');
|
||||
let action = req.params.action;
|
||||
let modal = '';
|
||||
|
||||
exports.RefreshSites = async function (req, res) {
|
||||
|
||||
let domain, type, host, port;
|
||||
let id = 1;
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
// Clear site_list.ejs
|
||||
req.app.locals.site_list = "";
|
||||
|
||||
|
||||
// check if ./caddyfiles/sites contains any .json files, then delete them
|
||||
try {
|
||||
let files = readdirSync('./caddyfiles/sites/');
|
||||
files.forEach(file => {
|
||||
if (file.includes(".json")) {
|
||||
execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
|
||||
if (err) { console.error(`error: ${err.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`removed ${file}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) { console.log("No .json files to delete") }
|
||||
|
||||
// get list of Caddyfiles
|
||||
let sites = readdirSync('./caddyfiles/sites/');
|
||||
|
||||
|
||||
sites.forEach(site_name => {
|
||||
// convert the caddyfile of each site to json
|
||||
let convert = {
|
||||
container: 'DweebProxy',
|
||||
command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
|
||||
switch (action) {
|
||||
case 'permissions':
|
||||
let title = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
let permissions_list = '';
|
||||
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
|
||||
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
|
||||
permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
|
||||
let users = await User.findAll({ attributes: ['username', 'UUID']});
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
|
||||
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
|
||||
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
|
||||
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
|
||||
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
|
||||
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
|
||||
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
|
||||
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
|
||||
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
|
||||
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
|
||||
if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
permissions_list += user_permissions;
|
||||
}
|
||||
containerExec(convert);
|
||||
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
|
||||
res.send(permissions_modal);
|
||||
return;
|
||||
case 'uninstall':
|
||||
modal = readFileSync('./views/modals/uninstall.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
res.send(modal);
|
||||
return;
|
||||
case 'details':
|
||||
modal = readFileSync('./views/modals/details.html', 'utf8');
|
||||
let details = await containerInfo(name);
|
||||
|
||||
try {
|
||||
// read the json file
|
||||
let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
|
||||
// fix whitespace and parse the json file
|
||||
site_file = site_file.replace(/ /g, " ");
|
||||
site_file = JSON.parse(site_file);
|
||||
} catch (error) { console.log("No .json file to read") }
|
||||
modal = modal.replace(/AppName/g, details.name);
|
||||
modal = modal.replace(/AppImage/g, details.image);
|
||||
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
|
||||
modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
|
||||
modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
|
||||
modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
|
||||
}
|
||||
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
|
||||
modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
|
||||
modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
|
||||
}
|
||||
|
||||
|
||||
// get the domain, type, host, and port from the json file
|
||||
try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
|
||||
try { type = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].handler } catch (error) { console.log("No Type") }
|
||||
try { host = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[0] } catch (error) { console.log("Not Localhost") }
|
||||
try { port = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[1] } catch (error) { console.log("No Port") }
|
||||
for (let i = 0; i <= 19; i++) {
|
||||
modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
|
||||
modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
|
||||
}
|
||||
|
||||
// build the site card
|
||||
let site = siteCard(type, domain, host, port, id);
|
||||
// console.log(details.env);
|
||||
for (let i = 0; i <= 19; i++) {
|
||||
modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
|
||||
modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
|
||||
}
|
||||
|
||||
// append the site card to site_list
|
||||
req.app.locals.site_list += site;
|
||||
|
||||
id++;
|
||||
});
|
||||
|
||||
res.send(modal);
|
||||
return;
|
||||
case 'updates':
|
||||
res.send(newCards);
|
||||
newCards = '';
|
||||
return;
|
||||
case 'card':
|
||||
await userCards(req.session);
|
||||
if (!req.session.container_list.find(c => c.container === name)) {
|
||||
res.send('');
|
||||
return;
|
||||
} else {
|
||||
let details = await containerInfo(name);
|
||||
let card = await createCard(details);
|
||||
res.send(card);
|
||||
return;
|
||||
}
|
||||
case 'logs':
|
||||
let logString = '';
|
||||
let options = { follow: true, stdout: true, stderr: false, timestamps: false };
|
||||
docker.getContainer(name).logs(options, function (err, stream) {
|
||||
if (err) { console.log(err); return; }
|
||||
const readableStream = Readable.from(stream);
|
||||
readableStream.on('data', function (chunk) {
|
||||
logString += chunk.toString('utf8');
|
||||
});
|
||||
readableStream.on('end', function () {
|
||||
res.send(`<pre>${logString}</pre>`);
|
||||
});
|
||||
});
|
||||
return;
|
||||
case 'hide':
|
||||
let user = req.session.user;
|
||||
let exists = await Permission.findOne({ where: {containerName: name, user: user}});
|
||||
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: user, hide: true, userID: req.session.UUID}); }
|
||||
else { exists.update({ hide: true }); }
|
||||
hidden = await Permission.findAll({ where: {user: user, hide: true}}, { attributes: ['containerName'] });
|
||||
hidden = hidden.map((container) => container.containerName);
|
||||
res.send("ok");
|
||||
return;
|
||||
case 'reset':
|
||||
await Permission.update({ hide: false }, { where: { user: req.session.user } });
|
||||
res.send("ok");
|
||||
return;
|
||||
case 'alert':
|
||||
req.session.alert = '';
|
||||
res.send('');
|
||||
return;
|
||||
}
|
||||
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/");
|
||||
function status (state) {
|
||||
return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
${state}
|
||||
</span>`);
|
||||
}
|
||||
|
||||
// Container actions
|
||||
if ((action == 'start') && (value == 'stopped')) {
|
||||
docker.getContainer(name).start();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'start') && (value == 'paused')) {
|
||||
docker.getContainer(name).unpause();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'stop') && (value != 'stopped')) {
|
||||
docker.getContainer(name).stop();
|
||||
res.send(status('stopping'));
|
||||
} else if ((action == 'pause') && (value == 'paused')) {
|
||||
docker.getContainer(name).unpause();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'pause') && (value == 'running')) {
|
||||
docker.getContainer(name).pause();
|
||||
res.send(status('pausing'));
|
||||
} else if (action == 'restart') {
|
||||
docker.getContainer(name).restart();
|
||||
res.send(status('restarting'));
|
||||
}
|
||||
}
|
||||
|
||||
async function containerInfo (containerName) {
|
||||
// get the container info
|
||||
let container = docker.getContainer(containerName);
|
||||
let info = await container.inspect();
|
||||
let image = info.Config.Image;
|
||||
// grab the service name from the end of the image name
|
||||
let service = image.split('/').pop();
|
||||
// remove the tag from the service name if it exists
|
||||
try { service = service.split(':')[0]; } catch {}
|
||||
let ports_list = [];
|
||||
let external = 0;
|
||||
let internal = 0;
|
||||
|
||||
try {
|
||||
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
|
||||
let ports = {
|
||||
check: 'checked',
|
||||
external: value[0].HostPort,
|
||||
internal: key.split('/')[0],
|
||||
protocol: key.split('/')[1]
|
||||
}
|
||||
ports_list.push(ports);
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
external = ports_list[0].external;
|
||||
internal = ports_list[0].internal;
|
||||
} catch {}
|
||||
|
||||
// console.log(ports_list);
|
||||
// console.log(info.HostConfig.PortBindings);
|
||||
|
||||
// console.log(info.HostConfig.Binds);
|
||||
|
||||
// console.log(info.Config.Env);
|
||||
// console.log(info.Config.Labels);
|
||||
|
||||
let details = {
|
||||
name: containerName,
|
||||
image: image,
|
||||
service: service,
|
||||
state: info.State.Status,
|
||||
external_port: external,
|
||||
internal_port: internal,
|
||||
ports: ports_list,
|
||||
volumes: info.Mounts,
|
||||
env: info.Config.Env,
|
||||
labels: info.Config.Labels,
|
||||
link: 'localhost',
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
async function createCard (details) {
|
||||
let shortname = details.name.slice(0, 10) + '...';
|
||||
let trigger = 'data-hx-trigger="load, every 3s"';
|
||||
let state = details.state;
|
||||
let card = readFileSync('./views/partials/containerFull.html', 'utf8');
|
||||
|
||||
let state_color = '';
|
||||
switch (state) {
|
||||
case 'running':
|
||||
state_color = 'green';
|
||||
break;
|
||||
case 'exited':
|
||||
state = 'stopped';
|
||||
state_color = 'red';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'paused':
|
||||
state_color = 'orange';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'installing':
|
||||
state_color = 'blue';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
}
|
||||
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
|
||||
|
||||
card = card.replace(/AppName/g, details.name);
|
||||
card = card.replace(/AppShortName/g, shortname);
|
||||
card = card.replace(/AppIcon/g, details.service);
|
||||
card = card.replace(/AppState/g, state);
|
||||
card = card.replace(/StateColor/g, state_color);
|
||||
card = card.replace(/ExternalPort/g, details.external_port);
|
||||
card = card.replace(/InternalPort/g, details.internal_port);
|
||||
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
|
||||
card = card.replace(/AppNameState/g, `${details.name}State`);
|
||||
card = card.replace(/data-trigger=""/, trigger);
|
||||
return card;
|
||||
}
|
||||
|
||||
async function userCards (session) {
|
||||
session.container_list = [];
|
||||
// check what containers the user wants hidden
|
||||
let hidden = await Permission.findAll({ where: {user: session.user, hide: true}}, { attributes: ['containerName'] });
|
||||
hidden = hidden.map((container) => container.containerName);
|
||||
// check what containers the user has permission to view
|
||||
let visable = await Permission.findAll({ where: { user: session.user, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] } });
|
||||
visable = visable.map((container) => container.containerName);
|
||||
// get all containers
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
// loop through containers
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
let container_name = containers[i].Names[0].replace('/', '');
|
||||
// skip hidden containers
|
||||
if (hidden.includes(container_name)) { continue; }
|
||||
// admin can see all containers that they don't have hidden
|
||||
if (session.role == 'admin') { session.container_list.push({ container: container_name, state: containers[i].State }); }
|
||||
// user can see any containers that they have any permissions for
|
||||
else if (visable.includes(container_name)){ session.container_list.push({ container: container_name, state: containers[i].State }); }
|
||||
}
|
||||
// create a sent list if it doesn't exist
|
||||
if (!session.sent_list) { session.sent_list = []; }
|
||||
if (!session.update_list) { session.update_list = []; }
|
||||
if (!session.new_cards) { session.new_cards = []; }
|
||||
}
|
||||
|
||||
async function updateDashboard (session) {
|
||||
let container_list = session.container_list;
|
||||
let sent_list = session.sent_list;
|
||||
session.new_cards = [];
|
||||
session.update_list = [];
|
||||
// loop through the containers list
|
||||
container_list.forEach(info => {
|
||||
let { container, state } = info;
|
||||
let sent = sent_list.find(c => c.container === container);
|
||||
if (!sent) { session.new_cards.push(container);}
|
||||
else if (sent.state !== state) { session.update_list.push(container); }
|
||||
});
|
||||
// loop through the sent list to see if any containers have been removed
|
||||
sent_list.forEach(info => {
|
||||
let { container } = info;
|
||||
let exists = container_list.find(c => c.container === container);
|
||||
if (!exists) { session.update_list.push(container); }
|
||||
});
|
||||
}
|
||||
|
||||
// HTMX server-side events
|
||||
export const SSE = async (req, res) => {
|
||||
// set the headers for server-sent events
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
||||
// check for container changes every 500ms
|
||||
let eventCheck = setInterval(async () => {
|
||||
await userCards(req.session);
|
||||
// check if the cards displayed are the same as what's in the session
|
||||
if ((JSON.stringify(req.session.container_list) === JSON.stringify(req.session.sent_list))) { return; }
|
||||
await updateDashboard(req.session);
|
||||
|
||||
for (let i = 0; i < req.session.new_cards.length; i++) {
|
||||
let details = await containerInfo(req.session.new_cards[i]);
|
||||
let card = await createCard(details);
|
||||
newCards += card;
|
||||
req.session.alert = '';
|
||||
}
|
||||
for (let i = 0; i < req.session.update_list.length; i++) {
|
||||
res.write(`event: ${req.session.update_list[i]}\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
}
|
||||
res.write(`event: update\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
req.session.sent_list = req.session.container_list.slice();
|
||||
}, 500);
|
||||
req.on('close', () => {
|
||||
clearInterval(eventCheck);
|
||||
});
|
||||
};
|
||||
|
||||
// Server metrics (CPU, RAM, TX, RX, DISK)
|
||||
export const Stats = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let color = req.header('hx-trigger');
|
||||
let value = 0;
|
||||
switch (name) {
|
||||
case 'CPU':
|
||||
await currentLoad().then(data => { value = Math.round(data.currentLoad); });
|
||||
break;
|
||||
case 'RAM':
|
||||
await mem().then(data => { value = Math.round((data.active / data.total) * 100); });
|
||||
break;
|
||||
case 'NET':
|
||||
let [down, up, percent] = [0, 0, 0];
|
||||
await networkStats().then(data => { down = Math.round(data[0].rx_bytes / (1024 * 1024)); up = Math.round(data[0].tx_bytes / (1024 * 1024)); percent = Math.round((down / 1000) * 100); });
|
||||
let net = `<div class="font-weight-medium"><label class="cpu-text mb-1">Down:${down}MB Up:${up}MB</label></div>
|
||||
<div class="cpu-bar meter animate ${color}"><span style="width:20%"><span></span></span></div>`;
|
||||
res.send(net);
|
||||
return;
|
||||
case 'DISK':
|
||||
await fsSize().then(data => { value = data[0].use; });
|
||||
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);
|
||||
}
|
||||
|
||||
// Imported by utils/install.js
|
||||
export async function addAlert (session, type, message) {
|
||||
session.alert = `<div class="alert alert-${type} alert-dismissible py-2 mb-0" role="alert" id="alert">
|
||||
<div class="d-flex">
|
||||
<div class="spinner-border text-info nav-link">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div>
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-close" data-hx-post="/dashboard/alert" data-hx-trigger="click" data-hx-target="#alert" data-hx-swap="outerHTML" style="padding-top: 0.5rem;" ></button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export const UpdatePermissions = async (req, res) => {
|
||||
let { user, container, reset_permissions } = req.body;
|
||||
let id = req.header('hx-trigger');
|
||||
if (reset_permissions) {
|
||||
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerName: container} });
|
||||
return;
|
||||
}
|
||||
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false }, { where: { containerName: container, user: user } });
|
||||
Object.keys(req.body).forEach(async function(key) {
|
||||
if (key != 'user' && key != 'container') {
|
||||
let permissions = req.body[key];
|
||||
if (permissions.includes('uninstall')) { await Permission.update({ uninstall: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('edit')) { await Permission.update({ edit: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('upgrade')) { await Permission.update({ upgrade: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('start')) { await Permission.update({ start: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('stop')) { await Permission.update({ stop: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('pause')) { await Permission.update({ pause: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('restart')) { await Permission.update({ restart: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('logs')) { await Permission.update({ logs: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('view')) { await Permission.update({ view: true }, { where: {containerName: container, user: user}}); }
|
||||
}
|
||||
});
|
||||
if (id == 'submit') {
|
||||
res.send('<button class="btn" type="button" id="confirmed" hx-post="/updatePermissions" hx-swap="outerHTML" hx-trigger="load delay:2s">Update ✔️</button>');
|
||||
return;
|
||||
} else if (id == 'confirmed') {
|
||||
res.send('<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissions" hx-swap="outerHTML">Update </button>');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.DisableSite = async function (req, res) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
console.log(req.body)
|
||||
console.log('Disable Site')
|
||||
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
exports.EnableSite = async function (req, res) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
console.log(req.body)
|
||||
console.log('Enable Site')
|
||||
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
// Container charts
|
||||
export const Chart = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
if (!stats[name]) { stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) }; }
|
||||
const info = await dockerContainerStats(name);
|
||||
stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
|
||||
stats[name].ramArray.push(Math.round(info[0].memPercent));
|
||||
stats[name].cpuArray = stats[name].cpuArray.slice(-15);
|
||||
stats[name].ramArray = stats[name].ramArray.slice(-15);
|
||||
let chart = `
|
||||
<script>
|
||||
${name}chart.updateSeries([{
|
||||
data: [${stats[name].cpuArray}]
|
||||
}, {
|
||||
data: [${stats[name].ramArray}]
|
||||
}])
|
||||
</script>`
|
||||
res.send(chart);
|
||||
}
|
110
controllers/images.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { docker } from '../server.js';
|
||||
import { addAlert } from './dashboard.js';
|
||||
|
||||
export const Images = async function(req, res) {
|
||||
|
||||
let action = req.params.action;
|
||||
|
||||
if (action == "remove") {
|
||||
let images = req.body.select;
|
||||
|
||||
if (typeof(images) == 'string') {
|
||||
images = [images];
|
||||
}
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
if (images[i] != 'on') {
|
||||
try {
|
||||
console.log(`Removing image: ${images[i]}`);
|
||||
let image = docker.getImage(images[i]);
|
||||
await image.remove();
|
||||
} catch (error) {
|
||||
console.log(`Unable to remove image: ${images[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.redirect("/images");
|
||||
return;
|
||||
} else if (action == "add") {
|
||||
let image = req.body.image;
|
||||
let tag = req.body.tag || 'latest';
|
||||
|
||||
try {
|
||||
console.log(`Pulling image: ${image}:${tag}`);
|
||||
await docker.pull(`${image}:${tag}`);
|
||||
} catch (error) {
|
||||
console.log(`Unable to pull image: ${image}:${tag}`);
|
||||
}
|
||||
res.redirect("/images");
|
||||
return;
|
||||
}
|
||||
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
let container_images = [];
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
container_images.push(containers[i].Image);
|
||||
}
|
||||
|
||||
let images = await docker.listImages({ all: true });
|
||||
|
||||
let image_list = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-type">Tag</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
|
||||
let name = '';
|
||||
let tag = '';
|
||||
try { name = images[i].RepoTags[0].split(':')[0]; } catch {}
|
||||
try { tag = images[i].RepoTags[0].split(':')[1]; } catch {}
|
||||
|
||||
let date = new Date(images[i].Created * 1000);
|
||||
let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
|
||||
let size = images[i].Size / 1000 / 1000; // to match docker desktop
|
||||
size = size.toFixed(2);
|
||||
|
||||
let status = '';
|
||||
if (container_images.includes(images[i].RepoTags[0])) {
|
||||
status = 'In use';
|
||||
}
|
||||
|
||||
let details = `
|
||||
<tr>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="${images[i].Id}" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-name">${name}</td>
|
||||
<td class="sort-type">${tag}</td>
|
||||
<td class="sort-city">${images[i].Id}</td>
|
||||
<td class="sort-score text-green">${status}</td>
|
||||
<td class="sort-date" data-date="1628122643">${created}</td>
|
||||
<td class="sort-quantity">${size} MB</td>
|
||||
<td class="text-end"><a class="btn" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
|
||||
</tr>`
|
||||
image_list += details;
|
||||
}
|
||||
|
||||
image_list += `</tbody>`
|
||||
|
||||
|
||||
res.render("images", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
image_list: image_list,
|
||||
image_count: images.length,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
71
controllers/login.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { User, Syslog } from '../database/models.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
|
||||
export const Login = function(req,res){
|
||||
if (req.session.user) { res.redirect("/logout"); }
|
||||
else { res.render("login",{ "error":"", }); }
|
||||
}
|
||||
|
||||
export const submitLogin = async function(req,res){
|
||||
let { email, password } = req.body;
|
||||
email = email.toLowerCase();
|
||||
|
||||
if (email && password) {
|
||||
let existingUser = await User.findOne({ where: {email:email}});
|
||||
if (existingUser) {
|
||||
|
||||
let match = await bcrypt.compare(password,existingUser.password);
|
||||
|
||||
if (match) {
|
||||
let currentDate = new Date();
|
||||
let newLogin = currentDate.toLocaleString();
|
||||
await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
|
||||
|
||||
req.session.user = existingUser.username;
|
||||
req.session.UUID = existingUser.UUID;
|
||||
req.session.role = existingUser.role;
|
||||
req.session.avatar = existingUser.avatar;
|
||||
|
||||
const syslog = await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: email,
|
||||
event: "Successful Login",
|
||||
message: "User logged in successfully",
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
|
||||
res.redirect("/dashboard");
|
||||
} else {
|
||||
|
||||
const syslog = await Syslog.create({
|
||||
user: null,
|
||||
email: email,
|
||||
event: "Bad Login",
|
||||
message: "Invalid password",
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
|
||||
res.render("login",{
|
||||
"error":"Invalid password",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.render("login",{
|
||||
"error":"User with that email does not exist.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.status(400);
|
||||
res.render("login",{
|
||||
"error":"Please fill in all the fields.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const Logout = function(req,res){
|
||||
req.session.destroy(() => {
|
||||
res.redirect("/login");
|
||||
});
|
||||
}
|
89
controllers/networks.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { docker } from '../server.js';
|
||||
|
||||
|
||||
export const Networks = async function(req, res) {
|
||||
|
||||
|
||||
let container_networks = [];
|
||||
// List all containers
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
let network_name = containers[i].HostConfig.NetworkMode;
|
||||
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID) } catch {}
|
||||
}
|
||||
|
||||
|
||||
let networks = await docker.listNetworks({ all: true });
|
||||
|
||||
let network_list = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
|
||||
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
|
||||
// let date = new Date(images[i].Created * 1000);
|
||||
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
|
||||
let status = '';
|
||||
if (container_networks.includes(networks[i].Id)) {
|
||||
status = `In use`;
|
||||
}
|
||||
|
||||
let details = `
|
||||
<tr>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-name">${networks[i].Name}</td>
|
||||
<td class="sort-city">${networks[i].Id}</td>
|
||||
<td class="sort-score text-green">${status}</td>
|
||||
<td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
|
||||
<td class="text-end"><a class="btn" href="#">Details</a></td>
|
||||
</tr>`
|
||||
network_list += details;
|
||||
}
|
||||
|
||||
network_list += `</tbody>`
|
||||
|
||||
res.render("networks", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
network_list: network_list,
|
||||
network_count: networks.length,
|
||||
alert: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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");
|
||||
}
|
389
controllers/portal.js
Normal file
|
@ -0,0 +1,389 @@
|
|||
import { Readable } from 'stream';
|
||||
import { Permission, Container, User } from '../database/models.js';
|
||||
import { docker } from '../server.js';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
let hidden = '';
|
||||
|
||||
// The actual page
|
||||
export const Portal = (req, res) => {
|
||||
let name = req.session.user;
|
||||
let role = req.session.role;
|
||||
let avatar = name.charAt(0).toUpperCase();
|
||||
|
||||
res.render("portal", {
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
role: role,
|
||||
alert: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function CardList () {
|
||||
let name = req.session.user;
|
||||
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
let details = await containerInfo(containers[i].containerName);
|
||||
let card = await createCard(details);
|
||||
cardList += card;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserContainers = async (req, res) => {
|
||||
let cardList = '';
|
||||
let name = req.session.user;
|
||||
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
|
||||
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
if (containers[i].containerName == null) { continue; }
|
||||
let details = await containerInfo(containers[i].containerName);
|
||||
let card = await createCard(details);
|
||||
cardList += card;
|
||||
}
|
||||
res.send(cardList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function containerInfo (containerName) {
|
||||
let container = docker.getContainer(containerName);
|
||||
let info = await container.inspect();
|
||||
let image = info.Config.Image.split('/');
|
||||
let ports_list = [];
|
||||
try {
|
||||
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
|
||||
let ports = {
|
||||
check: 'checked',
|
||||
external: value[0].HostPort,
|
||||
internal: key.split('/')[0],
|
||||
protocol: key.split('/')[1]
|
||||
}
|
||||
ports_list.push(ports);
|
||||
}
|
||||
} catch {
|
||||
// no exposed ports
|
||||
}
|
||||
|
||||
let external = 0;
|
||||
let internal = 0;
|
||||
try {
|
||||
external = ports_list[0].external;
|
||||
internal = ports_list[0].internal;
|
||||
} catch {
|
||||
// no exposed ports
|
||||
}
|
||||
|
||||
|
||||
let details = {
|
||||
name: containerName,
|
||||
image: image,
|
||||
service: image[image.length - 1].split(':')[0],
|
||||
state: info.State.Status,
|
||||
external_port: external,
|
||||
internal_port: internal,
|
||||
ports: ports_list,
|
||||
link: 'localhost',
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
async function createCard (details) {
|
||||
if (hidden.includes(details.name)) { return;}
|
||||
let shortname = details.name.slice(0, 10) + '...';
|
||||
let trigger = 'data-hx-trigger="load, every 3s"';
|
||||
let state = details.state;
|
||||
let state_color = '';
|
||||
switch (state) {
|
||||
case 'running':
|
||||
state_color = 'green';
|
||||
break;
|
||||
case 'exited':
|
||||
state = 'stopped';
|
||||
state_color = 'red';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'paused':
|
||||
state_color = 'orange';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'installing':
|
||||
state_color = 'blue';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
}
|
||||
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
|
||||
let card = readFileSync('./views/partials/containerSimple.html', 'utf8');
|
||||
card = card.replace(/AppName/g, details.name);
|
||||
card = card.replace(/AppShortName/g, shortname);
|
||||
card = card.replace(/AppIcon/g, details.service);
|
||||
card = card.replace(/AppState/g, state);
|
||||
card = card.replace(/StateColor/g, state_color);
|
||||
card = card.replace(/ExternalPort/g, details.external_port);
|
||||
card = card.replace(/InternalPort/g, details.internal_port);
|
||||
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
|
||||
card = card.replace(/AppNameState/g, `${details.name}State`);
|
||||
card = card.replace(/data-trigger=""/, trigger);
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
|
||||
|
||||
export async function addCard (name, state) {
|
||||
console.log(`Adding card for ${name}: ${state}`);
|
||||
|
||||
let details = {
|
||||
name: name,
|
||||
image: name,
|
||||
service: name,
|
||||
state: 'installing',
|
||||
external_port: 0,
|
||||
internal_port: 0,
|
||||
ports: [],
|
||||
link: 'localhost',
|
||||
|
||||
}
|
||||
createCard(details).then(card => {
|
||||
cardList += card;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// HTMX server-side events
|
||||
export const SSE = async (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
||||
|
||||
let eventCheck = setInterval(async () => {
|
||||
// builds array of containers and their states
|
||||
containersArray = [];
|
||||
await docker.listContainers({ all: true }).then(containers => {
|
||||
containers.forEach(container => {
|
||||
let name = container.Names[0].replace('/', '');
|
||||
if (!hidden.includes(name)) { // if not hidden
|
||||
containersArray.push({ container: name, state: container.State });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
|
||||
cardList = '';
|
||||
newCards = '';
|
||||
containersArray.forEach(container => {
|
||||
const { container: containerName, state } = container;
|
||||
const existingContainer = sentArray.find(c => c.container === containerName);
|
||||
if (!existingContainer) {
|
||||
containerInfo(containerName).then(details => {
|
||||
createCard(details).then(card => {
|
||||
newCards += card;
|
||||
});
|
||||
});
|
||||
res.write(`event: update\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
} else if (existingContainer.state !== state) {
|
||||
updatesArray.push(containerName);
|
||||
}
|
||||
containerInfo(containerName).then(details => {
|
||||
createCard(details).then(card => {
|
||||
cardList += card;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
sentArray.forEach(container => {
|
||||
const { container: containerName } = container;
|
||||
const existingContainer = containersArray.find(c => c.container === containerName);
|
||||
if (!existingContainer) {
|
||||
updatesArray.push(containerName);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < updatesArray.length; i++) {
|
||||
res.write(`event: ${updatesArray[i]}\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
}
|
||||
updatesArray = [];
|
||||
sentArray = containersArray.slice();
|
||||
}
|
||||
|
||||
}, 500);
|
||||
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(eventCheck);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const updateCards = async (req, res) => {
|
||||
console.log('updateCards called');
|
||||
res.send(newCards);
|
||||
newCards = '';
|
||||
}
|
||||
|
||||
|
||||
export const Containers = async (req, res) => {
|
||||
CardList();
|
||||
// res.send(cardList);
|
||||
}
|
||||
|
||||
export const Card = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
console.log(`${name} requesting updated card`);
|
||||
// return nothing if in hidden or not found in containersArray
|
||||
if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
|
||||
res.send('');
|
||||
return;
|
||||
} else {
|
||||
let details = await containerInfo(name);
|
||||
let card = await createCard(details);
|
||||
res.send(card);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function status (state) {
|
||||
let status = `<span class="text-yellow align-items-center lh-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
${state}
|
||||
</span>`;
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
export const Logs = (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
function containerLogs (data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let logString = '';
|
||||
var options = { follow: false, stdout: true, stderr: false, timestamps: false };
|
||||
var containerName = docker.getContainer(data);
|
||||
containerName.logs(options, function (err, stream) {
|
||||
if (err) { reject(err); return; }
|
||||
const readableStream = Readable.from(stream);
|
||||
readableStream.on('data', function (chunk) {
|
||||
logString += chunk.toString('utf8');
|
||||
});
|
||||
readableStream.on('end', function () {
|
||||
resolve(logString);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
containerLogs(name).then((data) => {
|
||||
res.send(`<pre>${data}</pre> `)
|
||||
});
|
||||
}
|
||||
|
||||
export const Action = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let state = req.header('hx-trigger');
|
||||
let action = req.params.action;
|
||||
// Start
|
||||
if ((action == 'start') && (state == 'stopped')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.start();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'start') && (state == 'paused')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.unpause();
|
||||
res.send(status('starting'));
|
||||
// Stop
|
||||
} else if ((action == 'stop') && (state != 'stopped')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.stop();
|
||||
res.send(status('stopping'));
|
||||
// Pause
|
||||
} else if ((action == 'pause') && (state == 'paused')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.unpause();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'pause') && (state == 'running')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.pause();
|
||||
res.send(status('pausing'));
|
||||
// Restart
|
||||
} else if (action == 'restart') {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.restart();
|
||||
res.send(status('restarting'));
|
||||
// Hide
|
||||
} else if (action == 'hide') {
|
||||
let exists = await Container.findOne({ where: {name: name}});
|
||||
if (!exists) {
|
||||
const newContainer = await Container.create({ name: name, visibility: false, });
|
||||
} else {
|
||||
exists.update({ visibility: false });
|
||||
}
|
||||
hidden = await Container.findAll({ where: {visibility:false}});
|
||||
hidden = hidden.map((container) => container.name);
|
||||
res.send("ok");
|
||||
// Reset View
|
||||
} else if (action == 'reset') {
|
||||
await Container.update({ visibility: true }, { where: {} });
|
||||
hidden = await Container.findAll({ where: {visibility:false}});
|
||||
hidden = hidden.map((container) => container.name);
|
||||
res.send("ok");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const Modals = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let id = req.header('hx-trigger');
|
||||
let title = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
if (id == 'permissions') {
|
||||
let permissions_list = '';
|
||||
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
|
||||
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
|
||||
let users = await User.findAll({ attributes: ['username', 'UUID']});
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
|
||||
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (!exists) {
|
||||
const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
|
||||
}
|
||||
|
||||
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
|
||||
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
|
||||
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
|
||||
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
|
||||
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
|
||||
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
|
||||
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
|
||||
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
|
||||
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
|
||||
permissions_list += user_permissions;
|
||||
}
|
||||
|
||||
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
|
||||
res.send(permissions_modal);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (id == 'uninstall') {
|
||||
let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
// let containerPermissions = await Permission.findAll({ where: {containerName: name}});
|
||||
res.send(modal);
|
||||
return;
|
||||
}
|
||||
|
||||
let modal = readFileSync('./views/modals/details.html', 'utf8');
|
||||
let details = await containerInfo(name);
|
||||
|
||||
modal = modal.replace(/AppName/g, details.name);
|
||||
modal = modal.replace(/AppImage/g, details.image);
|
||||
res.send(modal);
|
||||
}
|
104
controllers/register.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
import { User, Syslog, Permission } from '../database/models.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
let SECRET = process.env.SECRET || "MrWiskers"
|
||||
|
||||
export const Register = function(req,res){
|
||||
if(req.session.user){
|
||||
res.redirect("/logout");
|
||||
} else {
|
||||
res.render("register",{
|
||||
"error":"",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const submitRegister = async function(req,res){
|
||||
|
||||
let { name, username, email, password, confirmPassword, secret } = req.body;
|
||||
email = email.toLowerCase();
|
||||
|
||||
|
||||
if (secret != SECRET) {
|
||||
const syslog = await Syslog.create({
|
||||
user: username,
|
||||
email: email,
|
||||
event: "Failed Registration",
|
||||
message: "Invalid secret",
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
}
|
||||
|
||||
if((name && email && password && confirmPassword && username) && (secret == SECRET) && (password == confirmPassword)){
|
||||
|
||||
async function userRole () {
|
||||
let userCount = await User.count();
|
||||
if(userCount == 0){
|
||||
return "admin";
|
||||
}else{
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
|
||||
let existingUser = await User.findOne({ where: {email:email}});
|
||||
if(!existingUser){
|
||||
|
||||
try {
|
||||
let currentDate = new Date();
|
||||
let newLogin = currentDate.toLocaleString();
|
||||
|
||||
const user = await User.create({
|
||||
name: name,
|
||||
username: username,
|
||||
email: email,
|
||||
password: bcrypt.hashSync(password,10),
|
||||
role: await userRole(),
|
||||
group: 'all',
|
||||
lastLogin: newLogin,
|
||||
});
|
||||
|
||||
// make sure the user was created and get the UUID.
|
||||
let newUser = await User.findOne({ where: {email:email}});
|
||||
let match = await bcrypt.compare(password,newUser.password);
|
||||
|
||||
if(match){
|
||||
req.session.user = newUser.username;
|
||||
req.session.UUID = newUser.UUID;
|
||||
req.session.role = newUser.role;
|
||||
|
||||
const permission = await Permission.create({
|
||||
user: newUser.username,
|
||||
userID: newUser.UUID
|
||||
});
|
||||
|
||||
const syslog = await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: email,
|
||||
event: "Successful Registration",
|
||||
message: "User registered successfully",
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
|
||||
res.redirect("/dashboard");
|
||||
}
|
||||
} catch(err) {
|
||||
res.render("register",{
|
||||
"error":"Something went wrong when creating account.",
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
// return an error.
|
||||
res.render("register",{
|
||||
"error":"User with that email already exists.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Redirect to the signup page.
|
||||
res.render("register",{
|
||||
"error":"Please fill in all the fields.",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,22 +1,10 @@
|
|||
const User = require('../database/UserModel.js');
|
||||
const Server = require('../database/ServerSettings.js');
|
||||
|
||||
exports.Settings = async function(req, res) {
|
||||
if (req.session.role == "admin") {
|
||||
// Get the user.
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
export const Settings = (req, res) => {
|
||||
|
||||
|
||||
|
||||
// Render the home page
|
||||
res.render("pages/settings", {
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
isLoggedIn: true
|
||||
});
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
res.render("settings", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
alert: '',
|
||||
});
|
||||
}
|
31
controllers/supporters.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { User } from "../database/models.js";
|
||||
|
||||
export const Supporters = async (req, res) => {
|
||||
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
|
||||
|
||||
res.render("supporters", {
|
||||
first_name: user.name,
|
||||
last_name: user.name,
|
||||
name: user.name,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
alert: '',
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
let thanks = 0;
|
||||
export const Thanks = async (req, res) => {
|
||||
thanks++;
|
||||
let data = thanks.toString();
|
||||
if (thanks > 999) {
|
||||
data = 'Did you really click 1000 times?!';
|
||||
}
|
||||
res.send(data);
|
||||
}
|
37
controllers/syslogs.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Syslog } from '../database/models.js';
|
||||
|
||||
export const Syslogs = async function(req, res) {
|
||||
|
||||
let logs = '';
|
||||
|
||||
const syslogs = await Syslog.findAll({
|
||||
order: [
|
||||
['id', 'DESC']
|
||||
]
|
||||
});
|
||||
|
||||
for (const log of syslogs) {
|
||||
let date = (log.createdAt).toDateString();
|
||||
let time = (log.createdAt).toLocaleTimeString();
|
||||
let datetime = `${time} ${date}`;
|
||||
|
||||
logs += `<tr>
|
||||
<td class="sort-id">${log.id}</td>
|
||||
<td class="sort-user">${log.user}</td>
|
||||
<td class="sort-email">${log.email}</td>
|
||||
<td class="sort-event">${log.event}</td>
|
||||
<td class="sort-message">${log.message}</td>
|
||||
<td class="sort-ip">${log.ip}</td>
|
||||
<td class="sort-datetime">${datetime}</td>
|
||||
</tr>`
|
||||
}
|
||||
|
||||
res.render("syslogs", {
|
||||
name: req.session.user || 'Dev',
|
||||
role: req.session.role || 'Dev',
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
logs: logs,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
|
@ -1,54 +1,62 @@
|
|||
const User = require('../database/UserModel');
|
||||
import { User } from '../database/models.js';
|
||||
|
||||
exports.Users = async function(req, res) {
|
||||
if (req.session.role == "admin") {
|
||||
export const Users = async (req, res) => {
|
||||
|
||||
let user_list = `
|
||||
<tr>
|
||||
<th><input class="form-check-input" type="checkbox"></th>
|
||||
<th>ID</th>
|
||||
<th>Avatar</th>
|
||||
<th>Name</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>UUID</th>
|
||||
<th>Role</th>
|
||||
<th>Last Login</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>`
|
||||
|
||||
// Get the user.
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
let user_list = `
|
||||
let allUsers = await User.findAll();
|
||||
allUsers.forEach((account) => {
|
||||
|
||||
let active = '<span class="badge badge-outline text-green" title="User has logged-in within the last 30 days.">Active</span>'
|
||||
let lastLogin = new Date(account.lastLogin);
|
||||
let currentDate = new Date();
|
||||
let days = Math.floor((currentDate - lastLogin) / (1000 * 60 * 60 * 24));
|
||||
let avatar = account.username.charAt(0);
|
||||
|
||||
if (days > 30) {
|
||||
active = '<span class="badge badge-outline text-grey" title="User has not logged-in within the last 30 days.">Inactive</span>';
|
||||
}
|
||||
|
||||
|
||||
|
||||
let info = `
|
||||
<tr>
|
||||
<th><input class="form-check-input" type="checkbox"></th>
|
||||
<th>ID</th>
|
||||
<th>Avatar</th>
|
||||
<th>Name</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>UUID</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
<td><input class="form-check-input" type="checkbox"></td>
|
||||
<td>${account.id}</td>
|
||||
<td><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
|
||||
<td>${account.name}</td>
|
||||
<td>${account.username}</td>
|
||||
<td>${account.email}</td>
|
||||
<td>${account.UUID}</td>
|
||||
<td>${account.role}</td>
|
||||
<td>${account.lastLogin}</td>
|
||||
<td>${active}</td>
|
||||
<td><a href="#" class="btn">View</a></td>
|
||||
</tr>`
|
||||
|
||||
let users = await User.findAll();
|
||||
users.forEach((account) => {
|
||||
full_name = account.first_name + ' ' + account.last_name;
|
||||
user_info = `
|
||||
<tr>
|
||||
<td><input class="form-check-input" type="checkbox"></td>
|
||||
<td>${user.id}</td>
|
||||
<td><span class="avatar me-2">${account.avatar}</span></td>
|
||||
<td>${full_name}</td>
|
||||
<td>${account.username}</td>
|
||||
<td>${account.email}</td>
|
||||
<td>${account.UUID}</td>
|
||||
<td>${account.role}</td>
|
||||
<td><span class="badge badge-outline text-green">Active</span></td>
|
||||
<td><a href="#" class="btn">Edit</a></td>
|
||||
</tr>`
|
||||
user_list += info;
|
||||
});
|
||||
|
||||
user_list += user_info;
|
||||
});
|
||||
|
||||
// Render the home page
|
||||
res.render("pages/users", {
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
isLoggedIn: true,
|
||||
user_list: user_list
|
||||
});
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
res.render("users", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
user_list: user_list,
|
||||
alert: ''
|
||||
});
|
||||
|
||||
}
|
9
controllers/variables.js
Normal 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,
|
||||
});
|
||||
}
|
119
controllers/volumes.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { docker } from '../server.js';
|
||||
|
||||
export const Volumes = async function(req, res) {
|
||||
let container_volumes = [];
|
||||
let volume_list = '';
|
||||
|
||||
// Table header
|
||||
volume_list = `<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><label class="table-sort" data-sort="sort-type">Type</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-city">Mount point</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
|
||||
// List all containers
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
|
||||
// Get the first 6 volumes from each container
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
try { container_volumes.push({type: containers[i].Mounts[0].Type, source: containers[i].Mounts[0].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[1].Type, source: containers[i].Mounts[1].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[2].Type, source: containers[i].Mounts[2].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[3].Type, source: containers[i].Mounts[3].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[4].Type, source: containers[i].Mounts[4].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[5].Type, source: containers[i].Mounts[5].Source}); } catch { }
|
||||
}
|
||||
|
||||
// List ALL volumes
|
||||
let list = await docker.listVolumes({ all: true });
|
||||
let volumes = list.Volumes;
|
||||
|
||||
// Create a table row for each volume
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
let volume = volumes[i];
|
||||
let name = "" + volume.Name;
|
||||
let mount = "" + volume.Mountpoint;
|
||||
let type = "Bind";
|
||||
|
||||
// Check if the volume is being used by any of the containers
|
||||
let status = '';
|
||||
if (container_volumes.some(volume => volume.source === mount)) { status = "In use"; }
|
||||
if (container_volumes.some(volume => volume.source === mount && volume.type === 'volume')) { type = "Volume"; }
|
||||
|
||||
let row = `
|
||||
<tr>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="${name}" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-type">${type}</td>
|
||||
<td class="sort-name">${name}</td>
|
||||
<td class="sort-city">${mount}</td>
|
||||
<td class="sort-score text-green">${status}</td>
|
||||
<td class="sort-date" data-date="1628122643">${volume.CreatedAt}</td>
|
||||
<td class="sort-quantity">MB</td>
|
||||
<td class="text-end"><a class="btn" href="#">Details</a></td>
|
||||
</tr>`
|
||||
|
||||
volume_list += row;
|
||||
}
|
||||
|
||||
volume_list += `</tbody>`
|
||||
|
||||
|
||||
res.render("volumes", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
volume_list: volume_list,
|
||||
volume_count: volumes.length,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const addVolume = async function(req, res) {
|
||||
|
||||
let volume = req.body.volume;
|
||||
|
||||
docker.createVolume({
|
||||
Name: volume
|
||||
});
|
||||
res.redirect("/volumes");
|
||||
}
|
||||
|
||||
|
||||
export const removeVolume = async function(req, res) {
|
||||
let volumes = req.body.select;
|
||||
|
||||
if (typeof(volumes) == 'string') {
|
||||
volumes = [volumes];
|
||||
}
|
||||
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
|
||||
if (volumes[i] != 'on') {
|
||||
try {
|
||||
console.log(`Removing volume: ${volumes[i]}`);
|
||||
let volume = docker.getVolume(volumes[i]);
|
||||
await volume.remove();
|
||||
} catch (error) {
|
||||
console.log(`Unable to remove volume: ${volumes[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect("/volumes");
|
||||
}
|
||||
|
||||
|
||||
// docker.df(volume.Name).then((data) => {
|
||||
// for (let key in data) {
|
||||
// console.log(data[key]);
|
||||
// }
|
||||
// });
|
|
@ -1,42 +0,0 @@
|
|||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false
|
||||
});
|
||||
|
||||
|
||||
const Server = sequelize.define('Server', {
|
||||
// Model attributes are defined here
|
||||
timezone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
hwa: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
media: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
pgid: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
puid: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
}
|
||||
});
|
||||
|
||||
async function syncModel() {
|
||||
await sequelize.sync();
|
||||
console.log('Server model synced');
|
||||
}
|
||||
|
||||
syncModel();
|
||||
|
||||
|
||||
module.exports = Server;
|
|
@ -1,63 +0,0 @@
|
|||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false
|
||||
});
|
||||
|
||||
|
||||
const User = sequelize.define('User', {
|
||||
// Model attributes are defined here
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
first_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
last_name: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
// allowNull: false
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
group: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.STRING
|
||||
// allowNull defaults to true
|
||||
},
|
||||
UUID: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4
|
||||
}
|
||||
});
|
||||
|
||||
async function syncModel() {
|
||||
await sequelize.sync();
|
||||
console.log('User model synced');
|
||||
}
|
||||
|
||||
syncModel();
|
||||
|
||||
|
||||
module.exports = User;
|
256
database/models.js
Normal file
|
@ -0,0 +1,256 @@
|
|||
import { Sequelize, DataTypes } from 'sequelize';
|
||||
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false,
|
||||
});
|
||||
|
||||
export const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
group: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
lastLogin: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
UUID: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
}
|
||||
});
|
||||
|
||||
export const Container = sequelize.define('Container', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
service: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
image: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
external_port: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
internal_port: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
ports: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
volumes: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
environment_variables: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
labels: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
IPv4: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
style: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
cpu: {
|
||||
// store the last 15 values from dockerContainerStats
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
ram: {
|
||||
// store the last 15 values from dockerContainerStats
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
||||
|
||||
export const Permission = sequelize.define('Permission', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
containerName: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
containerID: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
user: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
userID: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
install: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
uninstall: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
edit: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
upgrade: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
start: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
stop: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
restart: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
pause: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
logs: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
hide: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
reset_view: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
view: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const Syslog = sequelize.define('Syslog', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
user: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
event: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
ip : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const Notification = sequelize.define('Notification', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
read: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
createdAt : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdBy : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const Settings = sequelize.define('Settings', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const Variables = sequelize.define('Variables', {
|
||||
find: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
replace: {
|
||||
type: DataTypes.STRING,
|
||||
}
|
||||
});
|
|
@ -1,205 +0,0 @@
|
|||
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const { exec, execSync } = require("child_process");
|
||||
|
||||
const { docker } = require('./system');
|
||||
|
||||
var DockerodeCompose = require('dockerode-compose');
|
||||
|
||||
|
||||
module.exports.install = async function (data) {
|
||||
|
||||
console.log(`[Start of install function]`);
|
||||
|
||||
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
|
||||
let { port0, port1, port2, port3, port4, port5 } = data;
|
||||
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
|
||||
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
|
||||
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
|
||||
|
||||
|
||||
if ((service_name.includes('caddy')) || (name.includes('caddy'))) {
|
||||
req.app.locals.caddy = 'enabled';
|
||||
}
|
||||
|
||||
let docker_volumes = [];
|
||||
|
||||
if (image.startsWith('https://')){
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
|
||||
console.log(`Downloaded stackfile: ${image}`);
|
||||
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
|
||||
let services = Object.keys(stackfile.services);
|
||||
|
||||
for ( let i = 0; i < services.length; i++ ) {
|
||||
try {
|
||||
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
|
||||
} catch { console.log('no env') }
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
let compose_file = `version: '3'`;
|
||||
compose_file += `\nservices:`
|
||||
compose_file += `\n ${service_name}:`
|
||||
compose_file += `\n container_name: ${name}`;
|
||||
compose_file += `\n image: ${image}`;
|
||||
|
||||
// Command
|
||||
if (command_check == 'on') {
|
||||
compose_file += `\n command: ${command}`
|
||||
}
|
||||
|
||||
// Network mode
|
||||
if (net_mode == 'host') {
|
||||
compose_file += `\n network_mode: 'host'`
|
||||
}
|
||||
else if (net_mode != 'host' && net_mode != 'docker') {
|
||||
compose_file += `\n network_mode: '${net_mode}'`
|
||||
}
|
||||
|
||||
// Restart policy
|
||||
if (restart_policy != '') {
|
||||
compose_file += `\n restart: ${restart_policy}`
|
||||
}
|
||||
|
||||
// Ports
|
||||
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n ports:`
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (data[`port${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Volumes
|
||||
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
|
||||
compose_file += `\n volumes:`
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
|
||||
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
|
||||
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
|
||||
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
}
|
||||
|
||||
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
|
||||
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
|
||||
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
|
||||
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
docker_volumes.push(`${name}_${volume_name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
compose_file += `\n environment:`
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`env${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Add labels
|
||||
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
|
||||
compose_file += `\n labels:`
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`label${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add privileged mode
|
||||
|
||||
if (data.privileged == 'on') {
|
||||
compose_file += `\n privileged: true`
|
||||
}
|
||||
|
||||
|
||||
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
|
||||
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`env${i}`] == 'on') {
|
||||
if (data[`env_${i}_name`] == 'DRINODE') {
|
||||
compose_file += `\n deploy:`
|
||||
compose_file += `\n resources:`
|
||||
compose_file += `\n reservations:`
|
||||
compose_file += `\n devices:`
|
||||
compose_file += `\n - driver: nvidia`
|
||||
compose_file += `\n count: 1`
|
||||
compose_file += `\n capabilities: [gpu]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add any docker volumes to the docker-compose file
|
||||
if ( docker_volumes.length > 0 ) {
|
||||
compose_file += `\n`
|
||||
compose_file += `\nvolumes:`
|
||||
|
||||
// check docker_volumes for duplicates and remove them completely
|
||||
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
|
||||
|
||||
for (let i = 0; i < docker_volumes.length; i++) {
|
||||
if ( docker_volumes[i] != '') {
|
||||
compose_file += `\n ${docker_volumes[i]}:`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
|
||||
} catch { console.log('error creating directory or compose file') }
|
||||
|
||||
try {
|
||||
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
|
||||
|
||||
(async () => {
|
||||
await compose.pull();
|
||||
await compose.up();
|
||||
})();
|
||||
|
||||
} catch { console.log('error running compose file')}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.uninstall = async function (data) {
|
||||
|
||||
|
||||
if (data.confirm == 'Yes') {
|
||||
|
||||
|
||||
var containerName = docker.getContainer(`${data.service_name}`);
|
||||
|
||||
try {
|
||||
containerName.stop(function (err, data) {
|
||||
if (data) {
|
||||
containerName.remove(function (err, data) {
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
containerName.remove(function (err, data) {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const { exec, execSync } = require("child_process");
|
||||
|
||||
const { docker } = require('./system');
|
||||
|
||||
var DockerodeCompose = require('dockerode-compose');
|
||||
|
||||
|
||||
module.exports.install = async function (data) {
|
||||
|
||||
console.log(`[Start of install function]`);
|
||||
|
||||
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
|
||||
let { port0, port1, port2, port3, port4, port5 } = data;
|
||||
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
|
||||
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
|
||||
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
|
||||
|
||||
|
||||
if ((service_name.includes('caddy')) || (name.includes('caddy'))) {
|
||||
req.app.locals.caddy = 'enabled';
|
||||
}
|
||||
|
||||
let docker_volumes = [];
|
||||
|
||||
if (image.startsWith('https://')){
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
|
||||
console.log(`Downloaded stackfile: ${image}`);
|
||||
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
|
||||
let services = Object.keys(stackfile.services);
|
||||
|
||||
for ( let i = 0; i < services.length; i++ ) {
|
||||
try {
|
||||
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
|
||||
} catch { console.log('no env') }
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
let compose_file = `version: '3'`;
|
||||
compose_file += `\nservices:`
|
||||
compose_file += `\n ${service_name}:`
|
||||
compose_file += `\n container_name: ${name}`;
|
||||
compose_file += `\n image: ${image}`;
|
||||
|
||||
// Command
|
||||
if (command_check == 'on') {
|
||||
compose_file += `\n command: ${command}`
|
||||
}
|
||||
|
||||
// Network mode
|
||||
if (net_mode == 'host') {
|
||||
compose_file += `\n network_mode: 'host'`
|
||||
}
|
||||
else if (net_mode != 'host' && net_mode != 'docker') {
|
||||
compose_file += `\n network_mode: '${net_mode}'`
|
||||
}
|
||||
|
||||
// Restart policy
|
||||
if (restart_policy != '') {
|
||||
compose_file += `\n restart: ${restart_policy}`
|
||||
}
|
||||
|
||||
// Ports
|
||||
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n ports:`
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (data[`port${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Volumes
|
||||
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
|
||||
compose_file += `\n volumes:`
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
|
||||
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
|
||||
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
|
||||
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
}
|
||||
|
||||
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
|
||||
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
|
||||
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
|
||||
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
docker_volumes.push(`${name}_${volume_name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
compose_file += `\n environment:`
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`env${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Add labels
|
||||
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
|
||||
compose_file += `\n labels:`
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`label${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add privileged mode
|
||||
|
||||
if (data.privileged == 'on') {
|
||||
compose_file += `\n privileged: true`
|
||||
}
|
||||
|
||||
|
||||
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
|
||||
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`env${i}`] == 'on') {
|
||||
if (data[`env_${i}_name`] == 'DRINODE') {
|
||||
compose_file += `\n deploy:`
|
||||
compose_file += `\n resources:`
|
||||
compose_file += `\n reservations:`
|
||||
compose_file += `\n devices:`
|
||||
compose_file += `\n - driver: nvidia`
|
||||
compose_file += `\n count: 1`
|
||||
compose_file += `\n capabilities: [gpu]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add any docker volumes to the docker-compose file
|
||||
if ( docker_volumes.length > 0 ) {
|
||||
compose_file += `\n`
|
||||
compose_file += `\nvolumes:`
|
||||
|
||||
// check docker_volumes for duplicates and remove them completely
|
||||
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
|
||||
|
||||
for (let i = 0; i < docker_volumes.length; i++) {
|
||||
if ( docker_volumes[i] != '') {
|
||||
compose_file += `\n ${docker_volumes[i]}:`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
|
||||
} catch { console.log('error creating directory or compose file') }
|
||||
|
||||
try {
|
||||
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
|
||||
|
||||
(async () => {
|
||||
await compose.pull();
|
||||
await compose.up();
|
||||
})();
|
||||
|
||||
} catch { console.log('error running compose file')}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.uninstall = async function (data) {
|
||||
if (data.confirm == 'Yes') {
|
||||
console.log(`Uninstalling ${data.service_name}: ${data}`);
|
||||
var containerName = docker.getContainer(`${data.service_name}`);
|
||||
try {
|
||||
await containerName.stop();
|
||||
console.log(`Stopped ${data.service_name} container`);
|
||||
} catch {
|
||||
console.log(`Error stopping ${data.service_name} container`);
|
||||
}
|
||||
try {
|
||||
await containerName.remove();
|
||||
console.log(`Removed ${data.service_name} container`);
|
||||
} catch {
|
||||
console.log(`Error removing ${data.service_name} container`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,295 +0,0 @@
|
|||
const { currentLoad, mem, networkStats, fsSize, dockerContainerStats } = require('systeminformation');
|
||||
var Docker = require('dockerode');
|
||||
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
// export docker
|
||||
module.exports.docker = docker;
|
||||
|
||||
module.exports.serverStats = async function () {
|
||||
const cpuUsage = await currentLoad();
|
||||
const ramUsage = await mem();
|
||||
const netUsage = await networkStats();
|
||||
const diskUsage = await fsSize();
|
||||
|
||||
const info = {
|
||||
cpu: Math.round(cpuUsage.currentLoad),
|
||||
ram: Math.round((ramUsage.active / ramUsage.total) * 100),
|
||||
tx: netUsage[0].tx_bytes,
|
||||
rx: netUsage[0].rx_bytes,
|
||||
disk: diskUsage[0].use,
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.containerList = async function () {
|
||||
let card_list = '';
|
||||
|
||||
const data = await docker.listContainers({ all: true });
|
||||
for (const container of data) {
|
||||
|
||||
|
||||
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
|
||||
|
||||
let imageVersion = container.Image.split('/');
|
||||
let service = imageVersion[imageVersion.length - 1].split(':')[0];
|
||||
|
||||
let containerId = docker.getContainer(container.Id);
|
||||
let containerInfo = await containerId.inspect();
|
||||
|
||||
// Get ports //////////////////////////
|
||||
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 { console.log('no ports') }
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (ports_list[i] == undefined) {
|
||||
let ports = {
|
||||
check : '',
|
||||
external: '',
|
||||
internal: '',
|
||||
protocol: ''
|
||||
}
|
||||
ports_list[i] = ports;
|
||||
}
|
||||
} /////////////////////////////////////
|
||||
|
||||
|
||||
// Get volumes ////////////////////////
|
||||
let volumes_list = [];
|
||||
try { for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
|
||||
let volumes = {
|
||||
check : 'checked',
|
||||
bind: value.split(':')[0],
|
||||
container: value.split(':')[1],
|
||||
readwrite: value.split(':')[2]
|
||||
}
|
||||
volumes_list.push(volumes);
|
||||
}} catch { console.log('no volumes') }
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (volumes_list[i] == undefined) {
|
||||
let volumes = {
|
||||
check : '',
|
||||
bind: '',
|
||||
container: '',
|
||||
readwrite: ''
|
||||
}
|
||||
volumes_list[i] = volumes;
|
||||
}
|
||||
} /////////////////////////////////////
|
||||
|
||||
|
||||
// Get environment variables.
|
||||
let environment_variables = [];
|
||||
try { for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
|
||||
let env = {
|
||||
check : 'checked',
|
||||
name: value.split('=')[0],
|
||||
default: value.split('=')[1]
|
||||
}
|
||||
environment_variables.push(env);
|
||||
}} catch { console.log('no env') }
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (environment_variables[i] == undefined) {
|
||||
let env = {
|
||||
check : '',
|
||||
name: '',
|
||||
default: ''
|
||||
}
|
||||
environment_variables[i] = env;
|
||||
}
|
||||
}
|
||||
|
||||
// Get labels.
|
||||
let labels = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
|
||||
let label = {
|
||||
check : 'checked',
|
||||
name: key,
|
||||
value: value
|
||||
}
|
||||
labels.push(label);
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (labels[i] == undefined) {
|
||||
let label = {
|
||||
check : '',
|
||||
name: '',
|
||||
value: ''
|
||||
}
|
||||
labels[i] = label;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let container_info = {
|
||||
name: container.Names[0].slice(1),
|
||||
service: service,
|
||||
id: container.Id,
|
||||
state: container.State,
|
||||
image: container.Image,
|
||||
external_port: ports_list[0].external || 0,
|
||||
internal_port: ports_list[0].internal || 0,
|
||||
ports: ports_list,
|
||||
volumes: volumes_list,
|
||||
environment_variables: environment_variables,
|
||||
labels: labels,
|
||||
}
|
||||
|
||||
let dockerCard = dashCard(container_info);
|
||||
|
||||
card_list += dockerCard;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return card_list;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports.containerStats = async function () {
|
||||
|
||||
let container_stats = [];
|
||||
const data = await docker.listContainers({ all: true });
|
||||
|
||||
for (const container of data) {
|
||||
|
||||
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
|
||||
const stats = await dockerContainerStats(container.Id);
|
||||
|
||||
let container_stat = {
|
||||
name: container.Names[0].slice(1),
|
||||
cpu: Math.round(stats[0].cpuPercent),
|
||||
ram: Math.round(stats[0].memPercent)
|
||||
}
|
||||
|
||||
//push stats to an array
|
||||
container_stats.push(container_stat);
|
||||
}
|
||||
}
|
||||
return container_stats;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports.containerAction = async function (data) {
|
||||
|
||||
let { user, role, action, container, state } = data;
|
||||
|
||||
console.log(`${user} wants to: ${action} ${container}`);
|
||||
|
||||
if (role == 'admin') {
|
||||
var containerName = docker.getContainer(container);
|
||||
|
||||
if ((action == 'start') && (state == 'stopped')) {
|
||||
containerName.start();
|
||||
} else if ((action == 'start') && (state == 'paused')) {
|
||||
containerName.unpause();
|
||||
} else if ((action == 'stop') && (state != 'stopped')) {
|
||||
containerName.stop();
|
||||
} else if ((action == 'pause') && (state == 'running')) {
|
||||
containerName.pause();
|
||||
} else if ((action == 'pause') && (state == 'paused')) {
|
||||
containerName.unpause();
|
||||
} else if (action == 'restart') {
|
||||
containerName.restart();
|
||||
}
|
||||
} else {
|
||||
console.log('User is not an admin');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.containerExec = async function (data) {
|
||||
|
||||
let { container, command } = data;
|
||||
|
||||
var containerName = docker.getContainer(container);
|
||||
|
||||
var options = {
|
||||
Cmd: ['/bin/sh', '-c', command],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true
|
||||
};
|
||||
|
||||
containerName.exec(options, function (err, exec) {
|
||||
if (err) return;
|
||||
|
||||
exec.start(function (err, stream) {
|
||||
if (err) return;
|
||||
|
||||
containerName.modem.demuxStream(stream, process.stdout, process.stderr);
|
||||
|
||||
exec.inspect(function (err, data) {
|
||||
if (err) return;
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports.containerLogs = function (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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
969
package-lock.json
generated
46
package.json
|
@ -1,23 +1,29 @@
|
|||
{
|
||||
"name": "dweeb-ui",
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.0",
|
||||
"child_process": "^1.0.2",
|
||||
"dockerode": "^4.0.0",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"sequelize": "^6.35.1",
|
||||
"socket.io": "^4.6.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"systeminformation": "^5.21.20"
|
||||
"name": "dweebui",
|
||||
"version": "0.60",
|
||||
"description": "Free and Open-Source WebUI For Managing Your Containers.",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"description": ""
|
||||
"keywords": [],
|
||||
"author": "lllllllillllllillll",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.12",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dockerode": "^4.0.2",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"memorystore": "^1.6.7",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sequelize": "^6.37.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"systeminformation": "^5.22.9",
|
||||
"yaml": "^2.4.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.meter {
|
||||
box-sizing: content-box;
|
||||
height: 15px; /* Can be anything */
|
||||
height: 15px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
|
@ -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 {
|
||||
|
|
82
public/css/tabler.min.css
vendored
|
@ -6036,7 +6036,7 @@ fieldset:disabled .btn {
|
|||
color: var(--tblr-alert-color);
|
||||
background-color: var(--tblr-alert-bg);
|
||||
border: var(--tblr-alert-border);
|
||||
border-radius: var(--tblr-alert-border-radius)
|
||||
border-radius: var(--tblr-alert-border-radius);
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
|
@ -11462,47 +11462,38 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.column-gap-0 {
|
||||
-moz-column-gap: 0 !important;
|
||||
column-gap: 0 !important
|
||||
}
|
||||
|
||||
.column-gap-1 {
|
||||
-moz-column-gap: .25rem !important;
|
||||
column-gap: .25rem !important
|
||||
}
|
||||
|
||||
.column-gap-2 {
|
||||
-moz-column-gap: .5rem !important;
|
||||
column-gap: .5rem !important
|
||||
}
|
||||
|
||||
.column-gap-3 {
|
||||
-moz-column-gap: 1rem !important;
|
||||
column-gap: 1rem !important
|
||||
}
|
||||
|
||||
.column-gap-4 {
|
||||
-moz-column-gap: 1.5rem !important;
|
||||
column-gap: 1.5rem !important
|
||||
}
|
||||
|
||||
.column-gap-5 {
|
||||
-moz-column-gap: 2rem !important;
|
||||
column-gap: 2rem !important
|
||||
}
|
||||
|
||||
.column-gap-6 {
|
||||
-moz-column-gap: 3rem !important;
|
||||
column-gap: 3rem !important
|
||||
}
|
||||
|
||||
.column-gap-7 {
|
||||
-moz-column-gap: 5rem !important;
|
||||
column-gap: 5rem !important
|
||||
}
|
||||
|
||||
.column-gap-8 {
|
||||
-moz-column-gap: 8rem !important;
|
||||
column-gap: 8rem !important
|
||||
}
|
||||
|
||||
|
@ -12910,17 +12901,14 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.columns-2 {
|
||||
-moz-columns: 2 !important;
|
||||
columns: 2 !important
|
||||
}
|
||||
|
||||
.columns-3 {
|
||||
-moz-columns: 3 !important;
|
||||
columns: 3 !important
|
||||
}
|
||||
|
||||
.columns-4 {
|
||||
-moz-columns: 4 !important;
|
||||
columns: 4 !important
|
||||
}
|
||||
|
||||
|
@ -13821,47 +13809,38 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.column-gap-sm-0 {
|
||||
-moz-column-gap: 0 !important;
|
||||
column-gap: 0 !important
|
||||
}
|
||||
|
||||
.column-gap-sm-1 {
|
||||
-moz-column-gap: .25rem !important;
|
||||
column-gap: .25rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-2 {
|
||||
-moz-column-gap: .5rem !important;
|
||||
column-gap: .5rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-3 {
|
||||
-moz-column-gap: 1rem !important;
|
||||
column-gap: 1rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-4 {
|
||||
-moz-column-gap: 1.5rem !important;
|
||||
column-gap: 1.5rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-5 {
|
||||
-moz-column-gap: 2rem !important;
|
||||
column-gap: 2rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-6 {
|
||||
-moz-column-gap: 3rem !important;
|
||||
column-gap: 3rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-7 {
|
||||
-moz-column-gap: 5rem !important;
|
||||
column-gap: 5rem !important
|
||||
}
|
||||
|
||||
.column-gap-sm-8 {
|
||||
-moz-column-gap: 8rem !important;
|
||||
column-gap: 8rem !important
|
||||
}
|
||||
|
||||
|
@ -13878,17 +13857,14 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.columns-sm-2 {
|
||||
-moz-columns: 2 !important;
|
||||
columns: 2 !important
|
||||
}
|
||||
|
||||
.columns-sm-3 {
|
||||
-moz-columns: 3 !important;
|
||||
columns: 3 !important
|
||||
}
|
||||
|
||||
.columns-sm-4 {
|
||||
-moz-columns: 4 !important;
|
||||
columns: 4 !important
|
||||
}
|
||||
}
|
||||
|
@ -14790,47 +14766,38 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.column-gap-md-0 {
|
||||
-moz-column-gap: 0 !important;
|
||||
column-gap: 0 !important
|
||||
}
|
||||
|
||||
.column-gap-md-1 {
|
||||
-moz-column-gap: .25rem !important;
|
||||
column-gap: .25rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-2 {
|
||||
-moz-column-gap: .5rem !important;
|
||||
column-gap: .5rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-3 {
|
||||
-moz-column-gap: 1rem !important;
|
||||
column-gap: 1rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-4 {
|
||||
-moz-column-gap: 1.5rem !important;
|
||||
column-gap: 1.5rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-5 {
|
||||
-moz-column-gap: 2rem !important;
|
||||
column-gap: 2rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-6 {
|
||||
-moz-column-gap: 3rem !important;
|
||||
column-gap: 3rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-7 {
|
||||
-moz-column-gap: 5rem !important;
|
||||
column-gap: 5rem !important
|
||||
}
|
||||
|
||||
.column-gap-md-8 {
|
||||
-moz-column-gap: 8rem !important;
|
||||
column-gap: 8rem !important
|
||||
}
|
||||
|
||||
|
@ -14847,17 +14814,14 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.columns-md-2 {
|
||||
-moz-columns: 2 !important;
|
||||
columns: 2 !important
|
||||
}
|
||||
|
||||
.columns-md-3 {
|
||||
-moz-columns: 3 !important;
|
||||
columns: 3 !important
|
||||
}
|
||||
|
||||
.columns-md-4 {
|
||||
-moz-columns: 4 !important;
|
||||
columns: 4 !important
|
||||
}
|
||||
}
|
||||
|
@ -15759,47 +15723,38 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.column-gap-lg-0 {
|
||||
-moz-column-gap: 0 !important;
|
||||
column-gap: 0 !important
|
||||
}
|
||||
|
||||
.column-gap-lg-1 {
|
||||
-moz-column-gap: .25rem !important;
|
||||
column-gap: .25rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-2 {
|
||||
-moz-column-gap: .5rem !important;
|
||||
column-gap: .5rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-3 {
|
||||
-moz-column-gap: 1rem !important;
|
||||
column-gap: 1rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-4 {
|
||||
-moz-column-gap: 1.5rem !important;
|
||||
column-gap: 1.5rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-5 {
|
||||
-moz-column-gap: 2rem !important;
|
||||
column-gap: 2rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-6 {
|
||||
-moz-column-gap: 3rem !important;
|
||||
column-gap: 3rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-7 {
|
||||
-moz-column-gap: 5rem !important;
|
||||
column-gap: 5rem !important
|
||||
}
|
||||
|
||||
.column-gap-lg-8 {
|
||||
-moz-column-gap: 8rem !important;
|
||||
column-gap: 8rem !important
|
||||
}
|
||||
|
||||
|
@ -15816,17 +15771,14 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.columns-lg-2 {
|
||||
-moz-columns: 2 !important;
|
||||
columns: 2 !important
|
||||
}
|
||||
|
||||
.columns-lg-3 {
|
||||
-moz-columns: 3 !important;
|
||||
columns: 3 !important
|
||||
}
|
||||
|
||||
.columns-lg-4 {
|
||||
-moz-columns: 4 !important;
|
||||
columns: 4 !important
|
||||
}
|
||||
}
|
||||
|
@ -16728,47 +16680,38 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.column-gap-xl-0 {
|
||||
-moz-column-gap: 0 !important;
|
||||
column-gap: 0 !important
|
||||
}
|
||||
|
||||
.column-gap-xl-1 {
|
||||
-moz-column-gap: .25rem !important;
|
||||
column-gap: .25rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-2 {
|
||||
-moz-column-gap: .5rem !important;
|
||||
column-gap: .5rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-3 {
|
||||
-moz-column-gap: 1rem !important;
|
||||
column-gap: 1rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-4 {
|
||||
-moz-column-gap: 1.5rem !important;
|
||||
column-gap: 1.5rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-5 {
|
||||
-moz-column-gap: 2rem !important;
|
||||
column-gap: 2rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-6 {
|
||||
-moz-column-gap: 3rem !important;
|
||||
column-gap: 3rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-7 {
|
||||
-moz-column-gap: 5rem !important;
|
||||
column-gap: 5rem !important
|
||||
}
|
||||
|
||||
.column-gap-xl-8 {
|
||||
-moz-column-gap: 8rem !important;
|
||||
column-gap: 8rem !important
|
||||
}
|
||||
|
||||
|
@ -16785,17 +16728,14 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.columns-xl-2 {
|
||||
-moz-columns: 2 !important;
|
||||
columns: 2 !important
|
||||
}
|
||||
|
||||
.columns-xl-3 {
|
||||
-moz-columns: 3 !important;
|
||||
columns: 3 !important
|
||||
}
|
||||
|
||||
.columns-xl-4 {
|
||||
-moz-columns: 4 !important;
|
||||
columns: 4 !important
|
||||
}
|
||||
}
|
||||
|
@ -17697,47 +17637,38 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.column-gap-xxl-0 {
|
||||
-moz-column-gap: 0 !important;
|
||||
column-gap: 0 !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-1 {
|
||||
-moz-column-gap: .25rem !important;
|
||||
column-gap: .25rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-2 {
|
||||
-moz-column-gap: .5rem !important;
|
||||
column-gap: .5rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-3 {
|
||||
-moz-column-gap: 1rem !important;
|
||||
column-gap: 1rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-4 {
|
||||
-moz-column-gap: 1.5rem !important;
|
||||
column-gap: 1.5rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-5 {
|
||||
-moz-column-gap: 2rem !important;
|
||||
column-gap: 2rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-6 {
|
||||
-moz-column-gap: 3rem !important;
|
||||
column-gap: 3rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-7 {
|
||||
-moz-column-gap: 5rem !important;
|
||||
column-gap: 5rem !important
|
||||
}
|
||||
|
||||
.column-gap-xxl-8 {
|
||||
-moz-column-gap: 8rem !important;
|
||||
column-gap: 8rem !important
|
||||
}
|
||||
|
||||
|
@ -17754,17 +17685,14 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.columns-xxl-2 {
|
||||
-moz-columns: 2 !important;
|
||||
columns: 2 !important
|
||||
}
|
||||
|
||||
.columns-xxl-3 {
|
||||
-moz-columns: 3 !important;
|
||||
columns: 3 !important
|
||||
}
|
||||
|
||||
.columns-xxl-4 {
|
||||
-moz-columns: 4 !important;
|
||||
columns: 4 !important
|
||||
}
|
||||
}
|
||||
|
@ -20293,11 +20221,11 @@ body[data-bs-theme=dark] .hide-theme-dark {
|
|||
}
|
||||
|
||||
.alert {
|
||||
--tblr-alert-color: var(--tblr-muted);
|
||||
background: #fff;
|
||||
--tblr-alert-color: var(--tblr-secondary);
|
||||
--tblr-alert-bg: var(--tblr-surface);
|
||||
border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);
|
||||
border-left: .25rem var(--tblr-border-style) var(--tblr-alert-color);
|
||||
box-shadow: rgba(24, 36, 51, .04) 0 2px 4px 0
|
||||
border-left: 0.25rem var(--tblr-border-style) var(--tblr-alert-color);
|
||||
box-shadow: rgba(24, 36, 51, 0.04) 0 2px 4px 0;
|
||||
}
|
||||
|
||||
.alert>:last-child {
|
||||
|
|
BIN
public/fonts/Inter-Black.woff2
Normal file
BIN
public/fonts/Inter-BlackItalic.woff2
Normal file
BIN
public/fonts/Inter-Bold.woff2
Normal file
BIN
public/fonts/Inter-BoldItalic.woff2
Normal file
BIN
public/fonts/Inter-ExtraBold.woff2
Normal file
BIN
public/fonts/Inter-ExtraBoldItalic.woff2
Normal file
BIN
public/fonts/Inter-ExtraLight.woff2
Normal file
BIN
public/fonts/Inter-ExtraLightItalic.woff2
Normal file
BIN
public/fonts/Inter-Italic.woff2
Normal file
BIN
public/fonts/Inter-Light.woff2
Normal file
BIN
public/fonts/Inter-LightItalic.woff2
Normal file
BIN
public/fonts/Inter-Medium.woff2
Normal file
BIN
public/fonts/Inter-MediumItalic.woff2
Normal file
BIN
public/fonts/Inter-Regular.woff2
Normal file
BIN
public/fonts/Inter-SemiBold.woff2
Normal file
BIN
public/fonts/Inter-SemiBoldItalic.woff2
Normal file
BIN
public/fonts/Inter-Thin.woff2
Normal file
BIN
public/fonts/Inter-ThinItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Black.woff2
Normal file
BIN
public/fonts/InterDisplay-BlackItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Bold.woff2
Normal file
BIN
public/fonts/InterDisplay-BoldItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraBold.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraBoldItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraLight.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraLightItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Italic.woff2
Normal file
BIN
public/fonts/InterDisplay-Light.woff2
Normal file
BIN
public/fonts/InterDisplay-LightItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Medium.woff2
Normal file
BIN
public/fonts/InterDisplay-MediumItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Regular.woff2
Normal file
BIN
public/fonts/InterDisplay-SemiBold.woff2
Normal file
BIN
public/fonts/InterDisplay-SemiBoldItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Thin.woff2
Normal file
BIN
public/fonts/InterDisplay-ThinItalic.woff2
Normal file
BIN
public/fonts/InterVariable-Italic.woff2
Normal file
BIN
public/fonts/InterVariable.woff2
Normal file
59
public/fonts/inter.css
Normal file
|
@ -0,0 +1,59 @@
|
|||
/* Variable fonts usage:
|
||||
:root { font-family: "Inter", sans-serif; }
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
|
||||
} */
|
||||
@font-face {
|
||||
font-family: InterVariable;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('InterVariable.woff2?v=4.0') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: InterVariable;
|
||||
font-style: italic;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('InterVariable-Italic.woff2?v=4.0') format('woff2');
|
||||
}
|
||||
/* legacy name "Inter var" (Oct 2023) */
|
||||
@font-face { font-family:'Inter var'; font-style:normal; font-weight:100 900; font-display:swap; src: url('InterVariable.woff2?v=4.0') format('woff2'); }
|
||||
@font-face { font-family:'Inter var'; font-style:italic; font-weight:100 900; font-display:swap; src: url('InterVariable-Italic.woff2?v=4.0') format('woff2'); }
|
||||
/* static fonts */
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:100; font-display:swap; src:url("Inter-Thin.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:100; font-display:swap; src:url("Inter-ThinItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:200; font-display:swap; src:url("Inter-ExtraLight.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:200; font-display:swap; src:url("Inter-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:300; font-display:swap; src:url("Inter-Light.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:300; font-display:swap; src:url("Inter-LightItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:400; font-display:swap; src:url("Inter-Regular.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:400; font-display:swap; src:url("Inter-Italic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:500; font-display:swap; src:url("Inter-Medium.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:500; font-display:swap; src:url("Inter-MediumItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:600; font-display:swap; src:url("Inter-SemiBold.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:600; font-display:swap; src:url("Inter-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:700; font-display:swap; src:url("Inter-Bold.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:700; font-display:swap; src:url("Inter-BoldItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:800; font-display:swap; src:url("Inter-ExtraBold.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:800; font-display:swap; src:url("Inter-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:normal; font-weight:900; font-display:swap; src:url("Inter-Black.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:Inter; font-style:italic; font-weight:900; font-display:swap; src:url("Inter-BlackItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:100; font-display:swap; src:url("InterDisplay-Thin.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:100; font-display:swap; src:url("InterDisplay-ThinItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLight.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:300; font-display:swap; src:url("InterDisplay-Light.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:300; font-display:swap; src:url("InterDisplay-LightItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:400; font-display:swap; src:url("InterDisplay-Regular.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:400; font-display:swap; src:url("InterDisplay-Italic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:500; font-display:swap; src:url("InterDisplay-Medium.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:500; font-display:swap; src:url("InterDisplay-MediumItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBold.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:700; font-display:swap; src:url("InterDisplay-Bold.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:700; font-display:swap; src:url("InterDisplay-BoldItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBold.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:normal; font-weight:900; font-display:swap; src:url("InterDisplay-Black.woff2?v=4.0") format("woff2"); }
|
||||
@font-face { font-family:InterDisplay; font-style:italic; font-weight:900; font-display:swap; src:url("InterDisplay-BlackItalic.woff2?v=4.0") format("woff2"); }
|
BIN
public/img/add to zip.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
3
public/img/dweebui.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="180" height="50" viewBox="0 0 180 50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g transform="translate(0 0)">
|
||||
<rect width="180" height="50" rx="2" transform="translate(0 0)" fill="transparent"></rect><text transform="translate(8 43)" fill="rgba(33, 33, 33, 0.7)" font-size="40" font-family="Roboto-Bold,Roboto" ><tspan x="0" y="0">DweebUI</tspan></text>
|
||||
</g></svg>
|
After Width: | Height: | Size: 413 B |
BIN
public/img/logo.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -12,7 +12,7 @@
|
|||
})((function () { 'use strict';
|
||||
|
||||
var themeStorageKey = "tablerTheme";
|
||||
var defaultTheme = "light";
|
||||
var defaultTheme = "dark";
|
||||
var selectedTheme;
|
||||
var params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: function get(searchParams, prop) {
|
||||
|
|
355
public/js/htmx-sse.js
Normal 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
|
@ -1,198 +0,0 @@
|
|||
// SOCKET IO
|
||||
const socket = io({
|
||||
auth: {
|
||||
token: "abc"
|
||||
}
|
||||
});
|
||||
|
||||
// ON CONNECT EVENT
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected');
|
||||
});
|
||||
|
||||
// SELECT METRICS ELEMENTS
|
||||
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');
|
||||
|
||||
const dockerCards = document.getElementById('cards');
|
||||
|
||||
const logViewer = document.getElementById('logView');
|
||||
|
||||
//Update usage bars
|
||||
socket.on('metrics', (data) => {
|
||||
|
||||
let {cpu, ram, tx, rx, disk} = data;
|
||||
|
||||
cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
|
||||
cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
|
||||
|
||||
ramText.innerHTML = `<span>RAM ${ram} %</span>`;
|
||||
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>`;
|
||||
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
|
||||
});
|
||||
|
||||
function drawCharts(name, cpu_array, ram_array) {
|
||||
var elements = document.querySelectorAll(`${name}`);
|
||||
|
||||
Array.from(elements).forEach(function(element) {
|
||||
if (window.ApexCharts) {
|
||||
new ApexCharts(element, {
|
||||
chart: {
|
||||
type: "line",
|
||||
fontFamily: 'inherit',
|
||||
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: cpu_array
|
||||
}, {
|
||||
name: "RAM",
|
||||
data: ram_array
|
||||
}],
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
padding: 0
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
type: 'datetime'
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
padding: 4
|
||||
}
|
||||
},
|
||||
labels: [
|
||||
'2020-06-20', '2020-06-21', '2020-06-22', '2020-06-23', '2020-06-24', '2020-06-25', '2020-06-26', '2020-06-27', '2020-06-28', '2020-06-29', '2020-06-30', '2020-07-01', '2020-07-02', '2020-07-03', '2020-07-04', '2020-07-05', '2020-07-06', '2020-07-07', '2020-07-08', '2020-07-09', '2020-07-10', '2020-07-11', '2020-07-12', '2020-07-13', '2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17', '2020-07-18', '2020-07-19'
|
||||
],
|
||||
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
}).render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// container button actions
|
||||
function buttonAction(button) {
|
||||
socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
|
||||
}
|
||||
|
||||
|
||||
let containerLogs;
|
||||
|
||||
function viewLogs(button) {
|
||||
|
||||
if (button.name != 'refresh') {
|
||||
containerLogs = button.name;
|
||||
}
|
||||
|
||||
|
||||
socket.emit('logs', {container: containerLogs});
|
||||
}
|
||||
|
||||
socket.on('logString', (data) => {
|
||||
logViewer.innerHTML = `<pre>${data}</pre>`;
|
||||
});
|
||||
|
||||
|
||||
|
||||
socket.on('cards', (data) => {
|
||||
|
||||
console.log('cards deleted');
|
||||
let deleteMeElements = document.querySelectorAll('.deleteme');
|
||||
deleteMeElements.forEach((element) => {
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
|
||||
dockerCards.insertAdjacentHTML("afterend", data);
|
||||
|
||||
// check localStorage for items ending with _cpu and redraw the charts
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
if (localStorage.key(i).endsWith('_cpu')) {
|
||||
let name = localStorage.key(i).split('_')[0];
|
||||
let cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
|
||||
let ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
|
||||
drawCharts(`#${name}_chart`, cpu_array, ram_array);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
socket.on('containerStats', (data) => {
|
||||
|
||||
let {name, cpu, ram} = data;
|
||||
|
||||
console.log(`drawing chart for ${name}`)
|
||||
|
||||
var cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
|
||||
var ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
|
||||
|
||||
if (cpu_array == null) { cpu_array = Array(30).fill(0); }
|
||||
if (ram_array == null) { ram_array = Array(30).fill(0); }
|
||||
|
||||
cpu_array.push(cpu);
|
||||
ram_array.push(ram);
|
||||
|
||||
cpu_array = cpu_array.slice(-30);
|
||||
ram_array = ram_array.slice(-30);
|
||||
|
||||
localStorage.setItem(`${name}_cpu`, JSON.stringify(cpu_array));
|
||||
localStorage.setItem(`${name}_ram`, JSON.stringify(ram_array));
|
||||
|
||||
// replace the old chart with the new one
|
||||
let chart = document.getElementById(`${name}_chart`);
|
||||
if (chart) {
|
||||
let newChart = document.createElement('div');
|
||||
newChart.id = `${name}_chart`;
|
||||
chart.parentNode.replaceChild(newChart, chart);
|
||||
drawCharts(`#${name}_chart`, cpu_array, ram_array);
|
||||
} else {
|
||||
console.log(`Chart element with id ${name}_chart not found in the DOM`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('install', (data) => {
|
||||
|
||||
console.log('added install card');
|
||||
dockerCards.insertAdjacentHTML("afterend", data);
|
||||
});
|
2020
public/libs/list.js/dist/list.js
vendored
Normal file
1
public/libs/list.js/dist/list.js.map
vendored
Normal file
2
public/libs/list.js/dist/list.min.js
vendored
Normal file
1
public/libs/list.js/dist/list.min.js.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 68">
|
||||
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#ffffff"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 599 B |
Before Width: | Height: | Size: 5.5 KiB |
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232 68">
|
||||
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#fff"/>
|
||||
<path d="M105.8 46.1c.4 0 .9.2 1.2.6s.6 1 .6 1.7c0 .9-.5 1.6-1.4 2.2s-2 .9-3.2.9c-2 0-3.7-.4-5-1.3s-2-2.6-2-5.4V31.6h-2.2c-.8 0-1.4-.3-1.9-.8s-.9-1.1-.9-1.9c0-.7.3-1.4.8-1.8s1.2-.7 1.9-.7h2.2v-3.1c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v3.1h3.4c.8 0 1.4.3 1.9.8s.8 1.2.8 1.9-.3 1.4-.8 1.8-1.2.7-1.9.7h-3.4v13c0 .7.2 1.2.5 1.5s.8.5 1.4.5c.3 0 .6-.1 1.1-.2.5-.2.8-.3 1.2-.3zm28-20.7c.8 0 1.5.3 2.1.8.5.5.8 1.2.8 2.1v20.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2-.8-.8-1.2-.8-2.1c-.8.9-1.9 1.7-3.2 2.4-1.3.7-2.8 1-4.3 1-2.2 0-4.2-.6-6-1.7-1.8-1.1-3.2-2.7-4.2-4.7s-1.6-4.3-1.6-6.9c0-2.6.5-4.9 1.5-6.9s2.4-3.6 4.2-4.8c1.8-1.1 3.7-1.7 5.9-1.7 1.5 0 3 .3 4.3.8 1.3.6 2.5 1.3 3.4 2.1 0-.8.3-1.5.8-2.1.5-.5 1.2-.7 2-.7zm-9.7 21.3c2.1 0 3.8-.8 5.1-2.3s2-3.4 2-5.7-.7-4.2-2-5.8c-1.3-1.5-3-2.3-5.1-2.3-2 0-3.7.8-5 2.3-1.3 1.5-2 3.5-2 5.8s.6 4.2 1.9 5.7 3 2.3 5.1 2.3zm32.1-21.3c2.2 0 4.2.6 6 1.7 1.8 1.1 3.2 2.7 4.2 4.7s1.6 4.3 1.6 6.9-.5 4.9-1.5 6.9-2.4 3.6-4.2 4.8c-1.8 1.1-3.7 1.7-5.9 1.7-1.5 0-3-.3-4.3-.9s-2.5-1.4-3.4-2.3v.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.5-.8-1.2-.8-2.1V18.9c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v10c.8-1 1.8-1.8 3.2-2.5 1.3-.7 2.8-1 4.3-1zm-.7 21.3c2 0 3.7-.8 5-2.3s2-3.5 2-5.8-.6-4.2-1.9-5.7-3-2.3-5.1-2.3-3.8.8-5.1 2.3-2 3.4-2 5.7.7 4.2 2 5.8c1.3 1.6 3 2.3 5.1 2.3zm23.6 1.9c0 .8-.3 1.5-.8 2.1s-1.3.8-2.1.8-1.5-.3-2-.8-.8-1.3-.8-2.1V18.9c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v29.7zm29.3-10.5c0 .8-.3 1.4-.9 1.9-.6.5-1.2.7-2 .7h-15.8c.4 1.9 1.3 3.4 2.6 4.4 1.4 1.1 2.9 1.6 4.7 1.6 1.3 0 2.3-.1 3.1-.4.7-.2 1.3-.5 1.8-.8.4-.3.7-.5.9-.6.6-.3 1.1-.4 1.6-.4.7 0 1.2.2 1.7.7s.7 1 .7 1.7c0 .9-.4 1.6-1.3 2.4-.9.7-2.1 1.4-3.6 1.9s-3 .8-4.6.8c-2.7 0-5-.6-7-1.7s-3.5-2.7-4.6-4.6-1.6-4.2-1.6-6.6c0-2.8.6-5.2 1.7-7.2s2.7-3.7 4.6-4.8 3.9-1.7 6-1.7 4.1.6 6 1.7 3.4 2.7 4.5 4.7c.9 1.9 1.5 4.1 1.5 6.3zm-12.2-7.5c-3.7 0-5.9 1.7-6.6 5.2h12.6v-.3c-.1-1.3-.8-2.5-2-3.5s-2.5-1.4-4-1.4zm30.3-5.2c1 0 1.8.3 2.4.8.7.5 1 1.2 1 1.9 0 1-.3 1.7-.8 2.2-.5.5-1.1.8-1.8.7-.5 0-1-.1-1.6-.3-.2-.1-.4-.1-.6-.2-.4-.1-.7-.1-1.1-.1-.8 0-1.6.3-2.4.8s-1.4 1.3-1.9 2.3-.7 2.3-.7 3.7v11.4c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.6-.8-1.3-.8-2.1V28.8c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v.6c.7-1.3 1.8-2.3 3.2-3 1.3-.7 2.8-1 4.3-1z" fill-rule="evenodd" clip-rule="evenodd" fill="#fff"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB |