浏览代码

Merge pull request #47 from lllllllillllllillll/dev

v0.20 - The rewrite
lllllllillllllillll 1 年之前
父节点
当前提交
c3f10fbb7c
共有 100 个文件被更改,包括 4432 次插入3180 次删除
  1. 1 2
      .github/FUNDING.yml
  2. 1 1
      .github/dependabot.yml
  3. 8 0
      .gitignore
  4. 29 0
      CHANGELOG.md
  5. 3 4
      Dockerfile
  6. 29 35
      README.md
  7. 0 143
      app.js
  8. 0 1
      caddyfiles/Caddyfile
  9. 8 10
      components/appCard.js
  10. 285 0
      components/containerCard.js
  11. 0 1109
      components/dashCard.js
  12. 0 18
      components/siteCard.js
  13. 17 20
      controllers/account.js
  14. 72 164
      controllers/apps.js
  15. 0 150
      controllers/auth.js
  16. 13 239
      controllers/dashboard.js
  17. 56 0
      controllers/images.js
  18. 82 0
      controllers/login.js
  19. 52 0
      controllers/networks.js
  20. 12 0
      controllers/portal.js
  21. 100 0
      controllers/register.js
  22. 6 19
      controllers/settings.js
  23. 36 0
      controllers/syslogs.js
  24. 54 48
      controllers/users.js
  25. 70 0
      controllers/volumes.js
  26. 0 47
      database/ContainerSettings.js
  27. 0 42
      database/ServerSettings.js
  28. 0 63
      database/UserModel.js
  29. 189 0
      database/models.js
  30. 3 10
      docker-compose.yaml
  31. 0 205
      functions/compose.js
  32. 208 0
      functions/install.js
  33. 0 194
      functions/package_manager.js
  34. 0 315
      functions/system.js
  35. 41 0
      functions/uninstall.js
  36. 798 47
      package-lock.json
  37. 23 9
      package.json
  38. 1 1
      public/css/meters.css
  39. 0 72
      public/css/tabler.min.css
  40. 二进制
      public/fonts/Inter-Black.woff2
  41. 二进制
      public/fonts/Inter-BlackItalic.woff2
  42. 二进制
      public/fonts/Inter-Bold.woff2
  43. 二进制
      public/fonts/Inter-BoldItalic.woff2
  44. 二进制
      public/fonts/Inter-ExtraBold.woff2
  45. 二进制
      public/fonts/Inter-ExtraBoldItalic.woff2
  46. 二进制
      public/fonts/Inter-ExtraLight.woff2
  47. 二进制
      public/fonts/Inter-ExtraLightItalic.woff2
  48. 二进制
      public/fonts/Inter-Italic.woff2
  49. 二进制
      public/fonts/Inter-Light.woff2
  50. 二进制
      public/fonts/Inter-LightItalic.woff2
  51. 二进制
      public/fonts/Inter-Medium.woff2
  52. 二进制
      public/fonts/Inter-MediumItalic.woff2
  53. 二进制
      public/fonts/Inter-Regular.woff2
  54. 二进制
      public/fonts/Inter-SemiBold.woff2
  55. 二进制
      public/fonts/Inter-SemiBoldItalic.woff2
  56. 二进制
      public/fonts/Inter-Thin.woff2
  57. 二进制
      public/fonts/Inter-ThinItalic.woff2
  58. 二进制
      public/fonts/InterDisplay-Black.woff2
  59. 二进制
      public/fonts/InterDisplay-BlackItalic.woff2
  60. 二进制
      public/fonts/InterDisplay-Bold.woff2
  61. 二进制
      public/fonts/InterDisplay-BoldItalic.woff2
  62. 二进制
      public/fonts/InterDisplay-ExtraBold.woff2
  63. 二进制
      public/fonts/InterDisplay-ExtraBoldItalic.woff2
  64. 二进制
      public/fonts/InterDisplay-ExtraLight.woff2
  65. 二进制
      public/fonts/InterDisplay-ExtraLightItalic.woff2
  66. 二进制
      public/fonts/InterDisplay-Italic.woff2
  67. 二进制
      public/fonts/InterDisplay-Light.woff2
  68. 二进制
      public/fonts/InterDisplay-LightItalic.woff2
  69. 二进制
      public/fonts/InterDisplay-Medium.woff2
  70. 二进制
      public/fonts/InterDisplay-MediumItalic.woff2
  71. 二进制
      public/fonts/InterDisplay-Regular.woff2
  72. 二进制
      public/fonts/InterDisplay-SemiBold.woff2
  73. 二进制
      public/fonts/InterDisplay-SemiBoldItalic.woff2
  74. 二进制
      public/fonts/InterDisplay-Thin.woff2
  75. 二进制
      public/fonts/InterDisplay-ThinItalic.woff2
  76. 二进制
      public/fonts/InterVariable-Italic.woff2
  77. 二进制
      public/fonts/InterVariable.woff2
  78. 59 0
      public/fonts/inter.css
  79. 0 0
      public/img/avatars/burns.jpg
  80. 二进制
      public/img/avatars/duffman.png
  81. 0 0
      public/img/avatars/frank.jpg
  82. 0 0
      public/img/avatars/moe.jpg
  83. 二进制
      public/img/avatars/moleman.png
  84. 0 0
      public/img/avatars/poochie.jpg
  85. 0 0
      public/img/avatars/rus.jpg
  86. 0 0
      public/img/avatars/skinner.jpg
  87. 1 1
      public/js/demo-theme.js
  88. 93 158
      public/js/main.js
  89. 2020 0
      public/libs/list.js/dist/list.js
  90. 0 0
      public/libs/list.js/dist/list.js.map
  91. 0 0
      public/libs/list.js/dist/list.min.js
  92. 1 0
      public/libs/list.js/dist/list.min.js.map
  93. 0 0
      public/static/logo-sm-black.svg
  94. 0 0
      public/static/logo-sm-white.svg
  95. 0 3
      public/static/logo-small-white.svg
  96. 0 0
      public/static/logo-small.svg
  97. 0 2
      public/static/logo-white.svg
  98. 0 0
      public/static/logo.svg
  99. 61 0
      router/index.js
  100. 0 48
      routes/index.js

+ 1 - 2
.github/FUNDING.yml

@@ -1,2 +1 @@
-github: [lllllllillllllillll]
-patreon: DweebUI
+patreon: DweebUI

+ 1 - 1
.github/dependabot.yml

@@ -15,6 +15,6 @@ updates:
   - package-ecosystem: "npm"
     directory: "/"
     schedule:
-      interval: "daily"
+      interval: "weekly"
     labels:
       - "🤖 Dependencies"

+ 8 - 0
.gitignore

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

+ 29 - 0
CHANGELOG.md

@@ -1,3 +1,32 @@
+## v0.20 (Jan 20th 2024) - The rewrite. Jumping all the way to v0.20.
+* Changed to ES6 imports.
+* Cleaned up file structure and code layout.
+* Updated DweebUI logo.
+* Visual tweaks to login and registration pages.
+* Added .gitignore and .dockerignore files.
+* Syslogs - View logs for sign-in and registration attempts. :new: 
+* Docker socket now uses default connection.
+* Updated Users page displays 'inactive' if no sign-ins within 30 days.
+* Dashboard updates now triggered by Docker events.
+* Massive reduction in the amount of HTML, CSS, and JS on client side.
+* Container graphs are significantly more efficent and no longer use localStorage.
+* Made dark mode the default theme.
+* Created intervals to allow application to idle or scale with more users.
+* Pages for images, volumes, and networks. :new: 
+* Localized fonts.
+* CORS.
+* Testing with Mocha and Supertest.
+* Created Portal page. :new:
+
+
+## <del>v0.09 (dev)</del> dead. (It had so many problems that I essentially rewrote everything)
+* Added authentication middleware to router.
+* Added gzip compression.
+* Added PM2.
+* Added Helmet.
+* Fixed missing session data.
+* Reduced sqlite queries.
+
 ## v0.08 (Dec 15th 2023)
 * Updates to compose file and instructions from [steveiliop56](https://github.com/steveiliop56)
 * Added SECRET field to compose file as a basic security measure.

+ 3 - 4
Dockerfile

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

+ 29 - 35
README.md

@@ -1,39 +1,46 @@
 # DweebUI
-DweebUI is a simple Docker web interface created using Javascript, Node.JS, and Express.
+DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
 
-Pre-Pre-Pre-Pre-Pre Alpha v0.08 ( :fire: Experimental. Don't install on any servers you care about :fire: )
+Alpha v0.20 ( :fire: Experimental :fire: )
+
+   
+[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
 
 [![GitHub Stars](https://img.shields.io/github/stars/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
 [![GitHub Activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
 [![Docker Pulls](https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui)](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
 [![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
+[![GitHub License](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
 
+* This is a personal project I started to get more familiar with Javascript and Node.js.
+* Some UI elements are placeholders and every version may have breaking changes.
+* Please post issues and discussions so I know what bugs and features to focus on.
 
-* This is a personal project that I decided to share. I'm sure it has plenty of bugs and mistakes.
-* I haven't used Github very much and I'm still new to Javascript.
-* I probably should have waited a lot longer to share this :|
-
-<a href="https://raw.githubusercontent.com//lllllllillllllillll/DweebUI/main/screenshots/dashboard.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard.png" width="50%"/></a>
+<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/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>
 
-<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="50%"/></a>
 
 
 ## Features
 * [x] Dashboard provides server metrics, container metrics, and container controls, on a single page.
 * [x] View container logs.
 * [ ] Update containers (planned).
-* [ ] Manage your Docker networks, images, and volumes (planned).
+* [ ] Manage your Docker networks, images, and volumes (in development).
 * [x] Light/Dark Mode.
 * [x] Easy to install app templates.
-* [x] Proxy manager for Caddy (Optional).
 * [x] Multi-User built-in.
-* [ ] User pages (planned).
+* [ ] Permissions system (in development).
 * [x] Support for Windows, Linux, and MacOS.
-* [ ] Docker compose support (planned).
+* [ ] Docker compose import (in development).
 * [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
 * [x] Automatically persists data in docker volumes if bind mount isn't used.
 * [ ] Preset variables (planned).
-* [ ] Offline/Local Icons (planned).
 
 
 ## Setup
@@ -42,37 +49,27 @@ Docker Compose:
 ```
 version: "3.9"
 services:
-
   dweebui:
     container_name: dweebui
-    image: lllllllillllllillll/dweebui:v0.08
-    # build:
-    #   context: .
+    image: lllllllillllllillll/dweebui:v0.20
     environment:
       NODE_ENV: production
       PORT: 8000
       SECRET: MrWiskers
-      #Proxy_Manager: enabled
     restart: unless-stopped
     ports:
       - 8000:8000
     volumes:
       - dweebui:/app
-      - caddyfiles:/app/caddyfiles
       - /var/run/docker.sock:/var/run/docker.sock
-      #- ./custom-templates.json:/app/custom-templates.json
-      #- ./composefiles:/app/composefiles
     networks:
-      - dweeb_network
-
+      - dweebui_net
 
 volumes:
   dweebui:
-  caddyfiles:
-
 
 networks:
-  dweeb_network:
+  dweebui_net:
     driver: bridge
 ```
 
@@ -83,18 +80,15 @@ Compose setup:
 * You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
 
 
-Using setup.sh: 
-```
-Extract DweebUI.zip and navigate to /DweebUI
-cd DweebUI
-chmod +x setup.sh
-sudo ./setup.sh
-```
-
 
 ## Credits
 
 * Dockerode and dockerode-compose by Apocas: https://github.com/apocas/dockerode
 * UI was built using HTML and CSS elements from https://tabler.io/
 * Apps template based on Portainer template provided by Lissy93: https://github.com/Lissy93/portainer-templates
-* Icons from Walkxcode with some renames and additions: https://github.com/walkxcode/dashboard-icons
+* Icons from Walkxcode with some renames and additions: https://github.com/walkxcode/dashboard-icons
+
+
+## Supporters
+
+* MM (Patreon)

+ 0 - 143
app.js

@@ -1,143 +0,0 @@
-// Express
-const express = require("express");
-const app = express();
-const session = require("express-session");
-const PORT = process.env.PORT || 8000;
-
-// Router
-const routes = require("./routes");
-
-// Functions and variables
-const { serverStats, containerList, containerStats, containerAction, containerLogs, hiddenContainers } = require('./functions/system');
-let sentList, clicked;
-app.locals.site_list = '';
-
-const Containers = require('./database/ContainerSettings');
-
-
-// Configure Session
-const sessionMiddleware = session({
-    secret: "keyboard cat", 
-    resave: false, 
-    saveUninitialized: false, 
-    cookie:{
-        secure:false, // Only set to true if you are using HTTPS.
-        httpOnly:false, // Only set to true if you are using HTTPS.
-        maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
-    } 
-})
-
-// Middleware
-app.set('view engine', 'ejs');
-app.use([
-    express.static("public"),
-    express.json(),
-    express.urlencoded({ extended: true }),
-    sessionMiddleware,
-    routes
-]);
-
-// Start Express server
-const server = app.listen(PORT, async () => {
-    console.log(`App listening on port ${PORT}`);
-});
-
-// Start Socket.io
-const io = require('socket.io')(server);
-io.engine.use(sessionMiddleware);
-
-io.on('connection', (socket) => {
-
-    // Set user session
-    const user_session = socket.request.session;
-    console.log(`${user_session.user} connected from ${socket.handshake.headers.host} ${socket.handshake.address}`);
-
-    // Check if a list of containers or an install card needs to be sent
-    if (sentList != null) { socket.emit('cards', sentList); }
-    if((app.locals.install != '') && (app.locals.install != null)){ socket.emit('install', app.locals.install); }
-
-    // Send server metrics
-    let ServerStats = setInterval(async () => {
-        socket.emit('metrics', await serverStats());
-    }, 1000);
-
-    // Send list of containers
-    let ContainerList = setInterval(async () => {
-        let cardList = await containerList();
-        if (sentList !== cardList) {
-            sentList = cardList;
-            app.locals.install = '';
-            socket.emit('cards', cardList);
-        }
-    }, 1000);
-
-    // Send container metrics
-    let ContainerStats = setInterval(async () => {
-        let stats = await containerStats();
-        for (let i = 0; i < stats.length; i++) {
-            socket.emit('containerStats', stats[i]);
-        }
-    }, 1000);
-
-    // Container controls
-    socket.on('clicked', (data) => {
-        if (clicked == true) { return; } clicked = true;
-        let buttonPress = {
-            user: socket.request.session.user,
-            role: socket.request.session.role,
-            action: data.action,
-            container: data.container,
-            state: data.state
-        }
-        containerAction(buttonPress);
-        clicked = false;
-    });
-
-    
-    socket.on('hide', async (data) => {
-        console.log(`Hide ${data.container}`);
-
-        let containerExists = await Containers.findOne({ where: {name:data.container}});
-        
-        if(!containerExists){
-            const container = await Containers.create({ 
-                name: data.container,
-                visibility: false
-             });
-             hiddenContainers();
-            console.log(`[Created] Container ${data.container} hidden`)
-        } else {
-            containerExists.update({ visibility: false });
-            console.log(`[Updated] Container ${data.container} hidden`)
-            hiddenContainers();
-        }
-    });
-
-    socket.on('reset', (data) => {
-        // set visibility to true for all containers
-        Containers.update({ visibility: true }, { where: {} });
-        console.log('All containers visible');
-        hiddenContainers();
-    });
-
-
-    // Container logs
-    socket.on('logs', (data) => {
-        containerLogs(data.container)
-        .then(logs => {
-            console.log(`Refreshed logs for ${data.container}`)
-            socket.emit('logString', logs);
-        })
-        .catch(err => {
-            console.error(err);
-        });
-    });
-
-    // On disconnect
-    socket.on('disconnect', () => {                
-        clearInterval(ServerStats);
-        clearInterval(ContainerList);
-        clearInterval(ContainerStats);
-    }); 
-
-});

+ 0 - 1
caddyfiles/Caddyfile

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

+ 8 - 10
components/appCard.js

@@ -1,6 +1,7 @@
-function appCard(data) {
+export const appCard = (data) => {
 
-  // make data.title lowercase
+
+  // 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) + "...";
@@ -15,11 +16,10 @@ function appCard(data) {
   let repository = data.repository || "";
   let source = data.image || "";
 
-
-  // if data.network is set to host, bridge, or docker set the radio button to checked
   let net_host, net_bridge, net_docker = '';
   let net_name = 'AppBridge';
   
+  // if data.network is set to host, bridge, or docker set the radio button to checked
   if (data.network == 'host') {
     net_host = 'checked';
   } else if (data.network) {
@@ -219,7 +219,7 @@ function appCard(data) {
   <div class="col-md-6 col-lg-3">
     <div class="card">
       <div class="card-body p-4 text-center">
-        <span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"></img></span>
+        <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">
@@ -227,11 +227,11 @@ function appCard(data) {
         </div>
       </div>
       <div class="d-flex">
-        <a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-info"><!-- Download SVG icon from http://tabler-icons.io/i/mail -->
+        <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"><!-- Download SVG icon from http://tabler-icons.io/i/phone -->
+        <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>
@@ -989,6 +989,4 @@ function appCard(data) {
               </div>`;
 
 
-}
-
-module.exports = { appCard };
+}

+ 285 - 0
components/containerCard.js

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

+ 0 - 1109
components/dashCard.js

@@ -1,1109 +0,0 @@
-module.exports.dashCard = function dashCard(data) {
-  
-  let { name, service, id, state, image, external_port, internal_port, ports, volumes, environment_variables, labels, IPv4, style } = data;
-
-  let margin, iconSize, fontSize = '';
-
-  if (style == "Large") {
-    iconSize = 'width="150px"'
-  } else if ((style == "Compact") || (style == undefined)) {
-    iconSize = 'width="100px"'
-    margin = 'style="margin-bottom: 0;"'
-  } else if (style == "Row") {
-    iconSize = 'width="50px"'
-    margin = 'style="margin-bottom: 0;"'
-  }
-
-
-  //disable controls for a docker container depending on its name
-  let actions = "";
-  if (name.startsWith('dweebui')) {
-    actions = 'disabled=""';
-  }
-
-  if ( external_port == undefined ) { external_port = 0; }
-  if ( internal_port == undefined ) { internal_port = 0; }
-
-
-  let shortened_name = name;
-  if (name.length > 13) {
-    shortened_name = name.slice(0, 10) + '...';
-  }
-
-  let state_indicator = 'green';
-  if (state == 'exited') {
-    state = 'stopped';
-    state_indicator = 'red';
-  } else if (state == 'paused') {
-    state_indicator = 'orange';
-  }
-
-
-  let app_name = name
-  let modal = app_name.replaceAll(" ", "-");
-  let form_id = app_name.replaceAll("-", "_");
-
-  let restart_policy = 'unless-stopped';
-  
-  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
-      });
-    }
-  }
-
-  let volumes_data = [];
-  if (volumes) {
-    volumes_data = volumes;
-  } else {
-    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 = [];
-  if (environment_variables) {
-    env_data = environment_variables;
-  } else {
-    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 = [];
-  if (labels) {
-    label_data = labels;
-  } else {
-    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
-      });
-    }
-  }
-
-  return `
-    <div class="col-sm-6 col-lg-3 deleteme">
-      <div class="card">
-        <div class="card-body">
-          <div class="card-stamp card-stamp-sm">
-            <img ${iconSize} src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service}.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
-          </div>
-          <div class="d-flex align-items-center">
-            <div class="subheader text-yellow">${external_port}:${internal_port}</div>
-            <div class="ms-auto lh-1">
-              <div class="card-actions btn-actions">
-                <div class="card-actions btn-actions">
-                  <button onclick="buttonAction(this)" name="${name}" value="start" id="${state}" class="btn-action" title="Start" ${actions}><!-- player-play -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
-                  </button>
-                  <button onclick="buttonAction(this)" name="${name}" value="stop" id="${state}" class="btn-action" title="Stop" ${actions}><!-- player-stop -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
-                  </button>
-                  <button onclick="buttonAction(this)" name="${name}" value="pause" id="${state}" class="btn-action" title="Pause" ${actions}><!-- player-pause -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
-                  </button>
-                  <button onclick="buttonAction(this)" name="${name}" value="restart" id="${state}" class="btn-action" title="Restart" ${actions}><!-- reload -->
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>                          
-                  </button>
-                  <div class="dropdown">
-                    <a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><!-- Download SVG icon from http://tabler-icons.io/i/dots-vertical -->
-                      <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">
-                      <a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#${name}_modal-details" href="#">Details</a>
-                      <a class="dropdown-item" onclick="viewLogs(this)" name="${name}" data-bs-toggle="modal" data-bs-target="#log_view" href="#">Logs</a>
-                      <a class="dropdown-item" href="#">Edit</a>
-                      <a class="dropdown-item text-primary" href="#">Update</a>
-                      <a class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#${name}_modal-danger" href="#">Remove</a>
-                    </div>
-                  </div>
-                  <div class="dropdown">
-                    <a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><!-- Download SVG icon from http://tabler-icons.io/i/dots-vertical -->
-                      <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">
-                      <a class="dropdown-item" onclick="hideContainer(this)" name="${name}" href="#">Hide</a>
-                      <a class="dropdown-item" onclick="resetView()" name="${name}" href="#">Reset View</a>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <div class="d-flex align-items-baseline">
-            <div class="h1 me-2" title="${name}" ${margin}>
-              <a href="http://${IPv4}:${external_port}" target="_blank">
-                ${shortened_name}
-              </a>
-            </div>
-            <div class="ms-auto">
-              <span class="text-${state_indicator} align-items-center lh-1">
-                <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
-                ${state} <!-- Download SVG icon from http://tabler-icons.io/i/minus -->
-              </span>
-            </div>
-          </div>
-          <div id="${name}_chart" class="chart-sm"></div>
-        </div>
-      </div>
-    </div>
-    
-
-
-
-
-    
-
-
-    <div class="modal modal-blur fade deleteme" id="${name}_modal-danger" tabindex="-1" style="display: none;" aria-hidden="true">
-      <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
-        <div class="modal-content">
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-          <div class="modal-status bg-danger"></div>
-          <div class="modal-body text-center py-3">
-            <!-- Download SVG icon from http://tabler-icons.io/i/alert-triangle -->
-            <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
-            <h3>Remove ${name}?</h3>
-            <form action="/uninstall" id="${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" checked="" name="remove_volumes">
-                    </label>
-                  </div>
-                </div>
-                <div class="row">
-                  <div class="col-9">
-                    <label class="row text-start">
-                      <span class="col">
-                        Remove Image
-                      </span>
-                    </label>
-                  </div>
-                  <div class="col-3">
-                    <label class="form-check form-check-single form-switch text-end">
-                      <input class="form-check-input" type="checkbox" checked="" name="remove_image">
-                    </label>
-                  </div>
-                </div>
-                <div class="row">
-                  <div class="col-9">
-                    <label class="row text-start">
-                      <span class="col">
-                        Remove Backups
-                      </span>
-                    </label>
-                  </div>
-                  <div class="col-3">
-                    <label class="form-check form-check-single form-switch text-end">
-                      <input class="form-check-input" type="checkbox" checked="" name="remove_backups">
-                    </label>
-                  </div>
-                </div>
-              </div>
-            </div>
-            <div class="mt-1"> </div>
-            <div class="text-muted">Enter "Yes" below to remove the container.</div>
-            <input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
-            </form>
-          </div>
-          <div class="modal-footer">
-            <div class="w-100">
-              <div class="row">
-                <div class="col">
-                  <a href="#" class="btn w-100" data-bs-dismiss="modal">
-                    Cancel
-                  </a>
-                </div>
-                <div class="col">
-                  <input type="submit" form="${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}_modal-details" 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 ${name}</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="" 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="${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="${image}"/>
-                        </div>
-                        <div class="col-lg-3">
-                          <label class="form-label">Restart Policy: </label>
-                          <select class="form-select" name="restart_policy" value="${restart_policy}">
-                            <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 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 - 18
components/siteCard.js

@@ -1,18 +0,0 @@
-function siteCard(type, domain, host, port, id) {
-  
-  let site = `<tr>`
-  site += `<td><input class="form-check-input m-0 align-middle" name="select${id}" value="${domain}" type="checkbox" aria-label="Select invoice"></td>`
-  site += `<td><span class="text-muted">${id}</span></td>`
-  site += `<td><a href="https://${domain}" class="text-reset" tabindex="-1" target="_blank">${domain}</a></td>`
-  site += `<td>${type}</td>`
-  site += `<td>${host}</td>`
-  site += `<td>${port}</td>`
-  site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
-  site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
-  site += `<td class="text-end"><a class="btn" href="#"> Edit </a></td>`
-  site += `</tr>`
-
-  return site;
-}
-
-module.exports = { siteCard };

+ 17 - 20
controllers/account.js

@@ -1,22 +1,19 @@
-const User = require('../database/UserModel');
+import { User } from "../database/models.js";
+
+export const Account = async (req, res) => {
+    
+
+    let user = await User.findOne({ where: { UUID: req.session.UUID }});
+
+    res.render("account", {
+        first_name: user.name,
+        last_name: user.name,
+        name: user.name,
+        id: user.id,
+        email: user.email,
+        role: user.role,
+        avatar: user.avatar,
+    });
+
 
-exports.Account = async function(req, res) {
-    if (req.session.user) {
-        // Get the user.
-        let user = await User.findOne({ where: { UUID: req.session.UUID }});
-        // Render the home page
-        res.render("pages/account", {
-            first_name: user.first_name,
-            last_name: user.last_name,
-            name: user.first_name + ' ' + user.last_name,
-            id: user.id,
-            email: user.email,
-            role: user.role,
-            avatar: user.avatar,
-            isLoggedIn: true
-        });
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
 }

+ 72 - 164
controllers/apps.js

@@ -1,190 +1,98 @@
-const User = require('../database/UserModel');
-const { appCard } = require('../components/appCard')
-const { dashCard } = require('../components/dashCard');
-const { install, uninstall } = require('../functions/package_manager');
+import { readFileSync } from 'fs';
+import { appCard } from '../components/appCard.js';
 
-const templates_json = require('../templates.json');
-let templates = templates_json.templates;
+let templatesJSON = readFileSync('./templates.json');
+let templates = JSON.parse(templatesJSON).templates;
 
-// sort templates alphabetically
 templates = templates.sort((a, b) => {
     if (a.name < b.name) {
       return -1;
     }
-  });
-  
+});
 
-exports.Apps = async function(req, res) {
-
-    if (req.session.role == "admin") {
-
-        // Get the user.
-        let user = await User.findOne({ where: { UUID: req.session.UUID }});
-
-        let page = Number(req.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);
-        }
-
-        let apps_list = '';
-        for (let i = list_start; i < list_end && i < templates.length; i++) {
-            let app_card = appCard(templates[i]);
-
-            apps_list += app_card;
-        }
-        
-        // Render the home page
-        res.render("pages/apps", {
-            name: user.first_name + ' ' + user.last_name,
-            role: user.role,
-            avatar: user.avatar,
-            isLoggedIn: true,
-            list_start: list_start + 1,
-            list_end: list_end,
-            app_count: templates.length,
-            prev: prev,
-            next: next,
-            apps_list: apps_list
-        });
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
+export const Apps = (req, res) => {
+    
+    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);
     }
-}
-
 
+    let apps_list = '';
+    for (let i = list_start; i < list_end && i < templates.length; i++) {
+        let app_card = appCard(templates[i]);
 
-exports.searchApps = async function(req, res) {
-    if (req.session.role == "admin") {
+        apps_list += app_card;
+    }
+    
+    res.render("apps", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+        list_start: list_start + 1,
+        list_end: list_end,
+        app_count: templates.length,
+        prev: prev,
+        next: next,
+        apps_list: apps_list
+    });
 
-        // Get the user.
-        let user = await User.findOne({ where: { UUID: req.session.UUID }});
+}
 
-        let page = Number(req.query.page) || 1;
-        let list_start = (page - 1) * 28;
-        let list_end = (page * 28);
-        let last_page = Math.ceil(templates.length / 28);
 
-        let prev = '/apps?page=' + (page - 1);
-        let next = '/apps?page=' + (page + 1);
-        if (page == 1) {
-            prev = '/apps?page=' + (page);
-        }
-        if (page == last_page) {
-            next = '/apps?page=' + (page);
-        }
 
-        let apps_list = '';
-        let search_results = [];
+export const appSearch = async (req, res) => {
 
-        let search = req.body.search;
+    let search = req.body.search.split(' ');
+    let apps_list = '';
+    let results = [];
 
-        // split value of search into an array of words
-        search = search.split(' ');
-        try {console.log(search[0]);} catch (error) {}
-        try {console.log(search[1]);} catch (error) {}
-        try {console.log(search[2]);} catch (error) {}
+    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);
 
-        function searchTemplates(word) {
+    let prev = '/apps?page=' + (page - 1);
+    let next = '/apps?page=' + (page + 1);
+    if (page == 1) {
+        prev = '/apps?page=' + (page);
+    }
+    if (page == last_page) {
+        next = '/apps?page=' + (page);
+    }
 
-            for (let i = 0; i < templates.length; i++) {
-                if ((templates[i].description.includes(word)) || (templates[i].name.includes(word)) || (templates[i].title.includes(word))) {
-                    search_results.push(templates[i]);
-                }
+    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]);
             }
-            // console.log(search_results);
-        }
-        
-        searchTemplates(search);
-
-        for (let i = 0; i < search_results.length; i++) {
-            let app_card = appCard(search_results[i]);
-            apps_list += app_card;
         }
-        
-        // Render the home page
-        res.render("pages/apps", {
-            name: user.first_name + ' ' + user.last_name,
-            role: user.role,
-            avatar: user.avatar,
-            isLoggedIn: true,
-            list_start: list_start + 1,
-            list_end: list_end,
-            app_count: templates.length,
-            prev: prev,
-            next: next,
-            apps_list: apps_list
-        });
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
     }
-}
-
-
+    searchTemplates(search);
 
-
-
-
-
-
-
-exports.Install = async function (req, res) {
-    
-    if (req.session.role == "admin") {
-
-        console.log(`Starting install for: ${req.body.name}`)
-
-        install(req.body);
-
-        let container_info = {
-            name: req.body.name,
-            service: req.body.service_name,
-            state: 'installing',
-            image: req.body.image,
-            restart_policy: req.body.restart_policy
-        }
-
-        let installCard = dashCard(container_info);
-
-        req.app.locals.install = installCard;
-
-        
-        // Redirect to the home page
-        res.redirect("/");
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
+    for (let i = 0; i < results.length; i++) {
+        let app_card = appCard(results[i]);
+        apps_list += app_card;
     }
-}
-
-
-
-exports.Uninstall = async function (req, res) {
     
-    if (req.session.role == "admin") {
-
+    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
+    });
 
-        if (req.body.confirm == 'Yes') {
-
-            uninstall(req.body);
-
-        }
-
-
-        // Redirect to the home page
-        res.redirect("/");
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
 }

+ 0 - 150
controllers/auth.js

@@ -1,150 +0,0 @@
-const User = require('../database/UserModel');
-const bcrypt = require('bcrypt');
-
-
-exports.Login = function(req,res){
-
-     // check whether we have a session
-     if(req.session.user){
-        // Redirect to log out.
-        res.redirect("/logout");
-    }else{
-        // Render the login page.
-        res.render("pages/login",{
-            "error":"",
-            "isLoggedIn": false
-        });
-    }
-}
-
-exports.processLogin = async function(req,res){
-    // get the data.
-    let email = req.body.email;
-    let password = req.body.password;
-    // check if we have data.
-    if(email && password){
-        // check if the user exists.
-        let existingUser = await User.findOne({ where: {email:email}});
-        if(existingUser){
-            // compare the password.
-            let match = await bcrypt.compare(password,existingUser.password);
-            if(match){
-                // set the session.
-                req.session.user = existingUser.username;
-                req.session.UUID = existingUser.UUID;
-                req.session.role = existingUser.role;
-
-                // Redirect to the home page.
-                res.redirect("/");
-            }else{
-                // return an error.
-                res.render("pages/login",{
-                    "error":"Invalid password",
-                    isLoggedIn: false
-                });
-            }
-        }else{
-            // return an error.
-            res.render("pages/login",{
-                "error":"User with that email does not exist.",
-                isLoggedIn:false
-            });
-        }
-    }else{
-        res.status(400);
-        res.render("pages/login",{
-            "error":"Please fill in all the fields.",
-            isLoggedIn:false
-        });
-    }
-}
-
-
-exports.Logout = function(req,res){
-    // clear the session.
-    req.session.destroy();
-    // Redirect to the login page.
-    res.redirect("/login");    
-}
-
-
-
-exports.Register = function(req,res){
-    // Check whether we have a session
-    if(req.session.user){
-        // Redirect to log out.
-        res.redirect("/logout");
-    } else {
-        // Render the signup page.
-        res.render("pages/register",{
-            "error":"",
-            isLoggedIn:false
-        });
-    }
-}
-
-exports.processRegister = async function(req,res){
-
-    // Get the data.
-    let { first_name, last_name, username, email, password, avatar, tos, secret } = req.body;
-    let role = "user";
-
-    // Check the data.
-    if((first_name && last_name && email && password && username && tos) && (secret == process.env.SECRET)){
-
-        // Check if there is an existing user with that username.
-        let existingUser = await User.findOne({ where: {username:username}});
-
-        let adminUser = await User.findOne({ where: {role:"admin"}});
-
-        if(!existingUser){
-            // hash the password.
-            let hashedPassword = bcrypt.hashSync(password,10);
-
-            if(!adminUser){
-                console.log('Creating admin User');
-                role = "admin";
-            }
-
-            try {
-                const user = await User.create({ 
-                    first_name: first_name,
-                    last_name: last_name,
-                    username: username,
-                    email: email,
-                    password: hashedPassword,
-                    role: role,
-                    group: 'all',
-                    avatar: `<img src="./static/avatars/${avatar}">`
-                 });
-
-                // set the session.
-                req.session.user = user.username;
-                req.session.UUID = user.UUID;
-                req.session.role = user.role;
-                // Redirect to the home page.
-                res.redirect("/");
-            }
-            catch (err) {
-                // return an error.
-                res.render("pages/register",{
-                    "error":"Something went wrong when creating account.",
-                    isLoggedIn:false
-                });
-            }
-
-        }else{
-            // return an error.
-            res.render("pages/register",{
-                "error":"User with that username already exists.",
-                isLoggedIn:false
-            });
-        }
-    }else{
-        // Redirect to the signup page.
-        res.render("pages/register",{
-            "error":"Please fill in all the fields and accept TOS.",
-            isLoggedIn:false
-        });
-    }
-}

+ 13 - 239
controllers/dashboard.js

@@ -1,249 +1,23 @@
-const User = require('../database/UserModel');
-const Containers = require('../database/ContainerSettings');
 
-const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
-const { execSync } = require("child_process");
-const { siteCard } = require('../components/siteCard');
-const { containerExec } = require('../functions/system')
+export const Dashboard = (req, res) => {
 
 
+    res.render("dashboard", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+    });
 
-exports.Dashboard = async function (req, res) {
-
-    if (req.session.role == "admin") {
-
-        // get user data with matching UUID from sqlite database
-        let user = await User.findOne({ where: { UUID: req.session.UUID } });
-
-        let caddy = 'd-none';
-
-        if (process.env.Proxy_Manager == 'enabled') {
-            caddy = '';
-        }
-
-        // Render the home page
-        res.render("pages/dashboard", {
-            name: user.first_name + ' ' + user.last_name,
-            role: user.role,
-            avatar: user.avatar,
-            isLoggedIn: true,
-            site_list: req.app.locals.site_list,
-            caddy: caddy
-        });
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
 }
 
+export const searchDashboard = (req, res) => {
 
+    console.log(req.params);
 
-exports.AddSite = async function (req, res) {
-
-    let { domain, type, host, port } = req.body;
-
-    if ((req.session.role == "admin") && ( domain && type && host && port)) {
-
-
-        let { domain, type, host, port } = req.body;
-
-        // build caddyfile
-        let caddyfile = `${domain} {`
-        caddyfile += `\n\t${type} ${host}:${port}`
-        caddyfile += `\n\theader {`
-        caddyfile += `\n\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains; preload"`
-        caddyfile += `\n\t}`
-        caddyfile += `\n}`
-
-        
-        // save caddyfile
-        writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
-
-
-        // format caddyfile
-        let format = {
-            container: 'DweebProxy',
-            command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
-        }
-        await containerExec(format, function(err, data) {
-            if (err) {
-                console.error(err);
-                return;
-            }
-            console.log(`Formatted ${domain}.Caddyfile`);
-        });
-        
-        ///////////////// convert caddyfile to json
-        let convert = {
-            container: 'DweebProxy',
-            command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
-        }
-        await containerExec(convert, function(err, data) {
-            if (err) {
-                console.error(err);
-                return;
-            }
-            console.log(`Converted ${domain}.Caddyfile to JSON`);
-        });
-
-        ////////////// reload caddy
-        let reload = {
-            container: 'DweebProxy',
-            command: `caddy reload --config /etc/caddy/Caddyfile`
-        }
-        await containerExec(reload, function(err, data) {
-            if (err) {
-                console.error(err);
-                return;
-            }
-            console.log(`Reloaded Caddy Config`);
-        });
-
-        let site = siteCard(type, domain, host, port, 0);
-
-        req.app.locals.site_list += site;
-
-
-        res.redirect("/");
-    } else {
-        // Redirect
-        console.log('not admin or missing info')
-        res.redirect("/");
-    }
-}
-
-
-exports.RemoveSite = async function (req, res) {
-
-    if (req.session.role == "admin") {
-
-
-        for (const [key, value] of Object.entries(req.body)) {
-
-            execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
-                if (err) { console.error(`error: ${err.message}`); return; }
-                if (stderr) { console.error(`stderr: ${stderr}`); return; }
-                console.log(`removed ${value}.Caddyfile`);
-            });
-
-        }
-
-        let reload = {
-            container: 'DweebProxy',
-            command: `caddy reload --config /etc/caddy/Caddyfile`
-        }
-        await containerExec(reload);
-
-        
-        console.log('Removed Site(s)')
-
-        res.redirect("/refreshsites");
-    } else {
-        res.redirect("/");
-    }
-    
-}
-
-
-exports.RefreshSites = async function (req, res) {
-
-    let domain, type, host, port;
-    let id = 1;
-
-    if (req.session.role == "admin") {
-
-
-        // Clear site_list.ejs
-        req.app.locals.site_list = "";
-        
-
-        // check if ./caddyfiles/sites contains any .json files, then delete them
-        try {
-            let files = readdirSync('./caddyfiles/sites/');
-            files.forEach(file => {
-                if (file.includes(".json")) {
-                    execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
-                        if (err) { console.error(`error: ${err.message}`); return; }
-                        if (stderr) { console.error(`stderr: ${stderr}`); return; }
-                        console.log(`removed ${file}`);
-                    });
-                }
-            });
-        } catch (error) { console.log("No .json files to delete") }
-   
-        // get list of Caddyfiles
-        let sites = readdirSync('./caddyfiles/sites/');
-
-
-        sites.forEach(site_name => {
-            // convert the caddyfile of each site to json
-            let convert = {
-                container: 'DweebProxy',
-                command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
-            }
-            containerExec(convert);
-
-            try {
-            // read the json file
-            let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
-            // fix whitespace and parse the json file
-            site_file = site_file.replace(/        /g, "  ");
-            site_file = JSON.parse(site_file);
-            } catch (error) { console.log("No .json file to read") }
-
-
-            // get the domain, type, host, and port from the json file
-            try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
-            try { type = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].handler } catch (error) { console.log("No Type") }
-            try { host = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[0] } catch (error) { console.log("Not Localhost") }
-            try { port = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[1] } catch (error) { console.log("No Port") }
-
-            // build the site card
-            let site = siteCard(type, domain, host, port, id);
-
-            // append the site card to site_list
-            req.app.locals.site_list += site;
-            
-            id++;
-        });
-        
-
-        res.redirect("/");
-    } else {
-        // Redirect to the login page
-        res.redirect("/");
-    }
-}
-
-
-
-exports.DisableSite = async function (req, res) {
-
-    if (req.session.role == "admin") {
-
-        
-        console.log(req.body)
-        console.log('Disable Site')
-
-        res.redirect("/");
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
-}
-
-
-exports.EnableSite = async function (req, res) {
-
-    if (req.session.role == "admin") {
-
-        
-        console.log(req.body)
-        console.log('Enable Site')
+    res.render("dashboard", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+    });
 
-        res.redirect("/");
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
 }

+ 56 - 0
controllers/images.js

@@ -0,0 +1,56 @@
+import { docker } from '../server.js';
+
+export const Images = async function(req, res) {
+
+    let images = await docker.listImages({ all: true });
+
+    let image_list = `
+    <thead>
+        <tr>
+            <th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
+            <th><button class="table-sort" data-sort="sort-name">Name</button></th>
+            <th><button class="table-sort" data-sort="sort-city">ID</button></th>
+            <th><button class="table-sort" data-sort="sort-type">Tag</button></th>
+            <th><button class="table-sort" data-sort="sort-score">Status</button></th>
+            <th><button class="table-sort" data-sort="sort-date">Created</button></th>
+            <th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
+            <th><button class="table-sort" data-sort="sort-progress">Action</button></th>
+        </tr>
+    </thead>
+    <tbody class="table-tbody">`
+
+
+    for (let i = 0; i < images.length; i++) {
+
+        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 details = `
+            <tr>
+                <td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
+                <td class="sort-name">${images[i].RepoTags}</td>
+                <td class="sort-city">${images[i].Id}</td>
+                <td class="sort-type">Latest</td>
+                <td class="sort-score text-green">In use</td>
+                <td class="sort-date" data-date="1628122643">${created}</td>
+                <td class="sort-quantity">${size} MB</td>
+                <td class="text-end"><a class="btn" href="#">Details</a></td>
+            </tr>`
+        image_list += details;
+    }
+    
+    image_list += `</tbody>`
+
+    
+    res.render("images", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+        image_list: image_list,
+        image_count: images.length
+    });
+
+}

+ 82 - 0
controllers/login.js

@@ -0,0 +1,82 @@
+import { User, Syslog } from '../database/models.js';
+import bcrypt from 'bcrypt';
+
+export const Login = function(req,res){
+    if(req.session.user){
+        res.redirect("/logout");
+    }else{
+        res.render("login",{
+            "error":"",
+        });
+    }
+}
+
+export const submitLogin = async function(req,res){
+
+    let { email, password } = req.body;
+
+    if(email && password){
+
+        let existingUser = await User.findOne({ where: {email:email}});
+        if(existingUser){
+
+            let match = await bcrypt.compare(password,existingUser.password);
+
+            if(match){
+
+                let currentDate = new Date();
+                let newLogin = currentDate.toLocaleString();
+                await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
+
+                req.session.user = existingUser.username;
+                req.session.UUID = existingUser.UUID;
+                req.session.role = existingUser.role;
+                req.session.avatar = existingUser.avatar;
+
+                const syslog = await Syslog.create({
+                    user: req.session.user,
+                    email: email,
+                    event: "Successful Login",
+                    message: "User logged in successfully",
+                    ip: req.socket.remoteAddress
+                });
+
+                if (req.session.role == "admin") {
+                    res.redirect("/");
+                }
+                else {
+                    res.redirect("/portal");
+                }
+            }else{
+
+                const syslog = await Syslog.create({
+                    user: null,
+                    email: email,
+                    event: "Bad Login",
+                    message: "Invalid password",
+                    ip: req.socket.remoteAddress
+                });
+
+                res.render("login",{
+                    "error":"Invalid password",
+                });
+            }
+        }else{
+            res.render("login",{
+                "error":"User with that email does not exist.",
+            });
+        }
+    }else{
+        res.status(400);
+        res.render("login",{
+            "error":"Please fill in all the fields.",
+        });
+    }
+}
+
+
+export const Logout = function(req,res){
+    req.session.destroy(() => {
+        res.redirect("/login");
+    });
+}

+ 52 - 0
controllers/networks.js

@@ -0,0 +1,52 @@
+import { docker } from '../server.js';
+
+
+export const Networks = async function(req, res) {
+
+    let networks = await docker.listNetworks({ all: true });
+
+    let network_list = `
+        <thead>
+            <tr>
+                <th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
+                <th><button class="table-sort" data-sort="sort-name">Name</button></th>
+                <th><button class="table-sort" data-sort="sort-city">ID</button></th>
+                <th><button class="table-sort" data-sort="sort-score">Status</button></th>
+                <th><button class="table-sort" data-sort="sort-date">Created</button></th>
+                <th><button class="table-sort" data-sort="sort-progress">Action</button></th>
+            </tr>
+        </thead>
+    <tbody class="table-tbody">`
+
+
+    for (let i = 0; i < networks.length; i++) {
+
+        // let date = new Date(images[i].Created * 1000);
+        // let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
+
+    
+
+        let details = `
+            <tr>
+                <td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
+                <td class="sort-name">${networks[i].Name}</td>
+                <td class="sort-city">${networks[i].Id}</td>
+                <td class="sort-score text-green">In use</td>
+                <td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
+                <td class="text-end"><a class="btn" href="#">Details</a></td>
+            </tr>`
+            network_list += details;
+    }
+    
+    network_list += `</tbody>`
+
+    
+    res.render("networks", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+        network_list: network_list,
+        network_count: networks.length
+    });
+
+}

+ 12 - 0
controllers/portal.js

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

+ 100 - 0
controllers/register.js

@@ -0,0 +1,100 @@
+import { User, Syslog } from '../database/models.js';
+import bcrypt from 'bcrypt';
+
+let SECRET = process.env.SECRET || "MrWiskers"
+
+export const Register = function(req,res){
+    if(req.session.user){
+        res.redirect("/logout");
+    } else {
+        res.render("register",{
+            "error":"",
+        });
+    }
+}
+
+
+
+export const submitRegister = async function(req,res){
+
+    let { name, username, email, password, confirmPassword, avatar, warning, secret } = req.body;
+
+
+    if (secret != SECRET) {
+        const syslog = await Syslog.create({
+            user: username,
+            email: email,
+            event: "Failed Registration",
+            message: "Invalid secret",
+            ip: req.socket.remoteAddress
+        });
+    }
+
+    if((name && email && password && confirmPassword && username && warning) && (secret == SECRET) && (password == confirmPassword)){
+
+        async function userRole () {
+            let userCount = await User.count();
+            if(userCount == 0){
+                return "admin";
+            }else{
+                return "user";
+            }
+        }
+
+        let existingUser = await User.findOne({ where: {email:email}});
+        if(!existingUser){
+
+            try {
+                let currentDate = new Date();
+                let newLogin = currentDate.toLocaleString();
+
+                const user = await User.create({ 
+                    name: name,
+                    username: username,
+                    email: email,
+                    password: bcrypt.hashSync(password,10),
+                    role: await userRole(),
+                    group: 'all',
+                    avatar: `<img src="img/avatars/${avatar}">`,
+                    lastLogin: newLogin,
+                });
+
+                // make sure the user was created and get the UUID.
+                let newUser = await User.findOne({ where: {email:email}});
+                let match = await bcrypt.compare(password,newUser.password);
+
+                if(match){  
+                    req.session.user = newUser.username;
+                    req.session.UUID = newUser.UUID;
+                    req.session.role = newUser.role;
+                    req.session.avatar = newUser.avatar;
+
+                    const syslog = await Syslog.create({
+                        user: req.session.user,
+                        email: email,
+                        event: "Successful Registration",
+                        message: "User registered successfully",
+                        ip: req.socket.remoteAddress
+                    });
+
+                    res.redirect("/");
+                }
+            } catch(err) {
+                res.render("register",{
+                    "error":"Something went wrong when creating account.",
+                });
+            }
+
+        } else {
+                // return an error.
+                res.render("register",{
+                    "error":"User with that email already exists.",
+                });
+            }
+    } else {
+        // Redirect to the signup page.
+        res.render("register",{
+            "error":"Please fill in all the fields and acknowledge security warning.",
+        });
+    }
+}

+ 6 - 19
controllers/settings.js

@@ -1,22 +1,9 @@
-const User = require('../database/UserModel.js');
-const Server = require('../database/ServerSettings.js');
 
-exports.Settings = async function(req, res) {
-    if (req.session.role == "admin") {
-        // Get the user.
-        let user = await User.findOne({ where: { UUID: req.session.UUID }});
+export const Settings = (req, res) => {
 
-        
-
-        // Render the home page
-        res.render("pages/settings", {
-            name: user.first_name + ' ' + user.last_name,
-            role: user.role,
-            avatar: user.avatar,
-            isLoggedIn: true
-        });
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
+    res.render("settings", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+    });
 }

+ 36 - 0
controllers/syslogs.js

@@ -0,0 +1,36 @@
+import { Syslog } from '../database/models.js';
+
+export const Syslogs = async function(req, res) {
+
+    let logs = '';
+
+    const syslogs = await Syslog.findAll({
+        order: [
+            ['id', 'DESC']
+        ]
+    });
+
+    for (const log of syslogs) {
+        let date = (log.createdAt).toDateString();
+        let time = (log.createdAt).toLocaleTimeString();
+        let datetime = `${time} ${date}`;
+
+        logs += `<tr>
+                    <td class="sort-id">${log.id}</td>
+                    <td class="sort-user">${log.user}</td>
+                    <td class="sort-email">${log.email}</td>
+                    <td class="sort-event">${log.event}</td>
+                    <td class="sort-message">${log.message}</td>
+                    <td class="sort-ip">${log.ip}</td>
+                    <td class="sort-datetime">${datetime}</td>
+                </tr>`
+    }
+    
+    res.render("syslogs", {
+        name: req.session.user || 'Dev',
+        role: req.session.role || 'Dev',
+        avatar: req.session.avatar || '<img src="/img/avatars/rus.jpg">',
+        logs: logs
+    });
+
+}

+ 54 - 48
controllers/users.js

@@ -1,54 +1,60 @@
-const User = require('../database/UserModel');
+import { User } from '../database/models.js';
 
-exports.Users = async function(req, res) {
-    if (req.session.role == "admin") {
+export const Users = async (req, res) => {
+   
+    let user_list = `
+    <tr>
+        <th><input class="form-check-input" type="checkbox"></th>
+        <th>ID</th>
+        <th>Avatar</th>
+        <th>Name</th>
+        <th>Username</th>
+        <th>Email</th>
+        <th>UUID</th>
+        <th>Role</th>
+        <th>Last Login</th>
+        <th>Status</th>
+        <th>Actions</th>
+    </tr>`
 
-        // Get the user.
-        let user = await User.findOne({ where: { UUID: req.session.UUID }});
-        let user_list = `
+    let allUsers = await User.findAll();
+    allUsers.forEach((account) => {
+
+        let active = '<span class="badge badge-outline text-green">Active</span>'
+        let lastLogin = new Date(account.lastLogin);
+        let currentDate = new Date();
+        let days = Math.floor((currentDate - lastLogin) / (1000 * 60 * 60 * 24));
+
+        if (days > 30) {
+            active = '<span class="badge badge-outline text-grey">Inactive</span>';
+        }
+
+
+
+        let info = `
         <tr>
-            <th><input class="form-check-input" type="checkbox"></th>
-            <th>ID</th>
-            <th>Avatar</th>
-            <th>Name</th>
-            <th>Username</th>
-            <th>Email</th>
-            <th>UUID</th>
-            <th>Role</th>
-            <th>Status</th>
-            <th>Actions</th>
+            <td><input class="form-check-input" type="checkbox"></td>
+            <td>${account.id}</td>
+            <td><span class="avatar me-2">${account.avatar}</span></td>
+            <td>${account.name}</td>
+            <td>${account.username}</td>
+            <td>${account.email}</td>
+            <td>${account.UUID}</td>
+            <td>${account.role}</td>
+            <td>${account.lastLogin}</td>
+            <td>${active}</td>
+            <td><a href="#" class="btn">Edit</a></td>
         </tr>`
 
-        let users = await User.findAll();
-        users.forEach((account) => {
-            full_name = account.first_name + ' ' + account.last_name;
-            user_info = `
-            <tr>
-                <td><input class="form-check-input" type="checkbox"></td>
-                <td>${user.id}</td>
-                <td><span class="avatar me-2">${account.avatar}</span></td>
-                <td>${full_name}</td>
-                <td>${account.username}</td>
-                <td>${account.email}</td>
-                <td>${account.UUID}</td>
-                <td>${account.role}</td>
-                <td><span class="badge badge-outline text-green">Active</span></td>
-                <td><a href="#" class="btn">Edit</a></td>
-            </tr>`
-
-            user_list += user_info;
-        });
-
-        // Render the home page
-        res.render("pages/users", {
-            name: user.first_name + ' ' + user.last_name,
-            role: user.role,
-            avatar: user.avatar,
-            isLoggedIn: true,
-            user_list: user_list
-        });
-    } else {
-        // Redirect to the login page
-        res.redirect("/login");
-    }
+        user_list += info;
+    });
+
+
+    res.render("users", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+        user_list: user_list
+    });
+
 }

+ 70 - 0
controllers/volumes.js

@@ -0,0 +1,70 @@
+import { docker } from '../server.js';
+
+
+export const Volumes = async function(req, res) {
+
+    let list = await docker.listVolumes({ all: true });
+    let volumes = list.Volumes;
+
+    let volume_list = `
+    <thead>
+        <tr>
+            <th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
+            <th><button class="table-sort" data-sort="sort-name">Name</button></th>
+            <th><button class="table-sort" data-sort="sort-city">Mount point</button></th>
+            <th><button class="table-sort" data-sort="sort-score">Status</button></th>
+            <th><button class="table-sort" data-sort="sort-date">Created</button></th>
+            <th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
+            <th><button class="table-sort" data-sort="sort-progress">Action</button></th>
+        </tr>
+    </thead>
+    <tbody class="table-tbody">`
+
+
+
+    for (let i = 0; i < volumes.length; i++) {
+        let volume = volumes[i];
+        let name = volume.Name;
+        let mount = volume.Mountpoint;
+
+        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]);
+        //     }
+        // });
+
+    
+        let details = `
+        <tr>
+            <td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
+            <td class="sort-name">${name}</td>
+            <td class="sort-city">${mount}</td>
+            <td class="sort-score text-green">In use</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 += `</tbody>`
+
+    
+    res.render("volumes", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+        volume_list: volume_list,
+        volume_count: volumes.length
+    });
+
+}

+ 0 - 47
database/ContainerSettings.js

@@ -1,47 +0,0 @@
-const { Sequelize, DataTypes } = require('sequelize');
-
-const sequelize = new Sequelize({
-  dialect: 'sqlite',
-  storage: './database/db.sqlite',
-  logging: false
-});
-
-
-const Containers = sequelize.define('Containers', {
-  // Model attributes are defined here
-  id: {
-    type: DataTypes.INTEGER,
-    autoIncrement: true,
-    primaryKey: true
-  },
-  name: {
-    type: DataTypes.STRING,
-    allowNull: false
-  },
-  visibility: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  size: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  group: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  permissions: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  }
-});
-
-async function syncModel() {
-  await sequelize.sync();
-  console.log('Containers model synced');
-}
-
-syncModel();
-
-
-module.exports = Containers;

+ 0 - 42
database/ServerSettings.js

@@ -1,42 +0,0 @@
-const { Sequelize, DataTypes } = require('sequelize');
-
-const sequelize = new Sequelize({
-  dialect: 'sqlite',
-  storage: './database/db.sqlite',
-  logging: false
-});
-
-
-const Server = sequelize.define('Server', {
-  // Model attributes are defined here
-  timezone: {
-    type: DataTypes.STRING,
-    allowNull: false
-  },
-  hwa: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  media: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  pgid: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  puid: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  }
-});
-
-async function syncModel() {
-  await sequelize.sync();
-  console.log('Server model synced');
-}
-
-syncModel();
-
-
-module.exports = Server;

+ 0 - 63
database/UserModel.js

@@ -1,63 +0,0 @@
-const { Sequelize, DataTypes } = require('sequelize');
-
-const sequelize = new Sequelize({
-  dialect: 'sqlite',
-  storage: './database/db.sqlite',
-  logging: false
-});
-
-
-const User = sequelize.define('User', {
-  // Model attributes are defined here
-  id: {
-    type: DataTypes.INTEGER,
-    autoIncrement: true,
-    primaryKey: true
-  },
-  first_name: {
-    type: DataTypes.STRING,
-    allowNull: false
-  },
-  last_name: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  username: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  email: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  password: {
-    type: DataTypes.STRING,
-    // allowNull: false
-  },
-  role: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  group: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  avatar: {
-    type: DataTypes.STRING
-    // allowNull defaults to true
-  },
-  UUID: {
-    type: DataTypes.UUID,
-    defaultValue: DataTypes.UUIDV4
-  }
-});
-
-async function syncModel() {
-  await sequelize.sync();
-  console.log('User model synced');
-}
-
-syncModel();
-
-
-module.exports = User;

+ 189 - 0
database/models.js

@@ -0,0 +1,189 @@
+import { Sequelize, DataTypes } from 'sequelize';
+
+// let SQLITE_PASS = process.env.SQLITE_PASS || 'some_long_elaborate_password';
+
+// export const sequelize = new Sequelize('dweebui', 'dweebui', SQLITE_PASS, { 
+//   dialect: 'sqlite',
+//   dialectModulePath: '@journeyapps/sqlcipher',
+//   storage: './database/database.sqlite',
+//   logging: false,
+// });
+
+export const sequelize = new Sequelize({ 
+  dialect: 'sqlite',
+  storage: './database/database.sqlite',
+  logging: false,
+});
+
+export const User = sequelize.define('User', {
+  id: {
+    type: DataTypes.INTEGER,
+    autoIncrement: true,
+    primaryKey: true
+  },
+  name: {
+    type: DataTypes.STRING
+  },
+  username: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  email: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  password: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  role: {
+    type: DataTypes.STRING
+  },
+  group: {
+    type: DataTypes.STRING
+  },
+  avatar: {
+    type: DataTypes.STRING
+  },
+  lastLogin: {
+    type: DataTypes.STRING
+  },
+  UUID: {
+    type: DataTypes.UUID,
+    defaultValue: DataTypes.UUIDV4,
+  }
+});
+
+export const Container = sequelize.define('Container', {
+  id: {
+    type: DataTypes.INTEGER,
+    autoIncrement: true,
+    primaryKey: true
+  },
+  name: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  visibility: {
+    type: DataTypes.STRING
+  },
+  size: {
+    type: DataTypes.STRING
+  },
+  group: {
+    type: DataTypes.STRING
+  }
+});
+
+export const Permission = sequelize.define('Permission', {
+  id: {
+    type: DataTypes.INTEGER,
+    autoIncrement: true,
+    primaryKey: true
+  },
+  containerName: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  containerID: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  user: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  userID: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  install: {
+    type: DataTypes.STRING,
+  },
+  uninstall: {
+    type: DataTypes.STRING
+  },
+  edit: {
+    type: DataTypes.STRING
+  },
+  upgrade: {
+    type: DataTypes.STRING
+  },
+  start: {
+    type: DataTypes.STRING
+  },
+  stop: {
+    type: DataTypes.STRING
+  },
+  restart: {
+    type: DataTypes.STRING
+  },
+  pause: {
+    type: DataTypes.STRING
+  },
+  logs: {
+    type: DataTypes.STRING
+  },
+  hide: {
+    type: DataTypes.STRING
+  },
+  view: {
+    type: DataTypes.STRING
+  },
+  reset_view: {
+    type: DataTypes.STRING
+  },
+});
+
+
+export const Syslog = sequelize.define('Syslog', {
+  id: {
+    type: DataTypes.INTEGER,
+    autoIncrement: true,
+    primaryKey: true
+  },
+  user: {
+    type: DataTypes.STRING
+  },
+  email: {
+    type: DataTypes.STRING
+  },
+  event: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  message: {
+    type: DataTypes.STRING,
+    allowNull: false
+  },
+  ip : {
+    type: DataTypes.STRING
+  },
+});
+
+
+export const Notification = sequelize.define('Notification', {
+  id: {
+    type: DataTypes.INTEGER,
+    autoIncrement: true,
+    primaryKey: true
+  },
+  title: {
+    type: DataTypes.STRING
+  },
+  message: {
+    type: DataTypes.STRING
+  },
+  icon: {
+    type: DataTypes.STRING,
+  },
+  color: {
+    type: DataTypes.STRING,
+  },
+  createdAt : {
+    type: DataTypes.STRING
+  },
+  createdBy : {
+    type: DataTypes.STRING
+  },
+});

+ 3 - 10
docker-compose.yaml

@@ -2,30 +2,23 @@ version: "3.9"
 services:
   dweebui:
     container_name: dweebui
-    image: lllllllillllllillll/dweebui:v0.08
-    # build:
-    #   context: .
+    image: lllllllillllllillll/dweebui:v0.20
     environment:
       NODE_ENV: production
       PORT: 8000
       SECRET: MrWiskers
-      #Proxy_Manager: enabled
     restart: unless-stopped
     ports:
       - 8000:8000
     volumes:
       - dweebui:/app
-      - caddyfiles:/app/caddyfiles
       - /var/run/docker.sock:/var/run/docker.sock
-      #- ./custom-templates.json:/app/custom-templates.json
-      #- ./composefiles:/app/composefiles
     networks:
-      - dweeb_network
+      - dweebui_net
 
 volumes:
   dweebui:
-  caddyfiles:
 
 networks:
-  dweeb_network:
+  dweebui_net:
     driver: bridge

+ 0 - 205
functions/compose.js

@@ -1,205 +0,0 @@
-const { writeFileSync, mkdirSync, readFileSync } = require("fs");
-const yaml = require('js-yaml');
-
-const { exec, execSync } = require("child_process");
-
-const { docker } = require('./system');
-
-var DockerodeCompose = require('dockerode-compose');
-
-
-module.exports.install = async function (data) {
-    
-        console.log(`[Start of install function]`);
-
-        let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;        
-        let { port0, port1, port2, port3, port4, port5 } = data;
-        let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
-        let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
-        let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
-
-
-        if ((service_name.includes('caddy')) || (name.includes('caddy'))) {
-            req.app.locals.caddy = 'enabled';
-        }
-
-        let docker_volumes = [];
-
-        if (image.startsWith('https://')){
-            mkdirSync(`./appdata/${name}`, { recursive: true });
-            execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
-            console.log(`Downloaded stackfile: ${image}`);
-            let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
-            let services = Object.keys(stackfile.services);
-
-            for ( let i = 0; i < services.length; i++ ) {
-                try {
-                    console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
-                } catch { console.log('no env') }
-            }
-            
-        } else {
-
-            let compose_file = `version: '3'`;
-                compose_file += `\nservices:`
-                compose_file += `\n  ${service_name}:`
-                compose_file += `\n    container_name: ${name}`;
-                compose_file += `\n    image: ${image}`;
-
-            // Command
-            if (command_check == 'on') {
-                compose_file += `\n    command: ${command}`
-            }
-
-            // Network mode
-            if (net_mode == 'host') {
-                compose_file += `\n    network_mode: 'host'`
-            }
-            else if (net_mode != 'host' && net_mode != 'docker') {
-                compose_file += `\n    network_mode: '${net_mode}'`
-            }
-            
-            // Restart policy
-            if (restart_policy != '') {
-                compose_file += `\n    restart: ${restart_policy}`
-            }
-
-            // Ports
-            if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
-                compose_file += `\n    ports:`
-
-                    for (let i = 0; i < 6; i++) {
-                        if (data[`port${i}`] == 'on') {
-                            compose_file += `\n      - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
-                        }
-                    }
-            }
-
-            // Volumes
-            if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
-                compose_file += `\n    volumes:`
-
-                for (let i = 0; i < 6; i++) {
-
-                    // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config  )
-                    if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
-                        compose_file += `\n      - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-                    }
-
-                    // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
-                    else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
-                        let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
-                        compose_file += `\n      - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-                        docker_volumes.push(`${name}_${volume_name}`);
-                    } 
-                }
-            }
-
-            // Environment variables
-            if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
-                compose_file += `\n    environment:`
-            }
-            for (let i = 0; i < 12; i++) {
-                if (data[`env${i}`] == 'on') {
-                    compose_file += `\n      - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
-
-                }
-            }
-
-            // Add labels
-            if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
-                compose_file += `\n    labels:`
-            }   
-            for (let i = 0; i < 12; i++) {
-                if (data[`label${i}`] == 'on') {
-                    compose_file += `\n      - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
-                }
-            }
-
-            // Add privileged mode 
-
-            if (data.privileged == 'on') {
-                compose_file += `\n    privileged: true`
-            }
-
-
-            // Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
-            if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
-                for (let i = 0; i < 12; i++) {
-                    if (data[`env${i}`] == 'on') {
-                        if (data[`env_${i}_name`] == 'DRINODE') {
-                            compose_file += `\n    deploy:`
-                            compose_file += `\n      resources:`
-                            compose_file += `\n        reservations:`
-                            compose_file += `\n          devices:`
-                            compose_file += `\n          - driver: nvidia`
-                            compose_file += `\n            count: 1`
-                            compose_file += `\n            capabilities: [gpu]`
-                        }
-                    }
-                }
-            }
-
-    
-            // add any docker volumes to the docker-compose file
-            if ( docker_volumes.length > 0 ) {
-                compose_file += `\n`
-                compose_file += `\nvolumes:`
-
-                // check docker_volumes for duplicates and remove them completely
-                docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
-
-                for (let i = 0; i < docker_volumes.length; i++) {
-                    if ( docker_volumes[i] != '') {
-                        compose_file += `\n  ${docker_volumes[i]}:`
-                    }
-                }
-            }
-
-            try {   
-                mkdirSync(`./appdata/${name}`, { recursive: true });
-                writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
-
-            } catch { console.log('error creating directory or compose file') }
-
-            try {
-                var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
-
-                (async () => {
-                await compose.pull();
-                await compose.up();
-                })();
-
-            } catch { console.log('error running compose file')}
-
-        }
-
-
-}
-
-
-
-module.exports.uninstall = async function (data) {
-    
-
-        if (data.confirm == 'Yes') {
-
-
-            var containerName = docker.getContainer(`${data.service_name}`);
-
-            try {
-                    containerName.stop(function (err, data) {
-                        if (data) {
-                            containerName.remove(function (err, data) {
-                            });
-                        }
-                    });
-                } catch { 
-                    containerName.remove(function (err, data) {
-                    });
-                }
-
-        }
-
-   
-}

+ 208 - 0
functions/install.js

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

+ 0 - 194
functions/package_manager.js

@@ -1,194 +0,0 @@
-const { writeFileSync, mkdirSync, readFileSync } = require("fs");
-const yaml = require('js-yaml');
-
-const { execSync } = require("child_process");
-
-const { docker } = require('./system');
-
-var DockerodeCompose = require('dockerode-compose');
-
-
-module.exports.install = async function (data) {
-    
-        console.log(`[Start of install function]`);
-
-        let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;        
-        let { port0, port1, port2, port3, port4, port5 } = data;
-        let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
-        let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
-        let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
-
-        let docker_volumes = [];
-
-        if (image.startsWith('https://')){
-            mkdirSync(`./appdata/${name}`, { recursive: true });
-            execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
-            console.log(`Downloaded stackfile: ${image}`);
-            let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
-            let services = Object.keys(stackfile.services);
-
-            for ( let i = 0; i < services.length; i++ ) {
-                try {
-                    console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
-                } catch { console.log('no env') }
-            }
-            
-        } else {
-
-            let compose_file = `version: '3'`;
-                compose_file += `\nservices:`
-                compose_file += `\n  ${service_name}:`
-                compose_file += `\n    container_name: ${name}`;
-                compose_file += `\n    image: ${image}`;
-
-            // Command
-            if (command_check == 'on') {
-                compose_file += `\n    command: ${command}`
-            }
-
-            // Network mode
-            if (net_mode == 'host') {
-                compose_file += `\n    network_mode: 'host'`
-            }
-            else if (net_mode != 'host' && net_mode != 'docker') {
-                compose_file += `\n    network_mode: '${net_mode}'`
-            }
-            
-            // Restart policy
-            if (restart_policy != '') {
-                compose_file += `\n    restart: ${restart_policy}`
-            }
-
-            // Ports
-            if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
-                compose_file += `\n    ports:`
-
-                    for (let i = 0; i < 6; i++) {
-                        if (data[`port${i}`] == 'on') {
-                            compose_file += `\n      - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
-                        }
-                    }
-            }
-
-            // Volumes
-            if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
-                compose_file += `\n    volumes:`
-
-                for (let i = 0; i < 6; i++) {
-
-                    // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config  )
-                    if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
-                        compose_file += `\n      - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-                    }
-
-                    // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
-                    else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
-                        let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
-                        compose_file += `\n      - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-                        docker_volumes.push(`${name}_${volume_name}`);
-                    } 
-                }
-            }
-
-            // Environment variables
-            if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
-                compose_file += `\n    environment:`
-            }
-            for (let i = 0; i < 12; i++) {
-                if (data[`env${i}`] == 'on') {
-                    compose_file += `\n      - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
-
-                }
-            }
-
-            // Add labels
-            if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
-                compose_file += `\n    labels:`
-            }   
-            for (let i = 0; i < 12; i++) {
-                if (data[`label${i}`] == 'on') {
-                    compose_file += `\n      - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
-                }
-            }
-
-            // Add privileged mode 
-
-            if (data.privileged == 'on') {
-                compose_file += `\n    privileged: true`
-            }
-
-
-            // Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
-            if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
-                for (let i = 0; i < 12; i++) {
-                    if (data[`env${i}`] == 'on') {
-                        if (data[`env_${i}_name`] == 'DRINODE') {
-                            compose_file += `\n    deploy:`
-                            compose_file += `\n      resources:`
-                            compose_file += `\n        reservations:`
-                            compose_file += `\n          devices:`
-                            compose_file += `\n          - driver: nvidia`
-                            compose_file += `\n            count: 1`
-                            compose_file += `\n            capabilities: [gpu]`
-                        }
-                    }
-                }
-            }
-
-    
-            // add any docker volumes to the docker-compose file
-            if ( docker_volumes.length > 0 ) {
-                compose_file += `\n`
-                compose_file += `\nvolumes:`
-
-                // check docker_volumes for duplicates and remove them completely
-                docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
-
-                for (let i = 0; i < docker_volumes.length; i++) {
-                    if ( docker_volumes[i] != '') {
-                        compose_file += `\n  ${docker_volumes[i]}:`
-                    }
-                }
-            }
-
-            try {   
-                mkdirSync(`./appdata/${name}`, { recursive: true });
-                writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
-
-            } catch { console.log('error creating directory or compose file') }
-
-            try {
-                var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
-
-                (async () => {
-                await compose.pull();
-                await compose.up();
-                })();
-
-            } catch { console.log('error running compose file')}
-
-        }
-
-
-}
-
-
-
-module.exports.uninstall = async function (data) {
-    if (data.confirm == 'Yes') {
-        console.log(`Uninstalling ${data.service_name}: ${data}`);
-        var containerName = docker.getContainer(`${data.service_name}`);
-        try {
-            await containerName.stop();
-            console.log(`Stopped ${data.service_name} container`);
-        } catch {
-            console.log(`Error stopping ${data.service_name} container`);
-        }
-        try {
-            await containerName.remove();
-            console.log(`Removed ${data.service_name} container`);
-        } catch {
-            console.log(`Error removing ${data.service_name} container`);
-        }
-    }
-}

+ 0 - 315
functions/system.js

@@ -1,315 +0,0 @@
-const { currentLoad, mem, networkStats, fsSize, dockerContainerStats, networkInterfaces } = require('systeminformation');
-var Docker = require('dockerode');
-var docker = new Docker({ socketPath: '/var/run/docker.sock' });
-const { dashCard } = require('../components/dashCard');
-const { Readable } = require('stream');
-
-const Containers = require('../database/ContainerSettings');
-
-// export docker
-module.exports.docker = docker;
-
-
-let IPv4 = '';
-networkInterfaces().then(data => {
-    IPv4 = data[0].ip4;
-});
-
-let hidden = '';
-module.exports.hiddenContainers = async function () {
-    hidden = await Containers.findAll({ where: {visibility:false}});
-    hidden = hidden.map(a => a.name);
-}
-
-module.exports.serverStats = async function () {
-    const cpuUsage = await currentLoad();
-    const ramUsage = await mem();
-    const netUsage = await networkStats();
-    const diskUsage = await fsSize();
-
-    const info = {
-        cpu: Math.round(cpuUsage.currentLoad),
-        ram: Math.round((ramUsage.active / ramUsage.total) * 100),
-        tx: netUsage[0].tx_bytes,
-        rx: netUsage[0].rx_bytes,
-        disk: diskUsage[0].use,
-    };
-
-    return info;
-}
-
-
-
-module.exports.containerList = async function () {
-    let card_list = '';
-
-    const data = await docker.listContainers({ all: true });
-    for (const container of data) {
-
-
-        if (!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();
-
-            // Get ports //////////////////////////
-            let ports_list = [];
-            try {
-                for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
-                    let ports = {
-                        check : 'checked',
-                        external: value[0].HostPort,
-                        internal: key.split('/')[0],
-                        protocol: key.split('/')[1]
-                    }
-                    ports_list.push(ports);
-                }
-            } catch { 
-                // console.log('no ports') 
-                }
-
-            for (let i = 0; i < 12; i++) {
-                if (ports_list[i] == undefined) {
-                    let ports = {
-                        check : '',
-                        external: '',
-                        internal: '',
-                        protocol: ''
-                    }
-                    ports_list[i] = ports;
-                }
-            } /////////////////////////////////////
-
-
-            // Get volumes ////////////////////////
-            let volumes_list = [];
-            try { for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
-                    let volumes = {
-                        check : 'checked',
-                        bind: value.split(':')[0],
-                        container: value.split(':')[1],
-                        readwrite: value.split(':')[2]
-                    }
-                    volumes_list.push(volumes);
-                }} catch { 
-                    // console.log('no volumes') 
-                    }
-            for (let i = 0; i < 12; i++) {
-                if (volumes_list[i] == undefined) {
-                    let volumes = {
-                        check : '',
-                        bind: '',
-                        container: '',
-                        readwrite: ''
-                    }
-                    volumes_list[i] = volumes;
-                }
-            } /////////////////////////////////////
-
-
-            // Get environment variables.
-            let environment_variables = [];
-            try { for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
-                let env = {
-                    check : 'checked',
-                    name: value.split('=')[0],
-                    default: value.split('=')[1]
-                }
-                environment_variables.push(env);
-            }} catch { console.log('no env') }
-            for (let i = 0; i < 12; i++) {
-                if (environment_variables[i] == undefined) {
-                    let env = {
-                        check : '',
-                        name: '',
-                        default: ''
-                    }
-                    environment_variables[i] = env;
-                }
-            }
-
-            // Get labels.
-            let labels = [];
-            for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
-                let label = {
-                    check : 'checked',
-                    name: key,
-                    value: value
-                }
-                labels.push(label);
-            }
-            for (let i = 0; i < 12; i++) {
-                if (labels[i] == undefined) {
-                    let label = {
-                        check : '',
-                        name: '',
-                        value: ''
-                    }
-                    labels[i] = label;
-                }
-            }
-
-
-            let container_info = {
-                name: container.Names[0].slice(1),
-                service: service,
-                id: container.Id,
-                state: container.State,
-                image: container.Image,
-                external_port: ports_list[0].external || 0,
-                internal_port: ports_list[0].internal || 0, 
-                ports: ports_list,
-                volumes: volumes_list,
-                environment_variables: environment_variables,
-                labels: labels,
-                IPv4: IPv4,
-                style: "Compact"
-            }
-
-            let dockerCard = dashCard(container_info);
-
-            card_list += dockerCard;
-        }
-        
-    }
-
-    return card_list;
-}
-
-
-
-
-
-
-
-module.exports.containerStats = async function () {
-
-    let container_stats = [];
-    const data = await docker.listContainers({ all: true });
-
-    for (const container of data) {
-
-        if (!hidden.includes(container.Names[0].slice(1))) {
-            const stats = await dockerContainerStats(container.Id);
-            
-            let container_stat = {
-                name: container.Names[0].slice(1),
-                cpu: Math.round(stats[0].cpuPercent),
-                ram: Math.round(stats[0].memPercent)
-            }
-            
-            //push stats to an array
-            container_stats.push(container_stat);
-        }
-    }
-    return container_stats;
-}
-
-
-
-
-
-
-module.exports.containerAction = async function (data) {
-
-    let { user, role, action, container, state } = data;
-
-    console.log(`${user} wants to: ${action} ${container}`);
-    
-    if (role == 'admin') {
-        var containerName = docker.getContainer(container);
-
-        if ((action == 'start') && (state == 'stopped')) {
-            containerName.start();
-        } else if ((action == 'start') && (state == 'paused')) {
-            containerName.unpause();
-        } else if ((action == 'stop') && (state != 'stopped')) {
-            containerName.stop();
-        } else if ((action == 'pause') && (state == 'running')) {
-            containerName.pause();
-        } else if ((action == 'pause') && (state == 'paused')) {
-            containerName.unpause();
-        } else if (action == 'restart') {
-            containerName.restart();
-        }
-    } else {
-        console.log('User is not an admin');
-    }
-}
-
-
-
-module.exports.containerExec = async function (data) {
-
-    let { container, command } = data;
-
-    var containerName = docker.getContainer(container);
-
-    var options = {
-        Cmd: ['/bin/sh', '-c', command],
-        AttachStdout: true,
-        AttachStderr: true,
-        Tty: true
-    };
-
-    containerName.exec(options, function (err, exec) {
-        if (err) return;
-
-        exec.start(function (err, stream) {
-            if (err) return;
-
-            containerName.modem.demuxStream(stream, process.stdout, process.stderr);
-
-            exec.inspect(function (err, data) {
-                if (err) return;
-
-              
-            });
-        });
-    });
-    
-}
-
-
-
-
-
-
-
-
-
-
-module.exports.containerLogs = function (data) {
-    return new Promise((resolve, reject) => {
-        let logString = '';
-
-        var options = {
-            follow: false,
-            stdout: true,
-            stderr: false,
-            timestamps: false
-        };
-
-        var containerName = docker.getContainer(data);
-
-        containerName.logs(options, function (err, stream) {
-            if (err) {
-                reject(err);
-                return;
-            }
-
-            const readableStream = Readable.from(stream);
-
-            readableStream.on('data', function (chunk) {
-                logString += chunk.toString('utf8');
-            });
-
-            readableStream.on('end', function () {
-                resolve(logString);
-            });
-        });
-    });
-};

+ 41 - 0
functions/uninstall.js

@@ -0,0 +1,41 @@
+import { docker } from "../server.js";
+import { Syslog } from "../database/models.js";
+
+
+export const Uninstall = async (req, res) => {
+
+    let { confirm, service_name } = req.body;
+
+    if (confirm == 'Yes') {
+        var containerName = docker.getContainer(`${service_name}`);
+        try {
+            await containerName.stop();
+        } catch {
+            console.log(`Error stopping ${service_name} container`);
+        }
+        try {
+            await containerName.remove();
+            
+
+            const syslog = await Syslog.create({
+                user: req.session.user,
+                email: null,
+                event: "App Removal",
+                message: `${service_name} uninstalled successfully`,
+                ip: req.socket.remoteAddress
+            });
+
+
+        } catch {
+            const syslog = await Syslog.create({
+                user: req.session.user,
+                email: null,
+                event: "App Removal",
+                message: `${service_name} uninstallation failed`,
+                ip: req.socket.remoteAddress
+            });
+        }
+    }
+    res.redirect('/');
+}
+

文件差异内容过多而无法显示
+ 798 - 47
package-lock.json


+ 23 - 9
package.json

@@ -1,23 +1,37 @@
 {
   "name": "dweebui",
   "version": "1.0.0",
-  "main": "app.js",
+  "description": "A web UI for Docker",
+  "main": "server.js",
+  "type": "module",
+  "scripts": {
+    "test": "mocha --require @babel/register"
+  },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "bcrypt": "^5.1.0",
-    "child_process": "^1.0.2",
-    "dockerode": "^4.0.0",
+    "@babel/register": "^7.23.7",
+    "@socket.io/admin-ui": "^0.5.1",
+    "bcrypt": "^5.1.1",
+    "chai": "^5.0.0",
+    "compression": "^1.7.4",
+    "cors": "^2.8.5",
+    "dockerode": "^4.0.2",
     "dockerode-compose": "^1.4.0",
     "ejs": "^3.1.9",
     "express": "^4.18.2",
+    "express-rate-limit": "^7.1.5",
     "express-session": "^1.17.3",
+    "helmet": "^7.1.0",
     "js-yaml": "^4.1.0",
+    "mocha": "^10.2.0",
     "sequelize": "^6.35.2",
-    "socket.io": "^4.6.1",
-    "sqlite3": "^5.1.6",
-    "systeminformation": "^5.21.20"
-  },
-  "description": ""
+    "sinon": "^17.0.1",
+    "socket.io": "^4.7.4",
+    "sqlite3": "^5.1.7",
+    "stream": "^0.0.2",
+    "supertest": "^6.3.3",
+    "systeminformation": "^5.21.22"
+  }
 }

+ 1 - 1
public/css/meters.css

@@ -2,7 +2,7 @@
 
 .meter {
     box-sizing: content-box;
-    height: 15px; /* Can be anything */
+    height: 15px;
     margin-left: auto;
     margin-right: auto;
     position: relative;

+ 0 - 72
public/css/tabler.min.css

@@ -11462,47 +11462,38 @@ fieldset:disabled .btn {
 }
 
 .column-gap-0 {
-  -moz-column-gap: 0 !important;
   column-gap: 0 !important
 }
 
 .column-gap-1 {
-  -moz-column-gap: .25rem !important;
   column-gap: .25rem !important
 }
 
 .column-gap-2 {
-  -moz-column-gap: .5rem !important;
   column-gap: .5rem !important
 }
 
 .column-gap-3 {
-  -moz-column-gap: 1rem !important;
   column-gap: 1rem !important
 }
 
 .column-gap-4 {
-  -moz-column-gap: 1.5rem !important;
   column-gap: 1.5rem !important
 }
 
 .column-gap-5 {
-  -moz-column-gap: 2rem !important;
   column-gap: 2rem !important
 }
 
 .column-gap-6 {
-  -moz-column-gap: 3rem !important;
   column-gap: 3rem !important
 }
 
 .column-gap-7 {
-  -moz-column-gap: 5rem !important;
   column-gap: 5rem !important
 }
 
 .column-gap-8 {
-  -moz-column-gap: 8rem !important;
   column-gap: 8rem !important
 }
 
@@ -12910,17 +12901,14 @@ fieldset:disabled .btn {
 }
 
 .columns-2 {
-  -moz-columns: 2 !important;
   columns: 2 !important
 }
 
 .columns-3 {
-  -moz-columns: 3 !important;
   columns: 3 !important
 }
 
 .columns-4 {
-  -moz-columns: 4 !important;
   columns: 4 !important
 }
 
@@ -13821,47 +13809,38 @@ fieldset:disabled .btn {
   }
 
   .column-gap-sm-0 {
-    -moz-column-gap: 0 !important;
     column-gap: 0 !important
   }
 
   .column-gap-sm-1 {
-    -moz-column-gap: .25rem !important;
     column-gap: .25rem !important
   }
 
   .column-gap-sm-2 {
-    -moz-column-gap: .5rem !important;
     column-gap: .5rem !important
   }
 
   .column-gap-sm-3 {
-    -moz-column-gap: 1rem !important;
     column-gap: 1rem !important
   }
 
   .column-gap-sm-4 {
-    -moz-column-gap: 1.5rem !important;
     column-gap: 1.5rem !important
   }
 
   .column-gap-sm-5 {
-    -moz-column-gap: 2rem !important;
     column-gap: 2rem !important
   }
 
   .column-gap-sm-6 {
-    -moz-column-gap: 3rem !important;
     column-gap: 3rem !important
   }
 
   .column-gap-sm-7 {
-    -moz-column-gap: 5rem !important;
     column-gap: 5rem !important
   }
 
   .column-gap-sm-8 {
-    -moz-column-gap: 8rem !important;
     column-gap: 8rem !important
   }
 
@@ -13878,17 +13857,14 @@ fieldset:disabled .btn {
   }
 
   .columns-sm-2 {
-    -moz-columns: 2 !important;
     columns: 2 !important
   }
 
   .columns-sm-3 {
-    -moz-columns: 3 !important;
     columns: 3 !important
   }
 
   .columns-sm-4 {
-    -moz-columns: 4 !important;
     columns: 4 !important
   }
 }
@@ -14790,47 +14766,38 @@ fieldset:disabled .btn {
   }
 
   .column-gap-md-0 {
-    -moz-column-gap: 0 !important;
     column-gap: 0 !important
   }
 
   .column-gap-md-1 {
-    -moz-column-gap: .25rem !important;
     column-gap: .25rem !important
   }
 
   .column-gap-md-2 {
-    -moz-column-gap: .5rem !important;
     column-gap: .5rem !important
   }
 
   .column-gap-md-3 {
-    -moz-column-gap: 1rem !important;
     column-gap: 1rem !important
   }
 
   .column-gap-md-4 {
-    -moz-column-gap: 1.5rem !important;
     column-gap: 1.5rem !important
   }
 
   .column-gap-md-5 {
-    -moz-column-gap: 2rem !important;
     column-gap: 2rem !important
   }
 
   .column-gap-md-6 {
-    -moz-column-gap: 3rem !important;
     column-gap: 3rem !important
   }
 
   .column-gap-md-7 {
-    -moz-column-gap: 5rem !important;
     column-gap: 5rem !important
   }
 
   .column-gap-md-8 {
-    -moz-column-gap: 8rem !important;
     column-gap: 8rem !important
   }
 
@@ -14847,17 +14814,14 @@ fieldset:disabled .btn {
   }
 
   .columns-md-2 {
-    -moz-columns: 2 !important;
     columns: 2 !important
   }
 
   .columns-md-3 {
-    -moz-columns: 3 !important;
     columns: 3 !important
   }
 
   .columns-md-4 {
-    -moz-columns: 4 !important;
     columns: 4 !important
   }
 }
@@ -15759,47 +15723,38 @@ fieldset:disabled .btn {
   }
 
   .column-gap-lg-0 {
-    -moz-column-gap: 0 !important;
     column-gap: 0 !important
   }
 
   .column-gap-lg-1 {
-    -moz-column-gap: .25rem !important;
     column-gap: .25rem !important
   }
 
   .column-gap-lg-2 {
-    -moz-column-gap: .5rem !important;
     column-gap: .5rem !important
   }
 
   .column-gap-lg-3 {
-    -moz-column-gap: 1rem !important;
     column-gap: 1rem !important
   }
 
   .column-gap-lg-4 {
-    -moz-column-gap: 1.5rem !important;
     column-gap: 1.5rem !important
   }
 
   .column-gap-lg-5 {
-    -moz-column-gap: 2rem !important;
     column-gap: 2rem !important
   }
 
   .column-gap-lg-6 {
-    -moz-column-gap: 3rem !important;
     column-gap: 3rem !important
   }
 
   .column-gap-lg-7 {
-    -moz-column-gap: 5rem !important;
     column-gap: 5rem !important
   }
 
   .column-gap-lg-8 {
-    -moz-column-gap: 8rem !important;
     column-gap: 8rem !important
   }
 
@@ -15816,17 +15771,14 @@ fieldset:disabled .btn {
   }
 
   .columns-lg-2 {
-    -moz-columns: 2 !important;
     columns: 2 !important
   }
 
   .columns-lg-3 {
-    -moz-columns: 3 !important;
     columns: 3 !important
   }
 
   .columns-lg-4 {
-    -moz-columns: 4 !important;
     columns: 4 !important
   }
 }
@@ -16728,47 +16680,38 @@ fieldset:disabled .btn {
   }
 
   .column-gap-xl-0 {
-    -moz-column-gap: 0 !important;
     column-gap: 0 !important
   }
 
   .column-gap-xl-1 {
-    -moz-column-gap: .25rem !important;
     column-gap: .25rem !important
   }
 
   .column-gap-xl-2 {
-    -moz-column-gap: .5rem !important;
     column-gap: .5rem !important
   }
 
   .column-gap-xl-3 {
-    -moz-column-gap: 1rem !important;
     column-gap: 1rem !important
   }
 
   .column-gap-xl-4 {
-    -moz-column-gap: 1.5rem !important;
     column-gap: 1.5rem !important
   }
 
   .column-gap-xl-5 {
-    -moz-column-gap: 2rem !important;
     column-gap: 2rem !important
   }
 
   .column-gap-xl-6 {
-    -moz-column-gap: 3rem !important;
     column-gap: 3rem !important
   }
 
   .column-gap-xl-7 {
-    -moz-column-gap: 5rem !important;
     column-gap: 5rem !important
   }
 
   .column-gap-xl-8 {
-    -moz-column-gap: 8rem !important;
     column-gap: 8rem !important
   }
 
@@ -16785,17 +16728,14 @@ fieldset:disabled .btn {
   }
 
   .columns-xl-2 {
-    -moz-columns: 2 !important;
     columns: 2 !important
   }
 
   .columns-xl-3 {
-    -moz-columns: 3 !important;
     columns: 3 !important
   }
 
   .columns-xl-4 {
-    -moz-columns: 4 !important;
     columns: 4 !important
   }
 }
@@ -17697,47 +17637,38 @@ fieldset:disabled .btn {
   }
 
   .column-gap-xxl-0 {
-    -moz-column-gap: 0 !important;
     column-gap: 0 !important
   }
 
   .column-gap-xxl-1 {
-    -moz-column-gap: .25rem !important;
     column-gap: .25rem !important
   }
 
   .column-gap-xxl-2 {
-    -moz-column-gap: .5rem !important;
     column-gap: .5rem !important
   }
 
   .column-gap-xxl-3 {
-    -moz-column-gap: 1rem !important;
     column-gap: 1rem !important
   }
 
   .column-gap-xxl-4 {
-    -moz-column-gap: 1.5rem !important;
     column-gap: 1.5rem !important
   }
 
   .column-gap-xxl-5 {
-    -moz-column-gap: 2rem !important;
     column-gap: 2rem !important
   }
 
   .column-gap-xxl-6 {
-    -moz-column-gap: 3rem !important;
     column-gap: 3rem !important
   }
 
   .column-gap-xxl-7 {
-    -moz-column-gap: 5rem !important;
     column-gap: 5rem !important
   }
 
   .column-gap-xxl-8 {
-    -moz-column-gap: 8rem !important;
     column-gap: 8rem !important
   }
 
@@ -17754,17 +17685,14 @@ fieldset:disabled .btn {
   }
 
   .columns-xxl-2 {
-    -moz-columns: 2 !important;
     columns: 2 !important
   }
 
   .columns-xxl-3 {
-    -moz-columns: 3 !important;
     columns: 3 !important
   }
 
   .columns-xxl-4 {
-    -moz-columns: 4 !important;
     columns: 4 !important
   }
 }

二进制
public/fonts/Inter-Black.woff2


二进制
public/fonts/Inter-BlackItalic.woff2


二进制
public/fonts/Inter-Bold.woff2


二进制
public/fonts/Inter-BoldItalic.woff2


二进制
public/fonts/Inter-ExtraBold.woff2


二进制
public/fonts/Inter-ExtraBoldItalic.woff2


二进制
public/fonts/Inter-ExtraLight.woff2


二进制
public/fonts/Inter-ExtraLightItalic.woff2


二进制
public/fonts/Inter-Italic.woff2


二进制
public/fonts/Inter-Light.woff2


二进制
public/fonts/Inter-LightItalic.woff2


二进制
public/fonts/Inter-Medium.woff2


二进制
public/fonts/Inter-MediumItalic.woff2


二进制
public/fonts/Inter-Regular.woff2


二进制
public/fonts/Inter-SemiBold.woff2


二进制
public/fonts/Inter-SemiBoldItalic.woff2


二进制
public/fonts/Inter-Thin.woff2


二进制
public/fonts/Inter-ThinItalic.woff2


二进制
public/fonts/InterDisplay-Black.woff2


二进制
public/fonts/InterDisplay-BlackItalic.woff2


二进制
public/fonts/InterDisplay-Bold.woff2


二进制
public/fonts/InterDisplay-BoldItalic.woff2


二进制
public/fonts/InterDisplay-ExtraBold.woff2


二进制
public/fonts/InterDisplay-ExtraBoldItalic.woff2


二进制
public/fonts/InterDisplay-ExtraLight.woff2


二进制
public/fonts/InterDisplay-ExtraLightItalic.woff2


二进制
public/fonts/InterDisplay-Italic.woff2


二进制
public/fonts/InterDisplay-Light.woff2


二进制
public/fonts/InterDisplay-LightItalic.woff2


二进制
public/fonts/InterDisplay-Medium.woff2


二进制
public/fonts/InterDisplay-MediumItalic.woff2


二进制
public/fonts/InterDisplay-Regular.woff2


二进制
public/fonts/InterDisplay-SemiBold.woff2


二进制
public/fonts/InterDisplay-SemiBoldItalic.woff2


二进制
public/fonts/InterDisplay-Thin.woff2


二进制
public/fonts/InterDisplay-ThinItalic.woff2


二进制
public/fonts/InterVariable-Italic.woff2


二进制
public/fonts/InterVariable.woff2


+ 59 - 0
public/fonts/inter.css

@@ -0,0 +1,59 @@
+/* Variable fonts usage:
+:root { font-family: "Inter", sans-serif; }
+@supports (font-variation-settings: normal) {
+  :root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
+} */
+@font-face {
+  font-family: InterVariable;
+  font-style: normal;
+  font-weight: 100 900;
+  font-display: swap;
+  src: url('InterVariable.woff2?v=4.0') format('woff2');
+}
+@font-face {
+  font-family: InterVariable;
+  font-style: italic;
+  font-weight: 100 900;
+  font-display: swap;
+  src: url('InterVariable-Italic.woff2?v=4.0') format('woff2');
+}
+/* legacy name "Inter var" (Oct 2023) */
+@font-face { font-family:'Inter var'; font-style:normal; font-weight:100 900; font-display:swap; src: url('InterVariable.woff2?v=4.0') format('woff2'); }
+@font-face { font-family:'Inter var'; font-style:italic; font-weight:100 900; font-display:swap; src: url('InterVariable-Italic.woff2?v=4.0') format('woff2'); }
+/* static fonts */
+@font-face { font-family:Inter; font-style:normal; font-weight:100; font-display:swap; src:url("Inter-Thin.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:100; font-display:swap; src:url("Inter-ThinItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:200; font-display:swap; src:url("Inter-ExtraLight.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:200; font-display:swap; src:url("Inter-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:300; font-display:swap; src:url("Inter-Light.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:300; font-display:swap; src:url("Inter-LightItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:400; font-display:swap; src:url("Inter-Regular.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:400; font-display:swap; src:url("Inter-Italic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:500; font-display:swap; src:url("Inter-Medium.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:500; font-display:swap; src:url("Inter-MediumItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:600; font-display:swap; src:url("Inter-SemiBold.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:600; font-display:swap; src:url("Inter-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:700; font-display:swap; src:url("Inter-Bold.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:700; font-display:swap; src:url("Inter-BoldItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:800; font-display:swap; src:url("Inter-ExtraBold.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:800; font-display:swap; src:url("Inter-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:normal; font-weight:900; font-display:swap; src:url("Inter-Black.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:Inter; font-style:italic; font-weight:900; font-display:swap; src:url("Inter-BlackItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:100; font-display:swap; src:url("InterDisplay-Thin.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:100; font-display:swap; src:url("InterDisplay-ThinItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLight.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:300; font-display:swap; src:url("InterDisplay-Light.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:300; font-display:swap; src:url("InterDisplay-LightItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:400; font-display:swap; src:url("InterDisplay-Regular.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:400; font-display:swap; src:url("InterDisplay-Italic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:500; font-display:swap; src:url("InterDisplay-Medium.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:500; font-display:swap; src:url("InterDisplay-MediumItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBold.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:700; font-display:swap; src:url("InterDisplay-Bold.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:700; font-display:swap; src:url("InterDisplay-BoldItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBold.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:normal; font-weight:900; font-display:swap; src:url("InterDisplay-Black.woff2?v=4.0") format("woff2"); }
+@font-face { font-family:InterDisplay; font-style:italic; font-weight:900; font-display:swap; src:url("InterDisplay-BlackItalic.woff2?v=4.0") format("woff2"); }

+ 0 - 0
public/static/avatars/burns.jpg → public/img/avatars/burns.jpg


二进制
public/img/avatars/duffman.png


+ 0 - 0
public/static/avatars/frank.jpg → public/img/avatars/frank.jpg


+ 0 - 0
public/static/avatars/moe.jpg → public/img/avatars/moe.jpg


二进制
public/img/avatars/moleman.png


+ 0 - 0
public/static/avatars/poochie.jpg → public/img/avatars/poochie.jpg


+ 0 - 0
public/static/avatars/rus.jpg → public/img/avatars/rus.jpg


+ 0 - 0
public/static/avatars/skinner.jpg → public/img/avatars/skinner.jpg


+ 1 - 1
public/js/demo-theme.js

@@ -12,7 +12,7 @@
 })((function () { 'use strict';
 
 	var themeStorageKey = "tablerTheme";
-	var defaultTheme = "light";
+	var defaultTheme = "dark";
 	var selectedTheme;
 	var params = new Proxy(new URLSearchParams(window.location.search), {
 	  get: function get(searchParams, prop) {

+ 93 - 158
public/js/main.js

@@ -1,16 +1,10 @@
-// SOCKET IO
-const socket = io({
-  auth: {
-    token: "abc"
-  }
-});
-
-// ON CONNECT EVENT
-socket.on('connect', () => {
-    console.log('Connected');
+socket.on('connect', () => { 
+  console.log('connected'); 
+  //clear localStorage (because of code in old versions)
+  localStorage.clear();
 });
 
-// SELECT METRICS ELEMENTS
+// Server metrics
 const cpuText = document.getElementById('cpu-text');
 const cpuBar = document.getElementById('cpu-bar');
 const ramText = document.getElementById('ram-text');
@@ -20,19 +14,22 @@ const netBar = document.getElementById('net-bar');
 const diskText = document.getElementById('disk-text');
 const diskBar = document.getElementById('disk-bar');
 
+// Container cards
 const dockerCards = document.getElementById('cards');
 
+// Container logs
 const logViewer = document.getElementById('logView');
 
-//Update usage bars
+// Server metrics
 socket.on('metrics', (data) => {
-
-    let {cpu, ram, tx, rx, disk} = data;
+    let [cpu, ram, tx, rx, disk] = data;
 
     cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
+    if (cpu < 7 ) { cpu = 7; }
     cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
     
     ramText.innerHTML = `<span>RAM ${ram} %</span>`;
+    if (ram < 7 ) { ram = 7; }
     ramBar.innerHTML = `<span style="width: ${ram}%"><span></span></span>`;
 
     tx = Math.round(tx / 1024 / 1024);
@@ -42,165 +39,103 @@ socket.on('metrics', (data) => {
     netBar.innerHTML = `<span style="width: 50%"><span></span></span>`;
 
     diskText.innerHTML = `<span>DISK ${disk} %</span>`;
+    if (disk < 7 ) { disk = 7; }
     diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
 });
 
-function drawCharts(name, cpu_array, ram_array) {
-  var elements = document.querySelectorAll(`${name}`);
-
-  Array.from(elements).forEach(function(element) {
-    if (window.ApexCharts) {
-      new ApexCharts(element, {
-        chart: {
-          type: "line",
-          fontFamily: 'inherit',
-          height: 40.0,
-          sparkline: {
-            enabled: true
-          },
-          animations: {
-            enabled: false
-          }
-        },
-        fill: {
-          opacity: 1
-        },
-        stroke: {
-          width: [2, 1],
-          dashArray: [0, 3],
-          lineCap: "round",
-          curve: "smooth"
-        },
-        series: [{
-          name: "CPU",
-          data: cpu_array
-        }, {
-          name: "RAM",
-          data: ram_array
-        }],
-        tooltip: {
-          theme: 'dark'
-        },
-        grid: {
-          strokeDashArray: 4
-        },
-        xaxis: {
-          labels: {
-            padding: 0
-          },
-          tooltip: {
-            enabled: false
-          },
-          type: 'datetime'
-        },
-        yaxis: {
-          labels: {
-            padding: 4
-          }
-        },
-        labels: [
-          '2020-06-20', '2020-06-21', '2020-06-22', '2020-06-23', '2020-06-24', '2020-06-25', '2020-06-26', '2020-06-27', '2020-06-28', '2020-06-29', '2020-06-30', '2020-07-01', '2020-07-02', '2020-07-03', '2020-07-04', '2020-07-05', '2020-07-06', '2020-07-07', '2020-07-08', '2020-07-09', '2020-07-10', '2020-07-11', '2020-07-12', '2020-07-13', '2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17', '2020-07-18', '2020-07-19'
-        ],
-        colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
-        legend: {
-          show: false
-        }
-      }).render();
-    }
-  });
-}
-
-// container button actions
-function buttonAction(button) {
-  socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
-}
+// Container cards
+socket.on('containers', (data) => {
+    let deleteMeElements = document.querySelectorAll('.deleteme');
+    deleteMeElements.forEach((element) => {
+      element.parentNode.removeChild(element);
+    });
+    dockerCards.insertAdjacentHTML("afterend", data);
+});
 
 
-function hideContainer(button) {
-  socket.emit('hide', {container: button.name});
-}
-
-function resetView() {
-  socket.emit('reset');
+function drawCharts(name, cpuArray, ramArray) {
+  let element = document.querySelector(`${name}`);
+
+  let chart = new ApexCharts(element, {
+    chart: {
+      type: "line",
+      height: 40.0,
+      sparkline: {
+        enabled: true
+      },
+      animations: {
+        enabled: false
+      }
+    },
+    fill: {
+      opacity: 1
+    },
+    stroke: {
+      width: [2, 1],
+      dashArray: [0, 3],
+      lineCap: "round",
+      curve: "smooth"
+    },
+    series: [{
+      name: "CPU",
+      data: cpuArray
+    }, {
+      name: "RAM",
+      data: ramArray
+    }],
+    tooltip: {
+      enabled: false
+    },
+    grid: {
+      strokeDashArray: 4
+    },
+    xaxis: {
+      labels: {
+        padding: 0
+      },
+      tooltip: {
+        enabled: false
+      }
+    },
+    yaxis: {
+      labels: {
+        padding: 4
+      }
+    },
+    colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
+    legend: {
+      show: false
+    }
+  })
+  chart.render();
 }
 
-let containerLogs;
-
-function viewLogs(button) {
-
-  if (button.name != 'refresh') {
-    containerLogs = button.name;
-  }
-
-
-  socket.emit('logs', {container: containerLogs});
+// Buttons functions
+function clicked(button) {
+  socket.emit('clicked', {name: button.name, id: button.id, value: button.value});
 }
 
-socket.on('logString', (data) => {
-  logViewer.innerHTML = `<pre>${data}</pre>`;
-});
-
-
 
-socket.on('cards', (data) => {
+socket.on('containerStats', (data) => {
+  let containerStats = data;
+  
+  for (const [name, statsArray] of Object.entries(containerStats)) {
 
-  console.log('cards deleted');
-  let deleteMeElements = document.querySelectorAll('.deleteme');
-  deleteMeElements.forEach((element) => {
-    element.parentNode.removeChild(element);
-  });
- 
-  dockerCards.insertAdjacentHTML("afterend", data);
+    let cpuArray = statsArray.cpuArray;
+    let ramArray = statsArray.ramArray;
 
-  // check localStorage for items ending with _cpu and redraw the charts
-  for (let i = 0; i < localStorage.length; i++) {
-    if (localStorage.key(i).endsWith('_cpu')) {
-      let name = localStorage.key(i).split('_')[0];
-      let cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
-      let ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
-      drawCharts(`#${name}_chart`, cpu_array, ram_array);
+    let chart = document.getElementById(`${name}_chart`);
+    if (chart) {
+      chart.innerHTML = '';
+      drawCharts(`#${name}_chart`, cpuArray, ramArray);
+    } else {
+      console.log(`Chart element with id ${name}_chart not found in the DOM`);
     }
   }
-  
-  
-});
-
-
-socket.on('containerStats', (data) => {
 
-  let {name, cpu, ram} = data;
-
-  console.log(`drawing chart for ${name}`)
-
-  var cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
-  var ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
-
-  if (cpu_array == null) { cpu_array = Array(30).fill(0); }
-  if (ram_array == null) { ram_array = Array(30).fill(0); }
-
-  cpu_array.push(cpu);
-  ram_array.push(ram);
-  
-  cpu_array = cpu_array.slice(-30);
-  ram_array = ram_array.slice(-30);
-
-  localStorage.setItem(`${name}_cpu`, JSON.stringify(cpu_array));
-  localStorage.setItem(`${name}_ram`, JSON.stringify(ram_array));
-
-  // replace the old chart with the new one
-  let chart = document.getElementById(`${name}_chart`);
-  if (chart) {
-    let newChart = document.createElement('div');
-    newChart.id = `${name}_chart`;
-    chart.parentNode.replaceChild(newChart, chart);
-    drawCharts(`#${name}_chart`, cpu_array, ram_array);
-  } else {
-    console.log(`Chart element with id ${name}_chart not found in the DOM`);
-  }
 });
 
-socket.on('install', (data) => {
-  
-  console.log('added install card');
-  dockerCards.insertAdjacentHTML("afterend", data);
+
+socket.on('logs', (data) => {
+  logViewer.innerHTML = `<pre>${data}</pre>`;
 });

+ 2020 - 0
public/libs/list.js/dist/list.js

@@ -0,0 +1,2020 @@
+var List;List =
+/******/ (function() { // webpackBootstrap
+/******/ 	var __webpack_modules__ = ({
+
+/***/ "./src/add-async.js":
+/*!**************************!*\
+  !*** ./src/add-async.js ***!
+  \**************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (list) {
+  var addAsync = function addAsync(values, callback, items) {
+    var valuesToAdd = values.splice(0, 50);
+    items = items || [];
+    items = items.concat(list.add(valuesToAdd));
+
+    if (values.length > 0) {
+      setTimeout(function () {
+        addAsync(values, callback, items);
+      }, 1);
+    } else {
+      list.update();
+      callback(items);
+    }
+  };
+
+  return addAsync;
+};
+
+/***/ }),
+
+/***/ "./src/filter.js":
+/*!***********************!*\
+  !*** ./src/filter.js ***!
+  \***********************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (list) {
+  // Add handlers
+  list.handlers.filterStart = list.handlers.filterStart || [];
+  list.handlers.filterComplete = list.handlers.filterComplete || [];
+  return function (filterFunction) {
+    list.trigger('filterStart');
+    list.i = 1; // Reset paging
+
+    list.reset.filter();
+
+    if (filterFunction === undefined) {
+      list.filtered = false;
+    } else {
+      list.filtered = true;
+      var is = list.items;
+
+      for (var i = 0, il = is.length; i < il; i++) {
+        var item = is[i];
+
+        if (filterFunction(item)) {
+          item.filtered = true;
+        } else {
+          item.filtered = false;
+        }
+      }
+    }
+
+    list.update();
+    list.trigger('filterComplete');
+    return list.visibleItems;
+  };
+};
+
+/***/ }),
+
+/***/ "./src/fuzzy-search.js":
+/*!*****************************!*\
+  !*** ./src/fuzzy-search.js ***!
+  \*****************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module, __webpack_require__ */
+/*! CommonJS bailout: module.exports is used directly at 8:0-14 */
+/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
+
+var classes = __webpack_require__(/*! ./utils/classes */ "./src/utils/classes.js"),
+    events = __webpack_require__(/*! ./utils/events */ "./src/utils/events.js"),
+    extend = __webpack_require__(/*! ./utils/extend */ "./src/utils/extend.js"),
+    toString = __webpack_require__(/*! ./utils/to-string */ "./src/utils/to-string.js"),
+    getByClass = __webpack_require__(/*! ./utils/get-by-class */ "./src/utils/get-by-class.js"),
+    fuzzy = __webpack_require__(/*! ./utils/fuzzy */ "./src/utils/fuzzy.js");
+
+module.exports = function (list, options) {
+  options = options || {};
+  options = extend({
+    location: 0,
+    distance: 100,
+    threshold: 0.4,
+    multiSearch: true,
+    searchClass: 'fuzzy-search'
+  }, options);
+  var fuzzySearch = {
+    search: function search(searchString, columns) {
+      // Substract arguments from the searchString or put searchString as only argument
+      var searchArguments = options.multiSearch ? searchString.replace(/ +$/, '').split(/ +/) : [searchString];
+
+      for (var k = 0, kl = list.items.length; k < kl; k++) {
+        fuzzySearch.item(list.items[k], columns, searchArguments);
+      }
+    },
+    item: function item(_item, columns, searchArguments) {
+      var found = true;
+
+      for (var i = 0; i < searchArguments.length; i++) {
+        var foundArgument = false;
+
+        for (var j = 0, jl = columns.length; j < jl; j++) {
+          if (fuzzySearch.values(_item.values(), columns[j], searchArguments[i])) {
+            foundArgument = true;
+          }
+        }
+
+        if (!foundArgument) {
+          found = false;
+        }
+      }
+
+      _item.found = found;
+    },
+    values: function values(_values, value, searchArgument) {
+      if (_values.hasOwnProperty(value)) {
+        var text = toString(_values[value]).toLowerCase();
+
+        if (fuzzy(text, searchArgument, options)) {
+          return true;
+        }
+      }
+
+      return false;
+    }
+  };
+  events.bind(getByClass(list.listContainer, options.searchClass), 'keyup', list.utils.events.debounce(function (e) {
+    var target = e.target || e.srcElement; // IE have srcElement
+
+    list.search(target.value, fuzzySearch.search);
+  }, list.searchDelay));
+  return function (str, columns) {
+    list.search(str, columns, fuzzySearch.search);
+  };
+};
+
+/***/ }),
+
+/***/ "./src/index.js":
+/*!**********************!*\
+  !*** ./src/index.js ***!
+  \**********************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module, __webpack_require__ */
+/*! CommonJS bailout: module.exports is used directly at 11:0-14 */
+/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
+
+var naturalSort = __webpack_require__(/*! string-natural-compare */ "./node_modules/string-natural-compare/natural-compare.js"),
+    getByClass = __webpack_require__(/*! ./utils/get-by-class */ "./src/utils/get-by-class.js"),
+    extend = __webpack_require__(/*! ./utils/extend */ "./src/utils/extend.js"),
+    indexOf = __webpack_require__(/*! ./utils/index-of */ "./src/utils/index-of.js"),
+    events = __webpack_require__(/*! ./utils/events */ "./src/utils/events.js"),
+    toString = __webpack_require__(/*! ./utils/to-string */ "./src/utils/to-string.js"),
+    classes = __webpack_require__(/*! ./utils/classes */ "./src/utils/classes.js"),
+    getAttribute = __webpack_require__(/*! ./utils/get-attribute */ "./src/utils/get-attribute.js"),
+    toArray = __webpack_require__(/*! ./utils/to-array */ "./src/utils/to-array.js");
+
+module.exports = function (id, options, values) {
+  var self = this,
+      init,
+      Item = __webpack_require__(/*! ./item */ "./src/item.js")(self),
+      addAsync = __webpack_require__(/*! ./add-async */ "./src/add-async.js")(self),
+      initPagination = __webpack_require__(/*! ./pagination */ "./src/pagination.js")(self);
+
+  init = {
+    start: function start() {
+      self.listClass = 'list';
+      self.searchClass = 'search';
+      self.sortClass = 'sort';
+      self.page = 10000;
+      self.i = 1;
+      self.items = [];
+      self.visibleItems = [];
+      self.matchingItems = [];
+      self.searched = false;
+      self.filtered = false;
+      self.searchColumns = undefined;
+      self.searchDelay = 0;
+      self.handlers = {
+        updated: []
+      };
+      self.valueNames = [];
+      self.utils = {
+        getByClass: getByClass,
+        extend: extend,
+        indexOf: indexOf,
+        events: events,
+        toString: toString,
+        naturalSort: naturalSort,
+        classes: classes,
+        getAttribute: getAttribute,
+        toArray: toArray
+      };
+      self.utils.extend(self, options);
+      self.listContainer = typeof id === 'string' ? document.getElementById(id) : id;
+
+      if (!self.listContainer) {
+        return;
+      }
+
+      self.list = getByClass(self.listContainer, self.listClass, true);
+      self.parse = __webpack_require__(/*! ./parse */ "./src/parse.js")(self);
+      self.templater = __webpack_require__(/*! ./templater */ "./src/templater.js")(self);
+      self.search = __webpack_require__(/*! ./search */ "./src/search.js")(self);
+      self.filter = __webpack_require__(/*! ./filter */ "./src/filter.js")(self);
+      self.sort = __webpack_require__(/*! ./sort */ "./src/sort.js")(self);
+      self.fuzzySearch = __webpack_require__(/*! ./fuzzy-search */ "./src/fuzzy-search.js")(self, options.fuzzySearch);
+      this.handlers();
+      this.items();
+      this.pagination();
+      self.update();
+    },
+    handlers: function handlers() {
+      for (var handler in self.handlers) {
+        if (self[handler] && self.handlers.hasOwnProperty(handler)) {
+          self.on(handler, self[handler]);
+        }
+      }
+    },
+    items: function items() {
+      self.parse(self.list);
+
+      if (values !== undefined) {
+        self.add(values);
+      }
+    },
+    pagination: function pagination() {
+      if (options.pagination !== undefined) {
+        if (options.pagination === true) {
+          options.pagination = [{}];
+        }
+
+        if (options.pagination[0] === undefined) {
+          options.pagination = [options.pagination];
+        }
+
+        for (var i = 0, il = options.pagination.length; i < il; i++) {
+          initPagination(options.pagination[i]);
+        }
+      }
+    }
+  };
+  /*
+   * Re-parse the List, use if html have changed
+   */
+
+  this.reIndex = function () {
+    self.items = [];
+    self.visibleItems = [];
+    self.matchingItems = [];
+    self.searched = false;
+    self.filtered = false;
+    self.parse(self.list);
+  };
+
+  this.toJSON = function () {
+    var json = [];
+
+    for (var i = 0, il = self.items.length; i < il; i++) {
+      json.push(self.items[i].values());
+    }
+
+    return json;
+  };
+  /*
+   * Add object to list
+   */
+
+
+  this.add = function (values, callback) {
+    if (values.length === 0) {
+      return;
+    }
+
+    if (callback) {
+      addAsync(values.slice(0), callback);
+      return;
+    }
+
+    var added = [],
+        notCreate = false;
+
+    if (values[0] === undefined) {
+      values = [values];
+    }
+
+    for (var i = 0, il = values.length; i < il; i++) {
+      var item = null;
+      notCreate = self.items.length > self.page ? true : false;
+      item = new Item(values[i], undefined, notCreate);
+      self.items.push(item);
+      added.push(item);
+    }
+
+    self.update();
+    return added;
+  };
+
+  this.show = function (i, page) {
+    this.i = i;
+    this.page = page;
+    self.update();
+    return self;
+  };
+  /* Removes object from list.
+   * Loops through the list and removes objects where
+   * property "valuename" === value
+   */
+
+
+  this.remove = function (valueName, value, options) {
+    var found = 0;
+
+    for (var i = 0, il = self.items.length; i < il; i++) {
+      if (self.items[i].values()[valueName] == value) {
+        self.templater.remove(self.items[i], options);
+        self.items.splice(i, 1);
+        il--;
+        i--;
+        found++;
+      }
+    }
+
+    self.update();
+    return found;
+  };
+  /* Gets the objects in the list which
+   * property "valueName" === value
+   */
+
+
+  this.get = function (valueName, value) {
+    var matchedItems = [];
+
+    for (var i = 0, il = self.items.length; i < il; i++) {
+      var item = self.items[i];
+
+      if (item.values()[valueName] == value) {
+        matchedItems.push(item);
+      }
+    }
+
+    return matchedItems;
+  };
+  /*
+   * Get size of the list
+   */
+
+
+  this.size = function () {
+    return self.items.length;
+  };
+  /*
+   * Removes all items from the list
+   */
+
+
+  this.clear = function () {
+    self.templater.clear();
+    self.items = [];
+    return self;
+  };
+
+  this.on = function (event, callback) {
+    self.handlers[event].push(callback);
+    return self;
+  };
+
+  this.off = function (event, callback) {
+    var e = self.handlers[event];
+    var index = indexOf(e, callback);
+
+    if (index > -1) {
+      e.splice(index, 1);
+    }
+
+    return self;
+  };
+
+  this.trigger = function (event) {
+    var i = self.handlers[event].length;
+
+    while (i--) {
+      self.handlers[event][i](self);
+    }
+
+    return self;
+  };
+
+  this.reset = {
+    filter: function filter() {
+      var is = self.items,
+          il = is.length;
+
+      while (il--) {
+        is[il].filtered = false;
+      }
+
+      return self;
+    },
+    search: function search() {
+      var is = self.items,
+          il = is.length;
+
+      while (il--) {
+        is[il].found = false;
+      }
+
+      return self;
+    }
+  };
+
+  this.update = function () {
+    var is = self.items,
+        il = is.length;
+    self.visibleItems = [];
+    self.matchingItems = [];
+    self.templater.clear();
+
+    for (var i = 0; i < il; i++) {
+      if (is[i].matching() && self.matchingItems.length + 1 >= self.i && self.visibleItems.length < self.page) {
+        is[i].show();
+        self.visibleItems.push(is[i]);
+        self.matchingItems.push(is[i]);
+      } else if (is[i].matching()) {
+        self.matchingItems.push(is[i]);
+        is[i].hide();
+      } else {
+        is[i].hide();
+      }
+    }
+
+    self.trigger('updated');
+    return self;
+  };
+
+  init.start();
+};
+
+/***/ }),
+
+/***/ "./src/item.js":
+/*!*********************!*\
+  !*** ./src/item.js ***!
+  \*********************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (list) {
+  return function (initValues, element, notCreate) {
+    var item = this;
+    this._values = {};
+    this.found = false; // Show if list.searched == true and this.found == true
+
+    this.filtered = false; // Show if list.filtered == true and this.filtered == true
+
+    var init = function init(initValues, element, notCreate) {
+      if (element === undefined) {
+        if (notCreate) {
+          item.values(initValues, notCreate);
+        } else {
+          item.values(initValues);
+        }
+      } else {
+        item.elm = element;
+        var values = list.templater.get(item, initValues);
+        item.values(values);
+      }
+    };
+
+    this.values = function (newValues, notCreate) {
+      if (newValues !== undefined) {
+        for (var name in newValues) {
+          item._values[name] = newValues[name];
+        }
+
+        if (notCreate !== true) {
+          list.templater.set(item, item.values());
+        }
+      } else {
+        return item._values;
+      }
+    };
+
+    this.show = function () {
+      list.templater.show(item);
+    };
+
+    this.hide = function () {
+      list.templater.hide(item);
+    };
+
+    this.matching = function () {
+      return list.filtered && list.searched && item.found && item.filtered || list.filtered && !list.searched && item.filtered || !list.filtered && list.searched && item.found || !list.filtered && !list.searched;
+    };
+
+    this.visible = function () {
+      return item.elm && item.elm.parentNode == list.list ? true : false;
+    };
+
+    init(initValues, element, notCreate);
+  };
+};
+
+/***/ }),
+
+/***/ "./src/pagination.js":
+/*!***************************!*\
+  !*** ./src/pagination.js ***!
+  \***************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module, __webpack_require__ */
+/*! CommonJS bailout: module.exports is used directly at 5:0-14 */
+/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
+
+var classes = __webpack_require__(/*! ./utils/classes */ "./src/utils/classes.js"),
+    events = __webpack_require__(/*! ./utils/events */ "./src/utils/events.js"),
+    List = __webpack_require__(/*! ./index */ "./src/index.js");
+
+module.exports = function (list) {
+  var isHidden = false;
+
+  var refresh = function refresh(pagingList, options) {
+    if (list.page < 1) {
+      list.listContainer.style.display = 'none';
+      isHidden = true;
+      return;
+    } else if (isHidden) {
+      list.listContainer.style.display = 'block';
+    }
+
+    var item,
+        l = list.matchingItems.length,
+        index = list.i,
+        page = list.page,
+        pages = Math.ceil(l / page),
+        currentPage = Math.ceil(index / page),
+        innerWindow = options.innerWindow || 2,
+        left = options.left || options.outerWindow || 0,
+        right = options.right || options.outerWindow || 0;
+    right = pages - right;
+    pagingList.clear();
+
+    for (var i = 1; i <= pages; i++) {
+      var className = currentPage === i ? 'active' : ''; //console.log(i, left, right, currentPage, (currentPage - innerWindow), (currentPage + innerWindow), className);
+
+      if (is.number(i, left, right, currentPage, innerWindow)) {
+        item = pagingList.add({
+          page: i,
+          dotted: false
+        })[0];
+
+        if (className) {
+          classes(item.elm).add(className);
+        }
+
+        item.elm.firstChild.setAttribute('data-i', i);
+        item.elm.firstChild.setAttribute('data-page', page);
+      } else if (is.dotted(pagingList, i, left, right, currentPage, innerWindow, pagingList.size())) {
+        item = pagingList.add({
+          page: '...',
+          dotted: true
+        })[0];
+        classes(item.elm).add('disabled');
+      }
+    }
+  };
+
+  var is = {
+    number: function number(i, left, right, currentPage, innerWindow) {
+      return this.left(i, left) || this.right(i, right) || this.innerWindow(i, currentPage, innerWindow);
+    },
+    left: function left(i, _left) {
+      return i <= _left;
+    },
+    right: function right(i, _right) {
+      return i > _right;
+    },
+    innerWindow: function innerWindow(i, currentPage, _innerWindow) {
+      return i >= currentPage - _innerWindow && i <= currentPage + _innerWindow;
+    },
+    dotted: function dotted(pagingList, i, left, right, currentPage, innerWindow, currentPageItem) {
+      return this.dottedLeft(pagingList, i, left, right, currentPage, innerWindow) || this.dottedRight(pagingList, i, left, right, currentPage, innerWindow, currentPageItem);
+    },
+    dottedLeft: function dottedLeft(pagingList, i, left, right, currentPage, innerWindow) {
+      return i == left + 1 && !this.innerWindow(i, currentPage, innerWindow) && !this.right(i, right);
+    },
+    dottedRight: function dottedRight(pagingList, i, left, right, currentPage, innerWindow, currentPageItem) {
+      if (pagingList.items[currentPageItem - 1].values().dotted) {
+        return false;
+      } else {
+        return i == right && !this.innerWindow(i, currentPage, innerWindow) && !this.right(i, right);
+      }
+    }
+  };
+  return function (options) {
+    var pagingList = new List(list.listContainer.id, {
+      listClass: options.paginationClass || 'pagination',
+      item: options.item || "<li><a class='page' href='#'></a></li>",
+      valueNames: ['page', 'dotted'],
+      searchClass: 'pagination-search-that-is-not-supposed-to-exist',
+      sortClass: 'pagination-sort-that-is-not-supposed-to-exist'
+    });
+    events.bind(pagingList.listContainer, 'click', function (e) {
+      var target = e.target || e.srcElement,
+          page = list.utils.getAttribute(target, 'data-page'),
+          i = list.utils.getAttribute(target, 'data-i');
+
+      if (i) {
+        list.show((i - 1) * page + 1, page);
+      }
+    });
+    list.on('updated', function () {
+      refresh(pagingList, options);
+    });
+    refresh(pagingList, options);
+  };
+};
+
+/***/ }),
+
+/***/ "./src/parse.js":
+/*!**********************!*\
+  !*** ./src/parse.js ***!
+  \**********************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module, __webpack_require__ */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
+
+module.exports = function (list) {
+  var Item = __webpack_require__(/*! ./item */ "./src/item.js")(list);
+
+  var getChildren = function getChildren(parent) {
+    var nodes = parent.childNodes,
+        items = [];
+
+    for (var i = 0, il = nodes.length; i < il; i++) {
+      // Only textnodes have a data attribute
+      if (nodes[i].data === undefined) {
+        items.push(nodes[i]);
+      }
+    }
+
+    return items;
+  };
+
+  var parse = function parse(itemElements, valueNames) {
+    for (var i = 0, il = itemElements.length; i < il; i++) {
+      list.items.push(new Item(valueNames, itemElements[i]));
+    }
+  };
+
+  var parseAsync = function parseAsync(itemElements, valueNames) {
+    var itemsToIndex = itemElements.splice(0, 50); // TODO: If < 100 items, what happens in IE etc?
+
+    parse(itemsToIndex, valueNames);
+
+    if (itemElements.length > 0) {
+      setTimeout(function () {
+        parseAsync(itemElements, valueNames);
+      }, 1);
+    } else {
+      list.update();
+      list.trigger('parseComplete');
+    }
+  };
+
+  list.handlers.parseComplete = list.handlers.parseComplete || [];
+  return function () {
+    var itemsToIndex = getChildren(list.list),
+        valueNames = list.valueNames;
+
+    if (list.indexAsync) {
+      parseAsync(itemsToIndex, valueNames);
+    } else {
+      parse(itemsToIndex, valueNames);
+    }
+  };
+};
+
+/***/ }),
+
+/***/ "./src/search.js":
+/*!***********************!*\
+  !*** ./src/search.js ***!
+  \***********************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (_list) {
+  var item, text, columns, searchString, customSearch;
+  var prepare = {
+    resetList: function resetList() {
+      _list.i = 1;
+
+      _list.templater.clear();
+
+      customSearch = undefined;
+    },
+    setOptions: function setOptions(args) {
+      if (args.length == 2 && args[1] instanceof Array) {
+        columns = args[1];
+      } else if (args.length == 2 && typeof args[1] == 'function') {
+        columns = undefined;
+        customSearch = args[1];
+      } else if (args.length == 3) {
+        columns = args[1];
+        customSearch = args[2];
+      } else {
+        columns = undefined;
+      }
+    },
+    setColumns: function setColumns() {
+      if (_list.items.length === 0) return;
+
+      if (columns === undefined) {
+        columns = _list.searchColumns === undefined ? prepare.toArray(_list.items[0].values()) : _list.searchColumns;
+      }
+    },
+    setSearchString: function setSearchString(s) {
+      s = _list.utils.toString(s).toLowerCase();
+      s = s.replace(/[-[\]{}()*+?.,\\^$|#]/g, '\\$&'); // Escape regular expression characters
+
+      searchString = s;
+    },
+    toArray: function toArray(values) {
+      var tmpColumn = [];
+
+      for (var name in values) {
+        tmpColumn.push(name);
+      }
+
+      return tmpColumn;
+    }
+  };
+  var search = {
+    list: function list() {
+      // Extract quoted phrases "word1 word2" from original searchString
+      // searchString is converted to lowercase by List.js
+      var words = [],
+          phrase,
+          ss = searchString;
+
+      while ((phrase = ss.match(/"([^"]+)"/)) !== null) {
+        words.push(phrase[1]);
+        ss = ss.substring(0, phrase.index) + ss.substring(phrase.index + phrase[0].length);
+      } // Get remaining space-separated words (if any)
+
+
+      ss = ss.trim();
+      if (ss.length) words = words.concat(ss.split(/\s+/));
+
+      for (var k = 0, kl = _list.items.length; k < kl; k++) {
+        var item = _list.items[k];
+        item.found = false;
+        if (!words.length) continue;
+
+        for (var i = 0, il = words.length; i < il; i++) {
+          var word_found = false;
+
+          for (var j = 0, jl = columns.length; j < jl; j++) {
+            var values = item.values(),
+                column = columns[j];
+
+            if (values.hasOwnProperty(column) && values[column] !== undefined && values[column] !== null) {
+              var text = typeof values[column] !== 'string' ? values[column].toString() : values[column];
+
+              if (text.toLowerCase().indexOf(words[i]) !== -1) {
+                // word found, so no need to check it against any other columns
+                word_found = true;
+                break;
+              }
+            }
+          } // this word not found? no need to check any other words, the item cannot match
+
+
+          if (!word_found) break;
+        }
+
+        item.found = word_found;
+      }
+    },
+    // Removed search.item() and search.values()
+    reset: function reset() {
+      _list.reset.search();
+
+      _list.searched = false;
+    }
+  };
+
+  var searchMethod = function searchMethod(str) {
+    _list.trigger('searchStart');
+
+    prepare.resetList();
+    prepare.setSearchString(str);
+    prepare.setOptions(arguments); // str, cols|searchFunction, searchFunction
+
+    prepare.setColumns();
+
+    if (searchString === '') {
+      search.reset();
+    } else {
+      _list.searched = true;
+
+      if (customSearch) {
+        customSearch(searchString, columns);
+      } else {
+        search.list();
+      }
+    }
+
+    _list.update();
+
+    _list.trigger('searchComplete');
+
+    return _list.visibleItems;
+  };
+
+  _list.handlers.searchStart = _list.handlers.searchStart || [];
+  _list.handlers.searchComplete = _list.handlers.searchComplete || [];
+
+  _list.utils.events.bind(_list.utils.getByClass(_list.listContainer, _list.searchClass), 'keyup', _list.utils.events.debounce(function (e) {
+    var target = e.target || e.srcElement,
+        // IE have srcElement
+    alreadyCleared = target.value === '' && !_list.searched;
+
+    if (!alreadyCleared) {
+      // If oninput already have resetted the list, do nothing
+      searchMethod(target.value);
+    }
+  }, _list.searchDelay)); // Used to detect click on HTML5 clear button
+
+
+  _list.utils.events.bind(_list.utils.getByClass(_list.listContainer, _list.searchClass), 'input', function (e) {
+    var target = e.target || e.srcElement;
+
+    if (target.value === '') {
+      searchMethod('');
+    }
+  });
+
+  return searchMethod;
+};
+
+/***/ }),
+
+/***/ "./src/sort.js":
+/*!*********************!*\
+  !*** ./src/sort.js ***!
+  \*********************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (list) {
+  var buttons = {
+    els: undefined,
+    clear: function clear() {
+      for (var i = 0, il = buttons.els.length; i < il; i++) {
+        list.utils.classes(buttons.els[i]).remove('asc');
+        list.utils.classes(buttons.els[i]).remove('desc');
+      }
+    },
+    getOrder: function getOrder(btn) {
+      var predefinedOrder = list.utils.getAttribute(btn, 'data-order');
+
+      if (predefinedOrder == 'asc' || predefinedOrder == 'desc') {
+        return predefinedOrder;
+      } else if (list.utils.classes(btn).has('desc')) {
+        return 'asc';
+      } else if (list.utils.classes(btn).has('asc')) {
+        return 'desc';
+      } else {
+        return 'asc';
+      }
+    },
+    getInSensitive: function getInSensitive(btn, options) {
+      var insensitive = list.utils.getAttribute(btn, 'data-insensitive');
+
+      if (insensitive === 'false') {
+        options.insensitive = false;
+      } else {
+        options.insensitive = true;
+      }
+    },
+    setOrder: function setOrder(options) {
+      for (var i = 0, il = buttons.els.length; i < il; i++) {
+        var btn = buttons.els[i];
+
+        if (list.utils.getAttribute(btn, 'data-sort') !== options.valueName) {
+          continue;
+        }
+
+        var predefinedOrder = list.utils.getAttribute(btn, 'data-order');
+
+        if (predefinedOrder == 'asc' || predefinedOrder == 'desc') {
+          if (predefinedOrder == options.order) {
+            list.utils.classes(btn).add(options.order);
+          }
+        } else {
+          list.utils.classes(btn).add(options.order);
+        }
+      }
+    }
+  };
+
+  var sort = function sort() {
+    list.trigger('sortStart');
+    var options = {};
+    var target = arguments[0].currentTarget || arguments[0].srcElement || undefined;
+
+    if (target) {
+      options.valueName = list.utils.getAttribute(target, 'data-sort');
+      buttons.getInSensitive(target, options);
+      options.order = buttons.getOrder(target);
+    } else {
+      options = arguments[1] || options;
+      options.valueName = arguments[0];
+      options.order = options.order || 'asc';
+      options.insensitive = typeof options.insensitive == 'undefined' ? true : options.insensitive;
+    }
+
+    buttons.clear();
+    buttons.setOrder(options); // caseInsensitive
+    // alphabet
+
+    var customSortFunction = options.sortFunction || list.sortFunction || null,
+        multi = options.order === 'desc' ? -1 : 1,
+        sortFunction;
+
+    if (customSortFunction) {
+      sortFunction = function sortFunction(itemA, itemB) {
+        return customSortFunction(itemA, itemB, options) * multi;
+      };
+    } else {
+      sortFunction = function sortFunction(itemA, itemB) {
+        var sort = list.utils.naturalSort;
+        sort.alphabet = list.alphabet || options.alphabet || undefined;
+
+        if (!sort.alphabet && options.insensitive) {
+          sort = list.utils.naturalSort.caseInsensitive;
+        }
+
+        return sort(itemA.values()[options.valueName], itemB.values()[options.valueName]) * multi;
+      };
+    }
+
+    list.items.sort(sortFunction);
+    list.update();
+    list.trigger('sortComplete');
+  }; // Add handlers
+
+
+  list.handlers.sortStart = list.handlers.sortStart || [];
+  list.handlers.sortComplete = list.handlers.sortComplete || [];
+  buttons.els = list.utils.getByClass(list.listContainer, list.sortClass);
+  list.utils.events.bind(buttons.els, 'click', sort);
+  list.on('searchStart', buttons.clear);
+  list.on('filterStart', buttons.clear);
+  return sort;
+};
+
+/***/ }),
+
+/***/ "./src/templater.js":
+/*!**************************!*\
+  !*** ./src/templater.js ***!
+  \**************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 216:0-14 */
+/***/ (function(module) {
+
+var Templater = function Templater(list) {
+  var createItem,
+      templater = this;
+
+  var init = function init() {
+    var itemSource;
+
+    if (typeof list.item === 'function') {
+      createItem = function createItem(values) {
+        var item = list.item(values);
+        return getItemSource(item);
+      };
+
+      return;
+    }
+
+    if (typeof list.item === 'string') {
+      if (list.item.indexOf('<') === -1) {
+        itemSource = document.getElementById(list.item);
+      } else {
+        itemSource = getItemSource(list.item);
+      }
+    } else {
+      /* If item source does not exists, use the first item in list as
+      source for new items */
+      itemSource = getFirstListItem();
+    }
+
+    if (!itemSource) {
+      throw new Error("The list needs to have at least one item on init otherwise you'll have to add a template.");
+    }
+
+    itemSource = createCleanTemplateItem(itemSource, list.valueNames);
+
+    createItem = function createItem() {
+      return itemSource.cloneNode(true);
+    };
+  };
+
+  var createCleanTemplateItem = function createCleanTemplateItem(templateNode, valueNames) {
+    var el = templateNode.cloneNode(true);
+    el.removeAttribute('id');
+
+    for (var i = 0, il = valueNames.length; i < il; i++) {
+      var elm = undefined,
+          valueName = valueNames[i];
+
+      if (valueName.data) {
+        for (var j = 0, jl = valueName.data.length; j < jl; j++) {
+          el.setAttribute('data-' + valueName.data[j], '');
+        }
+      } else if (valueName.attr && valueName.name) {
+        elm = list.utils.getByClass(el, valueName.name, true);
+
+        if (elm) {
+          elm.setAttribute(valueName.attr, '');
+        }
+      } else {
+        elm = list.utils.getByClass(el, valueName, true);
+
+        if (elm) {
+          elm.innerHTML = '';
+        }
+      }
+    }
+
+    return el;
+  };
+
+  var getFirstListItem = function getFirstListItem() {
+    var nodes = list.list.childNodes;
+
+    for (var i = 0, il = nodes.length; i < il; i++) {
+      // Only textnodes have a data attribute
+      if (nodes[i].data === undefined) {
+        return nodes[i].cloneNode(true);
+      }
+    }
+
+    return undefined;
+  };
+
+  var getItemSource = function getItemSource(itemHTML) {
+    if (typeof itemHTML !== 'string') return undefined;
+
+    if (/<tr[\s>]/g.exec(itemHTML)) {
+      var tbody = document.createElement('tbody');
+      tbody.innerHTML = itemHTML;
+      return tbody.firstElementChild;
+    } else if (itemHTML.indexOf('<') !== -1) {
+      var div = document.createElement('div');
+      div.innerHTML = itemHTML;
+      return div.firstElementChild;
+    }
+
+    return undefined;
+  };
+
+  var getValueName = function getValueName(name) {
+    for (var i = 0, il = list.valueNames.length; i < il; i++) {
+      var valueName = list.valueNames[i];
+
+      if (valueName.data) {
+        var data = valueName.data;
+
+        for (var j = 0, jl = data.length; j < jl; j++) {
+          if (data[j] === name) {
+            return {
+              data: name
+            };
+          }
+        }
+      } else if (valueName.attr && valueName.name && valueName.name == name) {
+        return valueName;
+      } else if (valueName === name) {
+        return name;
+      }
+    }
+  };
+
+  var setValue = function setValue(item, name, value) {
+    var elm = undefined,
+        valueName = getValueName(name);
+    if (!valueName) return;
+
+    if (valueName.data) {
+      item.elm.setAttribute('data-' + valueName.data, value);
+    } else if (valueName.attr && valueName.name) {
+      elm = list.utils.getByClass(item.elm, valueName.name, true);
+
+      if (elm) {
+        elm.setAttribute(valueName.attr, value);
+      }
+    } else {
+      elm = list.utils.getByClass(item.elm, valueName, true);
+
+      if (elm) {
+        elm.innerHTML = value;
+      }
+    }
+  };
+
+  this.get = function (item, valueNames) {
+    templater.create(item);
+    var values = {};
+
+    for (var i = 0, il = valueNames.length; i < il; i++) {
+      var elm = undefined,
+          valueName = valueNames[i];
+
+      if (valueName.data) {
+        for (var j = 0, jl = valueName.data.length; j < jl; j++) {
+          values[valueName.data[j]] = list.utils.getAttribute(item.elm, 'data-' + valueName.data[j]);
+        }
+      } else if (valueName.attr && valueName.name) {
+        elm = list.utils.getByClass(item.elm, valueName.name, true);
+        values[valueName.name] = elm ? list.utils.getAttribute(elm, valueName.attr) : '';
+      } else {
+        elm = list.utils.getByClass(item.elm, valueName, true);
+        values[valueName] = elm ? elm.innerHTML : '';
+      }
+    }
+
+    return values;
+  };
+
+  this.set = function (item, values) {
+    if (!templater.create(item)) {
+      for (var v in values) {
+        if (values.hasOwnProperty(v)) {
+          setValue(item, v, values[v]);
+        }
+      }
+    }
+  };
+
+  this.create = function (item) {
+    if (item.elm !== undefined) {
+      return false;
+    }
+
+    item.elm = createItem(item.values());
+    templater.set(item, item.values());
+    return true;
+  };
+
+  this.remove = function (item) {
+    if (item.elm.parentNode === list.list) {
+      list.list.removeChild(item.elm);
+    }
+  };
+
+  this.show = function (item) {
+    templater.create(item);
+    list.list.appendChild(item.elm);
+  };
+
+  this.hide = function (item) {
+    if (item.elm !== undefined && item.elm.parentNode === list.list) {
+      list.list.removeChild(item.elm);
+    }
+  };
+
+  this.clear = function () {
+    /* .innerHTML = ''; fucks up IE */
+    if (list.list.hasChildNodes()) {
+      while (list.list.childNodes.length >= 1) {
+        list.list.removeChild(list.list.firstChild);
+      }
+    }
+  };
+
+  init();
+};
+
+module.exports = function (list) {
+  return new Templater(list);
+};
+
+/***/ }),
+
+/***/ "./src/utils/classes.js":
+/*!******************************!*\
+  !*** ./src/utils/classes.js ***!
+  \******************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module, __webpack_require__ */
+/*! CommonJS bailout: module.exports is used directly at 24:0-14 */
+/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
+
+/**
+ * Module dependencies.
+ */
+var index = __webpack_require__(/*! ./index-of */ "./src/utils/index-of.js");
+/**
+ * Whitespace regexp.
+ */
+
+
+var re = /\s+/;
+/**
+ * toString reference.
+ */
+
+var toString = Object.prototype.toString;
+/**
+ * Wrap `el` in a `ClassList`.
+ *
+ * @param {Element} el
+ * @return {ClassList}
+ * @api public
+ */
+
+module.exports = function (el) {
+  return new ClassList(el);
+};
+/**
+ * Initialize a new ClassList for `el`.
+ *
+ * @param {Element} el
+ * @api private
+ */
+
+
+function ClassList(el) {
+  if (!el || !el.nodeType) {
+    throw new Error('A DOM element reference is required');
+  }
+
+  this.el = el;
+  this.list = el.classList;
+}
+/**
+ * Add class `name` if not already present.
+ *
+ * @param {String} name
+ * @return {ClassList}
+ * @api public
+ */
+
+
+ClassList.prototype.add = function (name) {
+  // classList
+  if (this.list) {
+    this.list.add(name);
+    return this;
+  } // fallback
+
+
+  var arr = this.array();
+  var i = index(arr, name);
+  if (!~i) arr.push(name);
+  this.el.className = arr.join(' ');
+  return this;
+};
+/**
+ * Remove class `name` when present, or
+ * pass a regular expression to remove
+ * any which match.
+ *
+ * @param {String|RegExp} name
+ * @return {ClassList}
+ * @api public
+ */
+
+
+ClassList.prototype.remove = function (name) {
+  // classList
+  if (this.list) {
+    this.list.remove(name);
+    return this;
+  } // fallback
+
+
+  var arr = this.array();
+  var i = index(arr, name);
+  if (~i) arr.splice(i, 1);
+  this.el.className = arr.join(' ');
+  return this;
+};
+/**
+ * Toggle class `name`, can force state via `force`.
+ *
+ * For browsers that support classList, but do not support `force` yet,
+ * the mistake will be detected and corrected.
+ *
+ * @param {String} name
+ * @param {Boolean} force
+ * @return {ClassList}
+ * @api public
+ */
+
+
+ClassList.prototype.toggle = function (name, force) {
+  // classList
+  if (this.list) {
+    if ('undefined' !== typeof force) {
+      if (force !== this.list.toggle(name, force)) {
+        this.list.toggle(name); // toggle again to correct
+      }
+    } else {
+      this.list.toggle(name);
+    }
+
+    return this;
+  } // fallback
+
+
+  if ('undefined' !== typeof force) {
+    if (!force) {
+      this.remove(name);
+    } else {
+      this.add(name);
+    }
+  } else {
+    if (this.has(name)) {
+      this.remove(name);
+    } else {
+      this.add(name);
+    }
+  }
+
+  return this;
+};
+/**
+ * Return an array of classes.
+ *
+ * @return {Array}
+ * @api public
+ */
+
+
+ClassList.prototype.array = function () {
+  var className = this.el.getAttribute('class') || '';
+  var str = className.replace(/^\s+|\s+$/g, '');
+  var arr = str.split(re);
+  if ('' === arr[0]) arr.shift();
+  return arr;
+};
+/**
+ * Check if class `name` is present.
+ *
+ * @param {String} name
+ * @return {ClassList}
+ * @api public
+ */
+
+
+ClassList.prototype.has = ClassList.prototype.contains = function (name) {
+  return this.list ? this.list.contains(name) : !!~index(this.array(), name);
+};
+
+/***/ }),
+
+/***/ "./src/utils/events.js":
+/*!*****************************!*\
+  !*** ./src/utils/events.js ***!
+  \*****************************/
+/*! default exports */
+/*! export bind [provided] [no usage info] [missing usage info prevents renaming] */
+/*! export debounce [provided] [no usage info] [missing usage info prevents renaming] */
+/*! export unbind [provided] [no usage info] [missing usage info prevents renaming] */
+/*! other exports [not provided] [no usage info] */
+/*! runtime requirements: __webpack_exports__, __webpack_require__ */
+/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
+
+var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
+    unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
+    prefix = bind !== 'addEventListener' ? 'on' : '',
+    toArray = __webpack_require__(/*! ./to-array */ "./src/utils/to-array.js");
+/**
+ * Bind `el` event `type` to `fn`.
+ *
+ * @param {Element} el, NodeList, HTMLCollection or Array
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @api public
+ */
+
+
+exports.bind = function (el, type, fn, capture) {
+  el = toArray(el);
+
+  for (var i = 0, il = el.length; i < il; i++) {
+    el[i][bind](prefix + type, fn, capture || false);
+  }
+};
+/**
+ * Unbind `el` event `type`'s callback `fn`.
+ *
+ * @param {Element} el, NodeList, HTMLCollection or Array
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @api public
+ */
+
+
+exports.unbind = function (el, type, fn, capture) {
+  el = toArray(el);
+
+  for (var i = 0, il = el.length; i < il; i++) {
+    el[i][unbind](prefix + type, fn, capture || false);
+  }
+};
+/**
+ * Returns a function, that, as long as it continues to be invoked, will not
+ * be triggered. The function will be called after it stops being called for
+ * `wait` milliseconds. If `immediate` is true, trigger the function on the
+ * leading edge, instead of the trailing.
+ *
+ * @param {Function} fn
+ * @param {Integer} wait
+ * @param {Boolean} immediate
+ * @api public
+ */
+
+
+exports.debounce = function (fn, wait, immediate) {
+  var timeout;
+  return wait ? function () {
+    var context = this,
+        args = arguments;
+
+    var later = function later() {
+      timeout = null;
+      if (!immediate) fn.apply(context, args);
+    };
+
+    var callNow = immediate && !timeout;
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+    if (callNow) fn.apply(context, args);
+  } : fn;
+};
+
+/***/ }),
+
+/***/ "./src/utils/extend.js":
+/*!*****************************!*\
+  !*** ./src/utils/extend.js ***!
+  \*****************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 4:0-14 */
+/***/ (function(module) {
+
+/*
+ * Source: https://github.com/segmentio/extend
+ */
+module.exports = function extend(object) {
+  // Takes an unlimited number of extenders.
+  var args = Array.prototype.slice.call(arguments, 1); // For each extender, copy their properties on our object.
+
+  for (var i = 0, source; source = args[i]; i++) {
+    if (!source) continue;
+
+    for (var property in source) {
+      object[property] = source[property];
+    }
+  }
+
+  return object;
+};
+
+/***/ }),
+
+/***/ "./src/utils/fuzzy.js":
+/*!****************************!*\
+  !*** ./src/utils/fuzzy.js ***!
+  \****************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (text, pattern, options) {
+  // Aproximately where in the text is the pattern expected to be found?
+  var Match_Location = options.location || 0; //Determines how close the match must be to the fuzzy location (specified above). An exact letter match which is 'distance' characters away from the fuzzy location would score as a complete mismatch. A distance of '0' requires the match be at the exact location specified, a threshold of '1000' would require a perfect match to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
+
+  var Match_Distance = options.distance || 100; // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match (of both letters and location), a threshold of '1.0' would match anything.
+
+  var Match_Threshold = options.threshold || 0.4;
+  if (pattern === text) return true; // Exact match
+
+  if (pattern.length > 32) return false; // This algorithm cannot be used
+  // Set starting location at beginning text and initialise the alphabet.
+
+  var loc = Match_Location,
+      s = function () {
+    var q = {},
+        i;
+
+    for (i = 0; i < pattern.length; i++) {
+      q[pattern.charAt(i)] = 0;
+    }
+
+    for (i = 0; i < pattern.length; i++) {
+      q[pattern.charAt(i)] |= 1 << pattern.length - i - 1;
+    }
+
+    return q;
+  }(); // Compute and return the score for a match with e errors and x location.
+  // Accesses loc and pattern through being a closure.
+
+
+  function match_bitapScore_(e, x) {
+    var accuracy = e / pattern.length,
+        proximity = Math.abs(loc - x);
+
+    if (!Match_Distance) {
+      // Dodge divide by zero error.
+      return proximity ? 1.0 : accuracy;
+    }
+
+    return accuracy + proximity / Match_Distance;
+  }
+
+  var score_threshold = Match_Threshold,
+      // Highest score beyond which we give up.
+  best_loc = text.indexOf(pattern, loc); // Is there a nearby exact match? (speedup)
+
+  if (best_loc != -1) {
+    score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); // What about in the other direction? (speedup)
+
+    best_loc = text.lastIndexOf(pattern, loc + pattern.length);
+
+    if (best_loc != -1) {
+      score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold);
+    }
+  } // Initialise the bit arrays.
+
+
+  var matchmask = 1 << pattern.length - 1;
+  best_loc = -1;
+  var bin_min, bin_mid;
+  var bin_max = pattern.length + text.length;
+  var last_rd;
+
+  for (var d = 0; d < pattern.length; d++) {
+    // Scan for the best match; each iteration allows for one more error.
+    // Run a binary search to determine how far from 'loc' we can stray at this
+    // error level.
+    bin_min = 0;
+    bin_mid = bin_max;
+
+    while (bin_min < bin_mid) {
+      if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) {
+        bin_min = bin_mid;
+      } else {
+        bin_max = bin_mid;
+      }
+
+      bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min);
+    } // Use the result from this iteration as the maximum for the next.
+
+
+    bin_max = bin_mid;
+    var start = Math.max(1, loc - bin_mid + 1);
+    var finish = Math.min(loc + bin_mid, text.length) + pattern.length;
+    var rd = Array(finish + 2);
+    rd[finish + 1] = (1 << d) - 1;
+
+    for (var j = finish; j >= start; j--) {
+      // The alphabet (s) is a sparse hash, so the following line generates
+      // warnings.
+      var charMatch = s[text.charAt(j - 1)];
+
+      if (d === 0) {
+        // First pass: exact match.
+        rd[j] = (rd[j + 1] << 1 | 1) & charMatch;
+      } else {
+        // Subsequent passes: fuzzy match.
+        rd[j] = (rd[j + 1] << 1 | 1) & charMatch | ((last_rd[j + 1] | last_rd[j]) << 1 | 1) | last_rd[j + 1];
+      }
+
+      if (rd[j] & matchmask) {
+        var score = match_bitapScore_(d, j - 1); // This match will almost certainly be better than any existing match.
+        // But check anyway.
+
+        if (score <= score_threshold) {
+          // Told you so.
+          score_threshold = score;
+          best_loc = j - 1;
+
+          if (best_loc > loc) {
+            // When passing loc, don't exceed our current distance from loc.
+            start = Math.max(1, 2 * loc - best_loc);
+          } else {
+            // Already passed loc, downhill from here on in.
+            break;
+          }
+        }
+      }
+    } // No hope for a (better) match at greater error levels.
+
+
+    if (match_bitapScore_(d + 1, loc) > score_threshold) {
+      break;
+    }
+
+    last_rd = rd;
+  }
+
+  return best_loc < 0 ? false : true;
+};
+
+/***/ }),
+
+/***/ "./src/utils/get-attribute.js":
+/*!************************************!*\
+  !*** ./src/utils/get-attribute.js ***!
+  \************************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 11:0-14 */
+/***/ (function(module) {
+
+/**
+ * A cross-browser implementation of getAttribute.
+ * Source found here: http://stackoverflow.com/a/3755343/361337 written by Vivin Paliath
+ *
+ * Return the value for `attr` at `element`.
+ *
+ * @param {Element} el
+ * @param {String} attr
+ * @api public
+ */
+module.exports = function (el, attr) {
+  var result = el.getAttribute && el.getAttribute(attr) || null;
+
+  if (!result) {
+    var attrs = el.attributes;
+    var length = attrs.length;
+
+    for (var i = 0; i < length; i++) {
+      if (attrs[i] !== undefined) {
+        if (attrs[i].nodeName === attr) {
+          result = attrs[i].nodeValue;
+        }
+      }
+    }
+  }
+
+  return result;
+};
+
+/***/ }),
+
+/***/ "./src/utils/get-by-class.js":
+/*!***********************************!*\
+  !*** ./src/utils/get-by-class.js ***!
+  \***********************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 53:0-14 */
+/***/ (function(module) {
+
+/**
+ * A cross-browser implementation of getElementsByClass.
+ * Heavily based on Dustin Diaz's function: http://dustindiaz.com/getelementsbyclass.
+ *
+ * Find all elements with class `className` inside `container`.
+ * Use `single = true` to increase performance in older browsers
+ * when only one element is needed.
+ *
+ * @param {String} className
+ * @param {Element} container
+ * @param {Boolean} single
+ * @api public
+ */
+var getElementsByClassName = function getElementsByClassName(container, className, single) {
+  if (single) {
+    return container.getElementsByClassName(className)[0];
+  } else {
+    return container.getElementsByClassName(className);
+  }
+};
+
+var querySelector = function querySelector(container, className, single) {
+  className = '.' + className;
+
+  if (single) {
+    return container.querySelector(className);
+  } else {
+    return container.querySelectorAll(className);
+  }
+};
+
+var polyfill = function polyfill(container, className, single) {
+  var classElements = [],
+      tag = '*';
+  var els = container.getElementsByTagName(tag);
+  var elsLen = els.length;
+  var pattern = new RegExp('(^|\\s)' + className + '(\\s|$)');
+
+  for (var i = 0, j = 0; i < elsLen; i++) {
+    if (pattern.test(els[i].className)) {
+      if (single) {
+        return els[i];
+      } else {
+        classElements[j] = els[i];
+        j++;
+      }
+    }
+  }
+
+  return classElements;
+};
+
+module.exports = function () {
+  return function (container, className, single, options) {
+    options = options || {};
+
+    if (options.test && options.getElementsByClassName || !options.test && document.getElementsByClassName) {
+      return getElementsByClassName(container, className, single);
+    } else if (options.test && options.querySelector || !options.test && document.querySelector) {
+      return querySelector(container, className, single);
+    } else {
+      return polyfill(container, className, single);
+    }
+  };
+}();
+
+/***/ }),
+
+/***/ "./src/utils/index-of.js":
+/*!*******************************!*\
+  !*** ./src/utils/index-of.js ***!
+  \*******************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 3:0-14 */
+/***/ (function(module) {
+
+var indexOf = [].indexOf;
+
+module.exports = function (arr, obj) {
+  if (indexOf) return arr.indexOf(obj);
+
+  for (var i = 0, il = arr.length; i < il; ++i) {
+    if (arr[i] === obj) return i;
+  }
+
+  return -1;
+};
+
+/***/ }),
+
+/***/ "./src/utils/to-array.js":
+/*!*******************************!*\
+  !*** ./src/utils/to-array.js ***!
+  \*******************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 11:0-14 */
+/***/ (function(module) {
+
+/**
+ * Source: https://github.com/timoxley/to-array
+ *
+ * Convert an array-like object into an `Array`.
+ * If `collection` is already an `Array`, then will return a clone of `collection`.
+ *
+ * @param {Array | Mixed} collection An `Array` or array-like object to convert e.g. `arguments` or `NodeList`
+ * @return {Array} Naive conversion of `collection` to a new `Array`.
+ * @api public
+ */
+module.exports = function toArray(collection) {
+  if (typeof collection === 'undefined') return [];
+  if (collection === null) return [null];
+  if (collection === window) return [window];
+  if (typeof collection === 'string') return [collection];
+  if (isArray(collection)) return collection;
+  if (typeof collection.length != 'number') return [collection];
+  if (typeof collection === 'function' && collection instanceof Function) return [collection];
+  var arr = [];
+
+  for (var i = 0, il = collection.length; i < il; i++) {
+    if (Object.prototype.hasOwnProperty.call(collection, i) || i in collection) {
+      arr.push(collection[i]);
+    }
+  }
+
+  if (!arr.length) return [];
+  return arr;
+};
+
+function isArray(arr) {
+  return Object.prototype.toString.call(arr) === '[object Array]';
+}
+
+/***/ }),
+
+/***/ "./src/utils/to-string.js":
+/*!********************************!*\
+  !*** ./src/utils/to-string.js ***!
+  \********************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
+/***/ (function(module) {
+
+module.exports = function (s) {
+  s = s === undefined ? '' : s;
+  s = s === null ? '' : s;
+  s = s.toString();
+  return s;
+};
+
+/***/ }),
+
+/***/ "./node_modules/string-natural-compare/natural-compare.js":
+/*!****************************************************************!*\
+  !*** ./node_modules/string-natural-compare/natural-compare.js ***!
+  \****************************************************************/
+/*! unknown exports (runtime-defined) */
+/*! runtime requirements: module */
+/*! CommonJS bailout: module.exports is used directly at 124:0-14 */
+/***/ (function(module) {
+
+"use strict";
+
+
+var alphabet;
+var alphabetIndexMap;
+var alphabetIndexMapLength = 0;
+
+function isNumberCode(code) {
+  return code >= 48 && code <= 57;
+}
+
+function naturalCompare(a, b) {
+  var lengthA = (a += '').length;
+  var lengthB = (b += '').length;
+  var aIndex = 0;
+  var bIndex = 0;
+
+  while (aIndex < lengthA && bIndex < lengthB) {
+    var charCodeA = a.charCodeAt(aIndex);
+    var charCodeB = b.charCodeAt(bIndex);
+
+    if (isNumberCode(charCodeA)) {
+      if (!isNumberCode(charCodeB)) {
+        return charCodeA - charCodeB;
+      }
+
+      var numStartA = aIndex;
+      var numStartB = bIndex;
+
+      while (charCodeA === 48 && ++numStartA < lengthA) {
+        charCodeA = a.charCodeAt(numStartA);
+      }
+      while (charCodeB === 48 && ++numStartB < lengthB) {
+        charCodeB = b.charCodeAt(numStartB);
+      }
+
+      var numEndA = numStartA;
+      var numEndB = numStartB;
+
+      while (numEndA < lengthA && isNumberCode(a.charCodeAt(numEndA))) {
+        ++numEndA;
+      }
+      while (numEndB < lengthB && isNumberCode(b.charCodeAt(numEndB))) {
+        ++numEndB;
+      }
+
+      var difference = numEndA - numStartA - numEndB + numStartB; // numA length - numB length
+      if (difference) {
+        return difference;
+      }
+
+      while (numStartA < numEndA) {
+        difference = a.charCodeAt(numStartA++) - b.charCodeAt(numStartB++);
+        if (difference) {
+          return difference;
+        }
+      }
+
+      aIndex = numEndA;
+      bIndex = numEndB;
+      continue;
+    }
+
+    if (charCodeA !== charCodeB) {
+      if (
+        charCodeA < alphabetIndexMapLength &&
+        charCodeB < alphabetIndexMapLength &&
+        alphabetIndexMap[charCodeA] !== -1 &&
+        alphabetIndexMap[charCodeB] !== -1
+      ) {
+        return alphabetIndexMap[charCodeA] - alphabetIndexMap[charCodeB];
+      }
+
+      return charCodeA - charCodeB;
+    }
+
+    ++aIndex;
+    ++bIndex;
+  }
+
+  if (aIndex >= lengthA && bIndex < lengthB && lengthA >= lengthB) {
+    return -1;
+  }
+
+  if (bIndex >= lengthB && aIndex < lengthA && lengthB >= lengthA) {
+    return 1;
+  }
+
+  return lengthA - lengthB;
+}
+
+naturalCompare.caseInsensitive = naturalCompare.i = function(a, b) {
+  return naturalCompare(('' + a).toLowerCase(), ('' + b).toLowerCase());
+};
+
+Object.defineProperties(naturalCompare, {
+  alphabet: {
+    get: function() {
+      return alphabet;
+    },
+
+    set: function(value) {
+      alphabet = value;
+      alphabetIndexMap = [];
+
+      var i = 0;
+
+      if (alphabet) {
+        for (; i < alphabet.length; i++) {
+          alphabetIndexMap[alphabet.charCodeAt(i)] = i;
+        }
+      }
+
+      alphabetIndexMapLength = alphabetIndexMap.length;
+
+      for (i = 0; i < alphabetIndexMapLength; i++) {
+        if (alphabetIndexMap[i] === undefined) {
+          alphabetIndexMap[i] = -1;
+        }
+      }
+    },
+  },
+});
+
+module.exports = naturalCompare;
+
+
+/***/ })
+
+/******/ 	});
+/************************************************************************/
+/******/ 	// The module cache
+/******/ 	var __webpack_module_cache__ = {};
+/******/ 	
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/ 		// Check if module is in cache
+/******/ 		if(__webpack_module_cache__[moduleId]) {
+/******/ 			return __webpack_module_cache__[moduleId].exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = __webpack_module_cache__[moduleId] = {
+/******/ 			// no module.id needed
+/******/ 			// no module.loaded needed
+/******/ 			exports: {}
+/******/ 		};
+/******/ 	
+/******/ 		// Execute the module function
+/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/ 	
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/ 	
+/************************************************************************/
+/******/ 	// module exports must be returned from runtime so entry inlining is disabled
+/******/ 	// startup
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__("./src/index.js");
+/******/ })()
+;
+//# sourceMappingURL=list.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/libs/list.js/dist/list.js.map


文件差异内容过多而无法显示
+ 0 - 0
public/libs/list.js/dist/list.min.js


+ 1 - 0
public/libs/list.js/dist/list.min.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}

文件差异内容过多而无法显示
+ 0 - 0
public/static/logo-sm-black.svg


文件差异内容过多而无法显示
+ 0 - 0
public/static/logo-sm-white.svg


+ 0 - 3
public/static/logo-small-white.svg

@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 68">
-  <path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#ffffff"/>
-</svg>

文件差异内容过多而无法显示
+ 0 - 0
public/static/logo-small.svg


文件差异内容过多而无法显示
+ 0 - 2
public/static/logo-white.svg


文件差异内容过多而无法显示
+ 0 - 0
public/static/logo.svg


+ 61 - 0
router/index.js

@@ -0,0 +1,61 @@
+import express from "express";
+export const router = express.Router();
+
+// Controllers
+import { Login, submitLogin, Logout } from "../controllers/login.js";
+import { Register, submitRegister } from "../controllers/register.js";
+import { Dashboard, searchDashboard } from "../controllers/dashboard.js";
+import { Apps, appSearch } from "../controllers/apps.js";
+import { Users } from "../controllers/users.js";
+import { Images } from "../controllers/images.js";
+import { Account } from "../controllers/account.js";
+import { Settings } from "../controllers/settings.js";
+import { Networks } from "../controllers/networks.js";
+import { Volumes } from "../controllers/volumes.js";
+import { Syslogs } from "../controllers/syslogs.js";
+import { Portal } from "../controllers/portal.js"
+
+/// Functions
+import { Install } from "../functions/install.js"
+import { Uninstall } from "../functions/uninstall.js"
+
+// Auth middleware
+const auth = (req, res, next) => {
+    if (req.session.role == "admin") {
+        next();
+    } else {
+        res.redirect("/login");
+    }
+};
+
+// Routes
+router.get("/login", Login);
+router.post("/login", submitLogin);
+router.get("/logout", Logout);
+
+router.get("/register", Register);
+router.post("/register", submitRegister);  
+
+
+router.get("/", auth, Dashboard);
+router.post("/", auth, searchDashboard);
+
+router.get("/images", auth, Images);
+router.get("/volumes", auth, Volumes);
+router.get("/networks", auth, Networks);
+router.get("/portal", Portal)
+
+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("/account", Account);
+router.get("/settings", auth, Settings);
+
+// Functions
+router.post("/install", auth, Install);
+router.post("/uninstall", auth, Uninstall);

+ 0 - 48
routes/index.js

@@ -1,48 +0,0 @@
-const express = require("express");
-const router = express.Router();
-
-const { Dashboard, AddSite, RemoveSite, RefreshSites, DisableSite, EnableSite } = require("../controllers/dashboard");
-const { Login, processLogin, Logout, Register, processRegister } = require("../controllers/auth");
-const { Apps, searchApps, Install, Uninstall } = require("../controllers/apps");
-
-const { Users } = require("../controllers/users");
-const { Account } = require("../controllers/account");
-const { Settings } = require("../controllers/settings");
-
-
-// Dashboard
-router.get("/", Dashboard);
-router.post("/addsite", AddSite)
-router.post("/removesite", RemoveSite)
-router.get("/refreshsites", RefreshSites)
-router.post("/disablesite", DisableSite)
-router.post("/enablesite", EnableSite)
-
-// Auth
-router.get("/login",Login);
-router.post("/login",processLogin);
-router.get("/register", Register);
-router.post("/register",processRegister);
-router.get("/logout",Logout);
-
-// Apps page
-router.get("/apps", Apps);
-router.get("/apps/:page", Apps);
-router.get("/apps/:template/:page", Apps);
-router.post("/apps", searchApps);
-
-
-
-// Settings page
-router.get("/settings", Settings);
-router.get("/account", Account);
-
-
-
-router.post("/install", Install)
-router.post("/uninstall", Uninstall)
-
-router.get("/users", Users);
-
-
-module.exports = router;

部分文件因为文件数量过多而无法显示