Parcourir la source

Refactored to stop spamming Docker API

lllllllillllllillll il y a 1 an
Parent
commit
33e45a8bbf

+ 2 - 1
README.md

@@ -16,7 +16,7 @@
 
 * [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
 * [x] Multi-user support with permissions system.
-* [ ] Display and control docker containers from multiple remote hosts (in development).
+* [ ] Multiple hosts (in development).
 * [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
 * [x] Windows, Linux, and MacOS compatable.
 * [x] Light/Dark Mode.
@@ -24,6 +24,7 @@
 * [x] Manage your Docker networks, images, and volumes.
 * [x] Easy to install app templates.
 * [x] Docker Compose Support.
+* [ ] Available updates without image pull (in development).
 * [ ] Update containers (planned).
 * [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
 * [ ] Preset variables (planned).

+ 224 - 307
controllers/dashboard.js

@@ -1,61 +1,19 @@
 import { Readable } from 'stream';
 import { readFileSync } from 'fs';
 import { currentLoad, mem, networkStats, fsSize, dockerContainerStats } from 'systeminformation';
-import { Op } from 'sequelize';
-
-import Docker from 'dockerode';
-
 import { Permission, User, ServerSettings } from '../database/models.js';
-import { docker, docker2, docker3, docker4, host_list, host2_list, host3_list, host4_list } from '../server.js';
+import { Op } from 'sequelize';
+import { docker, containerList, containerInspect } from '../utils/docker.js';
 
 let [ hidden, alert, newCards, stats ] = [ '', '', '', {} ];
 let logString = '';
 
-// async function hostInfo(host) {
-//     let info = await ServerSettings.findOne({ where: {key: host}});
-//     try {
-//         if (info.value != 'off' && info.value != '') {
-//             let values = info.value.split(',');
-//             return { tag: values[0], ip: values[1], port: values[2] };
-//         }
-//     } catch {
-//         // console.log(`${host}: No Value Set`);
-//     }
-// }
-
 
 export const Dashboard = async (req, res) => {
-
-    console.log(`Viewing Host: ${req.params.host}`);
-
+    const { host } = req.params;
     let { link1, link2, link3, link4, link5, link6, link7, link8, link9 } = ['', '', '', '', '', '', '', '', ''];
+    // if (host) { console.log(`Viewing Host: ${host}`); } else { console.log('Viewing Host: 1'); }
 
-    // let host2 = await hostInfo('host2');
-    // let host3 = await hostInfo('host3');
-    // let host4 = await hostInfo('host4');
-
-    if (docker2 || docker3 || docker4) {
-        link1 = `<a href="/1/dashboard" class="btn text-green" name="host">
-                    Host 1
-                </a>`;
-        link5 = `<a href="/0/dashboard" class="btn text-green" name="hosts">
-                    All
-                </a>`;
-    }
-    if (docker2) { link2 = `<a href="/2/dashboard" class="btn text-green" name="host2">
-                    Host2
-                </a>`;
-    }
-    if (docker3) { link3 = `<a href="/3/dashboard" class="btn text-green" name="host3">
-                    Host3
-                </a>`;
-    }
-    if (docker4) { link4 = `<a href="/4/dashboard" class="btn text-green" name="host4">
-                    Host4
-                </a>`;
-    }
-
-    
     res.render("dashboard", {
         username: req.session.username,
         avatar: req.session.username.charAt(0).toUpperCase(),
@@ -74,153 +32,86 @@ export const Dashboard = async (req, res) => {
 }
 
 
-export const ContainerAction = async (req, res) => {
-    // Assign values
-    let container_name = req.header('hx-trigger-name');
-    let container_id = req.header('hx-trigger');
-    let action = req.params.action;
-
-
-    if (container_id == 'reset') { 
-        console.log('Resetting view'); 
-        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
-        res.send('ok'); 
-        return;
-    }
-
-    // Inspect the container
-    let container = docker.getContainer(container_id);
-    let containerInfo = await container.inspect();
-    let state = containerInfo.State.Status;
-
-    console.log(`Container: ${container_name} ID: ${container_id} State: ${state} Action: ${action}`);
-
-    function status (state) {
-        return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
-                        ${state}
-                </span>`);
-    }
-
-    if ((action == 'start') && (state == 'exited')) {
-        await container.start();
-        res.send(status('starting'));
-    } else if ((action == 'start') && (state == 'paused')) {
-        await container.unpause();
-        res.send(status('starting'));
-    } else if ((action == 'stop') && (state != 'exited')) {
-        await container.stop();
-        res.send(status('stopping'));
-    } else if ((action == 'pause') && (state == 'paused')) {
-        await container.unpause();
-        res.send(status('starting'));
-    }   else if ((action == 'pause') && (state == 'running')) {
-        await container.pause();
-        res.send(status('pausing'));
-    } else if (action == 'restart') {
-        await container.restart();
-        res.send(status('restarting'));
-    } else if (action == 'hide') {
-        let exists = await Permission.findOne({ where: { containerID: container_id, userID: req.session.userID }});
-        if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: container_id, username: req.session.username, userID: req.session.userID, hide: true }); }
-        else { exists.update({ hide: true }); }
-        // Array of hidden containers
-        hidden = await Permission.findAll({ where: { userID: req.session.userID, hide: true}}, { attributes: ['containerID'] });
-        // Map the container IDs
-        hidden = hidden.map((container) => container.containerID);
-        console.log(hidden);
-        res.send("ok");
-    }
-    
-}
-
 export const DashboardAction = async (req, res) => {
     let name = req.header('hx-trigger-name');
     let value = req.header('hx-trigger');
-    let action = req.params.action;
+    const { action } = req.params;
     let modal = '';
-
-    // console.log(`Action: ${action} Name: ${name} Value: ${value}`);
+    console.log(`Action: ${action} Name: ${name} Value: ${value}`);
 
     if (req.body.search) { 
         console.log(req.body.search);
         res.send('search');
         return;
     }
+
+    if (action == 'get_containers') {
+        res.send(newCards);
+        newCards = '';
+        return;
+    }
     
+    // Creates the permissions modal 
+    if (action == 'permissions') {
+        // To capitalize the title
+        let title = name.charAt(0).toUpperCase() + name.slice(1);
+        // Empty the permissions list
+        let permissions_list = '';
+        // Get the container ID
+        let container = docker.getContainer(name);
+        let containerInfo = await container.inspect();
+        let container_id = containerInfo.Id;
+        // Get the body of the permissions modal
+        let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
+        // Replace the title and container name in the modal
+        permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
+        permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
+        permissions_modal = permissions_modal.replace(/ContainerID/g, container_id);
+        // Get a list of all users
+        let users = await User.findAll({ attributes: ['username', 'userID']});
+        // Loop through each user to check what permissions they have
+        for (let i = 0; i < users.length; i++) {
+            // Get the user_permissions form
+            let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
+            // Check if the user has any permissions for the container
+            let exists = await Permission.findOne({ where: { containerID: container_id, userID: users[i].userID }});
+            // Create an entry if one doesn't exist
+            if (!exists) { const newPermission = await Permission.create({ containerName: name, containerID: container_id, username: users[i].username, userID: users[i].userID }); }
+            // Get the permissions for the user
+            let permissions = await Permission.findOne({ where: { containerID: container_id, userID: users[i].userID }});
+            // Fill in the form values
+            if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
+            if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
+            if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
+            if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
+            if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
+            if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
+            if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
+            if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
+            if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
+            user_permissions = user_permissions.replace(/EntryNumber/g, i);
+            user_permissions = user_permissions.replace(/EntryNumber/g, i);
+            user_permissions = user_permissions.replace(/EntryNumber/g, i);
+            user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+            user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+            user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+            user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+            user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+            user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+            user_permissions = user_permissions.replace(/PermissionsUserID/g, users[i].userID);
+            user_permissions = user_permissions.replace(/PermissionsID/g, container_id);
+            // Add the user entry to the permissions list
+            permissions_list += user_permissions;
+        }
+        // Insert the user list into the permissions modal
+        permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
+        // Send the permissions modal
+        res.send(permissions_modal);
+        return;
+    }
+
+
     switch (action) {
-        // case 'checkhost':
-        //     let link = '';
-        //     let host_info = await hostInfo(name);
-        //     try {
-        //         var docker2 = new Docker({ protocol: 'http', host: host_info.ip, port: host_info.port });
-        //         let containers = await docker2.listContainers({ all: true });
-        //         link = `<button class="btn text-green" name="host2">
-        //                     ${host_info.tag}
-        //                 </button>`;
-        //     } catch {
-        //         console.log(`Error connecting to ${name}`);
-        //         link = `<button class="btn text-red" name="host2">
-        //                     ${host_info.tag}
-        //                 </button>`;
-        //     }
-        //     res.send(link);
-        // return;
-        case 'permissions': // (Action = Selecting 'Permissions' from the dropdown) Creates the permissions modal 
-            // To capitalize the title
-            let title = name.charAt(0).toUpperCase() + name.slice(1);
-            // Empty the permissions list
-            let permissions_list = '';
-            // Get the container ID
-            let container = docker.getContainer(name);
-            let containerInfo = await container.inspect();
-            let container_id = containerInfo.Id;
-            // Get the body of the permissions modal
-            let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
-            // Replace the title and container name in the modal
-            permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
-            permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
-            permissions_modal = permissions_modal.replace(/ContainerID/g, container_id);
-            // Get a list of all users
-            let users = await User.findAll({ attributes: ['username', 'userID']});
-            // Loop through each user to check what permissions they have
-            for (let i = 0; i < users.length; i++) {
-                // Get the user_permissions form
-                let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
-                // Check if the user has any permissions for the container
-                let exists = await Permission.findOne({ where: { containerID: container_id, userID: users[i].userID }});
-                // Create an entry if one doesn't exist
-                if (!exists) { const newPermission = await Permission.create({ containerName: name, containerID: container_id, username: users[i].username, userID: users[i].userID }); }
-                // Get the permissions for the user
-                let permissions = await Permission.findOne({ where: { containerID: container_id, userID: users[i].userID }});
-                // Fill in the form values
-                if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
-                if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
-                if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
-                if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
-                if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
-                if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
-                if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
-                if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
-                if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
-                user_permissions = user_permissions.replace(/EntryNumber/g, i);
-                user_permissions = user_permissions.replace(/EntryNumber/g, i);
-                user_permissions = user_permissions.replace(/EntryNumber/g, i);
-                user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
-                user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
-                user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
-                user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
-                user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
-                user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
-                user_permissions = user_permissions.replace(/PermissionsUserID/g, users[i].userID);
-                user_permissions = user_permissions.replace(/PermissionsID/g, container_id);
-                // Add the user entry to the permissions list
-                permissions_list += user_permissions;
-            }
-            // Insert the user list into the permissions modal
-            permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
-            // Send the permissions modal
-            res.send(permissions_modal);
-            return;
         case 'uninstall':
             modal = readFileSync('./views/modals/uninstall.html', 'utf8');
             modal = modal.replace(/AppName/g, name);
@@ -229,41 +120,30 @@ export const DashboardAction = async (req, res) => {
         case 'details':
             modal = readFileSync('./views/modals/details.html', 'utf8');
             let details = await containerInfo(name);
-
             modal = modal.replace(/AppName/g, details.name);
             modal = modal.replace(/AppImage/g, details.image);
-
             for (let i = 0; i <= 6; i++) {
                 modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
                 modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
                 modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
                 modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
             }
-
             for (let i = 0; i <= 6; i++) {
                 modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
                 modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
                 modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
             }
-
-
             for (let i = 0; i <= 19; i++) {
                 modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
                 modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
             }
-
             // console.log(details.env);
             for (let i = 0; i <= 19; i++) {
                 modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
                 modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
             }
-
             res.send(modal);
             return;
-        case 'updates':
-            res.send(newCards);
-            newCards = '';
-            return;
         case 'card':
             // Check which cards the user has permissions for
             await userCards(req.session);
@@ -300,65 +180,20 @@ export const DashboardAction = async (req, res) => {
     }
 }
 
-async function containerInfo (containerID) {
-    // get the container info
-    let container = docker.getContainer(containerID);
-    let info = await container.inspect();
-    let image = info.Config.Image;
-    let container_id = info.Id;
-    // grab the service name from the end of the image name
-    let service = image.split('/').pop();
-    // remove the tag from the service name if it exists
-    try { service = service.split(':')[0]; } catch {}
-    let ports_list = [];
-    let external = 0;
-    let internal = 0;
-    
-    try {
-        for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
-            let ports = {
-                check: 'checked',
-                external: value[0].HostPort,
-                internal: key.split('/')[0],
-                protocol: key.split('/')[1]
-            }
-            ports_list.push(ports);
-        }
-    } catch {}
-    try {
-        external = ports_list[0].external;
-        internal = ports_list[0].internal;
-    } catch {}
-
-    let details = {
-        name: info.Name.slice(1),
-        image: image,
-        service: service,
-        containerID: container_id,
-        state: info.State.Status,
-        external_port: external,
-        internal_port: internal,
-        ports: ports_list,
-        volumes: info.Mounts,
-        env: info.Config.Env,
-        labels: info.Config.Labels,
-        link: 'localhost',
-    }
-    return details;
-}
 
-async function createCard (details) {
-    let shortname = details.name.slice(0, 10) + '...';
-    let trigger = 'data-hx-trigger="load, every 3s"';
-    let state = details.state;
-    let card  = readFileSync('./views/partials/containerFull.html', 'utf8');
 
-    let app_icon = (details.labels['com.docker.compose.service']);
+async function createCard (details) {
+    let { containerName, containerID, containerState } = details;
+    // console.log(`Creating card for ${containerName} ID: ${containerID} Service: ${containerService} State: ${containerState}`);
+    let container = await containerInspect(containerID);
 
+    let shortname = containerName.slice(0, 10) + '...';
+    let trigger = 'data-hx-trigger="load, every 3s"';
+    let state = containerState;
+    let state_color = '';
+    let app_icon = container.service;
     let links = await ServerSettings.findOne({ where: {key: 'links'}});
     if (!links) { links = { value: 'localhost' }; }
-
-    let state_color = '';
     switch (state) {
         case 'running':
             state_color = 'green';
@@ -377,110 +212,135 @@ async function createCard (details) {
             trigger = 'data-hx-trigger="load"';
             break;
     }
-    // if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
-
-    card = card.replace(/AppName/g, details.name);
-    card = card.replace(/AppID/g, details.containerID);
+    let card  = readFileSync('./views/partials/containerFull.html', 'utf8');
+    card = card.replace(/AppName/g, containerName);
+    card = card.replace(/AppID/g, containerID);
     card = card.replace(/AppShortName/g, shortname);
     card = card.replace(/AppIcon/g, app_icon);
     card = card.replace(/AppState/g, state);
     card = card.replace(/StateColor/g, state_color);
     card = card.replace(/AppLink/g, links.value);
-    card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
-    card = card.replace(/AppNameState/g, `${details.name}State`);
+    card = card.replace(/ChartName/g, containerName.replace(/-/g, ''));
+    card = card.replace(/AppNameState/g, `${containerName}State`);
     card = card.replace(/data-trigger=""/, trigger);
-
-    // Show nothing if there are no ports exposed
-    if ((details.external_port == 0) && (details.internal_port == 0)) {
-        card = card.replace(/AppPorts/g, '');
-    } else {
-        card = card.replace(/AppPorts/g, `${details.external_port}:${details.internal_port}`);
-    }
+    
+    // Show nothing if there are no ports exposed.
+    if ((container.external_port == 0) && (container.internal_port == 0)) { card = card.replace(/AppPorts/g, ''); } 
+    else { card = card.replace(/AppPorts/g, `${container.external_port}:${container.internal_port}`); }
+    
     return card;
 }
 
+
 // Creates a list of containers that the user should be able to see.
-async function userCards (session) {
+async function updateLists(session, host) {
     // Create an empty container list.
     session.container_list = [];
     // Check what containers the user has hidden.
     let hidden = await Permission.findAll({ where: { userID: session.userID, hide: true }, attributes: ['containerID'], raw: true });
     // Check which containers the user has permissions for.
     let visable = await Permission.findAll({ where: { userID: session.userID, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] }, attributes: ['containerID'], raw: true});
-    // Get a list of all the containers.
-    let containers = await docker.listContainers({ all: true });
+    let containers = await containerList(host);
     // Loop through the list of containers.
     for (let i = 0; i < containers.length; i++) {
         // Get the container ID.
-        let containerID = containers[i].Id;
+        let containerID = containers[i].containerID;
         // Skip the container if it's ID is in the hidden list.
         if (hidden.includes(containerID)) { console.log('skipped hidden container'); continue; }
         // If the user is admin and they don't have it hidden, add it to the list.
-        if (session.role == 'admin') { session.container_list.push({ container: containerID, state: containers[i].State }); }
+        if (session.role == 'admin') { session.container_list.push({ containerName: containers[i].containerName, containerID: containerID, containerState: containers[i].containerState }); }
         // Add the container if it's ID is in the visable list.
-        else if (visable.includes(containerID)){ session.container_list.push({ container: containerID, state: containers[i].State }); }
+        else if (visable.includes(containerID)){ session.container_list.push({ containerName: containers[i].containerName, containerID: containerID, containerState: containers[i].containerState }); }
     }
     // Create the lists if they don't exist.
     if (!session.sent_list) { session.sent_list = []; }
     if (!session.update_list) { session.update_list = []; }
     if (!session.new_cards) { session.new_cards = []; }
-}
 
-async function updateDashboard (session) {
-    // Get the list of containers and the list of containers that have been sent.
-    let container_list = session.container_list;
-    let sent_list = session.sent_list;
     session.new_cards = [];
     session.update_list = [];
     // Loop through the containers list
-    container_list.forEach(info => {
+    session.container_list.forEach(info => {
         // Get the containerID and state
-        let { container, state } = info;
-        // Check if the container is in the sent list
-        let sent = sent_list.find(c => c.container === container);
+        let { containerName, containerID, containerState } = info;
+        // Check if the containerID is in the sent list
+        let sent = session.sent_list.find(c => c.containerID === containerID);
         // If it's not in the sent list, add it to the new cards list.
-        if (!sent) { session.new_cards.push(container);}
+        if (!sent) { session.new_cards.push({ containerName, containerID, containerState }); }
         // If it is in the sent list, check if the state has changed.
-        else if (sent.state !== state) { session.update_list.push(container); }
+        else if (sent.containerState !== containerState) { session.update_list.push({ containerName, containerID, containerState }); }
     });
     // Loop through the sent list to see if any containers have been removed
-    sent_list.forEach(info => {
-        let { container } = info;
-        let exists = container_list.find(c => c.container === container);
-        if (!exists) { session.update_list.push(container); }
+    session.sent_list.forEach(info => {
+        let { containerName, containerID, containerState } = info;
+        let exists = session.container_list.find(c => c.containerID === containerID);
+        if (!exists) { session.update_list.push({ containerName, containerID, containerState }); }
     });
 }
 
+
+
 // HTMX server-side events
 export const SSE = async (req, res) => {
+    let running = false;
+    let skipped_events = 0;
+    
     // Set the headers
     res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
-    // Check for container changes every 500ms
-    let eventCheck = setInterval(async () => {
-        await userCards(req.session);
-        // check if the cards displayed are the same as what's in the session
-        if ((JSON.stringify(req.session.container_list) === JSON.stringify(req.session.sent_list))) { return; }
-        await updateDashboard(req.session); 
-
-        for (let i = 0; i < req.session.new_cards.length; i++) {
-            let details = await containerInfo(req.session.new_cards[i]);
-            let card = await createCard(details);
-            newCards += card;
-            req.session.alert = '';
-        }
-        for (let i = 0; i < req.session.update_list.length; i++) {
-            res.write(`event: ${req.session.update_list[i]}\n`);
-            res.write(`data: 'update cards'\n\n`);
-        }
-        res.write(`event: update\n`);
-        res.write(`data: 'update cards'\n\n`);
-        req.session.sent_list = req.session.container_list.slice();
-    }, 500);
-    req.on('close', () => {
-        clearInterval(eventCheck);
-    });
+
+    // Updates req.session.container_list with the containers the user can see.
+    await newEvent();
+
+    // Event trigger with debounce
+
+    async function newEvent() {
+        if (!running) {
+            console.log('[Docker event]');
+            running = true;
+            // Update the container lists
+            await updateLists(req.session, 'host');
+            // Check if the container_list is the same as the sent_list
+            if ((JSON.stringify(req.session.container_list) != JSON.stringify(req.session.sent_list))) { 
+                console.log('Updating dashboard');
+                // New card
+                for (let i = 0; i < req.session.new_cards.length; i++) {
+                    console.log('SSE event: new card');
+                    let card = await createCard(req.session.new_cards[i]);
+                    newCards += card;
+                    req.session.alert = '';
+                }
+                // Card needs to be updated
+                for (let i = 0; i < req.session.update_list.length; i++) {
+                    console.log(`SSE event: update card ${req.session.update_list[i].containerName} ${req.session.update_list[i].containerID}`);
+                    res.write(`event: ${req.session.update_list[i].containerID}\n`);
+                    res.write(`data: 'update cards'\n\n`);
+                }
+
+                res.write(`event: update\n`);
+                res.write(`data: 'update cards'\n\n`);
+                req.session.sent_list = req.session.container_list.slice();
+            }
+
+            // res.write(`event: update\n`);
+            // res.write(`data: 'update cards'\n\n`);
+            
+            setTimeout(() => {
+                running = false;
+                // console.log(`Skipped ${skipped_events} events`);
+                skipped_events = 0;
+            }, 300);
+        } else { skipped_events++; }
+    }
+
+    // Listens for docker events
+    docker.getEvents({}, function (err, data) {
+        data.on('data', function () {
+            newEvent();
+        });
+    }); 
 };
 
+
 // Server metrics (CPU, RAM, TX, RX, DISK)
 export const Stats = async (req, res) => {
     let name = req.header('hx-trigger-name');
@@ -524,12 +384,11 @@ export async function addAlert (session, type, message) {
                     </div>`;
 }
 
+// Update container permissions.
 export const UpdatePermissions = async (req, res) => {
     let { userID, container, containerID, reset_permissions } = req.body;
     let id = req.header('hx-trigger');
-
-    console.log(`User: ${userID} Container: ${container} ContainerID: ${containerID} Reset: ${reset_permissions}`);
-
+    // console.log(`User: ${userID} Container: ${container} ContainerID: ${containerID} Reset: ${reset_permissions}`);
     if (reset_permissions) {
         await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerID: containerID} });
         return;
@@ -572,4 +431,62 @@ export const Chart = async (req, res) => {
             ${name}chart.updateSeries([{data: [${stats[name].cpuArray}]}, {data: [${stats[name].ramArray}]}])
         </script>`
     res.send(chart);
+}
+
+// Container actions (start, stop, pause, restart, hide)
+export const ContainerAction = async (req, res) => {
+    // Assign values
+    let container_name = req.header('hx-trigger-name');
+    let container_id = req.header('hx-trigger');
+    let action = req.params.action;
+
+    console.log(`Container: ${container_name} ID: ${container_id} Action: ${action}`);
+
+    // Reset the view
+    if (container_id == 'reset') { 
+        console.log('Resetting view'); 
+        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
+        res.send('ok'); 
+        return;
+    }
+    // Inspect the container
+    let container = docker.getContainer(container_id);
+    let containerInfo = await container.inspect();
+    let state = containerInfo.State.Status;
+    // console.log(`Container: ${container_name} ID: ${container_id} State: ${state} Action: ${action}`);
+    // Displays container state (starting, stopping, restarting, pausing)
+    function status (state) {
+        return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
+                        ${state}
+                </span>`);
+    }
+    // Perform the action
+    if ((action == 'start') && (state == 'exited')) {
+        await container.start();
+        res.send(status('starting'));
+    } else if ((action == 'start') && (state == 'paused')) {
+        await container.unpause();
+        res.send(status('starting'));
+    } else if ((action == 'stop') && (state != 'exited')) {
+        await container.stop();
+        res.send(status('stopping'));
+    } else if ((action == 'pause') && (state == 'paused')) {
+        await container.unpause();
+        res.send(status('starting'));
+    }   else if ((action == 'pause') && (state == 'running')) {
+        await container.pause();
+        res.send(status('pausing'));
+    } else if (action == 'restart') {
+        await container.restart();
+        res.send(status('restarting'));
+    } else if (action == 'hide') {
+        let exists = await Permission.findOne({ where: { containerID: container_id, userID: req.session.userID }});
+        if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: container_id, username: req.session.username, userID: req.session.userID, hide: true }); }
+        else { exists.update({ hide: true }); }
+        // Array of hidden containers
+        hidden = await Permission.findAll({ where: { userID: req.session.userID, hide: true}}, { attributes: ['containerID'] });
+        // Map the container IDs
+        hidden = hidden.map((container) => container.containerID);
+        res.send("ok");
+    }
 }

+ 1 - 1
controllers/images.js

@@ -1,4 +1,4 @@
-import { docker } from '../server.js';
+import { docker } from '../utils/docker.js';
 import { addAlert } from './dashboard.js';
 
 export const Images = async function(req, res) {

+ 1 - 1
controllers/networks.js

@@ -1,4 +1,4 @@
-import { docker } from '../server.js';
+import { docker } from '../utils/docker.js';
 
 
 export const Networks = async function(req, res) {

+ 1 - 1
controllers/volumes.js

@@ -1,4 +1,4 @@
-import { docker } from '../server.js';
+import { docker } from '../utils/docker.js';
 
 export const Volumes = async function(req, res) {
     let container_volumes = [];

+ 1 - 115
server.js

@@ -5,12 +5,6 @@ import ejs from 'ejs';
 import { router } from './router/index.js';
 import { sequelize, ServerSettings } from './database/models.js';
 
-import Docker from 'dockerode';
-
-export var [ docker, docker2, docker3, docker4 ] = [ null, null, null, null ];
-export let [ host_list, host2_list, host3_list, host4_list ] = [ [], [], [], [] ];
-var docker = new Docker();
-
 // Session middleware
 const secure = process.env.HTTPS || false;
 const MemoryStore = memorystore(session);
@@ -48,114 +42,6 @@ app.listen(PORT, async () => {
             () => { console.log('Synced Models: ✅') }); }
             catch { console.log('Synced Models: ❌'); } }
         await init().then(() => { 
-            newEvent('host');
             console.log(`Listening on http://localhost:${PORT}`);
     });
-});
-
-// Configure Docker hosts.
-for (let i = 2; i < 5; i++) {
-    try {
-        if (i == 2) { 
-            let config = await ServerSettings.findOne({ where: { key: 'host2' }});
-            if (config.value != 'off' && config.value != '') {
-                let values = config.value.split(',');
-                let port = values[2];
-                let address = values[1];
-                docker2 = new Docker({protocol:'http', host: address, port: port});
-                console.log(`Configured ${host} on ${address}:${port}`);
-                let test = await docker2.listContainers({ all: true });
-                console.log(`${host}: ${test.length} containers`);
-            }
-        } else if (i == 3) {
-            let config = await ServerSettings.findOne({ where: { key: 'host3' }});
-            if (config.value != 'off' && config.value != '') {
-                let values = config.value.split(',');
-                let port = values[2];
-                let address = values[1];
-                docker3 = new Docker({protocol:'http', host: address, port: port});
-                console.log(`Configured ${host} on ${address}:${port}`);
-                let test = await docker3.listContainers({ all: true });
-                console.log(`${host}: ${test.length} containers`);
-            }
-        } else if (i == 4) {
-            let config = await ServerSettings.findOne({ where: { key: 'host4' }});
-            if (config.value != 'off' && config.value != '') {
-                let values = config.value.split(',');
-                let port = values[2];
-                let address = values[1];
-                docker4 = new Docker({protocol:'http', host: address, port: port});
-                console.log(`Configured ${host} on ${address}:${port}`);
-                let test = await docker4.listContainers({ all: true });
-                console.log(`${host}: ${test.length} containers`);
-            }
-        }
-
-    } catch {
-       console.log(`host${i}: Not configured.`);
-    }
-}
-
-
-
-async function updateList(host) {
-    if (host == 'host') {
-        let containers = await docker.listContainers({ all: true });
-        host_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
-    } else if (host == 'host2') {
-        let containers = await docker2.listContainers({ all: true });
-        host2_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
-    } else if (host == 'host3') {
-        let containers = await docker3.listContainers({ all: true });
-        host3_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
-    } else if (host == 'host4') {
-        let containers = await docker4.listContainers({ all: true });
-        host4_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
-    }
-}
-
-let event = false;
-let skipped_events = 0;
-// Debounce.
-function newEvent(host) {
-    if (!event) {
-        event = true;
-        console.log(`New event from ${host}`);
-        updateList(host);
-        setTimeout(() => {
-            event = false;
-            console.log(`Skipped ${skipped_events} events`);
-            skipped_events = 0;
-        }, 300);
-    } else { skipped_events++; }
-}
-
-docker.getEvents({}, function (err, data) {
-    data.on('data', function () {
-        newEvent('host');
-    });
-}); 
-
-// if (docker2) {
-//     docker2.getEvents({}, function (err, data) {
-//         data.on('data', function () {
-//             newEvent('host2');
-//         });
-//     });
-// }
-
-// if (docker3) {
-//     docker3.getEvents({}, function (err, data) {
-//         data.on('data', function () {
-//             newEvent('host3');
-//         });
-//     });
-// }
-
-// if (docker4) {
-//     docker4.getEvents({}, function (err, data) {
-//         data.on('data', function () {
-//             newEvent('host4');
-//         });
-//     });
-// }
+});

+ 78 - 0
utils/docker.js

@@ -0,0 +1,78 @@
+import Docker from 'dockerode';
+
+export var docker = new Docker();
+
+export var docker2;
+export var docker3;
+export var docker4;
+
+
+export async function containerList(host) {
+    let containers = [];
+    if (host == 'host') {
+        containers = await docker.listContainers({ all: true });
+    } else if (host == 'host2') {
+        containers = await docker2.listContainers({ all: true });
+    } else if (host == 'host3') {
+        containers = await docker3.listContainers({ all: true });
+    } else if (host == 'host4') {
+        containers = await docker4.listContainers({ all: true });
+    }
+    containers = containers.map(container => ({ 
+        containerName: container.Names[0].split('/').pop(),
+        containerID: container.Id,
+        containerState: container.State,
+    }));
+    return containers;
+}
+
+
+export async function containerInspect (containerID) {
+    // get the container info
+    let container = docker.getContainer(containerID);
+    let info = await container.inspect();
+    let image = info.Config.Image;
+    let container_id = info.Id;
+    
+    let service = info.Config.Labels['com.docker.compose.service'];
+
+    let ports_list = [];
+    let external = 0;
+    let internal = 0;
+    
+    try {
+        for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
+            let ports = {
+                check: 'checked',
+                external: value[0].HostPort,
+                internal: key.split('/')[0],
+                protocol: key.split('/')[1]
+            }
+            ports_list.push(ports);
+        }
+    } catch {}
+    try {
+        external = ports_list[0].external;
+        internal = ports_list[0].internal;
+    } catch {}
+
+    let details = {
+        name: info.Name.slice(1),
+        image: image,
+        service: service,
+        containerID: container_id,
+        state: info.State.Status,
+        external_port: external,
+        internal_port: internal,
+        ports: ports_list,
+        volumes: info.Mounts,
+        env: info.Config.Env,
+        labels: info.Config.Labels,
+        link: 'localhost',
+    }
+    return details;
+}
+
+
+
+

+ 1 - 1
utils/install.js

@@ -2,7 +2,7 @@ import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "
 import { execSync } from "child_process";
 import { Syslog } from "../database/models.js";
 import { addAlert } from "../controllers/dashboard.js";
-import { docker } from "../server.js";
+import { docker } from "../utils/docker.js";
 import DockerodeCompose from "dockerode-compose";
 import yaml from 'js-yaml';
 

+ 1 - 1
utils/uninstall.js

@@ -1,4 +1,4 @@
-import { docker } from "../server.js";
+import { docker } from "../utils/docker.js";
 import { Syslog } from "../database/models.js";
 
 

+ 1 - 6
views/dashboard.html

@@ -138,11 +138,6 @@
               </div>
             </div>
 
-
-            
-
-
-
             <div class="col-12">
               <div class="row row-cards" id="containers">
               </div>
@@ -150,7 +145,7 @@
 
             <!-- HTMX -->
             <div class="col-12">
-              <div class="row row-cards" data-hx-post="/dashboard/updates" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
+              <div class="row row-cards" data-hx-post="/dashboard/get_containers" name="sse:update" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
               </div>
             </div>
             

+ 2 - 2
views/partials/containerFull.html

@@ -1,8 +1,8 @@
-  <div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName" id="AppID">
+  <div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppID" hx-swap="outerHTML" name="AppName" id="AppID">
     <div class="card">
       <div class="card-body">
         <div class="card-stamp card-stamp-sm">
-          <img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
+          <img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/Dashboard-Icons/main/png/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
         </div>
         <div class="d-flex align-items-center">
           <div class="subheader text-yellow">AppPorts</div>