Forráskód Böngészése

Almost a complete rewrite, part 5. Almost ready.

lllllllillllllillll 8 hónapja
szülő
commit
4bdf1e3148

+ 2 - 2
CHANGELOG.md

@@ -35,11 +35,11 @@
 * CSS and pages tweaks to make the style more consistent.
 * Improved container cards to be more compact.
 * Improved sponsors and credits pages.
-* New - Secret supporter code.
+* New - Secret code for sponsors.
 * Fixed installs not appearing or appearing multiple times.
 * Improved log view and fixed refresh button.
 * Made app cards more compact.
-
+* Updated container_card to only show exposed ports.
 
 
 ## v0.60 (June 9th 2024) - Permissions system and import templates

+ 4 - 4
Dockerfile

@@ -1,9 +1,9 @@
-FROM node:22-alpine
+FROM node:23-alpine
 ENV NODE_ENV=production
-WORKDIR /app
-COPY package.json /app
+WORKDIR /dweebui
+COPY package.json /dweebui
 RUN npm install
 RUN npm install pm2 -g
-COPY . /app
+COPY . /dweebui
 EXPOSE 8000
 CMD ["pm2-runtime", "server.js"]

+ 3 - 2
controllers/account.js

@@ -1,12 +1,13 @@
-import { User, ServerSettings } from '../database/config.js';
+import { User, ServerSettings } from '../db/config.js';
 import { Alert, getLanguage, Navbar, Sidebar, Footer } from '../utils/system.js';
 
 export const Account = async function(req,res){
 
+    req.session.host = `${req.params.host || 1}`;
+
     let container_links = await ServerSettings.findOne({ where: {key: 'container_links'}});
     let user_registration = await ServerSettings.findOne({ where: {key: 'user_registration'}});
 
-
     let user = await User.findOne({ where: {userID: req.session.userID}});
 
     res.render("account",{ 

+ 83 - 55
controllers/apps.js

@@ -1,4 +1,4 @@
-import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
+import { Alert, getLanguage, Navbar, Footer, Capitalize } from '../utils/system.js';
 import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
 import { parse } from 'yaml';
 import multer from 'multer';
@@ -26,6 +26,8 @@ export const searchApps = async function (req, res) {
 
 export const Apps = async function(req,res){
 
+  req.session.host = `${req.params.host || 1}`;
+  
   let [apps_list, app_count] = ['', ''];
   let page = Number(req.params.page) || 1;
   let template = req.params.template || 'default';
@@ -154,14 +156,68 @@ export const Apps = async function(req,res){
 
 export const submitApps = async function (req, res) {
 
-    let app_name = req.header('hx-trigger-name');
+    
+}
+
+
+
+
+function CatagoryColor(category) {
+  switch (category) {
+    case 'Other':
+      return '<span class="badge bg-blue-lt">Other</span> ';
+    case 'Productivity':
+      return '<span class="badge bg-blue-lt">Productivity</span> ';
+    case 'Tools':
+      return '<span class="badge bg-blue-lt">Tools</span> ';
+    case 'Dashboard':
+      return '<span class="badge bg-blue-lt">Dashboard</span> ';
+    case 'Communication':
+      return '<span class="badge bg-azure-lt">Communication</span> ';
+    case 'Media':
+      return '<span class="badge bg-azure-lt">Media</span> ';
+    case 'CMS':
+      return '<span class="badge bg-azure-lt">CMS</span> ';
+    case 'Monitoring':
+      return '<span class="badge bg-indigo-lt">Monitoring</span> ';
+    case 'LDAP':
+      return '<span class="badge bg-purple-lt">LDAP</span> ';
+    case 'Arr':
+      return '<span class="badge bg-purple-lt">Arr</span> ';
+    case 'Database':
+      return '<span class="badge bg-red-lt">Database</span> ';
+    case 'Paid':
+      return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
+    case 'Gaming':
+      return '<span class="badge bg-pink-lt">Gaming</span> ';
+    case 'Finance':
+      return '<span class="badge bg-orange-lt">Finance</span> ';
+    case 'Networking':
+      return '<span class="badge bg-yellow-lt">Networking</span> ';
+    case 'Authentication':
+      return '<span class="badge bg-lime-lt">Authentication</span> ';
+    case 'Development':
+      return '<span class="badge bg-green-lt">Development</span> ';
+    case 'Media Server':
+      return '<span class="badge bg-teal-lt">Media Server</span> ';
+    case 'Downloaders':
+      return '<span class="badge bg-cyan-lt">Downloaders</span> ';
+    default:
+      return ''; // default to other if the category is not recognized
+  }
+}
+
+
+
+export const appsModals = async function (req, res) {
+  let app_name = req.header('hx-trigger-name');
     let app_type = req.header('hx-trigger');
-    let action = req.params.action;
+    let modal = req.params.modal;
 
-    // console.log(`[submitApps] app_name: ${app_name} app_type: ${app_type} action: ${action}`);
+    // console.log(`[submitApps] app_name: ${app_name} app_type: ${app_type} modal: ${modal}`);
 
     // Modal for compose files
-    if (action == 'view_install' && app_type == 'compose') {
+    if (modal == 'view_install' && app_type == 'compose') {
       let compose = readFileSync(`appdata/compose/${app_name}/compose.yaml`, 'utf8');
       let modal = readFileSync('views/partials/compose.html', 'utf8');
       modal = modal.replace(/AppName/g, app_name);
@@ -170,8 +226,28 @@ export const submitApps = async function (req, res) {
       return;
     } 
 
+    // More Info modal
+    if (modal == 'info' && app_type == 'json') {
+
+      let modal = readFileSync('views/partials/info.html', 'utf8');
+
+      let app_title = Capitalize(app_name);
+      modal = modal.replace(/AppTitle/g, app_title);
+
+      let result = templates_global.find(t => t.name == app_name);
+  
+      modal = modal.replace(/AppDescription/g, result.description);
+
+      res.send(modal);
+      return;
+    } 
+
+
+
     // Modal for json templates
-    if (action == 'view_install' && app_type == 'json') {
+    if (modal == 'view_install' && app_type == 'json') {
+
+
       let result = templates_global.find(t => t.name == app_name);
       let name = result.name || result.title.toLowerCase();
       let short_name = name.slice(0, 25) + "...";
@@ -317,7 +393,7 @@ export const submitApps = async function (req, res) {
   
       }
       
-      let modal = readFileSync('views/partials/details.html', 'utf8');
+      let modal = readFileSync('views/partials/install.html', 'utf8');
       modal = modal.replace(/AppName/g, name);
       modal = modal.replace(/AppNote/g, note);
       modal = modal.replace(/AppImage/g, image);
@@ -358,52 +434,4 @@ export const submitApps = async function (req, res) {
 
     res.send(modal);
   }
-}
-
-
-
-
-function CatagoryColor(category) {
-  switch (category) {
-    case 'Other':
-      return '<span class="badge bg-blue-lt">Other</span> ';
-    case 'Productivity':
-      return '<span class="badge bg-blue-lt">Productivity</span> ';
-    case 'Tools':
-      return '<span class="badge bg-blue-lt">Tools</span> ';
-    case 'Dashboard':
-      return '<span class="badge bg-blue-lt">Dashboard</span> ';
-    case 'Communication':
-      return '<span class="badge bg-azure-lt">Communication</span> ';
-    case 'Media':
-      return '<span class="badge bg-azure-lt">Media</span> ';
-    case 'CMS':
-      return '<span class="badge bg-azure-lt">CMS</span> ';
-    case 'Monitoring':
-      return '<span class="badge bg-indigo-lt">Monitoring</span> ';
-    case 'LDAP':
-      return '<span class="badge bg-purple-lt">LDAP</span> ';
-    case 'Arr':
-      return '<span class="badge bg-purple-lt">Arr</span> ';
-    case 'Database':
-      return '<span class="badge bg-red-lt">Database</span> ';
-    case 'Paid':
-      return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
-    case 'Gaming':
-      return '<span class="badge bg-pink-lt">Gaming</span> ';
-    case 'Finance':
-      return '<span class="badge bg-orange-lt">Finance</span> ';
-    case 'Networking':
-      return '<span class="badge bg-yellow-lt">Networking</span> ';
-    case 'Authentication':
-      return '<span class="badge bg-lime-lt">Authentication</span> ';
-    case 'Development':
-      return '<span class="badge bg-green-lt">Development</span> ';
-    case 'Media Server':
-      return '<span class="badge bg-teal-lt">Media Server</span> ';
-    case 'Downloaders':
-      return '<span class="badge bg-cyan-lt">Downloaders</span> ';
-    default:
-      return ''; // default to other if the category is not recognized
-  }
 }

+ 3 - 6
controllers/credits.js

@@ -1,10 +1,10 @@
-import { ServerSettings, User } from '../database/config.js';
+import { ServerSettings, User } from '../db/config.js';
 import { Alert, getLanguage, Navbar, Sidebar, Footer, Capitalize } from '../utils/system.js';
 import { readdirSync, readFileSync } from 'fs';
 
 export const Credits = async function (req, res) {
 
-    let language = await getLanguage(req);
+    let language = await getLanguage(req.session.userID);
     let Language = Capitalize(language);
     let selected = `<option value="${language}" selected hidden>${Language}</option>`;
 
@@ -12,10 +12,7 @@ export const Credits = async function (req, res) {
     let preferences = JSON.parse(user.preferences);
     let hide_profile = preferences.hide_profile;
 
-    let checked = '';
-    if (hide_profile == true) { checked = 'checked'; }
-
-
+    let checked = ''; if (hide_profile == true) { checked = 'checked'; }
 
     res.render("credits",{ 
         alert: '',

+ 340 - 195
controllers/dashboard.js

@@ -1,57 +1,46 @@
 import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
-import { docker, containerInfo, containerLogs, containerStats, GetContainerLists } from '../utils/docker.js';
+import { docker, containerInfo, containerLogs, GetContainerLists, containerStats, trigger_docker_event } from '../utils/docker.js';
 import { readFileSync } from 'fs';
-import { User, Permission, ServerSettings, ContainerLists, Container } from '../database/config.js';
+import { User, Permission, ServerSettings, ContainerLists, Container } from '../db/config.js';
 import { Alert, Navbar, Footer, Capitalize } from '../utils/system.js';
 import { Op } from 'sequelize';
 
-let [ hidden, alert, stats ] = [ '', '', '', {} ];
-let container_link = 'http://localhost';
-
-
 
 // Dashboard
 export const Dashboard = async function (req, res) {
 
-    let host = req.params.host || 1;
-    req.session.host = `${host}`;
+    console.log(`[Dashboard] ${req.session.username}`);
 
+    let username = req.session.username;
+    let userID = req.session.userID;
+    let role = req.session.role;
+    let host = req.session.host;
+    
     // Create the lists needed for the dashboard
     const [list, created] = await ContainerLists.findOrCreate({
-        where: { userID: req.session.userID },
-        defaults: {
-            userID: req.session.userID,
-            username: req.session.username,
-            containers: '[]',
-            new: '[]',
-            updates: '[]',
-            sent: '[]',
-        },
+        where: { userID: userID },
+        defaults: { userID: userID, username: username, containers: '[]', new: '[]', updates: '[]', sent: '[]', },
     });
-    if (created) { console.log(`New entry created in ContainerLists for ${req.session.username}`); }
+
 
     res.render("dashboard",{ 
         alert: '',
-        username: req.session.username,
-        role: req.session.role,
+        username: username,
+        role: role,
         navbar: await Navbar(req),
         footer: await Footer(req),
     }); 
 }
 
 
-
-
 // Dashboard search
 export const searchDashboard = async function (req, res) {
-    console.log(`[Search] ${req.body.search}`);
+    // console.log(`[Search] ${req.body.search}`);
     res.send('ok');
     return;
 }
 
 
-
-
 // Server metrics (CPU, RAM, TX, RX, DISK)
 export const ServerMetrics = async (req, res) => {
     let name = req.header('hx-trigger-name');
@@ -81,6 +70,7 @@ export const ServerMetrics = async (req, res) => {
 }
 
 
+
 async function userCards (req) {
     
     let container_list = [];
@@ -108,196 +98,58 @@ async function userCards (req) {
     return container_list;
 }
 
-// Container actions (start, stop, pause, restart, hide)
-export const ContainerAction = async (req, res) => {
-
-    let container_name = req.header('hx-trigger-name');
-    let containerID = req.params.containerid;
-    let action = req.params.action;
-    
-    console.log(`[Action] ${action} ${container_name} ${containerID}`);
-
-    if (action == 'reset') { 
-        console.log('Resetting view'); 
-        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
-        res.redirect('/dashboard');
-        return;
-    }
-    else if (action == 'logs') {
-        let logs = await containerLogs(containerID);
-        let modal = readFileSync('./views/partials/logs.html', 'utf8');
-        modal = modal.replace(/AppName/g, container_name);
-        modal = modal.replace(/ContainerID/g, containerID);
-        modal = modal.replace(/ContainerLogs/g, logs);
-
-        res.send(modal);
-        return;
-    }
-    else if (action == 'details') {
-        let container = await containerInfo(containerID);
-        let modal = readFileSync('./views/partials/details.html', 'utf8');
-        modal = modal.replace(/AppName/g, container.containerName);
-        modal = modal.replace(/AppImage/g, container.containerImage);
-        res.send(modal);
-        return;
-    }
-    else if (action == 'uninstall') {
-        let modal = readFileSync('./views/partials/uninstall.html', 'utf8');
-        modal = modal.replace(/AppName/g, container_name);
-        modal = modal.replace(/ContainerID/g, containerID);
-        res.send(modal);
-        return;
-    }
-    else if (action == 'link_modal') {
-        const [container, created] = await Container.findOrCreate({ where: { containerID: containerID }, defaults: { containerName: container_name, containerID: containerID, link: '' } });
-        let modal = readFileSync('./views/partials/link.html', 'utf8');
-        modal = modal.replace(/AppName/g, container_name);
-        modal = modal.replace(/ContainerID/g, containerID);
-        modal = modal.replace(/AppLink/g, container.link);
-        res.send(modal);
-        return;
-    } else if (action == 'update_link') {
-        let url = req.body.url;
-        console.log(url);
-        // find the container entry with the containerID and userID
-        let container = await Container.findOne({ where: { containerID: containerID } });
-        container.update({ link: url });
-        res.send('ok');
-        return;
-    }
-
-    // Inspect the container
-    let info = docker.getContainer(containerID);
-    let container = await info.inspect();
-    let containerState = container.State.Status;
-    
-    // Displays container state (starting, stopping, restarting, pausing)
-    function status (state) {
-        return(`<div class="text-yellow d-inline-flex align-items-center lh-1 ms-auto" id="AltIDState">
-                <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>
-                <strong>${state}</strong>
-                </div>`);
-    }
-
-    // Perform the action
-    if ((action == 'start') && (containerState == 'exited')) {
-        info.start();
-        res.send(status('starting'));
-    } else if ((action == 'start') && (containerState == 'paused')) {
-        info.unpause();
-        res.send(status('starting'));
-    } else if ((action == 'stop') && (containerState != 'exited')) {
-        info.stop();
-        res.send(status('stopping'));
-    } else if ((action == 'pause') && (containerState == 'paused')) {
-        info.unpause();
-        res.send(status('starting'));
-    } else if ((action == 'pause') && (containerState == 'running')) {
-        info.pause();
-        res.send(status('pausing'));
-    } else if (action == 'restart') {
-        info.restart();
-        res.send(status('restarting'));
-    } else if (action == 'hide') {
-        let exists = await Permission.findOne({ where: { containerID: containerID, userID: req.session.userID }});
-        if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: containerID, username: req.session.username, userID: req.session.userID, hide: true }); }
-        else { exists.update({ hide: true }); }
-        res.send('ok'); 
-    }
-}
-
 
 
 
 async function createCard (details) {
 
-    // let trigger = 'data-hx-trigger="load, every 3s"';
+    let container_card = readFileSync('./views/partials/container_card.html', 'utf8');
 
     let containerName = details.containerName;
-    if (containerName.length > 13) { containerName = containerName.substring(0, 13) + '...'; }
-    let containerTitle = Capitalize(containerName);
-
-    let container_link = '';
-    let container = await Container.findOne({ where: { containerID: details.containerID } });
-    container_link = container.link || '#';
-
-    let titleLink = `<a href="${container_link}" class="nav-link" target="_blank">${containerTitle}</a>`;
-
+    let containerTitle = Capitalize(containerName); if (containerTitle.length > 14) { containerTitle = containerTitle.substring(0, 14) + '...'; }
     let containerID = details.containerID;
     let containerState = details.containerState;
     let containerService = details.containerService;
+    let AltID = `a${containerID}`;
+
+    let chart_trigger = `<div name="${containerName}" id="${AltID}info" hx-get="/dashboard/view/chart/${containerID}" hx-swap="outerHTML" hx-trigger="every 3s" hx-target="#${AltID}info">
+                        </div>`;
+
     let containerStateColor = '';
+    switch (containerState) {
+        case 'running': containerStateColor = 'green'; break;
+        case 'exited': containerStateColor = 'red'; containerState = 'stopped'; chart_trigger = ''; break;
+        case 'paused': containerStateColor = 'orange'; break;
+        default: containerStateColor = 'blue'; break;
+    }
 
-    if (containerState == 'running') { containerStateColor = 'green'; }
-    else if (containerState == 'exited') { containerStateColor = 'red'; containerState = 'stopped'; }
-    else if (containerState == 'paused') { containerStateColor = 'orange'; }
-    else { containerStateColor = 'blue'; }
+    let [title_link, created] = await Container.findOrCreate({ where: { containerID: details.containerID }, defaults: { containerName: containerName, containerID: containerID, link: '', cpu: '[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]', ram: '[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]' } });
+    if (title_link.link != '') { title_link = `<a href="${title_link.link}" class="nav-link" target="_blank">${containerTitle}</a>`; }
+    else { title_link = containerTitle; }
+    
+    let [port_link, created_link] = await ServerSettings.findOrCreate({ where: { key: 'custom_link' }, defaults: { key: 'custom_link', value: 'http://localhost' } });
+    port_link = port_link.value;
 
-    let container_card = readFileSync('./views/partials/container_card.html', 'utf8');
+    let exposed_ports = '';
+    for (let i = 0; i < details.ports.length; i++) {
+        if (details.ports[i].external != '' && details.ports[i].protocol != 'udp') { exposed_ports += `<a href="${port_link}:${details.ports[i].external}" target="_blank" style="color: inherit; text-decoration: none;"> ${details.ports[i].external}</a> `; }
+    }
 
-    container_card = container_card.replace(/ContainerID/g, containerID);
-    container_card = container_card.replace(/AltID/g, 'a' + containerID);
-    container_card = container_card.replace(/TitleLink/g, titleLink);
     container_card = container_card.replace(/AppName/g, containerName);
+    container_card = container_card.replace(/ContainerID/g, containerID);
+    container_card = container_card.replaceAll(/AltID/g, AltID);
+    container_card = container_card.replace(/AppPorts/g, exposed_ports);
+    container_card = container_card.replace(/TitleLink/g, title_link);
     container_card = container_card.replace(/AppTitle/g, containerTitle);
     container_card = container_card.replace(/AppService/g, containerService);
     container_card = container_card.replace(/AppState/g, containerState);
     container_card = container_card.replace(/StateColor/g, containerStateColor);
-    
+    container_card = container_card.replace(/ChartTrigger/g, chart_trigger);
 
-    if (details.external_port == 0 && details.internal_port == 0) {
-        container_card = container_card.replace(/AppPorts/g, ``);
-    } else {
-        container_card = container_card.replace(/AppPorts/g, `<a href="${container_link}:${details.external_port}" target="_blank" style="color: inherit; text-decoration: none;"> ${details.external_port}:${details.internal_port}</a>`);
-    }
     return container_card;
 }
 
 
-
-export const UpdateCard = async function (req, res) {
-
-        let containerID = req.params.containerid;
-
-        let lists = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['containers'] });
-        let container_list = JSON.parse(lists.containers);
-
-        let found = container_list.find(c => c.containerID === containerID);
-        if (!found) { res.send(''); return; }
-        let details = await containerInfo(containerID);
-        let card = await createCard(details);
-        res.send(card);
-}
-
-
-
-export const CardList = async function (req, res) {
-    let cards_list = '';
-    // Check if there are any new cards in queue.
-    let new_cards = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['new'] });
-    let new_list = JSON.parse(new_cards.new);
-    // Check what containers the user should see.
-    let containers = await userCards(req);
-    // Create the cards.
-    if (new_list.length > 0) {
-        for (let i = 0; i < new_list.length; i++) {
-            let details = await containerInfo(new_list[i]);
-            let card = await createCard(details);
-            cards_list += card;
-        }
-    } else {
-        for (let i = 0; i < containers.length; i++) {
-            let details = await containerInfo(containers[i].containerID);
-            let card = await createCard(details);
-            cards_list += card;
-        }
-    }
-    // Update lists, clear the queue, and send the cards.
-    await ContainerLists.update({ containers: JSON.stringify(containers), sent: JSON.stringify(containers), new: '[]' }, { where: { userID: req.session.userID } });
-    res.send(cards_list);
-}
-
-
 // HTMX - Server-side events
 export const SSE = async (req, res) => {
     
@@ -309,7 +161,6 @@ export const SSE = async (req, res) => {
     
     async function eventCheck () {
 
-
         let list = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['sent'] });
         let container_list = await userCards(req);
 
@@ -319,7 +170,8 @@ export const SSE = async (req, res) => {
         sent_cards = JSON.parse(list.sent);
 
         if (JSON.stringify(container_list) == list.sent) { return; }
-        console.log(`Update for ${req.session.username}`);
+
+        // console.log(`Update for ${req.session.username}`);
 
         // loop through the containers list to see if any new containers have been added or changed
         container_list.forEach(container => {
@@ -356,12 +208,305 @@ export const SSE = async (req, res) => {
     
     docker.getEvents({}, async function (err, data) {
         data.on('data', async function () {
-            console.log(`[Docker Event]`);
             await eventCheck();
         });
     });
 
     req.on('close', async () => {
-        // Nothing
     });
+}
+
+
+
+export const DashboardView = async function (req, res) {
+
+    let container_name = req.header('hx-trigger-name');
+    let view = req.params.view;
+    let containerID = req.params.id;
+    let AltID = `a${containerID}`;
+
+    // console.log(`[container_name] ${container_name} [view] ${view} [containerID] ${containerID}`);
+
+    // Container CPU and RAM chart
+
+    if (view == 'chart') {
+        let container = await Container.findOne({ where: { containerID: containerID } });
+        // Get the cpu and ram stats, remove the oldest entry, add the newest stats, then update container info.
+        let stats = await containerStats(containerID);        
+        let cpu = JSON.parse(container.cpu); cpu.shift(); cpu.push(stats.cpu);
+        let ram = JSON.parse(container.ram); ram.shift(); ram.push(stats.ram);
+        container.update({ cpu: JSON.stringify(cpu), ram: JSON.stringify(ram) });
+
+        let chartData = `<div name="${container_name}" id="${AltID}info" hx-get="/dashboard/view/chart/${containerID}" hx-swap="outerHTML" hx-trigger="every 3s" hx-target="#${AltID}info">
+                                <script>
+                                    ${AltID}chart.updateSeries([{
+                                        name: 'CPU',
+                                        data: ${container.cpu}
+                                    }, {
+                                        name: 'RAM',
+                                        data: ${container.ram}
+                                    }]);
+                                </script>
+                            </div>`;
+        res.send(chartData);
+        return;
+    }
+
+    // Permissions modal
+
+    if (view == 'permissions') {
+        let title = Capitalize(container_name);
+        let users = await User.findAll({ attributes: ['username', 'userID'] });
+    
+        let modal =`<div class="modal-header">
+                                <h5 class="modal-title">${title} Permissions</h5>
+                                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                            </div>
+                            <div class="modal-body"><div class="accordion" id="accordion-example">`;
+    
+        for (let i = 0; i < users.length; i++) {
+            if (users.length == 1) { modal += 'No other users.'; break; }
+            // Skip the admin user.
+            else if (i == 0) { continue; }
+            let exists = await Permission.findOne({ where: {containerID: containerID, userID: users[i].userID}});
+            if (!exists) { await Permission.create({ containerName: container_name, containerID: containerID, userID: users[i].userID, username: users[i].username}); }
+            let permissions = await Permission.findOne({ where: {containerID: containerID, userID: users[i].userID}});
+            let user_permissions = readFileSync('./views/partials/permissions.html', 'utf8');
+            if (permissions.uninstall == true && permissions.edit == true && permissions.upgrade == true && permissions.start == true && permissions.stop == true && permissions.pause == true && permissions.restart == true && permissions.logs == true && permissions.view == true) { user_permissions = user_permissions.replace(/data-AllCheck/g, 'checked'); }
+            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(/Entry/g, i);
+            user_permissions = user_permissions.replace(/Entry/g, i);
+            user_permissions = user_permissions.replace(/Entry/g, i);
+            user_permissions = user_permissions.replace(/container_id/g, containerID);
+            user_permissions = user_permissions.replace(/container_name/g, container_name);
+            user_permissions = user_permissions.replace(/user_id/g, users[i].userID);
+            user_permissions = user_permissions.replace(/Username/g, users[i].username);
+            modal += user_permissions;
+        }
+        modal += `</div></div>
+                        <div class="modal-footer">
+                            
+                            <form id="reset_permissions" class="me-auto">
+                                <input type="hidden" name="containerID" value="${containerID}">
+                                <button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" id="submit" hx-post="/dashboard/action/update_permissions/${containerID}" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
+                            </form>
+    
+                            <button type="button" class="btn" data-bs-dismiss="modal">Close</button>
+                        </div>`
+        res.send(modal);
+        return;
+    }   
+
+    // Logs modal
+
+    if (view == 'logs') {
+        let logs = await containerLogs(containerID);
+        let modal = readFileSync('./views/partials/logs.html', 'utf8');
+        modal = modal.replace(/AppName/g, container_name);
+        modal = modal.replace(/ContainerID/g, containerID);
+        modal = modal.replace(/ContainerLogs/g, logs);
+        res.send(modal);
+        return;
+    }
+
+    // Details modal
+
+    if (view == 'details') {
+        let container = await containerInfo(containerID);
+        let modal = readFileSync('./views/partials/details.html', 'utf8');
+        modal = modal.replace(/AppName/g, container.containerName);
+        modal = modal.replace(/AppImage/g, container.containerImage);
+        for (let i = 0; i <= 6; i++) {
+            modal = modal.replaceAll(`Port${i}Check`, container.ports[i]?.check || '');
+            modal = modal.replaceAll(`Port${i}External`, container.ports[i]?.external || '');
+            modal = modal.replaceAll(`Port${i}Internal`, container.ports[i]?.internal || '');
+            modal = modal.replaceAll(`Port${i}Protocol`, container.ports[i]?.protocol || '');
+        }
+        for (let i = 0; i <= 6; i++) {
+            modal = modal.replaceAll(`Vol${i}Source`, container.volumes[i]?.Source || '');
+            modal = modal.replaceAll(`Vol${i}Destination`, container.volumes[i]?.Destination || '');
+            modal = modal.replaceAll(`Vol${i}RW`, container.volumes[i]?.RW || '');
+        }
+        for (let i = 0; i <= 19; i++) {
+            modal = modal.replaceAll(`Label${i}Key`, Object.keys(container.labels)[i] || '');
+            modal = modal.replaceAll(`Label${i}Value`, Object.values(container.labels)[i] || '');
+        }
+        for (let i = 0; i <= 19; i++) {
+            modal = modal.replaceAll(`Env${i}Key`, container.env[i]?.split('=')[0] || '');
+            modal = modal.replaceAll(`Env${i}Value`, container.env[i]?.split('=')[1] || '');
+        }
+        res.send(modal);
+        return;
+    }
+
+    // Uninstall modal
+
+    if (view == 'uninstall') {
+        let modal = readFileSync('./views/partials/uninstall.html', 'utf8');
+        modal = modal.replace(/AppName/g, container_name);
+        modal = modal.replace(/ContainerID/g, containerID);
+        res.send(modal);
+        return;
+    }
+    
+    // Update link modal
+
+    if (view == 'link_modal') {
+        const [container, created] = await Container.findOrCreate({ where: { containerID: containerID }, defaults: { containerName: container_name, containerID: containerID, link: '' } });
+        let modal = readFileSync('./views/partials/link.html', 'utf8');
+        modal = modal.replace(/AppName/g, container_name);
+        modal = modal.replace(/ContainerID/g, containerID);
+        modal = modal.replace(/AppLink/g, container.link);
+        res.send(modal);
+        return;
+    }
+
+    // Update container_card
+
+    if (view == 'update_card'){
+
+        let lists = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['containers'] });
+        let container_list = JSON.parse(lists.containers);
+
+        let found = container_list.find(c => c.containerID === containerID);
+        if (!found) { res.send(''); return; }
+        let details = await containerInfo(containerID);
+        let card = await createCard(details);
+        res.send(card);
+        return;
+    }
+    
+    // Generate list of container_cards for the dashboard
+
+    if (view == 'card_list'){
+        let cards_list = '';
+        // Check if there are any new cards in queue.
+        let new_cards = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['new'] });
+        let new_list = JSON.parse(new_cards.new);
+        // Check what containers the user should see.
+        let containers = await userCards(req);
+        // Create the cards.
+        if (new_list.length > 0) {
+            for (let i = 0; i < new_list.length; i++) {
+                let details = await containerInfo(new_list[i]);
+                let card = await createCard(details);
+                cards_list += card;
+            }
+        } else {
+            for (let i = 0; i < containers.length; i++) {
+                let details = await containerInfo(containers[i].containerID);
+                let card = await createCard(details);
+                cards_list += card;
+            }
+        }
+        // Update lists, clear the queue, and send the cards.
+        await ContainerLists.update({ containers: JSON.stringify(containers), sent: JSON.stringify(containers), new: '[]' }, { where: { userID: req.session.userID } });
+        res.send(cards_list);
+        return;
+    }
+
+    
+}
+
+
+
+// Container actions (start, stop, pause, restart, hide)
+export const DashboardAction = async (req, res) => {
+
+    // let trigger_id = req.header('hx-trigger');
+    let container_name = req.header('hx-trigger-name');
+    let action = req.params.action;
+    let containerID = req.params.id;
+
+    // console.log(`[container_name] ${container_name} [action] ${action} [containerID] ${containerID}`);
+
+    if (action == 'reset') { 
+        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
+        res.redirect('/dashboard');
+        return;
+    } else if (action == 'update_link') {
+        let url = req.body.url;
+        let container = await Container.findOne({ where: { containerID: containerID } });
+        container.update({ link: url });
+        res.redirect('/dashboard');
+        return;
+    } else if (action == 'update_permissions') {
+        let { userID, username, reset_permissions, select } = req.body;
+        let button_id = req.header('hx-trigger');
+        // Replaces the update button if it's been pressed.
+        if (button_id == 'confirmed') { res.send(`<button class="btn" type="button" id="submit" hx-post="/dashboard/action/update_permissions/${containerID}" hx-swap="outerHTML">Update  </button>`); return; }
+        // Reset all permissions for the container.
+        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 } }); trigger_docker_event(); return; }
+        // Make sure req.body[select] is an array
+        if (typeof req.body[select] == 'string') { req.body[select] = [req.body[select]]; }
+    
+        await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerID: containerID, userID: userID } });
+        if (req.body[select]) {
+            for (let i = 0; i < req.body[select].length; i++) {
+                let permissions = req.body[select][i];
+                if (permissions == 'uninstall') { await Permission.update({ uninstall: true }, { where: {containerID: containerID, userID: userID}}); }  
+                if (permissions == 'edit') { await Permission.update({ edit: true }, { where: {containerID: containerID, userID: userID}}); }   
+                if (permissions == 'upgrade') { await Permission.update({ upgrade: true }, { where: {containerID: containerID, userID: userID}}); }   
+                if (permissions == 'start') { await Permission.update({ start: true }, { where: {containerID: containerID, userID: userID}}); }   
+                if (permissions == 'stop') { await Permission.update({ stop: true }, { where: {containerID: containerID, userID: userID}}); }   
+                if (permissions == 'pause') { await Permission.update({ pause: true }, { where: {containerID: containerID, userID: userID}}); }   
+                if (permissions == 'restart') { await Permission.update({ restart: true }, { where: {containerID: containerID, userID: userID}}); }   
+                if (permissions == 'logs') { await Permission.update({ logs: true }, { where: {containerID: containerID, userID: userID}}); }
+                if (permissions == 'view') { await Permission.update({ view: true }, { where: {containerID: containerID, userID: userID}}); }
+            }
+        }
+        trigger_docker_event();
+        res.send(`<button class="btn" type="button" id="confirmed" hx-post="/dashboard/action/update_permissions/${containerID}" hx-swap="outerHTML" hx-trigger="load delay:1s">Update ✔️</button>`);
+        return;
+    } else if (action == 'switch_host') {
+        req.session.host = req.body.host;
+        console.log(`Switched to host ${req.session.host}`);
+        res.redirect('/dashboard');
+        return;
+    }
+    // Inspect the container
+    let info = docker.getContainer(containerID);
+    let container = await info.inspect();
+    let containerState = container.State.Status;
+    
+    // Displays container state (starting, stopping, restarting, pausing)
+    function status (state) {
+        return(`<div class="text-yellow d-inline-flex align-items-center lh-1 ms-auto" id="AltIDState">
+                <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>
+                <strong>${state}</strong>
+                </div>`);
+    }
+
+    if ((action == 'start') && (containerState == 'exited')) {
+        info.start();
+        res.send(status('starting'));
+    } else if ((action == 'start') && (containerState == 'paused')) {
+        info.unpause();
+        res.send(status('starting'));
+    } else if ((action == 'stop') && (containerState != 'exited')) {
+        info.stop();
+        res.send(status('stopping'));
+    } else if ((action == 'pause') && (containerState == 'paused')) {
+        info.unpause();
+        res.send(status('starting'));
+    } else if ((action == 'pause') && (containerState == 'running')) {
+        info.pause();
+        res.send(status('pausing'));
+    } else if (action == 'restart') {
+        info.restart();
+        res.send(status('restarting'));
+    } else if (action == 'hide') {
+        let exists = await Permission.findOne({ where: { containerID: containerID, userID: req.session.userID }});
+        if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: containerID, username: req.session.username, userID: req.session.userID, hide: true }); }
+        else { exists.update({ hide: true }); }
+        res.send('ok'); 
+    }
 }

+ 9 - 4
controllers/images.js

@@ -3,9 +3,11 @@ import { imageList, GetContainerLists } from '../utils/docker.js';
 
 export const Images = async function(req,res){
 
+    req.session.host = `${req.params.host || 1}`;
+
     let container_images = [];
     let image_list = '';
-    
+
     let containers = await GetContainerLists();
     for (let i = 0; i < containers.length; i++) {
         container_images.push(containers[i].Image);
@@ -21,6 +23,9 @@ export const Images = async function(req,res){
         try { name = images[i].RepoTags[0].split(':')[0]; } catch {}
         try { tag = images[i].RepoTags[0].split(':')[1]; } catch {}
 
+        // let image_id = images[i].Id.split(':')[1].substring(0, 12);
+        let image_id = images[i].Id.split(':')[1];
+
         let date = new Date(images[i].Created * 1000);
         let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
 
@@ -39,11 +44,11 @@ export const Images = async function(req,res){
                 <td><input class="form-check-input m-0 align-middle" name="select" value="${images[i].Id}" type="checkbox" aria-label="Select"></td>
                 <td class="sort-name">${name}</td>
                 <td class="sort-type">${tag}</td>
-                <td class="sort-city">${images[i].Id}</td>
+                <td class="sort-city">${image_id}</td>
                 <td class="sort-score text-green">${status}</td>
-                <td class="sort-date" data-date="1628122643">${created}</td>
                 <td class="sort-quantity">${size} MB</td>
-                <td class="text-end"><a class="btn" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
+                <td class="sort-date" data-date="1628122643">${created}</td>
+                <td class=""><a class="container-action" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
             </tr>`
         image_list += details;
     }

+ 6 - 6
controllers/login.js

@@ -1,5 +1,5 @@
 import bcrypt from 'bcrypt';
-import { User, Syslog, ServerSettings } from '../database/config.js';
+import { User, Syslog, ServerSettings } from '../db/config.js';
 
 
 export const Login = async function (req, res) {
@@ -16,14 +16,14 @@ export const Login = async function (req, res) {
         req.session.username = 'Localhost';
         req.session.userID = '00000000-0000-0000-0000-000000000000';
         req.session.role = 'admin';
-        await Syslog.create({ username: 'Localhost', uniqueID: 'localhost', event: "Login", message: "User logged in", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: 'Localhost', uniqueID: 'localhost', event: "Login", message: "User logged in", ip: req.ip });
         res.redirect("/dashboard");
         return;
     } else if (authentication.value == 'no_auth') {
         req.session.username = 'No Auth';
         req.session.userID = '00000000-0000-0000-0000-000000000000';
         req.session.role = 'admin';
-        await Syslog.create({ username: 'No Auth', uniqueID: 'no_auth', event: "Login", message: "User logged in", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: 'No Auth', uniqueID: 'no_auth', event: "Login", message: "User logged in", ip: req.ip });
         res.redirect("/dashboard");
         return;
     }
@@ -45,7 +45,7 @@ export const submitLogin = async function (req, res) {
 
     // If there is no users with that email or the password is incorrect.
     if (!user || !await bcrypt.compare(password, user.password)) { 
-        await Syslog.create({ username: '', uniqueID: email, event: "Login Attempt", message: "User login failed", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: '', uniqueID: email, event: "Login Attempt", message: "User login failed", ip: req.ip });
         res.render("login",{ "error": "Invalid credentials." });
         return;
     }
@@ -59,7 +59,7 @@ export const submitLogin = async function (req, res) {
 
         console.log(`${req.session.username} logged in`);
 
-        await Syslog.create({ username: user.username, uniqueID: email, event: "Login", message: "User logged in", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: user.username, uniqueID: email, event: "Login", message: "User logged in", ip: req.ip });
         res.redirect("/dashboard");
         return;
     }
@@ -68,7 +68,7 @@ export const submitLogin = async function (req, res) {
 
 export const Logout = async function(req,res){
     console.log(`User ${req.session.username} logged out \n`);
-    await Syslog.create({ username: req.session.username, uniqueID: req.session.userID, event: "Logout", message: "User logged out", ip: req.socket.remoteAddress });
+    await Syslog.create({ username: req.session.username, uniqueID: req.session.userID, event: "Logout", message: "User logged out", ip: req.ip });
     req.session.destroy(() => {
         res.redirect("/login");
     });

+ 3 - 1
controllers/networks.js

@@ -3,6 +3,8 @@ import { networkList, GetContainerLists, removeNetwork } from '../utils/docker.j
 
 export const Networks = async function(req, res) {
 
+    req.session.host = `${req.params.host || 1}`;
+    
     let container_networks = [];
     let network_name = '';
 
@@ -30,7 +32,7 @@ export const Networks = async function(req, res) {
                 <td class="sort-city">${networks[i].Id}</td>
                 <td class="sort-score text-green">${status}</td>
                 <td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
-                <td class="text-end"><a class="btn" href="#">Details</a></td>
+                <td class=""><button class="badge badge-outline text-grey" id="" data-hx-get="/users/usersModals/user/" hx-target="#modal_content"  hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Details</button></td>
             </tr>`
             // Add the row to the network list
             network_list += details;

+ 5 - 40
controllers/preferences.js

@@ -1,21 +1,16 @@
-import { ServerSettings, User } from '../database/config.js';
+import { ServerSettings, User } from '../db/config.js';
 import { Alert, getLanguage, Navbar, Sidebar, Footer, Capitalize } from '../utils/system.js';
-import { readdirSync, readFileSync } from 'fs';
 
 export const Preferences = async function(req,res){
 
-    let language = await getLanguage(req);
+    let language = await getLanguage(req.session.userID);
     let Language = Capitalize(language);
     let selected = `<option value="${language}" selected hidden>${Language}</option>`;
 
     let user = await User.findOne({ where: { userID: req.session.userID }});
     let preferences = JSON.parse(user.preferences);
     let hide_profile = preferences.hide_profile;
-
-    let checked = '';
-    if (hide_profile == true) { checked = 'checked'; }
-
-
+    let checked = ''; if (hide_profile == true) { checked = 'checked'; }
 
     res.render("preferences",{ 
         alert: '',
@@ -36,46 +31,16 @@ export const submitPreferences = async function(req,res){
 
     let { language_input, hidden_input, check_languages } = req.body;
 
-    let trigger_name = req.header('hx-trigger-name');
-    let trigger_id = req.header('hx-trigger');
-
-    // console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`);
-    // console.log(req.body);
-
     if (hidden_input == 'on') { hidden_input = true; } else { hidden_input = false; }
 
     let user_preferences = {
-        language: language_input,
         hide_profile: hidden_input,
     };
 
     if (language_input != undefined && hidden_input != undefined) {
-        await User.update({ preferences: JSON.stringify(user_preferences) }, { where: { userID: req.session.userID }});
+        await User.update({ preferences: JSON.stringify(user_preferences), language: language_input }, { where: { userID: req.session.userID }});
     }
-
-    // [HTMX Triggered] Changes the update button.
-    if(trigger_id == 'preferences'){
-        res.send(`<button class="btn btn-success" hx-post="/preferences" hx-trigger="load delay:2s" hx-swap="outerHTML" id="submit" hx-target="#submit">Updated</button>`);
-        return;
-    } else if (trigger_id == 'submit'){
-        res.send(`<button class="btn btn-primary" id="submit" form="preferences">Update</button>`);
-        return;
-    }
-
-    let language = await getLanguage(req);
-    let Language = Capitalize(language);
-    let selected = `<option value="${language}" selected hidden>${Language}</option>`;
-
-    res.render("preferences",{
-        alert: '',
-        username: req.session.username,
-        role: req.session.role,
-        navbar: await Navbar(req),
-        sidebar: await Sidebar(req),
-        footer: await Footer(req),
-        selected: selected,
-    });
-
+    res.redirect('/preferences');
 }
 
 

+ 6 - 6
controllers/register.js

@@ -1,6 +1,6 @@
 import bcrypt from "bcrypt";
 import { Op } from "sequelize";
-import { User, ServerSettings, Permission, Syslog } from "../database/config.js";
+import { User, ServerSettings, Permission, Syslog } from "../db/config.js";
 
 
 export const Register = async function(req,res){
@@ -45,12 +45,12 @@ export const submitRegister = async function(req,res){
 
     else if (registration_secret && secret !== registration_secret) { 
         error = "Invalid secret";
-        await Syslog.create({ username: user.username, uniqueID: email, event: "Failed Registration", message: "Invalid Secret", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: user.username, uniqueID: email, event: "Failed Registration", message: "Invalid Secret", ip: req.ip });
     }
 
     else if (await User.findOne({ where: { [Op.or]: [{ username: username }, { email: email }] }})) { 
         error = "Username or email already exists"; 
-        await Syslog.create({ username: username, uniqueID: email, event: "Failed Registration", message: "Username or email already exists", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: username, uniqueID: email, event: "Failed Registration", message: "Username or email already exists", ip: req.ip });
     }
 
     if (error != '') { 
@@ -88,7 +88,7 @@ export const submitRegister = async function(req,res){
         email: email,
         password: bcrypt.hashSync(password, 10),
         role: await Role(),
-        preferences: JSON.stringify({ language: "english", hidden_profile: false }),
+        preferences: JSON.stringify({ hidden_profile: false }),
         lastLogin: new Date().toLocaleString(),
     });
 
@@ -100,13 +100,13 @@ export const submitRegister = async function(req,res){
         req.session.userID = user.userID;
         req.session.role = user.role;
         
-        await Syslog.create({ username: user.username, uniqueID: user.email, event: "Registration", message: "User created", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: user.username, uniqueID: user.email, event: "Registration", message: "User created", ip: req.ip });
 
         console.log(`User ${username} created`);
 
         res.redirect("/dashboard");
     } else {
-        await Syslog.create({ username: user.username, uniqueID: user.email, event: "Failed Registration", message: "Error. User not created", ip: req.socket.remoteAddress });
+        await Syslog.create({ username: user.username, uniqueID: user.email, event: "Failed Registration", message: "Error. User not created", ip: req.ip });
         res.render("register", { "error": "Error. User not created" });
     }
 }

+ 15 - 8
controllers/settings.js

@@ -1,9 +1,12 @@
-import { ServerSettings } from '../database/config.js';
-import { Alert, getLanguage, Navbar, Sidebar, Footer } from '../utils/system.js';
-import { read, readdirSync, readFileSync, writeFileSync } from 'fs';
+import { ServerSettings } from '../db/config.js';
+import { configureHost } from '../utils/docker.js';
+import { Alert, Navbar, Sidebar, Footer } from '../utils/system.js';
+import { readFileSync, writeFileSync } from 'fs';
 
 export const Settings = async function(req,res){
 
+    req.session.host = `${req.params.host || 1}`;
+
     let user_registration = await ServerSettings.findOne({ where: {key: 'user_registration'}});
     let registration_secret = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
 
@@ -68,7 +71,10 @@ export const Settings = async function(req,res){
 
 
 
-export const updateSettings = async function (req, res) {
+export const SettingsAction = async function (req, res) {
+
+    let action = req.params.action;
+    let id = req.params.id;
 
     let { user_registration, registration_secret, custom_link, link_url, authentication } = req.body;
     let { host2, tag2, ip2, port2 } = req.body;
@@ -144,12 +150,12 @@ export const updateSettings = async function (req, res) {
     }
 
 
-
     // Host 2
     if (host2) {
         let exists = await ServerSettings.findOne({ where: {key: 'host2'}});
         if (exists) { await ServerSettings.update({value: `${tag2},${ip2},${port2}`}, {where: {key: 'host2'}}); }
-        else { await ServerSettings.create({ key: 'host2', value: `${tag2},${ip2},${port2}`}); }   
+        else { await ServerSettings.create({ key: 'host2', value: `${tag2},${ip2},${port2}`}); }
+        configureHost(2, ip2, port2);
     } else if (!host2) {
         let exists = await ServerSettings.findOne({ where: {key: 'host2'}});
         if (exists) { await ServerSettings.update({value: ''}, {where: {key: 'host2'}}); }
@@ -161,6 +167,7 @@ export const updateSettings = async function (req, res) {
         let exists = await ServerSettings.findOne({ where: {key: 'host3'}});
         if (exists) { await ServerSettings.update({value: `${tag3},${ip3},${port3}`}, {where: {key: 'host3'}}); }
         else { await ServerSettings.create({ key: 'host3', value: `${tag3},${ip3},${port3}`}); }
+        configureHost(3, ip3, port3);
     } else if (!host3) {
         let exists = await ServerSettings.findOne({ where: {key: 'host3'}});
         if (exists) { await ServerSettings.update({value: ''}, {where: {key: 'host3'}}); }
@@ -172,15 +179,15 @@ export const updateSettings = async function (req, res) {
         let exists = await ServerSettings.findOne({ where: {key: 'host4'}});
         if (exists) { await ServerSettings.update({value: `${tag4},${ip4},${port4}`}, {where: {key: 'host4'}}); }
         else { await ServerSettings.create({ key: 'host4', value: `${tag4},${ip4},${port4}`}); }
+        configureHost(4, ip4, port4);
     } else if (!host4) {
         let exists = await ServerSettings.findOne({ where: {key: 'host4'}});
         if (exists) { await ServerSettings.update({value: ''}, {where: {key: 'host4'}}); }
         else { await ServerSettings.create({ key: 'host4', value: ''}); }
     }
 
-
     console.log('Settings updated');
-    res.send(`<button class="btn btn-success" hx-post="/settings" hx-trigger="load delay:2s" hx-swap="outerHTML" id="submit" hx-target="#submit">Updated</button>`);
+    res.send(`<button class="btn btn-success" hx-post="/settings/action/update_settings/0" hx-trigger="load delay:2s" hx-swap="outerHTML" id="submit" hx-target="#submit">Updated</button>`);
 }
 
 

+ 4 - 7
controllers/sponsors.js

@@ -1,11 +1,11 @@
-import { ServerSettings, User } from '../database/config.js';
+import { ServerSettings, User } from '../db/config.js';
 import { Alert, getLanguage, Navbar, Sidebar, Footer, Capitalize } from '../utils/system.js';
 import { readdirSync, readFileSync } from 'fs';
 import bcrypt from 'bcrypt';
 
 export const Sponsors = async function (req, res) {
-
-    let language = await getLanguage(req);
+    
+    let language = await getLanguage(req.session.userID);
     let Language = Capitalize(language);
     let selected = `<option value="${language}" selected hidden>${Language}</option>`;
 
@@ -13,10 +13,7 @@ export const Sponsors = async function (req, res) {
     let preferences = JSON.parse(user.preferences);
     let hide_profile = preferences.hide_profile;
 
-    let checked = '';
-    if (hide_profile == true) { checked = 'checked'; }
-
-
+    let checked = ''; if (hide_profile == true) { checked = 'checked'; }
 
     res.render("sponsors",{ 
         alert: '',

+ 4 - 2
controllers/syslogs.js

@@ -1,8 +1,10 @@
-import { Syslog } from '../database/config.js';
+import { Syslog } from '../db/config.js';
 import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
 
 export const Syslogs = async function(req, res) {
 
+    req.session.host = `${req.params.host || 1}`;
+    
     let logs = '';
 
     const syslogs = await Syslog.findAll({
@@ -38,7 +40,7 @@ export const Syslogs = async function(req, res) {
                     <td class="sort-message">${message}</td>
                     <td class="sort-ip">${log.ip}</td>
                     <td class="sort-timestamp">${datetime}</td>
-                    <td class="text-end"><a class="" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
+                    <td class=""><a class="" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
 
                 </tr>`
     }

+ 65 - 5
controllers/users.js

@@ -1,8 +1,11 @@
-import { ServerSettings, User } from '../database/config.js';
-import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
+import { User, Permission, ContainerLists, Container, ServerSettings } from '../db/config.js';
+import { Alert, Navbar, Footer } from '../utils/system.js';
+import { readFileSync } from 'fs';
 
 export const Users = async function(req,res){
 
+    req.session.host = `${req.params.host || 1}`;
+    
     let user_list = '';
 
     let allUsers = await User.findAll();
@@ -23,7 +26,7 @@ export const Users = async function(req,res){
             <td><input class="form-check-input" type="checkbox" name="select"></td>
             
             <td class="sort-id">${account.id}</td>
-            <td class="sort-avatar"><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
+            <td class="sort-avatar p-1"><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
             <td class="sort-name">${account.name}</td>
             <td class="sort-username">${account.username}</td>
             <td class="sort-email">${account.email}</td>
@@ -31,7 +34,7 @@ export const Users = async function(req,res){
             <td class="sort-role">${account.role}</td>
             <td class="sort-lastlogin">${account.lastLogin}</td>
             <td class="sort-active">${active}</td>
-            <td class="sort-action"><a href="#" class="btn">View</a></td>
+            <td class="sort-action"><button class="badge badge-outline text-grey" id="${account.username}" data-hx-get="/users/view/user/${account.userID}" hx-target="#modal_content"  hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">View</button></td>
         </tr>`
 
         user_list += info;
@@ -85,4 +88,61 @@ export const searchUsers = async function (req, res) {
     console.log(`[Search] ${req.body.search}`);
     res.send('ok');
     return;
-}
+}
+
+
+
+
+
+
+export const UsersView = async (req, res) => {
+
+    let view = req.params.view;
+    let userID = req.params.id;
+    let username = req.header('hx-trigger');
+
+    // console.log(`[view] ${view} - [userID] ${userID} - [username] ${username}`);
+
+    if (view == 'user') {
+        let user = await User.findOne({ where: { userID: userID } });
+        let modal = readFileSync('./views/partials/user.html', 'utf8');
+        modal = modal.replace(/Username/g, username);
+        modal = modal.replace(/USERID/g, user.userID);
+        modal = modal.replace(/FullName/g, user.name);
+        modal = modal.replace(/EmailAddress/g, user.email);
+        modal = modal.replace(/LastLogin/g, user.lastLogin);
+        modal = modal.replace(/CreatedAt/g, user.createdAt);
+        res.send(modal);
+        return;
+    }
+
+};
+
+
+export const UsersAction = async (req, res) => {
+
+    let action = req.params.action;
+    let userID = req.params.id;
+    let change = req.body.change;
+
+    console.log(`[action] ${action} [change] ${change} - [userID] ${userID}`);
+
+    if (change == 'remove') {
+        
+        let container_lists = await ContainerLists.findAll({ where: { userID: userID } });
+        container_lists.destroy();
+
+        let permissions = await Permission.findAll({ where: { userID: userID } });
+        permissions.forEach(async (permission) => {
+            await permission.destroy();
+        });
+
+        let user = await User.findOne({ where: { userID: userID } });
+        await user.destroy();
+
+        console.log(`User removed.`);
+    }
+
+    res.redirect('/users');
+
+};

+ 4 - 1
controllers/volumes.js

@@ -2,6 +2,9 @@ import { Alert, getLanguage, Navbar, Footer } from '../utils/system.js';
 import { volumeList, GetContainerLists } from '../utils/docker.js';
 
 export const Volumes = async function(req, res) {
+
+    req.session.host = `${req.params.host || 1}`;
+    
     let container_volumes = [];
     let volume_list = '';
 
@@ -43,7 +46,7 @@ export const Volumes = async function(req, res) {
             <td class="sort-score text-green">${status}</td>
             <td class="sort-date" data-date="1628122643">${volume.CreatedAt}</td>
             <td class="sort-quantity">MB</td>
-            <td class="text-end"><a class="btn" href="#">Details</a></td>
+            <td class=""><button class="badge badge-outline text-grey" id="" data-hx-get="/users/usersModals/user/" hx-target="#modal_content"  hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Details</button></td>
         </tr>`
     
         volume_list += row;    

+ 13 - 4
database/config.js → db/config.js

@@ -2,6 +2,7 @@ import session from 'express-session';
 import SessionSequelize from 'connect-session-sequelize';
 import { Sequelize, DataTypes} from 'sequelize';
 import { readFileSync } from 'fs';
+import { check_configured_hosts } from '../utils/docker.js';
 
 const SECURE = process.env.HTTPS || false;
 
@@ -9,7 +10,7 @@ const SECURE = process.env.HTTPS || false;
 const SequelizeStore = SessionSequelize(session.Store);
 const sessionData = new Sequelize('database', 'username', 'password', {
     dialect: 'sqlite',
-    storage: 'database/sessions.sqlite',
+    storage: 'data/sessions.sqlite',
     logging: false,
 });
 const SessionStore = new SequelizeStore({ db: sessionData });
@@ -29,7 +30,7 @@ export const sessionMiddleware = session({
 // Server settings
 const settings = new Sequelize('database', 'username', 'password', {
     dialect: 'sqlite',
-    storage: 'database/settings.sqlite',
+    storage: 'data/settings.sqlite',
     logging: false,
 });
 const SettingsDB = new SequelizeStore({ db: settings });
@@ -43,7 +44,6 @@ console.log(`\x1b[33mAuthor: ${package_info.author}\x1b[0m`);
 console.log(`\x1b[33mLicense: ${package_info.license}\x1b[0m`);
 console.log(`\x1b[33mDescription: ${package_info.description}\x1b[0m`);
 console.log('');
-
 // console.log in red
 console.log('\x1b[31m * Only Docker volumes are supported. No bind mounts.\n \x1b[0m');
 console.log('\x1b[31m * Breaking changes may require you to remove the DweebUI volume and start fresh. \n \x1b[0m');
@@ -53,7 +53,9 @@ try {
     await sessionData.authenticate();
     await settings.authenticate();
     sessionData.sync();
-    settings.sync();
+    settings.sync().then(() => {
+      check_configured_hosts();
+    });
     console.log(`\x1b[32mDatabase connection established.\x1b[0m`);
 } catch (error) {
     console.error('\x1b[31mDatabase connection failed:', error, '\x1b[0m');
@@ -85,6 +87,9 @@ export const User = settings.define('User', {
       type: DataTypes.STRING,
       allowNull: false
     },
+    status: {
+      type: DataTypes.STRING
+    },
     role: {
       type: DataTypes.STRING
     },
@@ -97,6 +102,10 @@ export const User = settings.define('User', {
     lastLogin: {
       type: DataTypes.STRING
     },
+    language: {
+      type: DataTypes.STRING,
+      defaultValue: 'english'
+    },
     preferences : {
       type: DataTypes.STRING
     },

+ 31 - 23
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "dweebui",
-  "version": "0.70.457",
+  "version": "0.70.474",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "dweebui",
-      "version": "0.70.457",
+      "version": "0.70.474",
       "license": "MIT",
       "dependencies": {
         "adm-zip": "^0.5.16",
@@ -15,13 +15,13 @@
         "dockerode": "^4.0.2",
         "dockerode-compose": "^1.4.0",
         "ejs": "^3.1.10",
-        "express": "^4.21.0",
-        "express-session": "^1.18.0",
+        "express": "^4.21.1",
+        "express-session": "^1.18.1",
         "multer": "^1.4.5-lts.1",
-        "sequelize": "^6.37.3",
+        "sequelize": "^6.37.5",
         "sqlite3": "^5.1.7",
         "systeminformation": "^5.23.5",
-        "yaml": "^2.5.1"
+        "yaml": "^2.6.0"
       }
     },
     "node_modules/@balena/dockerignore": {
@@ -618,9 +618,9 @@
       }
     },
     "node_modules/cookie": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
-      "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
       "engines": {
         "node": ">= 0.6"
       }
@@ -908,16 +908,16 @@
       }
     },
     "node_modules/express": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
-      "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+      "version": "4.21.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+      "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
       "dependencies": {
         "accepts": "~1.3.8",
         "array-flatten": "1.1.1",
         "body-parser": "1.20.3",
         "content-disposition": "0.5.4",
         "content-type": "~1.0.4",
-        "cookie": "0.6.0",
+        "cookie": "0.7.1",
         "cookie-signature": "1.0.6",
         "debug": "2.6.9",
         "depd": "2.0.0",
@@ -949,11 +949,11 @@
       }
     },
     "node_modules/express-session": {
-      "version": "1.18.0",
-      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
-      "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
       "dependencies": {
-        "cookie": "0.6.0",
+        "cookie": "0.7.2",
         "cookie-signature": "1.0.7",
         "debug": "2.6.9",
         "depd": "~2.0.0",
@@ -966,6 +966,14 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/express-session/node_modules/cookie-signature": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@@ -2245,9 +2253,9 @@
       }
     },
     "node_modules/sequelize": {
-      "version": "6.37.3",
-      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz",
-      "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==",
+      "version": "6.37.5",
+      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.5.tgz",
+      "integrity": "sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==",
       "funding": [
         {
           "type": "opencollective",
@@ -2876,9 +2884,9 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yaml": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
-      "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
+      "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
       "bin": {
         "yaml": "bin.mjs"
       },

+ 5 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "dweebui",
-  "version": "0.70.457",
+  "version": "0.70.474",
   "main": "server.js",
   "type": "module",
   "scripts": {
@@ -18,12 +18,12 @@
     "dockerode": "^4.0.2",
     "dockerode-compose": "^1.4.0",
     "ejs": "^3.1.10",
-    "express": "^4.21.0",
-    "express-session": "^1.18.0",
+    "express": "^4.21.1",
+    "express-session": "^1.18.1",
     "multer": "^1.4.5-lts.1",
-    "sequelize": "^6.37.3",
+    "sequelize": "^6.37.5",
     "sqlite3": "^5.1.7",
     "systeminformation": "^5.23.5",
-    "yaml": "^2.5.1"
+    "yaml": "^2.6.0"
   }
 }

+ 10 - 0
public/js/dweebui.js

@@ -41,3 +41,13 @@ function selectAll(group) {
     }
   }
 }
+
+
+function topScroll() {
+  window.scrollTo(0, 0);
+}
+
+
+function bottomScroll() {
+  window.scrollTo(0, document.body.scrollHeight);
+}

+ 29 - 35
router.js

@@ -3,25 +3,26 @@ export const router = express.Router();
 
 import { Login, submitLogin, Logout } from './controllers/login.js';
 import { Register, submitRegister } from './controllers/register.js';
-import { Dashboard, searchDashboard, ContainerAction, ServerMetrics, SSE, CardList, UpdateCard } from './controllers/dashboard.js';
+import { Dashboard, searchDashboard, ServerMetrics, SSE, DashboardView, DashboardAction } from './controllers/dashboard.js';
 import { Images, submitImages, searchImages } from './controllers/images.js';
 import { Volumes, submitVolumes, searchVolumes } from './controllers/volumes.js';
 import { Networks, NetworkAction, searchNetworks } from './controllers/networks.js';
-import { Apps, submitApps, searchApps } from './controllers/apps.js';
-import { Users, submitUsers, searchUsers } from './controllers/users.js';
+import { Apps, submitApps, searchApps, appsModals, } from './controllers/apps.js';
+import { Users, submitUsers, searchUsers, UsersView, UsersAction } from './controllers/users.js';
 import { Syslogs, searchSyslogs } from './controllers/syslogs.js';
 import { Account, searchAccount } from './controllers/account.js';
 import { Preferences, submitPreferences, searchPreferences } from './controllers/preferences.js';
-import { Settings, updateSettings, updateLanguages, searchSettings } from './controllers/settings.js';
+import { Settings, SettingsAction, updateLanguages, searchSettings } from './controllers/settings.js';
+import { Sponsors, searchSponsors } from './controllers/sponsors.js';
+import { Credits } from './controllers/credits.js';
 
 import { Install } from './utils/install.js';
 import { Uninstall } from './utils/uninstall.js';
 
-import { Sponsors, searchSponsors } from './controllers/sponsors.js';
-
-import { Credits } from './controllers/credits.js';
+import { sessionCheck, adminOnly, permissionCheck } from './utils/permissions.js';
 
-import { sessionCheck, adminOnly, permissionCheck, permissionModal, updatePermissions } from './utils/permissions.js';
+// router.get('*', (req, res, next) => { console.log(`[GET] ${req.url}`); next(); });
+// router.post('*', (req, res, next) => { console.log(`[POST] ${req.url}`); next(); });
 
 router.get('/login', Login);
 router.post('/login', submitLogin);
@@ -30,40 +31,33 @@ router.get('/register', Register);
 router.post('/register', submitRegister);
 
 router.get("/", sessionCheck, Dashboard);
-router.get("/:host?/dashboard", sessionCheck, Dashboard);
-router.get("/server_metrics", sessionCheck, ServerMetrics);
-
-router.get("/permission_modal", adminOnly, permissionModal);
-router.post("/update_permissions", adminOnly, updatePermissions);
 
+router.get("/dashboard", sessionCheck, Dashboard);
+router.get("/dashboard/view/:view/:id?", sessionCheck, DashboardView);
+router.post("/dashboard/action/:action/:id?", sessionCheck, DashboardAction);
+router.get("/server_metrics", sessionCheck, ServerMetrics);
 router.get("/sse", permissionCheck, SSE);
-router.get("/card_list", permissionCheck, CardList);
-router.get("/update_card/:containerid", permissionCheck, UpdateCard);
-
-router.post("/:host?/container/:action/:containerid?", permissionCheck, ContainerAction);
 
-router.get('/images', adminOnly, Images);
+router.get("/images", adminOnly, Images);
 router.post('/images', adminOnly, submitImages);
 
-router.get('/volumes', adminOnly, Volumes);
+router.get("/volumes", adminOnly, Volumes);
 router.post('/volumes', adminOnly, submitVolumes);
 
-router.get('/networks', adminOnly, Networks);
-router.post('/:host?/network/:action/:containerid?', adminOnly, NetworkAction);
+router.get("/networks", adminOnly, Networks);
+router.post('/network/:action/:containerid?', adminOnly, NetworkAction);
 
 router.get("/apps/:page?/:template?", adminOnly, Apps);
 router.post("/apps/:action?", adminOnly, submitApps);
-router.post("/install", adminOnly, Install);
-router.post("/uninstall", adminOnly, Uninstall);
 
-router.get('/users', adminOnly, Users);
-router.post('/users', adminOnly, submitUsers);
+router.get("/users", adminOnly, Users);
+router.get("/users/view/:view/:id?", adminOnly, UsersView);
+router.post("/users/action/:action/:id?", adminOnly, UsersAction);
 
 router.get('/syslogs', adminOnly, Syslogs);
 
 router.get('/settings', adminOnly, Settings);
-router.post('/settings', adminOnly, updateSettings);
-router.post('/update_languages', adminOnly, updateLanguages);
+router.post('/settings/action/:action?/:id?', adminOnly, SettingsAction);
 
 router.get('/preferences', sessionCheck, Preferences);
 router.post('/preferences', sessionCheck, submitPreferences);
@@ -77,6 +71,13 @@ router.get('/credits', sessionCheck, Credits);
 
 
 
+router.get("/appsModals/:modal?", adminOnly, appsModals);
+
+router.post("/install", adminOnly, Install);
+router.post("/uninstall", adminOnly, Uninstall);
+
+router.post('/update_languages', adminOnly, updateLanguages);
+
 
 router.post("/search", function (req, res) {
     // req.header('hx-current-url') == http://localhost:8000/dashboard
@@ -118,11 +119,4 @@ router.post("/search", function (req, res) {
             console.log(`[Search] ${req.body.search}`);
             res.send('ok');
     }
-});
-
-
-
-// router.get('*', (req, res) => {
-//     res.redirect('/dashboard');
-// });
-
+});

+ 2 - 1
server.js

@@ -1,12 +1,13 @@
 import express from 'express';
 import ejs from 'ejs';
 import { router } from './router.js';
-import { sessionMiddleware } from './database/config.js';
+import { sessionMiddleware } from './db/config.js';
 
 const app = express();
 const PORT = process.env.PORT || 8000;
 
 app.set('view engine', 'html');
+app.set('trust proxy', true);
 app.engine('html', ejs.renderFile);
 app.use([
     express.static('public'),

+ 108 - 83
utils/docker.js

@@ -1,55 +1,102 @@
 import Docker from 'dockerode';
 import { dockerContainerStats } from 'systeminformation';
-import { Container, ServerSettings } from '../database/config.js'
+import { Container, ServerSettings } from '../db/config.js'
+import stream from 'stream';
 
-// export var docker1 = new Docker();
-export var docker = new Docker();
+export var docker;
 var docker2;
 var docker3;
 var docker4;
 
-export async function GetContainerLists(hostid) {
+if (process.env.DOCKER_HOST && process.env.DOCKER_PORT) {
+    console.log('Connecting to Docker with environment variables.');
+    docker = new Docker({ host: process.env.DOCKER_HOST, port: process.env.DOCKER_PORT });
+    console.log('Docker host connected.');
+} else {
+    console.log('Connecting to default Docker host.');
+    docker = new Docker();
+    console.log('Docker host connected.');
+}
 
-    // key: host, value: `${tag3},${ip3},${port3}`
+export async function GetContainerLists(hostid) {
 
     let host = hostid || 1;
 
-    if (host == 1 || host == 0) {
-        let containers = await docker.listContainers({ all: true });
-        return containers;
+    let containers; 
+
+    if (host == 0) {
+        containers = await docker.listContainers({ all: true });
     }
 
-    if (host == 2 && !docker2) { 
-        let settings = await ServerSettings.findOne({ where: { key: 'host2' } });
-        let ip = settings.value.split(',')[1];
-        let port = settings.value.split(',')[2];
-        docker2 = new Docker({ host: ip, port: port });
-    } else if (host == 2 && docker2) { 
-        let containers = await docker2.listContainers({ all: true });
-        return containers;
+    if ((host == 0) && docker2) {
+        let containers2 = await docker2.listContainers({ all: true });
+        containers = containers.concat(containers2);
     }
 
-    if (host == 3 && !docker3) {
-        let settings = await ServerSettings.findOne({ where: { key: 'host3' } });
-        let ip = settings.value.split(',')[1];
-        let port = settings.value.split(',')[2];
-        docker3 = new Docker({ host: ip, port: port });
-    } else if (host == 3 && docker3) {
-        let containers = await docker3.listContainers({ all: true });
-        return containers;
+    if ((host == 0) && docker3) {
+        let containers3 = await docker3.listContainers({ all: true });
+        containers = containers.concat(containers3);
     }
 
-    if (host == 4 && !docker4) {
-        let settings = await ServerSettings.findOne({ where: { key: 'host4' } });
-        let ip = settings.value.split(',')[1];
-        let port = settings.value.split(',')[2];
-        docker4 = new Docker({ host: ip, port: port });
-    } else if (host == 4 && docker4) {
-        let containers = await docker4.listContainers({ all: true });
-        return containers;
+    if ((host == 0) && docker4) {
+        let containers4 = await docker4.listContainers({ all: true });
+        containers = containers.concat(containers4);
+    }
+
+    if (host == 1) {
+        containers = await docker.listContainers({ all: true });
+    }
+    
+    if (host == 2 && docker2) {
+        containers = await docker2.listContainers({ all: true });
     }
 
+    if (host == 3 && docker3) {
+        containers = await docker3.listContainers({ all: true });
+    }
 
+    if (host == 4 && docker4) {
+        containers = await docker4.listContainers({ all: true });
+    }
+
+    return containers;
+}
+
+
+
+export async function configureHost(hostid, ip, port) {
+
+    if (hostid == 2) {
+        docker2 = new Docker({ host: ip, port: port });
+        try {
+            let containers = await docker2.listContainers({ all: true });
+            console.log(`Host 2 connected. ${containers.length} containers found.`);
+        }
+        catch {
+            console.log('Host 2 connection failed.');
+            docker2;
+        }
+    } else if (hostid == 3) {
+        docker3 = new Docker({ host: ip, port: port });
+        try {
+            let containers = await docker3.listContainers({ all: true });
+            console.log(`Host 3 connected. ${containers.length} containers found.`);
+        }
+        catch {
+            console.log('Host 3 connection failed.');
+            docker3;
+        }
+    } else if (hostid == 4) {
+        docker4 = new Docker({ host: ip, port: port });
+        try {
+            let containers = await docker4.listContainers({ all: true });
+            console.log(`Host 4 connected. ${containers.length} containers found.`);
+        }
+        catch {
+            console.log('Host 4 connection failed.');
+            docker4;
+        }
+    }
 }
 
 export async function imageList() {
@@ -118,43 +165,31 @@ export async function containerInfo (containerID) {
     return container_info;
 }
 
-export async function containerLogs(containerID) {
 
+export async function containerLogs(containerID) {
     let container = docker.getContainer(containerID);
-
-    // Fetch logs from the container
-    const logs = await container.logs({
-        stdout: true,
-        stderr: true,
-        tail: 'all', // or specify a number for the number of lines
-    });
-    
+    const logs = await container.logs({ stdout: true, stderr: true, tail: 'all', });
     const logsString = logs.toString('utf8');
-
     return logsString;
 }
 
 
+let available_versions = '';
 async function version_check () {
-	// Fetch the data.
 	const resp = await fetch('https://registry.hub.docker.com/v2/namespaces/lllllllillllllillll/repositories/dweebui/tags/?page_size=10000');
-    // Parse the JSON.
     let hub = await resp.json();
-    console.log('Checking available versions...');
-    // Loop through the results.
     for (let i = 0; i < hub.results.length; i++) {
-        // Skip version tag if it includes a dash.
-        if (hub.results[i].name.includes('-')) { continue; }
-        console.log(hub.results[i].name);
+        available_versions += '| ' + hub.results[i].name + ' ';
     }
+    console.log('Available versions:');
+    console.log(available_versions);
 }
 version_check();
 
 
+// Creates then destroys a docker volume to trigger a docker event.
 export async function trigger_docker_event () {
-    // Create then destroy a docker volume.
-    let volume = await docker.createVolume({ Name: 'test_volume' });
-    console.log('Manually triggered docker event.');
+    let volume = await docker.createVolume({ Name: 'dweebui_test_volume' });
     setTimeout(async() => {
         await volume.remove();
     }, 200);
@@ -162,18 +197,17 @@ export async function trigger_docker_event () {
 
 
 export async function containerStats (containerID) {
-
     const stats = await dockerContainerStats(containerID);
-
     let info = {
         containerID: containerID,
         cpu: Math.round(stats[0].cpuPercent),
         ram: Math.round(stats[0].memPercent)
     }
-
     return info;
 }
 
+
+
 export async function removeNetwork(networkID) {
     let network = docker.getNetwork(networkID);
     await network.remove();
@@ -182,35 +216,26 @@ export async function removeNetwork(networkID) {
 
 
 
+export async function check_configured_hosts () {
 
-// Loop that runs every 5 seconds to update the container stats.
-export async function containerStatsLoop () {
-
-    let containers = await GetContainerLists(1);
-
-    for (let i = 0; i < containers.length; i++) {
-        let containerID = containers[i].Id;
-        let stats = await containerStats(containerID);
+    let [host2, created] = await ServerSettings.findOrCreate({ where: {key: 'host2'}, defaults: { key: 'host2', value: '' } });
+    if (host2.value != '') {
+        let [tag2, ip2, port2] = host2.value.split(',');
+        configureHost(2, ip2, port2);
+        console.log('Host 2 configured.');
+    }
 
-        let container = await Container.findOne({ where: { containerID: containerID } });
-        if (!container) {
-            container = await Container.create({
-                containerName: containers[i].Names[0].slice(1),
-                containerID: containerID,
-                cpu: JSON.stringify([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
-                ram: JSON.stringify([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
-            });
-        }
-        else {
-            let cpu = JSON.parse(container.cpu);
-            cpu.shift();
-            cpu.push(stats.cpu);
-            let ram = JSON.parse(container.ram);
-            ram.shift();
-            ram.push(stats.ram);
-            container.update({ cpu: JSON.stringify(cpu), ram: JSON.stringify(ram) });
-        }
+    let [host3, created3] = await ServerSettings.findOrCreate({ where: {key: 'host3'}, defaults: { key: 'host3', value: '' } });
+    if (host3.value != '') {
+        let [tag3, ip3, port3] = host3.value.split(',');
+        configureHost(3, ip3, port3);
+        console.log('Host 3 configured.');
     }
-}
-setInterval(containerStatsLoop, 5000);
 
+    let [host4, created4] = await ServerSettings.findOrCreate({ where: {key: 'host4'}, defaults: { key: 'host4', value: '' } });
+    if (host4.value != '') {
+        let [tag4, ip4, port4] = host4.value.split(',');
+        configureHost(4, ip4, port4);
+        console.log('Host 4 configured.');
+    }
+}

+ 193 - 164
utils/install.js

@@ -1,198 +1,227 @@
 import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "fs";
 import { execSync } from "child_process";
-import { Syslog } from "../database/config.js";
-import { docker } from "../utils/docker.js";
+import { Syslog } from "../db/config.js";
+import { docker } from "./docker.js";
 import DockerodeCompose from "dockerode-compose";
 import yaml from 'js-yaml';
-
+import { Alert } from "./system.js";
 
 
 export const Install = async (req, res) => {
 
-        let data = req.body;
-
-        let { name, service_name, image, command_check, command, net_mode, restart_policy } = data;        
-        let { port0, port1, port2, port3, port4, port5 } = data;
-        let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
-        let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
-        let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
-
-        let ports = [ port0, port1, port2, port3, port4, port5 ];
-        let volumes = [volume0, volume1, volume2, volume3, volume4, volume5];
-        let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11];
-        let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11];
-        
-        let docker_volumes = [];
-
-        // Make sure there isn't a container already running that has the same name
-        let containers = await docker.listContainers({ all: true });
-        for (let i = 0; i < containers.length; i++) {
-            if (containers[i].Names[0].includes(name)) {
-                // addAlert(req.session, 'danger', `App '${name}' already exists. Please choose a different name.`);
-                console.log(`App '${name}' already exists. Please choose a different name.`);
-                res.redirect('/');
-                return;
-            }
+    let data = req.body;
+    
+    let { name, service_name, image, command_check, command, net_mode, restart_policy } = data;        
+    let { port0, port1, port2, port3, port4, port5 } = data;
+    let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
+    let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
+    let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
+
+    let ports = [ port0, port1, port2, port3, port4, port5 ];
+    let volumes = [volume0, volume1, volume2, volume3, volume4, volume5];
+    let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11];
+    let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11];
+    
+    let docker_volumes = [];
+
+    // Make sure there isn't a container already running that has the same name
+    let containers = await docker.listContainers({ all: true });
+    for (let i = 0; i < containers.length; i++) {
+        if (containers[i].Names[0].includes(name)) {
+            console.log(`App '${name}' already exists. Please choose a different name.`);
+            let alert = Alert('danger', `App '${name}' already exists. Please choose a different name.`);
+            res.send(alert);
+            return;
         }
+    }
 
-        async function composeInstall (name, compose, req) {
-            try {
-                await compose.pull().then(() => {
-                    compose.up();
-
-                    Syslog.create({
-                        user: req.session.user,
-                        email: null,
-                        event: "App Installation",
-                        message: `${name} installed successfully`,
-                        ip: req.socket.remoteAddress
-                    }); 
-
-                });
-            } catch (err) {
-                await Syslog.create({
-                    user: req.session.user,
-                    email: null,
-                    event: "App Installation",
-                    message: `${name} installation failed: ${err}`,
-                    ip: req.socket.remoteAddress
-                });
-            }
-        }
 
-        // addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
-        console.log(`Installing ${name}. It should appear on the dashboard shortly.`);
-
-        // Compose file installation
-        if (req.body.compose) {
-            // Create the directory
-            mkdirSync(`./appdata/${name}`, { recursive: true });
-            // Write the form data to the compose file
-            writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) });
-            var compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`);
-            composeInstall(name, compose, req);
-            res.redirect('/');
-            return;
-        }
+    // async function composeInstall (name, compose, req) {
 
-        // Convert a JSON template into a compose file
-        let compose_file = `version: '3'`;
-            compose_file += `\nservices:`
-            compose_file += `\n  ${service_name}:`
-            compose_file += `\n    container_name: ${name}`;
-            compose_file += `\n    image: ${image}`;
-
-        // Command
-        if (command_check == 'on') { compose_file += `\n    command: ${command}` }
-
-        // Network mode
-        if (net_mode == 'host') { compose_file += `\n    network_mode: 'host'` } 
-        else if (net_mode != 'host' && net_mode != 'docker') { compose_file += `\n    network_mode: '${net_mode}'` }
-        
-        // Restart policy
-        if (restart_policy != '') { compose_file += `\n    restart: ${restart_policy}` }
-
-        // Ports
-        for (let i = 0; i < ports.length; i++) {
-            if ((ports[i] == 'on') && (net_mode != 'host')) {
-                compose_file += `\n    ports:`
-                break;
-            }
-        }
+    //     console.log('[composeInstall]');
 
-        for (let i = 0; i < ports.length; i++) {
-            if ((ports[i] == 'on') && (net_mode != 'host')) {
-                compose_file += `\n      - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
-            }
+    //     // try {
+    //     //     await compose.pull().then(() => {
+
+    //     //         compose.up();
+
+
+    //     //         Syslog.create({
+    //     //             user: req.session.user,
+    //     //             email: null,
+    //     //             event: "App Installation",
+    //     //             message: `${name} installed successfully`,
+    //     //             ip: req.socket.remoteAddress
+    //     //         }); 
+
+    //     //     });
+    //     // } catch (err) {
+    //     //     await Syslog.create({
+    //     //         user: req.session.user,
+    //     //         email: null,
+    //     //         event: "App Installation",
+    //     //         message: `${name} installation failed: ${err}`,
+    //     //         ip: req.socket.remoteAddress
+    //     //     });
+    //     // }
+
+    //     await compose.pull();
+    //     await compose.up();
+    //     console.log('compose.up');
+    // }
+
+
+
+
+    // Compose file installation
+    if (req.body.compose) {
+        // Create the directory
+        mkdirSync(`./appdata/${name}`, { recursive: true });
+        // Write the form data to the compose file
+        writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) });
+        var compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`);
+        composeInstall(name, compose, req);
+        res.redirect('/');
+        return;
+    }
+
+    // Convert a JSON template into a compose file
+    let compose_file = `version: '3'`;
+        compose_file += `\nservices:`
+        compose_file += `\n  ${service_name}:`
+        compose_file += `\n    container_name: ${name}`;
+        compose_file += `\n    image: ${image}`;
+
+    // Command
+    if (command_check == 'on') { compose_file += `\n    command: ${command}` }
+
+    // Network mode
+    if (net_mode == 'host') { compose_file += `\n    network_mode: 'host'` } 
+    else if (net_mode != 'host' && net_mode != 'docker') { compose_file += `\n    network_mode: '${net_mode}'` }
+    
+    // Restart policy
+    if (restart_policy != '') { compose_file += `\n    restart: ${restart_policy}` }
+
+    // Ports
+    for (let i = 0; i < ports.length; i++) {
+        if ((ports[i] == 'on') && (net_mode != 'host')) {
+            compose_file += `\n    ports:`
+            break;
         }
+    }
 
-        // Volumes
-        for (let i = 0; i < volumes.length; i++) {
-            if (volumes[i] == 'on') {
-                compose_file += `\n    volumes:`
-                break;
-            }
+    for (let i = 0; i < ports.length; i++) {
+        if ((ports[i] == 'on') && (net_mode != 'host')) {
+            compose_file += `\n      - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
         }
+    }
 
-        for (let i = 0; i < volumes.length; i++) {
-            // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config  )
-            if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
-                compose_file += `\n      - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-            }
-            // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
-            else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
-                let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
-                compose_file += `\n      - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-                docker_volumes.push(`${name}_${volume_name}`);
-            } 
+    // Volumes
+    for (let i = 0; i < volumes.length; i++) {
+        if (volumes[i] == 'on') {
+            compose_file += `\n    volumes:`
+            break;
         }
+    }
 
-        // Environment variables
-        for (let i = 0; i < env_vars.length; i++) {
-            if (env_vars[i] == 'on') {
-                compose_file += `\n    environment:`
-                break;
-            }
+
+    for (let i = 0; i < volumes.length; i++) {
+        // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config  )
+        if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
+            compose_file += `\n      - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
         }
-        for (let i = 0; i < env_vars.length; i++) {
-            if (env_vars[i] == 'on') {
-                compose_file += `\n      - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
-            }
+        // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
+        else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
+            let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
+            compose_file += `\n      - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
+            docker_volumes.push(`${name}_${volume_name}`);
+        } 
+    }
+
+    // Environment variables
+    for (let i = 0; i < env_vars.length; i++) {
+        if (env_vars[i] == 'on') {
+            compose_file += `\n    environment:`
+            break;
         }
-
-        // Labels
-        for (let i = 0; i < labels.length; i++) {
-            if (labels[i] == 'on') {
-                compose_file += `\n    labels:`
-                break;
-            }
+    }
+    for (let i = 0; i < env_vars.length; i++) {
+        if (env_vars[i] == 'on') {
+            compose_file += `\n      - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
         }
+    }
 
-        for (let i = 0; i < 12; i++) {
-            if (data[`label${i}`] == 'on') {
-                compose_file += `\n      - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
-            }
+    // Labels
+    for (let i = 0; i < labels.length; i++) {
+        if (labels[i] == 'on') {
+            compose_file += `\n    labels:`
+            break;
         }
+    }
 
-        // Privileged mode 
-        if (data.privileged == 'on') { compose_file += `\n    privileged: true` }
-
-        // Hardware acceleration
-        for (let i = 0; i < env_vars.length; i++) {
-            if ((env_vars[i] == 'on') && (data[`env_${i}_name`] == 'DRINODE')) {
-                compose_file += `\n    deploy:`
-                compose_file += `\n      resources:`
-                compose_file += `\n        reservations:`
-                compose_file += `\n          devices:`
-                compose_file += `\n          - driver: nvidia`
-                compose_file += `\n            count: 1`
-                compose_file += `\n            capabilities: [gpu]`
-                break;
-            }
+    for (let i = 0; i < 12; i++) {
+        if (data[`label${i}`] == 'on') {
+            compose_file += `\n      - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
         }
-
-        // add volumes to the compose file
-        if ( docker_volumes.length > 0 ) {
-            compose_file += `\n`
-            compose_file += `\nvolumes:`
-            // Removed any duplicates from docker_volumes
-            docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
-            for (let i = 0; i < docker_volumes.length; i++) {
-                if ( docker_volumes[i] != '') {
-                    compose_file += `\n  ${docker_volumes[i]}:`
-                }
+    }
+
+    // Privileged mode 
+    if (data.privileged == 'on') { compose_file += `\n    privileged: true` }
+
+    // Hardware acceleration
+    for (let i = 0; i < env_vars.length; i++) {
+        if ((env_vars[i] == 'on') && (data[`env_${i}_name`] == 'DRINODE')) {
+            compose_file += `\n    deploy:`
+            compose_file += `\n      resources:`
+            compose_file += `\n        reservations:`
+            compose_file += `\n          devices:`
+            compose_file += `\n          - driver: nvidia`
+            compose_file += `\n            count: 1`
+            compose_file += `\n            capabilities: [gpu]`
+            break;
+        }
+    }
+
+    // add volumes to the compose file
+    if ( docker_volumes.length > 0 ) {
+        compose_file += `\n`
+        compose_file += `\nvolumes:`
+        // Removed any duplicates from docker_volumes
+        docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
+        for (let i = 0; i < docker_volumes.length; i++) {
+            if ( docker_volumes[i] != '') {
+                compose_file += `\n  ${docker_volumes[i]}:`
             }
         }
-        
-        mkdirSync(`./appdata/${name}`, { recursive: true });
-        writeFileSync(`./appdata/${name}/compose.yaml`, compose_file, function (err) { console.log(err) });
-        var compose = new DockerodeCompose(docker, `./appdata/${name}/compose.yaml`, `${name}`);
-        composeInstall(name, compose, req);
+    }
     
-        
-    res.redirect('/');
+    mkdirSync(`./appdata/${name}`, { recursive: true });
+
+    writeFileSync(`./appdata/${name}/compose.yaml`, compose_file, function (err) { console.log(err) });
+
+    console.log(`Installing ${name}. It should appear on the dashboard shortly.`);
+
+    // composeInstall(name, compose, req);
+
+    var compose = new DockerodeCompose(docker, `./appdata/${name}/compose.yaml`, `${name}`);
+
+    (async () => {
+        console.log('Pulling image');
+        await compose.pull();
+        console.log('Starting container');
+        await compose.up();
+    })();
+
+    let alert = Alert('success', `Installing ${name}. It should appear on the dashboard shortly.`);
+
+    res.send(alert);
 }
 
+
+
+
+
+
 // im just going to leave this old stackfile snippet here for now
 
 // if (image.startsWith('https://')){

+ 1 - 88
utils/permissions.js

@@ -1,7 +1,6 @@
-import { Permission, User, Syslog } from "../database/config.js";
+import { Permission, User, Syslog } from "../db/config.js";
 import { readFileSync } from 'fs';
 import { Capitalize } from '../utils/system.js';
-import { trigger_docker_event } from "./docker.js";
 
 
 export const adminOnly = async (req, res, next) => {
@@ -15,8 +14,6 @@ export const adminOnly = async (req, res, next) => {
 
 
 export const sessionCheck = async (req, res, next) => {
-    let path = req.path;
-    // if (path != '/server_metrics') { console.log(`\x1b[90m ${req.session.username} ${path} \x1b[0m`); }
     if (req.session.userID) { next(); }
     else { res.redirect('/login'); }
 }
@@ -60,88 +57,4 @@ export const permissionCheck = async (req, res, next) => {
     } else {
         console.log(`User ${req.session.username} does NOT have permission for ${path}`);
     }
-}
-
-
-export const permissionModal = async (req, res) => {
-    let app_name = req.header('hx-trigger-name');
-    let app_id = req.header('hx-trigger');
-    let title = Capitalize(app_name);
-
-    let users = await User.findAll({ attributes: ['username', 'userID'] });
-
-    let modal_content =`<div class="modal-header">
-                            <h5 class="modal-title">${title} Permissions</h5>
-                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-                        </div>
-                        <div class="modal-body"><div class="accordion" id="accordion-example">`;
-
-    for (let i = 0; i < users.length; i++) {
-        if (users.length == 1) { modal_content += 'No other users.'; break; }
-        // Skip the admin user.
-        else if (i == 0) { continue; }
-        let exists = await Permission.findOne({ where: {containerID: app_id, userID: users[i].userID}});
-        if (!exists) { await Permission.create({ containerName: app_name, containerID: app_id, userID: users[i].userID, username: users[i].username}); }
-        let permissions = await Permission.findOne({ where: {containerID: app_id, userID: users[i].userID}});
-        let user_permissions = readFileSync('./views/partials/permissions.html', 'utf8');
-        if (permissions.uninstall == true && permissions.edit == true && permissions.upgrade == true && permissions.start == true && permissions.stop == true && permissions.pause == true && permissions.restart == true && permissions.logs == true && permissions.view == true) { user_permissions = user_permissions.replace(/data-AllCheck/g, 'checked'); }
-        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(/Entry/g, i);
-        user_permissions = user_permissions.replace(/Entry/g, i);
-        user_permissions = user_permissions.replace(/Entry/g, i);
-        user_permissions = user_permissions.replace(/container_id/g, app_id);
-        user_permissions = user_permissions.replace(/container_name/g, app_name);
-        user_permissions = user_permissions.replace(/user_id/g, users[i].userID);
-        user_permissions = user_permissions.replace(/Username/g, users[i].username);
-        modal_content += user_permissions;
-    }
-    modal_content += `</div></div>
-                    <div class="modal-footer">
-                        
-                        <form id="reset_permissions" class="me-auto">
-                            <input type="hidden" name="containerID" value="${app_id}">
-                            <button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" id="submit" hx-post="/update_permissions" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
-                        </form>
-
-                        <button type="button" class="btn" data-bs-dismiss="modal">Close</button>
-                    </div>`
-    res.send(modal_content);
-}
-
-
-export const updatePermissions = async (req, res) => {
-    let { containerID, containerName, userID, username, reset_permissions, select } = req.body;
-    let button_id = req.header('hx-trigger');
-    // Replaces the update button if it's been pressed.
-    if (button_id == 'confirmed') { res.send('<button class="btn" type="button" id="submit" hx-post="/update_permissions" hx-swap="outerHTML">Update  </button>'); return; }
-    // Reset all permissions for the container.
-    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 } }); trigger_docker_event(); return; }
-    // Make sure req.body[select] is an array
-    if (typeof req.body[select] == 'string') { req.body[select] = [req.body[select]]; }
-
-    await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerID: containerID, userID: userID } });
-    if (req.body[select]) {
-        for (let i = 0; i < req.body[select].length; i++) {
-            let permissions = req.body[select][i];
-            if (permissions == 'uninstall') { await Permission.update({ uninstall: true }, { where: {containerID: containerID, userID: userID}}); }  
-            if (permissions == 'edit') { await Permission.update({ edit: true }, { where: {containerID: containerID, userID: userID}}); }   
-            if (permissions == 'upgrade') { await Permission.update({ upgrade: true }, { where: {containerID: containerID, userID: userID}}); }   
-            if (permissions == 'start') { await Permission.update({ start: true }, { where: {containerID: containerID, userID: userID}}); }   
-            if (permissions == 'stop') { await Permission.update({ stop: true }, { where: {containerID: containerID, userID: userID}}); }   
-            if (permissions == 'pause') { await Permission.update({ pause: true }, { where: {containerID: containerID, userID: userID}}); }   
-            if (permissions == 'restart') { await Permission.update({ restart: true }, { where: {containerID: containerID, userID: userID}}); }   
-            if (permissions == 'logs') { await Permission.update({ logs: true }, { where: {containerID: containerID, userID: userID}}); }
-            if (permissions == 'view') { await Permission.update({ view: true }, { where: {containerID: containerID, userID: userID}}); }
-        }
-    }
-    trigger_docker_event();
-    res.send('<button class="btn" type="button" id="confirmed" hx-post="/update_permissions" hx-swap="outerHTML" hx-trigger="load delay:1s">Update ✔️</button>');
 }

+ 31 - 29
utils/system.js

@@ -1,4 +1,4 @@
-import { User, ServerSettings } from '../database/config.js';
+import { User, ServerSettings } from '../db/config.js';
 import { readFileSync } from 'fs';
 
 
@@ -6,15 +6,16 @@ import { readFileSync } from 'fs';
 // Navbar
 export async function Navbar (req) {
 
+    let userID = req.session.userID;
     let username = req.session.username;
+    let role = req.session.role;
+    let host = req.session.host;
 
-    let host = '' + req.session.host;
-
-    let language = await getLanguage(req);
+    let language = await getLanguage(userID);
 
     // Check if the user wants to hide their profile name.
-    if (req.session.userID != '00000000-0000-0000-0000-000000000000') { 
-        let user = await User.findOne({ where: { userID: req.session.userID }});
+    if (userID != '00000000-0000-0000-0000-000000000000') { 
+        let user = await User.findOne({ where: { userID: userID }});
         let preferences = JSON.parse(user.preferences);
         if (preferences.hide_profile == true) { username = 'Anon'; }
     }
@@ -32,30 +33,33 @@ export async function Navbar (req) {
     const [host3, created3] = await ServerSettings.findOrCreate({ where: { key: 'host3' }, defaults: { key: 'host3', value: '' }});
     const [host4, created4] = await ServerSettings.findOrCreate({ where: { key: 'host4' }, defaults: { key: 'host4', value: '' }});
 
-
     if (host2.value) { host2_toggle = 'checked'; [host2_tag, host2_ip, host2_port] = host2.value.split(','); }
     if (host3.value) { host3_toggle = 'checked'; [host3_tag, host3_ip, host3_port] = host3.value.split(','); }
     if (host4.value) { host4_toggle = 'checked'; [host4_tag, host4_ip, host4_port] = host4.value.split(','); }
     
-    let host_buttons = '';
+    let host_buttons = '<form action="/dashboard/action/switch_host/hostid" method="post">';
+    let nav_link = '';
 
-    if (host == '0') { host0_active = 'text-yellow'; }
+    if (host == '0') { host0_active = 'text-yellow'; nav_link = '/0'; }
     if (host == '1') { host1_active = 'text-yellow'; }
-    if (host == '2') { host2_active = 'text-yellow'; }
-    if (host == '3') { host3_active = 'text-yellow'; }
-    if (host == '4') { host4_active = 'text-yellow'; }
+    if (host == '2') { host2_active = 'text-yellow'; nav_link = '/2'; }
+    if (host == '3') { host3_active = 'text-yellow'; nav_link = '/3'; }
+    if (host == '4') { host4_active = 'text-yellow'; nav_link = '/4'; }
+
+    if (host2_toggle || host3_toggle || host4_toggle) { host_buttons += `<button type="submit" name="host" value="0" class="btn ${host0_active}" title="All">All</button>  <button type="submit" name="host" value="1" hx-swap="none" class="btn ${host1_active}" title="Host 1">Host 1</button>`; }
+    if (host2_toggle) { host_buttons += `<button type="submit" name="host" value="2" class="btn ${host2_active}" title="${host2_tag}">${host2_tag}</button>`; }
+    if (host3_toggle) { host_buttons += `<button type="submit" name="host" value="3" hx-swap="none" class="btn ${host3_active}" title="${host3_tag}">${host3_tag}</button>`; }
+    if (host4_toggle) { host_buttons += `<button type="submit" name="host" value="4" hx-swap="none" class="btn ${host4_active}" title="${host4_tag}">${host4_tag}</button>`; }
 
-    if (host2_toggle || host3_toggle || host4_toggle) { host_buttons += `<a href="/0/dashboard" class="btn ${host0_active}" title="All">All</a>  <a href="/1/dashboard" class="btn ${host1_active}" title="Host 1">Host 1</a>`; }
-    if (host2_toggle) { host_buttons += `<a href="/2/dashboard" class="btn ${host2_active}" title="${host2_tag}">${host2_tag}</a>`; }
-    if (host3_toggle) { host_buttons += `<a href="/3/dashboard" class="btn ${host3_active}" title="${host3_tag}">${host3_tag}</a>`; }
-    if (host4_toggle) { host_buttons += `<a href="/4/dashboard" class="btn ${host4_active}" title="${host4_tag}">${host4_tag}</a>`; }
+    host_buttons += '</form>';
 
     let navbar = readFileSync('./views/partials/navbar.html', 'utf8');
 
     if (language == 'english') {
         navbar = navbar.replace(/Username/g, username);
-        navbar = navbar.replace(/Userrole/g, req.session.role);
+        navbar = navbar.replace(/Userrole/g, role);
         navbar = navbar.replace(/HostButtons/g, host_buttons);
+        navbar = navbar.replace(/HOSTID/g, nav_link);
         return navbar;
     } else {
         let lang = readFileSync(`./languages/${language}.json`, 'utf8');
@@ -68,6 +72,7 @@ export async function Navbar (req) {
         navbar = navbar.replace(/Apps/g, lang.Apps);
         navbar = navbar.replace(/Users/g, lang.Users);
         navbar = navbar.replace(/Syslogs/g, lang.Syslogs);
+        navbar = navbar.replace(/HOSTID/g, nav_link);
 
         navbar = navbar.replace(/Search/g, lang.Search);
         navbar = navbar.replace(/Account/g, lang.Account);
@@ -76,9 +81,8 @@ export async function Navbar (req) {
         navbar = navbar.replace(/Settings/g, lang.Settings);
         navbar = navbar.replace(/Logout/g, lang.Logout);
 
-
         navbar = navbar.replace(/Username/g, username);
-        navbar = navbar.replace(/Userrole/g, req.session.role);
+        navbar = navbar.replace(/Userrole/g, role);
         navbar = navbar.replace(/HostButtons/g, host_buttons);
         return navbar;
     }
@@ -88,7 +92,7 @@ export async function Navbar (req) {
 // Sidebar
 export async function Sidebar (req) {
 
-    let language = await getLanguage(req);
+    let language = await getLanguage(req.session.userID);
 
     let sidebar = readFileSync('./views/partials/sidebar.html', 'utf8');
 
@@ -112,7 +116,7 @@ export async function Sidebar (req) {
 // Footer
 export async function Footer (req) {
 
-    let language = await getLanguage(req);
+    let language = await getLanguage(req.session.userID);
 
     let footer = readFileSync('./views/partials/footer.html', 'utf8');
 
@@ -152,17 +156,15 @@ export function Alert (type, message) {
 }
 
 
-export async function getLanguage (req) {
+export async function getLanguage (userID) {
 
-    // No userID if authentication is disabled.
-    if (req.session.userID == '00000000-0000-0000-0000-000000000000') { 
+    // Use the admin's language if authentication is disabled.
+    if (userID == '00000000-0000-0000-0000-000000000000') { 
         let user = await User.findOne({ where: { role: 'admin' }});
-        let preferences = JSON.parse(user.preferences);
-        return preferences.language;
+        return user.language;
     } else {
-        let user = await User.findOne({ where: { userID: req.session.userID }});
-        let preferences = JSON.parse(user.preferences);
-        return preferences.language;
+        let user = await User.findOne({ where: { userID: userID }});
+        return user.language;
     }
 }
 

+ 1 - 1
utils/uninstall.js

@@ -1,5 +1,5 @@
 import { docker } from "../utils/docker.js";
-import { Syslog } from "../database/config.js";
+import { Syslog } from "../db/config.js";
 
 
 export const Uninstall = async (req, res) => {

+ 16 - 22
views/apps.html

@@ -139,28 +139,6 @@
                 </div>
               </div>
 
-
-              <div class="modal medium-modal modal-blur fade" id="wide_modal" tabindex="-1" style="display: none;" aria-hidden="true">
-                <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
-                    <div class="modal-content" id="wide_modal_content">
-                    <!-- modal content inserted with htmx -->
-                    </div>
-                </div>
-              </div>
-        
-
-
-              <div class="modal slim-modal modal-blur fade" id="info_modal" tabindex="-1" style="display: none;" aria-hidden="true">
-                <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
-                  <div class="modal-content" id="info_modal_content">
-                    <!-- modal content inserted with htmx -->
-                  </div>
-                </div>
-              </div>
-
-
-
-              
             </div>
             <div class="d-flex mt-4">
               <ul class="pagination ms-auto">
@@ -190,6 +168,22 @@
       </div>
     </div>
 
+    <div class="modal medium-modal modal-blur fade" id="wide_modal" tabindex="-1" style="display: none;" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+          <div class="modal-content" id="wide_modal_content">
+          <!-- modal content inserted with htmx -->
+          </div>
+      </div>
+    </div>
+
+    <div class="modal slim-modal modal-blur fade" id="info_modal" tabindex="-1" style="display: none;" aria-hidden="true">
+      <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+        <div class="modal-content" id="info_modal_content">
+        </div>
+      </div>
+    </div>
+
+
 			<script src="/js/dweebui.js" defer></script>
 			<script src="/js/htmx.min.js"></script>
 

+ 14 - 3
views/dashboard.html

@@ -123,7 +123,9 @@
 					
 							<!-- HTMX -->
 							<div class="col-12">
-								<div class="row row-cards" hx-get="/card_list" data-hx-trigger="load, sse:update" data-hx-swap="afterbegin" hx-target="#containers"></div>
+
+								<div class="row row-cards" name="card_list" hx-get="/dashboard/view/card_list" data-hx-trigger="load, sse:update" data-hx-swap="afterbegin" hx-target="#containers"></div>
+								 
 							</div>
 
 						</div>
@@ -145,6 +147,7 @@
 				</div>
 			</div>
 		</div>
+
 		
 		<div class="modal medium-modal modal-blur fade" id="medium_modal" tabindex="-1" style="display: none;" aria-hidden="true">
 			<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
@@ -163,9 +166,17 @@
 		</div>
 
 
-		<script src="/libs/apexcharts/dist/apexcharts.min.js?1692870487" defer></script>
 
-		<script src="/js/dweebui.js" defer></script>
+		<script defer>
+			var modalScrollable = document.getElementById('wide_modal');
+			modalScrollable.addEventListener('shown.bs.modal', function () {
+			  modalScrollable.querySelector('.modal-body').scrollTop = modalScrollable.querySelector('.modal-body').scrollHeight;
+			});
+		</script>
+
+		<script src="/libs/apexcharts/dist/apexcharts.min.js?1692870487"></script>
+
+		<script src="/js/dweebui.js"></script>
 		<script src="/js/htmx.min.js"></script>
 		<script src="/js/htmx-sse.js"></script>
 

+ 1 - 1
views/images.html

@@ -51,8 +51,8 @@
                               <th><label class="table-sort" data-sort="sort-type">Tag</label></th>
                               <th><label class="table-sort" data-sort="sort-city">ID</label></th>
                               <th><label class="table-sort" data-sort="sort-score">Status</label></th>
-                              <th><label class="table-sort" data-sort="sort-date">Created</label></th>
                               <th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
+                              <th><label class="table-sort" data-sort="sort-date">Created</label></th>
                               <th><label class="table-sort" data-sort="sort-progress">Action</label></th>
                           </tr>
                         </thead>

+ 6 - 3
views/partials/app_card.html

@@ -23,19 +23,22 @@
         <div class="px-0">
           <div class="row align-items-center">
             <div class="col-auto">
-              <a href="#" class="btn" name="AppName" id="AppType" hx-post="/apps/info" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#wide_modal_content" data-bs-toggle="modal" data-bs-target="#wide_modal">
+              <a href="" class="btn" name="AppName" id="AppType" hx-get="/appsModals/info" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#info_modal_content" data-bs-toggle="modal" data-bs-target="#info_modal">
                 <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article mx-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
                   More Info
               </a>
             </div>
             <div class="col-auto ms-auto">
-              <a href="" class="btn" name="AppName" id="AppType" hx-post="/apps/view_install" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#wide_modal_content" data-bs-toggle="modal" data-bs-target="#wide_modal">
+
+              <button class="btn" name="AppName" id="AppType" hx-get="/appsModals/view_install" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#wide_modal_content" data-bs-toggle="modal" data-bs-target="#wide_modal">
                 <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down mx-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
                  Install  
-              </a>
+              </button>
+
             </div>
           </div>
         </div>
+        
 
       </div>
     </div>

+ 20 - 16
views/partials/container_card.html

@@ -1,4 +1,4 @@
-<div class="col-sm-6 col-lg-3" hx-get="/update_card/ContainerID" hx-trigger="sse:ContainerID" id="AltID" hx-swap="outerHTML" hx-target="#AltID" name="AppName">
+<div class="col-sm-6 col-lg-3" hx-get="/dashboard/view/update_card/ContainerID" hx-trigger="sse:ContainerID" id="AltID" hx-swap="outerHTML" hx-target="#AltID" name="AppName">
     <div class="card p-2">
         <div class="container-stamp">
             <img class="rounded-4 pt-1 px-1" width="95px" src="https://raw.githubusercontent.com/lllllllillllllillll/Dashboard-Icons/main/png/AppService.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
@@ -6,16 +6,16 @@
         <div class="d-flex align-items-center">
             <label style="font-size: smaller; font-weight: bold;" class="text-yellow">AppPorts</label>
             <div class="ms-auto lh-1">
-                <button class="container-action" title="Start" data-hx-post="/container/start/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Start" hx-swap="outerHTML">
+                <button class="container-action" title="Start" data-hx-post="/dashboard/action/start/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Start" id="AppName" hx-swap="outerHTML">
                     <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
                 </button>
-                <button class="container-action" title="Stop" data-hx-post="/container/stop/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Stop" hx-swap="outerHTML">
+                <button class="container-action" title="Stop" data-hx-post="/dashboard/action/stop/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Stop" id="AppName" hx-swap="outerHTML">
                     <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
                 </button>
-                <button class="container-action" title="Pause" data-hx-post="/container/pause/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Pause" hx-swap="outerHTML">
+                <button class="container-action" title="Pause" data-hx-post="/dashboard/action/pause/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Pause" id="AppName" hx-swap="outerHTML">
                     <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
                 </button>
-                <button class="container-action" title="Restart" data-hx-post="/container/restart/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Restart" hx-swap="outerHTML">
+                <button class="container-action" title="Restart" data-hx-post="/dashboard/action/restart/ContainerID" data-hx-trigger="mousedown" data-hx-target="#AltIDState" name="Restart" id="AppName" hx-swap="outerHTML">
                     <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>
 
@@ -23,20 +23,20 @@
                     <svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
                 </a>
                 <div class="dropdown-menu dropdown-menu-end">
-                    <button class="dropdown-item text-secondary" name="details" data-hx-post="/container/details/ContainerID" hx-swap="innerHTML" data-hx-trigger="mousedown" hx-target="#medium_modal_content" data-bs-toggle="modal" data-bs-target="#medium_modal">Details</button>
-                    <button class="dropdown-item text-secondary" name="AppName" data-hx-post="/container/logs/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content"  hx-swap="innerHTML"  data-bs-toggle="modal" data-bs-target="#wide_modal">Logs</button>
-                    <button class="dropdown-item text-secondary" name="edit" data-hx-post="/container/details/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#wide_modal">Edit</button>
+                    <button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/details/ContainerID" data-hx-trigger="mousedown" hx-target="#medium_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#medium_modal">Details</button>
+                    <button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/logs/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content"  hx-swap="innerHTML"  data-bs-toggle="modal" data-bs-target="#wide_modal">Logs</button>
+                    <button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/edit/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content" hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#wide_modal">Edit</button>
                     <button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
-                    <button class="dropdown-item text-danger" name="AppTitle" hx-trigger="mousedown" data-hx-post="/container/uninstall/ContainerID" hx-swap="innerHTML" data-hx-target="#modal_content"  data-bs-toggle="modal" data-bs-target="#scrolling_modal">Uninstall</button>
+                    <button class="dropdown-item text-danger" name="AppName" hx-trigger="mousedown" data-hx-get="/dashboard/view/uninstall/ContainerID" hx-swap="innerHTML" data-hx-target="#modal_content"  data-bs-toggle="modal" data-bs-target="#scrolling_modal">Uninstall</button>
                 </div>
                 
                 <a href="#" class="container-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                     <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
                 </a>
                 <div class="dropdown-menu dropdown-menu-end">
-                    <button class="dropdown-item text-secondary" data-hx-post="/container/hide/ContainerID" data-hx-trigger="mousedown" data-hx-swap="delete" data-hx-target="#AltID" name="AppName">Hide</button>
-                    <button class="dropdown-item text-secondary" name="AppTitle" hx-trigger="mousedown" data-hx-post="/container/link_modal/ContainerID" hx-swap="innerHTML" data-hx-target="#modal_content"  data-bs-toggle="modal" data-bs-target="#scrolling_modal">Edit Link</button>
-                    <button class="dropdown-item text-secondary" data-hx-get="/permission_modal" name="AppName" id="ContainerID" hx-target="#modal_content"  hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Permissions</button>
+                    <button class="dropdown-item text-secondary" name="AppName" data-hx-post="/dashboard/action/hide/ContainerID" data-hx-trigger="mousedown" data-hx-swap="delete" data-hx-target="#AltID">Hide</button>
+                    <button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/link_modal/ContainerID" hx-trigger="mousedown" hx-swap="innerHTML" data-hx-target="#modal_content"  data-bs-toggle="modal" data-bs-target="#scrolling_modal">Edit Link</button>
+                    <button class="dropdown-item text-secondary" name="AppName" data-hx-get="/dashboard/view/permissions/ContainerID" hx-target="#modal_content"  hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#scrolling_modal">Permissions</button>
                 </div>
                 
             </div>
@@ -52,7 +52,7 @@
 
         </div>
 
-        <div id="chart-AltID" class="chart-sm"></div>
+        <div id="AltIDchart" class="chart-sm"></div>
 
         <script>
             var options = {
@@ -100,8 +100,10 @@
                     type: 'datetime',
                 },
                 yaxis: {
+                    min: 0,
+                    max: 100,
                     labels: {
-                        padding: 4
+                        padding: 0,
                     },
                 },
                 labels: [
@@ -113,11 +115,13 @@
                 },
             };
 
-            var chart = new ApexCharts(document.querySelector("#chart-AltID"), options);
 
-            chart.render();
+            var AltIDchart = new ApexCharts(document.querySelector("#AltIDchart"), options);
+            AltIDchart.render();
 
         </script>
 
+        ChartTrigger
+
     </div>
 </div>

+ 40 - 31
views/partials/details.html

@@ -1,31 +1,37 @@
 
+
         <div class="modal-header">
             <h5 class="modal-title">Details</h5>
             <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
         </div>
+
+        
+
         <div class="modal-body">
             
-            <form action="/install" id="install" method="POST">
-            <div class="row mb-3 align-items-end">
-                <div class="col-lg-5">
-                    <label class="form-label">Container Name: </label>
-                    <input type="text" class="form-control" name="service_name" value="AppName" hidden/>
-                    <input type="text" class="form-control" name="name" value="AppName"/>
-                </div>
-                <div class="col-lg-4">
-                    <label class="form-label">Image: </label>
-                    <input type="text" class="form-control" name="image" value="AppImage"/>
-                </div>
-                <div class="col-lg-3">
-                    <label class="form-label">Restart Policy: </label>
-                    <select class="form-select" name="restart_policy">
-                        <option value="RestartPolicy" selected hidden>RestartPolicy</option>
-                        <option value="unless-stopped">unless-stopped</option>
-                        <option value="on-failure">on-failure</option>
-                        <option value="no">never</option>
-                        <option value="always">always</option>
-                    </select>
-                </div>
+            <form id="details">
+
+            
+                <div class="row mb-3 align-items-end">
+                    <div class="col-lg-5">
+                        <label class="form-label">Container Name: </label>
+                        <input type="text" class="form-control" name="service_name" value="AppName" hidden/>
+                        <input type="text" class="form-control" name="name" value="AppName"/>
+                    </div>
+                    <div class="col-lg-4">
+                        <label class="form-label">Image: </label>
+                        <input type="text" class="form-control" name="image" value="AppImage"/>
+                    </div>
+                    <div class="col-lg-3">
+                        <label class="form-label">Restart Policy: </label>
+                        <select class="form-select" name="restart_policy">
+                            <option value="RestartPolicy" selected hidden>RestartPolicy</option>
+                            <option value="unless-stopped">unless-stopped</option>
+                            <option value="on-failure">on-failure</option>
+                            <option value="no">never</option>
+                            <option value="always">always</option>
+                        </select>
+                    </div>
                 </div>
 
                 <label class="form-label">Network Mode</label>
@@ -219,7 +225,7 @@
                             <div class="accordion-body pt-0">
                                 <div class="row mb-1 align-items-end">
                                     <div class="col-auto">
-                                        <input class="form-check-input" name="volume_0_check" type="checkbox" Vol0Check>
+                                        <input class="form-check-input" name="volume0" type="checkbox" Vol0Check>
                                     </div>
                                     <div class="col">
                                         <input type="text" class="form-control" name="volume_0_bind" value="Vol0Source"/>
@@ -238,7 +244,7 @@
             
                                 <div class="row mb-1 align-items-end">
                                     <div class="col-auto">
-                                    <input class="form-check-input" name="volume_1_check" type="checkbox" Vol1Check>
+                                    <input class="form-check-input" name="volume1" type="checkbox" Vol1Check>
                                     </div>
                                     <div class="col">
                                     <input type="text" class="form-control" name="volume_1_bind" value="Vol1Source"/>
@@ -257,7 +263,7 @@
             
                                 <div class="row mb-1 align-items-end">
                                     <div class="col-auto">
-                                    <input class="form-check-input" name="volume_2_check" type="checkbox" Vol2Check>
+                                    <input class="form-check-input" name="volume2" type="checkbox" Vol2Check>
                                     </div>
                                     <div class="col">
                                     <input type="text" class="form-control" name="volume_2_bind" value="Vol2Source"/>
@@ -276,7 +282,7 @@
                     
                                 <div class="row mb-1 align-items-end">
                                     <div class="col-auto">
-                                    <input class="form-check-input" name="volume_3_check" type="checkbox" Vol3Check>
+                                    <input class="form-check-input" name="volume3" type="checkbox" Vol3Check>
                                     </div>
                                     <div class="col">
                                     <input type="text" class="form-control" name="volume_3_bind" value="Vol3Source"/>
@@ -295,7 +301,7 @@
                     
                                 <div class="row mb-1 align-items-end">
                                     <div class="col-auto">
-                                    <input class="form-check-input" name="volume_4_check" type="checkbox" Vol4Check>
+                                    <input class="form-check-input" name="volume4" type="checkbox" Vol4Check>
                                     </div>
                                     <div class="col">
                                     <input type="text" class="form-control" name="volume_4_bind" value="Vol4Source"/>
@@ -314,7 +320,7 @@
         
                                 <div class="row mb-1 align-items-end">
                                     <div class="col-auto">
-                                        <input class="form-check-input" name="volume_5_check" type="checkbox" Vol5Check>
+                                        <input class="form-check-input" name="volume5" type="checkbox" Vol5Check>
                                     </div>
                                     <div class="col">
                                         <input type="text" class="form-control" name="volume_5_bind" value="Vol5Source"/>
@@ -820,14 +826,17 @@
                     </div>
                 </div>
 
-
+            
 
             </div>
                         
-            </form>
 
+        </form>
         </div>
+        
         <div class="modal-footer">
             <button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
-            <button type="submit" class="btn btn-primary" form="install" value="Install">Install</button>
-        </div>
+            <button type="submit" class="btn btn-primary" form="install_info">Install</button>
+        </div>
+
+        

+ 8 - 12
views/partials/info.html

@@ -1,13 +1,9 @@
 
-    <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
-        <div class="modal-content">
-            <div class="modal-body">
-            <div class="modal-title">AppName</div>
-            <div>AppDesc</div>
-            </div>
-            <div class="modal-footer">
-            <button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
-            <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
-            </div>
-        </div>
-    </div>
+<div class="modal-body">
+    <div class="modal-title">AppTitle</div>
+    <div>AppDescription</div>
+</div>
+<div class="modal-footer">
+    <button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
+    <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
+</div>

+ 842 - 0
views/partials/install.html

@@ -0,0 +1,842 @@
+
+
+        <div class="modal-header">
+            <h5 class="modal-title">Details</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+        </div>
+
+        
+
+        <div class="modal-body">
+            
+            <form id="install_info" hx-post="/install" hx-swap="innerHTML" hx-target="#alert">
+
+            
+                <div class="row mb-3 align-items-end">
+                    <div class="col-lg-5">
+                        <label class="form-label">Container Name: </label>
+                        <input type="text" class="form-control" name="service_name" value="AppName" hidden/>
+                        <input type="text" class="form-control" name="name" value="AppName"/>
+                    </div>
+                    <div class="col-lg-4">
+                        <label class="form-label">Image: </label>
+                        <input type="text" class="form-control" name="image" value="AppImage"/>
+                    </div>
+                    <div class="col-lg-3">
+                        <label class="form-label">Restart Policy: </label>
+                        <select class="form-select" name="restart_policy">
+                            <option value="RestartPolicy" selected hidden>RestartPolicy</option>
+                            <option value="unless-stopped">unless-stopped</option>
+                            <option value="on-failure">on-failure</option>
+                            <option value="no">never</option>
+                            <option value="always">always</option>
+                        </select>
+                    </div>
+                </div>
+
+                <label class="form-label">Network Mode</label>
+                <div class="form-selectgroup-boxes row mb-3">
+                    <div class="col">
+                        <label class="form-selectgroup-item">
+                        <input type="radio" name="net_mode" value="host" class="form-selectgroup-input" NetHost>
+                        <span class="form-selectgroup-label d-flex align-items-center p-3">
+                            <span class="me-3">
+                            <span class="form-selectgroup-check"></span>
+                            </span>
+                            <span class="form-selectgroup-label-content">
+                            <span class="form-selectgroup-title strong mb-1">Host Network</span>
+                            <span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
+                            </span>
+                        </span>
+                        </label>
+                    </div>
+                    <div class="col">
+                        <label class="form-selectgroup-item">
+                        <input type="radio" name="net_mode" value="NetName" class="form-selectgroup-input" NetBridge>
+                        <span class="form-selectgroup-label d-flex align-items-center p-3">
+                            <span class="me-3">
+                            <span class="form-selectgroup-check"></span>
+                            </span>
+                            <span class="form-selectgroup-label-content">
+                            <span class="form-selectgroup-title strong mb-1">Bridge Network</span>
+                            <span class="d-block text-secondary">Containers can communicate using names.</span>
+                            </span>
+                        </span>
+                        </label>
+                    </div>
+                    <div class="col">
+                        <label class="form-selectgroup-item">
+                        <input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" NetDocker>
+                        <span class="form-selectgroup-label d-flex align-items-center p-3">
+                        <span class="me-3">
+                            <span class="form-selectgroup-check"></span>
+                        </span>
+                        <span class="form-selectgroup-label-content">
+                            <span class="form-selectgroup-title strong mb-1">Docker Network</span>
+                            <span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
+                        </span>
+                        </span>
+                        </label>
+                    </div>
+                </div>
+                
+                <div class="accordion" id="modal-accordion">
+                    <div class="accordion-item">
+                        <h2 class="accordion-header" id="heading-1">
+                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
+                            Ports
+                            </button>
+                        </h2>
+                        <div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
+                            <div class="accordion-body pt-0">
+        
+
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="port0" type="checkbox" Port0Check>
+                                    </div>
+                                    <div class="col">
+                                        <label class="form-label">External Port</label>
+                                    <input type="text" class="form-control" name="port_0_external" value="Port0External"/>
+                                    </div>
+                                    <div class="col">
+                                        <label class="form-label">Internal Port</label>
+                                    <input type="text" class="form-control" name="port_0_internal" value="Port0Internal"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                        <label class="form-label">Protocol</label>
+                                    <select class="form-select" name="port_0_protocol">
+                                        <option value="Port0Protocol" selected hidden>Port0Protocol</option>
+                                        <option value="tcp">tcp</option>
+                                        <option value="udp">udp</option>
+                                    </select>
+                                    </div>
+                                </div>
+            
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="port1" type="checkbox" Port1Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_1_external" value="Port1External"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_1_internal" value="Port1Internal"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="port_1_protocol">
+                                        <option value="Port1Protocol" selected hidden>Port1Protocol</option>
+                                        <option value="tcp">tcp</option>
+                                        <option value="udp">udp</option>
+                                    </select>
+                                    </div>
+                                </div>
+                
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="port2" type="checkbox" Port2Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_2_external" value="Port2External"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_2_internal" value="Port2Internal"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="port_2_protocol">
+                                        <option value="Port2Protocol" selected hidden>Port2Protocol</option>
+                                        <option value="tcp">tcp</option>
+                                        <option value="udp">udp</option>
+                                    </select>
+                                    </div>
+                                </div>
+
+                
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="port3" type="checkbox" Port3Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_3_external" value="Port3External"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_3_internal" value="Port3Internal"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="port_3_protocol">
+                                        <option value="Port3Protocol" selected hidden>Port3Protocol</option>
+                                        <option value="tcp">tcp</option>
+                                        <option value="udp">udp</option>
+                                    </select>
+                                    </div>
+                                </div>
+
+
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="port4" type="checkbox" Port4Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_4_external" value="Port4External"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_4_internal" value="Port4Internal"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="port_4_protocol">
+                                        <option value="Port4Protocol" selected hidden>Port4Protocol</option>
+                                        <option value="tcp">tcp</option>
+                                        <option value="udp">udp</option>
+                                    </select>
+                                    </div>
+                                </div>
+                
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="port5" type="checkbox" Port5Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_5_external" value="Port5External"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="port_5_internal" value="Port5Internal"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="port_5_protocol">
+                                        <option value="Port5Protocol" selected hidden>Port5Protocol</option>
+                                        <option value="tcp">tcp</option>
+                                        <option value="udp">udp</option>
+                                    </select>
+                                    </div>
+                                </div>
+            
+                            </div>
+                        </div>
+                    </div>
+                    <div class="accordion-item">
+                        <h2 class="accordion-header" id="heading-2">
+                            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
+                            Volumes
+                            </button>
+                        </h2>
+
+                        <div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
+
+                            <div class="accordion-body pt-0">
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                        <input class="form-check-input" name="volume0" type="checkbox" Vol0Check>
+                                    </div>
+                                    <div class="col">
+                                        <input type="text" class="form-control" name="volume_0_bind" value="Vol0Source"/>
+                                    </div>
+                                    <div class="col">
+                                        <input type="text" class="form-control" name="volume_0_container" value="Vol0Destination"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="volume_0_readwrite">
+                                        <option value="Vol0RW" selected hidden>Vol0RW</option>
+                                        <option value="rw">rw</option>
+                                        <option value="ro">ro</option>
+                                    </select>
+                                    </div>
+                                </div>
+            
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="volume1" type="checkbox" Vol1Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_1_bind" value="Vol1Source"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_1_container" value="Vol1Destination"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="volume_1_readwrite">
+                                        <option value="Vol1RW" selected hidden>Vol1RW</option>
+                                        <option value="rw">rw</option>
+                                        <option value="ro">ro</option>
+                                    </select>
+                                    </div>
+                                </div>
+            
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="volume2" type="checkbox" Vol2Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_2_bind" value="Vol2Source"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_2_container" value="Vol2Destination"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="volume_2_readwrite">
+                                        <option value="Vol2RW" selected hidden>Vol2RW</option>
+                                        <option value="rw">rw</option>
+                                        <option value="ro">ro</option>
+                                    </select>
+                                    </div>
+                                </div>
+                    
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="volume3" type="checkbox" Vol3Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_3_bind" value="Vol3Source"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_3_container" value="Vol3Destination"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="volume_3_readwrite">
+                                        <option value="Vol3RW" selected hidden>Vol3RW</option>
+                                        <option value="rw">rw</option>
+                                        <option value="ro">ro</option>
+                                    </select>
+                                    </div>
+                                </div>
+                    
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                    <input class="form-check-input" name="volume4" type="checkbox" Vol4Check>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_4_bind" value="Vol4Source"/>
+                                    </div>
+                                    <div class="col">
+                                    <input type="text" class="form-control" name="volume_4_container" value="Vol4Destination"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                    <select class="form-select" name="volume_4_readwrite">
+                                        <option value="Vol4RW" selected hidden>Vol4RW</option>
+                                        <option value="rw">rw</option>
+                                        <option value="ro">ro</option>
+                                    </select>
+                                    </div>
+                                </div>
+        
+                                <div class="row mb-1 align-items-end">
+                                    <div class="col-auto">
+                                        <input class="form-check-input" name="volume5" type="checkbox" Vol5Check>
+                                    </div>
+                                    <div class="col">
+                                        <input type="text" class="form-control" name="volume_5_bind" value="Vol5Source"/>
+                                    </div>
+                                    <div class="col">
+                                        <input type="text" class="form-control" name="volume_5_container" value="Vol5Destination"/>
+                                    </div>
+                                    <div class="col-lg-2">
+                                        <select class="form-select" name="volume_5_readwrite">
+                                        <option value="Vol5RW" selected hidden>Vol5RW</option>
+                                        <option value="rw">rw</option>
+                                        <option value="ro">ro</option>
+                                        </select>
+                                    </div>
+                                </div>
+
+                        </div>
+                    </div>
+                </div>
+                <div class="accordion-item">
+                    <h2 class="accordion-header" id="heading-3">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
+                        Environment Variables
+                        </button>
+                    </h2>
+                    <div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
+                        <div class="accordion-body pt-0">
+    
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="env_0_check" Env0Check>
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">Variable</label>
+                                    <input type="text" class="form-control" name="env_0_name" value="Env0Key"/>
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">Value</label>
+                                    <input type="text" class="form-control" name="env_0_default" value="Env0Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="env_1_check" Env1Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_1_name" value="Env1Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_1_default" value="Env1Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_2_check" Env2Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_2_name" value="Env2Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_2_default" value="Env2Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_3_check" Env3Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_3_name" value="Env3Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_3_default" value="Env3Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_4_check" Env4Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_4_name" value="Env4Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_4_default" value="Env4Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_5_check" Env5Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_5_name" value="Env5Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_5_default" value="Env5Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_6_check" Env6Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_6_name" value="Env6Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_6_default" value="Env6Value"/>
+                                </div>
+                            </div>
+            
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_7_check" Env7Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_7_name" value="Env7Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_7_default" value="Env7Value"/>
+                                </div>
+                            </div>
+            
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_8_check" Env8Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_8_name" value="Env8Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_8_default" value="Env8Value"/>
+                                </div>
+                            </div>
+            
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_9_check" Env9Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_9_name" value="Env9Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_9_default" value="Env9Value"/>
+                                </div>
+                            </div>
+            
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_10_check" Env10Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_10_name" value="Env10Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_10_default" value="Env10Value"/>
+                                </div>
+                            </div>
+            
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="env_11_check" Env11Check>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_11_name" value="Env11Key"/>
+                                </div>
+                                <div class="col">
+                                <input type="text" class="form-control" name="env_11_default" value="Env11Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="env_12_check" Env12Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_name" value="Env12Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_default" value="Env12Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="env_12_check" Env13Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_name" value="Env13Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_default" value="Env13Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="env_12_check" Env14Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_name" value="Env14Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_default" value="Env14Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="env_12_check" Env15Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_name" value="Env15Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="env_12_default" value="Env15Value"/>
+                                </div>
+                            </div>
+
+
+                            
+
+    
+                        </div>
+                    </div>
+                </div>
+                <div class="accordion-item">
+                    <h2 class="accordion-header" id="heading-4">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
+                        Labels
+                        </button>
+                    </h2>
+                    <div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
+                        <div class="accordion-body pt-0">
+    
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_0_check" Label0Check>
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">Variable</label>
+                                    <input type="text" class="form-control" name="label_0_name" value="Label0Key"/>
+                                </div>
+                                <div class="col">
+                                    <label class="form-label">Value</label>
+                                    <input type="text" class="form-control" name="label_0_value" value="Label0Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_1_check" Label1Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_1_name" value="Label1Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
+                                </div>
+                            </div>
+            
+                            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_2_check" Label2Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_2_name" value="Label2Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_3_check" Label3Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_3_name" value="Label3Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
+                                </div>
+                            </div>
+            
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_4_check" Label4Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_4_name" value="Label4Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_5_check" Label5Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_5_name" value="Label5Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_5_value" value="Label5Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_6_check" Label6Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_6_name" value="Label6Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_6_value" value="Label6Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_7_check" Label7Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_7_name" value="Label7Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_7_value" value="Label7Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_8_check" Label8Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_8_name" value="Label8Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_8_value" value="Label8Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_9_check" Label9Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_9_name" value="Label9Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_9_value" value="Label9Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_10_check" Label10Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_10_name" value="Label10Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_10_value" value="Label10Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                            <div class="col-auto">
+                                <input class="form-check-input" type="checkbox" name="label_11_check" Label11Check>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_11_name" value="Label11Key"/>
+                            </div>
+                            <div class="col">
+                                <input type="text" class="form-control" name="label_11_value" value="Label11Value"/>
+                            </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label12Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label12Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label12Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label13Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label13Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label13Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label14Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label14Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label14Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label15Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label15Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label15Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label16Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label16Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label16Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label17Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label17Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label17Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label18Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label18Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label18Value"/>
+                                </div>
+                            </div>
+
+                            <div class="row mb-1 align-items-end">
+                                <div class="col-auto">
+                                    <input class="form-check-input" type="checkbox" name="label_11_check" Label19Check>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_name" value="Label19Key"/>
+                                </div>
+                                <div class="col">
+                                    <input type="text" class="form-control" name="label_11_value" value="Label19Value"/>
+                                </div>
+                            </div>
+            
+    
+                        </div>
+                    </div>
+                </div>
+
+                <div class="accordion-item">
+                    <h2 class="accordion-header" id="heading-5">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
+                        Extras
+                        </button>
+                    </h2>
+                    <div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
+                        <div class="accordion-body pt-0">
+                    
+                
+                        </div>
+                    </div>
+                </div>
+
+            
+
+            </div>
+                        
+
+        </form>
+        </div>
+        
+        <div class="modal-footer">
+            <button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
+            <button type="submit" class="btn btn-primary" form="install_info" data-bs-dismiss="modal" onclick="topScroll()">Install</button>
+        </div>
+
+        

+ 3 - 3
views/partials/link.html

@@ -1,14 +1,14 @@
 
         <div class="modal-content" id="modal_content">
 
-            <form hx-post="/container/update_link/ContainerID" hx-swap="none">
+            <form action="/container/update_link/ContainerID" method="post">
 
                 <input type="hidden" name="service_id" value="ContainerID">
                 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 
                 <div class="modal-body text-center py-3">
                     <h3 class="mb-3">AppName Link</h3>
-                    <div class="text-muted mb-2">Enter URL</div>
+                    <div class="text-muted mb-2">URL needs to start with http:// or https://</div>
                     <input type="text" class="form-control mb-2" name="url" value="AppLink">
                 </div>
 
@@ -21,7 +21,7 @@
                                 </a>
                             </div>
                             <div class="col">
-                                <button class="btn btn-primary w-100" type="submit">Update&nbsp;&nbsp;</button>
+                                <input type="submit" value="Update" class="btn btn-primary w-100">
                             </div>
                         </div>
                     </div>

+ 5 - 11
views/partials/logs.html

@@ -4,9 +4,7 @@
             <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
         </div>
 
-        <div class="modal-body">
-            
-            <form action="/install" id="install" method="POST">
+        <div class="modal-body" id="modalBody">
 
             <div class="row mb-3 align-items-end">
                 <div class="col-lg-5">
@@ -23,11 +21,8 @@
                 <div class="col-lg-3">
                     <label class="form-label mb-1">Filter: </label>
                     <select class="form-select" name="restart_policy">
-                        <option value="RestartPolicy" selected hidden>24 Hours</option>
-                        <option value="unless-stopped">unless-stopped</option>
-                        <option value="on-failure">on-failure</option>
-                        <option value="no">never</option>
-                        <option value="always">always</option>
+                        <option value="All" selected hidden>All</option>
+                        <option value="All">All</option>
                     </select>
                 </div>
             </div>
@@ -36,14 +31,13 @@
 
             <div class="row mb-1 align-items-end">
                 <div class="col-12">
-                    <textarea class="form-control" style="height: 65vh; resize: none;" readonly>ContainerLogs</textarea>
+                    <textarea class="form-control" id="logs" style="height: 65vh; resize: none;" readonly>ContainerLogs</textarea>
                 </div>
             </div>
                         
-            </form>
         </div>
 
         <div class="modal-footer">
             <button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
-            <button type="submit" class="btn btn-success" name="AppName" data-hx-post="/container/logs/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content"  hx-swap="innerHTML"  data-bs-toggle="modal" data-bs-target="#wide_modal">Refresh</button>
+            <button type="submit" class="btn btn-success" name="AppName" data-hx-get="/dashboard/view/logs/ContainerID" data-hx-trigger="mousedown" hx-target="#wide_modal_content"  hx-swap="innerHTML"  data-bs-toggle="modal" data-bs-target="#wide_modal">Refresh</button>
         </div>

+ 4 - 6
views/partials/navbar.html

@@ -12,10 +12,8 @@
             </a>
           </h1>
 
-
-          <!-- <% if(alert) { %>
-            <%- alert %>
-          <% } %> -->
+          
+          <div id="alert"></div>
 
 
           <div class="navbar-nav flex-row order-md-last">
@@ -235,8 +233,8 @@
                     </a>
                     
                     <div class="dropdown-menu dropdown-menu-end">
-                      <form action="/container/reset/000" method="post">
-                        <button class="dropdown-item text-secondary" name="reset" id="reset" value="reset">Reset View</button>
+                      <form action="/dashboard/action/reset/000" method="post">
+                        <button class="dropdown-item text-secondary" name="reset">Reset View</button>
                       </form>
                     </div>
                     

+ 1 - 4
views/partials/permissions.html

@@ -18,10 +18,8 @@
                     <input type="hidden" name="username" value="Username">
                     <input type="hidden" name="select" value="selectEntry">
 
-
                     <div class="mb-3">
 
-
                         <div class="mb-3">
                             <label class="row">
                                 <span class="col">All</span>
@@ -39,7 +37,6 @@
 
                             </div>
 
-
                             <div>
                                 <label class="row">
                                     <span class="col">Uninstall</span>
@@ -147,7 +144,7 @@
                     </div>
 
                     <div class="row mb-2 pt-2">
-                        <button class="btn" type="button" id="submit" hx-post="/update_permissions" hx-swap="outerHTML">Update&nbsp;&nbsp;</button>
+                        <button class="btn" type="button" id="submit" hx-post="/dashboard/action/update_permissions/container_id" hx-swap="outerHTML">Update&nbsp;&nbsp;</button>
                     </div>
 
                 </div>

+ 84 - 0
views/partials/user.html

@@ -0,0 +1,84 @@
+
+    <form action="/users/action/change/USERID" method="post">
+
+        <div class="modal-header">
+            <h5 class="modal-title px-2">Username</h5>
+            <div class="me-auto badge badge-outline text-green">Active</div>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+        </div>
+        
+        <div class="modal-body py-2">
+
+            <div class="row mb-3 align-items-end">
+              <div class="col-auto">
+                <!-- <a href="#" class="avatar avatar-upload rounded">
+                  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 5l0 14"></path><path d="M5 12l14 0"></path></svg>
+                  <span class="avatar-upload-text">Add</span>
+                </a> -->
+                <span class="avatar avatar-upload rounded bg-green-lt">J</span>
+              </div>
+              <div class="col">
+                <label class="form-label">Full Name</label>
+                <input type="text" class="form-control" name="name" value="FullName">
+              </div>
+            </div>
+
+            <div class="col-12 mb-3">
+                <label class="mb-1">Email: </label>
+                <input type="text" class="form-control" name="email" value="EmailAddress"/>
+            </div>
+
+            <div class="col-12 mb-3">
+                <label class="mb-1">UserID: </label>
+                <input type="text" class="form-control text-secondary" name="userID" value="USERID" readonly/>
+            </div>        
+            
+            <div class="row mb-3">
+                <div class="col-lg-6">
+                    <label class="mb-1">Last Login: </label>
+                    <input type="text" class="form-control" name="last_login" value="LastLogin"/>
+                </div> 
+
+                <div class="col-lg-6">
+                    <label class="mb-1">Created: </label>
+                    <input type="text" class="form-control" name="created" value="CreatedAt"/>
+                </div> 
+            </div>
+
+
+            <div class="col mb-2">
+                <a href="#" class="btn w-100" data-bs-dismiss="modal">
+                    Reset Permissions
+                </a>
+            </div>
+
+        </div>
+
+
+        <div class="modal-footer">
+            <div class="w-100">
+                <div class="row">
+
+                    <div class="col">
+                        <a href="#" class="btn w-100" data-bs-dismiss="modal">
+                            Close
+                        </a>
+                    </div>
+
+                    <div class="col">
+                        <button type="submit" name="change" value="remove" class="btn btn-danger w-100" hx-confirm="Are you sure you want to remove this account?">Remove</button>
+                    </div>
+
+                    <div class="col">
+                        <button type="submit" name="change" value="disable" class="btn btn-secondary w-100" hx-confirm="Are you sure you want to disable this account?">Disable</button>
+                    </div>
+
+                    <div class="col">
+                        <button type="submit" name="change" value="update" class="btn btn-primary w-100" hx-confirm="Update account?">Update</button>
+                    </div>
+
+                </div>
+            </div>
+        </div>
+
+    </form>

+ 2 - 2
views/preferences.html

@@ -32,7 +32,7 @@
                 <div class="card-body">
 
                   <!-- HTMX - Submits the form and replaces the target with the response. Replaces the submit button with "Updated" -->
-                  <form id="preferences" hx-post="/preferences" hx-target="#submit" hx-swap="outerHTML">
+                  <form id="preferences" action="/preferences" method="POST">
 
                   <h1 class="">Preferences</h1>
                   <label class="text-muted mb-3">User Preferences.</label>
@@ -73,7 +73,7 @@
                       <a href="#" class="btn">
                         Cancel
                       </a>
-                      <button class="btn btn-primary" id="submit">
+                      <button class="btn btn-primary" id="submit" type="submit">
                         Update
                       </button>
                     </div>

+ 1 - 1
views/settings.html

@@ -32,7 +32,7 @@
                 <div class="card-body">
 
                   <!-- HTMX - Submits the form and replaces the target with the response. Replaces the submit button with "Updated" -->
-                  <form id="settings" hx-post="/settings" hx-target="#submit" hx-swap="outerHTML">
+                  <form id="settings" hx-post="/settings/action/update" hx-target="#submit" hx-swap="outerHTML">
 
                   <h1 class="">Settings</h1>
                   <label class="text-muted mb-3">Configure server settings. Admin only.</label>

+ 1 - 1
views/syslogs.html

@@ -101,7 +101,7 @@
                         </div>
 
                         <div class="modal-body text-center">
-                          <form method="post" action="/addVolume">
+                          <form method="post" action="/">
 
                               <div class="row g-2 align-items-end">
                                 <div class="col-9">

+ 32 - 31
views/users.html

@@ -28,22 +28,21 @@
 
               <div class="col-12 mt-12">
                 <div class="card">
-                  <form method="post">
-                    <div class="card-header">
+
+                  <div class="card-header">
                       <h3 class="card-title">Users</h3>
                         <div class="card-options btn-list">                  
                             <!-- <a href="#" class="btn">
                             <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
                               Refresh
                             </a> -->
+
                             <a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
-                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
-                              
+                              <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
                             </a>
                         </div>
                     </div>
     
-
                     <div id="table-default" class="table-responsive">
                       <table class="table">
                         <thead>
@@ -63,6 +62,7 @@
                         </thead>
                         <tbody class="table-tbody">
                           
+                          
                           <%- user_list %>
 
                         </tbody>
@@ -71,7 +71,7 @@
 
                     <div class="card-footer d-flex align-items-center">
 
-                        <button class="btn" type="submit" formaction="/removeVolume">Remove</button>
+                        <button class="btn" type="submit" formaction="/">Remove</button>
 
                       <!-- <span class="dropdown">
                         <button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
@@ -89,43 +89,44 @@
                       </span> -->
 
                                                         
-                      <p class="m-0 text-muted ms-auto">Events</p>
+                      <p class="m-0 text-muted ms-auto">Users</p>
 
                     </div>
-                  </form>
 
 
-                  <div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
+                  <!-- New User Modal -->
+                  <!-- <div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
                     <div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
                       <div class="modal-content">
                         <div class="modal-header">
-                            <h5 class="modal-title">New Volume</h5>
+                            <h5 class="modal-title">New User</h5>
                         </div>
-
                         <div class="modal-body text-center">
-                          <form method="post" action="/addVolume">
-
-                              <div class="row g-2 align-items-end">
-                                <div class="col-9">
-                                  <label class="form-label text-muted">Volume Name</label>
-                                  <input type="text" class="form-control" name="volume">
-                                </div>
-                                
-                                <div class="col-2">
-                                  <button type="submit" class="btn mt-2">Create</button>
-                                </div>
-                              </div>
-                                
-                                <label class="mt-3 text-muted"><label class="text-danger">*</label>Name cannot contain spaces or special characters.</label>
-                          </form>
+                          <div class="row g-2 align-items-end">
+                            <div class="col-9">
+                              <label class="form-label text-muted">User Name</label>
+                              <input type="text" class="form-control" name="name">
+                            </div>
+                            <div class="col-2">
+                              <button type="submit" class="btn mt-2">Create</button>
+                            </div>
+                          </div>
+                          <label class="mt-3 text-muted"><label class="text-danger">*</label>Name cannot contain spaces or special characters.</label>
                         </div>
-
                       </div>
                     </div>
-                  </div>
+                  </div> -->
 
 
+                  <div class="modal slim-modal modal-blur fade" id="scrolling_modal" tabindex="-1" style="display: none;" aria-hidden="true">
+                    <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+                      <div class="modal-content" id="modal_content">
+                        <!-- modal content inserted with htmx -->
+                      </div>
+                    </div>
+                  </div>
 
+                  
                 </div>
               </div>
 
@@ -140,9 +141,9 @@
     </div>
     
     <!-- Libs JS -->
-    <script src="/libs/list.js/dist/list.min.js" defer></script>
+    <script src="/libs/list.js/dist/list.min.js"></script>
 
-    <script src="/js/dweebui.js" ></script>
+    <script src="/js/dweebui.js"></script>
     <script src="/js/htmx.min.js"></script>
 
     <!-- Tabler Core -->
@@ -154,7 +155,7 @@
         const list = new List('table-default', {
           sortClass: 'table-sort',
           listClass: 'table-tbody',
-          valueNames: [ 'sort-id', 'sort-avatar', 'sort-name', 'sort-username', 'sort-email', 'sort-userid', 'sort-role', 'sort-lastlogin', 'sort-status', 'sort-action' ]
+          valueNames: [ 'sort-id', 'sort-avatar', 'sort-name', 'sort-username', 'sort-email', 'sort-userid', 'sort-role', 'sort-lastlogin', 'sort-status']
         });
       })
     </script>