Compare commits

...

115 commits
v0.20 ... main

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 19:56:57 +00:00
lllllllillllllillll
d905a95764
Update README.md 2024-06-16 12:45:41 -07:00
lllllllillllllillll
a67f65996e
Update compose.yaml
changed volume to /app/config
2024-06-15 02:27:08 -07:00
lllllllillllllillll
4b5ae32a97
Update README.md
fix compose volume
2024-06-15 02:23:57 -07:00
lllllllillllllillll
ee9870f554
Update README.md 2024-06-09 02:44:49 -07:00
lllllllillllllillll
8f97e17765
Merge pull request #74 from lllllllillllllillll/dev
## v0.60 (June 9th 2024) - Permissions system and import templates
2024-06-09 01:28:36 -07:00
lllllllillllllillll
b4d472f414 v0.60 release 2024-06-09 01:24:27 -07:00
lllllllillllllillll
109d9bc171 fixed details modal 2024-06-08 16:53:28 -07:00
lllllllillllllillll
842f83ea91 reduced by one line 2024-06-04 23:45:17 -07:00
lllllllillllllillll
0dae02c382 fixed details modal and dashboard icons 2024-06-04 23:32:06 -07:00
lllllllillllllillll
3080516d93 networks display 'in use' 2024-06-01 01:56:13 -07:00
lllllllillllllillll
928855d2f7 working 'new volume' button 2024-05-30 23:21:10 -07:00
lllllllillllllillll
29d630f1dd functioning 'new volume' modal 2024-05-30 00:58:19 -07:00
lllllllillllllillll
a8bf38eedb Volumes page displays bind type and status(in use) 2024-05-29 00:22:17 -07:00
lllllllillllllillll
7cf8b84169 buttons trigger from 'mousedown'
John Carmack and Theo told me to:
https://www.youtube.com/watch?v=yaMGtiPckAQ
2024-05-23 17:25:54 -07:00
lllllllillllllillll
83180e0a62 Images display 'In use' or 'Unused' 2024-05-22 01:54:52 -07:00
lllllllillllllillll
869b7b30e7 added image pull to the images page 2024-05-21 19:54:05 -07:00
lllllllillllllillll
d0db603efe updated tar to 6.2.1 2024-05-21 00:34:24 -07:00
lllllllillllllillll
774d5f4f62 added titles to activity indicators 2024-05-21 00:24:30 -07:00
lllllllillllllillll
03be1187ef empty folder fix 2024-05-20 19:10:20 -07:00
lllllllillllllillll
57f080ec0d empty folders fix 2024-05-20 12:08:37 -07:00
lllllllillllllillll
04dbeefeb9 Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2024-05-20 00:44:20 -07:00
lllllllillllllillll
d78cd645af removed files from git 2024-05-20 00:44:00 -07:00
lllllllillllllillll
e1367b58f1
Delete templates/compose/so_github_makes_folder.txt 2024-05-20 00:37:32 -07:00
lllllllillllllillll
37fd6f320d so_github_makes_folder 2024-05-20 00:35:55 -07:00
lllllllillllllillll
6aa325ed8e v0.60 pre-release 2024-05-19 15:55:29 -07:00
lllllllillllllillll
5081100f71 added option to remove template 2024-05-19 02:31:21 -07:00
lllllllillllllillll
cec389702c fixed install 2024-05-18 13:13:40 -07:00
lllllllillllllillll
82273d1fc1 Fixed app search and search by category 2024-05-17 19:45:38 -07:00
lllllllillllllillll
fbe26cd0ed Fixed apps.js pagnation and learn more modal 2024-05-16 18:36:16 -07:00
lllllllillllllillll
1558d61dcd apps.js - dropdown displays imported json 2024-05-16 01:44:26 -07:00
lllllllillllllillll
2d08e9fa3b added alerts for duplicate templates or apps 2024-05-15 18:47:58 -07:00
lllllllillllllillll
eda852b89e added compose install 2024-05-13 21:34:42 -07:00
lllllllillllllillll
c795cac009 created compose modal. updated import instructions 2024-05-13 00:18:39 -07:00
lllllllillllllillll
c71f330b49 added compose file import 2024-05-12 01:30:37 -07:00
lllllllillllllillll
319aab60f5 added ability to view custom json templates 2024-05-06 00:54:19 -07:00
lllllllillllllillll
5581710e75 improved app search 2024-05-05 15:34:01 -07:00
lllllllillllllillll
82c134158d updated dependencies. improved AddAlert function. 2024-05-04 15:47:24 -07:00
lllllllillllllillll
2d9914c458 Added Install Alerts 2024-05-04 01:18:51 -07:00
lllllllillllllillll
cf41f07bbd Multi-user dashboard 2024-05-03 01:13:24 -07:00
lllllllillllllillll
15722b1687 First draft of permissions system 2024-04-22 19:20:13 -07:00
lllllllillllllillll
62b7e73aac Improved UI for apps.js and new template import 2024-04-14 14:56:01 -07:00
lllllllillllllillll
64ec287286 updates to portal and router 2024-04-07 15:57:17 -07:00
lllllllillllllillll
74cf69b3d3 updated router. email no longer case sensitive. 2024-04-01 15:08:02 -07:00
lllllllillllllillll
c9d7dea132 updated dependencies and updated router auth 2024-03-31 14:37:00 -07:00
lllllllillllllillll
42ca573b51 fix README 2024-03-26 23:15:51 -07:00
lllllllillllllillll
9c41839852 added auth routes 2024-03-26 23:14:39 -07:00
lllllllillllllillll
a73a89b250 mostly working permission system. 2024-03-26 20:38:57 -07:00
lllllllillllllillll
365cdde0cc Updates to permissions modal 2024-03-24 14:09:53 -07:00
lllllllillllllillll
167dd8917e reduced html inside dashboard.js 2024-03-22 11:14:10 -07:00
lllllllillllllillll
8d9eb9981a improvements for multi-user and permissions 2024-03-21 23:59:45 -07:00
lllllllillllllillll
66f273e22e updated README and minor fixes 2024-03-20 13:19:56 -07:00
lllllllillllllillll
6a352281ab import modal. added UI elements to apps.js 2024-03-19 15:20:26 -07:00
lllllllillllllillll
5c6e2a9eaa Working install cards and fixes for dashboard.js 2024-03-18 17:14:47 -07:00
lllllllillllllillll
8b8e30772f hide/reset view fixes. updated express to 4.18.3 2024-03-18 01:08:44 -07:00
lllllllillllllillll
7c5670e92b Fixed app search and install 2024-03-17 19:32:02 -07:00
lllllllillllllillll
c2f06639f5 refactored container actions 2024-03-17 12:10:16 -07:00
lllllllillllllillll
f04f08d44d Fixed install modal and install function 2024-03-17 02:20:32 -07:00
lllllllillllllillll
974d32e350 updating apps.js to use html templates 2024-03-17 01:00:11 -07:00
lllllllillllllillll
b395de3445 removed/refactored metrics interval 2024-03-15 01:01:15 -07:00
lllllllillllllillll
e78afb90ca refactoring dashboard.js / updated compose file 2024-03-14 21:12:09 -07:00
lllllllillllllillll
c9da3bd30b Fixed appCard catagories. refactoring dashboard.js 2024-03-14 00:33:34 -07:00
lllllllillllllillll
bb84828ffe fixed users.js avatars. app.js modal 2024-03-13 19:55:13 -07:00
lllllllillllllillll
12e75af9b0 dynamically generated avatars. 2024-03-13 16:32:14 -07:00
lllllllillllllillll
a841fed064 moved footer, navbar, sidebar to partials folder 2024-03-13 15:41:49 -07:00
lllllllillllllillll
308538f579 fixes for appCard and apps.js 2024-03-13 02:14:50 -07:00
lllllllillllllillll
f615a492e8 updated container charts 2024-03-13 00:41:26 -07:00
lllllllillllllillll
6213c54165 fixed container charts not displaying 2024-03-12 20:09:38 -07:00
lllllllillllllillll
ff78e24913 I think I fixed it.... 2024-03-12 17:31:04 -07:00
lllllllillllllillll
705779ec29 fix un-done changes 2024-03-12 16:44:20 -07:00
lllllllillllllillll
c9c270fd81 Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2024-03-12 16:41:51 -07:00
lllllllillllllillll
eb952c0a50 independently updating container cards 2024-03-12 16:23:37 -07:00
lllllllillllllillll
7bf1739c52 fixed /sse_event with htmx 2.0 change 2024-03-01 01:51:06 -08:00
lllllllillllllillll
17a479be21 updated htmx to 2.0 alpha-2 2024-02-29 23:52:50 -08:00
lllllllillllllillll
7e3617f967 made sse_event look less stupid 2024-02-29 23:42:39 -08:00
lllllllillllllillll
785b54d5aa sse event can now trigger individual cards 2024-02-29 23:14:08 -08:00
lllllllillllllillll
2dc22fd75a removed files 2024-02-28 23:47:06 -08:00
lllllllillllllillll
9a994bfbf1 converting template literals into plain html. 2024-02-28 23:15:29 -08:00
lllllllillllllillll
1d7b56907c converting /components into html 2024-02-27 23:34:26 -08:00
lllllllillllllillll
cfe9660ac2 Dashboard cards are now html components. 2024-02-27 15:41:57 -08:00
lllllllillllllillll
c7d79b296c
Merge pull request #60 from lllllllillllllillll/dev
v0.40
2024-02-26 15:59:24 -08:00
lllllllillllllillll
0596793c89 date of release 2024-02-26 15:48:16 -08:00
lllllllillllllillll
575a689406 v0.40 screenshots 2024-02-26 15:39:56 -08:00
lllllllillllllillll
d288cdb205 updated image to v0.40 2024-02-26 15:30:38 -08:00
lllllllillllllillll
f97628e9cd Bumped version to v0.40. Added Podman support. 2024-02-25 00:19:31 -08:00
lllllllillllllillll
32c2301873 Container cards now display status while waiting 2024-02-24 09:20:35 -08:00
lllllllillllllillll
e294ca7089 Server metrics styling. Container action indicator 2024-02-24 08:55:47 -08:00
lllllllillllllillll
ea9ead5709 updated dependencies: systeminformation. sequelize 2024-02-23 00:46:04 -08:00
lllllllillllllillll
eb992f706e reduced server.js to 99 lines. bug fixes. 2024-02-23 00:41:54 -08:00
lllllllillllllillll
b62e209e6f Fixed issue with dashboard and improved event loop 2024-02-21 17:05:43 -08:00
lllllllillllllillll
25280ae174 Improved dashboard controller and router.
removed console.log()s.
2024-02-18 18:23:20 -08:00
lllllllillllllillll
c27f64f308 Fixed hide and resetView 2024-02-18 00:33:09 -08:00
lllllllillllllillll
a95b042960 New logo. Updated dependencies. 2024-02-17 13:32:09 -08:00
lllllllillllllillll
666f820a1f Fixed list sort on images, networks, volumes 2024-02-17 00:37:07 -08:00
lllllllillllllillll
97481b0b75 moved routes into dashboard.js 2024-02-14 01:45:29 -08:00
lllllllillllllillll
04cc1c1df3 Tweaks to event trigger. Improved Uninstall. 2024-02-11 16:50:21 -08:00
lllllllillllllillll
003db6d7d1 fixed image,volume,networks form submit.
added selectAll to permissions modal.
2024-02-09 20:06:33 -08:00
lllllllillllllillll
95dcedbdc1 fix volumes controller. update templates.json 2024-02-08 16:06:06 -08:00
lllllllillllllillll
24941d5f32 updated permissions modal 2024-02-08 03:27:15 -08:00
lllllllillllllillll
71bbb574d1 checkbox selectAll() fix 2024-02-07 23:57:49 -08:00
lllllllillllllillll
13ee350bb2 localized htmx. fixed images/volumes/networks
added hidden checkbox so forms always return an array
2024-02-07 18:13:44 -08:00
lllllllillllllillll
377ba6ae67 working 'remove' on images and networks pages 2024-02-07 01:42:20 -08:00
lllllllillllllillll
e786b32161 updated dependencies 2024-02-06 00:54:34 -08:00
lllllllillllllillll
1938d7b2fc updated images, networks, volumes forms 2024-02-06 00:35:16 -08:00
lllllllillllllillll
70ec201924 new permissions modal 2024-02-05 23:51:36 -08:00
lllllllillllllillll
883a65faae Added "Thanks" clicker to supporters page 2024-02-04 01:19:16 -08:00
lllllllillllllillll
8feb88a2a0 Fixed container stat charts 2024-02-03 22:00:17 -08:00
lllllllillllllillll
f94bd91898 modal and graph fixes. new supporters page. 2024-02-03 15:09:20 -08:00
lllllllillllllillll
f058360b19 htmx log view 2024-01-28 23:25:05 -08:00
lllllllillllllillll
5e45e084d0 fixed container charts 2024-01-28 21:03:20 -08:00
lllllllillllllillll
0f5575075e Another big rewrite 2024-01-28 00:33:50 -08:00
90 changed files with 7057 additions and 7513 deletions

8
.dockerignore Normal file
View file

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

View file

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

11
.gitignore vendored
View file

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

View file

@ -1,3 +1,55 @@
## 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.

View file

@ -1,19 +1,7 @@
FROM node:21-alpine
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", "server.js"]
CMD node server.js

View file

@ -1,47 +1,39 @@
# DweebUI
DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
Alpha v0.20 ( :fire: Experimental :fire: )
[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
[![GitHub Stars](https://img.shields.io/github/stars/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![GitHub Activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![Docker Pulls](https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui)](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
[![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
[![GitHub License](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
* This is a personal project I started to get more familiar with Javascript and Node.js.
* Some UI elements are placeholders and every version may have breaking changes.
* Please post issues and discussions so I know what bugs and features to focus on.
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard2.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard2.png" width="25%"/></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="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/images.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/images.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/register.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/register.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/login.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/login.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/syslogs.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/syslogs.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/volumes.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/volumes.png" width="25%"/></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, container metrics, and container controls, on a single page.
* [x] View container logs.
* [ ] Update containers (planned).
* [ ] Manage your Docker networks, images, and volumes (in development).
* [x] Light/Dark Mode.
* [x] Easy to install app templates.
* [x] Multi-User built-in.
* [ ] Permissions system (in development).
* [x] Support for Windows, Linux, and MacOS.
* [ ] Docker compose import (in development).
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [x] Automatically persists data in docker volumes if bind mount isn't used.
* [ ] Preset variables (planned).
* [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] Docker Compose Support.
* [ ] Update containers (planned).
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [ ] Preset variables (planned).
* [ ] Themes (planned).
## 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
@ -51,9 +43,8 @@ version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.20
image: lllllllillllllillll/dweebui
environment:
NODE_ENV: production
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
@ -61,7 +52,11 @@ services:
- 8000:8000
volumes:
- dweebui:/app
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket
#- /run/podman/podman.sock:/var/run/docker.sock
networks:
- dweebui_net
@ -73,13 +68,18 @@ networks:
driver: bridge
```
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
Compose setup:
* Paste the above content into a file named ```docker-compose.yml``` then place it in a folder named ```dweebui```.
* Open a terminal in the ```dweebui``` folder, then enter ```docker compose up -d```.
* You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
Configuration:
* `PORT` - Specifies which port the service binds to on startup. Default is `8000`.
* `SECRET` - A shared secret used by the registration page.
## Credits
@ -91,4 +91,5 @@ Compose setup:
## Supporters
* MM (Patreon)
* MM (Patreon)
* PD (Buymeacoffee)

View file

@ -1,992 +0,0 @@
export const appCard = (data) => {
// dont look at anything in here.
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 || "";
let net_host, net_bridge, net_docker = '';
let net_name = 'AppBridge';
// if data.network is set to host, bridge, or docker set the radio button to checked
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"/></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">
<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">
<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>`;
}

View file

@ -1,285 +0,0 @@
// export for app.js
export const containerCard = (data) => {
let { name, service, state, external_port, internal_port, ports, link } = data;
let wrapped = name;
let chart = name;
if (name.length > 13) {
wrapped = name.slice(0, 10) + '...';
}
//disable controls for a docker container depending on its name
let actions = "";
if (name.startsWith('dweebui')) {
actions = 'disabled=""';
}
if ( external_port == undefined ) { external_port = 0; }
if ( internal_port == undefined ) { internal_port = 0; }
let state_indicator = 'green';
if (state == 'exited') {
state = 'stopped';
state_indicator = 'red';
} else if (state == 'paused') {
state_indicator = 'orange';
}
let ports_data = [];
if (ports) {
ports_data = ports;
} else {
for (let i = 0; i < 12; i++) {
let port_check = "checked";
let external = i;
let internal = i;
let protocol = "tcp";
ports_data.push({
check: port_check,
external: external,
internal: internal,
protocol: protocol
});
}
}
return `
<div class="col-sm-6 col-lg-3 deleteme">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service}.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<div class="subheader text-yellow">${external_port}:${internal_port}</div>
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button onclick="clicked(this)" name="${name}" value="${state}" id="start" class="btn-action" title="Start" ${actions}><!-- player-play -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="stop" class="btn-action" title="Stop" ${actions}><!-- player-stop -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="pause" class="btn-action" title="Pause" ${actions}><!-- player-pause -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="restart" class="btn-action" title="Restart" ${actions}><!-- reload -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
</button>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" onclick="clicked(this)" id="details" data-bs-toggle="modal" data-bs-target="#details_modal" href="#">Details</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="logs" data-bs-toggle="modal" data-bs-target="#log_view" href="#">Logs</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="edit" href="#">Edit</button>
<button class="dropdown-item text-primary" onclick="clicked(this)" name="${name}" id="update" href="#">Update</button>
<button class="dropdown-item text-danger" onclick="clicked(this)" name="${name}" id="remove" data-bs-toggle="modal" data-bs-target="#${name}_uninstall_modal" href="#">Remove</button>
</div>
</div>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="resetView" value="resetView">Reset View</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="permissions" value="permissions" data-bs-toggle="modal" data-bs-target="#${name}_permissions">Permissions</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="${name}" style="margin-bottom: 0;">
<a href="http://${link}:${external_port}" target="_blank">
${wrapped}
</a>
</div>
<div class="ms-auto">
<span class="text-${state_indicator} align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
${state}
</span>
</div>
</div>
<div id="${chart}_chart" class="chart-sm"></div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="${name}_uninstall_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-danger"></div>
<div class="modal-body text-center py-3">
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
<h3>Remove ${name}?</h3>
<form action="/uninstall" id="${name}_uninstall" method="POST">
<input type="text" class="form-control" name="service_name" value="${name}" hidden/>
<div class="mb-3"> </div>
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">Remove Volumes</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Image
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_image" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Backups
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
</label>
</div>
</div>
</div>
</div>
<div class="mt-1"> </div>
<div class="text-muted">Enter "Yes" below to remove the container.</div>
<input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
</form>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" form="${name}_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="${name}_permissions" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-cyan"></div>
<div class="modal-body text-center py-3">
<h3>${name} permissions</h3>
<form action="#" id="${name}_permissions" method="POST">
<input type="text" class="form-control" name="service_name" value="${name}" hidden/>
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">Install</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_volumes">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Uninstall
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_image">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Edit
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Upgrade
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups">
</label>
</div>
</div>
</div>
</div>
<div class="mt-1"> </div>
</form>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" form="${name}_permissions" class="btn btn-primary w-100" value="Update"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>`;
}

View file

@ -1,24 +1,27 @@
version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.20
environment:
NODE_ENV: production
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
volumes:
- dweebui:/app
- /var/run/docker.sock:/var/run/docker.sock
networks:
- dweebui_net
volumes:
dweebui:
networks:
dweebui_net:
driver: bridge
version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.60
environment:
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
volumes:
- 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:
networks:
dweebui_net:
driver: bridge

View file

@ -12,7 +12,8 @@ export const Account = async (req, res) => {
id: user.id,
email: user.email,
role: user.role,
avatar: user.avatar,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});

View file

@ -1,98 +1,587 @@
import { readFileSync } from 'fs';
import { appCard } from '../components/appCard.js';
import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
import { parse } from 'yaml';
import multer from 'multer';
import AdmZip from 'adm-zip';
let templatesJSON = readFileSync('./templates.json');
let templates = JSON.parse(templatesJSON).templates;
const upload = multer({storage: multer.diskStorage({
destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
filename: function (req, file, cb) { cb(null, file.originalname) },
})});
templates = templates.sort((a, b) => {
if (a.name < b.name) {
return -1;
let alert = '';
let templates_global = '';
let json_templates = '';
let remove_button = '';
export const Apps = async (req, res) => {
let page = Number(req.params.page) || 1;
let template_param = req.params.template || 'default';
if ((template_param != 'default') && (template_param != 'compose')) {
remove_button = `<a href="/remove_template/${template_param}" class="btn" hx-confirm="Are you sure you want to remove this template?">Remove</a>`;
} else {
remove_button = '';
}
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;
}
});
}
export const Apps = (req, res) => {
let apps_list = '';
let app_count = '';
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = '';
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
let prev = '/apps/' + (page - 1) + '/' + template_param;
let next = '/apps/' + (page + 1) + '/' + template_param;
if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
if (template_param == 'compose') {
let compose_files = readdirSync('templates/compose/');
let page = Number(req.params.page) || 1;
let list_start = (page-1)*28;
let list_end = (page*28);
let last_page = Math.ceil(templates.length/28);
app_count = compose_files.length;
last_page = Math.ceil(compose_files.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);
}
compose_files.forEach(file => {
if (file == '.gitignore') { return; }
let apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let app_card = appCard(templates[i]);
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;
apps_list += app_card;
}
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
appCard = appCard.replace(/AppName/g, service_name);
appCard = appCard.replace(/AppShortName/g, service_name);
appCard = appCard.replace(/AppDesc/g, 'Compose File');
appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
appCard = appCard.replace(/AppType/g, 'compose');
apps_list += appCard;
});
} else {
let template_file = readFileSync(`./templates/json/${template_param}.json`);
let templates = JSON.parse(template_file).templates;
templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
app_count = templates.length;
templates_global = templates;
apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
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) => {
let search = req.body.search.split(' ');
let apps_list = '';
let results = [];
let search = req.body.search;
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 page = Number(req.params.page) || 1;
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 template_param = req.params.template || 'default';
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))) {
results.push(templates[i]);
}
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);
}
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';
switch (result.network) {
case 'host':
net_host = 'checked';
break;
case 'bridge':
net_bridge = 'checked';
net_name = result.network;
break;
default:
net_docker = 'checked';
}
if (repository != "") {
image = (`${repository.url}/raw/master/${repository.stackfile}`);
}
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
for (let i = 0; i < 12; i++) {
// Get port details
try {
let ports = result.ports[i];
let port_check = ports ? "checked" : "";
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
// remove /tcp or /udp from port_external if it exists
if (port_external.includes("/")) {
port_external = port_external.split("/")[0];
}
ports_data.push({
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_protocol
});
} catch {
ports_data.push({
check: "",
external: "",
internal: "",
protocol: ""
});
}
// Get volume details
try {
let volumes = result.volumes[i];
let volume_check = volumes ? "checked" : "";
let volume_bind = volumes.bind ? volumes.bind : "";
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw";
if (volumes.readonly == true) {
volume_readwrite = "ro";
}
volumes_data.push({
check: volume_check,
bind: volume_bind,
container: volume_container,
readwrite: volume_readwrite
});
} catch {
volumes_data.push({
check: "",
bind: "",
container: "",
readwrite: ""
});
}
// Get environment details
try {
let env = result.env[i];
let env_check = "";
let env_default = env.default ? env.default : "";
if (env.set) { env_default = env.set;}
let env_description = env.description ? env.description : "";
let env_label = env.label ? env.label : "";
let env_name = env.name ? env.name : "";
env_data.push({
check: env_check,
default: env_default,
description: env_description,
label: env_label,
name: env_name
});
} catch {
env_data.push({
check: "",
default: "",
description: "",
label: "",
name: ""
});
}
// Get label details
try {
let label = result.labels[i];
let label_check = "";
let label_name = label.name ? label.name : "";
let label_value = label.value ? label.value : "";
label_data.push({
check: label_check,
name: label_name,
value: label_value
});
} catch {
label_data.push({
check: "",
name: "",
value: ""
});
}
for (let i = 0; i < results.length; i++) {
let app_card = appCard(results[i]);
apps_list += app_card;
}
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
let modal = readFileSync('./views/modals/json.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppNote/g, note);
modal = modal.replace(/AppImage/g, image);
modal = modal.replace(/RestartPolicy/g, restart_policy);
modal = modal.replace(/NetHost/g, net_host);
modal = modal.replace(/NetBridge/g, net_bridge);
modal = modal.replace(/NetDocker/g, net_docker);
modal = modal.replace(/NetName/g, net_name);
modal = modal.replace(/ModalName/g, modal_name);
modal = modal.replace(/FormId/g, form_id);
modal = modal.replace(/CommandCheck/g, command_check);
modal = modal.replace(/CommandValue/g, command);
modal = modal.replace(/PrivilegedCheck/g, privileged_check);
}
for (let i = 0; i < 12; i++) {
modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
modal = modal.replaceAll(`Volume${i}Check`, volumes_data[i].check);
modal = modal.replaceAll(`Volume${i}Bind`, volumes_data[i].bind);
modal = modal.replaceAll(`Volume${i}Container`, volumes_data[i].container);
modal = modal.replaceAll(`Volume${i}RW`, volumes_data[i].readwrite);
modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
modal = modal.replaceAll(`Env${i}Default`, env_data[i].default);
modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
modal = modal.replaceAll(`Env${i}Name`, env_data[i].name);
modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
modal = modal.replaceAll(`Label${i}Name`, label_data[i].name);
modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
}
res.send(modal);
}
}
export const LearnMore = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
if (id == 'compose') {
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppDesc/g, 'Compose File');
res.send(modal);
return;
}
let result = templates_global.find(t => t.name == name);
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
modal = modal.replace(/AppName/g, result.title);
modal = modal.replace(/AppDesc/g, result.description);
res.send(modal);
}
export const ImportModal = async (req, res) => {
let modal = readFileSync('./views/modals/import.html', 'utf8');
res.send(modal);
}
export const Upload = (req, res) => {
upload.array('files', 10)(req, res, () => {
alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
<div>Template(s) Uploaded!</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
</div>`;
let exists_alert = `<div class="alert alert-danger alert-dismissible mb-0 py-2" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
<div>Template already exists</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
</div>`;
let files = readdirSync('templates/tmp/');
for (let i = 0; i < files.length; i++) {
if (files[i].endsWith('.zip')) {
let zip = new AdmZip(`templates/tmp/${files[i]}`);
zip.extractAllTo('templates/compose', true);
unlinkSync(`templates/tmp/${files[i]}`);
} else if (files[i].endsWith('.json')) {
if (existsSync(`templates/json/${files[i]}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
} else if (files[i].endsWith('.yml')) {
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services);
if (existsSync(`templates/compose/${service_name}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
mkdirSync(`templates/compose/${service_name}`);
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
} else if (files[i].endsWith('.yaml')) {
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services);
if (existsSync(`templates/compose/${service_name}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
mkdirSync(`templates/compose/${service_name}`);
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
} else {
// unsupported file type
unlinkSync(`templates/tmp/${files[i]}`);
}
}
res.redirect('/apps');
});
};

View file

@ -1,23 +1,448 @@
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) => {
let name = req.session.user;
let role = req.session.role;
alert = req.session.alert;
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
name: name,
avatar: name.charAt(0).toUpperCase(),
role: role,
alert: alert,
});
}
export const searchDashboard = (req, res) => {
// 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 = '';
console.log(req.params);
switch (action) {
case 'permissions':
let title = name.charAt(0).toUpperCase() + name.slice(1);
let permissions_list = '';
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
let users = await User.findAll({ attributes: ['username', 'UUID']});
for (let i = 0; i < users.length; i++) {
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
permissions_list += user_permissions;
}
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
res.send(permissions_modal);
return;
case 'uninstall':
modal = readFileSync('./views/modals/uninstall.html', 'utf8');
modal = modal.replace(/AppName/g, name);
res.send(modal);
return;
case 'details':
modal = readFileSync('./views/modals/details.html', 'utf8');
let details = await containerInfo(name);
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
modal = modal.replace(/AppName/g, details.name);
modal = modal.replace(/AppImage/g, details.image);
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
}
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
}
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
}
// console.log(details.env);
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
}
res.send(modal);
return;
case 'updates':
res.send(newCards);
newCards = '';
return;
case 'card':
await userCards(req.session);
if (!req.session.container_list.find(c => c.container === name)) {
res.send('');
return;
} else {
let details = await containerInfo(name);
let card = await createCard(details);
res.send(card);
return;
}
case 'logs':
let logString = '';
let options = { follow: true, stdout: true, stderr: false, timestamps: false };
docker.getContainer(name).logs(options, function (err, stream) {
if (err) { console.log(err); return; }
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
res.send(`<pre>${logString}</pre>`);
});
});
return;
case 'hide':
let user = req.session.user;
let exists = await Permission.findOne({ where: {containerName: name, user: user}});
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: user, hide: true, userID: req.session.UUID}); }
else { exists.update({ hide: true }); }
hidden = await Permission.findAll({ where: {user: user, hide: true}}, { attributes: ['containerName'] });
hidden = hidden.map((container) => container.containerName);
res.send("ok");
return;
case 'reset':
await Permission.update({ hide: false }, { where: { user: req.session.user } });
res.send("ok");
return;
case 'alert':
req.session.alert = '';
res.send('');
return;
}
function status (state) {
return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
${state}
</span>`);
}
// Container actions
if ((action == 'start') && (value == 'stopped')) {
docker.getContainer(name).start();
res.send(status('starting'));
} else if ((action == 'start') && (value == 'paused')) {
docker.getContainer(name).unpause();
res.send(status('starting'));
} else if ((action == 'stop') && (value != 'stopped')) {
docker.getContainer(name).stop();
res.send(status('stopping'));
} else if ((action == 'pause') && (value == 'paused')) {
docker.getContainer(name).unpause();
res.send(status('starting'));
} else if ((action == 'pause') && (value == 'running')) {
docker.getContainer(name).pause();
res.send(status('pausing'));
} else if (action == 'restart') {
docker.getContainer(name).restart();
res.send(status('restarting'));
}
}
async function containerInfo (containerName) {
// get the container info
let container = docker.getContainer(containerName);
let info = await container.inspect();
let image = info.Config.Image;
// grab the service name from the end of the image name
let service = image.split('/').pop();
// remove the tag from the service name if it exists
try { service = service.split(':')[0]; } catch {}
let ports_list = [];
let external = 0;
let internal = 0;
try {
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
try {
external = ports_list[0].external;
internal = ports_list[0].internal;
} catch {}
// console.log(ports_list);
// console.log(info.HostConfig.PortBindings);
// console.log(info.HostConfig.Binds);
// console.log(info.Config.Env);
// console.log(info.Config.Labels);
let details = {
name: containerName,
image: image,
service: service,
state: info.State.Status,
external_port: external,
internal_port: internal,
ports: ports_list,
volumes: info.Mounts,
env: info.Config.Env,
labels: info.Config.Labels,
link: 'localhost',
}
return details;
}
async function createCard (details) {
let shortname = details.name.slice(0, 10) + '...';
let trigger = 'data-hx-trigger="load, every 3s"';
let state = details.state;
let card = readFileSync('./views/partials/containerFull.html', 'utf8');
let state_color = '';
switch (state) {
case 'running':
state_color = 'green';
break;
case 'exited':
state = 'stopped';
state_color = 'red';
trigger = 'data-hx-trigger="load"';
break;
case 'paused':
state_color = 'orange';
trigger = 'data-hx-trigger="load"';
break;
case 'installing':
state_color = 'blue';
trigger = 'data-hx-trigger="load"';
break;
}
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
card = card.replace(/AppName/g, details.name);
card = card.replace(/AppShortName/g, shortname);
card = card.replace(/AppIcon/g, details.service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ExternalPort/g, details.external_port);
card = card.replace(/InternalPort/g, details.internal_port);
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
card = card.replace(/AppNameState/g, `${details.name}State`);
card = card.replace(/data-trigger=""/, trigger);
return card;
}
async function userCards (session) {
session.container_list = [];
// check what containers the user wants hidden
let hidden = await Permission.findAll({ where: {user: session.user, hide: true}}, { attributes: ['containerName'] });
hidden = hidden.map((container) => container.containerName);
// check what containers the user has permission to view
let visable = await Permission.findAll({ where: { user: session.user, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] } });
visable = visable.map((container) => container.containerName);
// get all containers
let containers = await docker.listContainers({ all: true });
// loop through containers
for (let i = 0; i < containers.length; i++) {
let container_name = containers[i].Names[0].replace('/', '');
// skip hidden containers
if (hidden.includes(container_name)) { continue; }
// admin can see all containers that they don't have hidden
if (session.role == 'admin') { session.container_list.push({ container: container_name, state: containers[i].State }); }
// user can see any containers that they have any permissions for
else if (visable.includes(container_name)){ session.container_list.push({ container: container_name, state: containers[i].State }); }
}
// create a sent list if it doesn't exist
if (!session.sent_list) { session.sent_list = []; }
if (!session.update_list) { session.update_list = []; }
if (!session.new_cards) { session.new_cards = []; }
}
async function updateDashboard (session) {
let container_list = session.container_list;
let sent_list = session.sent_list;
session.new_cards = [];
session.update_list = [];
// loop through the containers list
container_list.forEach(info => {
let { container, state } = info;
let sent = sent_list.find(c => c.container === container);
if (!sent) { session.new_cards.push(container);}
else if (sent.state !== state) { session.update_list.push(container); }
});
// loop through the sent list to see if any containers have been removed
sent_list.forEach(info => {
let { container } = info;
let exists = container_list.find(c => c.container === container);
if (!exists) { session.update_list.push(container); }
});
}
// HTMX server-side events
export const SSE = async (req, res) => {
// set the headers for server-sent events
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
// check for container changes every 500ms
let eventCheck = setInterval(async () => {
await userCards(req.session);
// check if the cards displayed are the same as what's in the session
if ((JSON.stringify(req.session.container_list) === JSON.stringify(req.session.sent_list))) { return; }
await updateDashboard(req.session);
for (let i = 0; i < req.session.new_cards.length; i++) {
let details = await containerInfo(req.session.new_cards[i]);
let card = await createCard(details);
newCards += card;
req.session.alert = '';
}
for (let i = 0; i < req.session.update_list.length; i++) {
res.write(`event: ${req.session.update_list[i]}\n`);
res.write(`data: 'update cards'\n\n`);
}
res.write(`event: update\n`);
res.write(`data: 'update cards'\n\n`);
req.session.sent_list = req.session.container_list.slice();
}, 500);
req.on('close', () => {
clearInterval(eventCheck);
});
};
// Server metrics (CPU, RAM, TX, RX, DISK)
export const 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;
}
}
// Container charts
export const Chart = async (req, res) => {
let name = req.header('hx-trigger-name');
if (!stats[name]) { stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) }; }
const info = await dockerContainerStats(name);
stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
stats[name].ramArray.push(Math.round(info[0].memPercent));
stats[name].cpuArray = stats[name].cpuArray.slice(-15);
stats[name].ramArray = stats[name].ramArray.slice(-15);
let chart = `
<script>
${name}chart.updateSeries([{
data: [${stats[name].cpuArray}]
}, {
data: [${stats[name].ramArray}]
}])
</script>`
res.send(chart);
}

View file

@ -1,20 +1,63 @@
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><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
<th><button class="table-sort" data-sort="sort-type">Tag</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-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">`
@ -22,22 +65,32 @@ export const Images = async function(req, res) {
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="" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${images[i].RepoTags}</td>
<td><input class="form-check-input m-0 align-middle" name="select" value="${images[i].Id}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${name}</td>
<td class="sort-type">${tag}</td>
<td class="sort-city">${images[i].Id}</td>
<td class="sort-type">Latest</td>
<td class="sort-score text-green">In use</td>
<td class="sort-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="#">Details</a></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;
}
@ -48,9 +101,10 @@ export const Images = async function(req, res) {
res.render("images", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
avatar: req.session.user.charAt(0).toUpperCase(),
image_list: image_list,
image_count: images.length
image_count: images.length,
alert: '',
});
}

View file

@ -1,29 +1,23 @@
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":"",
});
}
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){
if (email && password) {
let existingUser = await User.findOne({ where: {email:email}});
if(existingUser){
if (existingUser) {
let match = await bcrypt.compare(password,existingUser.password);
if(match){
if (match) {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
@ -40,14 +34,9 @@ export const submitLogin = async function(req,res){
message: "User logged in successfully",
ip: req.socket.remoteAddress
});
if (req.session.role == "admin") {
res.redirect("/");
}
else {
res.redirect("/portal");
}
}else{
res.redirect("/dashboard");
} else {
const syslog = await Syslog.create({
user: null,
@ -61,12 +50,12 @@ export const submitLogin = async function(req,res){
"error":"Invalid password",
});
}
}else{
} else {
res.render("login",{
"error":"User with that email does not exist.",
});
}
}else{
} else {
res.status(400);
res.render("login",{
"error":"Please fill in all the fields.",

View file

@ -3,35 +3,48 @@ 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><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
<tbody class="table-tbody">`
for (let i = 0; i < networks.length; i++) {
// let date = new Date(images[i].Created * 1000);
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let 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="" type="checkbox" aria-label="Select"></td>
<td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${networks[i].Name}</td>
<td class="sort-city">${networks[i].Id}</td>
<td class="sort-score text-green">In use</td>
<td class="sort-score text-green">${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>`
@ -40,13 +53,37 @@ export const Networks = async function(req, res) {
network_list += `</tbody>`
res.render("networks", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
avatar: req.session.user.charAt(0).toUpperCase(),
network_list: network_list,
network_count: networks.length
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");
}

View file

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

View file

@ -1,4 +1,4 @@
import { User, Syslog } from '../database/models.js';
import { User, Syslog, Permission } from '../database/models.js';
import bcrypt from 'bcrypt';
let SECRET = process.env.SECRET || "MrWiskers"
@ -17,7 +17,8 @@ export const Register = function(req,res){
export const submitRegister = async function(req,res){
let { name, username, email, password, confirmPassword, avatar, warning, secret } = req.body;
let { name, username, email, password, confirmPassword, secret } = req.body;
email = email.toLowerCase();
if (secret != SECRET) {
@ -30,7 +31,7 @@ export const submitRegister = async function(req,res){
});
}
if((name && email && password && confirmPassword && username && warning) && (secret == SECRET) && (password == confirmPassword)){
if((name && email && password && confirmPassword && username) && (secret == SECRET) && (password == confirmPassword)){
async function userRole () {
let userCount = await User.count();
@ -55,7 +56,6 @@ export const submitRegister = async function(req,res){
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
avatar: `<img src="img/avatars/${avatar}">`,
lastLogin: newLogin,
});
@ -67,7 +67,11 @@ export const submitRegister = async function(req,res){
req.session.user = newUser.username;
req.session.UUID = newUser.UUID;
req.session.role = newUser.role;
req.session.avatar = newUser.avatar;
const permission = await Permission.create({
user: newUser.username,
userID: newUser.UUID
});
const syslog = await Syslog.create({
user: req.session.user,
@ -77,7 +81,7 @@ export const submitRegister = async function(req,res){
ip: req.socket.remoteAddress
});
res.redirect("/");
res.redirect("/dashboard");
}
} catch(err) {
res.render("register",{
@ -94,7 +98,7 @@ export const submitRegister = async function(req,res){
} else {
// Redirect to the signup page.
res.render("register",{
"error":"Please fill in all the fields and acknowledge security warning.",
"error":"Please fill in all the fields.",
});
}
}

View file

@ -4,6 +4,7 @@ export const Settings = (req, res) => {
res.render("settings", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});
}

31
controllers/supporters.js Normal file
View file

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

View file

@ -29,8 +29,9 @@ export const Syslogs = async function(req, res) {
res.render("syslogs", {
name: req.session.user || 'Dev',
role: req.session.role || 'Dev',
avatar: req.session.avatar || '<img src="/img/avatars/rus.jpg">',
logs: logs
avatar: req.session.user.charAt(0).toUpperCase(),
logs: logs,
alert: '',
});
}

View file

@ -20,13 +20,14 @@ export const Users = async (req, res) => {
let allUsers = await User.findAll();
allUsers.forEach((account) => {
let active = '<span class="badge badge-outline text-green">Active</span>'
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">Inactive</span>';
active = '<span class="badge badge-outline text-grey" title="User has not logged-in within the last 30 days.">Inactive</span>';
}
@ -35,7 +36,7 @@ export const Users = async (req, res) => {
<tr>
<td><input class="form-check-input" type="checkbox"></td>
<td>${account.id}</td>
<td><span class="avatar me-2">${account.avatar}</span></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>
@ -43,7 +44,7 @@ export const Users = async (req, res) => {
<td>${account.role}</td>
<td>${account.lastLogin}</td>
<td>${active}</td>
<td><a href="#" class="btn">Edit</a></td>
<td><a href="#" class="btn">View</a></td>
</tr>`
user_list += info;
@ -53,8 +54,9 @@ export const Users = async (req, res) => {
res.render("users", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
user_list: user_list
avatar: req.session.user.charAt(0).toUpperCase(),
user_list: user_list,
alert: ''
});
}

9
controllers/variables.js Normal file
View file

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

View file

@ -1,59 +1,66 @@
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;
let 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><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">Mount point</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
</tr>
</thead>
<tbody class="table-tbody">`
// 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 name = "" + volume.Name;
let mount = "" + volume.Mountpoint;
let type = "Bind";
if (name.length > 40) {
name = name.slice(0, 37) + '...';
}
// 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"; }
if (mount.length > 70) {
mount = mount.slice(0, 67) + '...';
}
// docker.df(volume.Mountpoint).then((data) => {
// for (let key in data) {
// console.log(data[key]);
// }
// });
let details = `
let row = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<td><input class="form-check-input m-0 align-middle" name="select" value="${name}" type="checkbox" aria-label="Select"></td>
<td class="sort-type">${type}</td>
<td class="sort-name">${name}</td>
<td class="sort-city">${mount}</td>
<td class="sort-score text-green">In use</td>
<td class="sort-score text-green">${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 += details;
volume_list += row;
}
volume_list += `</tbody>`
@ -62,9 +69,51 @@ export const Volumes = async function(req, res) {
res.render("volumes", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
avatar: req.session.user.charAt(0).toUpperCase(),
volume_list: volume_list,
volume_count: volumes.length
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]);
// }
// });

View file

@ -1,17 +1,9 @@
import { Sequelize, DataTypes } from 'sequelize';
// let SQLITE_PASS = process.env.SQLITE_PASS || 'some_long_elaborate_password';
// export const sequelize = new Sequelize('dweebui', 'dweebui', SQLITE_PASS, {
// dialect: 'sqlite',
// dialectModulePath: '@journeyapps/sqlcipher',
// storage: './database/database.sqlite',
// logging: false,
// });
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/database.sqlite',
storage: './database/db.sqlite',
logging: false,
});
@ -55,25 +47,60 @@ export const User = sequelize.define('User', {
});
export const Container = sequelize.define('Container', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
},
size: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
}
});
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
},
service: {
type: DataTypes.STRING
},
state: {
type: DataTypes.STRING
},
image: {
type: DataTypes.STRING
},
external_port: {
type: DataTypes.STRING
},
internal_port: {
type: DataTypes.STRING
},
ports: {
type: DataTypes.STRING
},
volumes: {
type: DataTypes.STRING
},
environment_variables: {
type: DataTypes.STRING
},
labels: {
type: DataTypes.STRING
},
IPv4: {
type: DataTypes.STRING
},
style: {
type: DataTypes.STRING
},
cpu: {
// store the last 15 values from dockerContainerStats
type: DataTypes.STRING
},
ram: {
// store the last 15 values from dockerContainerStats
type: DataTypes.STRING
},
});
export const Permission = sequelize.define('Permission', {
id: {
@ -83,11 +110,9 @@ export const Permission = sequelize.define('Permission', {
},
containerName: {
type: DataTypes.STRING,
allowNull: false
},
containerID: {
type: DataTypes.STRING,
allowNull: false
},
user: {
type: DataTypes.STRING,
@ -99,39 +124,51 @@ export const Permission = sequelize.define('Permission', {
},
install: {
type: DataTypes.STRING,
defaultValue: false
},
uninstall: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
edit: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
upgrade: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
start: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
stop: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
restart: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
pause: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
logs: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
hide: {
type: DataTypes.STRING
},
view: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
reset_view: {
type: DataTypes.STRING
type: DataTypes.STRING,
defaultValue: false
},
view: {
type: DataTypes.STRING,
defaultValue: false
},
});
@ -180,10 +217,40 @@ export const Notification = sequelize.define('Notification', {
color: {
type: DataTypes.STRING,
},
read: {
type: DataTypes.STRING,
},
createdAt : {
type: DataTypes.STRING
},
createdBy : {
type: DataTypes.STRING
},
});
export const Settings = sequelize.define('Settings', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
key: {
type: DataTypes.STRING,
allowNull: false
},
value: {
type: DataTypes.STRING,
allowNull: false
}
});
export const Variables = sequelize.define('Variables', {
find: {
type: DataTypes.STRING,
},
replace: {
type: DataTypes.STRING,
}
});

View file

@ -1,208 +0,0 @@
import { writeFileSync, mkdirSync, readFileSync } from "fs";
import yaml from 'js-yaml';
import { execSync } from "child_process";
import { docker } from "../server.js";
import DockerodeCompose from "dockerode-compose";
import { Syslog } from "../database/models.js";
import { containerCard } from "../components/containerCard.js";
// This entire page hurts to look at.
export const Install = async (req, res) => {
console.log(req.app.locals.installCard);
let data = req.body;
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
let ports = [port0, port1, port2, port3, port4, port5]
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
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
break;
}
}
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
// Volumes
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5]
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] == 'on') {
compose_file += `\n volumes:`
break;
}
}
for (let i = 0; i < volumes.length; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
// Environment variables
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n environment:`
break;
}
}
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Labels
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]
for (let i = 0; i < labels.length; i++) {
if (labels[i] == 'on') {
compose_file += `\n labels:`
break;
}
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Hardware acceleration
for (let i = 0; i < env_vars.length; i++) {
if ((env_vars[i] == 'on') && (data[`env_${i}_name`] == 'DRINODE')) {
compose_file += `\n deploy:`
compose_file += `\n resources:`
compose_file += `\n reservations:`
compose_file += `\n devices:`
compose_file += `\n - driver: nvidia`
compose_file += `\n count: 1`
compose_file += `\n capabilities: [gpu]`
break;
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
} catch { console.log('error creating directory or compose file') }
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
try {
await compose.pull();
await compose.up().then(() => {
const syslog = Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
});
} catch (err) {
console.error(err);
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
})();
}
res.redirect('/');
}

2955
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,29 @@
{
"name": "dweebui",
"version": "1.0.0",
"description": "A web UI for Docker",
"version": "0.60",
"description": "Free and Open-Source WebUI For Managing Your Containers.",
"main": "server.js",
"type": "module",
"scripts": {
"test": "mocha --require @babel/register"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "lllllllillllllillll",
"license": "MIT",
"dependencies": {
"@babel/register": "^7.23.7",
"@socket.io/admin-ui": "^0.5.1",
"adm-zip": "^0.5.12",
"bcrypt": "^5.1.1",
"chai": "^5.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dockerode": "^4.0.2",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-session": "^1.17.3",
"helmet": "^7.1.0",
"js-yaml": "^4.1.0",
"mocha": "^10.2.0",
"sequelize": "^6.35.2",
"sinon": "^17.0.1",
"socket.io": "^4.7.4",
"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",
"stream": "^0.0.2",
"supertest": "^6.3.3",
"systeminformation": "^5.21.22"
"systeminformation": "^5.22.9",
"yaml": "^2.4.2"
}
}

View file

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

View file

@ -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 {
@ -20221,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/img/add to zip.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

BIN
public/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

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

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

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

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

347
server.js
View file

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

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

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

View file

@ -1128,7 +1128,7 @@
}
],
"image": "linuxserver/minetest:latest",
"logo": "https://raw.githubusercontent.com/linuxserver/beta-templates/master/lsiodev/img/minetest-icon.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/minetest.png",
"platform": "linux",
"ports": [
"30000/udp"
@ -1260,7 +1260,7 @@
}
],
"image": "linuxserver/letsencrypt:latest",
"logo": "https://raw.githubusercontent.com/thesugarat/portainer_templates-1/master/Images/letsencrypt.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/letsencrypt.png",
"platform": "linux",
"ports": [
"80/tcp",
@ -1467,7 +1467,7 @@
"note": "",
"description": "Changedetection.io provides free, open-source web page monitoring, notification and change detection. (https://github.com/dgtlmoon/changedetection.io)",
"platform": "linux",
"logo": "https://github.com/linuxserver/docker-templates/raw/master/linuxserver.io/img/changedetection-icon.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/changedetection.io.png",
"image": "linuxserver/changedetection.io:latest",
"env": [
{
@ -1841,7 +1841,7 @@
"note": "",
"description": "[Embystat](https://github.com/mregni/EmbyStat) is a personal web server that can calculate all kinds of statistics from your (local) Emby server. Just install this on your server and let him calculate all kinds of fun stuff.",
"platform": "linux",
"logo": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/embystat-logo.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/embystat.png",
"image": "linuxserver/embystat:latest",
"env": [
{
@ -2686,7 +2686,7 @@
"note": "",
"description": "SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt\u2122) sets up an Nginx webserver and reverse proxy with php support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.",
"platform": "linux",
"logo": "https://github.com/linuxserver/docker-templates/raw/master/linuxserver.io/img/swag.gif",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/linuxserverio.png",
"image": "linuxserver/swag:latest",
"env": [
{
@ -2994,38 +2994,6 @@
],
"restart_policy": "unless-stopped"
},
{
"categories": [
"Authentication"
],
"note": "recommend requirements: a host with at least 2 CPU cores and 2 GB of RAM",
"description": "authentik is an open-source Identity Provider focused on flexibility and versatility. You can use authentik in an existing environment to add support for new protocols, implement sign-up/recovery/etc. in your application so you don't have to deal with it, and many other things.",
"env": [
{
"default": "1000",
"label": "PUID",
"name": "PUID"
},
{
"default": "1000",
"label": "PGID",
"name": "PGID"
},
{
"label": "PORT",
"name": "PORT"
}
],
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/authentik.png",
"name": "authentik",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/authentik.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Authentik",
"type": 3
},
{
"categories": [
"Productivity"
@ -3043,7 +3011,7 @@
}
],
"image": "zadam/trilium:latest",
"logo": "https://www.saashub.com/images/app/service_logos/55/2901389fab77/large.png?1561117248",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/trilium.png",
"name": "trilium",
"platform": "linux",
"ports": [
@ -3059,50 +3027,6 @@
}
]
},
{
"categories": [
"Communication"
],
"description": "Rocket.Chat is an open-source fully customizable communications platform developed in JavaScript for organizations with high standards of data protection.",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/rocketchat.png",
"note": "Rocket.Chat Server Container",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/rocketchat.yml",
"url": "https://github.com/mycroftwilde/portainer_templates"
},
"title": "Rocket Chat",
"name": "rocketchat",
"type": 3
},
{
"categories": [
"Other"
],
"description": "Joplin is an open-source note-taking app",
"env": [
{
"default": "22300",
"label": "PORT",
"name": "PORT"
},
{
"default": "http://joplin.yourdomain.tld:22300",
"label": "URL",
"name": "URL"
}
],
"logo": "https://raw.githubusercontent.com/laurent22/joplin/master/Assets/SquareIcon512.png",
"note": "Joplin is an open-source note-taking app",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/joplin.yml",
"url": "https://github.com/mycroftwilde/portainer_templates"
},
"title": "Joplin",
"name": "joplin",
"type": 3
},
{
"categories": [
"Dashboard"
@ -3435,7 +3359,7 @@
"type": 1,
"volumes": [
{
"bind": "/portainer/Downloads",
"bind": "/home/docker/downloads",
"container": "/data"
},
{
@ -3552,7 +3476,7 @@
],
"description": "YoutubeDL-Material is a Material Design frontend for youtube-dl. It's coded using Angular 9 for the frontend, and Node.js on the backend.",
"image": "tzahi12345/youtubedl-material:latest",
"logo": "https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Images/ytdlm.png",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/youtubedl.png",
"name": "youtubedl-material",
"platform": "linux",
"ports": [
@ -4375,7 +4299,7 @@
"container": "/config"
},
{
"bind": "/portainer/Downloads",
"bind": "/home/docker/Downloads",
"container": "/app/qBittorrent/downloads"
}
]
@ -4677,50 +4601,6 @@
}
]
},
{
"categories": [
"Finance",
"Productivity"
],
"description": "Invoices, Expenses and Tasks built with Laravel and Flutter.",
"env": [
{
"default": "invoice.my.domain",
"label": "URL",
"name": "URL"
},
{
"label": "APP_KEY",
"name": "APP_KEY"
},
{
"label": "TZ",
"name": "TZ"
},
{
"label": "DATABASE_PASSWORD",
"name": "DATABASE_PASSWORD"
},
{
"label": "MYSQL_ROOT_PASSWORD",
"name": "MYSQL_ROOT_PASSWORD"
},
{
"label": "PORT",
"name": "PORT"
}
],
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/invoiceninja.png",
"name": "invoice_ninja",
"note": "The database user is invoice_ninja and the database is ninja_db. Please generate an app key following the documentation <a href='https://github.com/invoiceninja/dockerfiles'>here</a>. ",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/invoice-ninja.yml",
"url": "https://github.com/SelfhostedPro/selfhosted_templates"
},
"title": "Invoice Ninja",
"type": 3
},
{
"categories": [
"Gaming"
@ -4827,7 +4707,7 @@
"container": "/etc/localtime"
},
{
"bind": "/portainer/Downloads",
"bind": "/home/docker/Downloads",
"container": "/downloads"
},
{
@ -4854,7 +4734,7 @@
"container": "/app/configs"
},
{
"bind": "/portainer/Downloads",
"bind": "/home/docker/Downloads",
"container": "/downloads"
}
],
@ -4913,68 +4793,6 @@
}
]
},
{
"categories": [
"Downloaders"
],
"description": "A docker image with qBittorrent and the Flood UI, also optional WireGuard VPN support.",
"logo": "https://hotio.dev/img/image-logos/flood.svg",
"name": "flood",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/flood.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Flood",
"type": 3
},
{
"categories": [
"Multimedia",
"Productivity"
],
"description": "PhotoPrism is an AI-powered app for browsing, organizing & sharing your photo collection. It makes use of the latest technologies to tag and find pictures automatically without getting in your way.| Copy as Custom stack and EDIT environment variables.",
"logo": "https://photoprism.app/static/img/logo.svg",
"name": "photoprism",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/photoprism.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Photoprism",
"type": 3
},
{
"categories": [
"Multimedia",
"AI"
],
"description": "Immich is a high performance self-hosted photo and video backup solution.",
"logo": "https://github.com/immich-app/immich/raw/main/design/immich-logo.svg",
"name": "immich",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/immich.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Immich",
"type": 3
},
{
"categories": [
"Tools"
],
"description": "The recipe manager that allows you to manage your ever growing collection of digital recipes.",
"logo": "https://docs.tandoor.dev/logo_color.svg",
"name": "tandoor",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/tandoor.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Tandoor",
"type": 3
},
{
"type": 1,
"name": "influxdb",
@ -5023,36 +4841,6 @@
}
]
},
{
"categories": [
"Media Server"
],
"description": "Your media enjoyed through a minimal lightweight media server.",
"logo": "https://github.com/midarrlabs/midarr-server/raw/master/priv/static/logo.svg",
"name": "midarr",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/midarr.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Midarr",
"type": 3
},
{
"categories": [
"Development"
],
"description": "Appwrite is a self-hosted backend-as-a-service platform that provides developers with all the core APIs required to build any application.",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/appwrite.png",
"name": "appwrite",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/appwrite.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Appwrite",
"type": 3
},
{
"categories": [
"Media Server",
@ -5121,72 +4909,6 @@
],
"type": 3
},
{
"categories": [
"Downloaders",
"Multimedia"
],
"description": "Autobrr is the modern download automation tool for torrents.",
"logo": "https://autobrr.com/img/logo.png",
"name": "autobrr",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/autobrr.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Autobrr",
"type": 3
},
{
"categories": [
"Monitoring"
],
"description": "Glances is an open-source system cross-platform monitoring tool. It allows real-time monitoring of various aspects of your system such as CPU, memory, disk, network usage etc.",
"logo": "https://raw.githubusercontent.com/nicolargo/glances/develop/docs/_static/glances-responsive-webdesign.png",
"name": "glances",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/glances.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Glances",
"type": 3
},
{
"categories": [
"Development",
"Low Code",
"No code"
],
"description": "Tooljet is an Open-source low-code application development platform for building and deploying business applications.",
"logo": "https://uploads-ssl.webflow.com/6266634263b9179f76b2236e/63aaa161e3b3be42ec50eb6f_Logomark.svg",
"name": "tooljet",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/tooljet.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Tooljet",
"type": 3
},
{
"categories": [
"Productivity",
"Development",
"Low-code",
"No-code"
],
"description": "Budibase allows no-code users to build apps quickly, with more functionality available with a little bit of inline code.",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/budibase.png",
"name": "budibase",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/budibase.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Budibase",
"type": 3
},
{
"categories": [
"Development"
@ -5209,68 +4931,6 @@
],
"type": 3
},
{
"categories": [
"Networking",
"Monitoring"
],
"description": "Lazytainer monitors network traffic to containers. If there is traffic, the container runs, otherwise the container is stopped/paused.",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/lazytainer.png",
"name": "lazytainer",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/lazytainer.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Lazytainer",
"type": 3
},
{
"categories": [
"Finance"
],
"description": "I hate money is a web application made to ease shared budget management. It keeps track of who bought what, when, and for whom; and helps to settle the bills.",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/ihatemoney.png",
"name": "ihatemoney",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/ihatemoney.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "I hate money",
"type": 3
},
{
"categories": [
"Media Server",
"LDAP"
],
"description": "Fireshare: Share your game clips, videos, or other media via unique links.",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/fireshare.png",
"name": "fireshare",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/fireshare.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Fireshare",
"type": 3
},
{
"categories": [
"Productivity"
],
"description": "Xwiki s a free wiki software platform written in Java with a design emphasis on extensibility. XWiki is an enterprise wiki.",
"logo": "https://upload.wikimedia.org/wikipedia/commons/e/e2/Logo-xwikiorange.svg",
"name": "xwiki",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/xwiki.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Xwiki",
"type": 3
},
{
"categories": [
"Productivity"
@ -5339,21 +4999,6 @@
"5055:5055/tcp"
]
},
{
"categories": [
"Productivity"
],
"description": "Trudesk is an Open Source Help Desk Software and Ticketing System",
"logo": "https://trudesk.io/wp-content/uploads/2019/10/logo-med.png",
"name": "trudesk",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/trudesk.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Trudesk",
"type": 3
},
{
"categories": [
"Dashboard"
@ -5376,23 +5021,6 @@
],
"privileged": true
},
{
"categories": [
"Productivity",
"Development",
"No-code"
],
"description": "Open source no-code database and Airtable alternative.Create your own online database without technical experience. Our user friendly no-code tool gives you the powers of a developer without leaving your browser.",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/baserow.png",
"name": "baserow",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/baserow.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"title": "Baserow",
"type": 3
},
{
"name": "nocodb",
"title": "NocoDB",
@ -5454,25 +5082,6 @@
}
]
},
{
"categories": [
"Analytics",
"Tools"
],
"description": "Google Analytics alternative that protects your data and your customers' privacy",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/matomo.png",
"name": "matomo",
"platform": "linux",
"repository": {
"stackfile": "Template/Stack/matomo.yml",
"url": "https://github.com/xneo1/portainer_templates"
},
"ports": [
"8282:80/tcp"
],
"title": "Matomo",
"type": 3
},
{
"categories": [
"Tools"
@ -5516,11 +5125,11 @@
"type": 1,
"volumes": [
{
"bind": "/portainer/Files/AppData/Adguard/Workdir",
"bind": "/home/docker/Files/AppData/Adguard/Workdir",
"container": "/opt/adguardhome/work"
},
{
"bind": "/portainer/Files/AppData/Adguard/Conf",
"bind": "/home/docker/Files/AppData/Adguard/Conf",
"container": "/opt/adguardhome/conf"
}
]
@ -5611,7 +5220,7 @@
"note": "",
"description": "Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents. <a href='https://www.elastic.co/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/elasticsearch' target='_blank'>Docker Hub</a>",
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/elasticsearch.png",
"image": "elasticsearch",
"image": "elasticsearch:8.13.4",
"categories": [
"Database"
],
@ -5914,7 +5523,7 @@
"note": "",
"description": "Nvidia HWA Test is a test container for NVIDIA hardware acceleration. Start the container then check the logs to confirm your output matches the example from this page: <a href='https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/sample-workload.html'>Running a Sample Workload</a>. <a href='https://www.nvidia.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nvidia/cuda' target='_blank'>Docker Hub</a>",
"platform": "linux",
"logo": "https://avatars.githubusercontent.com/u/1728152",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nvidia.png",
"image": "nvidia/cuda:12.2.0-base-ubuntu20.04",
"env": [
{

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

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

250
utils/install.js Normal file
View file

@ -0,0 +1,250 @@
import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "fs";
import yaml from 'js-yaml';
import { execSync } from "child_process";
import { docker } from "../server.js";
import DockerodeCompose from "dockerode-compose";
import { Syslog } from "../database/models.js";
import { addAlert } from "../controllers/dashboard.js";
// This entire page hurts to look at.
export const Install = async (req, res) => {
let data = req.body;
let name = data.name;
let containers = await docker.listContainers({ all: true });
for (let i = 0; i < containers.length; i++) {
if (containers[i].Names[0].includes(name)) {
addAlert(req.session, 'danger', `App ${name} already exists. Please remove it first.`);
res.redirect('/');
return;
}
}
if (req.body.compose) {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) });
let compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`);
addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
try {
(async () => {
await compose.pull();
await compose.up();
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${app} installed successfully`,
ip: req.socket.remoteAddress
});
})();
} catch (err) {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${app} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
} else {
let { service_name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
let ports = [port0, port1, port2, port3, port4, port5]
let docker_volumes = [];
addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
if (image.startsWith('https://')){
mkdirSync(`./appdata/${name}`, { recursive: true });
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
console.log(`Downloaded stackfile: ${image}`);
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
let services = Object.keys(stackfile.services);
for ( let i = 0; i < services.length; i++ ) {
try {
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
} catch { console.log('no env') }
}
} else {
let compose_file = `version: '3'`;
compose_file += `\nservices:`
compose_file += `\n ${service_name}:`
compose_file += `\n container_name: ${name}`;
compose_file += `\n image: ${image}`;
// Command
if (command_check == 'on') {
compose_file += `\n command: ${command}`
}
// Network mode
if (net_mode == 'host') {
compose_file += `\n network_mode: 'host'`
}
else if (net_mode != 'host' && net_mode != 'docker') {
compose_file += `\n network_mode: '${net_mode}'`
}
// Restart policy
if (restart_policy != '') {
compose_file += `\n restart: ${restart_policy}`
}
// Ports
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
break;
}
}
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
// Volumes
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5]
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] == 'on') {
compose_file += `\n volumes:`
break;
}
}
for (let i = 0; i < volumes.length; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
// Environment variables
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n environment:`
break;
}
}
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Labels
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]
for (let i = 0; i < labels.length; i++) {
if (labels[i] == 'on') {
compose_file += `\n labels:`
break;
}
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Hardware acceleration
for (let i = 0; i < env_vars.length; i++) {
if ((env_vars[i] == 'on') && (data[`env_${i}_name`] == 'DRINODE')) {
compose_file += `\n deploy:`
compose_file += `\n resources:`
compose_file += `\n reservations:`
compose_file += `\n devices:`
compose_file += `\n - driver: nvidia`
compose_file += `\n count: 1`
compose_file += `\n capabilities: [gpu]`
break;
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
} catch {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed - error creating directory or compose file`,
ip: req.socket.remoteAddress
});
}
try {
(async () => {
await compose.pull();
await compose.up();
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
})();
} catch (err) {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
}
}
res.redirect('/');
}

View file

@ -6,17 +6,22 @@ export const Uninstall = async (req, res) => {
let { confirm, service_name } = req.body;
console.log(`Uninstalling ${service_name}...`);
if (confirm == 'Yes') {
var containerName = docker.getContainer(`${service_name}`);
let containerName = docker.getContainer(service_name);
console.log(`Stopping ${service_name}...`)
try {
await containerName.stop();
} catch {
console.log(`Error stopping ${service_name} container`);
}
try {
await containerName.remove();
try {
console.log(`Removing ${service_name}...`);
containerName.remove();
const syslog = await Syslog.create({
user: req.session.user,
email: null,
@ -35,7 +40,10 @@ export const Uninstall = async (req, res) => {
ip: req.socket.remoteAddress
});
}
} else {
console.log(`Didn't confirm uninstallation of ${service_name}...`);
}
res.redirect('/');
}

View file

@ -1,144 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Account</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Account Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<h4 class="subheader">Business settings</h4>
<div class="list-group list-group-transparent">
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center active">Accounts</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">My Notifications</a>
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Connected Apps</a> -->
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Billing & Invoices</a> -->
</div>
<h4 class="subheader mt-4">Experience</h4>
<div class="list-group list-group-transparent">
<a href="#" class="list-group-item list-group-item-action">Credits</a>
</div>
</div>
</div>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-4">My Account</h2>
<h3 class="card-title">Profile Details</h3>
<div class="row align-items-center">
<div class="col-auto"><span class="avatar avatar-xl"><%- avatar %></span>
</div>
<div class="col-auto"><a href="#" class="btn">
Change avatar
</a>
</div>
<div class="col-auto"><a href="#" class="btn btn-ghost-danger">
Delete avatar
</a>
</div>
</div>
<h3 class="card-title mt-4">Profile</h3>
<div class="row g-3">
<div class="col-md">
<div class="form-label">Full Name</div>
<input type="text" class="form-control" value="<%= name %>" readonly="<%= name %>">
</div>
<div class="col-md">
<div class="form-label">First Name</div>
<input type="text" class="form-control" value="<%= first_name %>" readonly="<%= first_name %>">
</div>
<div class="col-md">
<div class="form-label">Last Name</div>
<input type="text" class="form-control" value="<%= last_name %>" readonly="<%= last_name %>">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="<%= email %>" readonly="<%= email %>">
</div>
<div class="col-auto">
<a href="#" class="btn">Change</a>
</div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
<h3 class="card-title mt-4">Public profile</h3>
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
you.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" >
<span class="form-check-label form-check-label-on">You're currently visible</span>
<span class="form-check-label form-check-label-off">You're
currently invisible</span>
</label>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<a href="#" class="btn btn-primary">
Submit
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

131
views/account.html Normal file
View file

@ -0,0 +1,131 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Account</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-4">My Account</h2>
<h3 class="card-title">Profile Details</h3>
<div class="row align-items-center">
<div class="col-auto"><span class="avatar avatar-xl"><%- avatar %></span>
</div>
<div class="col-auto"><a href="#" class="btn">
Change avatar
</a>
</div>
<div class="col-auto"><a href="#" class="btn btn-ghost-danger">
Delete avatar
</a>
</div>
</div>
<h3 class="card-title mt-4">Profile</h3>
<div class="row g-3">
<div class="col-md">
<div class="form-label">Full Name</div>
<input type="text" class="form-control" value="<%= name %>" readonly="<%= name %>">
</div>
<div class="col-md">
<div class="form-label">First Name</div>
<input type="text" class="form-control" value="<%= first_name %>" readonly="<%= first_name %>">
</div>
<div class="col-md">
<div class="form-label">Last Name</div>
<input type="text" class="form-control" value="<%= last_name %>" readonly="<%= last_name %>">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="<%= email %>" readonly="<%= email %>">
</div>
<div class="col-auto">
<a href="#" class="btn">Change</a>
</div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
<h3 class="card-title mt-4">Public profile</h3>
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
you.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" >
<span class="form-check-label form-check-label-on">You're currently visible</span>
<span class="form-check-label form-check-label-off">You're
currently invisible</span>
</label>
</div>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
<a href="#" class="btn">
Cancel
</a>
<a href="#" class="btn btn-primary">
Submit
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

@ -1,149 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Apps</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Apps
</h2>
<div class="text-secondary mt-1"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps.</div>
</div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="d-flex">
<form action="/apps" id="search" name="search" method="POST">
<input type="search" class="form-control" name="search" placeholder="Search apps…">
</form>
<input type="submit" form="search" class="btn btn-outline-success h-50" value="search">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Change Templates">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="#">Default Template</a>
<a class="dropdown-item" href="#">Compose Files</a>
<a class="dropdown-item" href="#">Custom Template</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<%- apps_list %>
</div>
<div class="d-flex mt-4">
<ul class="pagination ms-auto">
<li class="page-item">
<a class="page-link" href="<%- prev %>" tabindex="-1" aria-disabled="true">
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>
prev
</a>
</li>
<li class="page-item"><a class="page-link" href="/apps?page=1">1</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=2">2</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=3">3</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=4">4</a></li>
<li class="page-item"><a class="page-link" href="/apps?page=5">5</a></li>
<li class="page-item">
<a class="page-link" href="<%- next %>">
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
</a>
</li>
</ul>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<!-- <script>
async function sendData(form) {
const formData = new FormData(form);
const jsonData = {};
for (let [key, value] of formData.entries()) {
jsonData[key] = value;
}
try {
const response = await fetch("/install", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonData),
});
console.log(await response.json());
} catch (e) {
console.error(e);
}
}
document.addEventListener("submit", (event) => {
event.preventDefault();
const buttonId = event.target.id;
console.log(buttonId);
const form = document.querySelector(`#${buttonId}`);
if (form) {
sendData(form);
} else {
console.error(`Form not found for button with ID: ${buttonId}`);
}
});
</script> -->
</body>
</html>

173
views/apps.html Normal file
View file

@ -0,0 +1,173 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Apps</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="mt-3">
<div class="container-xl">
<div class="row row-cards">
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body text-center">
<div class="d-flex align-items-center">
<div class="me-auto btn"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps</div>
<%- remove_button %>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body text-center">
<div class="d-flex align-items-center">
<div class="btn me-2">
Category:
</div>
<div class="dropdown">
<button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Everything</button>
<form action="/apps" method="POST">
<ul class="dropdown-menu">
<li><input type="submit" class="dropdown-item" name="search" value="Other"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Productivity"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Tools"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Dashboard"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Communication"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Media"></li>
<li><input type="submit" class="dropdown-item" name="search" value="CMS"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Monitoring"></li>
<li><input type="submit" class="dropdown-item" name="search" value="LDAP"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Arr"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Paid"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Database"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Gaming"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Finance"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Networking"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Authentication"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Development"></li>
<li><input type="submit" class="dropdown-item" name="search" value="Downloader"></li>
</ul>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body text-center">
<div class="d-flex align-items-center">
<dropdown class="me-2">
<button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Default Templates</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/apps">Default Templates</a></li>
<li><a class="dropdown-item" href="/apps/1/compose">Compose Files</a></li>
<%- json_templates %>
</ul>
</dropdown>
<button class="btn" name="Import" id="Import" data-hx-get="/import_modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Import</button>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body text-center">
<div class="text-secondary d-flex align-items-center">
<form action="/apps" id="search" name="search" method="POST" class="d-flex">
<input type="search" class="form-control me-2" name="search" placeholder="Search apps…" >
<input type="submit" form="search" class="btn" value="Search">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="mt-3">
<div class="container-xl">
<div class="row row-cards">
<%- apps_list %>
<!-- HTMX Target-->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Loading</h5>
</div>
<div class="modal-body text-center">
<div class="spinner-border"></div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex mt-4">
<ul class="pagination ms-auto">
<li class="page-item">
<a class="page-link" href="<%- prev %>" tabindex="-1" aria-disabled="true">
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>
prev
</a>
</li>
<%- pages %>
<li class="page-item">
<a class="page-link" href="<%- next %>">
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
</a>
</li>
</ul>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

File diff suppressed because it is too large Load diff

280
views/dashboard.html Normal file
View file

@ -0,0 +1,280 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Dashboard</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/meters.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<script src="/js/htmx-sse.js"></script>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards" hx-ext="sse" sse-connect="/sse">
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="CPU" id="green" data-hx-get="/stats" data-hx-trigger="load, every 1s">
<div class="font-weight-medium">
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
</div>
<div class="cpu-bar meter animate green">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="RAM" id="blue" data-hx-get="/stats" data-hx-trigger="load, every 2s">
<div class="font-weight-medium">
<label class="ram-text mb-1" for="ram">RAM 0%</label>
</div>
<div class="ram-bar meter animate blue">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-purple text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="NET" id="purple" data-hx-get="/stats" data-hx-trigger="load, every 2s">
<div class="font-weight-medium">
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
</div>
<div class="ram-bar meter animate purple">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-orange text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="DISK" id="orange" data-hx-get="/stats" data-hx-trigger="load, every 3s">
<div class="font-weight-medium">
<label class="disk-text mb-1" for="disk">DISK 0%</label>
</div>
<div class="meter animate orange">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="row row-cards" id="containers">
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" data-hx-post="/dashboard/updates" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
</div>
</div>
<!-- HTMX Modal Target -->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Loading</h5>
</div>
<div class="modal-body text-center">
<div class="spinner-border"></div>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card-body">
<h4>Logs:</h4>
<div id="logView">
<pre>No logs available</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
<script src="/js/tabler.min.js"></script>
<script>
var options = {
chart: {
type: "line",
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [3, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: []
}, {
name: "RAM",
data: []
}],
tooltip: {
enabled: false
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
}
},
yaxis: {
min: 0,
max: 100,
labels: {
padding: 4
}
},
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}
</script>
<!-- SelectAll for the permissions modal -->
<script>
function selectAll(group) {
let checkboxes = document.getElementsByName(group);
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -1,188 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Images</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Docker Images</h3>
<div class="card-options btn-list">
<a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a>
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
New Image
</a>
</div>
</div>
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<form action="/addsite" id="addsite" method="POST">
<div class="mb-3">
<div class="form-label">Type</div>
<select class="form-select" name="type">
<option value="reverse_proxy">Reverse Proxy</option>
<option value="proxy">Proxy</option>
<option value="file_server">File Server</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Domain / Subdomain</label>
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
</div>
<div class="mb-4">
<div class="row g-2">
<div class="col-8">
<label class="form-label">Hostname / Host IP</label>
<input type="text" class="form-control" name="host" placeholder="localhost">
</div>
<div class="col-4">
<label class="form-label">Port</label>
<input type="text" class="form-control" name="port" placeholder="8000">
</div>
</div>
</div>
<div class="mb-3">
<div class="divide-y">
<div>
<label class="row">
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
</label>
</span>
</label>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-label">Container</div>
<select class="form-select" name="container" disabled="">
<option value="0" selected></option>
<option value="1">Jellyfin</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
</div>
</div>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- image_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/enablesite">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/disablesite">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/removesite">
Delete
</button>
</div>
</span>
</form>
<p class="m-0 text-muted ms-auto"><%- image_count %> Images</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

159
views/images.html Normal file
View file

@ -0,0 +1,159 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Images</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Docker Images</h3>
<div class="card-options btn-list">
<!-- <a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a> -->
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
New Image
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- image_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/images/remove">Remove</button>
</form>
<p class="m-0 text-muted ms-auto"><%- image_count %> Images</p>
</div>
</form>
<!-- HTMX Modal Target -->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Image</h5>
</div>
<div class="modal-body text-center">
<form method="post" action="/images/add">
<div class="row row-cards">
<div class="col-sm-6 col-md-6">
</div>
<div class="col-sm-6 col-md-6">
</div>
</div>
<div class="row g-2 align-items-end">
<div class="col-7">
<label class="form-label text-muted">Image</label>
<input type="text" class="form-control" name="image" placeholder="lllllllillllllillll/dweebui">
</div>
<div class="col-3">
<label class="form-label text-muted">Tag</label>
<input type="text" class="form-control" name="tag" placeholder="latest">
</div>
<div class="col-2">
<button type="submit" class="btn mt-2">Pull</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

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

28
views/modals/compose.html Normal file
View file

@ -0,0 +1,28 @@
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Install AppName</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre class="text-secondary">AppNote</pre>
<form action="/install" name="FormId_install" id="FormId_install" method="POST">
<input type="hidden" name="name" value="AppName"/>
<textarea class="form-control" name="compose" rows="30" placeholder="Content..">COMPOSE_CONTENT</textarea>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<input type="submit" form="FormId_install" class="btn btn-success" value="Install"/>
</div>
</div>
</div>

956
views/modals/details.html Normal file
View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

@ -1,188 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Networks</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Docker Networks</h3>
<div class="card-options btn-list">
<a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a>
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
New Network
</a>
</div>
</div>
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<form action="/addsite" id="addsite" method="POST">
<div class="mb-3">
<div class="form-label">Type</div>
<select class="form-select" name="type">
<option value="reverse_proxy">Reverse Proxy</option>
<option value="proxy">Proxy</option>
<option value="file_server">File Server</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Domain / Subdomain</label>
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
</div>
<div class="mb-4">
<div class="row g-2">
<div class="col-8">
<label class="form-label">Hostname / Host IP</label>
<input type="text" class="form-control" name="host" placeholder="localhost">
</div>
<div class="col-4">
<label class="form-label">Port</label>
<input type="text" class="form-control" name="port" placeholder="8000">
</div>
</div>
</div>
<div class="mb-3">
<div class="divide-y">
<div>
<label class="row">
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
<span class="col-auto">
<label class="form-check form-check-single form-switch">
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
</label>
</span>
</label>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-label">Container</div>
<select class="form-select" name="container" disabled="">
<option value="0" selected></option>
<option value="1">Jellyfin</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
</div>
</div>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- network_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/enablesite">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/disablesite">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/removesite">
Delete
</button>
</div>
</span>
</form>
<p class="m-0 text-muted ms-auto"><%- network_count %> Networks</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

131
views/networks.html Normal file
View file

@ -0,0 +1,131 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Networks</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Docker Networks</h3>
<div class="card-options btn-list">
<!-- <a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a> -->
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
New Network
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- network_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/removeNetwork">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
Delete
</button>
</div>
</span> -->
<p class="m-0 text-muted ms-auto"><%- network_count %> Networks</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

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

View file

@ -0,0 +1,80 @@
<div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<div class="subheader text-yellow">ExternalPort:InternalPort</div>
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
</button>
<button class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
</button>
<button class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
</button>
<button class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
</button>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-post="/dashboard/details" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-post="/dashboard/logs" hx-swap="innerHTML" hx-trigger="mousedown" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
<button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
<button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="mousedown" data-hx-post="/dashboard/uninstall" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
</div>
</div>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/hide" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/permissions" name="AppName" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="mousedown" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="AppName" style="margin-bottom: 0;">
<a href="http://${link}:${external_port}" target="_blank">
AppShortName
</a>
</div>
<div class="ms-auto">
<label id="AppNameState">
<span class="text-StateColor align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
AppState
</span>
</label>
</div>
</div>
<script>
var ChartNamechart = new ApexCharts(document.querySelector("#ChartName_chart"), options);
</script>
<div class="chart-sm">
<div id="ChartName_chart" data-trigger="" data-hx-get="/chart" name="ChartName" hx-swap="innerHTML">
<script>
ChartNamechart.render();
</script>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,71 @@
<div class="col-sm-6 col-lg-3 pt-1" hx-get="" hx-trigger="" hx-swap="none" name="AppName">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<div class="subheader text-yellow">ExternalPort:InternalPort</div>
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
</button>
<button class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
</button>
<button class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
</button>
<button class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
</button>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-get="/dashboard/modals" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-get="/dashboard/logs" hx-swap="innerHTML" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
<button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
<button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="click" data-hx-get="/modals" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
</div>
</div>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/hide" data-hx-trigger="click" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="click" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
<button class="dropdown-item text-secondary" name="AppName" id="permissions" data-hx-get="/modals" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="AppName">
<a href="http://${link}:${external_port}" target="_blank">
AppShortName
</a>
</div>
<div class="ms-auto">
<label id="AppNameState">
<span class="text-StateColor align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
AppState
</span>
</label>
</div>
</div>
</div>
</div>
</div>

View file

@ -7,8 +7,7 @@
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE" class="link-secondary">License</a></li>
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/tree/main" target="_blank" class="link-secondary" rel="noopener">Source code</a></li>
<li class="list-inline-item">
<a href="https://github.com/lllllllillllllillll/DweebUI/tree/main" target="_blank" class="link-secondary" rel="noopener">
<!-- Download SVG icon from http://tabler-icons.io/i/heart -->
<a href="https://www.buymeacoffee.com/lllllllillllllillll" target="_blank" class="link-secondary" rel="noopener">
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-pink icon-filled icon-inline" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>
Sponsor
</a>
@ -23,8 +22,8 @@
All rights reserved.
</li>
<li class="list-inline-item">
<a href="#" class="link-secondary" rel="noopener">
v0.10
<a href="https://github.com/lllllllillllllillll/DweebUI/releases" class="link-secondary" rel="noopener">
v0.60
</a>
</li>
</ul>

View file

@ -27,21 +27,30 @@
}
}
</script>
<header class="navbar navbar-expand-md d-print-none">
<header class="navbar navbar-expand-md d-print-none py-0">
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu"
aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
<a href="#">
<img src="/static/logo.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="40px">
</a>
<a href="#">
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</a>
</h1>
<% if(alert) { %>
<%- alert %>
<% } %>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item d-none d-md-flex me-3">
<!-- <div class="btn-list">
<a href="#" class="btn text-green">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path> <path d="M8 11v-4a4 4 0 1 1 8 0v4"></path> </svg>
@ -56,6 +65,16 @@
VNC
</a>
</div> -->
<!-- <% if(role == 'admin') { %>
<div class="btn-list">
<a href="#" class="btn text-red">
Admin
</a>
</div>
<% } %> -->
</div>
@ -73,17 +92,21 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /> <path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /> </svg>
</button>
<div class="nav-item dropdown d-none d-md-flex me-3">
<div class="nav-item dropdown d-none d-md-flex me-2">
<a href="#" class="nav-link px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="Show notifications">
<!-- Download SVG icon from http://tabler-icons.io/i/bell -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" /> <path d="M9 17v1a3 3 0 0 0 6 0v-1" /> </svg>
<!-- <span class="badge bg-red"></span> -->
</a>
<div class="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
<div class="card">
<div class="card-header">
<h3 class="card-title">Notifications</h3>
</div>
<div class="list-group list-group-flush list-group-hoverable">
<div class="list-group-item">
<div class="row align-items-center">
@ -120,14 +143,16 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="nav-item dropdown">
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar avatar-sm"><%- avatar %></span>
<span class="avatar avatar-sm bg-green-lt"><%= avatar %></span></span>
<div class="d-none d-xl-block ps-2">
<div>
<%= name %>
@ -138,11 +163,10 @@
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<!-- <a href="#" class="dropdown-item">Status</a> -->
<a href="/account" class="dropdown-item">Account</a>
<!-- <a href="#" class="dropdown-item">Feedback</a> -->
<!-- <div class="dropdown-divider"></div> -->
<a href="/settings" class="dropdown-item">Settings</a>
<!-- <div class="dropdown-divider"></div> -->
<a href="/logout" class="dropdown-item">Logout</a>
</div>
</div>
@ -155,7 +179,7 @@
<div class="container-xl">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">
<a class="nav-link" href="/dashboard">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/dashboard -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dashboard" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 13m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path> <path d="M13.45 11.55l2.05 -2.05"></path> <path d="M6.4 20a9 9 0 1 1 11.2 0z"></path> </svg>
@ -239,17 +263,6 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/portal">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-star" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" /></svg> </span>
<span class="nav-link-title">
Portal
</span>
</a>
</li>
</ul>
<div class="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">

View file

@ -0,0 +1,13 @@
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<h4 class="subheader">Menu</h4>
<div class="list-group list-group-transparent">
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Accounts</a>
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
</div>
<h4 class="subheader mt-4">Other</h4>
<div class="list-group list-group-transparent">
<a href="/supporters" class="list-group-item list-group-item-action">Supporters</a>
</div>
</div>
</div>

View file

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

View file

@ -1,146 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Dashboard</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/meters.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12" id="cards">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-primary text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
<label id="cpu-text" class="cpu-text mb-1" for="cpu">I had to put something here.</label>
</div>
<div id="cpu-bar" class="cpu-bar meter animate">
<span style="width: 25%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
<label id="ram-text" class="ram-text mb-1" for="ram">RAM 0%</label>
</div>
<div id="ram-bar" class="ram-bar meter animate orange">
<span style="width: 25%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-twitter text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
</div>
<div id="net-bar" class="meter animate blue">
<span style="width: 25%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-facebook text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
<label id="disk-text" class="disk-text mb-1" for="disk">DISK 0%</label>
</div>
<div id="disk-bar" class="meter animate red">
<span style="width: 25%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- inserted here from -->
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/apexcharts/dist/apexcharts.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js"></script>
</body>
</html>

275
views/portal.html Normal file
View file

@ -0,0 +1,275 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Dashboard</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/meters.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<script src="/js/htmx-sse.js"></script>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards" hx-ext="sse" sse-connect="/sse_event">
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="CPU" id="green">
<div class="font-weight-medium">
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
</div>
<div class="cpu-bar meter animate green">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="RAM" id="blue">
<div class="font-weight-medium">
<label class="ram-text mb-1" for="ram">RAM 0%</label>
</div>
<div class="ram-bar meter animate blue">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-purple text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="NET" id="purple">
<div class="font-weight-medium">
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
</div>
<div class="ram-bar meter animate purple">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-orange text-white avatar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
</span>
</div>
<!-- HTMX -->
<div class="col" name="DISK" id="orange">
<div class="font-weight-medium">
<label class="disk-text mb-1" for="disk">DISK 0%</label>
</div>
<div class="meter animate orange">
<span style="width:20%"><span></span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" id="containers" data-hx-get="/user_containers" data-hx-trigger="load" data-hx-swap="innerHTML">
</div>
</div>
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" data-hx-get="/new_user_cards" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
</div>
</div>
<!-- HTMX Target-->
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Loading</h5>
</div>
<div class="modal-body text-center">
<div class="spinner-border"></div>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card-body">
<h4>Logs:</h4>
<div id="logView">
<pre>No logs available</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
<script src="/js/tabler.min.js"></script>
<script>
var options = {
chart: {
type: "line",
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [3, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: []
}, {
name: "RAM",
data: []
}],
tooltip: {
enabled: false
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
}
},
yaxis: {
min: 0,
max: 100,
labels: {
padding: 4
}
},
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}
</script>
<!-- SelectAll for the permissions modal -->
<script>
function selectAll(group) {
let checkboxes = document.getElementsByName(group);
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>

View file

@ -9,7 +9,7 @@
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('fonts/inter.css');
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
@ -18,15 +18,23 @@
}
</style>
</head>
<body class=" d-flex flex-column">
<body class="d-flex flex-column">
<script src="/js/demo-theme.js"></script>
<div class="page page-center">
<form class="container container-tight py-4" action="/register" method="POST" novalidate>
<div class="text-center">
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="100px">
</h1>
</div>
<div class="text-center mb-4">
<a href="#" class="navbar-brand navbar-brand-autodark d-none"><img src="/static/logo.svg" height="36" alt=""></a>
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
</h1>
</div>
<div class="card">
@ -81,84 +89,15 @@
<input type="text" class="form-control" id="secret" name="secret">
</div>
<div class="mb-2">
<label class="form-label">Avatar</label>
<div class="row g-2">
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="rus.jpg" class="form-imagecheck-input" checked/>
<span class="form-imagecheck-figure">
<img src="img/avatars/rus.jpg" alt="Rich Uncle Skeleton" title="Rich Uncle Skeleton" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="burns.jpg" class="form-imagecheck-input"/>
<span class="form-imagecheck-figure">
<img src="img/avatars/burns.jpg" alt="Montgomery Burns" title="Montgomery Burns" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="frank.jpg" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/frank.jpg" alt="Frank Grimes" title= "Frank Grimes" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="moe.jpg" class="form-imagecheck-input"/>
<span class="form-imagecheck-figure">
<img src="img/avatars/moe.jpg" alt="Moe Szyslak" title="Moe Szyslak" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="poochie.jpg" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/poochie.jpg" alt="Poochie" title="Poochie" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="skinner.jpg" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/skinner.jpg" alt="Seymour Skinner" title="Seymour Skinner" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="moleman.png" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/moleman.png" alt="Hans Moleman" title="Hans Moleman" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
<div class="col-6 col-sm-3">
<label class="form-imagecheck mb-2">
<input name="avatar" type="radio" value="duffman.png" class="form-imagecheck-input" />
<span class="form-imagecheck-figure">
<img src="img/avatars/duffman.png" alt="Duffman" title="Duffman" class="form-imagecheck-image" width="100px">
</span>
</label>
</div>
</div>
</div>
<div class="mb-2">
<!-- <div class="mb-2">
<label class="form-check">
<input type="checkbox" class="form-check-input" name="warning"/>
<span class="form-check-label">
I understand that<a href="https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet"> exposing DweebUI directly to the internet</a> is a bad idea.
</span>
</label>
</div>
</div> -->
</div>
</div>
@ -176,16 +115,14 @@
<div class="d-none d-md-flex">
<a href="?theme=dark" class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
<!-- Download SVG icon from http://tabler-icons.io/i/moon -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>
</a>
<a href="?theme=light" class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
<!-- Download SVG icon from http://tabler-icons.io/i/sun -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>
</a>
</div>
<button type="submit" class="btn btn-primary">Install</button>
<button type="submit" class="btn btn-primary">Register</button>
</div>
</div>

View file

@ -1,136 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('navbar.ejs') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Account Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<h4 class="subheader">Business settings</h4>
<div class="list-group list-group-transparent">
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Account</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">My Notifications</a>
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Connected Apps</a> -->
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center active">Settings</a>
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Billing & Invoices</a> -->
</div>
<h4 class="subheader mt-4">Experience</h4>
<div class="list-group list-group-transparent">
<a href="#" class="list-group-item list-group-item-action">Credits</a>
</div>
</div>
</div>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Settings</h2>
<p class="text-muted mb-4">Configure server below</p>
<div class="row align-items-center">
<div class="col">
<a href="./QuickConnect.bat" class="btn" download="QuickConnect.bat">
<!-- Download SVG icon from https://tabler-icons.io/i/brand-tabler-->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-windows" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M17.8 20l-12 -1.5c-1 -.1 -1.8 -.9 -1.8 -1.9v-9.2c0 -1 .8 -1.8 1.8 -1.9l12 -1.5c1.2 -.1 2.2 .8 2.2 1.9v12.1c0 1.2 -1.1 2.1 -2.2 1.9z"></path> <path d="M12 5l0 14"></path> <path d="M4 12l16 0"></path> </svg>
Windows QuickConnect
</a>
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Full Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">First Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Last Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="" readonly="">
</div>
<div class="col-auto">
<a href="#" class="btn">Change</a>
</div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
<h3 class="card-title mt-4">Public profile</h3>
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
you.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" >
<span class="form-check-label form-check-label-on">You're currently visible</span>
<span class="form-check-label form-check-label-off">You're
currently invisible</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('footer.ejs') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

120
views/settings.html Normal file
View file

@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Settings</h2>
<p class="text-muted mb-4">Configure server below</p>
<div class="row align-items-center">
<div class="col">
<!-- <a href="./QuickConnect.bat" class="btn" download="QuickConnect.bat">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-windows" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M17.8 20l-12 -1.5c-1 -.1 -1.8 -.9 -1.8 -1.9v-9.2c0 -1 .8 -1.8 1.8 -1.9l12 -1.5c1.2 -.1 2.2 .8 2.2 1.9v12.1c0 1.2 -1.1 2.1 -2.2 1.9z"></path> <path d="M12 5l0 14"></path> <path d="M4 12l16 0"></path> </svg>
Windows QuickConnect
</a> -->
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Full Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">First Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Last Name</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<h3 class="card-title mt-4">Email</h3>
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
<div>
<div class="row g-2">
<div class="col-auto">
<input type="text" class="form-control w-auto" value="" readonly="">
</div>
<div class="col-auto">
<a href="#" class="btn">Change</a>
</div>
</div>
</div>
<h3 class="card-title mt-4">Password</h3>
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
<div>
<a href="#" class="btn">
Set new password
</a>
</div>
<h3 class="card-title mt-4">Public profile</h3>
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
you.</p>
<div>
<label class="form-check form-switch form-switch-lg">
<input class="form-check-input" type="checkbox" >
<span class="form-check-label form-check-label-on">You're currently visible</span>
<span class="form-check-label form-check-label-off">You're
currently invisible</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

85
views/supporters.html Normal file
View file

@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<script src="/js/htmx.min.js"></script>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Supporters</h2>
<p class="text-muted mb-4">[Click to Thank]</p>
<div class="row align-items-center">
<div class="col">
<span type="button" class="avatar avatar-md bg-green-lt" hx-trigger="load, click" hx-post="/thank" hx-target="#count" name="MM" title="MM" style="margin-right: 5px;">mm</span>
<span type="button" class="avatar avatar-md bg-cyan-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="PD" title="PD" style="margin-right: 5px;">pd</span>
</div>
</div>
</div>
<div class="card-body">
<p class="text-muted mb-4">Thanks counter:</p>
<div class="row align-items-center">
<div class="col">
<span class="avatar avatar-md bg-yellow-lt" id="count" style="margin-right: 5px;">0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

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

View file

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

120
views/variables.html Normal file
View file

@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Settings</title>
<!-- CSS files -->
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<%- include('partials/sidebar.html') %>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-2">Variables</h2>
<p class="text-muted mb-4">Configure default variables below.</p>
<!-- <div class="row align-items-center">
<div class="col">
</div>
</div> -->
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Match 1</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Match 2</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Default</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Match 1</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Match 2</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Default</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
<div class="row mt-4">
<div class="col-md">
<div class="form-label">Match 1</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Match 2</div>
<input type="text" class="form-control" value="" readonly="">
</div>
<div class="col-md">
<div class="form-label">Default</div>
<input type="text" class="form-control" value="" readonly="">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
</html>

View file

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

163
views/volumes.html Normal file
View file

@ -0,0 +1,163 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>DweebUI - Volumes</title>
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<style>
@import url('/fonts/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style>
</head>
<body >
<div class="page">
<!-- Navbar -->
<%- include('partials/navbar.html') %>
<div class="page-wrapper">
<!-- Page header -->
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-deck row-cards">
<div class="col-12 mt-12">
<div class="card">
<form method="post">
<div class="card-header">
<h3 class="card-title">Docker Volumes</h3>
<div class="card-options btn-list">
<!-- <a href="#" class="btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</a> -->
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
New Volume
</a>
</div>
</div>
<div id="table-default" class="table-responsive">
<table class="table">
<%- volume_list %>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<button class="btn" type="submit" formaction="/removeVolume">Remove</button>
<!-- <span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Enable
</button>
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Disable
</button>
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
Delete
</button>
</div>
</span> -->
<p class="m-0 text-muted ms-auto"><%- volume_count %> Volumes</p>
</div>
</form>
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Volume</h5>
</div>
<div class="modal-body text-center">
<form method="post" action="/addVolume">
<div class="row g-2 align-items-end">
<div class="col-9">
<label class="form-label text-muted">Volume Name</label>
<input type="text" class="form-control" name="volume">
</div>
<div class="col-2">
<button type="submit" class="btn mt-2">Create</button>
</div>
</div>
<label class="mt-3 text-muted"><label class="text-danger">*</label>Name cannot contain spaces or special characters.</label>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/footer.html') %>
</div>
</div>
<!-- Libs JS -->
<script src="/libs/list.js/dist/list.min.js" defer></script>
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const list = new List('table-default', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity'
]
});
})
</script>
<script>
function selectAll() {
let checkboxes = document.getElementsByName('select');
if (checkboxes[0].checked == true) {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = true;
}
} else {
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = false;
}
}
}
</script>
</body>
</html>