Переглянути джерело

mostly working permission system.

lllllllillllllillll 1 рік тому
батько
коміт
a73a89b250

+ 17 - 5
controllers/dashboard.js

@@ -9,13 +9,15 @@ let hidden = '';
 
 // The actual page
 export const Dashboard = (req, res) => {
-
-    if (!req.session.user) { res.redirect("/logout"); return; }
     
+    let name = req.session.user;
+    let role = req.session.role;
+    let avatar = name.charAt(0).toUpperCase();
+
     res.render("dashboard", {
-        name: req.session.user,
-        role: req.session.role,
-        avatar: req.session.avatar,
+        name: name,
+        avatar: avatar,
+        role: role
     });
 }
 
@@ -393,6 +395,16 @@ export const Modals = async (req, res) => {
                 const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
             }
             
+            let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+            if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
+            if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
+            if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
+            if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
+            if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
+            if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
+            if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
+            if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
+
             user_permissions = user_permissions.replace(/EntryNumber/g, i);
             user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
             user_permissions = user_permissions.replace(/PermissionsContainer/g, name);

+ 289 - 6
controllers/portal.js

@@ -1,16 +1,299 @@
+import { Readable } from 'stream';
+import { Permission, Container, User } from '../database/models.js';
+import { docker } from '../server.js';
+import { dockerContainerStats } from 'systeminformation';
+import { readFileSync } from 'fs';
+import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
 
+let hidden = '';
+
+// The actual page
 export const Portal = (req, res) => {
+    
+    let name = req.session.user;
+    let role = req.session.role;
+    let avatar = name.charAt(0).toUpperCase();
+
+    res.render("portal", {
+        name: name,
+        avatar: avatar,
+        role: role
+    });
+}
+
+async function containerInfo (containerName) {
+    let container = docker.getContainer(containerName);
+    let info = await container.inspect();
+    let image = info.Config.Image.split('/');
+    let ports_list = [];
+    try {
+        for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
+            let ports = {
+                check: 'checked',
+                external: value[0].HostPort,
+                internal: key.split('/')[0],
+                protocol: key.split('/')[1]
+            }
+            ports_list.push(ports);
+        }
+    } catch {
+        // no exposed ports
+    }
+
+    let external = 0;
+    let internal = 0;
+    try {
+        external = ports_list[0].external;
+        internal = ports_list[0].internal;
+    }   catch {
+        // no exposed ports
+    }
+    
+
+    let details = {
+        name: containerName,
+        image: image,
+        service: image[image.length - 1].split(':')[0],
+        state: info.State.Status,
+        external_port: external,
+        internal_port: internal,
+        ports: ports_list,
+        link: 'localhost',
+    }
+    return details;
+}
+
+async function createCard (details) {
+    if (hidden.includes(details.name)) { return;}
+    let shortname = details.name.slice(0, 10) + '...';
+    let trigger = 'data-hx-trigger="load, every 3s"';
+    let state = details.state;
+    let state_color = '';
+    switch (state) {
+        case 'running':
+            state_color = 'green';
+            break;
+        case 'exited':
+            state = 'stopped';
+            state_color = 'red';
+            trigger = 'data-hx-trigger="load"';
+            break;
+        case 'paused':
+            state_color = 'orange';
+            trigger = 'data-hx-trigger="load"';
+            break;
+        case 'installing':
+            state_color = 'blue';
+            trigger = 'data-hx-trigger="load"';
+            break;
+    }
+    // if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
+    let card  = readFileSync('./views/partials/containerCard.html', 'utf8');
+    card = card.replace(/AppName/g, details.name);
+    card = card.replace(/AppShortName/g, shortname);
+    card = card.replace(/AppIcon/g, details.service);
+    card = card.replace(/AppState/g, state);
+    card = card.replace(/StateColor/g, state_color);
+    card = card.replace(/ExternalPort/g, details.external_port);
+    card = card.replace(/InternalPort/g, details.internal_port);
+    card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
+    card = card.replace(/AppNameState/g, `${details.name}State`);
+    card = card.replace(/data-trigger=""/, trigger);
+    return card;
+}
+
+
+let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
+
+export async function addCard (name, state) {
+    console.log(`Adding card for ${name}: ${state}`);
+
+    let details = {
+        name: name,
+        image: name,
+        service: name,
+        state: 'installing',
+        external_port: 0,
+        internal_port: 0,
+        ports: [],
+        link: 'localhost',
+    
+    }
+    createCard(details).then(card => {
+        cardList += card;
+    });
+}
+
+
+
+export const updateCards = async (req, res) => {
+    console.log('updateCards called');
+    res.send(newCards);
+    newCards = '';
+}
+
 
-    if (!req.session.user) {
-        res.redirect("/login");
+export const Containers = async (req, res) => {
+    res.send(cardList);
+}
+
+export const Card = async (req, res) => {
+    let name = req.header('hx-trigger-name');
+    console.log(`${name} requesting updated card`);
+    // return nothing if in hidden or not found in containersArray
+    if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
+        res.send('');
         return;
+    } else {
+        let details = await containerInfo(name);
+        let card = await createCard(details);
+        res.send(card);
     }
+}
 
-    res.render("portal", {
-        name: req.session.user,
-        role: req.session.role,
-        avatar: req.session.avatar,
+
+
+
+function status (state) {
+    let status = `<span class="text-yellow align-items-center lh-1">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
+                    ${state}
+                </span>`;
+    return status;
+}
+
+
+export const Logs = (req, res) => {
+    let name = req.header('hx-trigger-name');
+    function containerLogs (data) {
+        return new Promise((resolve, reject) => {
+            let logString = '';
+            var options = { follow: false, stdout: true, stderr: false, timestamps: false };
+            var containerName = docker.getContainer(data);
+            containerName.logs(options, function (err, stream) {
+                if (err) { reject(err); return; }
+                const readableStream = Readable.from(stream);
+                readableStream.on('data', function (chunk) {
+                    logString += chunk.toString('utf8');
+                });
+                readableStream.on('end', function () {
+                    resolve(logString);
+                });
+            });
+        });
+    };
+    containerLogs(name).then((data) => {
+        res.send(`<pre>${data}</pre> `)
     });
+}
 
+export const Action = async (req, res) => {
+    let name = req.header('hx-trigger-name');
+    let state = req.header('hx-trigger');
+    let action = req.params.action;
+    // Start
+    if ((action == 'start') && (state == 'stopped')) {
+        var containerName = docker.getContainer(name);
+        containerName.start();
+        res.send(status('starting'));
+    } else if ((action == 'start') && (state == 'paused')) {
+        var containerName = docker.getContainer(name);
+        containerName.unpause();
+        res.send(status('starting'));
+    // Stop
+    } else if ((action == 'stop') && (state != 'stopped')) {
+        var containerName = docker.getContainer(name);
+        containerName.stop();
+        res.send(status('stopping'));
+    // Pause
+    } else if ((action == 'pause') && (state == 'paused')) {
+        var containerName = docker.getContainer(name);
+        containerName.unpause();
+        res.send(status('starting'));
+    }   else if ((action == 'pause') && (state == 'running')) {
+        var containerName = docker.getContainer(name);
+        containerName.pause();
+        res.send(status('pausing'));
+    // Restart
+    } else if (action == 'restart') {
+        var containerName = docker.getContainer(name);
+        containerName.restart();
+        res.send(status('restarting'));
+    // Hide
+    } else if (action == 'hide') {
+        let exists = await Container.findOne({ where: {name: name}});
+        if (!exists) {
+            const newContainer = await Container.create({ name: name, visibility: false, });
+        } else {
+            exists.update({ visibility: false });
+        }
+        hidden = await Container.findAll({ where: {visibility:false}});
+        hidden = hidden.map((container) => container.name);
+        res.send("ok");
+    // Reset View
+    } else if (action == 'reset') {
+        await Container.update({ visibility: true }, { where: {} });
+        hidden = await Container.findAll({ where: {visibility:false}});
+        hidden = hidden.map((container) => container.name);
+        res.send("ok");
+    }   
 }
 
+
+export const Modals = async (req, res) => {
+    let name = req.header('hx-trigger-name');
+    let id = req.header('hx-trigger');
+    let title = name.charAt(0).toUpperCase() + name.slice(1);
+
+    if (id == 'permissions') {
+        let permissions_list = '';
+        let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
+        permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
+        let users = await User.findAll({ attributes: ['username', 'UUID']});
+
+        for (let i = 0; i < users.length; i++) {
+            let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
+            let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+            if (!exists) {
+                const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
+            }
+            
+            let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+            if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
+            if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
+            if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
+            if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
+            if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
+            if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
+            if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
+            if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
+
+            user_permissions = user_permissions.replace(/EntryNumber/g, i);
+            user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+            user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+
+            permissions_list += user_permissions;
+        }
+
+        permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
+        res.send(permissions_modal);
+        return;
+    }
+
+
+
+    if (id == 'uninstall') {
+        let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
+        modal = modal.replace(/AppName/g, name);
+        // let containerPermissions = await Permission.findAll({ where: {containerName: name}});
+        res.send(modal);
+        return;
+    }
+
+    let modal = readFileSync('./views/modals/details.html', 'utf8');
+    let details = await containerInfo(name);
+
+    modal = modal.replace(/AppName/g, details.name);
+    modal = modal.replace(/AppImage/g, details.image);
+    res.send(modal);
+}

+ 0 - 2
controllers/supporters.js

@@ -2,8 +2,6 @@ import { User } from "../database/models.js";
 
 export const Supporters = async (req, res) => {
     
-    if (!req.session.UUID) return res.redirect("/login");
-
     let user = await User.findOne({ where: { UUID: req.session.UUID }});
     
 

+ 23 - 11
database/models.js

@@ -124,39 +124,51 @@ export const Permission = sequelize.define('Permission', {
   },
   install: {
     type: DataTypes.STRING,
+    defaultValue: false
   },
   uninstall: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   edit: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   upgrade: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   start: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   stop: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   restart: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   pause: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   logs: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   hide: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   reset_view: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
   view: {
-    type: DataTypes.STRING
+    type: DataTypes.STRING,
+    defaultValue: false
   },
 });
 

+ 43 - 17
router/index.js

@@ -1,5 +1,5 @@
 import express from "express";
-import { Permission, User } from '../database/models.js';
+import { Permission } from '../database/models.js';
 
 export const router = express.Router();
 
@@ -20,28 +20,54 @@ import { Syslogs } from "../controllers/syslogs.js";
 import { Portal } from "../controllers/portal.js"
 
 // Auth middleware
-const auth = (req, res, next) => {
-    if (req.session.role == "admin") {
+const auth = async (req, res, next) => {
+    if (!req.session.user) { res.redirect('/login'); return; }
+
+    let user = req.session.user;
+    let role = req.session.role;
+    let action = req.path.split("/")[2];
+    let trigger = req.header('hx-trigger-name');
+    // console.log("Auth: ", user, role, action, trigger);
+
+    if (role == "admin") {
         next();
-    } else {
-        res.redirect("/portal");
     }
-};
+    else if (action == "start" || action == "stop" || action == "pause" || action == "restart") {
+        let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
+        
+        if (permission) {
+            if (permission[action] == true) {
+                console.log(`User ${user} has permission to ${action} ${trigger}`);
+                next();
+            }
+            else {
+                console.log(`User ${user} does not have permission to ${action} ${trigger}`);
+            }
+        } else {
+            console.log(`No entry found for ${user} in ${trigger} permissions`);
+        }
+    }
+    else {
+        res.redirect('/portal');
+    }
+
+}
 
 // Admin routes
 router.get("/", auth, Dashboard);
 router.post("/action/:action", auth, Action);
-
-router.get("/logs", auth, Logs);
-router.get("/modals", auth, Modals);
-router.get("/stats", auth, Stats);
-router.get("/chart", auth, Chart);
-router.get("/sse_event", auth, SSE);
-router.get("/containers", auth, Containers);
-router.get("/card", auth, Card);
-router.get("/new_cards", auth, updateCards);
 router.post("/updatePermissions", auth, UpdatePermissions);
 
+router.get("/logs", Logs);
+router.get("/modals", Modals);
+router.get("/stats", Stats);
+router.get("/chart",  Chart);
+router.get("/sse_event", SSE);
+router.get("/containers", Containers);
+router.get("/card", Card);
+router.get("/new_cards", updateCards);
+
+
 router.get("/images", auth, Images);
 router.post("/removeImage", auth, removeImage);
 
@@ -81,6 +107,6 @@ router.get("/logout", Logout);
 import { Install } from "../functions/install.js"
 import { Uninstall } from "../functions/uninstall.js"
 
-router.post("/install", auth, Install);
-router.post("/uninstall", auth, Uninstall);
+router.post("/install", Install);
+router.post("/uninstall", Uninstall);
 

+ 1 - 1
views/partials/navbar.html

@@ -138,7 +138,7 @@
       </div>
       <div class="nav-item dropdown">
         <a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
-          <span class="avatar avatar-sm bg-green-lt"><%= name.charAt(0) %></span></span>
+          <span class="avatar avatar-sm bg-green-lt"><%= avatar %></span></span>
           <div class="d-none d-xl-block ps-2">
             <div>
               <%= name %>

+ 8 - 8
views/partials/user_permissions.html

@@ -40,7 +40,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="uninstall">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="uninstall" data-UninstallCheck>
                   </label>
                 </div>
               </div>
@@ -55,7 +55,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="edit">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="edit" data-EditCheck>
                   </label>
                 </div>
               </div>
@@ -70,7 +70,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="upgrade">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="upgrade" data-UpgradeCheck>
                   </label>
                 </div>
               </div>
@@ -85,7 +85,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="start">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="start" data-StartCheck>
                   </label>
                 </div>
               </div>
@@ -100,7 +100,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="stop">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="stop" data-StopCheck>
                   </label>
                 </div>
               </div>
@@ -115,7 +115,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="pause">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="pause" data-PauseCheck>
                   </label>
                 </div>
               </div>
@@ -130,7 +130,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="restart">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="restart" data-RestartCheck>
                   </label>
                 </div>
               </div>
@@ -146,7 +146,7 @@
                 </div>
                 <div class="col-3">
                   <label class="form-check form-check-single form-switch text-end">
-                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="logs">
+                    <input class="form-check-input" type="checkbox" name="selectEntryNumber" value="logs" data-LogsCheck>
                   </label>
                 </div>
               </div>

+ 223 - 87
views/portal.html

@@ -8,6 +8,7 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css" rel="stylesheet"/>
     <link href="/css/meters.css" rel="stylesheet"/>
+    <script src="/js/htmx.min.js"></script>
     <style>
       @import url('/fonts/inter.css');
       :root {
@@ -19,128 +20,263 @@
     </style>
   </head>
   <body >
-    <div class="page">
-
-      <%- include('partials/navbar.html') %>
-      <div class="page-wrapper">
   
-        <div class="page-body">
-          <div class="container-xl">
-            <div class="row row-deck row-cards">
-              
-              <div class="col-12" id="cards">
-                <div class="row row-cards">
-                  
-                  <div class="col-sm-6 col-lg-3">
-                    <div class="card card-sm">
-                      <div class="card-body">
-                        <div class="row align-items-center">
-                          <div class="col-auto">
-                            <span class="bg-primary text-white avatar">
-                              <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
-                            </span>
+  <div class="page">
+
+    <%- include('partials/navbar.html') %>
+    
+    <div class="page-wrapper">
+
+      <div class="page-body">
+        <div class="container-xl">
+          <div class="row row-deck row-cards">
+            
+            <div class="col-12">
+              <div class="row row-cards">
+                
+                <div class="col-sm-6 col-lg-3">
+                  <div class="card card-sm">
+                    <div class="card-body">
+                      <div class="row align-items-center">
+                        <div class="col-auto">
+                          <span class="bg-green text-white avatar">
+                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
+                          </span>
+                        </div>
+
+                        <!-- HTMX -->
+                        <div class="col" name="CPU" id="green" data-hx-get="" data-hx-trigger="">
+                          <div class="font-weight-medium">
+                            <label class="cpu-text mb-1" for="cpu">CPU 0%</label>
                           </div>
-                          <div class="col">
-                            <div class="font-weight-medium">
-                              <label id="cpu-text" class="cpu-text mb-1" for="cpu">I had to put something here.</label>
-                            </div>
-                            <div id="cpu-bar" class="cpu-bar meter animate">
-                              <span style="width: 25%"><span></span></span>
-                            </div>
+                          <div class="cpu-bar meter animate green">
+                            <span style="width:20%"><span></span></span>
                           </div>
                         </div>
+
                       </div>
                     </div>
                   </div>
-  
-                  <div class="col-sm-6 col-lg-3">
-                    <div class="card card-sm">
-                      <div class="card-body">
-                        <div class="row align-items-center">
-                          <div class="col-auto">
-                            <span class="bg-green text-white avatar">
-                              <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>                            
-                            </span>
+                </div>
+
+                <div class="col-sm-6 col-lg-3">
+                  <div class="card card-sm">
+                    <div class="card-body">
+                      <div class="row align-items-center">
+                        <div class="col-auto">
+                          <span class="bg-blue text-white avatar">
+                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>                            
+                          </span>
+                        </div>
+
+                        <!-- HTMX -->
+                        <div class="col" name="RAM" id="blue" data-hx-get="" data-hx-trigger="">
+                          <div class="font-weight-medium">
+                            <label class="ram-text mb-1" for="ram">RAM 0%</label>
                           </div>
-                          <div class="col">
-                            <div class="font-weight-medium">
-                              <label id="ram-text" class="ram-text mb-1" for="ram">RAM 0%</label>
-                            </div>
-                            <div id="ram-bar" class="ram-bar meter animate orange">
-                              <span style="width: 25%"><span></span></span>
-                            </div>
+                          <div class="ram-bar meter animate blue">
+                            <span style="width:20%"><span></span></span>
                           </div>
                         </div>
+
                       </div>
                     </div>
                   </div>
-                  
-                  <div class="col-sm-6 col-lg-3">
-                    <div class="card card-sm">
-                      <div class="card-body">
-                        <div class="row align-items-center">
-                          <div class="col-auto">
-                            <span class="bg-twitter text-white avatar">
-                              <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>                            
-                            </span>
+                </div>
+                
+                <div class="col-sm-6 col-lg-3">
+                  <div class="card card-sm">
+                    <div class="card-body">
+                      <div class="row align-items-center">
+                        <div class="col-auto">
+                          <span class="bg-purple text-white avatar">
+                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>                            
+                          </span>
+                        </div>
+
+                        <!-- HTMX -->
+                        <div class="col" name="NET" id="purple" data-hx-get="" data-hx-trigger="">
+                          <div class="font-weight-medium">
+                            <label id="net-text" class="net-text mb-1" for="network">Down: 0MB  Up: 0MB</label>
                           </div>
-                          <div class="col">
-                            <div class="font-weight-medium">
-                              <label id="net-text" class="net-text mb-1" for="network">Down: 0MB  Up: 0MB</label>
-                            </div>
-                            <div id="net-bar" class="meter animate blue">
-                              <span style="width: 25%"><span></span></span>
-                            </div>
+                          <div class="ram-bar meter animate purple">
+                            <span style="width:20%"><span></span></span>
                           </div>
                         </div>
+
                       </div>
                     </div>
                   </div>
-  
-                  <div class="col-sm-6 col-lg-3">
-                    <div class="card card-sm">
-                      <div class="card-body">
-                        <div class="row align-items-center">
-                          <div class="col-auto">
-                            <span class="bg-facebook text-white avatar">
-                              <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
-                            </span>
+                </div>
+
+                <div class="col-sm-6 col-lg-3">
+                  <div class="card card-sm">
+                    <div class="card-body">
+                      <div class="row align-items-center">
+                        <div class="col-auto">
+                          <span class="bg-orange text-white avatar">
+                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
+                          </span>
+                        </div>
+
+                        <!-- HTMX -->
+                        <div class="col" name="DISK" id="orange" data-hx-get="" data-hx-trigger="">
+                          <div class="font-weight-medium">
+                            <label class="disk-text mb-1" for="disk">DISK 0%</label>
                           </div>
-                          <div class="col">
-                            <div class="font-weight-medium">
-                              <label id="disk-text" class="disk-text mb-1" for="disk">DISK 0%</label>
-                            </div>
-                            <div id="disk-bar" class="meter animate red">
-                              <span style="width: 25%"><span></span></span>
-                            </div>
+                          <div class="meter animate orange">
+                            <span style="width:20%"><span></span></span>
                           </div>
                         </div>
+
                       </div>
                     </div>
                   </div>
-  
-  
+                </div>
+
+
+              </div>
+            </div>
+
+            
+            <!-- HTMX -->
+            <div class="col-12">
+              <div class="row row-cards" id="containers" data-hx-get="/containers" data-hx-trigger="load" data-hx-swap="innerHTML">
+              </div>
+            </div>
+
+            <!-- HTMX -->
+            <div class="col-12">
+              <div class="row row-cards" data-hx-get="/new_cards" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
+              </div>
+            </div>
+            
+            <!-- HTMX Target-->
+            <div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
+              <div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
+                <div class="modal-content">
+                  <div class="modal-header">
+                      <h5 class="modal-title">Loading</h5>
+                  </div>
+                  <div class="modal-body text-center">
+                    <div class="spinner-border"></div>
+                  </div>
                 </div>
               </div>
-  
-  
-              <!-- inserted here from  -->
-              
-  
             </div>
+            
+
+            <div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
+              <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+                <div class="modal-content">
+                  <div class="modal-header">
+                    <h5 class="modal-title">Logs</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                  </div>
+                  <div class="modal-body">
+                    <div class="card-body">
+                      <h4>Logs:</h4>
+                        <div id="logView">
+                          <pre>No logs available</pre>
+                        </div>
+                    </div>
+                  </div>
+                  <div class="modal-footer">
+                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
+                    <button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh"> 
+                      <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
+                        Refresh
+                    </button>
+                  </div>
+                </div>
+              </div>
+            </div>
+
           </div>
         </div>
-        
-        <%- include('partials/footer.html') %>
-        
       </div>
+      
+    <%- include('partials/footer.html') %>
+      
     </div>
+  </div>
     
 
-  <!-- Libs JS -->
-  <script src="/libs/apexcharts/dist/apexcharts.min.js" defer></script>
-  <!-- Tabler Core -->
+  <script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
   <script src="/js/tabler.min.js"></script>
+  <script>
+    var options = {
+        chart: {
+          type: "line",
+          height: 40.0,
+          sparkline: {
+            enabled: true
+          },
+          animations: {
+            enabled: false
+          }
+        },
+        fill: {
+          opacity: 1
+        },
+        stroke: {
+          width: [3, 1],
+          dashArray: [0, 3],
+          lineCap: "round",
+          curve: "smooth"
+        },
+        series: [{
+          name: "CPU",
+          data: []
+        }, {
+          name: "RAM",
+          data: []
+        }],
+        tooltip: {
+          enabled: false
+        },
+        grid: {
+          strokeDashArray: 4
+        },
+        xaxis: {
+          labels: {
+            padding: 0
+          },
+          tooltip: {
+            enabled: false
+          }
+        },
+        yaxis: {
+          min: 0,
+          max: 100,
+          labels: {
+            padding: 4
+          }
+        },
+        colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
+        legend: {
+          show: false
+        }
+    }
+  </script>
+  
+  <!-- SelectAll for the permissions modal -->
+  <script>
+    function selectAll(group) {
+      
+      let checkboxes = document.getElementsByName(group);
+      if (checkboxes[0].checked == true) {
+        for (var i = 0; i < checkboxes.length; i++) {
+          checkboxes[i].checked = true;
+        }
+      } else {
+        for (var i = 0; i < checkboxes.length; i++) {
+          checkboxes[i].checked = false;
+        }
+      }
+    }
+  </script>
+
   </body>
 </html>