Sfoglia il codice sorgente

Merge pull request #12 from lllllllillllllillll/dev

dev to main: v0.05 release
lllllllillllllillll 1 anno fa
parent
commit
8ac19ada26

+ 14 - 0
CHANGELOG.md

@@ -1,3 +1,17 @@
+## v0.05 ( Nov 17th 2023 )
+* Environment Variables and Labels are now unchecked by default.
+* Support for Docker volumes.
+* Fixed app uninstall.
+* Fixed Proxy Manager.
+* Updated functions to ignore the three DweebUI containers: DweebUI, DweebCache(redis), and DweebProxy(caddy).
+* Visual updates: Tabs for networks, images, and volumes. Added 'update' option in container drop-down.
+* Updated main.js to prevent javascript errors.
+* Fix for templates using 'set' instead of 'default' in environment variables.
+* Fixes for templates with no volumes or no labels.
+* New README.md.
+* New screenshots.
+* Automatically persists data in docker volumes if there is no bind mount.
+
 ## v0.04 (Nov 11th 2023)
 * Docker Image and Compose file available.
 * The containers DweebUI and DweebCache are hidden from the dashboard.

+ 60 - 36
README.md

@@ -1,57 +1,82 @@
 # DweebUI
+DweebUI is a simple Docker web interface created with javascript and node.js
 
+Pre-Pre-Pre-Pre-Pre Alpha v0.05 ( :fire: Experimental. Don't install on any servers you care about :fire: )
 
-DweebUI is a simple Docker web interface created with javascript and node.js
+[![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
+[![GitHub activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
 
-Pre-Pre-Pre-Pre-Pre Alpha v 0.04 ( :fire: Experimental. Don't install on any servers you care about :fire: )
 
 * I haven't used Github very much and I'm still new to javascript.
 * This is the first project I've ever released and I'm sure it's full of plenty of bugs and mistakes.
 * I probably should have waited a lot longer to share this :|
 
-Requirements: Docker
+<a href="https://raw.githubusercontent.com//lllllllillllllillll/DweebUI/main/screenshots/dashboard.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard.png" width="50%"/></a>
+
+<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="50%"/></a>
 
-![DweebUI](https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/DweebUI.png)
 
 ## Features
+* [x] Dashboard provides server metrics (cpu, ram, network, disk) and container controls on a single page.
+* [x] Light/Dark Mode.
+* [x] Easy to install app templates.
+* [x] Automatically persists data in docker volumes if bind mount isn't used. 
+* [x] Proxy manager for Caddy.
+* [x] Partial Portainer Template Support (Network Mode, Ports, Volumes, Enviroment Variables, Labels, Commands, Restart Policy, Nvidia Hardware Acceleration).
+* [x] Multi-User built-in.
+* [ ] User pages: Shortcuts, Requests, Support. (planned)
+* [x] Support for Windows, Linux, and MacOS.
+* [ ] Import compose files. (planned)
+* [x] Pure javascript. No frameworks or typescript.
+* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
+* [ ] Manage your Docker networks, images, and volumes. (planned)
+* [ ] Preset variables. (planned)
 
-* Dashboard provides server metrics (cpu, ram, network, disk) and container controls on a single page.
-* Partial Portainer Template Support (Network Mode, Ports, Volumes, Enviroment Variables, Labels, Commands, Restart Policy, Nvidia Hardware Acceleration).
-* Light/Dark Mode.
-* Support for multiple users is built in (but unused).
-* ~~Caddy Proxy Manager (very simple. proof of concept)~~ Broken since moving to docker container.
-* Pure javascript. No frameworks or typescript.
-* User data is stored in a sqlite database and uses browser sessions and a redis store for authentication.
-* Templates.json maintains compatability with Portainer, so you can use the template without needing to use DweebUI.
 
 ## Setup
 
 * Docker compose.yaml: 
 ```
 services:
-  dweebui:
-    container_name: DweebUI
-    image: lllllllillllllillll/dweebui:v0.04
-    ports:
-      - 8000:8000
-    depends_on:
-      - cache
-    links:
-      - cache
-    volumes:
-      - dweebui:/app
-      - /var/run/docker.sock:/var/run/docker.sock
-  cache:
-    container_name: DweebCache
-    image: redis:6.2-alpine
-    restart: always
-    command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
-    volumes: 
-      - cache:/data
+  dweebui:
+    container_name: DweebUI
+    image: lllllllillllllillll/dweebui:v0.05
+    restart: unless-stopped
+    ports:
+      - 8000:8000
+    depends_on:
+      - cache
+    links:
+      - cache
+    volumes:
+      - dweebui:/app
+      - ./caddyfiles/Caddyfile:/app/caddyfiles/Caddyfile
+      - ./caddyfiles/sites:/app/caddyfiles/sites
+      - /var/run/docker.sock:/var/run/docker.sock
+  cache:
+    container_name: DweebCache
+    image: redis:6.2-alpine
+    restart: always
+    command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
+    volumes: 
+      - cache:/data
+  proxy:
+    container_name: DweebProxy
+    image: caddy:2.4.5-alpine
+    depends_on:
+      - dweebui
+    restart: unless-stopped
+    network_mode: host
+    volumes:
+      - caddy:/data
+      - caddy:/config
+      - ./caddyfiles/Caddyfile:/etc/caddy/Caddyfile
+      - ./caddyfiles/sites:/etc/caddy/sites
+
 volumes:
-  dweebui:
-  cache:
+  dweebui:
+  cache:
+  caddy:
 ```
 
 * Using setup.sh: 
@@ -61,8 +86,7 @@ cd DweebUI
 chmod +x setup.sh
 sudo ./setup.sh
 ```
-Once setup is complete, I recommend installing Caddy first, then something like code-server. 
-The template is very rough. 
+
 
 ## Credit
 

+ 2 - 0
app.js

@@ -5,8 +5,10 @@ const app = express();
 const routes = require("./routes");
 
 const { serverStats, containerList, containerStats, containerAction } = require('./functions/system_information');
+const { RefreshSites } = require('./controllers/site_actions');
 
 let sent_list, clicked;
+app.locals.site_list = '';
 
 const redisClient = require('redis').createClient({
     url: 'redis://DweebCache:6379',

+ 1 - 0
caddyfiles/Caddyfile

@@ -0,0 +1 @@
+import ./sites/*

+ 7 - 6
components/appCard.js

@@ -139,11 +139,11 @@ function appCard(data) {
     try {
       let volumes = data.volumes[i];
       let volume_check = volumes ? "checked" : "";
-      let volume_bind = volumes.bind.split(":")[0] ? volumes.bind.split(":")[0] : "";
-      let volume_container = volumes.container.split(":")[0] ? volumes.container.split(":")[0] : "";
-      let volume_readwrite = "rw"
+      let volume_bind = volumes.bind ? volumes.bind : "";
+      let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
+      let volume_readwrite = "rw";
 
-      if ((volumes.readonly == true) || (volumes.container.endsWith(":ro"))) {
+      if (volumes.readonly == true) {
         volume_readwrite = "ro";
       }
 
@@ -166,8 +166,9 @@ function appCard(data) {
     // Get environment details
     try {
       let env = data.env[i];
-      let env_check = env ? "checked" : "";
+      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 : "";
@@ -193,7 +194,7 @@ function appCard(data) {
 
     try {
       let label = data.labels[i];
-      let label_check = label ? "checked" : "";
+      let label_check = "";
       let label_name = label.name ? label.name : "";
       let label_value = label.value ? label.value : "";
 

+ 159 - 77
components/dashCard.js

@@ -117,12 +117,10 @@ module.exports.dashCard = function dashCard(data) {
           <div class="card-stamp card-stamp-sm">
             <img heigh="150px" width="150px" 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/dweebui.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="buttonAction(this)" name="${name}" value="start" id="${state}" class="btn-action" title="Start" ${enabled}><!-- 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>
@@ -142,13 +140,13 @@ module.exports.dashCard = function dashCard(data) {
                     </a>
                     <div class="dropdown-menu dropdown-menu-end">
                       <a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#${name}_modal-details" href="#">Details</a>
-                      <a class="dropdown-item" href="#">Logs</a>
+                      <a class="dropdown-item" onclick="viewLogs(this)" name="${name}" data-bs-toggle="modal" data-bs-target="#${name}_logs" href="#">Logs</a>
                       <a class="dropdown-item" href="#">Edit</a>
+                      <a class="dropdown-item text-primary" href="#">Update</a>
                       <a class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#${name}_modal-danger" href="#">Remove</a>
                     </div>
                   </div>
                 </div>
-
               </div>
             </div>
           </div>
@@ -170,84 +168,168 @@ module.exports.dashCard = function dashCard(data) {
       </div>
     </div>
     
-    <div class="modal modal-blur fade deleteme" id="${name}_modal-danger" 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">
-                      <!-- Download SVG icon from http://tabler-icons.io/i/alert-triangle -->
-                      <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="uninstall" method="POST">
-                      <input type="text" class="form-control" name="service_name" value="${app_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" checked="" name="remove_volumes">
-                              </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" checked="" name="remove_image">
-                              </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" checked="" name="remove_backups">
-                              </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">
 
-                      </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="uninstall" class="btn btn-danger w-100" value="Uninstall"/>
-                          </div>
-                        </div>
-                      </div>
-                    </div>
+
+
+    
+
+
+    <div class="modal modal-blur fade deleteme" id="${name}_modal-danger" 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">
+            <!-- Download SVG icon from http://tabler-icons.io/i/alert-triangle -->
+            <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="uninstall" method="POST">
+            <input type="text" class="form-control" name="service_name" value="${app_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" checked="" name="remove_volumes">
+                    </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" checked="" name="remove_image">
+                    </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" checked="" name="remove_backups">
+                    </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="uninstall" class="btn btn-danger w-100" value="Uninstall"/>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    
+
+
+
+
+
+
+
+
+
+
+    <div class="modal modal-blur fade" id="${name}_logs" 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">Scrollable modal</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          </div>
+          <div class="modal-body">
+            <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas
+              eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+            <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue
+              laoreet rutrum faucibus dolor auctor.</p>
+            <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl
+              consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+            <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas
+              eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+            <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue
+              laoreet rutrum faucibus dolor auctor.</p>
+            <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl
+              consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+            <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas
+              eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+            <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue
+              laoreet rutrum faucibus dolor auctor.</p>
+            <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl
+              consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+            <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas
+              eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+            <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue
+              laoreet rutrum faucibus dolor auctor.</p>
+            <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl
+              consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+            <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas
+              eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+            <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue
+              laoreet rutrum faucibus dolor auctor.</p>
+            <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl
+              consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+            <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas
+              eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+            <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue
+              laoreet rutrum faucibus dolor auctor.</p>
+            <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl
+              consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+          </div>
+          <div class="modal-footer">
+            <button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
+            <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Save changes</button>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
     
     
     <div class="modal modal-blur fade" id="${name}_modal-details" tabindex="-1" role="dialog" aria-hidden="true">

+ 19 - 6
compose.yaml

@@ -1,10 +1,8 @@
 services:
   dweebui:
     container_name: DweebUI
-    build:
-      context: .
-    environment:
-      NODE_ENV: production
+    image: lllllllillllllillll/dweebui:v0.05
+    restart: unless-stopped
     ports:
       - 8000:8000
     depends_on:
@@ -13,6 +11,8 @@ services:
       - cache
     volumes:
       - dweebui:/app
+      - ./caddyfiles/Caddyfile:/app/caddyfiles/Caddyfile
+      - ./caddyfiles/sites:/app/caddyfiles/sites
       - /var/run/docker.sock:/var/run/docker.sock
   cache:
     container_name: DweebCache
@@ -21,7 +21,20 @@ services:
     command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
     volumes: 
       - cache:/data
-  
+  proxy:
+    container_name: DweebProxy
+    image: caddy:2.4.5-alpine
+    depends_on:
+      - dweebui
+    restart: unless-stopped
+    network_mode: host
+    volumes:
+      - caddy:/data
+      - caddy:/config
+      - ./caddyfiles/Caddyfile:/etc/caddy/Caddyfile
+      - ./caddyfiles/sites:/etc/caddy/sites
+
 volumes:
   dweebui:
-  cache:
+  cache:
+  caddy:

+ 2 - 0
controllers/apps.js

@@ -144,6 +144,8 @@ exports.Install = async function (req, res) {
     
     if (req.session.role == "admin") {
 
+        console.log(`Starting install for: ${req.body.name}`)
+
         install(req.body);
 
         let container_info = {

+ 2 - 1
controllers/dashboard.js

@@ -13,7 +13,8 @@ exports.Dashboard = async function (req, res) {
             name: user.first_name + ' ' + user.last_name,
             role: user.role,
             avatar: user.avatar,
-            isLoggedIn: true
+            isLoggedIn: true,
+            site_list: req.app.locals.site_list,
         });
     } else {
         // Redirect to the login page

+ 67 - 50
controllers/site_actions.js

@@ -1,7 +1,7 @@
 const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
 const { execSync } = require("child_process");
 const { siteCard } = require('../components/siteCard');
-
+const { containerExec } = require('../functions/system_information')
 
 exports.AddSite = async function (req, res) {
 
@@ -20,31 +20,54 @@ exports.AddSite = async function (req, res) {
         caddyfile += `\n\t}`
         caddyfile += `\n}`
 
-        // save caddyfile
-        writeFileSync(`/home/docker/caddy/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
         
+        // save caddyfile
+        writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
+
 
         // format caddyfile
-        execSync(`docker exec caddy caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`, (err, stdout, stderr) => {
-            if (err) { console.error(`error: ${err.message}`); return; }
-            if (stderr) { console.error(`stderr: ${stderr}`); return; }
-            if (stdout) { console.log(`stdout:\n${stdout}`); return; }
-            console.log(`Formatted ${domain}.Caddyfile`)
+        let format = {
+            container: 'DweebProxy',
+            command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
+        }
+        await containerExec(format, function(err, data) {
+            if (err) {
+                console.error(err);
+                return;
+            }
+            console.log(`Formatted ${domain}.Caddyfile`);
         });
         
+        ///////////////// convert caddyfile to json
+        let convert = {
+            container: 'DweebProxy',
+            command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
+        }
+        await containerExec(convert, function(err, data) {
+            if (err) {
+                console.error(err);
+                return;
+            }
+            console.log(`Converted ${domain}.Caddyfile to JSON`);
+        });
+
+        ////////////// reload caddy
+        let reload = {
+            container: 'DweebProxy',
+            command: `caddy reload --config /etc/caddy/Caddyfile`
+        }
+        await containerExec(reload, function(err, data) {
+            if (err) {
+                console.error(err);
+                return;
+            }
+            console.log(`Reloaded Caddy Config`);
+        });
 
         let site = siteCard(type, domain, host, port, 0);
 
-        // reload caddy config to enable new site
-        execSync(`docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, (err, stdout, stderr) => {
-            if (err) { console.error(`error: ${err.message}`); return; }
-            if (stderr) { console.error(`stderr: ${stderr}`); return; }
-            if (stdout) { console.log(`stdout:\n${stdout}`); return; }
-            console.log(`reloaded caddy config`)
-        });
+        req.app.locals.site_list += site;
 
-        // append the site to site_list.ejs
-        appendFileSync('./views/partials/site_list.ejs', site, function (err) { console.log(err) });
 
         res.redirect("/");
     } else {
@@ -61,22 +84,20 @@ exports.RemoveSite = async function (req, res) {
 
 
         for (const [key, value] of Object.entries(req.body)) {
-            console.log(`${key}: ${value}`);
-            execSync(`rm /home/docker/caddy/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
+
+            execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
                 if (err) { console.error(`error: ${err.message}`); return; }
                 if (stderr) { console.error(`stderr: ${stderr}`); return; }
                 console.log(`removed ${value}.Caddyfile`);
             });
+
         }
 
-        
-        // reload caddy config to disable sites
-        try {
-            execSync(`docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, (err, stdout, stderr) => {
-            if (err) { console.error(`error: ${err.message}`); return; }
-            if (stderr) { console.error(`stderr: ${stderr}`); return; }
-            console.log(`reloaded caddy config`)
-        }); } catch (error) { console.log("No sites to reload") }
+        let reload = {
+            container: 'DweebProxy',
+            command: `caddy reload --config /etc/caddy/Caddyfile`
+        }
+        await containerExec(reload);
 
         
         console.log('Removed Site(s)')
@@ -98,21 +119,15 @@ exports.RefreshSites = async function (req, res) {
 
 
         // Clear site_list.ejs
-        writeFileSync('./views/partials/site_list.ejs', '', function (err) {
-            if (err) {
-                console.log(err);
-            } else {
-                console.log('site_list.ejs has been cleared');
-            }
-        });
+        req.app.locals.site_list = "";
         
 
-        // check if /home/docker/caddy/sites/ contains any .json files, then delete them
+        // check if ./caddyfiles/sites contains any .json files, then delete them
         try {
-            let files = readdirSync('/home/docker/caddy/sites/');
+            let files = readdirSync('./caddyfiles/sites/');
             files.forEach(file => {
                 if (file.includes(".json")) {
-                    execSync(`rm /home/docker/caddy/sites/${file}`, (err, stdout, stderr) => {
+                    execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
                         if (err) { console.error(`error: ${err.message}`); return; }
                         if (stderr) { console.error(`stderr: ${stderr}`); return; }
                         console.log(`removed ${file}`);
@@ -122,23 +137,25 @@ exports.RefreshSites = async function (req, res) {
         } catch (error) { console.log("No .json files to delete") }
    
         // get list of Caddyfiles
-        let sites = readdirSync('/home/docker/caddy/sites/');
+        let sites = readdirSync('./caddyfiles/sites/');
 
-        sites.forEach(site_name => {
 
+        sites.forEach(site_name => {
             // convert the caddyfile of each site to json
-            execSync(`docker exec caddy caddy adapt --config /etc/caddy/sites/${site_name} --pretty >> /home/docker/caddy/sites/${site_name}.json`, (err, stdout, stderr) => {
-                if (err) { console.error(`error: ${err.message}`); return; }
-                if (stderr) { console.error(`stderr: ${stderr}`); return; }
-                console.log(`stdout:\n${stdout}`);
-            });
-            
-            // read the json file
-            let site_file = readFileSync(`/home/docker/caddy/sites/${site_name}.json`, 'utf8');
+            let convert = {
+                container: 'DweebProxy',
+                command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
+            }
+            containerExec(convert);
 
+            try {
+            // read the json file
+            let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
             // fix whitespace and parse the json file
             site_file = site_file.replace(/        /g, "  ");
             site_file = JSON.parse(site_file);
+            } catch (error) { console.log("No .json file to read") }
+
 
             // get the domain, type, host, and port from the json file
             try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
@@ -149,13 +166,13 @@ exports.RefreshSites = async function (req, res) {
             // build the site card
             let site = siteCard(type, domain, host, port, id);
 
-            // append the site card to site_list.ejs
-            appendFileSync('./views/partials/site_list.ejs', site, function (err) { console.log(err) });
+            // append the site card to site_list
+            req.app.locals.site_list += site;
             
             id++;
-
         });
         
+
         res.redirect("/");
     } else {
         // Redirect to the login page

+ 39 - 11
functions/package_manager.js

@@ -9,6 +9,7 @@ var DockerodeCompose = require('dockerode-compose');
 
 module.exports.install = async function (data) {
     
+        console.log(`[Start of install function]`);
 
         let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;        
         let { port0, port1, port2, port3, port4, port5 } = data;
@@ -16,6 +17,8 @@ module.exports.install = async function (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 docker_volumes = [];
+
         if (image.startsWith('https://')){
             mkdirSync(`./appdata/${name}`, { recursive: true });
             execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
@@ -71,9 +74,18 @@ module.exports.install = async function (data) {
                 compose_file += `\n    volumes:`
 
                 for (let i = 0; i < 6; i++) {
-                    if (data[`volume${i}`] == 'on') {
+
+                    // 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}`);
+                    } 
                 }
             }
 
@@ -122,6 +134,22 @@ module.exports.install = async function (data) {
                 }
             }
 
+    
+            // 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) });
@@ -154,16 +182,16 @@ module.exports.uninstall = async function (data) {
             var containerName = docker.getContainer(`${data.service_name}`);
 
             try {
-                containerName.stop(function (err, data) {
-                });
-            } catch { console.log('unable to stop container') }
-
-
-            try {
-                containerName.remove(function (err, data) {
-                });
-            } catch { console.log('unable to remove container') }
-
+                    containerName.stop(function (err, data) {
+                        if (data) {
+                            containerName.remove(function (err, data) {
+                            });
+                        }
+                    });
+                } catch { 
+                    containerName.remove(function (err, data) {
+                    });
+                }
 
         }
 

+ 70 - 37
functions/system_information.js

@@ -32,7 +32,7 @@ module.exports.containerList = async function () {
     for (const container of data) {
 
 
-        if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
+        if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache') && (container.Names[0].slice(1) != 'DweebProxy')) {
 
             let imageVersion = container.Image.split('/');
             let service = imageVersion[imageVersion.length - 1].split(':')[0];
@@ -40,20 +40,20 @@ module.exports.containerList = async function () {
             let containerId = docker.getContainer(container.Id);
             let containerInfo = await containerId.inspect();
 
-            let external_port = 0;
-            let internal_port = 0;
-
-            // Get ports
+            // Get ports //////////////////////////
             let ports_list = [];
-            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]
+            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);
                 }
-                ports_list.push(ports);
-            }
+            } catch { console.log('no ports') }
+
             for (let i = 0; i < 12; i++) {
                 if (ports_list[i] == undefined) {
                     let ports = {
@@ -64,20 +64,20 @@ module.exports.containerList = async function () {
                     }
                     ports_list[i] = ports;
                 }
-            }
+            } /////////////////////////////////////
 
 
-            // Get volumes.
+            // Get volumes ////////////////////////
             let volumes_list = [];
-            for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
-                let volumes = {
-                    check : 'checked',
-                    bind: value.split(':')[0],
-                    container: value.split(':')[1],
-                    readwrite: value.split(':')[2]
-                }
-                volumes_list.push(volumes);
-            }
+            try { for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
+                    let volumes = {
+                        check : 'checked',
+                        bind: value.split(':')[0],
+                        container: value.split(':')[1],
+                        readwrite: value.split(':')[2]
+                    }
+                    volumes_list.push(volumes);
+                }} catch { console.log('no volumes') }
             for (let i = 0; i < 12; i++) {
                 if (volumes_list[i] == undefined) {
                     let volumes = {
@@ -88,19 +88,19 @@ module.exports.containerList = async function () {
                     }
                     volumes_list[i] = volumes;
                 }
-            }
+            } /////////////////////////////////////
 
 
             // Get environment variables.
             let environment_variables = [];
-            for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
+            try { for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
                 let env = {
                     check : 'checked',
                     name: value.split('=')[0],
                     default: value.split('=')[1]
                 }
                 environment_variables.push(env);
-            }
+            }} catch { console.log('no env') }
             for (let i = 0; i < 12; i++) {
                 if (environment_variables[i] == undefined) {
                     let env = {
@@ -140,8 +140,8 @@ module.exports.containerList = async function () {
                 id: container.Id,
                 state: container.State,
                 image: container.Image,
-                external_port: external_port,
-                internal_port: internal_port, 
+                external_port: ports_list[0].external || 0,
+                internal_port: ports_list[0].internal || 0, 
                 ports: ports_list,
                 volumes: volumes_list,
                 environment_variables: environment_variables,
@@ -172,15 +172,17 @@ module.exports.containerStats = async function () {
 
     for (const container of data) {
 
-        const stats = await dockerContainerStats(container.Id);
-        let container_stat = {
-            name: container.Names[0].slice(1),
-            cpu: Math.round(stats[0].cpuPercent),
-            ram: Math.round(stats[0].memPercent)
-        }
+        if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache') && (container.Names[0].slice(1) != 'DweebProxy')) {
+            const stats = await dockerContainerStats(container.Id);
+            let container_stat = {
+                name: container.Names[0].slice(1),
+                cpu: Math.round(stats[0].cpuPercent),
+                ram: Math.round(stats[0].memPercent)
+            }
 
-        //push stats to an array
-        container_stats.push(container_stat);
+            //push stats to an array
+            container_stats.push(container_stat);
+        }
     }
     return container_stats;
 }
@@ -219,5 +221,36 @@ module.exports.containerAction = async function (data) {
 
 
 
+module.exports.containerExec = async function (data) {
+
+    let { container, command } = data;
+
+    var containerName = docker.getContainer(container);
+
+    var options = {
+        Cmd: ['/bin/sh', '-c', command],
+        AttachStdout: true,
+        AttachStderr: true,
+        Tty: true
+    };
+
+    containerName.exec(options, function (err, exec) {
+        if (err) return;
+
+        exec.start(function (err, stream) {
+            if (err) return;
+
+            containerName.modem.demuxStream(stream, process.stdout, process.stderr);
+
+            exec.inspect(function (err, data) {
+                if (err) return;
+
+              
+            });
+        });
+    });
+    
+}
+
 
 

+ 13 - 9
public/js/main.js

@@ -115,6 +115,10 @@ function buttonAction(button) {
   socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
 }
 
+function viewLogs(button) {
+  console.log(`trying to view logs for ${button.name}`);
+}
+
 socket.on('cards', (data) => {
 
   console.log('cards deleted');
@@ -143,31 +147,31 @@ socket.on('container_stats', (data) => {
 
   let {name, cpu, ram} = data;
 
-  // get cpu and ram array of the container from local storage
   var cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
   var ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
 
-  // if the cpu and ram arrays are null, create both arrays with 30 values of 0
   if (cpu_array == null) { cpu_array = Array(30).fill(0); }
   if (ram_array == null) { ram_array = Array(30).fill(0); }
 
-  // add the new cpu and ram values to the arrays, but limit the array to 30 values
   cpu_array.push(cpu);
   ram_array.push(ram);
-
+  
   cpu_array = cpu_array.slice(-30);
   ram_array = ram_array.slice(-30);
 
-  // save the arrays to local storage
   localStorage.setItem(`${name}_cpu`, JSON.stringify(cpu_array));
   localStorage.setItem(`${name}_ram`, JSON.stringify(ram_array));
 
   // replace the old chart with the new one
   let chart = document.getElementById(`${name}_chart`);
-  let newChart = document.createElement('div');
-  newChart.id = `${name}_chart`;
-  chart.parentNode.replaceChild(newChart, chart);
-  drawCharts(`#${name}_chart`, cpu_array, ram_array);  
+  if (chart) {
+    let newChart = document.createElement('div');
+    newChart.id = `${name}_chart`;
+    chart.parentNode.replaceChild(newChart, chart);
+    drawCharts(`#${name}_chart`, cpu_array, ram_array);
+  } else {
+    console.log(`Chart element with id ${name}_chart not found in the DOM`);
+  }
 });
 
 socket.on('install', (data) => {

+ 8 - 8
templates.json

@@ -910,18 +910,13 @@
           "name": "PGID",
           "set": "1000"
         },
-        {
-          "label": "UMASK_SET",
-          "name": "UMASK_SET",
-          "set": "000"
-        },
         {
           "label": "TZ",
           "name": "TZ",
           "set": "America/Chicago"
         }
       ],
-      "image": "linuxserver/deluge:latest",
+      "image": "lscr.io/linuxserver/deluge:latest",
       "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/deluge.png",
       "platform": "linux",
       "title": "Deluge",
@@ -934,6 +929,11 @@
         {
           "container": "/downloads"
         }
+      ],
+      "ports": [
+        "8112/tcp",
+        "6881/tcp",
+        "6881/udp"
       ]
     },
     {
@@ -3705,11 +3705,11 @@
           "container": "/data"
         },
         {
-          "bind": "/home/docker/caddy/Caddyfile",
+          "bind": "caddy/Caddyfile",
           "container": "/etc/caddy/Caddyfile"
         },
         {
-          "bind": "/home/docker/caddy/sites",
+          "bind": "caddy/sites",
           "container": "/etc/caddy/sites"
         }
       ]

+ 2 - 2
views/pages/dashboard.ejs

@@ -224,7 +224,7 @@
                       </thead>
                       <tbody> 
                         
-                        <%- include('../partials/site_list.ejs') %>
+                        <%- site_list %>
 
                       </tbody>
                     </table>
@@ -248,7 +248,7 @@
 
                   </form>
                                         
-                    <p class="m-0 text-muted ms-auto">Imported: /home/docker/caddy/Caddyfile</p>
+                    <p class="m-0 text-muted ms-auto">./caddyfiles/Caddyfile</p>
 
                   </div>
                 </div>

+ 1 - 1
views/partials/footer.ejs

@@ -24,7 +24,7 @@
           </li>
           <li class="list-inline-item">
             <a href="#" class="link-secondary" rel="noopener">
-              v0.04
+              v0.05
             </a>
           </li>
         </ul>

+ 39 - 0
views/partials/navbar.ejs

@@ -206,6 +206,45 @@
               </span>
             </a>
           </li>
+
+          <li class="nav-item">
+            <a class="nav-link" href="#">
+              <span
+                class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
+                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-world" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M3.6 9h16.8" /><path d="M3.6 15h16.8" /><path d="M11.5 3a17 17 0 0 0 0 18" /><path d="M12.5 3a17 17 0 0 1 0 18" /></svg>
+              </span>
+              <span class="nav-link-title">
+                Networks
+              </span>
+            </a>
+          </li>
+
+          <li class="nav-item">
+            <a class="nav-link" href="#">
+              <span
+                class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
+                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-augmented-reality" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M12 12.5l4 -2.5" /><path d="M8 10l4 2.5v4.5l4 -2.5v-4.5l-4 -2.5z" /><path d="M8 10v4.5l4 2.5" /></svg>
+              </span>
+              <span class="nav-link-title">
+                Images
+              </span>
+            </a>
+          </li>
+
+
+          <li class="nav-item">
+            <a class="nav-link" href="#">
+              <span
+                class="nav-link-icon d-md-none d-lg-inline-block">
+                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
+              </span>
+              <span class="nav-link-title">
+                Volumes
+              </span>
+            </a>
+          </li>
+
+
           <li class="nav-item dropdown">
             <a class="nav-link dropdown-toggle" href="#navbar-help" data-bs-toggle="dropdown"
               data-bs-auto-close="outside" role="button" aria-expanded="false">