Quellcode durchsuchen

Merge pull request #74 from lllllllillllllillll/dev

## v0.60 (June 9th 2024) - Permissions system and import templates
lllllllillllllillll vor 1 Jahr
Ursprung
Commit
8f97e17765
77 geänderte Dateien mit 4980 neuen und 15157 gelöschten Zeilen
  1. 5 2
      .dockerignore
  2. 3 1
      .gitignore
  3. 28 0
      CHANGELOG.md
  4. 1 1
      Dockerfile
  5. 31 38
      README.md
  6. 0 992
      components/appCard.js
  7. 0 314
      components/containerCard.js
  8. 0 893
      components/modal.js
  9. 0 371
      components/permissions_modal.js
  10. 26 26
      compose.yaml
  11. 2 1
      controllers/account.js
  12. 560 71
      controllers/apps.js
  13. 385 235
      controllers/dashboard.js
  14. 61 31
      controllers/images.js
  15. 11 24
      controllers/login.js
  16. 19 3
      controllers/networks.js
  17. 381 4
      controllers/portal.js
  18. 11 7
      controllers/register.js
  19. 2 1
      controllers/settings.js
  20. 2 3
      controllers/supporters.js
  21. 3 2
      controllers/syslogs.js
  22. 8 6
      controllers/users.js
  23. 58 43
      controllers/volumes.js
  24. 55 15
      database/models.js
  25. 0 207
      functions/install.js
  26. 270 120
      package-lock.json
  27. 11 9
      package.json
  28. 5 5
      public/css/tabler.min.css
  29. BIN
      public/images/avatars/burns.jpg
  30. BIN
      public/images/avatars/duffman.png
  31. BIN
      public/images/avatars/frank.jpg
  32. BIN
      public/images/avatars/moe.jpg
  33. BIN
      public/images/avatars/moleman.png
  34. BIN
      public/images/avatars/poochie.jpg
  35. BIN
      public/images/avatars/rus.jpg
  36. BIN
      public/images/avatars/skinner.jpg
  37. BIN
      public/img/add to zip.jpg
  38. 0 0
      public/img/dweebui.svg
  39. 0 0
      public/img/logo.png
  40. 82 57
      router/index.js
  41. BIN
      screenshots/dashboard1.png
  42. 11 63
      server.js
  43. 2 0
      templates/compose/.gitignore
  44. 0 5357
      templates/foss.json
  45. 7 7
      templates/json/default.json
  46. 0 5930
      templates/templates-bak.json
  47. 2 0
      templates/tmp/.gitignore
  48. 250 0
      utils/install.js
  49. 6 1
      utils/uninstall.js
  50. 3 3
      views/account.html
  51. 97 73
      views/apps.html
  52. 22 24
      views/dashboard.html
  53. 50 7
      views/images.html
  54. 2 2
      views/login.html
  55. 28 0
      views/modals/compose.html
  56. 956 0
      views/modals/details.html
  57. 20 0
      views/modals/import.html
  58. 728 0
      views/modals/json.html
  59. 13 0
      views/modals/learnmore.html
  60. 22 0
      views/modals/permissions.html
  61. 77 0
      views/modals/uninstall.html
  62. 4 4
      views/networks.html
  63. 22 0
      views/partials/appCard.html
  64. 80 0
      views/partials/containerFull.html
  65. 71 0
      views/partials/containerSimple.html
  66. 2 3
      views/partials/footer.html
  67. 22 20
      views/partials/navbar.html
  68. 0 1
      views/partials/sidebar.html
  69. 182 0
      views/partials/user_permissions.html
  70. 216 87
      views/portal.html
  71. 14 75
      views/register.html
  72. 3 3
      views/settings.html
  73. 3 3
      views/supporters.html
  74. 2 2
      views/syslogs.html
  75. 2 2
      views/users.html
  76. 3 3
      views/variables.html
  77. 38 5
      views/volumes.html

+ 5 - 2
.dockerignore

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

+ 3 - 1
.gitignore

@@ -1,3 +1,5 @@
 **/db.sqlite
 **/node_modules
-**/appdata
+**/appdata
+.github
+.git

+ 28 - 0
CHANGELOG.md

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

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM node:21-alpine
+FROM node:22-alpine
 ENV NODE_ENV=production
 WORKDIR /app
 COPY . /app

+ 31 - 38
README.md

@@ -1,49 +1,39 @@
-# DweebUI
-DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
-
-Alpha v0.40 ( :fire: Experimental :fire: )
-
-   
-[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
-
-[![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)
-[![Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
-
-* This is a personal project I started to get more familiar with Javascript and Node.js.
-* Some UI elements are placeholders and every version may have breaking changes.
-* 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/images/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).
-* [x] Manage your Docker networks, images, and volumes.
+
+* [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] Multi-User built-in.
-* [ ] Permissions system (in development).
-* [x] Support for Windows, Linux, and MacOS.
-* [ ] Docker compose import (in development).
+* [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.
-* [x] Automatically persists data in docker volumes if bind mount isn't used.
 * [ ] 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
 
@@ -53,10 +43,11 @@ version: "3.9"
 services:
   dweebui:
     container_name: dweebui
-    image: lllllllillllllillll/dweebui:v0.40
+    image: lllllllillllllillll/dweebui:v0.60
     environment:
       PORT: 8000
       SECRET: MrWiskers
+      HTTPS: false
     restart: unless-stopped
     ports:
       - 8000:8000
@@ -78,6 +69,8 @@ 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```.

+ 0 - 992
components/appCard.js

@@ -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>`;
-
-
-}

+ 0 - 314
components/containerCard.js

@@ -1,314 +0,0 @@
-export const containerCard = (data) => {
-  
-  let { name, service, state, external_port, internal_port, ports, link } = data;
-  let wrapped = name;
-  let disable = "";
-  let chartName = name.replace(/-/g, '');
-
-  // shorten long names
-  if (name.length > 13) { wrapped = name.slice(0, 10) + '...'; }
-  // disable buttons for dweebui
-  if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
-
-  if ( external_port == undefined ) { external_port = 0; }
-  if ( internal_port == undefined ) { internal_port = 0; }
-
-  let state_indicator = 'green';
-  if (state == 'exited') {
-      state = 'stopped';
-      state_indicator = 'red';
-  } else if (state == 'paused') {
-      state_indicator = 'orange';
-  }
-
-  let noChart = 'hx-swap="none"';
-  if (state == 'running') { noChart = ''; }
-
-  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 pt-1">
-      <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 class="btn-action" title="Start" data-hx-post="/start" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- player-play -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
-                  </button>
-                  <button class="btn-action" title="Stop" data-hx-post="/stop" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- player-stop -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
-                  </button>
-                  <button class="btn-action" title="Pause" data-hx-post="/pause" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- player-pause -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
-                  </button>
-                  <button class="btn-action" title="Restart" data-hx-post="/restart" data-hx-trigger="click" data-hx-target="#${name}state" name="${name}" id="${state}" ${disable}><!-- reload -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>                          
-                  </button>
-                  <div class="dropdown">
-                    <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="${name}" data-hx-get="/modal" data-hx-target="#modals-here" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
-                      <button class="dropdown-item text-secondary" name="${name}" id="logs" data-hx-get="/logs" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
-                      <button class="dropdown-item text-secondary" name="${name}" id="edit">Edit</button>
-                      <button class="dropdown-item text-primary" name="${name}" id="update" disabled="">Update</button>
-                      <button class="dropdown-item text-danger" name="${name}" id="remove" data-bs-toggle="modal" data-bs-target="#${name}_uninstall_modal" href="#">Remove</button>
-                    </div>
-                  </div>
-                  <div class="dropdown">
-                    <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="/hide" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="hide" value="hide">Hide</button>
-                      <button class="dropdown-item text-secondary" data-hx-post="/reset" data-hx-trigger="click" data-hx-swap="none" name="${name}" id="reset" value="reset">Reset View</button>
-                      <button class="dropdown-item text-secondary" name="${name}" id="permissions" data-hx-get="/modal" data-hx-target="#modals-here" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
-                    </div>
-                  </div>
-                </div>
-              </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">
-              <label id="${name}state">
-                <span class="text-${state_indicator} align-items-center lh-1">
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
-                    ${state}
-                </span>
-              </label>
-            </div>
-          </div>
-          
-          <script>
-              var ${chartName}chart = new ApexCharts(document.querySelector("#${chartName}_chart"), options);
-          </script>
-
-          <div class="chart-sm">
-            <div id="${chartName}_chart" data-hx-trigger="load, every 2s" data-hx-get="/chart" name="${chartName}" ${noChart}>
-              <script>
-                ${chartName}chart.render();
-              </script>
-            </div>
-            
-          </div>
-        </div>
-      </div>
-    </div>
-    <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">
-                    <div class="d-flex align-items-center">
-                      User:
-                      <select class="form-select ms-2">
-                        <option value="john_doe" selected hidden>John Doe</option>
-                        <option value="john_doe">John Doe</option>
-                        <option value="jane_doe">Jane Doe</option>
-                      </select>
-                    </div>
-                  </div>
-                </div>
-
-                <div class="row">
-                  <div class="col-9">
-                    <label class="row text-start">
-                      <span class="col">
-                        View
-                      </span>
-                    </label>
-                  </div>
-                  <div class="col-3">
-                    <label class="form-check form-check-single form-switch text-end">
-                      <input class="form-check-input" type="checkbox" name="remove_image">
-                    </label>
-                  </div>
-                </div>
-
-                <div class="row">
-                  <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>`;
-}

+ 0 - 893
components/modal.js

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

+ 0 - 371
components/permissions_modal.js

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

+ 26 - 26
docker-compose.yaml → compose.yaml

@@ -1,27 +1,27 @@
-version: "3.9"
-services:
-  dweebui:
-    container_name: dweebui
-    image: lllllllillllllillll/dweebui:v0.40
-    environment:
-      PORT: 8000
-      SECRET: MrWiskers
-    restart: unless-stopped
-    ports:
-      - 8000:8000
-    volumes:
-      - dweebui:/app
-      # Docker socket
-      - /var/run/docker.sock:/var/run/docker.sock
-      # Podman socket
-      #- /run/podman/podman.sock:/var/run/docker.sock
-
-    networks:
-      - dweebui_net
-
-volumes:
-  dweebui:
-
-networks:
-  dweebui_net:
+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
+      # 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

+ 2 - 1
controllers/account.js

@@ -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: '',
     });
 
 

+ 560 - 71
controllers/apps.js

@@ -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/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;
     }
-});
+  }
+
+  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>`
 
-export const Apps = (req, res) => {
+
+  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);
-
-    let prev = '/apps/' + (page-1);
-    let next = '/apps/' + (page+1);
-    if (page == 1) {
-        prev = '/apps/' + (page);
-    }
-    if (page == last_page) {
-        next = '/apps/' + (page);
-    }
+    app_count = compose_files.length;
+    last_page = Math.ceil(compose_files.length/28);
 
-    let apps_list = '';
-    for (let i = list_start; i < list_end && i < templates.length; i++) {
-        let app_card = appCard(templates[i]);
+    compose_files.forEach(file => {
+      if (file == '.gitignore') { return; }
 
-        apps_list += app_card;
+      let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
+      let compose_data = parse(compose);
+      let service_name = Object.keys(compose_data.services)
+      let container = compose_data.services[service_name].container_name;
+      let image = compose_data.services[service_name].image;
+
+      let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
+      appCard = appCard.replace(/AppName/g, service_name);
+      appCard = appCard.replace(/AppShortName/g, service_name);
+      appCard = appCard.replace(/AppDesc/g, 'Compose File');
+      appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
+      appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
+      appCard = appCard.replace(/AppType/g, 'compose');
+      apps_list += appCard;
+    });
+  } else {
+
+      let template_file = readFileSync(`./templates/json/${template_param}.json`);
+      let 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.avatar,
-        list_start: list_start + 1,
-        list_end: list_end,
-        app_count: templates.length,
-        prev: prev,
-        next: next,
-        apps_list: apps_list
-    });
 
+
+  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);
+  let template_param = req.params.template || 'default';
+
+  let template_file = readFileSync(`./templates/json/${template_param}.json`);
+
+  let templates = JSON.parse(template_file).templates;
+
+  templates = templates.sort((a, b) => {
+      if (a.name < b.name) { return -1; }
+  });
+
+  let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
+  <li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
+  <li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
+  <li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
+  <li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
+
+  
+  let list_start = (page-1)*28;
+  let list_end = (page*28);
+  let last_page = Math.ceil(templates.length/28);
+  let prev = '/apps/' + (page-1);
+  let next = '/apps/' + (page+1);
+  if (page == 1) { prev = '/apps/' + (page); }
+  if (page == last_page) { next = '/apps/' + (page); }
+
+
+  let apps_list = '';
+  let results = [];
+  let [cat_1, cat_2, cat_3] = ['','',''];
+
+  function searchTemplates(terms) {
+    terms = terms.toLowerCase();
+    for (let i = 0; i < templates.length; i++) {
+      if (templates[i].categories) {
+        if (templates[i].categories[0]) {
+          cat_1 = (templates[i].categories[0]).toLowerCase();
+        }
+        if (templates[i].categories[1]) {
+          cat_2 = (templates[i].categories[1]).toLowerCase();
+        }
+        if (templates[i].categories[2]) {
+          cat_3 = (templates[i].categories[2]).toLowerCase();
+        }
+      }
+      if ((templates[i].description.includes(terms)) || (templates[i].name.includes(terms)) || (templates[i].title.includes(terms)) || (cat_1.includes(terms)) || (cat_2.includes(terms)) || (cat_3.includes(terms))){
+          results.push(templates[i]);
+      }
+    }
+  }
+  searchTemplates(search);
+
+  for (let i = 0; i < results.length; i++) {
+    let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
+    let name = results[i].name || results[i].title.toLowerCase();
+    let desc = results[i].description.slice(0, 60) + "...";
+    let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
+    let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
+    let image = results[i].image;
+    let logo = results[i].logo;let categories = '';
+    // set data.catagories to 'other' if data.catagories is empty or undefined
+    if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
+      results[i].categories = ['Other'];
     }
-    if (page == last_page) {
-        next = '/apps?page=' + (page);
+    // 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');
 
-    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]);
-            }
+  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: ""
+        });
+      }
+
     }
-    searchTemplates(search);
+    
+    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);
 
-    for (let i = 0; i < results.length; i++) {
-        let app_card = appCard(results[i]);
-        apps_list += app_card;
+      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>`;
     
-    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 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');
+  });
+};

+ 385 - 235
controllers/dashboard.js

@@ -1,150 +1,441 @@
 import { Readable } from 'stream';
-import { Permission, Container } from '../database/models.js';
-import { modal } from '../components/modal.js';
-import { permissionsModal } from '../components/permissions_modal.js';
-import { setEvent, cpu, ram, tx, rx, disk, docker } from '../server.js';
+import { Permission, User } from '../database/models.js';
+import { docker } from '../server.js';
 import { dockerContainerStats } from 'systeminformation';
-import { containerCard } from '../components/containerCard.js';
+import { readFileSync } from 'fs';
+import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
+import { Op } from 'sequelize';
 
-let [ hidden, cardList ] = [ '', '' ];
+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 Logs = (req, res) => {
+// The page actions
+export const DashboardAction = async (req, res) => {
     let name = req.header('hx-trigger-name');
-    function containerLogs (data) {
-        return new Promise((resolve, reject) => {
+    let value = req.header('hx-trigger');
+    let action = req.params.action;
+    let modal = '';
+
+    switch (action) {
+        case 'permissions':
+            let title = name.charAt(0).toUpperCase() + name.slice(1);
+            let permissions_list = '';
+            let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
+            permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
+            permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
+            let users = await User.findAll({ attributes: ['username', 'UUID']});
+            for (let i = 0; i < users.length; i++) {
+                let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
+                let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+                if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
+                let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+                if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
+                if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
+                if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
+                if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
+                if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
+                if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
+                if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
+                if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
+                if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
+                user_permissions = user_permissions.replace(/EntryNumber/g, i);
+                user_permissions = user_permissions.replace(/EntryNumber/g, i);
+                user_permissions = user_permissions.replace(/EntryNumber/g, i);
+                user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+                user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+                user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+                user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+                user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+                user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+                permissions_list += user_permissions;
+            }
+            permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
+            res.send(permissions_modal);
+            return;
+        case 'uninstall':
+            modal = readFileSync('./views/modals/uninstall.html', 'utf8');
+            modal = modal.replace(/AppName/g, name);
+            res.send(modal);
+            return;
+        case 'details':
+            modal = readFileSync('./views/modals/details.html', 'utf8');
+            let details = await containerInfo(name);
+
+            modal = modal.replace(/AppName/g, details.name);
+            modal = modal.replace(/AppImage/g, details.image);
+
+            for (let i = 0; i <= 6; i++) {
+                modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
+                modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
+                modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
+                modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
+            }
+
+            for (let i = 0; i <= 6; i++) {
+                modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
+                modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
+                modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
+            }
+
+
+            for (let i = 0; i <= 19; i++) {
+                modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
+                modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
+            }
+
+            // console.log(details.env);
+            for (let i = 0; i <= 19; i++) {
+                modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
+                modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
+            }
+
+            res.send(modal);
+            return;
+        case 'updates':
+            res.send(newCards);
+            newCards = '';
+            return;
+        case 'card':
+            await userCards(req.session);
+            if (!req.session.container_list.find(c => c.container === name)) {
+                res.send('');
+                return;
+            } else {
+                let details = await containerInfo(name);
+                let card = await createCard(details);
+                res.send(card);
+                return;
+            }
+        case 'logs':
             let logString = '';
-            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; }
+            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 () {
-                    resolve(logString);
+                    res.send(`<pre>${logString}</pre>`);
                 });
             });
-        });
-    };
-    containerLogs(name).then((data) => {
-        res.send(`<pre>${data}</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;
+    }
 
-export const Modal = async (req, res) => {
-    let name = req.header('hx-trigger-name');
-    let id = req.header('hx-trigger');
-    if (id == 'permissions') {
-        let containerPermissions = await Permission.findAll({ where: {containerName: name}});
-        let form = permissionsModal();
-        res.send(form);
-        return;
+    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>`);
     }
-    let containerId = docker.getContainer(name);
-    let containerInfo = await containerId.inspect();
+
+    // 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(containerInfo.HostConfig.PortBindings)) {
-        let ports = {
-            check: 'checked',
-            external: value[0].HostPort,
-            internal: key.split('/')[0],
-            protocol: key.split('/')[1]
+        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);
         }
-        ports_list.push(ports);
-    }
     } catch {}
-    let external_port = ports_list[0]?.external || 0;
-    let internal_port = ports_list[0]?.internal || 0;
+    try {
+        external = ports_list[0].external;
+        internal = ports_list[0].internal;
+    } catch {}
+
+    // console.log(ports_list);
+    // console.log(info.HostConfig.PortBindings);
 
-    let container_info = {
-        name: containerInfo.Name.slice(1),
-        state: containerInfo.State.Status,
-        image: containerInfo.Config.Image,
-        external_port: external_port,
-        internal_port: internal_port,
+    // 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',
     }
-    let form = modal(container_info);
-    res.send(form);
+    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': value = cpu;
-            break;
-        case 'RAM': value = ram;
-            break;
-        case 'TX': value = tx;
+        case 'CPU': 
+            await currentLoad().then(data => { value = Math.round(data.currentLoad); });
             break;
-        case 'RX': value = rx;
+        case 'RAM': 
+            await mem().then(data => { value = Math.round((data.active / data.total) * 100); });
             break;
-        case 'DISK': value = disk;
+        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>`;
+    let info = `<div class="font-weight-medium"> <label class="cpu-text mb-1">${name} ${value}%</label></div>
+                <div class="cpu-bar meter animate ${color}"> <span style="width:${value}%"><span></span></span> </div>`;
     res.send(info);
 }
 
-
-export const Hide = async (req, res) => {
-    let name = req.header('hx-trigger-name');
-    let exists = await Container.findOne({ where: {name: name}});
-    if (!exists) {
-        const newContainer = await Container.create({ name: name, visibility: false, });
-    } else {
-        exists.update({ visibility: false });
-    }
-    setEvent(true, 'docker');
-    res.send("ok");
+// 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 Reset = async (req, res) => {
-    Container.update({ visibility: true }, { where: {} });
-    setEvent(true, 'docker');
-    res.send("ok");
+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;
+    }
 }
 
-
-let stats = {};
+// Container charts
 export const Chart = async (req, res) => {
     let name = req.header('hx-trigger-name');
-    // create an empty array if it doesn't exist
-    if (!stats[name]) {
-        stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) };
-    }
-    // get the stats
+    if (!stats[name]) { stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) }; }
     const info = await dockerContainerStats(name);
-    // update the arrays
     stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
     stats[name].ramArray.push(Math.round(info[0].memPercent));
-    // slice them down to the last 15 values
     stats[name].cpuArray = stats[name].cpuArray.slice(-15);
     stats[name].ramArray = stats[name].ramArray.slice(-15);
-    // replace the chart with the new data
     let chart = `
         <script>
             ${name}chart.updateSeries([{
@@ -154,145 +445,4 @@ export const Chart = async (req, res) => {
             }])
         </script>`
     res.send(chart);
-}
-
-// Get hidden containers
-async function getHidden() {
-    hidden = await Container.findAll({ where: {visibility:false}});
-    hidden = hidden.map((container) => container.name);
-}
-
-// Create list of docker containers cards
-async function containerCards() {
-    let list = '';
-    const allContainers = await docker.listContainers({ all: true });
-    for (const container of allContainers) {
-        if (!hidden.includes(container.Names[0].slice(1))) {
-
-            let imageVersion = container.Image.split('/');
-            let service = imageVersion[imageVersion.length - 1].split(':')[0];
-            let containerId = docker.getContainer(container.Id);
-            let containerInfo = await containerId.inspect();
-            let ports_list = [];
-            try {
-            for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
-                let ports = {
-                    check: 'checked',
-                    external: value[0].HostPort,
-                    internal: key.split('/')[0],
-                    protocol: key.split('/')[1]
-                }
-                ports_list.push(ports);
-            }
-            } catch {}
-
-            let external_port = ports_list[0]?.external || 0;
-            let internal_port = ports_list[0]?.internal || 0;
-
-            let container_info = {
-                name: container.Names[0].slice(1),
-                service: service,
-                id: container.Id,
-                state: container.State,
-                image: container.Image,
-                external_port: external_port,
-                internal_port: internal_port,
-                ports: ports_list,
-                link: 'localhost',
-            }
-            let card = containerCard(container_info);
-            list += card;
-        }
-    }
-    cardList = list;
-}
-
-export const Containers = async (req, res) => {
-    await getHidden();
-    await containerCards();
-    res.send(cardList);
-}
-
-
-function status (state) {
-    let status = `<span class="text-yellow align-items-center lh-1">
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
-                    ${state}
-                </span>`;
-    return status;
-}
-
-export const Start = (req, res) => {
-    let name = req.header('hx-trigger-name');
-    let state = req.header('hx-trigger');
-    if (state == 'stopped') {
-        var containerName = docker.getContainer(name);
-        containerName.start();
-    } else if (state == 'paused') {
-        var containerName = docker.getContainer(name);
-        containerName.unpause();
-    }
-
-    res.send(status('starting'));
-}
-
-export const Stop = (req, res) => {   
-    let name = req.header('hx-trigger-name');
-    let state = req.header('hx-trigger');
-    if (state != 'stopped') {
-        var containerName = docker.getContainer(name);
-        containerName.stop();
-    }
-    res.send(status('stopping'));
-}
-
-export const Pause = (req, res) => {
-    let name = req.header('hx-trigger-name');
-    let state = req.header('hx-trigger');
-    if (state == 'running') {
-        var containerName = docker.getContainer(name);
-        containerName.pause();
-    } else if (state == 'paused') {
-        var containerName = docker.getContainer(name);
-        containerName.unpause();
-    }
-    res.send(status('pausing'));
-}
-
-export const Restart = (req, res) => {   
-    let name = req.header('hx-trigger-name');
-    var containerName = docker.getContainer(name);
-    containerName.restart();
-    res.send(status('restarting'));
-}
-
-export const Installs = async (req, res) => {
-
-    let name = req.header('hx-trigger-name');
-    let all_containers = '';
-
-    await docker.listContainers({ all: true }).then(containers => {
-        containers.forEach(container => {
-            if (container.Names[0].slice(1) == name) {
-                return;
-            }
-        });
-
-        let install_info = {
-            name: name,
-            service: 'Service Name',
-            id: '',
-            state: '',
-            image: '',
-            external_port: 0,
-            internal_port: 0,
-            ports: '',
-            link: 'localhost',
-        }
-        let card = containerCard(install_info);
-        res.send(card);
-
-    });
-
-    
 }

+ 61 - 31
controllers/images.js

@@ -1,7 +1,50 @@
 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 = `
@@ -9,8 +52,8 @@ export const Images = async function(req, res) {
         <tr>
             <th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
             <th><label class="table-sort" data-sort="sort-name">Name</label></th>
-            <th><label class="table-sort" data-sort="sort-city">ID</label></th>
             <th><label class="table-sort" data-sort="sort-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>
@@ -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="${images[i].Id}" type="checkbox" aria-label="Select"></td>
-                <td class="sort-name">${images[i].RepoTags}</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"> - </td>
-                <td class="sort-score text-green"> - </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,33 +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: '',
     });
 
-}
-
-
-
-export const removeImage = async function(req, res) {
-    let images = req.body.select;
-
-    if (typeof(images) == 'string') {
-        images = [images];
-    }
-
-    for (let i = 0; i < images.length; i++) {
-        
-        if (images[i] != 'on') {
-            try {
-                console.log(`Removing image: ${images[i]}`);
-                let image = docker.getImage(images[i]);
-                await image.remove();
-            } catch (error) {
-                console.log(`Unable to remove image: ${images[i]}`);
-            }
-        }
-    }
-    res.redirect("/images");
 }

+ 11 - 24
controllers/login.js

@@ -2,30 +2,22 @@ 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}});
@@ -42,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,
@@ -63,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.",

+ 19 - 3
controllers/networks.js

@@ -3,6 +3,16 @@ 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 = `
@@ -24,12 +34,17 @@ export const Networks = async function(req, res) {
         // let date = new Date(images[i].Created * 1000);
         // let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
         
+        let status = '';
+        if (container_networks.includes(networks[i].Id)) {
+            status = `In use`;
+        }
+
         let details = `
             <tr>
                 <td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
                 <td class="sort-name">${networks[i].Name}</td>
                 <td class="sort-city">${networks[i].Id}</td>
-                <td class="sort-score text-green"> - </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>`
@@ -41,9 +56,10 @@ export const Networks = async function(req, res) {
     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: '',
     });
 }
 

+ 381 - 4
controllers/portal.js

@@ -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';
 
-export const Portal = (req, res) => {
+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);
+}

+ 11 - 7
controllers/register.js

@@ -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="/images/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.",
         });
     }
 }

+ 2 - 1
controllers/settings.js

@@ -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: '',
     });
 }

+ 2 - 3
controllers/supporters.js

@@ -2,8 +2,6 @@ import { User } from "../database/models.js";
 
 export const Supporters = async (req, res) => {
     
-    if (!req.session.UUID) return res.redirect("/login");
-
     let user = await User.findOne({ where: { UUID: req.session.UUID }});
     
 
@@ -14,7 +12,8 @@ export const Supporters = async (req, res) => {
         id: user.id,
         email: user.email,
         role: user.role,
-        avatar: user.avatar,
+        avatar: req.session.user.charAt(0).toUpperCase(),
+        alert: '',
     });
 
 

+ 3 - 2
controllers/syslogs.js

@@ -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: '',
     });
 
 }

+ 8 - 6
controllers/users.js

@@ -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: ''
     });
 
 }

+ 58 - 43
controllers/volumes.js

@@ -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><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">`
-
-
-
+    // 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) + '...';
-        }
-
-        if (mount.length > 70) {
-            mount = mount.slice(0, 67) + '...';
-        }
-        
-        // docker.df(volume.Mountpoint).then((data) => {
-        //     for (let key in data) {
-        //         console.log(data[key]);
-        //     }
-        // });
+        // Check if the volume is being used by any of the containers
+        let status = '';
+        if (container_volumes.some(volume => volume.source === mount)) { status = "In use"; }
+        if (container_volumes.some(volume => volume.source === mount && volume.type === 'volume')) { type = "Volume"; }
 
-    
-        let details = `
+        let row = `
         <tr>
             <td><input class="form-check-input m-0 align-middle" name="select" value="${name}" type="checkbox" aria-label="Select"></td>
+            <td class="sort-type">${type}</td>
             <td class="sort-name">${name}</td>
             <td class="sort-city">${mount}</td>
-            <td class="sort-score text-green"> - </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,19 +69,20 @@ 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 createVolume = async function(req, res) {
+export const addVolume = async function(req, res) {
     
-    let name = req.body.name;
+    let volume = req.body.volume;
 
     docker.createVolume({
-        Name: name
+        Name: volume
     });
     res.redirect("/volumes");
 }
@@ -101,4 +109,11 @@ export const removeVolume = async function(req, res) {
     }
 
     res.redirect("/volumes");
-}
+}
+
+
+// docker.df(volume.Name).then((data) => {
+//     for (let key in data) {
+//         console.log(data[key]);
+//     }
+// });

+ 55 - 15
database/models.js

@@ -110,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,
@@ -126,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
   },
 });
 
@@ -207,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,
+  }
 });

+ 0 - 207
functions/install.js

@@ -1,207 +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) => {
-
-        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) });
-                var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
-
-            } catch { 
-                const syslog = await Syslog.create({
-                    user: req.session.user,
-                    email: null,
-                    event: "App Installation",
-                    message: `${name} installation failed - error creating directory or compose file : ${err}`,
-                    ip: req.socket.remoteAddress
-                });
-             }
-
-            try {
-                (async () => {
-                    await compose.pull();
-                    await compose.up();
-
-                    const syslog = await Syslog.create({
-                        user: req.session.user,
-                        email: null,
-                        event: "App Installation",
-                        message: `${name} installed successfully`,
-                        ip: req.socket.remoteAddress
-                    });  
-                })();
-            } catch (err) {
-                const syslog = await Syslog.create({
-                    user: req.session.user,
-                    email: null,
-                    event: "App Installation",
-                    message: `${name} installation failed: ${err}`,
-                    ip: req.socket.remoteAddress
-                });
-            }
-        }
-    res.redirect('/');
-}

+ 270 - 120
package-lock.json

@@ -1,25 +1,27 @@
 {
   "name": "dweebui",
-  "version": "1.0.0",
+  "version": "0.60",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "dweebui",
-      "version": "1.0.0",
-      "license": "ISC",
+      "version": "0.60",
+      "license": "MIT",
       "dependencies": {
+        "adm-zip": "^0.5.12",
         "bcrypt": "^5.1.1",
         "dockerode": "^4.0.2",
         "dockerode-compose": "^1.4.0",
-        "ejs": "^3.1.9",
-        "express": "^4.18.2",
+        "ejs": "^3.1.10",
+        "express": "^4.19.2",
         "express-session": "^1.18.0",
-        "js-yaml": "^4.1.0",
         "memorystore": "^1.6.7",
-        "sequelize": "^6.37.1",
+        "multer": "^1.4.5-lts.1",
+        "sequelize": "^6.37.3",
         "sqlite3": "^5.1.7",
-        "systeminformation": "^5.22.0"
+        "systeminformation": "^5.22.9",
+        "yaml": "^2.4.2"
       }
     },
     "node_modules/@balena/dockerignore": {
@@ -46,7 +48,7 @@
         "npmlog": "^5.0.1",
         "rimraf": "^3.0.2",
         "semver": "^7.3.5",
-        "tar": "^6.1.11"
+        "tar": "^6.2.1"
       },
       "bin": {
         "node-pre-gyp": "bin/node-pre-gyp"
@@ -76,6 +78,18 @@
         "node": ">=10"
       }
     },
+    "node_modules/@npmcli/move-file/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "optional": true,
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -99,17 +113,17 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.20",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
-      "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
+      "version": "20.12.12",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
+      "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
     },
     "node_modules/@types/validator": {
-      "version": "13.11.9",
-      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
-      "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw=="
+      "version": "13.11.10",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz",
+      "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg=="
     },
     "node_modules/abbrev": {
       "version": "1.1.1",
@@ -128,6 +142,14 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/adm-zip": {
+      "version": "0.5.12",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
+      "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
     "node_modules/agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -186,6 +208,11 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
+    },
     "node_modules/aproba": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@@ -195,6 +222,7 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
       "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+      "deprecated": "This package is no longer supported.",
       "dependencies": {
         "delegates": "^1.0.0",
         "readable-stream": "^3.6.0"
@@ -290,12 +318,12 @@
       }
     },
     "node_modules/body-parser": {
-      "version": "1.20.1",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
-      "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+      "version": "1.20.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+      "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
       "dependencies": {
         "bytes": "3.1.2",
-        "content-type": "~1.0.4",
+        "content-type": "~1.0.5",
         "debug": "2.6.9",
         "depd": "2.0.0",
         "destroy": "1.2.0",
@@ -303,7 +331,7 @@
         "iconv-lite": "0.4.24",
         "on-finished": "2.4.1",
         "qs": "6.11.0",
-        "raw-body": "2.5.1",
+        "raw-body": "2.5.2",
         "type-is": "~1.6.18",
         "unpipe": "1.0.0"
       },
@@ -357,6 +385,11 @@
         "ieee754": "^1.1.13"
       }
     },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+    },
     "node_modules/buildcheck": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
@@ -366,6 +399,17 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
     "node_modules/bytes": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -415,6 +459,18 @@
         "node": ">=10"
       }
     },
+    "node_modules/cacache/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "optional": true,
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/cacache/node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -500,6 +556,47 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
+    "node_modules/concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "engines": [
+        "node >= 0.8"
+      ],
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "node_modules/concat-stream/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/concat-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "node_modules/concat-stream/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
     "node_modules/console-control-strings": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -525,9 +622,9 @@
       }
     },
     "node_modules/cookie": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
-      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
       "engines": {
         "node": ">= 0.6"
       }
@@ -537,15 +634,20 @@
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
     },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+    },
     "node_modules/cpu-features": {
-      "version": "0.0.9",
-      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
-      "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
+      "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
       "hasInstallScript": true,
       "optional": true,
       "dependencies": {
         "buildcheck": "~0.0.6",
-        "nan": "^2.17.0"
+        "nan": "^2.19.0"
       },
       "engines": {
         "node": ">=10.0.0"
@@ -628,9 +730,9 @@
       }
     },
     "node_modules/detect-libc": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
-      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+      "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
       "engines": {
         "node": ">=8"
       }
@@ -699,9 +801,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "node_modules/ejs": {
-      "version": "3.1.9",
-      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
-      "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+      "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
       "dependencies": {
         "jake": "^10.8.5"
       },
@@ -810,16 +912,16 @@
       }
     },
     "node_modules/express": {
-      "version": "4.18.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
-      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+      "version": "4.19.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
+      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
       "dependencies": {
         "accepts": "~1.3.8",
         "array-flatten": "1.1.1",
-        "body-parser": "1.20.1",
+        "body-parser": "1.20.2",
         "content-disposition": "0.5.4",
         "content-type": "~1.0.4",
-        "cookie": "0.5.0",
+        "cookie": "0.6.0",
         "cookie-signature": "1.0.6",
         "debug": "2.6.9",
         "depd": "2.0.0",
@@ -868,14 +970,6 @@
         "node": ">= 0.8.0"
       }
     },
-    "node_modules/express-session/node_modules/cookie": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
-      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
     "node_modules/express-session/node_modules/cookie-signature": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@@ -1018,6 +1112,7 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
       "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+      "deprecated": "This package is no longer supported.",
       "dependencies": {
         "aproba": "^1.0.3 || ^2.0.0",
         "color-support": "^1.1.2",
@@ -1139,9 +1234,9 @@
       "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
     },
     "node_modules/hasown": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
-      "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "dependencies": {
         "function-bind": "^1.1.2"
       },
@@ -1321,6 +1416,11 @@
       "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
       "optional": true
     },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1328,9 +1428,9 @@
       "optional": true
     },
     "node_modules/jake": {
-      "version": "10.8.7",
-      "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
-      "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
+      "version": "10.9.1",
+      "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
+      "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==",
       "dependencies": {
         "async": "^3.2.3",
         "chalk": "^4.0.2",
@@ -1634,14 +1734,14 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/mkdirp": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+      "dependencies": {
+        "minimist": "^1.2.6"
+      },
       "bin": {
         "mkdirp": "bin/cmd.js"
-      },
-      "engines": {
-        "node": ">=10"
       }
     },
     "node_modules/mkdirp-classic": {
@@ -1673,10 +1773,27 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "node_modules/multer": {
+      "version": "1.4.5-lts.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+      "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.0.0",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.4",
+        "object-assign": "^4.1.1",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
     "node_modules/nan": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
-      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
+      "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
       "optional": true
     },
     "node_modules/napi-build-utils": {
@@ -1693,9 +1810,9 @@
       }
     },
     "node_modules/node-abi": {
-      "version": "3.55.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.55.0.tgz",
-      "integrity": "sha512-uPEjtyh2tFEvWYt4Jw7McOD5FPcHkcxm/tHZc5PWaDB3JYq0rGFUbgaAK+CT5pYpQddBfsZVWI08OwoRfdfbcQ==",
+      "version": "3.62.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz",
+      "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==",
       "dependencies": {
         "semver": "^7.3.5"
       },
@@ -1755,6 +1872,7 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
       "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+      "deprecated": "This package is no longer supported.",
       "optional": true,
       "dependencies": {
         "delegates": "^1.0.0",
@@ -1768,6 +1886,7 @@
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
       "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+      "deprecated": "This package is no longer supported.",
       "optional": true,
       "dependencies": {
         "aproba": "^1.0.3 || ^2.0.0",
@@ -1787,6 +1906,7 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
       "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+      "deprecated": "This package is no longer supported.",
       "optional": true,
       "dependencies": {
         "are-we-there-yet": "^3.0.0",
@@ -1816,6 +1936,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
       "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+      "deprecated": "This package is no longer supported.",
       "dependencies": {
         "are-we-there-yet": "^2.0.0",
         "console-control-strings": "^1.1.0",
@@ -1903,14 +2024,14 @@
       "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
     },
     "node_modules/pg-connection-string": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
-      "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
+      "version": "2.6.4",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
+      "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA=="
     },
     "node_modules/prebuild-install": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
-      "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
+      "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
       "dependencies": {
         "detect-libc": "^2.0.0",
         "expand-template": "^2.0.3",
@@ -1932,6 +2053,11 @@
         "node": ">=10"
       }
     },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
     "node_modules/promise-inflight": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -2008,9 +2134,9 @@
       }
     },
     "node_modules/raw-body": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
-      "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
       "dependencies": {
         "bytes": "3.1.2",
         "http-errors": "2.0.0",
@@ -2101,12 +2227,9 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/semver": {
-      "version": "7.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
-      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
+      "version": "7.6.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
+      "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
       "bin": {
         "semver": "bin/semver.js"
       },
@@ -2114,22 +2237,6 @@
         "node": ">=10"
       }
     },
-    "node_modules/semver/node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/semver/node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-    },
     "node_modules/send": {
       "version": "0.18.0",
       "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
@@ -2172,9 +2279,9 @@
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
     },
     "node_modules/sequelize": {
-      "version": "6.37.1",
-      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.1.tgz",
-      "integrity": "sha512-vIKKzQ9dGp2aBOxQRD1FmUYViuQiKXSJ8yah8TsaBx4U3BokJt+Y2A0qz2C4pj08uX59qpWxRqSLEfRmVOEgQw==",
+      "version": "6.37.3",
+      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz",
+      "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==",
       "funding": [
         {
           "type": "opencollective",
@@ -2260,16 +2367,16 @@
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
     },
     "node_modules/set-function-length": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
-      "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dependencies": {
-        "define-data-property": "^1.1.2",
+        "define-data-property": "^1.1.4",
         "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.3",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.1"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -2281,11 +2388,11 @@
       "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
     },
     "node_modules/side-channel": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
-      "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bind": "^1.0.7",
         "es-errors": "^1.3.0",
         "get-intrinsic": "^1.2.4",
         "object-inspect": "^1.13.1"
@@ -2356,9 +2463,9 @@
       }
     },
     "node_modules/socks": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz",
-      "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==",
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+      "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
       "optional": true,
       "dependencies": {
         "ip-address": "^9.0.5",
@@ -2462,6 +2569,14 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -2514,9 +2629,9 @@
       }
     },
     "node_modules/systeminformation": {
-      "version": "5.22.0",
-      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.0.tgz",
-      "integrity": "sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==",
+      "version": "5.22.9",
+      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.9.tgz",
+      "integrity": "sha512-qUWJhQ9JSBhdjzNUQywpvc0icxUAjMY3sZqUoS0GOtaJV9Ijq8s9zEP8Gaqmymn1dOefcICyPXK1L3kgKxlUpg==",
       "os": [
         "darwin",
         "linux",
@@ -2539,9 +2654,9 @@
       }
     },
     "node_modules/tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "dependencies": {
         "chownr": "^2.0.0",
         "fs-minipass": "^2.0.0",
@@ -2593,6 +2708,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/tar/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/tar/node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -2644,6 +2770,11 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
+    },
     "node_modules/uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -2708,9 +2839,9 @@
       }
     },
     "node_modules/validator": {
-      "version": "13.11.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
-      "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
       "engines": {
         "node": ">= 0.10"
       }
@@ -2773,10 +2904,29 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
     "node_modules/yallist": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+    },
+    "node_modules/yaml": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
+      "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
     }
   }
 }

+ 11 - 9
package.json

@@ -1,7 +1,7 @@
 {
   "name": "dweebui",
-  "version": "1.0.0",
-  "description": "",
+  "version": "0.60",
+  "description": "Free and Open-Source WebUI For Managing Your Containers.",
   "main": "server.js",
   "type": "module",
   "scripts": {
@@ -9,19 +9,21 @@
     "start": "node server.js"
   },
   "keywords": [],
-  "author": "",
-  "license": "ISC",
+  "author": "lllllllillllllillll",
+  "license": "MIT",
   "dependencies": {
+    "adm-zip": "^0.5.12",
     "bcrypt": "^5.1.1",
     "dockerode": "^4.0.2",
     "dockerode-compose": "^1.4.0",
-    "ejs": "^3.1.9",
-    "express": "^4.18.2",
+    "ejs": "^3.1.10",
+    "express": "^4.19.2",
     "express-session": "^1.18.0",
-    "js-yaml": "^4.1.0",
     "memorystore": "^1.6.7",
-    "sequelize": "^6.37.1",
+    "multer": "^1.4.5-lts.1",
+    "sequelize": "^6.37.3",
     "sqlite3": "^5.1.7",
-    "systeminformation": "^5.22.0"
+    "systeminformation": "^5.22.9",
+    "yaml": "^2.4.2"
   }
 }

+ 5 - 5
public/css/tabler.min.css

@@ -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/images/avatars/burns.jpg


BIN
public/images/avatars/duffman.png


BIN
public/images/avatars/frank.jpg


BIN
public/images/avatars/moe.jpg


BIN
public/images/avatars/moleman.png


BIN
public/images/avatars/poochie.jpg


BIN
public/images/avatars/rus.jpg


BIN
public/images/avatars/skinner.jpg


BIN
public/img/add to zip.jpg


+ 0 - 0
public/images/dweebui.svg → public/img/dweebui.svg


+ 0 - 0
public/images/logo.png → public/img/logo.png


+ 82 - 57
router/index.js

@@ -1,81 +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, Start, Stop, Pause, Restart, Logs, Modal, Stats, Hide, Reset, Chart, Containers, Installs } 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, removeImage } from "../controllers/images.js";
+import { Images } from "../controllers/images.js";
 import { Networks, removeNetwork } from "../controllers/networks.js";
-import { Volumes, removeVolume } from "../controllers/volumes.js";
+import { Volumes, addVolume, removeVolume } from "../controllers/volumes.js";
 import { Account } from "../controllers/account.js";
 import { Variables } from "../controllers/variables.js";
 import { Settings } from "../controllers/settings.js";
 import { Supporters, Thanks } from "../controllers/supporters.js";
 import { Syslogs } from "../controllers/syslogs.js";
-import { Portal } from "../controllers/portal.js"
+import { Install } from "../utils/install.js"
+import { Uninstall } from "../utils/uninstall.js"
 
-// Auth middleware
-const auth = (req, res, next) => {
-    if (req.session.role == "admin") {
+// Permission Middleware
+const adminOnly = async (req, res, next) => {
+    if (req.session.role == 'admin') { next(); } 
+    else { res.redirect('/dashboard'); }
+}
+
+const sessionCheck = async (req, res, next) => {
+    if (req.session.user) { next(); }
+    else { res.redirect('/login'); }
+}
+
+const permissionCheck = async (req, res, next) => {
+    if (req.session.role == 'admin') { next(); return; }
+    let user = req.session.user;
+    let action = req.path.split("/")[2];
+    let trigger = req.header('hx-trigger-name');
+    const userAction = ['start', 'stop', 'restart', 'pause', 'uninstall', 'upgrade', 'edit', 'logs', 'view'];
+    const userPaths = ['card', 'updates', 'hide', 'reset', 'alert'];
+    if (userAction.includes(action)) {
+        let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
+        if (permission) { 
+            if (permission[action] == true) {
+                console.log(`User ${user} has permission to ${action} ${trigger}`);
+                next();
+                return;
+            }
+            else {
+                console.log(`User ${user} does not have permission to ${action} ${trigger}`);
+            }
+        }
+    } else if (userPaths.includes(action)) {
         next();
-    } else {
-        res.redirect("/login");
+        return;
     }
-};
-
-// Admin routes
-router.get("/", auth, Dashboard);
-router.get("/containers", auth, Containers);
-router.post("/start", auth, Start);
-router.post("/stop", auth, Stop);
-router.post("/pause", auth, Pause);
-router.post("/restart", auth, Restart);
-router.get("/logs", auth, Logs);
-router.get ("/modal", auth, Modal);
-router.get("/stats", auth, Stats);
-router.post("/hide", auth, Hide);
-router.post("/reset", auth, Reset);
-router.get("/chart", auth, Chart);
-router.get("/installs", auth, Installs);
-
-router.get("/images", auth, Images);
-router.post("/removeImage", auth, removeImage);
-
-router.get("/volumes", auth, Volumes);
-router.post("/removeVolume", auth, removeVolume);
-
-router.get("/networks", auth, Networks);
-router.post("/removeNetwork", auth, removeNetwork);
-
-router.get("/apps", auth, Apps);
-router.get("/apps/:page", auth, Apps);
-router.post("/apps", auth, appSearch);
-
-router.get("/users", auth, Users);
-router.get("/syslogs", auth, Syslogs);
-
-router.get("/variables", auth, Variables);
-router.get("/settings", auth, Settings);
-
-// User routes
-router.get("/portal", Portal);
-router.get("/account", Account);
-router.get("/supporters", Supporters);
-router.post("/thank", Thanks);
+}
 
+// 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("/logout", Logout);
+
+router.get("/", sessionCheck, Dashboard);
+router.get("/dashboard", sessionCheck, Dashboard);
+router.post("/dashboard/:action", sessionCheck, permissionCheck, DashboardAction);
+router.get("/sse", sessionCheck, SSE);
+router.post("/updatePermissions", adminOnly, UpdatePermissions);
+router.get("/stats", sessionCheck, Stats);
+router.get("/chart", sessionCheck, Chart);
+
+router.get("/images", adminOnly, Images);
+router.post("/images/:action", adminOnly, Images);
+
+router.get("/volumes", adminOnly, Volumes);
+router.post("/addVolume", adminOnly, addVolume);
+router.post("/removeVolume", adminOnly, removeVolume);
+
+router.get("/networks", adminOnly, Networks);
+router.post("/removeNetwork", adminOnly, removeNetwork);
+
+router.get("/apps/:page?/:template?", adminOnly, Apps);
+router.post("/apps", adminOnly, appSearch);
+router.get("/remove_template/:template", adminOnly, removeTemplate);
+
+router.get("/install_modal", adminOnly, InstallModal)
+router.get("/import_modal", adminOnly, ImportModal)
+router.get("/learn_more", adminOnly, LearnMore)
+router.post("/upload", adminOnly, Upload);
+
+router.get("/users", adminOnly, Users);
+router.get("/syslogs", adminOnly, Syslogs);
+
+router.get("/variables", adminOnly, Variables);
+router.get("/settings", adminOnly, Settings);
 
 
-// Functions
-import { Install } from "../functions/install.js"
-import { Uninstall } from "../functions/uninstall.js"
+router.get("/account", sessionCheck, Account);
+router.get("/supporters", sessionCheck, Supporters);
+router.post("/thank", sessionCheck, Thanks);
 
-router.post("/install", auth, Install);
-router.post("/uninstall", auth, Uninstall);

BIN
screenshots/dashboard1.png


+ 11 - 63
server.js

@@ -5,96 +5,44 @@ import ejs from 'ejs';
 import Docker from 'dockerode';
 import { router } from './router/index.js';
 import { sequelize } from './database/models.js';
-import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
-
-export var docker = new Docker();
-export { setEvent, cpu, ram, tx, rx, disk }
-
-const app = express();
-const MemoryStore = memorystore(session);
-const port = process.env.PORT || 8000;
-let [ cpu, ram, tx, rx, disk ] = [0, 0, 0, 0, 0];
-let [ event, eventType ] = [false, 'docker'];
+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.
     }
 });
 
 // Express middleware
+const app = express();
+const PORT = process.env.PORT || 8000;
 app.set('view engine', 'html');
 app.engine('html', ejs.renderFile);
 app.use([
     express.static('public'),
-    express.json(),
     express.urlencoded({ extended: true }),
     sessionMiddleware,
     router
 ]);
 
 // Initialize server
-app.listen(port, async () => {
-    async function init() {
+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( // check out that formatting
+        try { await sequelize.sync().then(
             () => { console.log('Synced Models: ✔️') }); }
             catch { console.log('Synced Models: ❌'); } }
         await init().then(() => { 
-            console.log(`Listening on http://localhost:${port}`);
-    });
-});
-
-function setEvent(value, type) {
-    event = value;
-    eventType = type;
-}
-
-// Server metrics
-let serverMetrics = async () => {
-    currentLoad().then(data => { 
-        cpu = Math.round(data.currentLoad); 
-    });
-    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; 
-    });
-}
-setInterval(serverMetrics, 1000);
-
-let sent_list = '';
-router.get('/sse_event', (req, res) => {
-    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', });
-    let eventCheck = setInterval(async() => {
-        let all_containers = '';
-        await docker.listContainers({ all: true }).then(containers => {
-            containers.forEach(container => {
-                all_containers += `${container.Names}: ${container.State}\n`;
-            });
-        });
-        if ((all_containers != sent_list) || (event == true)) {
-            sent_list = all_containers;
-            event = false;
-            res.write(`event: ${eventType}\n`);
-            res.write(`data: there was an event!\n\n`);
-        }
-    }, 1000);
-    req.on('close', () => {
-        clearInterval(eventCheck);
+            console.log(`Listening on http://localhost:${PORT}`);
     });
 });

+ 2 - 0
templates/compose/.gitignore

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

+ 0 - 5357
templates/foss.json

@@ -1,5357 +0,0 @@
-{
-  "version": "2",
-  "templates": [
-    {
-      "type": 1,
-      "name": "heimdall",
-      "title": "Heimdall",
-      "note": "",
-      "description": "Heimdall is a way to organise all those links to your most used web sites and web applications in a simple way. <a href='https://hub.docker.com/r/linuxserver/heimdall/' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/heimdall.png",
-      "image": "lscr.io/linuxserver/heimdall:latest",
-      "categories": [
-        "Dashboard"
-      ],
-      "ports": [
-        "8001:80/tcp",
-        "4001:443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/heimdall",
-          "container": "/config"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "homepage",
-      "title": "Homepage",
-      "description": "A modern (fully static, fast), secure (fully proxied), highly customizable application dashboard with integrations for more than 25 services and translations for over 15 languages. Easily configured via YAML files (or discovery via docker labels). <a href='https://github.com/benphelps/homepage/'>Github</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homepage.png",
-      "image": "ghcr.io/benphelps/homepage:latest",
-      "categories": [
-        "Dashboard"
-      ],
-      "ports": [
-        "3000:3000/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/homepage",
-          "container": "/app/config"
-        },
-        {
-          "bind": "/var/run/docker.sock",
-          "container": "/var/run/docker.sock:ro"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "jackett",
-      "title": "Jackett",
-      "description": "Jackett works as a proxy server: it translates queries from apps (Sonarr, SickRage, CouchPotato, Mylar, etc) into tracker-site-specific http queries, parses the html response, then sends results back to the requesting software. This allows for getting recent uploads (like RSS) and performing searches. <a href='https://github.com/Jackett/Jackett/'>Github</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/jackett.png",
-      "image": "linuxserver/jackett:latest",
-      "categories": [
-        "Downloaders",
-        "Tools"
-      ],
-      "ports": [
-        "9117:9117/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/jackett",
-          "container": "/config"
-        },
-        {
-          "bind": "/media",
-          "container": "/downloads"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Librespeed",
-      "name": "librespeed",
-      "note": "",
-      "description": "Librespeed is a very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers. No Flash, No Java, No Websocket, No Bullshit. <a href='https://github.com/librespeed/speedtest/'>Github</a>",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/librespeed.png",
-      "categories": [
-        "Networking",
-        "Tools"
-      ],
-      "image": "adolfintel/speedtest",
-      "ports": [
-        "81:81/tcp"
-      ],
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles",
-          "description": "Specify a timezone to use for example Europe/Amsterdam"
-        },
-        {
-          "name": "MODE",
-          "label": "MODE",
-          "default": "standalone",
-          "description": "Set the mode."
-        },
-        {
-          "name": "PASSWORD",
-          "label": "PASSWORD",
-          "default": "SOMEPASSWORD",
-          "description": "Password to access the stats page. If not set, stats page will not allow accesses."
-        },
-        {
-          "name": "WEBPORT",
-          "label": "WEBPORT",
-          "default": "81",
-          "description": "Allows choosing a custom port for the included web server."
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "name": "ombi",
-      "title": "Ombi",
-      "description": "Ombi allows you to host your own Plex Request and user management system. If you are sharing your Plex server with other users, allow them to request new content using an easy to manage interface. . [There is no official Ombi docker image. This one is created and maintained by <a href='https://hub.docker.com/r/linuxserver/ombi/' target='_blank'>linuxserver.io</a>]",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/ombi.png",
-      "image": "linuxserver/ombi:latest",
-      "categories": [
-        "Tools"
-      ],
-      "ports": [
-        "3579:3579/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/ombi",
-          "container": "/config"
-        },
-        {
-          "bind": "/etc/localtime",
-          "container": "/etc/localtime:ro"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "overseerr",
-      "title": "Overseerr",
-      "description": "Overseerr is a request management and media discovery tool built to work with your existing Plex ecosystem. <a href='https://overseerr.dev/' target='_blank'>Official Site</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/overseerr.png",
-      "image": "sctx/overseerr:latest",
-      "categories": [
-        "Multimedia",
-        "Tools"
-      ],
-      "ports": [
-        "5055/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/overseerr",
-          "container": "/app/config"
-        },
-        {
-          "container": "/etc/localtime",
-          "bind": "/etc/localtime:ro",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "prowlarr",
-      "title": "Prowlarr",
-      "description": "Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. ",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/prowlarr.png",
-      "image": "ghcr.io/linuxserver/prowlarr:develop",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "ports": [
-        "9696/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/prowlarr",
-          "container": "/config"
-        },
-        {
-          "bind": "/etc/localtime:ro",
-          "container": "/etc/localtime:ro",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles",
-          "preset": true
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "radarr",
-      "title": "Radarr",
-      "note": "There is no official Radarr docker image. This one is created and maintained by <a href='https://hotio.dev/containers/radarr/' target='_blank'>Hotio.dev</a>",
-      "description": "Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. <a href='https://radarr.video/#downloads-v3-docker' target='_blank'>Official Site</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/radarr.png",
-      "image": "ghcr.io/hotio/radarr",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "network": "AppBridge",
-      "ports": [
-        "7878:7878/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/radarr",
-          "container": "/config"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "readarr",
-      "title": "Readarr",
-      "description": "Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/readarr.png",
-      "image": "ghcr.io/linuxserver/readarr:nightly",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "ports": [
-        "8787/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/readarr",
-          "container": "/config"
-        },
-        {
-          "bind": "/media/downloads/ebooks",
-          "container": "/downloads"
-        },
-        {
-          "container": "/books",
-          "bind": "/media/storage/ebooks"
-        },
-        {
-          "container": "/blackhole",
-          "bind": "/media/temp/blackhole/ebooks"
-        },
-        {
-          "container": "/etc/localtime",
-          "bind": "/etc/localtime",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "scrutiny",
-      "title": "Scrutiny",
-      "description": "WebUI for smartd S.M.A.R.T monitoring",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/scrutiny.png",
-      "image": "analogj/scrutiny:latest",
-      "categories": [
-        "Monitoring"
-      ],
-      "ports": [
-        "8080/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/scrutiny/config/",
-          "bind": "/opt/mediadepot/apps/scrutiny"
-        },
-        {
-          "container": "/run/udev",
-          "bind": "/run/udev",
-          "readonly": true
-        }
-      ],
-      "env": [],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.scrutiny.loadbalancer.server.port",
-          "value": "8080"
-        },
-        {
-          "name": "traefik.http.routers.scrutiny.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.scrutiny.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "sonarr",
-      "title": "Sonarr",
-      "description": "Sonarr is a PVR for usenet and bittorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/sonarr.png",
-      "image": "linuxserver/sonarr:latest",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "ports": [
-        "8989/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/opt/mediadepot/apps/sonarr"
-        },
-        {
-          "container": "/downloads",
-          "bind": "/media/storage/downloads/tvshows"
-        },
-        {
-          "container": "/tv",
-          "bind": "/media/storage/tvshows"
-        },
-        {
-          "container": "/blackhole",
-          "bind": "/media/temp/blackhole/tvshows"
-        },
-        {
-          "container": "/etc/localtime",
-          "bind": "/etc/localtime",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        }
-      ],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.sonarr.loadbalancer.server.port",
-          "value": "8989"
-        },
-        {
-          "name": "traefik.http.routers.sonarr.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.sonarr.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "tautulli",
-      "title": "Tautulli",
-      "description": "A Python based monitoring and tracking tool for Plex Media Server.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/tautulli.png",
-      "image": "linuxserver/tautulli:latest",
-      "categories": [
-        "MediaServer:Other",
-        "Tools"
-      ],
-      "ports": [
-        "8181/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/opt/mediadepot/apps/tautulli"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.tautulli.loadbalancer.server.port",
-          "value": "8181"
-        },
-        {
-          "name": "traefik.http.routers.tautulli.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.tautulli.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "watchtower",
-      "title": "Watchtower",
-      "description": "Automatically update running Docker containers",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/watchtower.png",
-      "image": "containrrr/watchtower:latest",
-      "command": "--cleanup --label-enable",
-      "categories": [
-        "Monitoring"
-      ],
-      "volumes": [
-        {
-          "container": "/var/run/docker.sock",
-          "bind": "/var/run/docker.sock"
-        }
-      ],
-      "env": []
-    },
-    {
-      "type": 1,
-      "name": "wizarr",
-      "title": "Wizarr",
-      "description": "Wizarr is an advanced user invitation and management system for Jellyfin, Plex, Emby etc. ",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wizarr.png",
-      "image": "ghcr.io/wizarrrr/wizarr",
-      "categories": [
-        "Arr"
-      ],
-      "ports": [
-        "5690/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wizarr",
-          "container": "/data/database"
-        },
-        {
-          "bind": "/etc/localtime:ro",
-          "container": "/etc/localtime",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "APP_URL",
-          "label": "APP_URL",
-          "default": "https://wizarr.domain.com"
-        }
-      ],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.wizarr.loadbalancer.server.port",
-          "value": "5690"
-        },
-        {
-          "name": "traefik.http.routers.wizarr.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.wizarr.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "Transmission is designed for easy, powerful use. Transmission has the features you want from a BitTorrent client: encryption, a web interface, peer exchange, magnet links, DHT, \u00ef\u00bf\u00bdTP, UPnP and NAT-PMP port forwarding, webseed support, watch directories, tracker editing, global and per-torrent speed limits, and more.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/transmission:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/transmission.png",
-      "platform": "linux",
-      "ports": [
-        "9091/tcp",
-        "51413/tcp",
-        "51413/udp"
-      ],
-      "title": "Transmission",
-      "name": "transmission",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/transmission",
-          "container": "/config"
-        },
-        {
-          "bind": "/media",
-          "container": "/downloads"
-        },
-        {
-          "container": "/watch"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Video"
-      ],
-      "description": "Headless installation of Kodi.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/kodi-headless:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/kodi.png",
-      "platform": "linux",
-      "ports": [
-        "8080/tcp",
-        "9777/udp"
-      ],
-      "title": "Kodi-Headless",
-      "name": "kodi-headless",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config/.kodi"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Backup:",
-        "Cloud",
-        "Other",
-        "Tools"
-      ],
-      "description": "Syncthing replaces proprietary sync and cloud services with something open, trustworthy and decentralized. Your data is your data alone and you deserve to choose where it is stored, if it is shared with some third party and how it's transmitted over the Internet.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/syncthing:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/syncthing.png",
-      "platform": "linux",
-      "ports": [
-        "8384/tcp",
-        "21027/udp",
-        "22000/tcp"
-      ],
-      "title": "Syncthing",
-      "name": "syncthing",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/sync"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "mysql",
-      "title": "MySQL",
-      "note": "",
-      "description": "MySQL is the world's most popular open source database. With its proven performance, reliability and ease-of-use, MySQL has become the leading database choice for web-based applications, covering the entire range from personal projects and websites, via e-commerce and information services, all the way to high profile web properties including Facebook, Twitter, YouTube, Yahoo! and many more. <a href='https://www.mysql.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/mysql' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mysql.png",
-      "image": "mysql",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/mysql",
-          "container": "/var/lib/mysql",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "3306",
-          "container": "3306"
-        }
-      ],
-      "env": [
-        {
-          "name": "MYSQL_ROOT_PASSWORD",
-          "label": "Root Password",
-          "description": "Root password for MySQL",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "MYSQL_DATABASE",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "Deluge is a lightweight, Free Software, cross-platform BitTorrent client providing: Full Encryption, WebUI, Plugin System, Much more...",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "lscr.io/linuxserver/deluge:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/deluge.png",
-      "platform": "linux",
-      "title": "Deluge",
-      "name": "deluge",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/downloads"
-        }
-      ],
-      "ports": [
-        "8112/tcp",
-        "6881/tcp",
-        "6881/udp"
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Proxy"
-      ],
-      "description": "Nginx is a web server with a strong focus on high concurrency, performance and low memory usage. It can also act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/nginx:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nginx.png",
-      "platform": "linux",
-      "ports": [
-        "80/tcp",
-        "443/tcp"
-      ],
-      "title": "Nginx",
-      "name": "nginx",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "description": "Lidarr is a music collection manager for Usenet and BitTorrent users.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/lidarr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/lidarr.png",
-      "platform": "linux",
-      "ports": [
-        "8686/tcp"
-      ],
-      "title": "Lidarr",
-      "name": "lidarr",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/downloads"
-        },
-        {
-          "container": "/music"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "The qBittorrent project aims to provide an open-source software alternative to \u00ef\u00bf\u00bdTorrent. qBittorrent is based on the Qt toolkit and libtorrent-rasterbar library.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/qbittorrent:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/qbittorrent.png",
-      "platform": "linux",
-      "ports": [
-        "6881/tcp",
-        "6881/udp",
-        "8080/tcp"
-      ],
-      "title": "qbittorrent",
-      "name": "qbittorrent",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/downloads"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other"
-      ],
-      "description": "OpenVPN Access Server is a full featured secure network tunneling VPN software solution that integrates OpenVPN server capabilities, enterprise management capabilities, simplified OpenVPN Connect UI, and OpenVPN Client software packages that accommodate Windows, MAC, Linux, Android, and iOS environments.",
-      "env": [
-        {
-          "label": "INTERFACE",
-          "name": "INTERFACE",
-          "set": "eth0"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/openvpn-as:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/openvpn.png",
-      "platform": "linux",
-      "ports": [
-        "943/tcp",
-        "9443/tcp",
-        "1194/udp"
-      ],
-      "name": "openvpn-as",
-      "title": "OpenVPN Access Server",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other"
-      ],
-      "description": "Server version of minetest, a free, open source alternative to minecraft.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/minetest:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/minetest.png",
-      "platform": "linux",
-      "ports": [
-        "30000/udp"
-      ],
-      "title": "Minetest",
-      "name": "minetest",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config/.minetest"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Media"
-      ],
-      "description": "Airsonic is a free, web-based media streamer, providing ubiqutious access to your music. Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple players simultaneously, for instance to one player in your kitchen and another in your living room.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "CONTEXT_PATH",
-          "name": "CONTEXT_PATH",
-          "set": "airsonic"
-        },
-        {
-          "label": "JAVA_OPTS",
-          "name": "JAVA_OPTS",
-          "set": "-Xms256m -Xmx512m"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/airsonic:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/airsonic.png",
-      "platform": "linux",
-      "ports": [
-        "4040/tcp"
-      ],
-      "name": "airsonic",
-      "title": "Airsonic",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/music"
-        },
-        {
-          "container": "/playlists"
-        },
-        {
-          "container": "/podcasts"
-        },
-        {
-          "container": "/media"
-        },
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Web"
-      ],
-      "description": "This container sets up an Nginx webserver and reverse proxy with php support and a built-in letsencrypt client that automates free SSL server certificate generation and renewal processes. It also contains fail2ban for intrusion prevention.\r\n  \r\n  Before running this container, make sure that the url and subdomains are properly forwarded to this container's host.\r\n  \r\n  - Port 443 on the internet side of the router should be forwarded to this container's port 443.\r\n  - If you need a dynamic dns provider, you can use the free provider duckdns.org where the url will be yoursubdomain.duckdns.org and the subdomains    can be www,ftp,cloud\r\n  - The container detects changes to url and subdomains, revokes existing certs and generates new ones during start. \r\n  - It also detects changes to the DHLEVEL parameter and replaces the dhparams file.\r\n  \r\n  - If you'd like to password protect your sites, you can use htpasswd. Run the following command on your host to generate the htpasswd file docker exec -it letsencrypt htpasswd -c /config/nginx/.htpasswd &lt;username&gt;",
-      "env": [
-        {
-          "label": "EMAIL",
-          "name": "EMAIL",
-          "set": "-Xms256m -Xmx512m"
-        },
-        {
-          "label": "URL",
-          "name": "URL",
-          "set": "-Xms256m -Xmx512m"
-        },
-        {
-          "label": "SUBDOMAINS",
-          "name": "SUBDOMAINS",
-          "set": "www,"
-        },
-        {
-          "label": "ONLY_SUBDOMAINS",
-          "name": "ONLY_SUBDOMAINS",
-          "set": "false"
-        },
-        {
-          "label": "DHLEVEL",
-          "name": "DHLEVEL",
-          "set": "2048"
-        },
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "VALIDATION",
-          "name": "VALIDATION",
-          "set": "http"
-        },
-        {
-          "label": "DNSPLUGIN",
-          "name": "DNSPLUGIN",
-          "set": "http"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/letsencrypt:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/letsencrypt.png",
-      "platform": "linux",
-      "ports": [
-        "80/tcp",
-        "443/tcp"
-      ],
-      "title": "Let's Encrypt",
-      "name": "letsencrypt",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Cloud",
-        "Productivity",
-        "Tools",
-        "Other",
-        "Web"
-      ],
-      "description": "Nextcloud is an open source, self-hosted file sync and communication app platform. Access and sync your files, contacts, calendars and communicate and collaborate across your devices. You decide what happens with your data, where it is and who can access it!",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/nextcloud:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nextcloud.png",
-      "platform": "linux",
-      "ports": [
-        "443/tcp"
-      ],
-      "title": "Nextcloud",
-      "name": "nextcloud",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/data"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Apprise-api",
-      "name": "apprise-api",
-      "note": "",
-      "description": "Apprise-api takes advantage of Apprise through your network with a user-friendly API. * Send notifications to more then 65+ services. * An incredibly lightweight gateway to Apprise. * A production ready micro-service at your disposal. Apprise API was designed to easily fit into existing (and new) eco-systems that are looking for a simple notification solution.",
-      "categories": [
-        "Tools",
-        "Development"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/apprise-api.png",
-      "image": "linuxserver/apprise-api:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "8000:8000/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/apprise-api"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Audacity",
-      "name": "audacity",
-      "note": "",
-      "description": "Audacity is an easy-to-use, multi-track audio editor and recorder. Developed by a group of volunteers as open source. (https://www.audacityteam.org/)",
-      "platform": "linux",
-      "categories": [
-        "Media"
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/audacity.png",
-      "image": "linuxserver/audacity:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/audacity/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Bazarr",
-      "name": "bazarr",
-      "note": "",
-      "description": "Bazarr is a companion application to Sonarr and Radarr. It can manage and download subtitles based on your requirements. You define your preferences by TV show or movie and Bazarr takes care of everything for you.",
-      "categories": [
-        "Multimedia",
-        "Downloader",
-        "Arr"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/bazarr.png",
-      "image": "linuxserver/bazarr:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "6767:6767/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/bazarr",
-          "container": "/config"
-        },
-        {
-          "bind": "/home/docker/bazarr/movies",
-          "container": "/movies"
-        },
-        {
-          "container": "/tv",
-          "bind": "/home/docker/bazarr/tv"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Changedetection.io",
-      "name": "changedetection.io",
-      "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://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/changedetection.io.png",
-      "image": "linuxserver/changedetection.io:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "BASE_URL",
-          "label": "BASE_URL",
-          "default": "",
-          "description": "Specify the full URL (including protocol) when running behind a reverse proxy"
-        }
-      ],
-      "ports": [
-        "5000:5000/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/changedetection.io"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Code-server",
-      "name": "code-server",
-      "note": "",
-      "description": "Code-server is VS Code running on a remote server, accessible through the browser. - Code on your Chromebook, tablet, and laptop with a consistent dev environment. - If you have a Windows or Mac workstation, more easily develop for Linux. - Take advantage of large cloud servers to speed up tests, compilations, downloads, and more. - Preserve battery life when you're on the go. - All intensive computation runs on your server. - You're no longer running excess instances of Chrome. (https://coder.com)",
-      "categories": [
-        "Development"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/code-server.png",
-      "image": "linuxserver/code-server:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000"
-
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "PASSWORD",
-          "label": "PASSWORD",
-          "default": "password",
-          "description": "Optional web gui password, if `PASSWORD` or `HASHED_PASSWORD` is not provided, there will be no auth."
-        },
-        {
-          "name": "HASHED_PASSWORD",
-          "label": "HASHED_PASSWORD",
-          "default": "",
-          "description": "Optional web gui password, overrides `PASSWORD`, instructions on how to create it is below."
-        },
-        {
-          "name": "SUDO_PASSWORD",
-          "label": "SUDO_PASSWORD",
-          "default": "password",
-          "description": "If this optional variable is set, user will have sudo access in the code-server terminal with the specified password."
-        },
-        {
-          "name": "SUDO_PASSWORD_HASH",
-          "label": "SUDO_PASSWORD_HASH",
-          "default": "",
-          "description": "Optionally set sudo password via hash (takes priority over `SUDO_PASSWORD` var). Format is `$type$salt$hashed`."
-        },
-        {
-          "name": "PROXY_DOMAIN",
-          "label": "PROXY_DOMAIN",
-          "default": "code-server.mydomain",
-          "description": "If this optional variable is set, this domain will be proxied for subdomain proxying. See [Documentation](https://github.com/cdr/code-server/blob/master/docs/FAQ.md#sub-domains)"
-        },
-        {
-          "name": "DEFAULT_WORKSPACE",
-          "label": "DEFAULT_WORKSPACE",
-          "default": "/config/workspace",
-          "description": "If this optional variable is set, code-server will open this directory by default"
-        }
-      ],
-      "ports": [
-        "8443:8443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/code-server/config",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Dokuwiki",
-      "name": "dokuwiki",
-      "note": "",
-      "description": "Dokuwiki is a simple to use and highly versatile Open Source wiki software that doesn't require a database. It is loved by users for its clean and readable syntax. The ease of maintenance, backup and integration makes it an administrator's favorite. Built in access controls and authentication connectors make DokuWiki especially useful in the enterprise context and the large number of plugins contributed by its vibrant community allow for a broad range of use cases beyond a traditional wiki.  (https://www.dokuwiki.org/dokuwiki/)",
-      "categories": [
-        "Productivity",
-        "CMS"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dokuwiki.png",
-      "image": "linuxserver/dokuwiki:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/dokuwiki"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Doplarr",
-      "name": "doplarr",
-      "note": "",
-      "description": "Doplarr (https://github.com/kiranshila/Doplarr) is an *arr request bot for Discord.'",
-      "categories": [
-        "Arr"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/doplarr.png",
-      "image": "linuxserver/doplarr:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "DISCORD__TOKEN",
-          "label": "DISCORD__TOKEN",
-          "default": "",
-          "description": "Specify your discord bot token."
-        },
-        {
-          "name": "OVERSEERR__API",
-          "label": "OVERSEERR__API",
-          "default": "",
-          "description": "Specify your Overseerr API key. Leave blank if using Radarr/Sonarr."
-        },
-        {
-          "name": "OVERSEERR__URL",
-          "label": "OVERSEERR__URL",
-          "default": "http://localhost:5055",
-          "description": "Specify your Overseerr URL. Leave blank if using Radarr/Sonarr."
-        },
-        {
-          "name": "RADARR__API",
-          "label": "RADARR__API",
-          "default": "",
-          "description": "Specify your Radarr API key. Leave blank if using Overseerr."
-        },
-        {
-          "name": "RADARR__URL",
-          "label": "RADARR__URL",
-          "default": "http://localhost:7878",
-          "description": "Specify your Radarr URL. Leave blank if using Overseerr."
-        },
-        {
-          "name": "SONARR__API",
-          "label": "SONARR__API",
-          "default": "",
-          "description": "Specify your Sonarr API key. Leave blank if using Overseerr."
-        },
-        {
-          "name": "SONARR__URL",
-          "label": "SONARR__URL",
-          "default": "http://localhost:8989",
-          "description": "Specify your Sonarr URL. Leave blank if using Overseerr."
-        },
-        {
-          "name": "DISCORD__MAX_RESULTS",
-          "label": "DISCORD__MAX_RESULTS",
-          "default": "25",
-          "description": "Sets the maximum size of the search results selection"
-        },
-        {
-          "name": "DISCORD__REQUESTED_MSG_STYLE",
-          "label": "DISCORD__REQUESTED_MSG_STYLE",
-          "default": ":plain",
-          "description": "Sets the style of the request alert message. One of `:plain` `:embed` `:none`"
-        },
-        {
-          "name": "SONARR__QUALITY_PROFILE",
-          "label": "SONARR__QUALITY_PROFILE",
-          "default": "",
-          "description": "The name of the quality profile to use by default for Sonarr"
-        },
-        {
-          "name": "RADARR__QUALITY_PROFILE",
-          "label": "RADARR__QUALITY_PROFILE",
-          "default": "",
-          "description": "The name of the quality profile to use by default for Radarr"
-        },
-        {
-          "name": "SONARR__ROOTFOLDER",
-          "label": "SONARR__ROOTFOLDER",
-          "default": "",
-          "description": "The root folder to use by default for Sonarr"
-        },
-        {
-          "name": "RADARR__ROOTFOLDER",
-          "label": "RADARR__ROOTFOLDER",
-          "default": "",
-          "description": "The root folder to use by default for Radarr"
-        },
-        {
-          "name": "SONARR__LANGUAGE_PROFILE",
-          "label": "SONARR__LANGUAGE_PROFILE",
-          "default": "",
-          "description": "The name of the language profile to use by default for Sonarr"
-        },
-        {
-          "name": "OVERSEERR__DEFAULT_ID",
-          "label": "OVERSEERR__DEFAULT_ID",
-          "default": "",
-          "description": "The Overseerr user id to use by default if there is no associated discord account for the requester"
-        },
-        {
-          "name": "PARTIAL_SEASONS",
-          "label": "PARTIAL_SEASONS",
-          "default": "true",
-          "description": "Sets whether users can request partial seasons."
-        },
-        {
-          "name": "LOG_LEVEL",
-          "label": "LOG_LEVEL",
-          "default": ":info",
-          "description": "The log level for the logging backend. This can be changed for debugging purposes. One of trace `:debug` `:info` `:warn` `:error` `:fatal` `:report`"
-        },
-        {
-          "name": "JAVA_OPTS",
-          "label": "JAVA_OPTS",
-          "default": "",
-          "description": "For passing additional java options."
-        }
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/doplarr"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "EmbyStat",
-      "name": "embystat",
-      "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/lllllllillllllillll/DweebUI-Icons/main/embystat.png",
-      "image": "linuxserver/embystat:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "6555:6555/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/embystat"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Emulatorjs",
-      "name": "emulatorjs",
-      "note": "",
-      "description": "Emulatorjs - In browser web based emulation portable to nearly any device for many retro consoles. A mix of emulators is used between Libretro and EmulatorJS.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/emulatorjs.png",
-      "image": "linuxserver/emulatorjs:latest",
-      "categories": [
-        "Gaming"
-      ],
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "SUBFOLDER",
-          "label": "SUBFOLDER",
-          "default": "/",
-          "description": "Specify a subfolder for reverse proxies IE '/FOLDER/'"
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "80:80/tcp",
-        "4001:4001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/emulatorjs"
-        },
-        {
-          "container": "/data",
-          "bind": "/home/docker/emulatorjs/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Fail2ban",
-      "name": "fail2ban",
-      "note": "",
-      "description": "Fail2ban is a daemon to ban hosts that cause multiple authentication errors.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/fail2ban.png",
-      "image": "linuxserver/fail2ban:latest",
-      "network": "host",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "VERBOSITY",
-          "label": "VERBOSITY",
-          "default": "-vv",
-          "description": "Set the container log verbosity. Valid options are -v, -vv, -vvv, -vvvv, or leaving the value blank or not setting the variable."
-        }
-      ],
-      "ports": [
-        "80:80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/fail2ban/config"
-        },
-        {
-          "container": "/var/log:ro",
-          "bind": "/home/docker/fail2ban/var/log:ro"
-        },
-        {
-          "container": "/remotelogs/airsonic:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/airsonic:ro"
-        },
-        {
-          "container": "/remotelogs/apache2:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/apache2:ro"
-        },
-        {
-          "container": "/remotelogs/authelia:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/authelia:ro"
-        },
-        {
-          "container": "/remotelogs/emby:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/emby:ro"
-        },
-        {
-          "container": "/remotelogs/filebrowser:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/filebrowser:ro"
-        },
-        {
-          "container": "/remotelogs/homeassistant:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/homeassistant:ro"
-        },
-        {
-          "container": "/remotelogs/lighttpd:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/lighttpd:ro"
-        },
-        {
-          "container": "/remotelogs/nextcloud:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/nextcloud:ro"
-        },
-        {
-          "container": "/remotelogs/nginx:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/nginx:ro"
-        },
-        {
-          "container": "/remotelogs/nzbget:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/nzbget:ro"
-        },
-        {
-          "container": "/remotelogs/overseerr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/overseerr:ro"
-        },
-        {
-          "container": "/remotelogs/prowlarr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/prowlarr:ro"
-        },
-        {
-          "container": "/remotelogs/radarr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/radarr:ro"
-        },
-        {
-          "container": "/remotelogs/sabnzbd:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/sabnzbd:ro"
-        },
-        {
-          "container": "/remotelogs/sonarr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/sonarr:ro"
-        },
-        {
-          "container": "/remotelogs/unificontroller:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/unificontroller:ro"
-        },
-        {
-          "container": "/remotelogs/vaultwarden:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/vaultwarden:ro"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Homeassistant",
-      "name": "homeassistant",
-      "note": "",
-      "description": "Home Assistant Core is an open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.",
-      "categories": [
-        "Tools"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homeassistant.png",
-      "image": "linuxserver/homeassistant:latest",
-      "network": "host",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "8123:8123/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/homeassistant"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Jellyfin",
-      "name": "jellyfin",
-      "note": "",
-      "description": "Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/jellyfin.png",
-      "image": "linuxserver/jellyfin:latest",
-      "categories": [
-        "Media Server",
-        "LDAP"
-      ],
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "JELLYFIN_PublishedServerUrl",
-          "label": "JELLYFIN_PublishedServerUrl",
-          "default": "192.168.0.5",
-          "description": "Set the autodiscovery response domain or IP address."
-        }
-      ],
-      "network": "AppBridge",
-      "ports": [
-        "8096:8096/tcp",
-        "8920:8920/tcp",
-        "7359:7359/udp",
-        "1900:1900/udp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/jellyfin",
-          "container": "/config"
-        },
-        {
-          "bind": "/media/tvshows",
-          "container": "/data/tvshows"
-        },
-        {
-          "bind": "/media/movies",
-          "container": "/data/movies"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Kasm",
-      "name": "kasm",
-      "note": "",
-      "description": "Kasm workspaces is a docker container streaming platform for delivering browser-based access to desktops, applications, and web services. Kasm uses devops-enabled Containerized Desktop Infrastructure (CDI) to create on-demand, disposable, docker containers that are accessible via web browser. Example use-cases include Remote Browser Isolation (RBI), Data Loss Prevention (DLP), Desktop as a Service (DaaS), Secure Remote Access Services (RAS), and Open Source Intelligence (OSINT) collections. The rendering of the graphical-based containers is powered by the open-source project [KasmVNC](https://www.kasmweb.com/kasmvnc.html?utm_campaign=LinuxServer&utm_source=kasmvnc).",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/kasm.png",
-      "image": "linuxserver/kasm:latest",
-      "privileged": true,
-      "env": [
-        {
-          "name": "KASM_PORT",
-          "label": "KASM_PORT",
-          "default": "443",
-          "description": "Specify the port you bind to the outside for Kasm Workspaces."
-        },
-        {
-          "name": "DOCKER_HUB_USERNAME",
-          "label": "DOCKER_HUB_USERNAME",
-          "default": "USER",
-          "description": "Optionally specify a DockerHub Username to pull private images."
-        },
-        {
-          "name": "DOCKER_HUB_PASSWORD",
-          "label": "DOCKER_HUB_PASSWORD",
-          "default": "PASS",
-          "description": "Optionally specify a DockerHub password to pull private images."
-        },
-        {
-          "name": "DOCKER_MTU",
-          "label": "DOCKER_MTU",
-          "default": "1500",
-          "description": "Optionally specify the mtu options passed to dockerd."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/kasm/opt",
-          "container": "/opt"
-        },
-        {
-          "bind": "/home/docker/kasm/profiles",
-          "container": "/profiles"
-        },
-        {
-          "bind": "/dev/input",
-          "container": "/dev/input"
-        },
-        {
-          "bind": "/run/udev/data",
-          "container": "/run/udev/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Libreoffice",
-      "name": "libreoffice",
-      "note": "",
-      "description": "LibreOffice is a free and powerful office suite, and a successor to OpenOffice.org (commonly known as OpenOffice). Its clean interface and feature-rich tools help you unleash your creativity and enhance your productivity.",
-      "categories": [
-        "Productivity",
-        "Tools"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/libreoffice.png",
-      "image": "linuxserver/libreoffice:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/libreoffice/config",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Mastodon",
-      "name": "mastodon",
-      "note": "",
-      "description": "Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. (https://github.com/mastodon/mastodon/)",
-      "categories": [
-        "Communication"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mastodon.png",
-      "image": "linuxserver/mastodon:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "LOCAL_DOMAIN",
-          "label": "LOCAL_DOMAIN",
-          "default": "example.com",
-          "description": "This is the unique identifier of your server in the network. It cannot be safely changed later."
-        },
-        {
-          "name": "REDIS_HOST",
-          "label": "REDIS_HOST",
-          "default": "redis",
-          "description": "Redis server hostname"
-        },
-        {
-          "name": "REDIS_PORT",
-          "label": "REDIS_PORT",
-          "default": "6379",
-          "description": "Redis port"
-        },
-        {
-          "name": "DB_HOST",
-          "label": "DB_HOST",
-          "default": "db",
-          "description": "Postgres database hostname"
-        },
-        {
-          "name": "DB_USER",
-          "label": "DB_USER",
-          "default": "mastodon",
-          "description": "Postgres username"
-        },
-        {
-          "name": "DB_NAME",
-          "label": "DB_NAME",
-          "default": "mastodon",
-          "description": "Postgres db name"
-        },
-        {
-          "name": "DB_PASS",
-          "label": "DB_PASS",
-          "default": "mastodon",
-          "description": "Postgres password"
-        },
-        {
-          "name": "DB_PORT",
-          "label": "DB_PORT",
-          "default": "5432",
-          "description": "Portgres port"
-        },
-        {
-          "name": "ES_ENABLED",
-          "label": "ES_ENABLED",
-          "default": "false",
-          "description": "Enable or disable Elasticsearch (requires a separate ES instance)"
-        },
-        {
-          "name": "SECRET_KEY_BASE",
-          "label": "SECRET_KEY_BASE",
-          "default": "",
-          "description": "Browser session secret. Changing it will break all active browser sessions."
-        },
-        {
-          "name": "OTP_SECRET",
-          "label": "OTP_SECRET",
-          "default": "",
-          "description": "MFA secret. Changing it will break two-factor authentication."
-        },
-        {
-          "name": "VAPID_PRIVATE_KEY",
-          "label": "VAPID_PRIVATE_KEY",
-          "default": "",
-          "description": "Push notification private key. Changing it will break push notifications."
-        },
-        {
-          "name": "VAPID_PUBLIC_KEY",
-          "label": "VAPID_PUBLIC_KEY",
-          "default": "",
-          "description": "Push notification public key. Changing it will break push notifications."
-        },
-        {
-          "name": "SMTP_SERVER",
-          "label": "SMTP_SERVER",
-          "default": "mail.example.com",
-          "description": "SMTP server for email notifications"
-        },
-        {
-          "name": "SMTP_PORT",
-          "label": "SMTP_PORT",
-          "default": "25",
-          "description": "SMTP server port"
-        },
-        {
-          "name": "SMTP_LOGIN",
-          "label": "SMTP_LOGIN",
-          "default": "",
-          "description": "SMTP username"
-        },
-        {
-          "name": "SMTP_PASSWORD",
-          "label": "SMTP_PASSWORD",
-          "default": "",
-          "description": "SMTP password"
-        },
-        {
-          "name": "SMTP_FROM_ADDRESS",
-          "label": "SMTP_FROM_ADDRESS",
-          "default": "notifications@example.com",
-          "description": "From address for emails send from Mastodon"
-        },
-        {
-          "name": "S3_ENABLED",
-          "label": "S3_ENABLED",
-          "default": "false",
-          "description": "Enable or disable S3 storage of uploaded files"
-        },
-        {
-          "name": "WEB_DOMAIN",
-          "label": "WEB_DOMAIN",
-          "default": "mastodon.example.com",
-          "description": "This can be set if you want your server identifier to be different to the subdomain hosting Mastodon. See [https://docs.joinmastodon.org/admin/config/#basic](https://docs.joinmastodon.org/admin/config/#basic)"
-        },
-        {
-          "name": "ES_HOST",
-          "label": "ES_HOST",
-          "default": "es",
-          "description": "Elasticsearch server hostname"
-        },
-        {
-          "name": "ES_PORT",
-          "label": "ES_PORT",
-          "default": "9200",
-          "description": "Elasticsearch port"
-        },
-        {
-          "name": "ES_USER",
-          "label": "ES_USER",
-          "default": "elastic",
-          "description": "Elasticsearch username"
-        },
-        {
-          "name": "ES_PASS",
-          "label": "ES_PASS",
-          "default": "elastic",
-          "description": "Elasticsearch password"
-        },
-        {
-          "name": "S3_BUCKET",
-          "label": "S3_BUCKET",
-          "default": "",
-          "description": "S3 bucket hostname"
-        },
-        {
-          "name": "AWS_ACCESS_KEY_ID",
-          "label": "AWS_ACCESS_KEY_ID",
-          "default": "",
-          "description": "S3 bucket access key ID"
-        },
-        {
-          "name": "AWS_SECRET_ACCESS_KEY",
-          "label": "AWS_SECRET_ACCESS_KEY",
-          "default": "",
-          "description": "S3 bucket secret access key"
-        },
-        {
-          "name": "S3_ALIAS_HOST",
-          "label": "S3_ALIAS_HOST",
-          "default": "",
-          "description": "Alternate hostname for object fetching if you are front the S3 connections."
-        },
-        {
-          "name": "SIDEKIQ_ONLY",
-          "label": "SIDEKIQ_ONLY",
-          "default": "false",
-          "description": "Only run the sidekiq service in this container instance. For large scale instances that need better queue handling."
-        },
-        {
-          "name": "SIDEKIQ_QUEUE",
-          "label": "SIDEKIQ_QUEUE",
-          "default": "",
-          "description": "The name of the sidekiq queue to run in this container. See [notes](https://docs.joinmastodon.org/admin/scaling/#sidekiq-queues)."
-        },
-        {
-          "name": "SIDEKIQ_DEFAULT",
-          "label": "SIDEKIQ_DEFAULT",
-          "default": "false",
-          "description": "Set to `true` on the main container if you're running additional sidekiq instances. It will run the `default` queue."
-        },
-        {
-          "name": "SIDEKIQ_THREADS",
-          "label": "SIDEKIQ_THREADS",
-          "default": "5",
-          "description": "The number of threads for sidekiq to use. See [notes](https://docs.joinmastodon.org/admin/scaling/#sidekiq-threads)."
-        },
-        {
-          "name": "DB_POOL",
-          "label": "DB_POOL",
-          "default": "5",
-          "description": "The size of the DB connection pool, must be *at least* the same as `SIDEKIQ_THREADS`. See [notes](https://docs.joinmastodon.org/admin/scaling/#sidekiq-threads)."
-        }
-      ],
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/mastodon/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Phpmyadmin",
-      "name": "phpmyadmin",
-      "note": "",
-      "description": "Phpmyadmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB.",
-      "categories": [
-        "Tools"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/phpmyadmin.png",
-      "image": "linuxserver/phpmyadmin:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "PMA_ARBITRARY",
-          "label": "PMA_ARBITRARY",
-          "default": "1",
-          "description": "Set to `1` to allow you to connect to any server. Setting to `0` will only allow you to connect to specified hosts (See Application Setup)"
-        },
-        {
-          "name": "PMA_ABSOLUTE_URI",
-          "label": "PMA_ABSOLUTE_URI",
-          "default": "https://phpmyadmin.example.com",
-          "description": "Set the URL you will use to access the web frontend"
-        }
-      ],
-      "ports": [
-        "80:80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/phpmyadmin/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Pidgin",
-      "name": "pidgin",
-      "note": "",
-      "description": "Pidgin is a chat program which lets you log into accounts on multiple chat networks simultaneously. This means that you can be chatting with friends on XMPP and sitting in an IRC channel at the same time.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/pidgin.png",
-      "image": "linuxserver/pidgin:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/pidgin",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Remmina",
-      "name": "remmina",
-      "note": "",
-      "description": "Remmina is a remote desktop client written in GTK, aiming to be useful for system administrators and travellers, who need to work with lots of remote computers in front of either large or tiny screens. Remmina supports multiple network protocols, in an integrated and consistent user interface. Currently RDP, VNC, SPICE, NX, XDMCP, SSH and EXEC are supported.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/remmina.png",
-      "image": "linuxserver/remmina:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/remmina/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Sqlitebrowser",
-      "name": "sqlitebrowser",
-      "note": "",
-      "description": "DB Browser for SQLite is a high quality, visual, open source tool to create, design, and edit database files compatible with SQLite.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/sqlitebrowser.png",
-      "image": "linuxserver/sqlitebrowser:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/sqlitebrowser/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Swag",
-      "name": "swag",
-      "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://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/linuxserverio.png",
-      "image": "linuxserver/swag:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "URL",
-          "label": "URL",
-          "default": "yourdomain.url",
-          "description": "Top url you have control over (`customdomain.com` if you own it, or `customsubdomain.ddnsprovider.com` if dynamic dns)."
-        },
-        {
-          "name": "VALIDATION",
-          "label": "VALIDATION",
-          "default": "http",
-          "description": "Certbot validation method to use, options are `http` or `dns` (`dns` method also requires `DNSPLUGIN` variable set)."
-        },
-        {
-          "name": "SUBDOMAINS",
-          "label": "SUBDOMAINS",
-          "default": "www,",
-          "description": "Subdomains you'd like the cert to cover (comma separated, no spaces) ie. `www,ftp,cloud`. For a wildcard cert, set this *exactly* to `wildcard` (wildcard cert is available via `dns` validation only)"
-        },
-        {
-          "name": "CERTPROVIDER",
-          "label": "CERTPROVIDER",
-          "default": "",
-          "description": "Optionally define the cert provider. Set to `zerossl` for ZeroSSL certs (requires existing [ZeroSSL account](https://app.zerossl.com/signup) and the e-mail address entered in `EMAIL` env var). Otherwise defaults to Let's Encrypt."
-        },
-        {
-          "name": "DNSPLUGIN",
-          "label": "DNSPLUGIN",
-          "default": "cloudflare",
-          "description": "Required if `VALIDATION` is set to `dns`. Options are `acmedns`, `aliyun`, `azure`, `bunny`, `cloudflare`, `cpanel`, `desec`, `digitalocean`, `directadmin`, `dnsimple`, `dnsmadeeasy`, `dnspod`, `do`, `domeneshop`, `dreamhost`, `duckdns`, `dynu`, `freedns`, `gandi`, `gehirn`, `godaddy`, `google`, `google-domains`, `he`, `hetzner`, `infomaniak`, `inwx`, `ionos`, `linode`, `loopia`, `luadns`, `netcup`, `njalla`, `nsone`, `ovh`, `porkbun`, `rfc2136`, `route53`, `sakuracloud`, `standalone`, `transip`, and `vultr`. Also need to enter the credentials into the corresponding ini (or json for some plugins) file under `/config/dns-conf`."
-        },
-        {
-          "name": "PROPAGATION",
-          "label": "PROPAGATION",
-          "default": "",
-          "description": "Optionally override (in seconds) the default propagation time for the dns plugins."
-        },
-        {
-          "name": "EMAIL",
-          "label": "EMAIL",
-          "default": "",
-          "description": "Optional e-mail address used for cert expiration notifications (Required for ZeroSSL)."
-        },
-        {
-          "name": "ONLY_SUBDOMAINS",
-          "label": "ONLY_SUBDOMAINS",
-          "default": "false",
-          "description": "If you wish to get certs only for certain subdomains, but not the main domain (main domain may be hosted on another machine and cannot be validated), set this to `true`"
-        },
-        {
-          "name": "EXTRA_DOMAINS",
-          "label": "EXTRA_DOMAINS",
-          "default": "",
-          "description": "Additional fully qualified domain names (comma separated, no spaces) ie. `extradomain.com,subdomain.anotherdomain.org,*.anotherdomain.org`"
-        },
-        {
-          "name": "STAGING",
-          "label": "STAGING",
-          "default": "false",
-          "description": "Set to `true` to retrieve certs in staging mode. Rate limits will be much higher, but the resulting cert will not pass the browser's security test. Only to be used for testing purposes."
-        }
-      ],
-      "ports": [
-        "443:443/tcp",
-        "80:80/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/swag",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Wikijs",
-      "name": "wikijs",
-      "note": "Setup mysql, postgres, mariadb, mssql database first, or use the default sqlite.",
-      "description": "Wikijs is a modern, lightweight and powerful wiki app built on NodeJS. (https://wiki.js.org/)",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wikijs.png",
-      "image": "linuxserver/wikijs:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "DB_TYPE",
-          "label": "DB_TYPE",
-          "default": "sqlite",
-          "description": "Type of database (mysql, postgres, mariadb, mssql or sqlite)"
-        },
-        {
-          "name": "DB_HOST",
-          "label": "DB_HOST",
-          "default": "",
-          "description": "Hostname or IP of the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_PORT",
-          "label": "DB_PORT",
-          "default": "",
-          "description": "Port of the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_NAME",
-          "label": "DB_NAME",
-          "default": "",
-          "description": "Database name (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_USER",
-          "label": "DB_USER",
-          "default": "",
-          "description": "Username to connect to the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_PASS",
-          "label": "DB_PASS",
-          "default": "",
-          "description": "Password to connect to the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wikijs/config",
-          "container": "/config"
-        },
-        {
-          "bind": "/home/docker/wikijs/data",
-          "container": "/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Wireguard",
-      "name": "wireguard",
-      "note": "",
-      "description": "WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. WireGuard is designed as a general purpose VPN for running on embedded interfaces and super computers alike, fit for many different circumstances. Initially released for the Linux kernel, it is now cross-platform (Windows, macOS, BSD, iOS, Android) and widely deployable. It is currently under heavy development, but already it might be regarded as the most secure, easiest to use, and simplest VPN solution in the industry.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wireguard.png",
-      "image": "linuxserver/wireguard:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "SERVERURL",
-          "label": "SERVERURL",
-          "default": "wireguard.domain.com",
-          "description": "External IP or domain name for docker host. Used in server mode. If set to `auto`, the container will try to determine and set the external IP automatically"
-        },
-        {
-          "name": "SERVERPORT",
-          "label": "SERVERPORT",
-          "default": "51820",
-          "description": "External port for docker host. Used in server mode."
-        },
-        {
-          "name": "PEERS",
-          "label": "PEERS",
-          "default": "1",
-          "description": "Number of peers to create confs for. Required for server mode. Can also be a list of names: `myPC,myPhone,myTablet` (alphanumeric only)"
-        },
-        {
-          "name": "PEERDNS",
-          "label": "PEERDNS",
-          "default": "auto",
-          "description": "DNS server set in peer/client configs (can be set as `8.8.8.8`). Used in server mode. Defaults to `auto`, which uses wireguard docker host's DNS via included CoreDNS forward."
-        },
-        {
-          "name": "INTERNAL_SUBNET",
-          "label": "INTERNAL_SUBNET",
-          "default": "10.13.13.0",
-          "description": "Internal subnet for the wireguard and server and peers (only change if it clashes). Used in server mode."
-        },
-        {
-          "name": "ALLOWEDIPS",
-          "label": "ALLOWEDIPS",
-          "default": "0.0.0.0/0",
-          "description": "The IPs/Ranges that the peers will be able to reach using the VPN connection. If not specified the default value is: '0.0.0.0/0, ::0/0' This will cause ALL traffic to route through the VPN, if you want split tunneling, set this to only the IPs you would like to use the tunnel AND the ip of the server's WG ip, such as 10.13.13.1."
-        },
-        {
-          "name": "PERSISTENTKEEPALIVE_PEERS",
-          "label": "PERSISTENTKEEPALIVE_PEERS",
-          "default": "",
-          "description": "Set to `all` or a list of comma separated peers (ie. `1,4,laptop`) for the wireguard server to send keepalive packets to listed peers every 25 seconds. Useful if server is accessed via domain name and has dynamic IP. Used only in server mode."
-        },
-        {
-          "name": "LOG_CONFS",
-          "label": "LOG_CONFS",
-          "default": "true",
-          "description": "Generated QR codes will be displayed in the docker log. Set to `false` to skip log output."
-        }
-      ],
-      "ports": [
-        "51820:51820/udp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wireguard/config",
-          "container": "/config"
-        },
-        {
-          "bind": "/lib/modules",
-          "container": "/lib/modules"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Wireshark",
-      "name": "wireshark",
-      "note": "",
-      "description": "Wireshark is the world's foremost and widely-used network protocol analyzer. It lets you see what's happening on your network at a microscopic level and is the de facto (and often de jure) standard across many commercial and non-profit enterprises, government agencies, and educational institutions.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wireshark.png",
-      "image": "linuxserver/wireshark:latest",
-      "network": "host",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wireshark",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "categories": [
-        "Productivity"
-      ],
-      "description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases",
-      "env": [
-        {
-          "default": "/home/node/trilium-data",
-          "label": "TRILIUM_DATA_DIR",
-          "name": "TRILIUM_DATA_DIR"
-        },
-        {
-          "label": "PORT",
-          "name": "PORT"
-        }
-      ],
-      "image": "zadam/trilium:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/trilium.png",
-      "name": "trilium",
-      "platform": "linux",
-      "ports": [
-        "3388:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Trilium",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/trilium-data",
-          "container": "/home/node/trilium-data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Dashboard"
-      ],
-      "description": "Helps you organize your self-hosted services by making them accessible from a single place.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dashy.png",
-      "name": "dashy",
-      "platform": "linux",
-      "image": "lissy93/dashy:latest",
-      "title": "Dashy",
-      "restart_policy": "unless-stopped",
-      "type": 3,
-      "ports": [
-        "4000:80/tcp"
-      ]
-    },
-    {
-      "categories": [
-        "Dashboard"
-      ],
-      "description": "Flame is self-hosted startpage for your server. Its design is inspired (heavily) by SUI. Flame is very easy to setup and use. With built-in editors, it allows you to setup your very own application hub in no time - no file editing necessary.",
-      "image": "pawelmalak/flame",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/flame.png",
-      "name": "flame-dashboard",
-      "platform": "linux",
-      "ports": [
-        "5005:5005/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Flame-Dashboard",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/flame-dashboard",
-          "container": "/app/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Authentication"
-      ],
-      "description": "An open-source authentication and authorization server providing 2-factor authentication and single sign-on (SSO) for your applications via a web portal.",
-      "env": [
-        {
-          "label": "TZ",
-          "name": "TZ"
-        }
-      ],
-      "image": "authelia/authelia:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/authelia.png",
-      "name": "authelia",
-      "note": "Requires a configuration.yml file in order to work. Documentation is available <a href='https://docs.authelia.com/deployment/deployment-ha'>here</a>.",
-      "platform": "linux",
-      "ports": [
-        "9091:9091/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Authelia",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/authelia",
-          "container": "/etc/authelia/"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Security"
-      ],
-      "description": "This is a Bitwarden server API implementation written in Rust compatible with upstream Bitwarden clients*, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.",
-      "image": "vaultwarden/server:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/vaultwarden.png",
-      "name": "vaultwarden",
-      "note": "This project is not associated with the Bitwarden project nor 8bit Solutions LLC.",
-      "platform": "linux",
-      "ports": [
-        "80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Vaultwarden",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/vaultwarden",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Tools"
-      ],
-      "description": "A clientless remote desktop gateway.",
-      "image": "oznu/guacamole:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/guacamole.png",
-      "name": "guacamole",
-      "note": "The default login will be guacadmin/guacadmin. It is common practice to add a new admin user and remove the default user for Guacamole.",
-      "platform": "linux",
-      "ports": [
-        "8080:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Guacamole",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/guacamole",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Dashboard"
-      ],
-      "description": "A dead simple static HOMepage for your servER to keep your s ervices on hand, from a simple yaml configuration file.",
-      "image": "b4bz/homer:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homer.png",
-      "name": "homer",
-      "note": "This container requires a yml file within the config volume. See the documentation here https://github.com/bastienwirtz/homer",
-      "platform": "linux",
-      "ports": [
-        "8902:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Homer",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/homer/assets",
-          "container": "/www/assets"
-        },
-        {
-          "bind": "/home/docker/homer/config",
-          "container": "/www/config.yml"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Monitoring"
-      ],
-      "description": "Create agents that monitor and act on your behalf.",
-      "image": "huginn/huginn:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/huginn.png",
-      "name": "huginn",
-      "platform": "linux",
-      "ports": [
-        "3000:3000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Huginn",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/huginn",
-          "container": "/var/lib/mysql"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Security"
-      ],
-      "description": "Nginx Proxy Manager enables you to easily forward to your websites running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.",
-      "image": "jc21/nginx-proxy-manager",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nginx-proxy-manager.png",
-      "name": "nginx-proxy-manager",
-      "platform": "linux",
-      "env": [
-        {
-          "label": "DB_SQLITE_FILE",
-          "name": "DB_SQLITE_FILE",
-          "default": "/data/database.sqlite"
-        }
-      ],
-      "ports": [
-        "80:80/tcp",
-        "81:81/tcp",
-        "443:443/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Nginx Proxy Manager",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/nginx-proxy/data",
-          "container": "/data"
-        },
-        {
-          "bind": "/home/docker/nginx-proxy/letsencrypt",
-          "container": "/etc/letsencrypt"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "ownCloud is a self-hosted file sync and share server. It provides access to your data through a web interface, sync clients or WebDAV while providing a platform to view, sync and share across devices easily\u2014all under your control. ownCloud\u2019s open architecture is extensible via a simple but powerful API for applications and plugins and it works with any storage.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ"
-        },
-        {
-          "label": "OWNCLOUD_DOMAIN",
-          "name": "OWNCLOUD_DOMAIN"
-        },
-        {
-          "label": "DB_PASSWORD",
-          "name": "DB_PASSWORD"
-        },
-        {
-          "label": "ADMIN_USERNAME",
-          "name": "ADMIN_USERNAME"
-        },
-        {
-          "label": "ADMIN_PASSWORD",
-          "name": "ADMIN_PASSWORD"
-        },
-        {
-          "label": "PORT",
-          "name": "PORT"
-        }
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/owncloud.png",
-      "name": "owncloud",
-      "note": "The database user is owncloud and the database is owncloud.",
-      "platform": "linux",
-      "image": "owncloud/server:latest",
-      "title": "Owncloud",
-      "type": 3
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "A Linux network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole.",
-      "image": "pihole/pihole:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/pihole.png",
-      "name": "pihole",
-      "note": "When the installation is complete, navigate to your.ip.goes.here:1010/admin. Follow the article <a href='https://medium.com/@niktrix/getting-rid-of-systemd-resolved-consuming-port-53-605f0234f32f'>here</a> if you run into issues binding to port 53.",
-      "platform": "linux",
-      "ports": [
-        "53:53/tcp",
-        "53:53/udp",
-        "67:67/udp",
-        "1010:80/tcp",
-        "4443:443/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Pi-Hole",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/pihole",
-          "container": "/etc/pihole"
-        },
-        {
-          "bind": "/home/docker/pihole/DNS",
-          "container": "/etc/dnsmasq.d"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Tools"
-      ],
-      "description": "This container contains OpenVPN and Transmission with a configuration where Transmission is running only when OpenVPN has an active tunnel. It bundles configuration files for many popular VPN providers to make the setup easier.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "MULLVAD",
-          "description": "https://haugene.github.io/docker-transmission-openvpn/supported-providers/",
-          "label": "OPENVPN_PROVIDER",
-          "name": "OPENVPN_PROVIDER"
-        },
-        {
-          "default": "",
-          "label": "OPENVPN_USERNAME",
-          "name": "OPENVPN_USERNAME"
-        },
-        {
-          "default": "",
-          "label": "OPENVPN_PASSWORD",
-          "name": "OPENVPN_PASSWORD"
-        },
-        {
-          "default": "192.168.0.0/24",
-          "label": "LOCAL_NETWORK",
-          "name": "LOCAL_NETWORK"
-        }
-      ],
-      "image": "haugene/transmission-openvpn:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/transmission.png",
-      "name": "transmission-openvpn",
-      "note": "List of supported providers available <a href='https://haugene.github.io/docker-transmission-openvpn/supported-providers'/>here</a>.",
-      "platform": "linux",
-      "ports": [
-        "9091:9091/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Transmission-OpenVPN",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/data"
-        },
-        {
-          "bind": "/etc/localtime",
-          "container": "/etc/localtime"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "Self-hosted, ad-free, privacy-respecting Google metasearch engine.",
-      "image": "benbusby/whoogle-search:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/whoogle-search.png",
-      "name": "whoogle",
-      "platform": "linux",
-      "ports": [
-        "5001:5000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Whoogle",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/whoogle",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "Yacht is a web interface for managing docker containers with an emphasis on templating to provide 1 click deployments. Think of it like a decentralized app store for servers that anyone can make packages for.",
-      "image": "selfhostedpro/yacht:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/yacht.png",
-      "name": "yacht",
-      "platform": "linux",
-      "ports": [
-        "8001:8000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Yacht",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/yacht",
-          "container": "/config"
-        },
-        {
-          "bind": "/var/run/docker.sock",
-          "container": "/var/run/docker.sock"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "categories": [
-        "Tools"
-      ],
-      "title": "Paperless-ng",
-      "name": "paperless-ng",
-      "note": "",
-      "description": "Paperless-ng is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.'",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/paperless-ng.png",
-      "image": "linuxserver/paperless-ng:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for GroupID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for UserID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "Specify a timezone to use for example Europe/Amsterdam"
-        },
-        {
-          "name": "REDIS_URL",
-          "label": "REDIS_URL",
-          "default": "",
-          "description": "Specify an external redis instance to use. Can optionally include a port (`redis:6379`) and/or db (`redis/foo`). If left blank or not included, will use a built-in redis instance. If changed after initial setup will also require manual modification of /config/settings.py"
-        }
-      ],
-      "ports": [
-        "8000:8000/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/paperless-ng/config",
-          "container": "/config"
-        },
-        {
-          "container": "/data",
-          "bind": "/home/docker/paperless-ng/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "categories": [
-        "Downloaders",
-        "Multimedia"
-      ],
-      "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/lllllllillllllillll/DweebUI-Icons/main/youtubedl.png",
-      "name": "youtubedl-material",
-      "platform": "linux",
-      "ports": [
-        "17442:17442/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "YouTubeDL-Material",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/ytdlm",
-          "container": "/app/appdata"
-        },
-        {
-          "bind": "/home/docker/ytdlm/video",
-          "container": "/app/video"
-        },
-        {
-          "bind": "/home/docker/ytdlm/subscriptions",
-          "container": "/app/subscriptions"
-        },
-        {
-          "bind": "/home/docker/ytdlm/users",
-          "container": "/app/users"
-        },
-        {
-          "bind": "/home/docker/ytdlm/audio",
-          "container": "/app/audio"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools",
-        "Gaming"
-      ],
-      "description": "C# application with primary purpose of farming Steam cards from multiple accounts simultaneously.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "justarchi/archisteamfarm:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/archisteamfarm.png",
-      "name": "archisteamfarm",
-      "platform": "linux",
-      "ports": [
-        "1242:1242/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "ArchiSteamFarm",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/archiSteamFarm/config",
-          "container": "/app/config"
-        },
-        {
-          "bind": "/home/docker/archiSteamFarm/plugins",
-          "container": "/app/plugins"
-        },
-        {
-          "bind": "/home/docker/archiSteamFarm/logs",
-          "container": "/app/logs"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "ArchiveBox is a powerful, self-hosted internet archiving solution to collect, save, and view sites you want to preserve offline.",
-      "env": [
-        {
-          "default": "*",
-          "label": "ALLOWED_HOSTS",
-          "name": "ALLOWED_HOSTS"
-        },
-        {
-          "default": "750m",
-          "label": "MEDIA_MAX_SIZE",
-          "name": "MEDIA_MAX_SIZE"
-        },
-        {
-          "default": "true",
-          "label": "PUBLIC_INDEX",
-          "name": "PUBLIC_INDEX"
-        },
-        {
-          "default": "true",
-          "label": "PUBLIC_SNAPSHOTS",
-          "name": "PUBLIC_SNAPSHOTS"
-        },
-        {
-          "default": "false",
-          "label": "PUBLIC_ADD_VIEW",
-          "name": "PUBLIC_ADD_VIEW"
-        }
-      ],
-      "image": "archivebox/archivebox:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/archivebox.png",
-      "name": "archivebox",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "8002:8000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Archivebox",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/archivebox",
-          "container": "/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go. ",
-      "image": "caddy:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/caddy.png",
-      "name": "caddy",
-      "note": "Create firewall rule first: 'sudo ufw allow http' and 'sudo ufw allow https'. Add port 2019/tcp for admin endpoint",
-      "platform": "linux",
-      "network": "host",
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp",
-        "443:443/udp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Caddy",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "caddy",
-          "container": "/data"
-        },
-        {
-          "bind": "caddy",
-          "container": "/config"
-        },
-        {
-          "bind": "caddyfiles",
-          "container": "/etc/caddy"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Security",
-        "Tools"
-      ],
-      "description": "ClamAV is an open source antivirus engine for detecting trojans, viruses, malware & other malicious threats.",
-      "image": "clamav/clamav",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/clamav.png",
-      "name": "clamav",
-      "platform": "linux",
-      "ports": [
-        "3310:3310/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "ClamAV",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/etc/timezone",
-          "container": "/etc/timezone:ro"
-        },
-        {
-          "bind": "/home/docker/clamav/virus_definitions",
-          "container": "/var/lib/clamav"
-        },
-        {
-          "bind": "/home/docker",
-          "container": "/scandir"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Arr",
-        "Tools"
-      ],
-      "description": "FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.",
-      "env": [
-        {
-          "default": "info",
-          "label": "LOG_LEVEL",
-          "name": "LOG_LEVEL"
-        },
-        {
-          "default": "false",
-          "label": "LOG_HTML",
-          "name": "LOG_HTML"
-        },
-        {
-          "default": "none",
-          "label": "CAPTCHA_SOLVER",
-          "name": "CAPTCHA_SOLVER"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        }
-      ],
-      "image": "ghcr.io/flaresolverr/flaresolverr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/flaresolverr.png",
-      "name": "flaresolverr",
-      "platform": "linux",
-      "ports": [
-        "8191:8191/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "FlareSolverr",
-      "type": 1,
-      "note": ""
-    },
-    {
-      "categories": [
-        "CMS"
-      ],
-      "description": "Ghost is a free and open source blogging platform written in JavaScript and distributed under the MIT License, designed to simplify the process of online publishing for individual bloggers as well as online publications.",
-      "env": [
-        {
-          "default": "development",
-          "label": "NODE_ENV",
-          "name": "NODE_ENV"
-        },
-        {
-          "default": "http://localhost/",
-          "label": "url",
-          "name": "url"
-        }
-      ],
-      "image": "ghost:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/ghost.png",
-      "name": "ghost",
-      "platform": "linux",
-      "ports": [
-        "2368:2368/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Ghost",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/ghost",
-          "container": "/var/lib/ghost/content"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Dashboard",
-        "Web",
-        "Other"
-      ],
-      "description": "Homarr is a simple and lightweight homepage for your server, that helps you easily access all of your services in one place.",
-      "note": "",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "ghcr.io/ajnart/homarr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homarr.png",
-      "name": "homarr-secured",
-      "platform": "linux",
-      "ports": [
-        "7575:7575/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Homarr-Secured",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/homarr/configs",
-          "container": "/app/data/configs"
-        },
-        {
-          "bind": "/home/docker/homarr/icons",
-          "container": "/app/public/icons"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "Homebridge allows you to integrate with smart home devices that do not natively support HomeKit. There are over 2,000 Homebridge plugins supporting thousands of different smart accessories.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1",
-          "label": "HOMEBRIDGE_CONFIG_UI",
-          "name": "HOMEBRIDGE_CONFIG_UI"
-        },
-        {
-          "default": "8581",
-          "label": "HOMEBRIDGE_CONFIG_UI_PORT",
-          "name": "HOMEBRIDGE_CONFIG_UI_PORT"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        }
-      ],
-      "image": "homebridge/homebridge:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/homebridge.png",
-      "name": "homebridge",
-      "network": "host",
-      "note": "",
-      "platform": "linux",
-      "privileged": true,
-      "restart_policy": "unless-stopped",
-      "title": "Homebridge",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/homebridge",
-          "container": "/homebridge"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders",
-        "Tools"
-      ],
-      "description": "JDownloader 2 is a free, open-source download management tool with a huge community of developers that makes downloading as easy and fast as it should be. Users can start, stop or pause downloads, set bandwith limitations, auto-extract archives and much more. It's an easy-to-extend framework that can save hours of your valuable time every day!. <a href='https://hub.docker.com/r/jlesage/jdownloader-2'>Docker Hub</a>",
-      "env": [
-        {
-          "default": "",
-          "label": "MYJD_DEVICE_NAME",
-          "name": "MYJD_DEVICE_NAME"
-        },
-        {
-          "default": "",
-          "label": "MYJD_USER",
-          "name": "MYJD_USER"
-        },
-        {
-          "default": "",
-          "label": "MYJD_PASSWORD",
-          "name": "MYJD_PASSWORD"
-        }
-      ],
-      "image": "jlesage/jdownloader-2",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/jdownloader.png",
-      "name": "jdownloader",
-      "platform": "linux",
-      "ports": [
-        "5800:5800/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "JDownloader",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/jdownloader",
-          "container": "/opt/JDownloader/app/cfg"
-        },
-        {
-          "bind": "/media",
-          "container": "/opt/JDownloader/Downloads"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "An alternative private front-end to Reddit",
-      "image": "libreddit/libreddit:armv7",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/libreddit.png",
-      "name": "libreddit",
-      "platform": "linux",
-      "ports": [
-        "8088:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Libreddit",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/libreddit",
-          "container": "/config"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Authentication",
-        "LDAP"
-      ],
-      "description": "This project is a lightweight authentication server that provides an opinionated, simplified LDAP interface for authentication.",
-      "env": [
-        {
-          "default": "somesecretjwt",
-          "label": "LLDAP_JWT_SECRET",
-          "name": "LLDAP_JWT_SECRET"
-        },
-        {
-          "default": "someadminpassword",
-          "label": "LLDAP_LDAP_USER_PASS",
-          "name": "LLDAP_LDAP_USER_PASS"
-        },
-        {
-          "default": "dc=example,dc=com",
-          "label": "LLDAP_LDAP_BASE_DN",
-          "name": "LLDAP_LDAP_BASE_DN"
-        }
-      ],
-      "image": "nitnelave/lldap:stable-debian",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/lldap.png",
-      "name": "lldap",
-      "platform": "linux",
-      "ports": [
-        "3890:3890/tcp",
-        "17170:17170/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "LLDAP",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/lldap",
-          "container": "/data"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "A self-hosted recipe manager and meal planner",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        },
-        {
-          "default": "2",
-          "label": "WEB_CONCURRENCY",
-          "name": "WEB_CONCURRENCY"
-        },
-        {
-          "default": "8",
-          "label": "MAX_WORKERS",
-          "name": "MAX_WORKERS"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_PUBLIC",
-          "name": "RECIPE_PUBLIC"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_SHOW_NUTRITION",
-          "name": "RECIPE_SHOW_NUTRITION"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_SHOW_ASSETS",
-          "name": "RECIPE_SHOW_ASSETS"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_LANDSCAPE_VIEW",
-          "name": "RECIPE_LANDSCAPE_VIEW"
-        },
-        {
-          "default": "false",
-          "label": "RECIPE_DISABLE_COMMENTS",
-          "name": "RECIPE_DISABLE_COMMENTS"
-        },
-        {
-          "default": "false",
-          "label": "RECIPE_DISABLE_AMOUNT",
-          "name": "RECIPE_DISABLE_AMOUNT"
-        }
-      ],
-      "image": "hkotel/mealie:v0.4.3",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mealie.png",
-      "name": "mealie",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "9925:80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Mealie",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/mealie",
-          "container": "/app/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools",
-        "Gaming"
-      ],
-      "description": "This docker image provides a Minecraft Server that will automatically download the latest stable version at startup. You can also run/upgrade to any specific version or the latest snapshot. See the Versions section below for more information.",
-      "env": [
-        {
-          "default": "TRUE",
-          "label": "EULA",
-          "name": "EULA"
-        }
-      ],
-      "image": "itzg/minecraft-server:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/minecraft.png",
-      "name": "minecraft",
-      "platform": "linux",
-      "ports": [
-        "25565:25565/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Minecraft Server",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/minecraft-data",
-          "container": "/data"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Networking",
-        "Monitoring"
-      ],
-      "description": "Troubleshoot slowdowns and anomalies in your infrastructure with thousands of per-second metrics, meaningful visualizations, and insightful health alarms with zero configuration.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "DOCKER_USR",
-          "name": "DOCKER_USR"
-        },
-        {
-          "default": "1000",
-          "label": "DOCKER_GRP",
-          "name": "DOCKER_GRP"
-        }
-      ],
-      "image": "netdata/netdata:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/netdata.png",
-      "name": "netdata",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "19999:19999/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Netdata",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/netdata/netdataconfig",
-          "container": "/etc/netdata"
-        },
-        {
-          "bind": "/home/docker/netdata/netdatalib",
-          "container": "/var/lib/netdata"
-        },
-        {
-          "bind": "/etc/passwd",
-          "container": "/host/etc/passwd:ro"
-        },
-        {
-          "bind": "/etc/group",
-          "container": "/host/etc/group:ro"
-        },
-        {
-          "bind": "/proc",
-          "container": "/host/proc:ro"
-        },
-        {
-          "bind": "/sys",
-          "container": "/host/sys:ro"
-        },
-        {
-          "bind": "/etc/os-release",
-          "container": "/host/etc/os-release:ro"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "Organizr allows you to setup Tabs that will be loaded all in one webpage. You can then work on your server with ease.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "organizr/organizr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/organizr.png",
-      "name": "organizr-v2",
-      "platform": "linux",
-      "ports": [
-        "7171:80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Organizr v2",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/organizr",
-          "container": "/config"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "type": 1,
-      "name": "postgres",
-      "title": "PostgreSQL",
-      "note": "",
-      "description": "PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance. It was originally named POSTGRES, referring to its origins as a successor to the Ingres database developed at the University of California, Berkeley. <a href='https://www.postgresql.org/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/postgres' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/postgres.png",
-      "image": "postgres",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/postgres",
-          "container": "/var/lib/postgresql/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "5432",
-          "container": "5432"
-        }
-      ],
-      "env": [
-        {
-          "name": "POSTGRES_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "POSTGRES_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "POSTGRES_DB",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "heimdall"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "A docker image with qBittorrent and the Flood UI, also optional WireGuard VPN support. See the official documentation for WireGuard VPN support at https://hotio.dev/containers/qflood/",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "002",
-          "label": "UMASK",
-          "name": "UMASK"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        },
-        {
-          "default": "false",
-          "label": "FLOOD_AUTH",
-          "name": "FLOOD_AUTH"
-        }
-      ],
-      "image": "hotio/qflood:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/qflood.png",
-      "name": "qflood",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "3000:3000/tcp",
-        "8080:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "qFlood",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/qflood",
-          "container": "/config"
-        },
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/app/qBittorrent/downloads"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "A remote desktop software, the open source TeamViewer alternative, works out of the box, no configuration required. You have full control of your data, with no concerns about security.",
-      "env": [
-        {
-          "default": "rustdesk.example.com:21117",
-          "description": "Use your domain with the default 21117 port",
-          "label": "RELAY",
-          "name": "RELAY"
-        },
-        {
-          "default": "1",
-          "description": "if set to \"1\" unencrypted connection will not be accepted",
-          "label": "ENCRYPTED_ONLY",
-          "name": "ENCRYPTED_ONLY"
-        }
-      ],
-      "image": "rustdesk/rustdesk-server-s6:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/rustdesk.png",
-      "name": "rustdesk",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "21115:21115/tcp",
-        "21116:21116/tcp",
-        "21116:21116/udp",
-        "21117:21117/tcp",
-        "21118:21118/tcp",
-        "21119:21119/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "RustDesk",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/rustdesk",
-          "container": "/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "Open-Source Privacy-respecting metasearch engine",
-      "env": [
-        {
-          "default": "http://localhost:9017",
-          "label": "BASE_URL",
-          "name": "BASE_URL"
-        },
-        {
-          "default": "my-instance",
-          "label": "INSTANCE_NAME",
-          "name": "INSTANCE_NAME"
-        }
-      ],
-      "image": "searxng/searxng:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/searx.png",
-      "name": "searxng",
-      "platform": "linux",
-      "ports": [
-        "9017:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "SearXNG",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/searxng",
-          "container": "/etc/searxng"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "type": 1,
-      "name": "surrealdb",
-      "title": "SurrealDB",
-      "note": "",
-      "description": "SurrealDB acts as both a database and a modern, realtime, collaborative API backend layer. SurrealDB can run as a single server or in a highly-available, highly-scalable distributed mode - with support for SQL querying from client devices, GraphQL, ACID transactions, WebSocket connections, structured and unstructured data, graph querying, full-text indexing, geospatial querying, and row-by-row permissions-based access. <a href='https://surrealdb.co/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/surrealdb/surrealdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/surrealdb.png",
-      "image": "surrealdb/surrealdb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/surrealdb",
-          "container": "/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "3306",
-          "container": "3306"
-        }
-      ],
-      "env": [
-        {
-          "name": "SURREALDB_ADMIN_PASSWORD",
-          "label": "Admin Password",
-          "description": "Admin password for SurrealDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "SURREALDB_ADMIN_EMAIL",
-          "label": "Admin Email",
-          "description": "Admin email for SurrealDB",
-          "type": "text",
-          "default": "root"
-
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Security"
-      ],
-      "description": "Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience. It receives requests on behalf of your system and finds out which components are responsible for handling them. ",
-      "image": "traefik:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/traefik.png",
-      "name": "traefik",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp",
-        "8080:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "command": "--api.insecure=true --providers.docker",
-      "title": "Traefik",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/traefik/traefik.yml",
-          "container": "/traefik.yml"
-        },
-        {
-          "bind": "/home/docker/traefik/config.yml",
-          "container": "/config.yml"
-        },
-        {
-          "bind": "/home/docker/traefik/acme.json",
-          "container": "/acme.json"
-        },
-        {
-          "bind": "/var/run/docker.sock",
-          "container": "/var/run/docker.sock"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Multimedia"
-      ],
-      "description": "Unmanic is a simple tool for optimising your file library. You can use it to convert your files into a single, uniform format, manage file movements based on timestamps, or execute custom commands against a file based on its file size.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "josh5/unmanic:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/unmanic.png",
-      "name": "unmanic",
-      "platform": "linux",
-      "ports": [
-        "8888:8888/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Unmanic",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/unmanic",
-          "container": "/config"
-        },
-        {
-          "bind": "/media",
-          "container": "/library"
-        },
-        {
-          "bind": "/var/tmp/unmanic",
-          "container": "/tmp/unmanic"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "Quickly and easily configure Wireguard through a web browser.",
-      "env": [
-        {
-          "default": "example.domain.com",
-          "description": "Set here your DDNS domain",
-          "label": "WG_HOST",
-          "name": "WG_HOST"
-        },
-        {
-          "default": "ENTER AN ADMIN PASSWORD",
-          "description": "Leave blank to access WebUI without loggin",
-          "label": "PASSWORD",
-          "name": "PASSWORD"
-        },
-        {
-          "default": "51820",
-          "label": "WG_PORT",
-          "name": "WG_PORT"
-        },
-        {
-          "default": "1.1.1.1",
-          "label": "WG_DEFAULT_DNS",
-          "name": "WG_DEFAULT_DNS"
-        },
-        {
-          "default": "10.8.0.x",
-          "label": "WG_DEFAULT_ADDRESS",
-          "name": "WG_DEFAULT_ADDRESS"
-        },
-        {
-          "default": "0.0.0.0/0, ::/0",
-          "label": "WG_ALLOWED_IPS",
-          "name": "WG_ALLOWED_IPS"
-        }
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wireguard.png",
-      "name": "wg-easy",
-      "platform": "linux",
-      "image": "weejewel/wg-easy",
-      "title": "WireGuard-Easy",
-      "type": 3,
-      "note": ""
-    },
-    {
-      "categories": [
-        "CMS"
-      ],
-      "description": "WordPress is a web content management system. It was originally created as a tool to publish blogs but has evolved to support publishing other web content, including more traditional websites, mailing lists and Internet forum, media galleries, membership sites, learning management systems and online stores.",
-      "env": [
-        {
-          "default": "db",
-          "label": "Port or host of database server",
-          "name": "WORDPRESS_DB_HOST"
-        },
-        {
-          "default": "exampleuser",
-          "label": "Database user name",
-          "name": "WORDPRESS_DB_USER"
-        },
-        {
-          "default": "examplepass",
-          "label": "Database password for user",
-          "name": "WORDPRESS_DB_PASSWORD"
-        },
-        {
-          "default": "exampledb",
-          "label": "Database name",
-          "name": "WORDPRESS_DB_NAME"
-        }
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wordpress.png",
-      "note": "Create a mysql or mariadb database for the container first.",
-      "platform": "linux",
-      "image": "wordpress:latest",
-      "title": "Wordpress",
-      "name": "wordpress",
-      "type": 3,
-      "restart_policy": "unless-stopped",
-      "ports": [
-        "8080:80/tcp"
-      ],
-      "network": "AppBridge",
-      "volumes": [
-        {
-          "bind": "/home/docker/wordpress",
-          "container": "/var/www/html"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Gaming"
-      ],
-      "description": "McMyAdmin 2 is the leading web control panel and administration console for Minecraft servers.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "linuxserver/mcmyadmin2:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mcmyadmin2.png",
-      "name": "mcmyadmin2",
-      "platform": "linux",
-      "ports": [
-        "8080:8080/tcp",
-        "25565:25565/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "McMyAdmin 2",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/minecraft"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "A one-of-a-kind resume builder that's not out to get your data. Completely secure, customizable, portable, open-source and free forever.",
-      "image": "amruthpillai/reactive-resume:latest",
-      "logo": "https://raw.githubusercontent.com/SelfhostedPro/selfhosted_templates/master/Images/reactiveresume.png",
-      "name": "reactive-resume",
-      "platform": "linux",
-      "ports": [
-        "80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Reactive-Resume",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/reactiveresume",
-          "container": "/usr/src/app"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "VPN",
-        "Tools"
-      ],
-      "description": "This container contains OpenVPN and Deluge with a configuration where Deluge is running only when OpenVPN has an active tunnel. It bundles configuration files for many popular VPN providers to make the setup easier.",
-      "env": [
-        {
-          "default": "1001",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1001",
-          "label": "PGID",
-          "name": "PUID"
-        },
-        {
-          "default": "MULLVAD",
-          "description": "see https://github.com/sgtsquiggs/docker-deluge-openvpn",
-          "label": "OPENVPN_PROVIDER",
-          "name": "OPENVPN_PROVIDER"
-        },
-        {
-          "label": "OPENVPN_USERNAME",
-          "name": "OPENVPN_USERNAME"
-        },
-        {
-          "label": "OPENVPN_PASSWORD",
-          "name": "OPENVPN_PASSWORD"
-        }
-      ],
-      "image": "sgtsquiggs/deluge-openvpn:latest",
-      "name": "deluge-openvpn",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/deluge.png",
-      "platform": "linux",
-      "ports": [
-        "8112:8112/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Deluge OpenVPN",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/etc/localtime",
-          "container": "/etc/localtime"
-        },
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/downloads"
-        },
-        {
-          "bind": "/home/docker/delugeopenvpn/config",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Multimedia"
-      ],
-      "note": "",
-      "description": "Tdarr is a popular conditional transcoding application for processing large (or small) media libraries. The application comes in the form of a click-to-run web-app, which you run on your own device and access through a web browser.",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/tdarr.png",
-      "name": "tdarr",
-      "platform": "linux",
-      "image": "ghcr.io/haveagitgat/tdarr",
-      "title": "Tdarr",
-      "type": 3,
-      "volumes": [
-        {
-          "bind": "/home/docker/tdarr/configs",
-          "container": "/app/configs"
-        },
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/downloads"
-        }
-      ],
-      "ports": [
-        "8265:8265/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Finance"
-      ],
-      "description": "Cryptofolio is an open-source, and self-hosted solution for tracking your cryptocurrency holdings. It features a web interface, an Android mobile app, and a cross-platform desktop application for Windows, macOS, and Linux.",
-      "image": "xtrendence/cryptofolio:latest",
-      "logo": "https://i.imgur.com/5v8lzea.png",
-      "name": "cryptofolio",
-      "platform": "linux",
-      "ports": [
-        "7280:80/tcp"
-      ],
-      "restart_policy": "always",
-      "title": "Cryptofolio",
-      "type": 1
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "An easy to use Status Page for your websites and applications. Statping will automatically fetch the application and render a beautiful status page with tons of features for you to build an even better status page.",
-      "image": "statping/statping:latest",
-      "logo": "https://raw.githubusercontent.com/xneo1/portainer_templates/master/Images/statping.png",
-      "name": "statping",
-      "platform": "linux",
-      "ports": [
-        "4040:8080/tcp"
-      ],
-      "restart_policy": "always",
-      "title": "Statping",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/statping",
-          "container": "/app"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "influxdb",
-      "title": "InfluxDB",
-      "note": "",
-      "description": "InfluxDB is an open source time series database developed by InfluxData. It is written in Go and optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics. <a href='https://www.influxdata.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/influxdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/influxdb.png",
-      "image": "influxdb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/influxdb",
-          "container": "/var/lib/influxdb",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "8086",
-          "container": "8086"
-        }
-      ],
-      "env": [
-        {
-          "name": "INFLUXDB_ADMIN_USER",
-          "label": "Admin User",
-          "description": "Admin user for InfluxDB",
-          "type": "text",
-          "default": "admin"
-        },
-        {
-          "name": "INFLUXDB_ADMIN_PASSWORD",
-          "label": "Admin Password",
-          "description": "Admin password for InfluxDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "INFLUXDB_DB",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "mydb"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Media Server",
-        "LDAP"
-      ],
-      "description": "Tubearchivist is your self hosted YouTube media server",
-      "note": "Requires a Redis and Elasticsearch database. Tube Archivist needs around 2GB of available memory for a small testing setup and around 4GB of available memory for a mid to large sized installation. Minimal with dual core with 4 threads, better quad core plus.",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/tube-archivist.png",
-      "name": "tubearchivist",
-      "platform": "linux",
-      "image": "bbilly1/tubearchivist",
-      "title": "Tubearchivist",
-      "type": 3,
-      "ports": [
-        "8080:8080/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/media/youtube",
-          "container": "/youtube"
-        }
-      ],
-      "env": [
-        {
-          "name": "INFLUXDB_ADMIN_USER",
-          "label": "Admin User",
-          "description": "Admin user for InfluxDB",
-          "type": "text",
-          "default": "admin"
-        },
-        {
-          "name": "INFLUXDB_ADMIN_PASSWORD",
-          "label": "Admin Password",
-          "description": "Admin password for InfluxDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "INFLUXDB_DB",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "mydb"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Finance"
-      ],
-      "description": "Actual is a local-first personal finance tool. It is 100% free and open-source. It has a synchronization element so that all your changes can move between devices without any heavy lifting.",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/actual.png",
-      "name": "actual-server",
-      "platform": "linux",
-      "image": "actualbudget/actual-server:latest",
-      "restart_policy": "unless-stopped",
-      "title": "Actual-Server",
-      "ports": [
-        "5006:5006/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/actual",
-          "container": "/data"
-        }
-      ],
-      "type": 3
-    },
-    {
-      "categories": [
-        "Development"
-      ],
-      "description": "Appsmith (www.appsmith.com) is the first open-source low code tool that helps developers build dashboards and admin panels very quickly.",
-      "logo": "https://cdn-images.himalayas.app/vr60veq4neiptamhqm6qxwi3toi3",
-      "name": "appsmith",
-      "platform": "linux",
-      "image": "appsmith/appsmith-ce",
-      "title": "Appsmith",
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/appsmith",
-          "container": "/appsmith-stacks"
-        }
-      ],
-      "type": 3
-    },
-    {
-      "categories": [
-        "Productivity"
-      ],
-      "note": "Requires a MySQL database. Create a database first.",
-      "description": "Leantime is an open source project management system for small teams and startups written in PHP, Javascript using MySQL. https://leantime.io",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/leantime.png",
-      "name": "leantime",
-      "platform": "linux",
-      "image": "leantime/leantime:latest",
-      "title": "Leantime",
-      "type": 3,
-      "ports": [
-        "80:80/tcp"
-      ],
-      "env": [
-        {
-          "name": "MYSQL_HOST",
-          "label": "Database Host",
-          "description": "Database host",
-          "type": "text",
-          "default": "mysql_leantime"
-        },
-        {
-          "name": "MYSQL_USER",
-          "label": "Database User",
-          "description": "Database user",
-          "type": "text",
-          "default": "leantime_user"
-        },
-        {
-          "name": "MYSQL_PASSWORD",
-          "label": "Database Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "choose.a.long.random.password"
-        },
-        {
-          "name": "MYSQL_DATABASE",
-          "label": "Database Name",
-          "description": "Database name",
-          "type": "text",
-          "default": "leantime_database"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other"
-      ],
-      "description": "Jellyseerr is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!. <a href='https://github.com/Fallenbagel/jellyseerr/'>Github</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/jellyseerr.png",
-      "name": "jellyseerr",
-      "note": "For Emby : add the environment variable JELLYFIN_TYPE=emby . (this will allow you to use the play button)",
-      "platform": "linux",
-      "image": "fallenbagel/jellyseerr:latest",
-      "title": "Jellyseerr",
-      "type": 3,
-      "volumes": [
-        {
-          "bind": "/home/docker/jellyseerr",
-          "container": "/app/config"
-        }
-      ],
-      "ports": [
-        "5055:5055/tcp"
-      ]
-    },
-    {
-      "categories": [
-        "Dashboard"
-      ],
-      "description": "Dashdot is a modern server dashboard, running on the latest tech, designed with glassmorphism in mind. It is intended to be used for smaller VPS and private servers.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dashdot.png",
-      "name": "dashdot",
-      "platform": "linux",
-      "image": "mauricenino/dashdot",
-      "title": "Dashdot",
-      "type": 3,
-      "ports": [
-        "80:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/",
-          "container": "/mnt/host:ro"
-        }
-      ],
-      "privileged": true
-    },
-    {
-      "name": "nocodb",
-      "title": "NocoDB",
-      "note": "",
-      "description": "NocoDB is a free, open-source, self-hosted, no-code platform to make database driven application. <a href='https://www.nocodb.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nocodb/nocodb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/nocodb.png",
-      "image": "nocodb/nocodb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/nocodb",
-          "container": "/var/lib/nocodb",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "8080",
-          "container": "8080"
-        }
-      ],
-      "env": [
-        {
-          "name": "NOCODB_DB_HOST",
-          "label": "Database Host",
-          "description": "Database host",
-          "type": "text",
-          "default": "mysql"
-        },
-        {
-          "name": "NOCODB_DB_PORT",
-          "label": "Database Port",
-          "description": "Database port",
-          "type": "text",
-          "default": "3306"
-        },
-        {
-          "name": "NOCODB_DB_USER",
-          "label": "Database User",
-          "description": "Database user",
-          "type": "text",
-          "default": "root"
-        },
-        {
-          "name": "NOCODB_DB_PASSWORD",
-          "label": "Database Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "NOCODB_DB_NAME",
-          "label": "Database Name",
-          "description": "Database name",
-          "type": "text",
-          "default": "nocodb"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it\u2019ll cover ALL your home devices, and you don\u2019t need any client-side software for that. With the rise of Internet-Of-Things and connected devices, it becomes more and more important to be able to control your whole network.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "label": "CONTEXT_PATH",
-          "name": "CONTEXT_PATH",
-          "set": "adguard home"
-        }
-      ],
-      "note": "DNS-over-HTTPS: [80:80/TCP] [443:443/TCP] [443:443/UDP] [3000:3000/TCP] [DEFAULT]. DNS: [53:53/TCP] [53:53/UDP]. Admin Panel: [3000:3000/TCP]. DHCP: [67:67/UDP] [68:68/TCP] [68:68/UDP]. DNS-over-TLS: [853:853/TCP]. DNS-over-QUIC: [784:784/UDP] [853:853/UDP] [8853:8853/UDP]. DNSCrypt: [5443:5443/TCP] [5443:5443/UDP]",
-      "image": "adguard/adguardhome:latest",
-      "logo": "https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Images/adguard.png",
-      "name": "adguard",
-      "platform": "linux",
-      "ports": [
-        "53:53/tcp",
-        "53:53/udp",
-        "67:67/udp",
-        "68:68/tcp",
-        "68:68/udp",
-        "80:80/tcp",
-        "443:443/tcp",
-        "853:853/tcp",
-        "3000:3000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Adguard",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/portainer/Files/AppData/Adguard/Workdir",
-          "container": "/opt/adguardhome/work"
-        },
-        {
-          "bind": "/portainer/Files/AppData/Adguard/Conf",
-          "container": "/opt/adguardhome/conf"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "mongodb",
-      "title": "MongoDB",
-      "note": "",
-      "description": "MongoDB is a source-available cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with optional schemas. <a href='https://www.mongodb.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/mongo' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/mongodb.png",
-      "image": "mongo",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/mongodb",
-          "container": "/data/db",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "27017",
-          "container": "27017"
-        }
-      ],
-      "env": [
-        {
-          "name": "MONGO_INITDB_ROOT_USERNAME",
-          "label": "Root Username",
-          "description": "Root username for MongoDB",
-          "type": "text",
-          "default": "root"
-        },
-        {
-          "name": "MONGO_INITDB_ROOT_PASSWORD",
-          "label": "Root Password",
-          "description": "Root password for MongoDB",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "cratedb",
-      "title": "CrateDB",
-      "note": "",
-      "description": "CrateDB is a distributed SQL database that combines SQL and search in a way that's simple to scale. It is designed to be highly available, horizontally scalable, and easy to use. <a href='https://crate.io/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/crate' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/cratedb.png",
-      "image": "crate",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/cratedb",
-          "container": "/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "4200",
-          "container": "4200"
-        },
-        {
-          "host": "4300",
-          "container": "4300"
-        }
-      ],
-      "env": [
-        {
-          "name": "CRATE_HEAP_SIZE",
-          "label": "Heap Size",
-          "description": "Heap size for CrateDB",
-          "type": "text",
-          "default": "2g"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "elasticsearch",
-      "title": "Elasticsearch",
-      "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",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/elasticsearch",
-          "container": "/usr/share/elasticsearch/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "9200",
-          "container": "9200"
-        },
-        {
-          "host": "9300",
-          "container": "9300"
-        }
-      ],
-      "env": [
-        {
-          "name": "discovery.type",
-          "label": "Discovery Type",
-          "description": "Discovery type for Elasticsearch",
-          "type": "text",
-          "default": "single-node"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "SQL Server",
-      "name": "mssql",
-      "description": "Official image for Microsoft SQL Server based on Ubuntu 20.04. <a href=\"https://hub.docker.com/_/microsoft-mssql-server\" target=\"_blank\">Docker Hub</a>",
-      "categories": [
-        "Database"
-      ],
-      "platform": "linux",
-      "note": "Requires at least 2GB of RAM. Make sure to assign enough memory to the Docker VM if you're running on Docker for Mac or Windows. Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.",
-      "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png",
-      "image": "mcr.microsoft.com/mssql/server:2022-latest",
-      "ports": [
-        "1433/tcp"
-      ],
-      "env": [
-        {
-          "name": "ACCEPT_EULA",
-          "default": "Y",
-          "preset": true
-        },
-        {
-          "name": "MSSQL_SA_PASSWORD",
-          "label": "MSSQL_SA_PASSWORD",
-          "default": "YOUR_STRONG_PASSWORD"
-        },
-        {
-          "name": "MSSQL_PID",
-          "label": "MSSQL_PID",
-          "default": "Developer"
-        }
-      ]
-    },
-    {
-      "name": "redis",
-      "title": "Redis",
-      "note": "",
-      "description": "Redis is an in-memory data structure project implementing a distributed, in-memory key-value database with optional durability. Redis supports different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indexes. <a href='https://redis.io/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/redis' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/redis.png",
-      "image": "redis",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/redis",
-          "container": "/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "6379",
-          "container": "6379"
-        }
-      ],
-      "env": [
-        {
-          "name": "REDIS_PASSWORD",
-          "label": "Password",
-          "description": "Password for Redis",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Joomla",
-      "name": "joomla",
-      "description": "Joomla! is an award-winning content management system (CMS), which enables you to build web sites and powerful online applications.",
-      "categories": [
-        "CMS"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/joomla.png",
-      "image": "joomla:latest",
-      "env": [
-        {
-          "name": "JOOMLA_DB_HOST",
-          "label": "MySQL database host",
-          "type": "container"
-        },
-        {
-          "name": "JOOMLA_DB_PASSWORD",
-          "label": "Database password"
-        }
-      ],
-      "ports": [
-        "80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/var/www/html"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Drupal",
-      "name": "drupal",
-      "description": "Open-source content management framework",
-      "categories": [
-        "CMS"
-      ],
-      "platform": "linux",
-      "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/drupal.png",
-      "image": "drupal:latest",
-      "ports": [
-        "80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/var/www/html"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "cockroachdb",
-      "title": "CockroachDB",
-      "note": "",
-      "description": "CockroachDB is a distributed SQL database built on a transactional and strongly-consistent key-value store. It scales horizontally; survives disk, machine, rack, and even datacenter failures with minimal latency disruption and no manual intervention; supports strongly-consistent ACID transactions; and provides a familiar SQL API for structuring, manipulating, and querying data. <a href='https://www.cockroachlabs.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/cockroachdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/cockroachdb.png",
-      "image": "cockroachdb/cockroach",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/cockroachdb",
-          "container": "/cockroach/cockroach-data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "26257",
-          "container": "26257"
-        },
-        {
-          "host": "8080",
-          "container": "8080"
-        }
-      ],
-      "env": [
-        {
-          "name": "COCKROACH_CHANNEL",
-          "label": "Channel",
-          "description": "CockroachDB channel",
-          "type": "text",
-          "default": "stable"
-        },
-        {
-          "name": "COCKROACH_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "root"
-        },
-        {
-          "name": "COCKROACH_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "mariadb",
-      "title": "MariaDB",
-      "note": "",
-      "description": "MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system (RDBMS), intended to remain free and open-source software under the GNU General Public License. Development is led by some of the original developers of MySQL, who forked it due to concerns over its acquisition by Oracle Corporation in 2009. <a href='https://mariadb.org/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/mariadb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mariadb.png",
-      "image": "mariadb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/mariadb",
-          "container": "/var/lib/mysql",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "3306",
-          "container": "3306"
-        }
-      ],
-      "env": [
-        {
-          "name": "MYSQL_ROOT_PASSWORD",
-          "label": "Root Password",
-          "description": "Root password for MariaDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "MYSQL_DATABASE",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "couchdb",
-      "title": "CouchDB",
-      "note": "",
-      "description": "Apache CouchDB is open source database software that focuses on ease of use and having a scalable architecture. It has a document-oriented NoSQL database architecture and is implemented in the concurrency-oriented language Erlang; it uses JSON to store data, JavaScript as its query language using MapReduce, and HTTP for an API. <a href='https://couchdb.apache.org/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/couchdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/couchdb.png",
-      "image": "couchdb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/couchdb",
-          "container": "/opt/couchdb/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "5984",
-          "container": "5984"
-        }
-      ],
-      "env": [
-        {
-          "name": "COUCHDB_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "COUCHDB_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    }
-  ]
-}

+ 7 - 7
templates/templates.json → templates/json/default.json

@@ -3359,7 +3359,7 @@
       "type": 1,
       "volumes": [
         {
-          "bind": "/portainer/Downloads",
+          "bind": "/home/docker/downloads",
           "container": "/data"
         },
         {
@@ -4299,7 +4299,7 @@
           "container": "/config"
         },
         {
-          "bind": "/portainer/Downloads",
+          "bind": "/home/docker/Downloads",
           "container": "/app/qBittorrent/downloads"
         }
       ]
@@ -4707,7 +4707,7 @@
           "container": "/etc/localtime"
         },
         {
-          "bind": "/portainer/Downloads",
+          "bind": "/home/docker/Downloads",
           "container": "/downloads"
         },
         {
@@ -4734,7 +4734,7 @@
           "container": "/app/configs"
         },
         {
-          "bind": "/portainer/Downloads",
+          "bind": "/home/docker/Downloads",
           "container": "/downloads"
         }
       ],
@@ -5125,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"
         }
       ]
@@ -5220,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"
       ],

+ 0 - 5930
templates/templates-bak.json

@@ -1,5930 +0,0 @@
-{
-  "version": "2",
-  "templates": [
-    {
-      "type": 1,
-      "name": "heimdall",
-      "title": "Heimdall",
-      "note": "",
-      "description": "Heimdall is a way to organise all those links to your most used web sites and web applications in a simple way. <a href='https://hub.docker.com/r/linuxserver/heimdall/' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/heimdall.png",
-      "image": "lscr.io/linuxserver/heimdall:latest",
-      "categories": [
-        "Dashboard"
-      ],
-      "ports": [
-        "8001:80/tcp",
-        "4001:443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/heimdall",
-          "container": "/config"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "homepage",
-      "title": "Homepage",
-      "description": "A modern (fully static, fast), secure (fully proxied), highly customizable application dashboard with integrations for more than 25 services and translations for over 15 languages. Easily configured via YAML files (or discovery via docker labels). <a href='https://github.com/benphelps/homepage/'>Github</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homepage.png",
-      "image": "ghcr.io/benphelps/homepage:latest",
-      "categories": [
-        "Dashboard"
-      ],
-      "ports": [
-        "3000:3000/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/homepage",
-          "container": "/app/config"
-        },
-        {
-          "bind": "/var/run/docker.sock",
-          "container": "/var/run/docker.sock:ro"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "jackett",
-      "title": "Jackett",
-      "description": "Jackett works as a proxy server: it translates queries from apps (Sonarr, SickRage, CouchPotato, Mylar, etc) into tracker-site-specific http queries, parses the html response, then sends results back to the requesting software. This allows for getting recent uploads (like RSS) and performing searches. <a href='https://github.com/Jackett/Jackett/'>Github</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/jackett.png",
-      "image": "linuxserver/jackett:latest",
-      "categories": [
-        "Downloaders",
-        "Tools"
-      ],
-      "ports": [
-        "9117:9117/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/jackett",
-          "container": "/config"
-        },
-        {
-          "bind": "/media",
-          "container": "/downloads"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Librespeed",
-      "name": "librespeed",
-      "note": "",
-      "description": "Librespeed is a very lightweight speed test implemented in Javascript, using XMLHttpRequest and Web Workers. No Flash, No Java, No Websocket, No Bullshit. <a href='https://github.com/librespeed/speedtest/'>Github</a>",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/librespeed.png",
-      "categories": [
-        "Networking",
-        "Tools"
-      ],
-      "image": "adolfintel/speedtest",
-      "ports": [
-        "81:81/tcp"
-      ],
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles",
-          "description": "Specify a timezone to use for example Europe/Amsterdam"
-        },
-        {
-          "name": "MODE",
-          "label": "MODE",
-          "default": "standalone",
-          "description": "Set the mode."
-        },
-        {
-          "name": "PASSWORD",
-          "label": "PASSWORD",
-          "default": "SOMEPASSWORD",
-          "description": "Password to access the stats page. If not set, stats page will not allow accesses."
-        },
-        {
-          "name": "WEBPORT",
-          "label": "WEBPORT",
-          "default": "81",
-          "description": "Allows choosing a custom port for the included web server."
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "name": "ombi",
-      "title": "Ombi",
-      "description": "Ombi allows you to host your own Plex Request and user management system. If you are sharing your Plex server with other users, allow them to request new content using an easy to manage interface. . [There is no official Ombi docker image. This one is created and maintained by <a href='https://hub.docker.com/r/linuxserver/ombi/' target='_blank'>linuxserver.io</a>]",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/ombi.png",
-      "image": "linuxserver/ombi:latest",
-      "categories": [
-        "Tools"
-      ],
-      "ports": [
-        "3579:3579/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/ombi",
-          "container": "/config"
-        },
-        {
-          "bind": "/etc/localtime",
-          "container": "/etc/localtime:ro"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "overseerr",
-      "title": "Overseerr",
-      "description": "Overseerr is a request management and media discovery tool built to work with your existing Plex ecosystem. <a href='https://overseerr.dev/' target='_blank'>Official Site</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/overseerr.png",
-      "image": "sctx/overseerr:latest",
-      "categories": [
-        "Multimedia",
-        "Tools"
-      ],
-      "ports": [
-        "5055/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/overseerr",
-          "container": "/app/config"
-        },
-        {
-          "container": "/etc/localtime",
-          "bind": "/etc/localtime:ro",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "plex",
-      "title": "Plex",
-      "description": "Plex organizes your video, music, and photo collections and streams them to all of your screens.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/plex.png",
-      "image": "linuxserver/plex:latest",
-      "network": "host",
-      "categories": [
-        "Media Server",
-        "Paid"
-      ],
-      "privileged": true,
-      "ports": [],
-      "volumes": [
-        {
-          "bind": "/home/docker/plex",
-          "container": "/config"
-        },
-        {
-          "bind": "/media/tvshows",
-          "container": "/data/tvshows"
-        },
-        {
-          "bind": "/media/movies",
-          "container": "/data/movies"
-        },
-        {
-          "bind": "/media/music",
-          "container": "/data/music"
-        },
-        {
-          "container": "/transcode"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        },
-        {
-          "name": "VERSION",
-          "label": "VERSION",
-          "default": "latest"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "prowlarr",
-      "title": "Prowlarr",
-      "description": "Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. ",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/prowlarr.png",
-      "image": "ghcr.io/linuxserver/prowlarr:develop",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "ports": [
-        "9696/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/prowlarr",
-          "container": "/config"
-        },
-        {
-          "bind": "/etc/localtime:ro",
-          "container": "/etc/localtime:ro",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles",
-          "preset": true
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "radarr",
-      "title": "Radarr",
-      "note": "There is no official Radarr docker image. This one is created and maintained by <a href='https://hotio.dev/containers/radarr/' target='_blank'>Hotio.dev</a>",
-      "description": "Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. <a href='https://radarr.video/#downloads-v3-docker' target='_blank'>Official Site</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/radarr.png",
-      "image": "ghcr.io/hotio/radarr",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "network": "AppBridge",
-      "ports": [
-        "7878:7878/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/radarr",
-          "container": "/config"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "readarr",
-      "title": "Readarr",
-      "description": "Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/readarr.png",
-      "image": "ghcr.io/linuxserver/readarr:nightly",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "ports": [
-        "8787/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/readarr",
-          "container": "/config"
-        },
-        {
-          "bind": "/media/downloads/ebooks",
-          "container": "/downloads"
-        },
-        {
-          "container": "/books",
-          "bind": "/media/storage/ebooks"
-        },
-        {
-          "container": "/blackhole",
-          "bind": "/media/temp/blackhole/ebooks"
-        },
-        {
-          "container": "/etc/localtime",
-          "bind": "/etc/localtime",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "scrutiny",
-      "title": "Scrutiny",
-      "description": "WebUI for smartd S.M.A.R.T monitoring",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/scrutiny.png",
-      "image": "analogj/scrutiny:latest",
-      "categories": [
-        "Monitoring"
-      ],
-      "ports": [
-        "8080/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/scrutiny/config/",
-          "bind": "/opt/mediadepot/apps/scrutiny"
-        },
-        {
-          "container": "/run/udev",
-          "bind": "/run/udev",
-          "readonly": true
-        }
-      ],
-      "env": [],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.scrutiny.loadbalancer.server.port",
-          "value": "8080"
-        },
-        {
-          "name": "traefik.http.routers.scrutiny.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.scrutiny.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "sonarr",
-      "title": "Sonarr",
-      "description": "Sonarr is a PVR for usenet and bittorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/sonarr.png",
-      "image": "linuxserver/sonarr:latest",
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "ports": [
-        "8989/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/opt/mediadepot/apps/sonarr"
-        },
-        {
-          "container": "/downloads",
-          "bind": "/media/storage/downloads/tvshows"
-        },
-        {
-          "container": "/tv",
-          "bind": "/media/storage/tvshows"
-        },
-        {
-          "container": "/blackhole",
-          "bind": "/media/temp/blackhole/tvshows"
-        },
-        {
-          "container": "/etc/localtime",
-          "bind": "/etc/localtime",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        }
-      ],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.sonarr.loadbalancer.server.port",
-          "value": "8989"
-        },
-        {
-          "name": "traefik.http.routers.sonarr.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.sonarr.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "tautulli",
-      "title": "Tautulli",
-      "description": "A Python based monitoring and tracking tool for Plex Media Server.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/tautulli.png",
-      "image": "linuxserver/tautulli:latest",
-      "categories": [
-        "MediaServer:Other",
-        "Tools"
-      ],
-      "ports": [
-        "8181/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/opt/mediadepot/apps/tautulli"
-        }
-      ],
-      "env": [
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "preset": true
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles"
-        }
-      ],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.tautulli.loadbalancer.server.port",
-          "value": "8181"
-        },
-        {
-          "name": "traefik.http.routers.tautulli.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.tautulli.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "watchtower",
-      "title": "Watchtower",
-      "description": "Automatically update running Docker containers",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/watchtower.png",
-      "image": "containrrr/watchtower:latest",
-      "command": "--cleanup --label-enable",
-      "categories": [
-        "Monitoring"
-      ],
-      "volumes": [
-        {
-          "container": "/var/run/docker.sock",
-          "bind": "/var/run/docker.sock"
-        }
-      ],
-      "env": []
-    },
-    {
-      "type": 1,
-      "name": "wizarr",
-      "title": "Wizarr",
-      "description": "Wizarr is an advanced user invitation and management system for Jellyfin, Plex, Emby etc. ",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wizarr.png",
-      "image": "ghcr.io/wizarrrr/wizarr",
-      "categories": [
-        "Arr"
-      ],
-      "ports": [
-        "5690/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wizarr",
-          "container": "/data/database"
-        },
-        {
-          "bind": "/etc/localtime:ro",
-          "container": "/etc/localtime",
-          "readonly": true
-        }
-      ],
-      "env": [
-        {
-          "name": "APP_URL",
-          "label": "APP_URL",
-          "default": "https://wizarr.domain.com"
-        }
-      ],
-      "labels": [
-        {
-          "name": "traefik.enable",
-          "value": "true"
-        },
-        {
-          "name": "traefik.http.services.wizarr.loadbalancer.server.port",
-          "value": "5690"
-        },
-        {
-          "name": "traefik.http.routers.wizarr.entrypoints",
-          "value": "websecure"
-        },
-        {
-          "name": "traefik.http.routers.wizarr.tls.certresolver",
-          "value": "mydnschallenge"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "Transmission is designed for easy, powerful use. Transmission has the features you want from a BitTorrent client: encryption, a web interface, peer exchange, magnet links, DHT, \u00ef\u00bf\u00bdTP, UPnP and NAT-PMP port forwarding, webseed support, watch directories, tracker editing, global and per-torrent speed limits, and more.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/transmission:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/transmission.png",
-      "platform": "linux",
-      "ports": [
-        "9091/tcp",
-        "51413/tcp",
-        "51413/udp"
-      ],
-      "title": "Transmission",
-      "name": "transmission",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/transmission",
-          "container": "/config"
-        },
-        {
-          "bind": "/media",
-          "container": "/downloads"
-        },
-        {
-          "container": "/watch"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Video"
-      ],
-      "description": "Headless installation of Kodi.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/kodi-headless:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/kodi.png",
-      "platform": "linux",
-      "ports": [
-        "8080/tcp",
-        "9777/udp"
-      ],
-      "title": "Kodi-Headless",
-      "name": "kodi-headless",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config/.kodi"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Backup:",
-        "Cloud",
-        "Other",
-        "Tools"
-      ],
-      "description": "Syncthing replaces proprietary sync and cloud services with something open, trustworthy and decentralized. Your data is your data alone and you deserve to choose where it is stored, if it is shared with some third party and how it's transmitted over the Internet.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/syncthing:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/syncthing.png",
-      "platform": "linux",
-      "ports": [
-        "8384/tcp",
-        "21027/udp",
-        "22000/tcp"
-      ],
-      "title": "Syncthing",
-      "name": "syncthing",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/sync"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "mysql",
-      "title": "MySQL",
-      "note": "",
-      "description": "MySQL is the world's most popular open source database. With its proven performance, reliability and ease-of-use, MySQL has become the leading database choice for web-based applications, covering the entire range from personal projects and websites, via e-commerce and information services, all the way to high profile web properties including Facebook, Twitter, YouTube, Yahoo! and many more. <a href='https://www.mysql.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/mysql' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mysql.png",
-      "image": "mysql",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/mysql",
-          "container": "/var/lib/mysql",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "3306",
-          "container": "3306"
-        }
-      ],
-      "env": [
-        {
-          "name": "MYSQL_ROOT_PASSWORD",
-          "label": "Root Password",
-          "description": "Root password for MySQL",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "MYSQL_DATABASE",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "Deluge is a lightweight, Free Software, cross-platform BitTorrent client providing: Full Encryption, WebUI, Plugin System, Much more...",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "lscr.io/linuxserver/deluge:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/deluge.png",
-      "platform": "linux",
-      "title": "Deluge",
-      "name": "deluge",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/downloads"
-        }
-      ],
-      "ports": [
-        "8112/tcp",
-        "6881/tcp",
-        "6881/udp"
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Proxy"
-      ],
-      "description": "Nginx is a web server with a strong focus on high concurrency, performance and low memory usage. It can also act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/nginx:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nginx.png",
-      "platform": "linux",
-      "ports": [
-        "80/tcp",
-        "443/tcp"
-      ],
-      "title": "Nginx",
-      "name": "nginx",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders",
-        "Arr"
-      ],
-      "description": "Lidarr is a music collection manager for Usenet and BitTorrent users.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/lidarr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/lidarr.png",
-      "platform": "linux",
-      "ports": [
-        "8686/tcp"
-      ],
-      "title": "Lidarr",
-      "name": "lidarr",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/downloads"
-        },
-        {
-          "container": "/music"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "The qBittorrent project aims to provide an open-source software alternative to \u00ef\u00bf\u00bdTorrent. qBittorrent is based on the Qt toolkit and libtorrent-rasterbar library.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/qbittorrent:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/qbittorrent.png",
-      "platform": "linux",
-      "ports": [
-        "6881/tcp",
-        "6881/udp",
-        "8080/tcp"
-      ],
-      "title": "qbittorrent",
-      "name": "qbittorrent",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/downloads"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other"
-      ],
-      "description": "OpenVPN Access Server is a full featured secure network tunneling VPN software solution that integrates OpenVPN server capabilities, enterprise management capabilities, simplified OpenVPN Connect UI, and OpenVPN Client software packages that accommodate Windows, MAC, Linux, Android, and iOS environments.",
-      "env": [
-        {
-          "label": "INTERFACE",
-          "name": "INTERFACE",
-          "set": "eth0"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/openvpn-as:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/openvpn.png",
-      "platform": "linux",
-      "ports": [
-        "943/tcp",
-        "9443/tcp",
-        "1194/udp"
-      ],
-      "name": "openvpn-as",
-      "title": "OpenVPN Access Server",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other"
-      ],
-      "description": "Server version of minetest, a free, open source alternative to minecraft.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/minetest:latest",
-      "logo": "https://raw.githubusercontent.com/linuxserver/beta-templates/master/lsiodev/img/minetest-icon.png",
-      "platform": "linux",
-      "ports": [
-        "30000/udp"
-      ],
-      "title": "Minetest",
-      "name": "minetest",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config/.minetest"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Media"
-      ],
-      "description": "Airsonic is a free, web-based media streamer, providing ubiqutious access to your music. Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple players simultaneously, for instance to one player in your kitchen and another in your living room.",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "CONTEXT_PATH",
-          "name": "CONTEXT_PATH",
-          "set": "airsonic"
-        },
-        {
-          "label": "JAVA_OPTS",
-          "name": "JAVA_OPTS",
-          "set": "-Xms256m -Xmx512m"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/airsonic:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/airsonic.png",
-      "platform": "linux",
-      "ports": [
-        "4040/tcp"
-      ],
-      "name": "airsonic",
-      "title": "Airsonic",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/music"
-        },
-        {
-          "container": "/playlists"
-        },
-        {
-          "container": "/podcasts"
-        },
-        {
-          "container": "/media"
-        },
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Web"
-      ],
-      "description": "This container sets up an Nginx webserver and reverse proxy with php support and a built-in letsencrypt client that automates free SSL server certificate generation and renewal processes. It also contains fail2ban for intrusion prevention.\r\n  \r\n  Before running this container, make sure that the url and subdomains are properly forwarded to this container's host.\r\n  \r\n  - Port 443 on the internet side of the router should be forwarded to this container's port 443.\r\n  - If you need a dynamic dns provider, you can use the free provider duckdns.org where the url will be yoursubdomain.duckdns.org and the subdomains    can be www,ftp,cloud\r\n  - The container detects changes to url and subdomains, revokes existing certs and generates new ones during start. \r\n  - It also detects changes to the DHLEVEL parameter and replaces the dhparams file.\r\n  \r\n  - If you'd like to password protect your sites, you can use htpasswd. Run the following command on your host to generate the htpasswd file docker exec -it letsencrypt htpasswd -c /config/nginx/.htpasswd &lt;username&gt;",
-      "env": [
-        {
-          "label": "EMAIL",
-          "name": "EMAIL",
-          "set": "-Xms256m -Xmx512m"
-        },
-        {
-          "label": "URL",
-          "name": "URL",
-          "set": "-Xms256m -Xmx512m"
-        },
-        {
-          "label": "SUBDOMAINS",
-          "name": "SUBDOMAINS",
-          "set": "www,"
-        },
-        {
-          "label": "ONLY_SUBDOMAINS",
-          "name": "ONLY_SUBDOMAINS",
-          "set": "false"
-        },
-        {
-          "label": "DHLEVEL",
-          "name": "DHLEVEL",
-          "set": "2048"
-        },
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "VALIDATION",
-          "name": "VALIDATION",
-          "set": "http"
-        },
-        {
-          "label": "DNSPLUGIN",
-          "name": "DNSPLUGIN",
-          "set": "http"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/letsencrypt:latest",
-      "logo": "https://raw.githubusercontent.com/thesugarat/portainer_templates-1/master/Images/letsencrypt.png",
-      "platform": "linux",
-      "ports": [
-        "80/tcp",
-        "443/tcp"
-      ],
-      "title": "Let's Encrypt",
-      "name": "letsencrypt",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Cloud",
-        "Productivity",
-        "Tools",
-        "Other",
-        "Web"
-      ],
-      "description": "Nextcloud is an open source, self-hosted file sync and communication app platform. Access and sync your files, contacts, calendars and communicate and collaborate across your devices. You decide what happens with your data, where it is and who can access it!",
-      "env": [
-        {
-          "label": "PUID",
-          "name": "PUID",
-          "set": "1000"
-        },
-        {
-          "label": "PGID",
-          "name": "PGID",
-          "set": "1000"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ",
-          "set": "America/Chicago"
-        }
-      ],
-      "image": "linuxserver/nextcloud:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nextcloud.png",
-      "platform": "linux",
-      "ports": [
-        "443/tcp"
-      ],
-      "title": "Nextcloud",
-      "name": "nextcloud",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/config"
-        },
-        {
-          "container": "/data"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Apprise-api",
-      "name": "apprise-api",
-      "note": "",
-      "description": "Apprise-api takes advantage of Apprise through your network with a user-friendly API. * Send notifications to more then 65+ services. * An incredibly lightweight gateway to Apprise. * A production ready micro-service at your disposal. Apprise API was designed to easily fit into existing (and new) eco-systems that are looking for a simple notification solution.",
-      "categories": [
-        "Tools",
-        "Development"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/apprise-api.png",
-      "image": "linuxserver/apprise-api:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "8000:8000/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/apprise-api"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Audacity",
-      "name": "audacity",
-      "note": "",
-      "description": "Audacity is an easy-to-use, multi-track audio editor and recorder. Developed by a group of volunteers as open source. (https://www.audacityteam.org/)",
-      "platform": "linux",
-      "categories": [
-        "Media"
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/audacity.png",
-      "image": "linuxserver/audacity:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/audacity/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Bazarr",
-      "name": "bazarr",
-      "note": "",
-      "description": "Bazarr is a companion application to Sonarr and Radarr. It can manage and download subtitles based on your requirements. You define your preferences by TV show or movie and Bazarr takes care of everything for you.",
-      "categories": [
-        "Multimedia",
-        "Downloader",
-        "Arr"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/bazarr.png",
-      "image": "linuxserver/bazarr:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "6767:6767/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/bazarr",
-          "container": "/config"
-        },
-        {
-          "bind": "/home/docker/bazarr/movies",
-          "container": "/movies"
-        },
-        {
-          "container": "/tv",
-          "bind": "/home/docker/bazarr/tv"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Changedetection.io",
-      "name": "changedetection.io",
-      "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",
-      "image": "linuxserver/changedetection.io:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "BASE_URL",
-          "label": "BASE_URL",
-          "default": "",
-          "description": "Specify the full URL (including protocol) when running behind a reverse proxy"
-        }
-      ],
-      "ports": [
-        "5000:5000/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/changedetection.io"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Code-server",
-      "name": "code-server",
-      "note": "",
-      "description": "Code-server is VS Code running on a remote server, accessible through the browser. - Code on your Chromebook, tablet, and laptop with a consistent dev environment. - If you have a Windows or Mac workstation, more easily develop for Linux. - Take advantage of large cloud servers to speed up tests, compilations, downloads, and more. - Preserve battery life when you're on the go. - All intensive computation runs on your server. - You're no longer running excess instances of Chrome. (https://coder.com)",
-      "categories": [
-        "Development"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/code-server.png",
-      "image": "linuxserver/code-server:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000"
-
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "America/Los_Angeles",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "PASSWORD",
-          "label": "PASSWORD",
-          "default": "password",
-          "description": "Optional web gui password, if `PASSWORD` or `HASHED_PASSWORD` is not provided, there will be no auth."
-        },
-        {
-          "name": "HASHED_PASSWORD",
-          "label": "HASHED_PASSWORD",
-          "default": "",
-          "description": "Optional web gui password, overrides `PASSWORD`, instructions on how to create it is below."
-        },
-        {
-          "name": "SUDO_PASSWORD",
-          "label": "SUDO_PASSWORD",
-          "default": "password",
-          "description": "If this optional variable is set, user will have sudo access in the code-server terminal with the specified password."
-        },
-        {
-          "name": "SUDO_PASSWORD_HASH",
-          "label": "SUDO_PASSWORD_HASH",
-          "default": "",
-          "description": "Optionally set sudo password via hash (takes priority over `SUDO_PASSWORD` var). Format is `$type$salt$hashed`."
-        },
-        {
-          "name": "PROXY_DOMAIN",
-          "label": "PROXY_DOMAIN",
-          "default": "code-server.mydomain",
-          "description": "If this optional variable is set, this domain will be proxied for subdomain proxying. See [Documentation](https://github.com/cdr/code-server/blob/master/docs/FAQ.md#sub-domains)"
-        },
-        {
-          "name": "DEFAULT_WORKSPACE",
-          "label": "DEFAULT_WORKSPACE",
-          "default": "/config/workspace",
-          "description": "If this optional variable is set, code-server will open this directory by default"
-        }
-      ],
-      "ports": [
-        "8443:8443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/code-server/config",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Dokuwiki",
-      "name": "dokuwiki",
-      "note": "",
-      "description": "Dokuwiki is a simple to use and highly versatile Open Source wiki software that doesn't require a database. It is loved by users for its clean and readable syntax. The ease of maintenance, backup and integration makes it an administrator's favorite. Built in access controls and authentication connectors make DokuWiki especially useful in the enterprise context and the large number of plugins contributed by its vibrant community allow for a broad range of use cases beyond a traditional wiki.  (https://www.dokuwiki.org/dokuwiki/)",
-      "categories": [
-        "Productivity",
-        "CMS"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dokuwiki.png",
-      "image": "linuxserver/dokuwiki:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/dokuwiki"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Doplarr",
-      "name": "doplarr",
-      "note": "",
-      "description": "Doplarr (https://github.com/kiranshila/Doplarr) is an *arr request bot for Discord.'",
-      "categories": [
-        "Arr"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/doplarr.png",
-      "image": "linuxserver/doplarr:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "DISCORD__TOKEN",
-          "label": "DISCORD__TOKEN",
-          "default": "",
-          "description": "Specify your discord bot token."
-        },
-        {
-          "name": "OVERSEERR__API",
-          "label": "OVERSEERR__API",
-          "default": "",
-          "description": "Specify your Overseerr API key. Leave blank if using Radarr/Sonarr."
-        },
-        {
-          "name": "OVERSEERR__URL",
-          "label": "OVERSEERR__URL",
-          "default": "http://localhost:5055",
-          "description": "Specify your Overseerr URL. Leave blank if using Radarr/Sonarr."
-        },
-        {
-          "name": "RADARR__API",
-          "label": "RADARR__API",
-          "default": "",
-          "description": "Specify your Radarr API key. Leave blank if using Overseerr."
-        },
-        {
-          "name": "RADARR__URL",
-          "label": "RADARR__URL",
-          "default": "http://localhost:7878",
-          "description": "Specify your Radarr URL. Leave blank if using Overseerr."
-        },
-        {
-          "name": "SONARR__API",
-          "label": "SONARR__API",
-          "default": "",
-          "description": "Specify your Sonarr API key. Leave blank if using Overseerr."
-        },
-        {
-          "name": "SONARR__URL",
-          "label": "SONARR__URL",
-          "default": "http://localhost:8989",
-          "description": "Specify your Sonarr URL. Leave blank if using Overseerr."
-        },
-        {
-          "name": "DISCORD__MAX_RESULTS",
-          "label": "DISCORD__MAX_RESULTS",
-          "default": "25",
-          "description": "Sets the maximum size of the search results selection"
-        },
-        {
-          "name": "DISCORD__REQUESTED_MSG_STYLE",
-          "label": "DISCORD__REQUESTED_MSG_STYLE",
-          "default": ":plain",
-          "description": "Sets the style of the request alert message. One of `:plain` `:embed` `:none`"
-        },
-        {
-          "name": "SONARR__QUALITY_PROFILE",
-          "label": "SONARR__QUALITY_PROFILE",
-          "default": "",
-          "description": "The name of the quality profile to use by default for Sonarr"
-        },
-        {
-          "name": "RADARR__QUALITY_PROFILE",
-          "label": "RADARR__QUALITY_PROFILE",
-          "default": "",
-          "description": "The name of the quality profile to use by default for Radarr"
-        },
-        {
-          "name": "SONARR__ROOTFOLDER",
-          "label": "SONARR__ROOTFOLDER",
-          "default": "",
-          "description": "The root folder to use by default for Sonarr"
-        },
-        {
-          "name": "RADARR__ROOTFOLDER",
-          "label": "RADARR__ROOTFOLDER",
-          "default": "",
-          "description": "The root folder to use by default for Radarr"
-        },
-        {
-          "name": "SONARR__LANGUAGE_PROFILE",
-          "label": "SONARR__LANGUAGE_PROFILE",
-          "default": "",
-          "description": "The name of the language profile to use by default for Sonarr"
-        },
-        {
-          "name": "OVERSEERR__DEFAULT_ID",
-          "label": "OVERSEERR__DEFAULT_ID",
-          "default": "",
-          "description": "The Overseerr user id to use by default if there is no associated discord account for the requester"
-        },
-        {
-          "name": "PARTIAL_SEASONS",
-          "label": "PARTIAL_SEASONS",
-          "default": "true",
-          "description": "Sets whether users can request partial seasons."
-        },
-        {
-          "name": "LOG_LEVEL",
-          "label": "LOG_LEVEL",
-          "default": ":info",
-          "description": "The log level for the logging backend. This can be changed for debugging purposes. One of trace `:debug` `:info` `:warn` `:error` `:fatal` `:report`"
-        },
-        {
-          "name": "JAVA_OPTS",
-          "label": "JAVA_OPTS",
-          "default": "",
-          "description": "For passing additional java options."
-        }
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/doplarr"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Emby",
-      "name": "emby",
-      "note": "",
-      "description": "Emby organizes video, music, live TV, and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices. This container is packaged as a standalone emby Media Server.",
-      "categories": [
-        "Media Server",
-        "Paid"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/emby.png",
-      "image": "linuxserver/emby:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "8096:8096/tcp",
-        "8920:8920/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/emby/config"
-        },
-        {
-          "container": "/data/tvshows",
-          "bind": "/home/docker/emby/data/tvshows"
-        },
-        {
-          "container": "/data/movies",
-          "bind": "/home/docker/emby/data/movies"
-        },
-        {
-          "container": "/opt/vc/lib",
-          "bind": "/opt/vc/lib"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "EmbyStat",
-      "name": "embystat",
-      "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",
-      "image": "linuxserver/embystat:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "6555:6555/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/embystat"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Emulatorjs",
-      "name": "emulatorjs",
-      "note": "",
-      "description": "Emulatorjs - In browser web based emulation portable to nearly any device for many retro consoles. A mix of emulators is used between Libretro and EmulatorJS.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/emulatorjs.png",
-      "image": "linuxserver/emulatorjs:latest",
-      "categories": [
-        "Gaming"
-      ],
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "SUBFOLDER",
-          "label": "SUBFOLDER",
-          "default": "/",
-          "description": "Specify a subfolder for reverse proxies IE '/FOLDER/'"
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "80:80/tcp",
-        "4001:4001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/emulatorjs"
-        },
-        {
-          "container": "/data",
-          "bind": "/home/docker/emulatorjs/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Fail2ban",
-      "name": "fail2ban",
-      "note": "",
-      "description": "Fail2ban is a daemon to ban hosts that cause multiple authentication errors.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/fail2ban.png",
-      "image": "linuxserver/fail2ban:latest",
-      "network": "host",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "VERBOSITY",
-          "label": "VERBOSITY",
-          "default": "-vv",
-          "description": "Set the container log verbosity. Valid options are -v, -vv, -vvv, -vvvv, or leaving the value blank or not setting the variable."
-        }
-      ],
-      "ports": [
-        "80:80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/fail2ban/config"
-        },
-        {
-          "container": "/var/log:ro",
-          "bind": "/home/docker/fail2ban/var/log:ro"
-        },
-        {
-          "container": "/remotelogs/airsonic:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/airsonic:ro"
-        },
-        {
-          "container": "/remotelogs/apache2:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/apache2:ro"
-        },
-        {
-          "container": "/remotelogs/authelia:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/authelia:ro"
-        },
-        {
-          "container": "/remotelogs/emby:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/emby:ro"
-        },
-        {
-          "container": "/remotelogs/filebrowser:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/filebrowser:ro"
-        },
-        {
-          "container": "/remotelogs/homeassistant:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/homeassistant:ro"
-        },
-        {
-          "container": "/remotelogs/lighttpd:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/lighttpd:ro"
-        },
-        {
-          "container": "/remotelogs/nextcloud:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/nextcloud:ro"
-        },
-        {
-          "container": "/remotelogs/nginx:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/nginx:ro"
-        },
-        {
-          "container": "/remotelogs/nzbget:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/nzbget:ro"
-        },
-        {
-          "container": "/remotelogs/overseerr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/overseerr:ro"
-        },
-        {
-          "container": "/remotelogs/prowlarr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/prowlarr:ro"
-        },
-        {
-          "container": "/remotelogs/radarr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/radarr:ro"
-        },
-        {
-          "container": "/remotelogs/sabnzbd:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/sabnzbd:ro"
-        },
-        {
-          "container": "/remotelogs/sonarr:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/sonarr:ro"
-        },
-        {
-          "container": "/remotelogs/unificontroller:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/unificontroller:ro"
-        },
-        {
-          "container": "/remotelogs/vaultwarden:ro",
-          "bind": "/home/docker/fail2ban/remotelogs/vaultwarden:ro"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Homeassistant",
-      "name": "homeassistant",
-      "note": "",
-      "description": "Home Assistant Core is an open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.",
-      "categories": [
-        "Tools"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homeassistant.png",
-      "image": "linuxserver/homeassistant:latest",
-      "network": "host",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "8123:8123/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/homeassistant"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Jellyfin",
-      "name": "jellyfin",
-      "note": "",
-      "description": "Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/jellyfin.png",
-      "image": "linuxserver/jellyfin:latest",
-      "categories": [
-        "Media Server",
-        "LDAP"
-      ],
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "JELLYFIN_PublishedServerUrl",
-          "label": "JELLYFIN_PublishedServerUrl",
-          "default": "192.168.0.5",
-          "description": "Set the autodiscovery response domain or IP address."
-        }
-      ],
-      "network": "AppBridge",
-      "ports": [
-        "8096:8096/tcp",
-        "8920:8920/tcp",
-        "7359:7359/udp",
-        "1900:1900/udp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/jellyfin",
-          "container": "/config"
-        },
-        {
-          "bind": "/media/tvshows",
-          "container": "/data/tvshows"
-        },
-        {
-          "bind": "/media/movies",
-          "container": "/data/movies"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Kasm",
-      "name": "kasm",
-      "note": "",
-      "description": "Kasm workspaces is a docker container streaming platform for delivering browser-based access to desktops, applications, and web services. Kasm uses devops-enabled Containerized Desktop Infrastructure (CDI) to create on-demand, disposable, docker containers that are accessible via web browser. Example use-cases include Remote Browser Isolation (RBI), Data Loss Prevention (DLP), Desktop as a Service (DaaS), Secure Remote Access Services (RAS), and Open Source Intelligence (OSINT) collections. The rendering of the graphical-based containers is powered by the open-source project [KasmVNC](https://www.kasmweb.com/kasmvnc.html?utm_campaign=LinuxServer&utm_source=kasmvnc).",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/kasm.png",
-      "image": "linuxserver/kasm:latest",
-      "privileged": true,
-      "env": [
-        {
-          "name": "KASM_PORT",
-          "label": "KASM_PORT",
-          "default": "443",
-          "description": "Specify the port you bind to the outside for Kasm Workspaces."
-        },
-        {
-          "name": "DOCKER_HUB_USERNAME",
-          "label": "DOCKER_HUB_USERNAME",
-          "default": "USER",
-          "description": "Optionally specify a DockerHub Username to pull private images."
-        },
-        {
-          "name": "DOCKER_HUB_PASSWORD",
-          "label": "DOCKER_HUB_PASSWORD",
-          "default": "PASS",
-          "description": "Optionally specify a DockerHub password to pull private images."
-        },
-        {
-          "name": "DOCKER_MTU",
-          "label": "DOCKER_MTU",
-          "default": "1500",
-          "description": "Optionally specify the mtu options passed to dockerd."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/kasm/opt",
-          "container": "/opt"
-        },
-        {
-          "bind": "/home/docker/kasm/profiles",
-          "container": "/profiles"
-        },
-        {
-          "bind": "/dev/input",
-          "container": "/dev/input"
-        },
-        {
-          "bind": "/run/udev/data",
-          "container": "/run/udev/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Libreoffice",
-      "name": "libreoffice",
-      "note": "",
-      "description": "LibreOffice is a free and powerful office suite, and a successor to OpenOffice.org (commonly known as OpenOffice). Its clean interface and feature-rich tools help you unleash your creativity and enhance your productivity.",
-      "categories": [
-        "Productivity",
-        "Tools"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/libreoffice.png",
-      "image": "linuxserver/libreoffice:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/libreoffice/config",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Mastodon",
-      "name": "mastodon",
-      "note": "",
-      "description": "Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. (https://github.com/mastodon/mastodon/)",
-      "categories": [
-        "Communication"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mastodon.png",
-      "image": "linuxserver/mastodon:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "LOCAL_DOMAIN",
-          "label": "LOCAL_DOMAIN",
-          "default": "example.com",
-          "description": "This is the unique identifier of your server in the network. It cannot be safely changed later."
-        },
-        {
-          "name": "REDIS_HOST",
-          "label": "REDIS_HOST",
-          "default": "redis",
-          "description": "Redis server hostname"
-        },
-        {
-          "name": "REDIS_PORT",
-          "label": "REDIS_PORT",
-          "default": "6379",
-          "description": "Redis port"
-        },
-        {
-          "name": "DB_HOST",
-          "label": "DB_HOST",
-          "default": "db",
-          "description": "Postgres database hostname"
-        },
-        {
-          "name": "DB_USER",
-          "label": "DB_USER",
-          "default": "mastodon",
-          "description": "Postgres username"
-        },
-        {
-          "name": "DB_NAME",
-          "label": "DB_NAME",
-          "default": "mastodon",
-          "description": "Postgres db name"
-        },
-        {
-          "name": "DB_PASS",
-          "label": "DB_PASS",
-          "default": "mastodon",
-          "description": "Postgres password"
-        },
-        {
-          "name": "DB_PORT",
-          "label": "DB_PORT",
-          "default": "5432",
-          "description": "Portgres port"
-        },
-        {
-          "name": "ES_ENABLED",
-          "label": "ES_ENABLED",
-          "default": "false",
-          "description": "Enable or disable Elasticsearch (requires a separate ES instance)"
-        },
-        {
-          "name": "SECRET_KEY_BASE",
-          "label": "SECRET_KEY_BASE",
-          "default": "",
-          "description": "Browser session secret. Changing it will break all active browser sessions."
-        },
-        {
-          "name": "OTP_SECRET",
-          "label": "OTP_SECRET",
-          "default": "",
-          "description": "MFA secret. Changing it will break two-factor authentication."
-        },
-        {
-          "name": "VAPID_PRIVATE_KEY",
-          "label": "VAPID_PRIVATE_KEY",
-          "default": "",
-          "description": "Push notification private key. Changing it will break push notifications."
-        },
-        {
-          "name": "VAPID_PUBLIC_KEY",
-          "label": "VAPID_PUBLIC_KEY",
-          "default": "",
-          "description": "Push notification public key. Changing it will break push notifications."
-        },
-        {
-          "name": "SMTP_SERVER",
-          "label": "SMTP_SERVER",
-          "default": "mail.example.com",
-          "description": "SMTP server for email notifications"
-        },
-        {
-          "name": "SMTP_PORT",
-          "label": "SMTP_PORT",
-          "default": "25",
-          "description": "SMTP server port"
-        },
-        {
-          "name": "SMTP_LOGIN",
-          "label": "SMTP_LOGIN",
-          "default": "",
-          "description": "SMTP username"
-        },
-        {
-          "name": "SMTP_PASSWORD",
-          "label": "SMTP_PASSWORD",
-          "default": "",
-          "description": "SMTP password"
-        },
-        {
-          "name": "SMTP_FROM_ADDRESS",
-          "label": "SMTP_FROM_ADDRESS",
-          "default": "notifications@example.com",
-          "description": "From address for emails send from Mastodon"
-        },
-        {
-          "name": "S3_ENABLED",
-          "label": "S3_ENABLED",
-          "default": "false",
-          "description": "Enable or disable S3 storage of uploaded files"
-        },
-        {
-          "name": "WEB_DOMAIN",
-          "label": "WEB_DOMAIN",
-          "default": "mastodon.example.com",
-          "description": "This can be set if you want your server identifier to be different to the subdomain hosting Mastodon. See [https://docs.joinmastodon.org/admin/config/#basic](https://docs.joinmastodon.org/admin/config/#basic)"
-        },
-        {
-          "name": "ES_HOST",
-          "label": "ES_HOST",
-          "default": "es",
-          "description": "Elasticsearch server hostname"
-        },
-        {
-          "name": "ES_PORT",
-          "label": "ES_PORT",
-          "default": "9200",
-          "description": "Elasticsearch port"
-        },
-        {
-          "name": "ES_USER",
-          "label": "ES_USER",
-          "default": "elastic",
-          "description": "Elasticsearch username"
-        },
-        {
-          "name": "ES_PASS",
-          "label": "ES_PASS",
-          "default": "elastic",
-          "description": "Elasticsearch password"
-        },
-        {
-          "name": "S3_BUCKET",
-          "label": "S3_BUCKET",
-          "default": "",
-          "description": "S3 bucket hostname"
-        },
-        {
-          "name": "AWS_ACCESS_KEY_ID",
-          "label": "AWS_ACCESS_KEY_ID",
-          "default": "",
-          "description": "S3 bucket access key ID"
-        },
-        {
-          "name": "AWS_SECRET_ACCESS_KEY",
-          "label": "AWS_SECRET_ACCESS_KEY",
-          "default": "",
-          "description": "S3 bucket secret access key"
-        },
-        {
-          "name": "S3_ALIAS_HOST",
-          "label": "S3_ALIAS_HOST",
-          "default": "",
-          "description": "Alternate hostname for object fetching if you are front the S3 connections."
-        },
-        {
-          "name": "SIDEKIQ_ONLY",
-          "label": "SIDEKIQ_ONLY",
-          "default": "false",
-          "description": "Only run the sidekiq service in this container instance. For large scale instances that need better queue handling."
-        },
-        {
-          "name": "SIDEKIQ_QUEUE",
-          "label": "SIDEKIQ_QUEUE",
-          "default": "",
-          "description": "The name of the sidekiq queue to run in this container. See [notes](https://docs.joinmastodon.org/admin/scaling/#sidekiq-queues)."
-        },
-        {
-          "name": "SIDEKIQ_DEFAULT",
-          "label": "SIDEKIQ_DEFAULT",
-          "default": "false",
-          "description": "Set to `true` on the main container if you're running additional sidekiq instances. It will run the `default` queue."
-        },
-        {
-          "name": "SIDEKIQ_THREADS",
-          "label": "SIDEKIQ_THREADS",
-          "default": "5",
-          "description": "The number of threads for sidekiq to use. See [notes](https://docs.joinmastodon.org/admin/scaling/#sidekiq-threads)."
-        },
-        {
-          "name": "DB_POOL",
-          "label": "DB_POOL",
-          "default": "5",
-          "description": "The size of the DB connection pool, must be *at least* the same as `SIDEKIQ_THREADS`. See [notes](https://docs.joinmastodon.org/admin/scaling/#sidekiq-threads)."
-        }
-      ],
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/mastodon/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Phpmyadmin",
-      "name": "phpmyadmin",
-      "note": "",
-      "description": "Phpmyadmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB.",
-      "categories": [
-        "Tools"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/phpmyadmin.png",
-      "image": "linuxserver/phpmyadmin:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "PMA_ARBITRARY",
-          "label": "PMA_ARBITRARY",
-          "default": "1",
-          "description": "Set to `1` to allow you to connect to any server. Setting to `0` will only allow you to connect to specified hosts (See Application Setup)"
-        },
-        {
-          "name": "PMA_ABSOLUTE_URI",
-          "label": "PMA_ABSOLUTE_URI",
-          "default": "https://phpmyadmin.example.com",
-          "description": "Set the URL you will use to access the web frontend"
-        }
-      ],
-      "ports": [
-        "80:80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/phpmyadmin/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Pidgin",
-      "name": "pidgin",
-      "note": "",
-      "description": "Pidgin is a chat program which lets you log into accounts on multiple chat networks simultaneously. This means that you can be chatting with friends on XMPP and sitting in an IRC channel at the same time.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/pidgin.png",
-      "image": "linuxserver/pidgin:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/pidgin",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Remmina",
-      "name": "remmina",
-      "note": "",
-      "description": "Remmina is a remote desktop client written in GTK, aiming to be useful for system administrators and travellers, who need to work with lots of remote computers in front of either large or tiny screens. Remmina supports multiple network protocols, in an integrated and consistent user interface. Currently RDP, VNC, SPICE, NX, XDMCP, SSH and EXEC are supported.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/remmina.png",
-      "image": "linuxserver/remmina:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/remmina/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Sqlitebrowser",
-      "name": "sqlitebrowser",
-      "note": "",
-      "description": "DB Browser for SQLite is a high quality, visual, open source tool to create, design, and edit database files compatible with SQLite.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/sqlitebrowser.png",
-      "image": "linuxserver/sqlitebrowser:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/config",
-          "bind": "/home/docker/sqlitebrowser/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Swag",
-      "name": "swag",
-      "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",
-      "image": "linuxserver/swag:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "URL",
-          "label": "URL",
-          "default": "yourdomain.url",
-          "description": "Top url you have control over (`customdomain.com` if you own it, or `customsubdomain.ddnsprovider.com` if dynamic dns)."
-        },
-        {
-          "name": "VALIDATION",
-          "label": "VALIDATION",
-          "default": "http",
-          "description": "Certbot validation method to use, options are `http` or `dns` (`dns` method also requires `DNSPLUGIN` variable set)."
-        },
-        {
-          "name": "SUBDOMAINS",
-          "label": "SUBDOMAINS",
-          "default": "www,",
-          "description": "Subdomains you'd like the cert to cover (comma separated, no spaces) ie. `www,ftp,cloud`. For a wildcard cert, set this *exactly* to `wildcard` (wildcard cert is available via `dns` validation only)"
-        },
-        {
-          "name": "CERTPROVIDER",
-          "label": "CERTPROVIDER",
-          "default": "",
-          "description": "Optionally define the cert provider. Set to `zerossl` for ZeroSSL certs (requires existing [ZeroSSL account](https://app.zerossl.com/signup) and the e-mail address entered in `EMAIL` env var). Otherwise defaults to Let's Encrypt."
-        },
-        {
-          "name": "DNSPLUGIN",
-          "label": "DNSPLUGIN",
-          "default": "cloudflare",
-          "description": "Required if `VALIDATION` is set to `dns`. Options are `acmedns`, `aliyun`, `azure`, `bunny`, `cloudflare`, `cpanel`, `desec`, `digitalocean`, `directadmin`, `dnsimple`, `dnsmadeeasy`, `dnspod`, `do`, `domeneshop`, `dreamhost`, `duckdns`, `dynu`, `freedns`, `gandi`, `gehirn`, `godaddy`, `google`, `google-domains`, `he`, `hetzner`, `infomaniak`, `inwx`, `ionos`, `linode`, `loopia`, `luadns`, `netcup`, `njalla`, `nsone`, `ovh`, `porkbun`, `rfc2136`, `route53`, `sakuracloud`, `standalone`, `transip`, and `vultr`. Also need to enter the credentials into the corresponding ini (or json for some plugins) file under `/config/dns-conf`."
-        },
-        {
-          "name": "PROPAGATION",
-          "label": "PROPAGATION",
-          "default": "",
-          "description": "Optionally override (in seconds) the default propagation time for the dns plugins."
-        },
-        {
-          "name": "EMAIL",
-          "label": "EMAIL",
-          "default": "",
-          "description": "Optional e-mail address used for cert expiration notifications (Required for ZeroSSL)."
-        },
-        {
-          "name": "ONLY_SUBDOMAINS",
-          "label": "ONLY_SUBDOMAINS",
-          "default": "false",
-          "description": "If you wish to get certs only for certain subdomains, but not the main domain (main domain may be hosted on another machine and cannot be validated), set this to `true`"
-        },
-        {
-          "name": "EXTRA_DOMAINS",
-          "label": "EXTRA_DOMAINS",
-          "default": "",
-          "description": "Additional fully qualified domain names (comma separated, no spaces) ie. `extradomain.com,subdomain.anotherdomain.org,*.anotherdomain.org`"
-        },
-        {
-          "name": "STAGING",
-          "label": "STAGING",
-          "default": "false",
-          "description": "Set to `true` to retrieve certs in staging mode. Rate limits will be much higher, but the resulting cert will not pass the browser's security test. Only to be used for testing purposes."
-        }
-      ],
-      "ports": [
-        "443:443/tcp",
-        "80:80/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/swag",
-          "container": "/config"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Wikijs",
-      "name": "wikijs",
-      "note": "Setup mysql, postgres, mariadb, mssql database first, or use the default sqlite.",
-      "description": "Wikijs is a modern, lightweight and powerful wiki app built on NodeJS. (https://wiki.js.org/)",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wikijs.png",
-      "image": "linuxserver/wikijs:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "DB_TYPE",
-          "label": "DB_TYPE",
-          "default": "sqlite",
-          "description": "Type of database (mysql, postgres, mariadb, mssql or sqlite)"
-        },
-        {
-          "name": "DB_HOST",
-          "label": "DB_HOST",
-          "default": "",
-          "description": "Hostname or IP of the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_PORT",
-          "label": "DB_PORT",
-          "default": "",
-          "description": "Port of the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_NAME",
-          "label": "DB_NAME",
-          "default": "",
-          "description": "Database name (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_USER",
-          "label": "DB_USER",
-          "default": "",
-          "description": "Username to connect to the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        },
-        {
-          "name": "DB_PASS",
-          "label": "DB_PASS",
-          "default": "",
-          "description": "Password to connect to the database (For PostgreSQL, MySQL, MariaDB and MSSQL)"
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wikijs/config",
-          "container": "/config"
-        },
-        {
-          "bind": "/home/docker/wikijs/data",
-          "container": "/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Wireguard",
-      "name": "wireguard",
-      "note": "",
-      "description": "WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. WireGuard is designed as a general purpose VPN for running on embedded interfaces and super computers alike, fit for many different circumstances. Initially released for the Linux kernel, it is now cross-platform (Windows, macOS, BSD, iOS, Android) and widely deployable. It is currently under heavy development, but already it might be regarded as the most secure, easiest to use, and simplest VPN solution in the industry.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wireguard.png",
-      "image": "linuxserver/wireguard:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1000",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "1000",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        },
-        {
-          "name": "SERVERURL",
-          "label": "SERVERURL",
-          "default": "wireguard.domain.com",
-          "description": "External IP or domain name for docker host. Used in server mode. If set to `auto`, the container will try to determine and set the external IP automatically"
-        },
-        {
-          "name": "SERVERPORT",
-          "label": "SERVERPORT",
-          "default": "51820",
-          "description": "External port for docker host. Used in server mode."
-        },
-        {
-          "name": "PEERS",
-          "label": "PEERS",
-          "default": "1",
-          "description": "Number of peers to create confs for. Required for server mode. Can also be a list of names: `myPC,myPhone,myTablet` (alphanumeric only)"
-        },
-        {
-          "name": "PEERDNS",
-          "label": "PEERDNS",
-          "default": "auto",
-          "description": "DNS server set in peer/client configs (can be set as `8.8.8.8`). Used in server mode. Defaults to `auto`, which uses wireguard docker host's DNS via included CoreDNS forward."
-        },
-        {
-          "name": "INTERNAL_SUBNET",
-          "label": "INTERNAL_SUBNET",
-          "default": "10.13.13.0",
-          "description": "Internal subnet for the wireguard and server and peers (only change if it clashes). Used in server mode."
-        },
-        {
-          "name": "ALLOWEDIPS",
-          "label": "ALLOWEDIPS",
-          "default": "0.0.0.0/0",
-          "description": "The IPs/Ranges that the peers will be able to reach using the VPN connection. If not specified the default value is: '0.0.0.0/0, ::0/0' This will cause ALL traffic to route through the VPN, if you want split tunneling, set this to only the IPs you would like to use the tunnel AND the ip of the server's WG ip, such as 10.13.13.1."
-        },
-        {
-          "name": "PERSISTENTKEEPALIVE_PEERS",
-          "label": "PERSISTENTKEEPALIVE_PEERS",
-          "default": "",
-          "description": "Set to `all` or a list of comma separated peers (ie. `1,4,laptop`) for the wireguard server to send keepalive packets to listed peers every 25 seconds. Useful if server is accessed via domain name and has dynamic IP. Used only in server mode."
-        },
-        {
-          "name": "LOG_CONFS",
-          "label": "LOG_CONFS",
-          "default": "true",
-          "description": "Generated QR codes will be displayed in the docker log. Set to `false` to skip log output."
-        }
-      ],
-      "ports": [
-        "51820:51820/udp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wireguard/config",
-          "container": "/config"
-        },
-        {
-          "bind": "/lib/modules",
-          "container": "/lib/modules"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "type": 1,
-      "title": "Wireshark",
-      "name": "wireshark",
-      "note": "",
-      "description": "Wireshark is the world's foremost and widely-used network protocol analyzer. It lets you see what's happening on your network at a microscopic level and is the de facto (and often de jure) standard across many commercial and non-profit enterprises, government agencies, and educational institutions.",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wireshark.png",
-      "image": "linuxserver/wireshark:latest",
-      "network": "host",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for UserID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for GroupID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "specify a timezone to use, see this [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)."
-        }
-      ],
-      "ports": [
-        "3000:3000/tcp",
-        "3001:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/wireshark",
-          "container": "/config"
-        }
-      ],
-      "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"
-      ],
-      "description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases",
-      "env": [
-        {
-          "default": "/home/node/trilium-data",
-          "label": "TRILIUM_DATA_DIR",
-          "name": "TRILIUM_DATA_DIR"
-        },
-        {
-          "label": "PORT",
-          "name": "PORT"
-        }
-      ],
-      "image": "zadam/trilium:latest",
-      "logo": "https://www.saashub.com/images/app/service_logos/55/2901389fab77/large.png?1561117248",
-      "name": "trilium",
-      "platform": "linux",
-      "ports": [
-        "3388:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Trilium",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/trilium-data",
-          "container": "/home/node/trilium-data"
-        }
-      ]
-    },
-    {
-      "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"
-      ],
-      "description": "Helps you organize your self-hosted services by making them accessible from a single place.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dashy.png",
-      "name": "dashy",
-      "platform": "linux",
-      "image": "lissy93/dashy:latest",
-      "title": "Dashy",
-      "restart_policy": "unless-stopped",
-      "type": 3,
-      "ports": [
-        "4000:80/tcp"
-      ]
-    },
-    {
-      "categories": [
-        "Dashboard"
-      ],
-      "description": "Flame is self-hosted startpage for your server. Its design is inspired (heavily) by SUI. Flame is very easy to setup and use. With built-in editors, it allows you to setup your very own application hub in no time - no file editing necessary.",
-      "image": "pawelmalak/flame",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/flame.png",
-      "name": "flame-dashboard",
-      "platform": "linux",
-      "ports": [
-        "5005:5005/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Flame-Dashboard",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/flame-dashboard",
-          "container": "/app/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Authentication"
-      ],
-      "description": "An open-source authentication and authorization server providing 2-factor authentication and single sign-on (SSO) for your applications via a web portal.",
-      "env": [
-        {
-          "label": "TZ",
-          "name": "TZ"
-        }
-      ],
-      "image": "authelia/authelia:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/authelia.png",
-      "name": "authelia",
-      "note": "Requires a configuration.yml file in order to work. Documentation is available <a href='https://docs.authelia.com/deployment/deployment-ha'>here</a>.",
-      "platform": "linux",
-      "ports": [
-        "9091:9091/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Authelia",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/authelia",
-          "container": "/etc/authelia/"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Security"
-      ],
-      "description": "This is a Bitwarden server API implementation written in Rust compatible with upstream Bitwarden clients*, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.",
-      "image": "vaultwarden/server:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/vaultwarden.png",
-      "name": "vaultwarden",
-      "note": "This project is not associated with the Bitwarden project nor 8bit Solutions LLC.",
-      "platform": "linux",
-      "ports": [
-        "80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Vaultwarden",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/vaultwarden",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Tools"
-      ],
-      "description": "A clientless remote desktop gateway.",
-      "image": "oznu/guacamole:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/guacamole.png",
-      "name": "guacamole",
-      "note": "The default login will be guacadmin/guacadmin. It is common practice to add a new admin user and remove the default user for Guacamole.",
-      "platform": "linux",
-      "ports": [
-        "8080:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Guacamole",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/guacamole",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Dashboard"
-      ],
-      "description": "A dead simple static HOMepage for your servER to keep your s ervices on hand, from a simple yaml configuration file.",
-      "image": "b4bz/homer:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homer.png",
-      "name": "homer",
-      "note": "This container requires a yml file within the config volume. See the documentation here https://github.com/bastienwirtz/homer",
-      "platform": "linux",
-      "ports": [
-        "8902:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Homer",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/homer/assets",
-          "container": "/www/assets"
-        },
-        {
-          "bind": "/home/docker/homer/config",
-          "container": "/www/config.yml"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Monitoring"
-      ],
-      "description": "Create agents that monitor and act on your behalf.",
-      "image": "huginn/huginn:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/huginn.png",
-      "name": "huginn",
-      "platform": "linux",
-      "ports": [
-        "3000:3000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Huginn",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/huginn",
-          "container": "/var/lib/mysql"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Security"
-      ],
-      "description": "Nginx Proxy Manager enables you to easily forward to your websites running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.",
-      "image": "jc21/nginx-proxy-manager",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nginx-proxy-manager.png",
-      "name": "nginx-proxy-manager",
-      "platform": "linux",
-      "env": [
-        {
-          "label": "DB_SQLITE_FILE",
-          "name": "DB_SQLITE_FILE",
-          "default": "/data/database.sqlite"
-        }
-      ],
-      "ports": [
-        "80:80/tcp",
-        "81:81/tcp",
-        "443:443/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Nginx Proxy Manager",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/nginx-proxy/data",
-          "container": "/data"
-        },
-        {
-          "bind": "/home/docker/nginx-proxy/letsencrypt",
-          "container": "/etc/letsencrypt"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "ownCloud is a self-hosted file sync and share server. It provides access to your data through a web interface, sync clients or WebDAV while providing a platform to view, sync and share across devices easily\u2014all under your control. ownCloud\u2019s open architecture is extensible via a simple but powerful API for applications and plugins and it works with any storage.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "label": "TZ",
-          "name": "TZ"
-        },
-        {
-          "label": "OWNCLOUD_DOMAIN",
-          "name": "OWNCLOUD_DOMAIN"
-        },
-        {
-          "label": "DB_PASSWORD",
-          "name": "DB_PASSWORD"
-        },
-        {
-          "label": "ADMIN_USERNAME",
-          "name": "ADMIN_USERNAME"
-        },
-        {
-          "label": "ADMIN_PASSWORD",
-          "name": "ADMIN_PASSWORD"
-        },
-        {
-          "label": "PORT",
-          "name": "PORT"
-        }
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/owncloud.png",
-      "name": "owncloud",
-      "note": "The database user is owncloud and the database is owncloud.",
-      "platform": "linux",
-      "image": "owncloud/server:latest",
-      "title": "Owncloud",
-      "type": 3
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "A Linux network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole.",
-      "image": "pihole/pihole:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/pihole.png",
-      "name": "pihole",
-      "note": "When the installation is complete, navigate to your.ip.goes.here:1010/admin. Follow the article <a href='https://medium.com/@niktrix/getting-rid-of-systemd-resolved-consuming-port-53-605f0234f32f'>here</a> if you run into issues binding to port 53.",
-      "platform": "linux",
-      "ports": [
-        "53:53/tcp",
-        "53:53/udp",
-        "67:67/udp",
-        "1010:80/tcp",
-        "4443:443/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Pi-Hole",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/pihole",
-          "container": "/etc/pihole"
-        },
-        {
-          "bind": "/home/docker/pihole/DNS",
-          "container": "/etc/dnsmasq.d"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Tools"
-      ],
-      "description": "This container contains OpenVPN and Transmission with a configuration where Transmission is running only when OpenVPN has an active tunnel. It bundles configuration files for many popular VPN providers to make the setup easier.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "MULLVAD",
-          "description": "https://haugene.github.io/docker-transmission-openvpn/supported-providers/",
-          "label": "OPENVPN_PROVIDER",
-          "name": "OPENVPN_PROVIDER"
-        },
-        {
-          "default": "",
-          "label": "OPENVPN_USERNAME",
-          "name": "OPENVPN_USERNAME"
-        },
-        {
-          "default": "",
-          "label": "OPENVPN_PASSWORD",
-          "name": "OPENVPN_PASSWORD"
-        },
-        {
-          "default": "192.168.0.0/24",
-          "label": "LOCAL_NETWORK",
-          "name": "LOCAL_NETWORK"
-        }
-      ],
-      "image": "haugene/transmission-openvpn:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/transmission.png",
-      "name": "transmission-openvpn",
-      "note": "List of supported providers available <a href='https://haugene.github.io/docker-transmission-openvpn/supported-providers'/>here</a>.",
-      "platform": "linux",
-      "ports": [
-        "9091:9091/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Transmission-OpenVPN",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/data"
-        },
-        {
-          "bind": "/etc/localtime",
-          "container": "/etc/localtime"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "Self-hosted, ad-free, privacy-respecting Google metasearch engine.",
-      "image": "benbusby/whoogle-search:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/whoogle-search.png",
-      "name": "whoogle",
-      "platform": "linux",
-      "ports": [
-        "5001:5000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Whoogle",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/whoogle",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "Yacht is a web interface for managing docker containers with an emphasis on templating to provide 1 click deployments. Think of it like a decentralized app store for servers that anyone can make packages for.",
-      "image": "selfhostedpro/yacht:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/yacht.png",
-      "name": "yacht",
-      "platform": "linux",
-      "ports": [
-        "8001:8000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Yacht",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/yacht",
-          "container": "/config"
-        },
-        {
-          "bind": "/var/run/docker.sock",
-          "container": "/var/run/docker.sock"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "categories": [
-        "Tools"
-      ],
-      "title": "Paperless-ng",
-      "name": "paperless-ng",
-      "note": "",
-      "description": "Paperless-ng is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.'",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/paperless-ng.png",
-      "image": "linuxserver/paperless-ng:latest",
-      "env": [
-        {
-          "name": "PUID",
-          "label": "PUID",
-          "default": "1024",
-          "description": "for GroupID"
-        },
-        {
-          "name": "PGID",
-          "label": "PGID",
-          "default": "100",
-          "description": "for UserID"
-        },
-        {
-          "name": "TZ",
-          "label": "TZ",
-          "default": "Europe/Amsterdam",
-          "description": "Specify a timezone to use for example Europe/Amsterdam"
-        },
-        {
-          "name": "REDIS_URL",
-          "label": "REDIS_URL",
-          "default": "",
-          "description": "Specify an external redis instance to use. Can optionally include a port (`redis:6379`) and/or db (`redis/foo`). If left blank or not included, will use a built-in redis instance. If changed after initial setup will also require manual modification of /config/settings.py"
-        }
-      ],
-      "ports": [
-        "8000:8000/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/paperless-ng/config",
-          "container": "/config"
-        },
-        {
-          "container": "/data",
-          "bind": "/home/docker/paperless-ng/data"
-        }
-      ],
-      "restart_policy": "unless-stopped"
-    },
-    {
-      "categories": [
-        "Downloaders",
-        "Multimedia"
-      ],
-      "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",
-      "name": "youtubedl-material",
-      "platform": "linux",
-      "ports": [
-        "17442:17442/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "YouTubeDL-Material",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/ytdlm",
-          "container": "/app/appdata"
-        },
-        {
-          "bind": "/home/docker/ytdlm/video",
-          "container": "/app/video"
-        },
-        {
-          "bind": "/home/docker/ytdlm/subscriptions",
-          "container": "/app/subscriptions"
-        },
-        {
-          "bind": "/home/docker/ytdlm/users",
-          "container": "/app/users"
-        },
-        {
-          "bind": "/home/docker/ytdlm/audio",
-          "container": "/app/audio"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools",
-        "Gaming"
-      ],
-      "description": "C# application with primary purpose of farming Steam cards from multiple accounts simultaneously.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "justarchi/archisteamfarm:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/archisteamfarm.png",
-      "name": "archisteamfarm",
-      "platform": "linux",
-      "ports": [
-        "1242:1242/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "ArchiSteamFarm",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/archiSteamFarm/config",
-          "container": "/app/config"
-        },
-        {
-          "bind": "/home/docker/archiSteamFarm/plugins",
-          "container": "/app/plugins"
-        },
-        {
-          "bind": "/home/docker/archiSteamFarm/logs",
-          "container": "/app/logs"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "ArchiveBox is a powerful, self-hosted internet archiving solution to collect, save, and view sites you want to preserve offline.",
-      "env": [
-        {
-          "default": "*",
-          "label": "ALLOWED_HOSTS",
-          "name": "ALLOWED_HOSTS"
-        },
-        {
-          "default": "750m",
-          "label": "MEDIA_MAX_SIZE",
-          "name": "MEDIA_MAX_SIZE"
-        },
-        {
-          "default": "true",
-          "label": "PUBLIC_INDEX",
-          "name": "PUBLIC_INDEX"
-        },
-        {
-          "default": "true",
-          "label": "PUBLIC_SNAPSHOTS",
-          "name": "PUBLIC_SNAPSHOTS"
-        },
-        {
-          "default": "false",
-          "label": "PUBLIC_ADD_VIEW",
-          "name": "PUBLIC_ADD_VIEW"
-        }
-      ],
-      "image": "archivebox/archivebox:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/archivebox.png",
-      "name": "archivebox",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "8002:8000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Archivebox",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/archivebox",
-          "container": "/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go. ",
-      "image": "caddy:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/caddy.png",
-      "name": "caddy",
-      "note": "Create firewall rule first: 'sudo ufw allow http' and 'sudo ufw allow https'. Add port 2019/tcp for admin endpoint",
-      "platform": "linux",
-      "network": "host",
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp",
-        "443:443/udp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Caddy",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "caddy",
-          "container": "/data"
-        },
-        {
-          "bind": "caddy",
-          "container": "/config"
-        },
-        {
-          "bind": "caddyfiles",
-          "container": "/etc/caddy"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Security",
-        "Tools"
-      ],
-      "description": "ClamAV is an open source antivirus engine for detecting trojans, viruses, malware & other malicious threats.",
-      "image": "clamav/clamav",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/clamav.png",
-      "name": "clamav",
-      "platform": "linux",
-      "ports": [
-        "3310:3310/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "ClamAV",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/etc/timezone",
-          "container": "/etc/timezone:ro"
-        },
-        {
-          "bind": "/home/docker/clamav/virus_definitions",
-          "container": "/var/lib/clamav"
-        },
-        {
-          "bind": "/home/docker",
-          "container": "/scandir"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Arr",
-        "Tools"
-      ],
-      "description": "FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.",
-      "env": [
-        {
-          "default": "info",
-          "label": "LOG_LEVEL",
-          "name": "LOG_LEVEL"
-        },
-        {
-          "default": "false",
-          "label": "LOG_HTML",
-          "name": "LOG_HTML"
-        },
-        {
-          "default": "none",
-          "label": "CAPTCHA_SOLVER",
-          "name": "CAPTCHA_SOLVER"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        }
-      ],
-      "image": "ghcr.io/flaresolverr/flaresolverr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/flaresolverr.png",
-      "name": "flaresolverr",
-      "platform": "linux",
-      "ports": [
-        "8191:8191/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "FlareSolverr",
-      "type": 1,
-      "note": ""
-    },
-    {
-      "categories": [
-        "Gaming",
-        "Paid"
-      ],
-      "description": "This docker image provides the FoundryVTT system for hosting your own virtual table top games.",
-      "env": [
-        {
-          "default": "John",
-          "label": "Foundry Account Name",
-          "name": "FOUNDRY_USERNAME"
-        },
-        {
-          "default": "password",
-          "label": "Foundry Password",
-          "name": "FOUNDRY_PASSWORD"
-        },
-        {
-          "default": "changeme",
-          "label": "Instance Admin Password",
-          "name": "FOUNDRY_ADMIN_KEY"
-        },
-        {
-          "default": "true",
-          "label": "CONTAINER_PRESERVE_CONFIG",
-          "name": "CONTAINER_PRESERVE_CONFIG"
-        }
-      ],
-      "image": "felddy/foundryvtt:release",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/foundryvtt.png",
-      "name": "foundryVTT",
-      "platform": "linux",
-      "ports": [
-        "30000:30000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "FoundryVTT Server",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/foundryvtt",
-          "container": "/data"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "CMS"
-      ],
-      "description": "Ghost is a free and open source blogging platform written in JavaScript and distributed under the MIT License, designed to simplify the process of online publishing for individual bloggers as well as online publications.",
-      "env": [
-        {
-          "default": "development",
-          "label": "NODE_ENV",
-          "name": "NODE_ENV"
-        },
-        {
-          "default": "http://localhost/",
-          "label": "url",
-          "name": "url"
-        }
-      ],
-      "image": "ghost:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/ghost.png",
-      "name": "ghost",
-      "platform": "linux",
-      "ports": [
-        "2368:2368/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Ghost",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/ghost",
-          "container": "/var/lib/ghost/content"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Dashboard",
-        "Web",
-        "Other"
-      ],
-      "description": "Homarr is a simple and lightweight homepage for your server, that helps you easily access all of your services in one place.",
-      "note": "",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "ghcr.io/ajnart/homarr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/homarr.png",
-      "name": "homarr-secured",
-      "platform": "linux",
-      "ports": [
-        "7575:7575/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Homarr-Secured",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/homarr/configs",
-          "container": "/app/data/configs"
-        },
-        {
-          "bind": "/home/docker/homarr/icons",
-          "container": "/app/public/icons"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools"
-      ],
-      "description": "Homebridge allows you to integrate with smart home devices that do not natively support HomeKit. There are over 2,000 Homebridge plugins supporting thousands of different smart accessories.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1",
-          "label": "HOMEBRIDGE_CONFIG_UI",
-          "name": "HOMEBRIDGE_CONFIG_UI"
-        },
-        {
-          "default": "8581",
-          "label": "HOMEBRIDGE_CONFIG_UI_PORT",
-          "name": "HOMEBRIDGE_CONFIG_UI_PORT"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        }
-      ],
-      "image": "homebridge/homebridge:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/homebridge.png",
-      "name": "homebridge",
-      "network": "host",
-      "note": "",
-      "platform": "linux",
-      "privileged": true,
-      "restart_policy": "unless-stopped",
-      "title": "Homebridge",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/homebridge",
-          "container": "/homebridge"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders",
-        "Tools"
-      ],
-      "description": "JDownloader 2 is a free, open-source download management tool with a huge community of developers that makes downloading as easy and fast as it should be. Users can start, stop or pause downloads, set bandwith limitations, auto-extract archives and much more. It's an easy-to-extend framework that can save hours of your valuable time every day!. <a href='https://hub.docker.com/r/jlesage/jdownloader-2'>Docker Hub</a>",
-      "env": [
-        {
-          "default": "",
-          "label": "MYJD_DEVICE_NAME",
-          "name": "MYJD_DEVICE_NAME"
-        },
-        {
-          "default": "",
-          "label": "MYJD_USER",
-          "name": "MYJD_USER"
-        },
-        {
-          "default": "",
-          "label": "MYJD_PASSWORD",
-          "name": "MYJD_PASSWORD"
-        }
-      ],
-      "image": "jlesage/jdownloader-2",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/jdownloader.png",
-      "name": "jdownloader",
-      "platform": "linux",
-      "ports": [
-        "5800:5800/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "JDownloader",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/jdownloader",
-          "container": "/opt/JDownloader/app/cfg"
-        },
-        {
-          "bind": "/media",
-          "container": "/opt/JDownloader/Downloads"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "An alternative private front-end to Reddit",
-      "image": "libreddit/libreddit:armv7",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/libreddit.png",
-      "name": "libreddit",
-      "platform": "linux",
-      "ports": [
-        "8088:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Libreddit",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/libreddit",
-          "container": "/config"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Authentication",
-        "LDAP"
-      ],
-      "description": "This project is a lightweight authentication server that provides an opinionated, simplified LDAP interface for authentication.",
-      "env": [
-        {
-          "default": "somesecretjwt",
-          "label": "LLDAP_JWT_SECRET",
-          "name": "LLDAP_JWT_SECRET"
-        },
-        {
-          "default": "someadminpassword",
-          "label": "LLDAP_LDAP_USER_PASS",
-          "name": "LLDAP_LDAP_USER_PASS"
-        },
-        {
-          "default": "dc=example,dc=com",
-          "label": "LLDAP_LDAP_BASE_DN",
-          "name": "LLDAP_LDAP_BASE_DN"
-        }
-      ],
-      "image": "nitnelave/lldap:stable-debian",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/lldap.png",
-      "name": "lldap",
-      "platform": "linux",
-      "ports": [
-        "3890:3890/tcp",
-        "17170:17170/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "LLDAP",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/lldap",
-          "container": "/data"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "A self-hosted recipe manager and meal planner",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        },
-        {
-          "default": "2",
-          "label": "WEB_CONCURRENCY",
-          "name": "WEB_CONCURRENCY"
-        },
-        {
-          "default": "8",
-          "label": "MAX_WORKERS",
-          "name": "MAX_WORKERS"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_PUBLIC",
-          "name": "RECIPE_PUBLIC"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_SHOW_NUTRITION",
-          "name": "RECIPE_SHOW_NUTRITION"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_SHOW_ASSETS",
-          "name": "RECIPE_SHOW_ASSETS"
-        },
-        {
-          "default": "true",
-          "label": "RECIPE_LANDSCAPE_VIEW",
-          "name": "RECIPE_LANDSCAPE_VIEW"
-        },
-        {
-          "default": "false",
-          "label": "RECIPE_DISABLE_COMMENTS",
-          "name": "RECIPE_DISABLE_COMMENTS"
-        },
-        {
-          "default": "false",
-          "label": "RECIPE_DISABLE_AMOUNT",
-          "name": "RECIPE_DISABLE_AMOUNT"
-        }
-      ],
-      "image": "hkotel/mealie:v0.4.3",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mealie.png",
-      "name": "mealie",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "9925:80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Mealie",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/mealie",
-          "container": "/app/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools",
-        "Gaming"
-      ],
-      "description": "This docker image provides a Minecraft Server that will automatically download the latest stable version at startup. You can also run/upgrade to any specific version or the latest snapshot. See the Versions section below for more information.",
-      "env": [
-        {
-          "default": "TRUE",
-          "label": "EULA",
-          "name": "EULA"
-        }
-      ],
-      "image": "itzg/minecraft-server:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/minecraft.png",
-      "name": "minecraft",
-      "platform": "linux",
-      "ports": [
-        "25565:25565/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Minecraft Server",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/minecraft-data",
-          "container": "/data"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Networking",
-        "Monitoring"
-      ],
-      "description": "Troubleshoot slowdowns and anomalies in your infrastructure with thousands of per-second metrics, meaningful visualizations, and insightful health alarms with zero configuration.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "DOCKER_USR",
-          "name": "DOCKER_USR"
-        },
-        {
-          "default": "1000",
-          "label": "DOCKER_GRP",
-          "name": "DOCKER_GRP"
-        }
-      ],
-      "image": "netdata/netdata:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/netdata.png",
-      "name": "netdata",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "19999:19999/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Netdata",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/netdata/netdataconfig",
-          "container": "/etc/netdata"
-        },
-        {
-          "bind": "/home/docker/netdata/netdatalib",
-          "container": "/var/lib/netdata"
-        },
-        {
-          "bind": "/etc/passwd",
-          "container": "/host/etc/passwd:ro"
-        },
-        {
-          "bind": "/etc/group",
-          "container": "/host/etc/group:ro"
-        },
-        {
-          "bind": "/proc",
-          "container": "/host/proc:ro"
-        },
-        {
-          "bind": "/sys",
-          "container": "/host/sys:ro"
-        },
-        {
-          "bind": "/etc/os-release",
-          "container": "/host/etc/os-release:ro"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "Organizr allows you to setup Tabs that will be loaded all in one webpage. You can then work on your server with ease.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "organizr/organizr:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/organizr.png",
-      "name": "organizr-v2",
-      "platform": "linux",
-      "ports": [
-        "7171:80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Organizr v2",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/organizr",
-          "container": "/config"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "type": 1,
-      "name": "postgres",
-      "title": "PostgreSQL",
-      "note": "",
-      "description": "PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance. It was originally named POSTGRES, referring to its origins as a successor to the Ingres database developed at the University of California, Berkeley. <a href='https://www.postgresql.org/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/postgres' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/postgres.png",
-      "image": "postgres",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/postgres",
-          "container": "/var/lib/postgresql/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "5432",
-          "container": "5432"
-        }
-      ],
-      "env": [
-        {
-          "name": "POSTGRES_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "POSTGRES_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "POSTGRES_DB",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "heimdall"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Downloaders"
-      ],
-      "description": "A docker image with qBittorrent and the Flood UI, also optional WireGuard VPN support. See the official documentation for WireGuard VPN support at https://hotio.dev/containers/qflood/",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "default": "002",
-          "label": "UMASK",
-          "name": "UMASK"
-        },
-        {
-          "default": "America/New_York",
-          "label": "TZ",
-          "name": "TZ"
-        },
-        {
-          "default": "false",
-          "label": "FLOOD_AUTH",
-          "name": "FLOOD_AUTH"
-        }
-      ],
-      "image": "hotio/qflood:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/qflood.png",
-      "name": "qflood",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "3000:3000/tcp",
-        "8080:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "qFlood",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/qflood",
-          "container": "/config"
-        },
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/app/qBittorrent/downloads"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "A remote desktop software, the open source TeamViewer alternative, works out of the box, no configuration required. You have full control of your data, with no concerns about security.",
-      "env": [
-        {
-          "default": "rustdesk.example.com:21117",
-          "description": "Use your domain with the default 21117 port",
-          "label": "RELAY",
-          "name": "RELAY"
-        },
-        {
-          "default": "1",
-          "description": "if set to \"1\" unencrypted connection will not be accepted",
-          "label": "ENCRYPTED_ONLY",
-          "name": "ENCRYPTED_ONLY"
-        }
-      ],
-      "image": "rustdesk/rustdesk-server-s6:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/rustdesk.png",
-      "name": "rustdesk",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "21115:21115/tcp",
-        "21116:21116/tcp",
-        "21116:21116/udp",
-        "21117:21117/tcp",
-        "21118:21118/tcp",
-        "21119:21119/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "RustDesk",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/rustdesk",
-          "container": "/data"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "Open-Source Privacy-respecting metasearch engine",
-      "env": [
-        {
-          "default": "http://localhost:9017",
-          "label": "BASE_URL",
-          "name": "BASE_URL"
-        },
-        {
-          "default": "my-instance",
-          "label": "INSTANCE_NAME",
-          "name": "INSTANCE_NAME"
-        }
-      ],
-      "image": "searxng/searxng:latest",
-      "logo": "https://raw.githubusercontent.com/pi-hosted/pi-hosted/master/images/searx.png",
-      "name": "searxng",
-      "platform": "linux",
-      "ports": [
-        "9017:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "SearXNG",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/searxng",
-          "container": "/etc/searxng"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "type": 1,
-      "name": "surrealdb",
-      "title": "SurrealDB",
-      "note": "",
-      "description": "SurrealDB acts as both a database and a modern, realtime, collaborative API backend layer. SurrealDB can run as a single server or in a highly-available, highly-scalable distributed mode - with support for SQL querying from client devices, GraphQL, ACID transactions, WebSocket connections, structured and unstructured data, graph querying, full-text indexing, geospatial querying, and row-by-row permissions-based access. <a href='https://surrealdb.co/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/surrealdb/surrealdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/surrealdb.png",
-      "image": "surrealdb/surrealdb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/surrealdb",
-          "container": "/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "3306",
-          "container": "3306"
-        }
-      ],
-      "env": [
-        {
-          "name": "SURREALDB_ADMIN_PASSWORD",
-          "label": "Admin Password",
-          "description": "Admin password for SurrealDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "SURREALDB_ADMIN_EMAIL",
-          "label": "Admin Email",
-          "description": "Admin email for SurrealDB",
-          "type": "text",
-          "default": "root"
-
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Networking",
-        "Security"
-      ],
-      "description": "Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience. It receives requests on behalf of your system and finds out which components are responsible for handling them. ",
-      "image": "traefik:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/traefik.png",
-      "name": "traefik",
-      "note": "",
-      "platform": "linux",
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp",
-        "8080:8080/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "command": "--api.insecure=true --providers.docker",
-      "title": "Traefik",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/traefik/traefik.yml",
-          "container": "/traefik.yml"
-        },
-        {
-          "bind": "/home/docker/traefik/config.yml",
-          "container": "/config.yml"
-        },
-        {
-          "bind": "/home/docker/traefik/acme.json",
-          "container": "/acme.json"
-        },
-        {
-          "bind": "/var/run/docker.sock",
-          "container": "/var/run/docker.sock"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Tools",
-        "Multimedia"
-      ],
-      "description": "Unmanic is a simple tool for optimising your file library. You can use it to convert your files into a single, uniform format, manage file movements based on timestamps, or execute custom commands against a file based on its file size.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "josh5/unmanic:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/unmanic.png",
-      "name": "unmanic",
-      "platform": "linux",
-      "ports": [
-        "8888:8888/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Unmanic",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/unmanic",
-          "container": "/config"
-        },
-        {
-          "bind": "/media",
-          "container": "/library"
-        },
-        {
-          "bind": "/var/tmp/unmanic",
-          "container": "/tmp/unmanic"
-        }
-      ],
-      "note": ""
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "Quickly and easily configure Wireguard through a web browser.",
-      "env": [
-        {
-          "default": "example.domain.com",
-          "description": "Set here your DDNS domain",
-          "label": "WG_HOST",
-          "name": "WG_HOST"
-        },
-        {
-          "default": "ENTER AN ADMIN PASSWORD",
-          "description": "Leave blank to access WebUI without loggin",
-          "label": "PASSWORD",
-          "name": "PASSWORD"
-        },
-        {
-          "default": "51820",
-          "label": "WG_PORT",
-          "name": "WG_PORT"
-        },
-        {
-          "default": "1.1.1.1",
-          "label": "WG_DEFAULT_DNS",
-          "name": "WG_DEFAULT_DNS"
-        },
-        {
-          "default": "10.8.0.x",
-          "label": "WG_DEFAULT_ADDRESS",
-          "name": "WG_DEFAULT_ADDRESS"
-        },
-        {
-          "default": "0.0.0.0/0, ::/0",
-          "label": "WG_ALLOWED_IPS",
-          "name": "WG_ALLOWED_IPS"
-        }
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wireguard.png",
-      "name": "wg-easy",
-      "platform": "linux",
-      "image": "weejewel/wg-easy",
-      "title": "WireGuard-Easy",
-      "type": 3,
-      "note": ""
-    },
-    {
-      "categories": [
-        "CMS"
-      ],
-      "description": "WordPress is a web content management system. It was originally created as a tool to publish blogs but has evolved to support publishing other web content, including more traditional websites, mailing lists and Internet forum, media galleries, membership sites, learning management systems and online stores.",
-      "env": [
-        {
-          "default": "db",
-          "label": "Port or host of database server",
-          "name": "WORDPRESS_DB_HOST"
-        },
-        {
-          "default": "exampleuser",
-          "label": "Database user name",
-          "name": "WORDPRESS_DB_USER"
-        },
-        {
-          "default": "examplepass",
-          "label": "Database password for user",
-          "name": "WORDPRESS_DB_PASSWORD"
-        },
-        {
-          "default": "exampledb",
-          "label": "Database name",
-          "name": "WORDPRESS_DB_NAME"
-        }
-      ],
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/wordpress.png",
-      "note": "Create a mysql or mariadb database for the container first.",
-      "platform": "linux",
-      "image": "wordpress:latest",
-      "title": "Wordpress",
-      "name": "wordpress",
-      "type": 3,
-      "restart_policy": "unless-stopped",
-      "ports": [
-        "8080:80/tcp"
-      ],
-      "network": "AppBridge",
-      "volumes": [
-        {
-          "bind": "/home/docker/wordpress",
-          "container": "/var/www/html"
-        }
-      ]
-    },
-    {
-      "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"
-      ],
-      "description": "McMyAdmin 2 is the leading web control panel and administration console for Minecraft servers.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ],
-      "image": "linuxserver/mcmyadmin2:latest",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mcmyadmin2.png",
-      "name": "mcmyadmin2",
-      "platform": "linux",
-      "ports": [
-        "8080:8080/tcp",
-        "25565:25565/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "McMyAdmin 2",
-      "type": 1,
-      "volumes": [
-        {
-          "container": "/minecraft"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "Tools"
-      ],
-      "description": "A one-of-a-kind resume builder that's not out to get your data. Completely secure, customizable, portable, open-source and free forever.",
-      "image": "amruthpillai/reactive-resume:latest",
-      "logo": "https://raw.githubusercontent.com/SelfhostedPro/selfhosted_templates/master/Images/reactiveresume.png",
-      "name": "reactive-resume",
-      "platform": "linux",
-      "ports": [
-        "80/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Reactive-Resume",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/reactiveresume",
-          "container": "/usr/src/app"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other",
-        "VPN",
-        "Tools"
-      ],
-      "description": "This container contains OpenVPN and Deluge with a configuration where Deluge is running only when OpenVPN has an active tunnel. It bundles configuration files for many popular VPN providers to make the setup easier.",
-      "env": [
-        {
-          "default": "1001",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1001",
-          "label": "PGID",
-          "name": "PUID"
-        },
-        {
-          "default": "MULLVAD",
-          "description": "see https://github.com/sgtsquiggs/docker-deluge-openvpn",
-          "label": "OPENVPN_PROVIDER",
-          "name": "OPENVPN_PROVIDER"
-        },
-        {
-          "label": "OPENVPN_USERNAME",
-          "name": "OPENVPN_USERNAME"
-        },
-        {
-          "label": "OPENVPN_PASSWORD",
-          "name": "OPENVPN_PASSWORD"
-        }
-      ],
-      "image": "sgtsquiggs/deluge-openvpn:latest",
-      "name": "deluge-openvpn",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/deluge.png",
-      "platform": "linux",
-      "ports": [
-        "8112:8112/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Deluge OpenVPN",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/etc/localtime",
-          "container": "/etc/localtime"
-        },
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/downloads"
-        },
-        {
-          "bind": "/home/docker/delugeopenvpn/config",
-          "container": "/config"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Multimedia"
-      ],
-      "note": "",
-      "description": "Tdarr is a popular conditional transcoding application for processing large (or small) media libraries. The application comes in the form of a click-to-run web-app, which you run on your own device and access through a web browser.",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/tdarr.png",
-      "name": "tdarr",
-      "platform": "linux",
-      "image": "ghcr.io/haveagitgat/tdarr",
-      "title": "Tdarr",
-      "type": 3,
-      "volumes": [
-        {
-          "bind": "/home/docker/tdarr/configs",
-          "container": "/app/configs"
-        },
-        {
-          "bind": "/portainer/Downloads",
-          "container": "/downloads"
-        }
-      ],
-      "ports": [
-        "8265:8265/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "1000",
-          "label": "PGID",
-          "name": "PGID"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Finance"
-      ],
-      "description": "Cryptofolio is an open-source, and self-hosted solution for tracking your cryptocurrency holdings. It features a web interface, an Android mobile app, and a cross-platform desktop application for Windows, macOS, and Linux.",
-      "image": "xtrendence/cryptofolio:latest",
-      "logo": "https://i.imgur.com/5v8lzea.png",
-      "name": "cryptofolio",
-      "platform": "linux",
-      "ports": [
-        "7280:80/tcp"
-      ],
-      "restart_policy": "always",
-      "title": "Cryptofolio",
-      "type": 1
-    },
-    {
-      "categories": [
-        "Networking"
-      ],
-      "description": "An easy to use Status Page for your websites and applications. Statping will automatically fetch the application and render a beautiful status page with tons of features for you to build an even better status page.",
-      "image": "statping/statping:latest",
-      "logo": "https://raw.githubusercontent.com/xneo1/portainer_templates/master/Images/statping.png",
-      "name": "statping",
-      "platform": "linux",
-      "ports": [
-        "4040:8080/tcp"
-      ],
-      "restart_policy": "always",
-      "title": "Statping",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/home/docker/statping",
-          "container": "/app"
-        }
-      ]
-    },
-    {
-      "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",
-      "title": "InfluxDB",
-      "note": "",
-      "description": "InfluxDB is an open source time series database developed by InfluxData. It is written in Go and optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics. <a href='https://www.influxdata.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/influxdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/influxdb.png",
-      "image": "influxdb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/influxdb",
-          "container": "/var/lib/influxdb",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "8086",
-          "container": "8086"
-        }
-      ],
-      "env": [
-        {
-          "name": "INFLUXDB_ADMIN_USER",
-          "label": "Admin User",
-          "description": "Admin user for InfluxDB",
-          "type": "text",
-          "default": "admin"
-        },
-        {
-          "name": "INFLUXDB_ADMIN_PASSWORD",
-          "label": "Admin Password",
-          "description": "Admin password for InfluxDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "INFLUXDB_DB",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "mydb"
-        }
-      ]
-    },
-    {
-      "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",
-        "LDAP"
-      ],
-      "description": "Tubearchivist is your self hosted YouTube media server",
-      "note": "Requires a Redis and Elasticsearch database. Tube Archivist needs around 2GB of available memory for a small testing setup and around 4GB of available memory for a mid to large sized installation. Minimal with dual core with 4 threads, better quad core plus.",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/tube-archivist.png",
-      "name": "tubearchivist",
-      "platform": "linux",
-      "image": "bbilly1/tubearchivist",
-      "title": "Tubearchivist",
-      "type": 3,
-      "ports": [
-        "8080:8080/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/media/youtube",
-          "container": "/youtube"
-        }
-      ],
-      "env": [
-        {
-          "name": "INFLUXDB_ADMIN_USER",
-          "label": "Admin User",
-          "description": "Admin user for InfluxDB",
-          "type": "text",
-          "default": "admin"
-        },
-        {
-          "name": "INFLUXDB_ADMIN_PASSWORD",
-          "label": "Admin Password",
-          "description": "Admin password for InfluxDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "INFLUXDB_DB",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "mydb"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Finance"
-      ],
-      "description": "Actual is a local-first personal finance tool. It is 100% free and open-source. It has a synchronization element so that all your changes can move between devices without any heavy lifting.",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/actual.png",
-      "name": "actual-server",
-      "platform": "linux",
-      "image": "actualbudget/actual-server:latest",
-      "restart_policy": "unless-stopped",
-      "title": "Actual-Server",
-      "ports": [
-        "5006:5006/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/actual",
-          "container": "/data"
-        }
-      ],
-      "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"
-      ],
-      "description": "Appsmith (www.appsmith.com) is the first open-source low code tool that helps developers build dashboards and admin panels very quickly.",
-      "logo": "https://cdn-images.himalayas.app/vr60veq4neiptamhqm6qxwi3toi3",
-      "name": "appsmith",
-      "platform": "linux",
-      "image": "appsmith/appsmith-ce",
-      "title": "Appsmith",
-      "ports": [
-        "80:80/tcp",
-        "443:443/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/appsmith",
-          "container": "/appsmith-stacks"
-        }
-      ],
-      "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"
-      ],
-      "note": "Requires a MySQL database. Create a database first.",
-      "description": "Leantime is an open source project management system for small teams and startups written in PHP, Javascript using MySQL. https://leantime.io",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/leantime.png",
-      "name": "leantime",
-      "platform": "linux",
-      "image": "leantime/leantime:latest",
-      "title": "Leantime",
-      "type": 3,
-      "ports": [
-        "80:80/tcp"
-      ],
-      "env": [
-        {
-          "name": "MYSQL_HOST",
-          "label": "Database Host",
-          "description": "Database host",
-          "type": "text",
-          "default": "mysql_leantime"
-        },
-        {
-          "name": "MYSQL_USER",
-          "label": "Database User",
-          "description": "Database user",
-          "type": "text",
-          "default": "leantime_user"
-        },
-        {
-          "name": "MYSQL_PASSWORD",
-          "label": "Database Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "choose.a.long.random.password"
-        },
-        {
-          "name": "MYSQL_DATABASE",
-          "label": "Database Name",
-          "description": "Database name",
-          "type": "text",
-          "default": "leantime_database"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "Other"
-      ],
-      "description": "Jellyseerr is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!. <a href='https://github.com/Fallenbagel/jellyseerr/'>Github</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/jellyseerr.png",
-      "name": "jellyseerr",
-      "note": "For Emby : add the environment variable JELLYFIN_TYPE=emby . (this will allow you to use the play button)",
-      "platform": "linux",
-      "image": "fallenbagel/jellyseerr:latest",
-      "title": "Jellyseerr",
-      "type": 3,
-      "volumes": [
-        {
-          "bind": "/home/docker/jellyseerr",
-          "container": "/app/config"
-        }
-      ],
-      "ports": [
-        "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"
-      ],
-      "description": "Dashdot is a modern server dashboard, running on the latest tech, designed with glassmorphism in mind. It is intended to be used for smaller VPS and private servers.",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dashdot.png",
-      "name": "dashdot",
-      "platform": "linux",
-      "image": "mauricenino/dashdot",
-      "title": "Dashdot",
-      "type": 3,
-      "ports": [
-        "80:3001/tcp"
-      ],
-      "volumes": [
-        {
-          "bind": "/",
-          "container": "/mnt/host:ro"
-        }
-      ],
-      "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",
-      "note": "",
-      "description": "NocoDB is a free, open-source, self-hosted, no-code platform to make database driven application. <a href='https://www.nocodb.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nocodb/nocodb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/nocodb.png",
-      "image": "nocodb/nocodb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/nocodb",
-          "container": "/var/lib/nocodb",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "8080",
-          "container": "8080"
-        }
-      ],
-      "env": [
-        {
-          "name": "NOCODB_DB_HOST",
-          "label": "Database Host",
-          "description": "Database host",
-          "type": "text",
-          "default": "mysql"
-        },
-        {
-          "name": "NOCODB_DB_PORT",
-          "label": "Database Port",
-          "description": "Database port",
-          "type": "text",
-          "default": "3306"
-        },
-        {
-          "name": "NOCODB_DB_USER",
-          "label": "Database User",
-          "description": "Database user",
-          "type": "text",
-          "default": "root"
-        },
-        {
-          "name": "NOCODB_DB_PASSWORD",
-          "label": "Database Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "NOCODB_DB_NAME",
-          "label": "Database Name",
-          "description": "Database name",
-          "type": "text",
-          "default": "nocodb"
-        }
-      ]
-    },
-    {
-      "categories": [
-        "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"
-      ],
-      "description": "AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it\u2019ll cover ALL your home devices, and you don\u2019t need any client-side software for that. With the rise of Internet-Of-Things and connected devices, it becomes more and more important to be able to control your whole network.",
-      "env": [
-        {
-          "default": "1000",
-          "label": "PUID",
-          "name": "PUID"
-        },
-        {
-          "default": "100",
-          "label": "PGID",
-          "name": "PGID"
-        },
-        {
-          "label": "CONTEXT_PATH",
-          "name": "CONTEXT_PATH",
-          "set": "adguard home"
-        }
-      ],
-      "note": "DNS-over-HTTPS: [80:80/TCP] [443:443/TCP] [443:443/UDP] [3000:3000/TCP] [DEFAULT]. DNS: [53:53/TCP] [53:53/UDP]. Admin Panel: [3000:3000/TCP]. DHCP: [67:67/UDP] [68:68/TCP] [68:68/UDP]. DNS-over-TLS: [853:853/TCP]. DNS-over-QUIC: [784:784/UDP] [853:853/UDP] [8853:8853/UDP]. DNSCrypt: [5443:5443/TCP] [5443:5443/UDP]",
-      "image": "adguard/adguardhome:latest",
-      "logo": "https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Images/adguard.png",
-      "name": "adguard",
-      "platform": "linux",
-      "ports": [
-        "53:53/tcp",
-        "53:53/udp",
-        "67:67/udp",
-        "68:68/tcp",
-        "68:68/udp",
-        "80:80/tcp",
-        "443:443/tcp",
-        "853:853/tcp",
-        "3000:3000/tcp"
-      ],
-      "restart_policy": "unless-stopped",
-      "title": "Adguard",
-      "type": 1,
-      "volumes": [
-        {
-          "bind": "/portainer/Files/AppData/Adguard/Workdir",
-          "container": "/opt/adguardhome/work"
-        },
-        {
-          "bind": "/portainer/Files/AppData/Adguard/Conf",
-          "container": "/opt/adguardhome/conf"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "mongodb",
-      "title": "MongoDB",
-      "note": "",
-      "description": "MongoDB is a source-available cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with optional schemas. <a href='https://www.mongodb.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/mongo' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/mongodb.png",
-      "image": "mongo",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/mongodb",
-          "container": "/data/db",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "27017",
-          "container": "27017"
-        }
-      ],
-      "env": [
-        {
-          "name": "MONGO_INITDB_ROOT_USERNAME",
-          "label": "Root Username",
-          "description": "Root username for MongoDB",
-          "type": "text",
-          "default": "root"
-        },
-        {
-          "name": "MONGO_INITDB_ROOT_PASSWORD",
-          "label": "Root Password",
-          "description": "Root password for MongoDB",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "cratedb",
-      "title": "CrateDB",
-      "note": "",
-      "description": "CrateDB is a distributed SQL database that combines SQL and search in a way that's simple to scale. It is designed to be highly available, horizontally scalable, and easy to use. <a href='https://crate.io/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/crate' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/cratedb.png",
-      "image": "crate",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/cratedb",
-          "container": "/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "4200",
-          "container": "4200"
-        },
-        {
-          "host": "4300",
-          "container": "4300"
-        }
-      ],
-      "env": [
-        {
-          "name": "CRATE_HEAP_SIZE",
-          "label": "Heap Size",
-          "description": "Heap size for CrateDB",
-          "type": "text",
-          "default": "2g"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "elasticsearch",
-      "title": "Elasticsearch",
-      "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",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/elasticsearch",
-          "container": "/usr/share/elasticsearch/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "9200",
-          "container": "9200"
-        },
-        {
-          "host": "9300",
-          "container": "9300"
-        }
-      ],
-      "env": [
-        {
-          "name": "discovery.type",
-          "label": "Discovery Type",
-          "description": "Discovery type for Elasticsearch",
-          "type": "text",
-          "default": "single-node"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "SQL Server",
-      "name": "mssql",
-      "description": "Official image for Microsoft SQL Server based on Ubuntu 20.04. <a href=\"https://hub.docker.com/_/microsoft-mssql-server\" target=\"_blank\">Docker Hub</a>",
-      "categories": [
-        "Database"
-      ],
-      "platform": "linux",
-      "note": "Requires at least 2GB of RAM. Make sure to assign enough memory to the Docker VM if you're running on Docker for Mac or Windows. Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.",
-      "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png",
-      "image": "mcr.microsoft.com/mssql/server:2022-latest",
-      "ports": [
-        "1433/tcp"
-      ],
-      "env": [
-        {
-          "name": "ACCEPT_EULA",
-          "default": "Y",
-          "preset": true
-        },
-        {
-          "name": "MSSQL_SA_PASSWORD",
-          "label": "MSSQL_SA_PASSWORD",
-          "default": "YOUR_STRONG_PASSWORD"
-        },
-        {
-          "name": "MSSQL_PID",
-          "label": "MSSQL_PID",
-          "default": "Developer"
-        }
-      ]
-    },
-    {
-      "name": "redis",
-      "title": "Redis",
-      "note": "",
-      "description": "Redis is an in-memory data structure project implementing a distributed, in-memory key-value database with optional durability. Redis supports different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indexes. <a href='https://redis.io/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/redis' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/redis.png",
-      "image": "redis",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/redis",
-          "container": "/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "6379",
-          "container": "6379"
-        }
-      ],
-      "env": [
-        {
-          "name": "REDIS_PASSWORD",
-          "label": "Password",
-          "description": "Password for Redis",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Joomla",
-      "name": "joomla",
-      "description": "Joomla! is an award-winning content management system (CMS), which enables you to build web sites and powerful online applications.",
-      "categories": [
-        "CMS"
-      ],
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/joomla.png",
-      "image": "joomla:latest",
-      "env": [
-        {
-          "name": "JOOMLA_DB_HOST",
-          "label": "MySQL database host",
-          "type": "container"
-        },
-        {
-          "name": "JOOMLA_DB_PASSWORD",
-          "label": "Database password"
-        }
-      ],
-      "ports": [
-        "80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/var/www/html"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "title": "Drupal",
-      "name": "drupal",
-      "description": "Open-source content management framework",
-      "categories": [
-        "CMS"
-      ],
-      "platform": "linux",
-      "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/drupal.png",
-      "image": "drupal:latest",
-      "ports": [
-        "80/tcp"
-      ],
-      "volumes": [
-        {
-          "container": "/var/www/html"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "cockroachdb",
-      "title": "CockroachDB",
-      "note": "",
-      "description": "CockroachDB is a distributed SQL database built on a transactional and strongly-consistent key-value store. It scales horizontally; survives disk, machine, rack, and even datacenter failures with minimal latency disruption and no manual intervention; supports strongly-consistent ACID transactions; and provides a familiar SQL API for structuring, manipulating, and querying data. <a href='https://www.cockroachlabs.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/cockroachdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/cockroachdb.png",
-      "image": "cockroachdb/cockroach",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/cockroachdb",
-          "container": "/cockroach/cockroach-data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "26257",
-          "container": "26257"
-        },
-        {
-          "host": "8080",
-          "container": "8080"
-        }
-      ],
-      "env": [
-        {
-          "name": "COCKROACH_CHANNEL",
-          "label": "Channel",
-          "description": "CockroachDB channel",
-          "type": "text",
-          "default": "stable"
-        },
-        {
-          "name": "COCKROACH_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "root"
-        },
-        {
-          "name": "COCKROACH_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "mariadb",
-      "title": "MariaDB",
-      "note": "",
-      "description": "MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system (RDBMS), intended to remain free and open-source software under the GNU General Public License. Development is led by some of the original developers of MySQL, who forked it due to concerns over its acquisition by Oracle Corporation in 2009. <a href='https://mariadb.org/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/mariadb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/mariadb.png",
-      "image": "mariadb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/mariadb",
-          "container": "/var/lib/mysql",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "3306",
-          "container": "3306"
-        }
-      ],
-      "env": [
-        {
-          "name": "MYSQL_ROOT_PASSWORD",
-          "label": "Root Password",
-          "description": "Root password for MariaDB",
-          "type": "password",
-          "default": "password"
-        },
-        {
-          "name": "MYSQL_DATABASE",
-          "label": "Database",
-          "description": "Database name",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "MYSQL_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "couchdb",
-      "title": "CouchDB",
-      "note": "",
-      "description": "Apache CouchDB is open source database software that focuses on ease of use and having a scalable architecture. It has a document-oriented NoSQL database architecture and is implemented in the concurrency-oriented language Erlang; it uses JSON to store data, JavaScript as its query language using MapReduce, and HTTP for an API. <a href='https://couchdb.apache.org/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/couchdb' target='_blank'>Docker Hub</a>",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/couchdb.png",
-      "image": "couchdb",
-      "categories": [
-        "Database"
-      ],
-      "volumes": [
-        {
-          "bind": "/home/docker/couchdb",
-          "container": "/opt/couchdb/data",
-          "mode": "rw"
-        }
-      ],
-      "ports" : [
-        {
-          "host": "5984",
-          "container": "5984"
-        }
-      ],
-      "env": [
-        {
-          "name": "COUCHDB_USER",
-          "label": "User",
-          "description": "Database user",
-          "type": "text",
-          "default": "heimdall"
-        },
-        {
-          "name": "COUCHDB_PASSWORD",
-          "label": "Password",
-          "description": "Database password",
-          "type": "password",
-          "default": "password"
-        }
-      ]
-    },
-    {
-      "type": 1,
-      "name": "nvidia-hwa-test",
-      "title": "NVIDIA HWA Test",
-      "note": "",
-      "description": "Nvidia HWA Test is a test container for NVIDIA hardware acceleration. Start the container then check the logs to confirm your output matches the example from this page: <a href='https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/sample-workload.html'>Running a Sample Workload</a>. <a href='https://www.nvidia.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nvidia/cuda' target='_blank'>Docker Hub</a>",
-      "platform": "linux",
-      "logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nvidia.png",
-	    "image": "nvidia/cuda:12.2.0-base-ubuntu20.04",
-      "env": [
-        {
-          "name": "DRINODE",
-          "label": "DRINODE",
-          "default": "/dev/dri/renderD128",
-          "description": "Specify the render device (GPU) for the contianer to use."
-        }
-      ],
-      "command": "nvidia-smi"  
-    }
-  ]
-}

+ 2 - 0
templates/tmp/.gitignore

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

+ 250 - 0
utils/install.js

@@ -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('/');
+}

+ 6 - 1
functions/uninstall.js → utils/uninstall.js

@@ -10,7 +10,8 @@ export const Uninstall = async (req, res) => {
 
     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 {
@@ -18,6 +19,7 @@ export const Uninstall = async (req, res) => {
         }
 
         try {
+            console.log(`Removing ${service_name}...`);
             containerName.remove();
             
             const syslog = await Syslog.create({
@@ -38,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('/');
 }
 

+ 3 - 3
views/account.html

@@ -21,7 +21,7 @@
 	<body >
 		<div class="page">
 			<!-- Navbar -->
-			<%- include('navbar.html') %>
+			<%- include('partials/navbar.html') %>
 			<div class="page-wrapper">
 				<!-- Page header -->
 				<div class="page-header d-print-none">
@@ -41,7 +41,7 @@
 						<div class="card">
 							<div class="row g-0">
 								
-								<%- include('sidebar.html') %>
+								<%- include('partials/sidebar.html') %>
 								
 								<div class="col d-flex flex-column">
 								<div class="card-body">
@@ -120,7 +120,7 @@
 						</div>
 					</div>
 				</div>
-				<%- include('footer.html') %>
+				<%- include('partials/footer.html') %>
 			</div>
 		</div>
 		<!-- Libs JS -->

+ 97 - 73
views/apps.html

@@ -8,6 +8,7 @@
     <!-- 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 {
@@ -22,55 +23,119 @@
     <div class="page">
       <!-- Navbar -->
 
-      <%- include('navbar.html') %>
+      <%- include('partials/navbar.html') %>
 
       <div class="page-wrapper">
         <!-- Page header -->
-        <div class="page-header d-print-none">
+        <div class="mt-3">
           <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="row row-cards">
 
-                <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="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="card-actions btn-actions">
-                    <div class="card-actions btn-actions">
+              <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">
-                        <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>
+                        <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="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">
@@ -82,11 +147,9 @@
                     prev
                   </a>
                 </li>
-                <li class="page-item"><a class="page-link" href="/apps/1">1</a></li>
-                <li class="page-item"><a class="page-link" href="/apps/2">2</a></li>
-                <li class="page-item"><a class="page-link" href="/apps/3">3</a></li>
-                <li class="page-item"><a class="page-link" href="/apps/4">4</a></li>
-                <li class="page-item"><a class="page-link" href="/apps/5">5</a></li>
+
+                <%- pages %>
+                
                 <li class="page-item">
                   <a class="page-link" href="<%- next %>">
                     next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
@@ -98,52 +161,13 @@
           </div>
         </div>
         
-        <%- include('footer.html') %>
+        <%- 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>
     
-    <!-- <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>

+ 22 - 24
views/dashboard.html

@@ -5,7 +5,6 @@
     <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>
@@ -24,13 +23,13 @@
   
   <div class="page">
 
-    <%- include('navbar.html') %>
+    <%- 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="row row-deck row-cards" hx-ext="sse" sse-connect="/sse">
             
             <div class="col-12">
               <div class="row row-cards">
@@ -96,7 +95,7 @@
                         </div>
 
                         <!-- HTMX -->
-                        <div class="col" name="NET" id="purple">
+                        <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>
@@ -139,26 +138,27 @@
               </div>
             </div>
 
-
-            
-            <!-- HTMX -->
             <div class="col-12">
-              <div class="row row-cards" id="containerCards" data-hx-get="/containers" data-hx-trigger="load, sse:docker" data-hx-swap="innerHTML">
-
+              <div class="row row-cards" id="containers">
               </div>
             </div>
 
             <!-- HTMX -->
-            <!-- <div class="col-12">
-              <div class="row row-cards" data-hx-get="/installs" name="jellyfin" data-hx-trigger="load delay:2s, sse:install" data-hx-swap="beforeend" hx-target="#containerCards">
-
+            <div 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> -->
+            </div>
             
-            <!-- HTMX Target-->
+            <!-- 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-lg modal-dialog-centered" role="document">
+              <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>
@@ -172,14 +172,12 @@
                     <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 class="card-body">
+                      <h4>Logs:</h4>
                         <div id="logView">
                           <pre>No logs available</pre>
                         </div>
-                  </div>
-        
+                    </div>
                   </div>
                   <div class="modal-footer">
                     <button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
@@ -192,13 +190,11 @@
               </div>
             </div>
 
-            
-
           </div>
         </div>
       </div>
       
-    <%- include('footer.html') %>
+    <%- include('partials/footer.html') %>
       
     </div>
   </div>
@@ -222,7 +218,7 @@
           opacity: 1
         },
         stroke: {
-          width: [2, 1],
+          width: [3, 1],
           dashArray: [0, 3],
           lineCap: "round",
           curve: "smooth"
@@ -261,6 +257,8 @@
         }
     }
   </script>
+  
+  <!-- SelectAll for the permissions modal -->
   <script>
     function selectAll(group) {
       

+ 50 - 7
views/images.html

@@ -20,7 +20,7 @@
   <body >
     <div class="page">
       <!-- Navbar -->
-      <%- include('navbar.html') %>
+      <%- include('partials/navbar.html') %>
       <div class="page-wrapper">
         <!-- Page header -->
         
@@ -35,11 +35,11 @@
                     <div class="card-header">
                       <h3 class="card-title">Docker Images</h3>
                         <div class="card-options btn-list">                  
-                            <a href="#" class="btn">
+                            <!-- <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">
+                            </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>
@@ -55,16 +55,59 @@
                       </table>
                     </div>
 
+
                     <div class="card-footer d-flex align-items-center">
 
-                      <button class="btn" type="submit" formaction="/removeImage">Remove</button>
+                      <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>
+                    </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>
 
@@ -72,7 +115,7 @@
           </div>
         </div>
         
-        <%- include('footer.html') %>
+        <%- include('partials/footer.html') %>
         
       </div>
     </div>

+ 2 - 2
views/login.html

@@ -24,12 +24,12 @@
       <div class="container container-tight py-4">
         <div class="text-center">
           <h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
-              <img src="/images/logo.png" alt="DweebUI" title="DweebUI" height="100px">
+              <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="/images/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
+            <img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
           </h1>
         </div>
 

+ 28 - 0
views/modals/compose.html

@@ -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 - 0
views/modals/details.html

@@ -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 - 0
views/modals/import.html

@@ -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 - 0
views/modals/json.html

@@ -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>

+ 13 - 0
views/modals/learnmore.html

@@ -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>

+ 22 - 0
views/modals/permissions.html

@@ -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>

+ 77 - 0
views/modals/uninstall.html

@@ -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>

+ 4 - 4
views/networks.html

@@ -20,7 +20,7 @@
   <body >
     <div class="page">
       <!-- Navbar -->
-      <%- include('navbar.html') %>
+      <%- include('partials/navbar.html') %>
       <div class="page-wrapper">
         <!-- Page header -->
         
@@ -35,10 +35,10 @@
                     <div class="card-header">
                       <h3 class="card-title">Docker Networks</h3>
                         <div class="card-options btn-list">                  
-                            <a href="#" class="btn">
+                            <!-- <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> -->
                             <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
@@ -87,7 +87,7 @@
           </div>
         </div>
         
-        <%- include('footer.html') %>
+        <%- include('partials/footer.html') %>
         
       </div>
     </div>

+ 22 - 0
views/partials/appCard.html

@@ -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>

+ 80 - 0
views/partials/containerFull.html

@@ -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>

+ 71 - 0
views/partials/containerSimple.html

@@ -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>

+ 2 - 3
views/footer.html → views/partials/footer.html

@@ -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>
@@ -24,7 +23,7 @@
           </li>
           <li class="list-inline-item">
             <a href="https://github.com/lllllllillllllillll/DweebUI/releases" class="link-secondary" rel="noopener">
-              v0.40
+              v0.60
             </a>
           </li>
         </ul>

+ 22 - 20
views/navbar.html → views/partials/navbar.html

@@ -27,7 +27,7 @@
     }
   }
 </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">
@@ -35,18 +35,22 @@
     </button>
     <h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
       <a href="#">
-        <img src="/images/logo.png" alt="DweebUI" title="DweebUI" height="40px">
+        <img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="40px">
       </a>
       <a href="#">
-        <img src="/images/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">     
+        <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>
@@ -61,6 +65,16 @@
             VNC
           </a>
         </div> -->
+
+        <!-- <% if(role == 'admin') { %>
+          <div class="btn-list">
+            <a href="#" class="btn text-red">
+              Admin
+            </a>
+          </div>
+        <% } %> -->
+
+
         
         
       </div>
@@ -78,7 +92,7 @@
           <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>
@@ -138,7 +152,7 @@
       </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 %>
@@ -150,7 +164,6 @@
         </a>
         <div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
           <a href="/account" class="dropdown-item">Account</a>
-          <a href="/variables" class="dropdown-item">Variables</a>
           <a href="/settings" class="dropdown-item">Settings</a>
           <!-- <div class="dropdown-divider"></div> -->
 
@@ -166,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>
@@ -250,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">

+ 0 - 1
views/sidebar.html → views/partials/sidebar.html

@@ -3,7 +3,6 @@
 		<h4 class="subheader">Menu</h4>
 		<div class="list-group list-group-transparent">
 			<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Accounts</a>
-			<a href="/variables" class="list-group-item list-group-item-action d-flex align-items-center" disabled="">Variables</a>
 			<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
 		</div>
 		<h4 class="subheader mt-4">Other</h4>

+ 182 - 0
views/partials/user_permissions.html

@@ -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>

+ 216 - 87
views/portal.html

@@ -8,6 +8,8 @@
     <!-- 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 {
@@ -19,128 +21,255 @@
     </style>
   </head>
   <body >
-    <div class="page">
-
-      <%- include('navbar.html') %>
-      <div class="page-wrapper">
   
-        <div class="page-body">
-          <div class="container-xl">
-            <div class="row row-deck row-cards">
-              
-              <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 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="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 class="cpu-bar meter animate green">
+                            <span style="width:20%"><span></span></span>
                           </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-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="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 class="ram-bar meter animate blue">
+                            <span style="width:20%"><span></span></span>
                           </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-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="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 class="ram-bar meter animate purple">
+                            <span style="width:20%"><span></span></span>
                           </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-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="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 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>
-  
-  
-              <!-- inserted here from  -->
-              
-  
             </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>
-        
-        <%- include('footer.html') %>
-        
       </div>
+      
+    <%- include('partials/footer.html') %>
+      
     </div>
+  </div>
     
 
-  <!-- Libs JS -->
-  <script src="/libs/apexcharts/dist/apexcharts.min.js" defer></script>
-  <!-- Tabler Core -->
+  <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>
 

+ 14 - 75
views/register.html

@@ -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="/images/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="/images/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="/images/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="/images/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="/images/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="/images/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="/images/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="/images/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="/images/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>
@@ -183,7 +122,7 @@
                 </a>
               </div>
 
-              <button type="submit" class="btn btn-primary">Install</button>
+              <button type="submit" class="btn btn-primary">Register</button>
               
             </div>
           </div>

+ 3 - 3
views/settings.html

@@ -21,7 +21,7 @@
 	<body >
 		<div class="page">
 		<!-- Navbar -->
-		<%- include('navbar.html') %>
+		<%- include('partials/navbar.html') %>
 		<div class="page-wrapper">
 			<!-- Page header -->
 			<div class="page-header d-print-none">
@@ -40,7 +40,7 @@
 				<div class="container-xl">
 				  <div class="card">
 					<div class="row g-0">
-						<%- include('sidebar.html') %>
+						<%- include('partials/sidebar.html') %>
 					  	<div class="col d-flex flex-column">
 					  
 							<div class="card-body">
@@ -109,7 +109,7 @@
 				</div>
 			  </div>
 
-			<%- include('footer.html') %>
+			<%- include('partials/footer.html') %>
 		</div>
 		</div>
 		<!-- Libs JS -->

+ 3 - 3
views/supporters.html

@@ -22,7 +22,7 @@
 	<body >
 	<div class="page">
 		<!-- Navbar -->
-		<%- include('navbar.html') %>
+		<%- include('partials/navbar.html') %>
 		<div class="page-wrapper">
 			<!-- Page header -->
 			<div class="page-header d-print-none">
@@ -41,7 +41,7 @@
 				<div class="container-xl">
 					<div class="card">
 						<div class="row g-0">
-							<%- include('sidebar.html') %>
+							<%- include('partials/sidebar.html') %>
 							<div class="col d-flex flex-column">
 					
 								<div class="card-body">
@@ -74,7 +74,7 @@
 				</div>
 			</div>
 
-				<%- include('footer.html') %>
+				<%- include('partials/footer.html') %>
 		</div>
 	</div>
 		<!-- Libs JS -->

+ 2 - 2
views/syslogs.html

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

+ 2 - 2
views/users.html

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

+ 3 - 3
views/variables.html

@@ -21,7 +21,7 @@
 	<body >
 		<div class="page">
 		<!-- Navbar -->
-		<%- include('navbar.html') %>
+		<%- include('partials/navbar.html') %>
 		<div class="page-wrapper">
 			<!-- Page header -->
 			<div class="page-header d-print-none">
@@ -40,7 +40,7 @@
 				<div class="container-xl">
 				  <div class="card">
 					<div class="row g-0">
-						<%- include('sidebar.html') %>
+						<%- include('partials/sidebar.html') %>
 					  	<div class="col d-flex flex-column">
 					  
 							<div class="card-body">
@@ -109,7 +109,7 @@
 				</div>
 			  </div>
 
-			<%- include('footer.html') %>
+			<%- include('partials/footer.html') %>
 		</div>
 		</div>
 		<!-- Libs JS -->

+ 38 - 5
views/volumes.html

@@ -20,7 +20,7 @@
   <body >
     <div class="page">
       <!-- Navbar -->
-      <%- include('navbar.html') %>
+      <%- include('partials/navbar.html') %>
       <div class="page-wrapper">
         <!-- Page header -->
         
@@ -35,11 +35,11 @@
                     <div class="card-header">
                       <h3 class="card-title">Docker Volumes</h3>
                         <div class="card-options btn-list">                  
-                            <a href="#" class="btn">
+                            <!-- <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">
+                            </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>
@@ -79,6 +79,39 @@
 
                     </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>
 
@@ -86,7 +119,7 @@
           </div>
         </div>
         
-        <%- include('footer.html') %>
+        <%- include('partials/footer.html') %>
         
       </div>
     </div>