瀏覽代碼

independently updating container cards

lllllllillllllillll 1 年之前
父節點
當前提交
eb952c0a50
共有 8 個文件被更改,包括 315 次插入288 次删除
  1. 7 0
      CHANGELOG.md
  2. 1 1
      controllers/apps.js
  3. 293 206
      controllers/dashboard.js
  4. 4 7
      router/index.js
  5. 0 63
      server.js
  6. 2 3
      views/dashboard.html
  7. 1 1
      views/modals/uninstall.html
  8. 7 7
      views/partials/containerCard.html

+ 7 - 0
CHANGELOG.md

@@ -1,3 +1,10 @@
+## v0.50 (dev)
+* Converted template literals into html.
+* Converted modals into html/htmx.
+* Moved functions into dashboard controller.
+* Added modal placeholder with loading spinner.
+* Independently updating container cards.
+
 ## v0.40 (Feb 26th 2024) - HTMX rewrite
 ## v0.40 (Feb 26th 2024) - HTMX rewrite
 * Pages rewritten to use HTMX.
 * Pages rewritten to use HTMX.
 * Removed Socket.io.
 * Removed Socket.io.

+ 1 - 1
controllers/apps.js

@@ -27,7 +27,7 @@ export const Apps = (req, res) => {
 
 
     let apps_list = '';
     let apps_list = '';
     for (let i = list_start; i < list_end && i < templates.length; i++) {
     for (let i = list_start; i < list_end && i < templates.length; i++) {
-        let app_card = appCard(templates[i]);
+        let app_card = readFileSync('./views/partials/appCard.html', 'utf8');
 
 
         apps_list += app_card;
         apps_list += app_card;
     }
     }

+ 293 - 206
controllers/dashboard.js

@@ -1,13 +1,16 @@
 import { Readable } from 'stream';
 import { Readable } from 'stream';
 import { Permission, Container } from '../database/models.js';
 import { Permission, Container } from '../database/models.js';
-import { setEvent, cpu, ram, tx, rx, disk, docker } from '../server.js';
+import { docker } from '../server.js';
 import { dockerContainerStats } from 'systeminformation';
 import { dockerContainerStats } from 'systeminformation';
 import { readFileSync } from 'fs';
 import { readFileSync } from 'fs';
+import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
 
 
-let containerCard = readFileSync('./views/partials/containerCard.html', 'utf8');
+const permissionsModal = readFileSync('./views/modals/permissions.html', 'utf8');
+const uninstallModal = readFileSync('./views/modals/uninstall.html', 'utf8');
+const detailsModal = readFileSync('./views/modals/details.html', 'utf8');
 
 
-let [ hidden, cardList ] = [ '', '' ];
 
 
+// The actual page
 export const Dashboard = (req, res) => {
 export const Dashboard = (req, res) => {
     res.render("dashboard", {
     res.render("dashboard", {
         name: req.session.user,
         name: req.session.user,
@@ -16,85 +19,25 @@ export const Dashboard = (req, res) => {
     });
     });
 }
 }
 
 
-export const Logs = (req, res) => {
-    let name = req.header('hx-trigger-name');
-    function containerLogs (data) {
-        return new Promise((resolve, reject) => {
-            let logString = '';
-            var options = {
-                follow: false,
-                stdout: true,
-                stderr: false,
-                timestamps: false
-            };
-            var containerName = docker.getContainer(data);
-            containerName.logs(options, function (err, stream) {
-                if (err) { reject(err); return; }
-                const readableStream = Readable.from(stream);
-                readableStream.on('data', function (chunk) {
-                    logString += chunk.toString('utf8');
-                });
-                readableStream.on('end', function () {
-                    resolve(logString);
-                });
-            });
-        });
-    };
-    containerLogs(name).then((data) => {
-        res.send(`<pre>${data}</pre> `)
-    });
-}
+// Server metrics (CPU, RAM, TX, RX, DISK)
+let [ cpu, ram, tx, rx, disk, stats ] = [0, 0, 0, 0, 0, {}];
 
 
-export const Modal = async (req, res) => {
-    let name = req.header('hx-trigger-name');
-    let id = req.header('hx-trigger');
-
-    if (id == 'permissions') {
-        let containerPermissions = await Permission.findAll({ where: {containerName: name}});
-        let permissions = readFileSync('./views/modals/permissions.html', 'utf8');
-        res.send(permissions);
-        return;
-    }
-
-    if (id == 'remove') {
-        let containerPermissions = await Permission.findAll({ where: {containerName: name}});
-        let remove = readFileSync('./views/modals/remove.html', 'utf8');
-        res.send(remove);
-        return;
-    }
-
-    let containerId = docker.getContainer(name);
-    let containerInfo = await containerId.inspect();
-    let ports_list = [];
-    try {
-    for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
-        let ports = {
-            check: 'checked',
-            external: value[0].HostPort,
-            internal: key.split('/')[0],
-            protocol: key.split('/')[1]
-        }
-        ports_list.push(ports);
-    }
-    } catch {}
-    let external_port = ports_list[0]?.external || 0;
-    let internal_port = ports_list[0]?.internal || 0;
-
-    let container_info = {
-        name: containerInfo.Name.slice(1),
-        state: containerInfo.State.Status,
-        image: containerInfo.Config.Image,
-        external_port: external_port,
-        internal_port: internal_port,
-        ports: ports_list,
-        link: 'localhost',
-    }
+let serverMetrics = setInterval(async () => {
+    currentLoad().then(data => { 
+        cpu = Math.round(data.currentLoad); 
+    });
+    mem().then(data => { 
+        ram = Math.round((data.active / data.total) * 100); 
+    });
+    networkStats().then(data => { 
+        tx = data[0].tx_bytes / (1024 * 1024); 
+        rx = data[0].rx_bytes / (1024 * 1024); 
+    });
+    fsSize().then(data => { 
+        disk = data[0].use; 
+    });
+}, 1000);
 
 
-    let details = readFileSync('./views/modals/details.html', 'utf8');
-    details = details.replace(/AppName/g, containerInfo.Name.slice(1));
-    details = details.replace(/AppImage/g, containerInfo.Config.Image);
-    res.send(details);
-}
 
 
 export const Stats = async (req, res) => {
 export const Stats = async (req, res) => {
     let name = req.header('hx-trigger-name');
     let name = req.header('hx-trigger-name');
@@ -122,41 +65,100 @@ export const Stats = async (req, res) => {
 }
 }
 
 
 
 
-export const Hide = async (req, res) => {
-    let name = req.header('hx-trigger-name');
-    let exists = await Container.findOne({ where: {name: name}});
-    if (!exists) {
-        const newContainer = await Container.create({ name: name, visibility: false, });
-    } else {
-        exists.update({ visibility: false });
-    }
-    setEvent(true, 'docker');
-    res.send("ok");
+let [ hidden, cardList, eventType, containersArray, sentArray ] = [ '', '', '', [], [] ];
+
+// Container cards
+export const Containers = async (req, res) => {
+    console.log('Containers called');
+    res.send(cardList);
 }
 }
 
 
-export const Reset = async (req, res) => {
-    Container.update({ visibility: true }, { where: {} });
-    setEvent(true, 'docker');
-    res.send("ok");
+function addContainer(container, state) {
+    containersArray.push({ container, state });
 }
 }
 
 
+let cardUpdates = [];
+let newCards = '';
+export const SSE = (req, res) => {
+    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
+
+    let eventCheck = setInterval(async () => {
+        // builds array of containers and their states
+        containersArray = [];
+        await docker.listContainers({ all: true }).then(containers => {
+            containers.forEach(container => {
+                let name = container.Names[0].replace('/', '');
+                if (hidden.includes(name)) {
+                    // do nothing
+                } else {
+                    addContainer(name, container.State);
+                }
+
+            });
+        });
+
+        if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
+            cardList = '';
+            newCards = '';
+            cardUpdates = [];
+            containersArray.forEach(container => {
+                const { container: containerName, state } = container;
+                const existingContainer = sentArray.find(c => c.container === containerName);
+                if (!existingContainer) {
+                    console.log(`New container: ${containerName}`);
+                    addCard(containerName, 'newCards');
+                    res.write(`event: update\n`);
+                    res.write(`data: 'update cards'\n\n`);
+                } else if (existingContainer.state !== state) {
+                    console.log(`State of ${containerName} changed`);
+                    cardUpdates.push(containerName);
+                }
+                addCard(containerName, 'cardList');
+            });
+
+            sentArray.forEach(container => {
+                const { container: containerName } = container;
+                const existingContainer = containersArray.find(c => c.container === containerName);
+                if (!existingContainer) {
+                    console.log(`Removed container: ${containerName}`);
+                    cardUpdates.push(containerName);
+                }
+            });
+
+            for (let i = 0; i < cardUpdates.length; i++) {
+                res.write(`event: ${cardUpdates[i]}\n`);
+                res.write(`data: 'update cards'\n\n`);
+            }
+
+            sentArray = containersArray.slice();
+        }
+
+    }, 1000);
 
 
-let stats = {};
+
+    req.on('close', () => {
+        clearInterval(eventCheck);
+    });
+};
+
+
+
+
+
+
+
+
+// Container charts
 export const Chart = async (req, res) => {
 export const Chart = async (req, res) => {
     let name = req.header('hx-trigger-name');
     let name = req.header('hx-trigger-name');
-    // create an empty array if it doesn't exist
     if (!stats[name]) {
     if (!stats[name]) {
         stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) };
         stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) };
     }
     }
-    // get the stats
     const info = await dockerContainerStats(name);
     const info = await dockerContainerStats(name);
-    // update the arrays
     stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
     stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
     stats[name].ramArray.push(Math.round(info[0].memPercent));
     stats[name].ramArray.push(Math.round(info[0].memPercent));
-    // slice them down to the last 15 values
     stats[name].cpuArray = stats[name].cpuArray.slice(-15);
     stats[name].cpuArray = stats[name].cpuArray.slice(-15);
     stats[name].ramArray = stats[name].ramArray.slice(-15);
     stats[name].ramArray = stats[name].ramArray.slice(-15);
-    // replace the chart with the new data
     let chart = `
     let chart = `
         <script>
         <script>
             ${name}chart.updateSeries([{
             ${name}chart.updateSeries([{
@@ -168,117 +170,118 @@ export const Chart = async (req, res) => {
     res.send(chart);
     res.send(chart);
 }
 }
 
 
-// Get hidden containers
-async function getHidden() {
-    hidden = await Container.findAll({ where: {visibility:false}});
-    hidden = hidden.map((container) => container.name);
+
+export const Installs = async (req, res) => {
+    let name = req.header('hx-trigger-name');
+    let all_containers = '';
+    res.send('ok');
 }
 }
 
 
-// Create list of docker containers cards
-async function containerCards() {
-    let list = '';
-    const allContainers = await docker.listContainers({ all: true });
-    for (const container of allContainers) {
-        if (!hidden.includes(container.Names[0].slice(1))) {
-
-            let imageVersion = container.Image.split('/');
-            let service = imageVersion[imageVersion.length - 1].split(':')[0];
-            let containerId = docker.getContainer(container.Id);
-            let containerInfo = await containerId.inspect();
-            let ports_list = [];
-            try {
-            for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
-                let ports = {
-                    check: 'checked',
-                    external: value[0].HostPort,
-                    internal: key.split('/')[0],
-                    protocol: key.split('/')[1]
-                }
-                ports_list.push(ports);
-            }
-            } catch {}
-
-            let external_port = ports_list[0]?.external || 0;
-            let internal_port = ports_list[0]?.internal || 0;
-
-            let container_info = {
-                name: container.Names[0].slice(1),
-                service: service,
-                id: container.Id,
-                state: container.State,
-                image: container.Image,
-                external_port: external_port,
-                internal_port: internal_port,
-                ports: ports_list,
-                link: 'localhost',
-            }
-            
-            let name = container.Names[0].slice(1);
-            let state = container.State;
-            
-            let wrapped = name;
-            let disable = "";
-            let chartName = name.replace(/-/g, '');
-          
-            // shorten long names
-            if (name.length > 13) { wrapped = name.slice(0, 10) + '...'; }
-            // disable buttons for dweebui
-            if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
-          
-            // if ( external_port == undefined ) { external_port = 0; }
-            // if ( internal_port == undefined ) { internal_port = 0; }
-          
-            let state_indicator = 'green';
-            if (state == 'exited') {
-                state = 'stopped';
-                state_indicator = 'red';
-            } else if (state == 'paused') {
-                state_indicator = 'orange';
+async function addCard(container, list) {
+    let card  = readFileSync('./views/partials/containerCard.html', 'utf8');
+    let containerId = docker.getContainer(container);
+    containerId.inspect().then(data => {
+
+        let state = data.State.Status;
+        let wrapped = container;
+        let disable = "";
+        let chartName = container.replace(/-/g, '');
+        
+        // shorten long names
+        if (container.length > 13) { wrapped = container.slice(0, 10) + '...'; }
+        // disable buttons for dweebui
+        if (container.startsWith('dweebui')) { disable = 'disabled=""'; }
+        
+        // if ( external_port == undefined ) { external_port = 0; }
+        // if ( internal_port == undefined ) { internal_port = 0; }
+        
+        let state_indicator = 'green';
+        if (state == 'exited') {
+            state = 'stopped';
+            state_indicator = 'red';
+        } else if (state == 'paused') {
+            state_indicator = 'orange';
+        }
+        
+        let noChart = 'hx-swap="none"';
+        if (state == 'running') { noChart = ''; }
+
+        let imageVersion = data.Config.Image.split('/');
+        let service = imageVersion[imageVersion.length - 1].split(':')[0];
+        let ports_list = [];
+        try {
+        for (const [key, value] of Object.entries(data.HostConfig.PortBindings)) {
+            let ports = {
+                check: 'checked',
+                external: value[0].HostPort,
+                internal: key.split('/')[0],
+                protocol: key.split('/')[1]
             }
             }
-          
-            let noChart = 'hx-swap="none"';
-            if (state == 'running') { noChart = ''; }
-          
-            let ports_data = [];
-            // if (ports) {
-            //   ports_data = ports;
-            // } else {
-            //   for (let i = 0; i < 12; i++) {
-          
-            //     let port_check = "checked";
-            //     let external = i;
-            //     let internal = i;
-            //     let protocol = "tcp";
-          
-            //     ports_data.push({
-            //       check: port_check,
-            //       external: external,
-            //       internal: internal,
-            //       protocol: protocol
-            //     });
-            //   }
-            // }
-          
-            let card = containerCard;
-            card = card.replace(/AppName/g, name);
-            card = card.replace(/AppShortName/g, wrapped);
-            card = card.replace(/ChartName/g, chartName);
-            card = card.replace(/AppIcon/g, service);
-            card = card.replace(/AppState/g, state);
-            card = card.replace(/StateColor/g, state_indicator);
-            list += card;
+            ports_list.push(ports);
         }
         }
-    }
-    cardList = list;
+        } catch {}
+        let external_port = ports_list[0]?.external || 0;
+        let internal_port = ports_list[0]?.internal || 0;
+        card = card.replace(/AppName/g, container);
+        card = card.replace(/AppShortName/g, wrapped);
+        card = card.replace(/AppIcon/g, service);
+        card = card.replace(/AppState/g, state);
+        card = card.replace(/StateColor/g, state_indicator);
+        card = card.replace(/ChartName/g, chartName);
+        card = card.replace(/AppNameState/g, `${container}State`);
+        if (list == 'newCards') {
+            newCards += card;
+        } else {
+            cardList += card;
+        }
+    });   
 }
 }
 
 
+export const updateCards = async (req, res) => {
+    console.log('updateCards called');
+    res.send(newCards);
+}
 
 
 
 
 
 
-export const Containers = async (req, res) => {
-    await getHidden();
-    await containerCards();
-    res.send(cardList);
+
+
+
+export const Card = (req, res) => {
+    let name = req.header('hx-trigger-name');
+    console.log(`Updated card for ${name}`);
+
+    let newCard = readFileSync('./views/partials/containerCard.html', 'utf8');
+    let containerId = docker.getContainer(name);
+    containerId.inspect().then(data => {
+        let imageVersion = data.Config.Image.split('/');
+        let service = imageVersion[imageVersion.length - 1].split(':')[0];
+        let ports_list = [];
+        try {
+        for (const [key, value] of Object.entries(data.HostConfig.PortBindings)) {
+            let ports = {
+                check: 'checked',
+                external: value[0].HostPort,
+                internal: key.split('/')[0],
+                protocol: key.split('/')[1]
+            }
+            ports_list.push(ports);
+        }
+        } catch {}
+        let external_port = ports_list[0]?.external || 0;
+        let internal_port = ports_list[0]?.internal || 0;
+        
+
+        newCard = newCard.replace(/AppName/g, name);
+        newCard = newCard.replace(/AppShortName/g, name);
+        newCard = newCard.replace(/AppIcon/g, service);
+        newCard = newCard.replace(/AppState/g, data.State.Status);
+        newCard = newCard.replace(/AppImage/g, data.Config.Image.split('/'));
+
+        if (hidden.includes(name)) { newCard = ''; }
+
+        res.send(newCard);
+    });
 }
 }
 
 
 
 
@@ -334,11 +337,95 @@ export const Restart = (req, res) => {
     res.send(status('restarting'));
     res.send(status('restarting'));
 }
 }
 
 
-export const Installs = async (req, res) => {
+export const Logs = (req, res) => {
+    let name = req.header('hx-trigger-name');
+    function containerLogs (data) {
+        return new Promise((resolve, reject) => {
+            let logString = '';
+            var options = { follow: false, stdout: true, stderr: false, timestamps: false };
+            var containerName = docker.getContainer(data);
+            containerName.logs(options, function (err, stream) {
+                if (err) { reject(err); return; }
+                const readableStream = Readable.from(stream);
+                readableStream.on('data', function (chunk) {
+                    logString += chunk.toString('utf8');
+                });
+                readableStream.on('end', function () {
+                    resolve(logString);
+                });
+            });
+        });
+    };
+    containerLogs(name).then((data) => {
+        res.send(`<pre>${data}</pre> `)
+    });
+}
 
 
+export const Hide = async (req, res) => {
     let name = req.header('hx-trigger-name');
     let name = req.header('hx-trigger-name');
-    let all_containers = '';
+    let exists = await Container.findOne({ where: {name: name}});
+    if (!exists) {
+        const newContainer = await Container.create({ name: name, visibility: false, });
+    } else {
+        exists.update({ visibility: false });
+    }
+    hidden = await Container.findAll({ where: {visibility:false}});
+    hidden = hidden.map((container) => container.name);
+    res.send("ok");
+}
 
 
-    res.send('ok');
-   
+export const Reset = async (req, res) => {
+    await Container.update({ visibility: true }, { where: {} });
+    hidden = await Container.findAll({ where: {visibility:false}});
+    hidden = hidden.map((container) => container.name);
+    res.send("ok");
+}
+
+export const Modal = async (req, res) => {
+    let name = req.header('hx-trigger-name');
+    let id = req.header('hx-trigger');
+
+    if (id == 'permissions') {
+        // let containerPermissions = await Permission.findAll({ where: {containerName: name}});
+        res.send(permissionsModal);
+        return;
+    }
+
+    if (id == 'uninstall') {
+        // let containerPermissions = await Permission.findAll({ where: {containerName: name}});
+        res.send(uninstallModal);
+        return;
+    }
+
+    let containerId = docker.getContainer(name);
+    let containerInfo = await containerId.inspect();
+    let ports_list = [];
+    try {
+    for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
+        let ports = {
+            check: 'checked',
+            external: value[0].HostPort,
+            internal: key.split('/')[0],
+            protocol: key.split('/')[1]
+        }
+        ports_list.push(ports);
+    }
+    } catch {}
+    let external_port = ports_list[0]?.external || 0;
+    let internal_port = ports_list[0]?.internal || 0;
+
+    let container_info = {
+        name: containerInfo.Name.slice(1),
+        state: containerInfo.State.Status,
+        image: containerInfo.Config.Image,
+        external_port: external_port,
+        internal_port: internal_port,
+        ports: ports_list,
+        link: 'localhost',
+    }
+
+    let details = detailsModal;
+    details = details.replace(/AppName/g, containerInfo.Name.slice(1));
+    details = details.replace(/AppImage/g, containerInfo.Config.Image);
+    res.send(details);
 }
 }

+ 4 - 7
router/index.js

@@ -4,7 +4,7 @@ export const router = express.Router();
 // Controllers
 // Controllers
 import { Login, submitLogin, Logout } from "../controllers/login.js";
 import { Login, submitLogin, Logout } from "../controllers/login.js";
 import { Register, submitRegister } from "../controllers/register.js";
 import { Register, submitRegister } from "../controllers/register.js";
-import { Dashboard, Start, Stop, Pause, Restart, Logs, Modal, Stats, Hide, Reset, Chart, Containers, Installs } from "../controllers/dashboard.js";
+import { Dashboard, Start, Stop, Pause, Restart, Logs, Modal, Stats, Hide, Reset, Chart, Containers, Installs, SSE, Card, updateCards } from "../controllers/dashboard.js";
 import { Apps, appSearch } from "../controllers/apps.js";
 import { Apps, appSearch } from "../controllers/apps.js";
 import { Users } from "../controllers/users.js";
 import { Users } from "../controllers/users.js";
 import { Images, removeImage } from "../controllers/images.js";
 import { Images, removeImage } from "../controllers/images.js";
@@ -40,6 +40,9 @@ router.post("/hide", auth, Hide);
 router.post("/reset", auth, Reset);
 router.post("/reset", auth, Reset);
 router.get("/chart", auth, Chart);
 router.get("/chart", auth, Chart);
 router.get("/installs", auth, Installs);
 router.get("/installs", auth, Installs);
+router.get("/sse_event", auth, SSE);
+router.get("/card", auth, Card);
+router.get("/new_cards", auth, updateCards);
 
 
 router.get("/images", auth, Images);
 router.get("/images", auth, Images);
 router.post("/removeImage", auth, removeImage);
 router.post("/removeImage", auth, removeImage);
@@ -79,9 +82,3 @@ import { Uninstall } from "../functions/uninstall.js"
 
 
 // router.post("/install", auth, Install);
 // router.post("/install", auth, Install);
 router.post("/uninstall", auth, Uninstall);
 router.post("/uninstall", auth, Uninstall);
-
-
-router.get("/card", (req, res) => {
-    console.log('card route hit');
-    res.send('ok');
-});

+ 0 - 63
server.js

@@ -5,16 +5,11 @@ import ejs from 'ejs';
 import Docker from 'dockerode';
 import Docker from 'dockerode';
 import { router } from './router/index.js';
 import { router } from './router/index.js';
 import { sequelize } from './database/models.js';
 import { sequelize } from './database/models.js';
-import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
-
 export var docker = new Docker();
 export var docker = new Docker();
-export { setEvent, cpu, ram, tx, rx, disk }
 
 
 const app = express();
 const app = express();
 const MemoryStore = memorystore(session);
 const MemoryStore = memorystore(session);
 const port = process.env.PORT || 8000;
 const port = process.env.PORT || 8000;
-let [ cpu, ram, tx, rx, disk ] = [0, 0, 0, 0, 0];
-let [ event, eventType ] = [false, 'docker'];
 
 
 // Session middleware
 // Session middleware
 const sessionMiddleware = session({
 const sessionMiddleware = session({
@@ -54,61 +49,3 @@ app.listen(port, async () => {
     });
     });
 });
 });
 
 
-function setEvent(value, type) {
-    event = value;
-    eventType = type;
-}
-
-// Server metrics
-let serverMetrics = async () => {
-    currentLoad().then(data => { 
-        cpu = Math.round(data.currentLoad); 
-    });
-    mem().then(data => { 
-        ram = Math.round((data.active / data.total) * 100); 
-    });
-    networkStats().then(data => { 
-        tx = data[0].tx_bytes / (1024 * 1024); 
-        rx = data[0].rx_bytes / (1024 * 1024); 
-    });
-    fsSize().then(data => { 
-        disk = data[0].use; 
-    });
-}
-setInterval(serverMetrics, 1000);
-
-
-let containersArray = [];
-let sentArray = [];
-
-function addContainer(container, state) {
-    containersArray.push({ container, state });
-}
-
-router.get('/sse_event', (req, res) => {
-    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
-    let eventCheck = setInterval(async () => {
-        containersArray = [];
-        await docker.listContainers({ all: true }).then(containers => {
-            containers.forEach(container => {
-                let name = container.Names[0].replace('/', '');
-                addContainer(name, container.State);
-            });
-        });
-
-        if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray)) || event) {
-            for (let i = 0; i < containersArray.length; i++) {
-                const { container, state } = containersArray[i];
-                if (!sentArray[i] || JSON.stringify({ container, state }) !== JSON.stringify(sentArray[i])) {
-                    console.log(`Event: ${container}`);
-                    res.write(`event: ${container}\n`);
-                    res.write(`data: ${state}\n\n`);
-                }
-            }
-            sentArray = containersArray.slice();
-        }
-    }, 1000);
-    req.on('close', () => {
-        clearInterval(eventCheck);
-    });
-});

+ 2 - 3
views/dashboard.html

@@ -139,17 +139,16 @@
               </div>
               </div>
             </div>
             </div>
 
 
-
             
             
             <!-- HTMX -->
             <!-- HTMX -->
             <div class="col-12">
             <div class="col-12">
-              <div class="row row-cards" id="containerCards" data-hx-get="/containers" data-hx-trigger="load" data-hx-swap="beforeend">
+              <div class="row row-cards" data-hx-get="/containers" data-hx-trigger="load" data-hx-swap="innerHTML" id="containers">
               </div>
               </div>
             </div>
             </div>
 
 
             <!-- HTMX -->
             <!-- HTMX -->
             <div class="col-12">
             <div class="col-12">
-              <div class="row row-cards" data-hx-get="/installs" data-hx-trigger="" data-hx-swap="beforeend" hx-target="#containerCards">
+              <div class="row row-cards" data-hx-get="/new_cards" data-hx-trigger="sse:update" data-hx-swap="beforeend" hx-target="#containers">
               </div>
               </div>
             </div>
             </div>
             
             

+ 1 - 1
views/modals/remove.html → views/modals/uninstall.html

@@ -5,7 +5,7 @@
             <div class="modal-status bg-danger"></div>
             <div class="modal-status bg-danger"></div>
             <div class="modal-body text-center py-3">
             <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>
             <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 AppName?</h3>
+            <h3>Uninstall AppName?</h3>
             <form action="/uninstall" id="AppName_uninstall" method="POST">
             <form action="/uninstall" id="AppName_uninstall" method="POST">
             <input type="text" class="form-control" name="service_name" value="AppName" hidden/>
             <input type="text" class="form-control" name="service_name" value="AppName" hidden/>
             <div class="mb-3"> </div>
             <div class="mb-3"> </div>

+ 7 - 7
views/partials/containerCard.html

@@ -1,4 +1,4 @@
-  <div class="col-sm-6 col-lg-3 pt-1" hx-get="/card" hx-trigger="sse:AppName" hx-swap="outerHTML">
+  <div class="col-sm-6 col-lg-3 pt-1" hx-get="/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName">
     <div class="card">
     <div class="card">
       <div class="card-body">
       <div class="card-body">
         <div class="card-stamp card-stamp-sm">
         <div class="card-stamp card-stamp-sm">
@@ -9,16 +9,16 @@
           <div class="ms-auto lh-1">
           <div class="ms-auto lh-1">
             <div class="card-actions btn-actions">
             <div class="card-actions btn-actions">
               <div class="card-actions btn-actions">
               <div class="card-actions btn-actions">
-                <button class="btn-action" title="Start" data-hx-post="/start" data-hx-trigger="click" data-hx-target="#AppNamestate" name="AppName" id="${state}" ${disable}>
+                <button class="btn-action" title="Start" data-hx-post="/start" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
                   <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>
-                <button class="btn-action" title="Stop" data-hx-post="/stop" data-hx-trigger="click" data-hx-target="#AppNamestate" name="AppName" id="${state}" ${disable}>
+                <button class="btn-action" title="Stop" data-hx-post="/stop" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
                   <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>
-                <button class="btn-action" title="Pause" data-hx-post="/pause" data-hx-trigger="click" data-hx-target="#AppNamestate" name="AppName" id="${state}" ${disable}>
+                <button class="btn-action" title="Pause" data-hx-post="/pause" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
                   <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>
-                <button class="btn-action" title="Restart" data-hx-post="/restart" data-hx-trigger="click" data-hx-target="#AppNamestate" name="AppName" id="${state}" ${disable}>
+                <button class="btn-action" title="Restart" data-hx-post="/restart" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>                          
                   <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>
                 </button>
                 <div class="dropdown">
                 <div class="dropdown">
@@ -30,7 +30,7 @@
                     <button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-get="/logs" hx-swap="innerHTML" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
                     <button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-get="/logs" hx-swap="innerHTML" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
                     <button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
                     <button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
                     <button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
                     <button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
-                    <button class="dropdown-item text-danger" name="AppName" id="remove" hx-trigger="click" data-hx-get="/modal" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Remove</button>
+                    <button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="click" data-hx-get="/modal" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
                   </div>
                   </div>
                 </div>
                 </div>
                 <div class="dropdown">
                 <div class="dropdown">
@@ -54,7 +54,7 @@
             </a>
             </a>
           </div>
           </div>
           <div class="ms-auto">
           <div class="ms-auto">
-            <label id="AppState">
+            <label id="AppNameState">
               <span class="text-StateColor align-items-center lh-1">
               <span class="text-StateColor align-items-center lh-1">
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
                   AppState
                   AppState