Bläddra i källkod

Almost a complete rewrite, part 2.

lllllllillllllillll 10 månader sedan
förälder
incheckning
00c31f0fb6

+ 1 - 1
CHANGELOG.md

@@ -4,7 +4,6 @@
 * Updated adm-zip.
 * Updated yaml.
 * Pushed new docker image with 'latest' tag.
-* Updated compose.yaml volume to /app/config.
 * Fixed container card links.
 * Moved 'Reset view' button.
 * New - 'Grid view' and 'List view' button (non-functioning).
@@ -18,6 +17,7 @@
 * Fixed issue updating view permission.
 * Fixed issue viewing container logs.
 * App icons are now determined by service label instead of image name.
+* App icons sourced from new repo with 1000+ icons.
 
 ## v0.60 (June 9th 2024) - Permissions system and import templates
 * Converted JS template literals into HTML.

+ 272 - 52
controllers/dashboard.js

@@ -1,72 +1,250 @@
 import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
-import { containerList, containerInspect } from '../utils/docker.js';
+import { docker, getContainer, containerInspect } from '../utils/docker.js';
 import { readFileSync } from 'fs';
-import { User } from '../database/config.js';
-import { Alert, getLanguage, Navbar } from '../utils/system.js';
+import { User, Permission } from '../database/config.js';
+import { Alert, Navbar, Capitalize } from '../utils/system.js';
+import { Op } from 'sequelize';
 
-export const Dashboard = async function(req,res){
+let [ hidden, alert, newCards, stats ] = [ '', '', '', {} ];
+let logString = '';
 
-    let container_list = '';
+// Dashboard
+export const Dashboard = async function (req, res) {
 
-    let containers = await containerList();
-    for (let container of containers) {
-        let details = await containerInspect(container.containerID);
-        let container_card = readFileSync('./views/partials/container_card.html', 'utf8');
+    res.render("dashboard",{ 
+        alert: '',
+        username: req.session.username,
+        role: req.session.role,
+        navbar: await Navbar(req),
+    }); 
+}
 
-        if (details.name.length > 17) {
-            details.name = details.name.substring(0, 17) + '...';
-        }
+// Dashboard search
+export const submitDashboard = async function (req, res) {
+    console.log('[SubmitDashboard]');
+    console.log(req.body);
+    res.send('ok');
+    return;
+}
+
+
+export const CardList = async function (req, res) {
+
+    res.send(newCards);
+    newCards = '';
+    return;
+}
+
+
+async function containerInfo (containerID) {
 
-        // Capitalize the first letter of the name
-        details.name = details.name.charAt(0).toUpperCase() + details.name.slice(1);
-
-
-        let state = details.state;
-        let state_color = '';
-
-        switch (state) {
-            case 'running':
-                state_color = 'green';
-                break;
-            case 'exited':
-                state = 'stopped';
-                state_color = 'red';
-                break;
-            case 'paused':
-                state_color = 'orange';
-                break;
-            case 'installing':
-                state_color = 'blue';
-                break;
+    // get the container info
+    let info = docker.getContainer(containerID);
+    let container = await info.inspect();
+
+    let container_name = container.Name.slice(1);
+    let container_image = container.Config.Image;
+    let container_service = container.Config.Labels['com.docker.compose.service'];
+
+    let ports_list = [];
+    let external = 0;
+    let internal = 0;
+    
+    try {
+        for (const [key, value] of Object.entries(container.HostConfig.PortBindings)) {
+            let ports = {
+                check: 'checked',
+                external: value[0].HostPort,
+                internal: key.split('/')[0],
+                protocol: key.split('/')[1]
+            }
+            ports_list.push(ports);
         }
+    } catch {}
+
+    try { external = ports_list[0].external; internal = ports_list[0].internal; } catch { }
+
+    let container_info = {
+        containerName: container_name,
+        containerID: containerID,
+        containerImage: container_image,
+        containerService: container_service,
+        containerState: container.State.Status,
+        external_port: external,
+        internal_port: internal,
+        ports: ports_list,
+        volumes: container.Mounts,
+        env: container.Config.Env,
+        labels: container.Config.Labels,
+        link: 'localhost',
+    }
 
-        container_card = container_card.replace(/AppName/g, details.name);
-        container_card = container_card.replace(/AppService/g, details.service);
-        container_card = container_card.replace(/AppState/g, state);
-        container_card = container_card.replace(/StateColor/g, state_color);
+    return container_info;
+}
+
+
+async function userCards (session) {
+    session.container_list = [];
+    // check what containers the user wants hidden
+    let hidden = await Permission.findAll({ where: {userID: session.userID, hide: true}}, { attributes: ['containerID'] });
+    hidden = hidden.map((container) => container.containerID);
+    // check what containers the user has permission to view
+    let visable = await Permission.findAll({ where: { userID: session.userID, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] }, attributes: ['containerID'] });
+    visable = visable.map((container) => container.containerID);
+    // get all containers
+    let containers = await docker.listContainers({ all: true });
+    // loop through containers
+    for (let i = 0; i < containers.length; i++) {
+        let container_name = containers[i].Names[0].split('/').pop();
+        // skip hidden containers
+        if (hidden.includes(containers[i].Id)) { continue; }
+        // admin can see all containers that they don't have hidden
+        if (session.role == 'admin') { session.container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); }
+        // user can see any containers that they have any permissions for
+        else if (visable.includes(containers[i].Id)){ session.container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); }
+    }
+    // Create the lists if they don't exist
+    if (!session.sent_list) { session.sent_list = []; }
+    if (!session.update_list) { session.update_list = []; }
+    if (!session.new_cards) { session.new_cards = []; }
+}
 
-        if (details.external_port == 0 && details.internal_port == 0) {
-            container_card = container_card.replace(/AppPorts/g, ``);
+
+async function updateDashboard (session) {
+    let container_list = session.container_list;
+    let sent_list = session.sent_list;
+    session.new_cards = [];
+    session.update_list = [];
+    // loop through the containers list
+    container_list.forEach(container => {
+        let { containerName, containerID, containerState } = container;
+        let sent = sent_list.find(c => c.containerID === containerID);
+        if (!sent) { session.new_cards.push(containerID);}
+        else if (sent.containerState !== containerState) { session.update_list.push(containerID); }
+    });
+    // loop through the sent list to see if any containers have been removed
+    sent_list.forEach(container => {
+        let { containerName, containerID, containerState } = container;
+        let exists = container_list.find(c => c.containerID === containerID);
+        if (!exists) { session.update_list.push(containerID); }
+    });
+}
+
+
+// Container actions (start, stop, pause, restart, hide)
+export const ContainerAction = async (req, res) => {
+
+    // let trigger_id = req.header('hx-trigger');
+    let container_name = req.header('hx-trigger-name');
+    let containerID = req.params.containerid;
+    let action = req.params.action;
+    
+    console.log(`Container: ${container_name} ID: ${containerID} Action: ${action}`);
+
+    // Reset the view
+    if (action == 'reset') { 
+        console.log('Resetting view'); 
+        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
+        res.redirect('/dashboard');
+        return;
+    }
+
+    if (action == 'update') {
+        await userCards(req.session);
+        if (!req.session.container_list.find(c => c.containerID === containerID)) {
+            res.send('');
+            return;
         } else {
-            container_card = container_card.replace(/AppPorts/g, `${details.external_port}:${details.internal_port}`);
+            let details = await containerInfo(containerID);
+            let card = await createCard(details);
+            res.send(card);
+            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>`);
+    }
 
-        container_list += container_card;
+    // 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'); 
     }
+}
 
 
-    res.render("dashboard",{ 
-        alert: '',
-        username: req.session.username,
-        role: req.session.role,
-        container_list: container_list,
-        navbar: await Navbar(req),
-    }); 
+async function createCard (details) {
+    // let shortname = details.name.slice(0, 10) + '...';
+    // let trigger = 'data-hx-trigger="load, every 3s"';
+
+    // Capitalize the container name and shorten it if it's too long
+    let containerName = Capitalize(details.containerName);
+    if (containerName.length > 17) { containerName = containerName.substring(0, 17) + '...'; }
+
+    let containerID = details.containerID;
+    let containerState = details.containerState;
+    let containerService = details.containerService;
+    let containerStateColor = '';
+
+    if (containerState == 'running') { containerStateColor = 'green'; }
+    else if (containerState == 'exited') { containerStateColor = 'red'; containerState = 'stopped'; }
+    else if (containerState == 'paused') { containerStateColor = 'orange'; }
+    else { containerStateColor = 'blue'; }
+
+    let container_card = readFileSync('./views/partials/container_card.html', 'utf8');
+
+    // let links = await ServerSettings.findOne({ where: {key: 'links'}});
+    // if (!links) { links = { value: 'localhost' }; }
+
+    container_card = container_card.replace(/ContainerID/g, containerID);
+    container_card = container_card.replace(/AltID/g, 'a' + containerID);
+    container_card = container_card.replace(/AppName/g, containerName);
+    container_card = container_card.replace(/AppService/g, containerService);
+    container_card = container_card.replace(/AppState/g, containerState);
+    container_card = container_card.replace(/StateColor/g, containerStateColor);
+
+    if (details.external_port == 0 && details.internal_port == 0) {
+        container_card = container_card.replace(/AppPorts/g, ``);
+    } else {
+        container_card = container_card.replace(/AppPorts/g, `${details.external_port}:${details.internal_port}`);
+    }
+    // card = card.replace(/data-trigger=""/, trigger);
+    return container_card;
 }
 
 
 
+
 // Server metrics (CPU, RAM, TX, RX, DISK)
 export const ServerMetrics = async (req, res) => {
     let name = req.header('hx-trigger-name');
@@ -96,8 +274,50 @@ export const ServerMetrics = async (req, res) => {
 }
 
 
-export const submitDashboard = async function(req,res){
-    console.log(req.body);
-    res.send('ok');
-    return;
+export const SSE = async (req, res) => {
+
+    // Set the response headers
+    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
+
+    async function eventCheck () {
+        await userCards(req.session);
+        await updateDashboard(req.session);
+
+        if (JSON.stringify(req.session.sent_list) === JSON.stringify(req.session.container_list)) { console.log('Event - No Change'); return; }
+
+        console.log('Event - Change Detected');
+
+        for (let i = 0; i < req.session.new_cards.length; i++) {
+            let details = await containerInfo(req.session.new_cards[i]);
+            let card = await createCard(details);
+            newCards += card;
+        }
+
+        for (let i = 0; i < req.session.update_list.length; i++) {
+            res.write(`event: ${req.session.update_list[i]}\n`);
+            res.write(`data: 'update cards'\n\n`);
+        }
+        res.write(`event: update\n`);
+        res.write(`data: 'update cards'\n\n`);
+
+        req.session.sent_list = req.session.container_list.slice();
+    }
+
+    await eventCheck();
+
+    // Listens for docker events. Only triggers every other event.
+    docker.getEvents({}, function (err, data) {
+        let count = 0;
+        data.on('data', async function () {
+            count++;
+            if (count % 2 === 0) {
+                await eventCheck();
+            }
+        });
+    });
+
+
+    req.on('close', () => {
+    });
+
 }

+ 2 - 2
controllers/images.js

@@ -1,11 +1,11 @@
 import { Alert, getLanguage, Navbar } from '../utils/system.js';
-import { containerList, imageList } from '../utils/docker.js';
+import { imageList } from '../utils/docker.js';
 
 export const Images = async function(req,res){
 
     let container_images = [];
 
-    let containers = await containerList();
+    let containers = await containerList(req);
     for (let i = 0; i < containers.length; i++) {
         container_images.push(containers[i].Image);
     }

+ 31 - 6
controllers/login.js

@@ -1,14 +1,39 @@
 import bcrypt from 'bcrypt';
-import { User, Syslog } from '../database/config.js';
+import { User, Syslog, ServerSettings } from '../database/config.js';
 
 
-export const Login = function(req,res){
-    if (req.session.userID) { res.redirect("/dashboard"); }
-    else { res.render("login",{ 
+
+// Login page
+export const Login = async function(req,res){
+
+    if (req.session.userID) { res.redirect("/dashboard"); return; }
+
+    let authentication = await ServerSettings.findOne({ where: { key: 'authentication' }});
+    if (!authentication) { await ServerSettings.create({ key: 'authentication', value: 'default' }); }
+    authentication = await ServerSettings.findOne({ where: { key: 'authentication' }});
+
+    if (authentication.value == 'localhost' && req.hostname == 'localhost') {
+        req.session.username = 'Localhost';
+        req.session.userID = '00000000-0000-0000-0000-000000000000';
+        req.session.role = 'admin';
+        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';
+        res.redirect("/dashboard");
+        return;
+    }
+
+    res.render("login",{ 
         "error":"", 
-    }); }
+    });
 }
 
+
+
+// Submit login
 export const submitLogin = async function(req,res){
     const { password } = req.body;
     let email = req.body.email.toLowerCase();
@@ -27,9 +52,9 @@ export const submitLogin = async function(req,res){
         req.session.role = user.role;
         res.redirect("/dashboard");
     }
-  
 }
 
+// Logout
 export const Logout = function(req,res){
     req.session.destroy(() => {
         res.redirect("/login");

+ 10 - 7
controllers/register.js

@@ -1,6 +1,6 @@
 import bcrypt from "bcrypt";
 import { Op } from "sequelize";
-import { User, ServerSettings } from "../database/config.js";
+import { User, ServerSettings, Permission } from "../database/config.js";
 
 
 export const Register = async function(req,res){
@@ -8,19 +8,21 @@ export const Register = async function(req,res){
     // Redirect to dashboard if user is already logged in.
     if (req.session.username) { res.redirect("/dashboard"); }
 
+
+    let user_registration = await ServerSettings.findOne({ where: { key: 'user_registration' }});
+
     let secret_input = '';
-    let registration_secret = await ServerSettings.findOne({ where: { key: 'registration' }}).value;
 
     // Input field for secret if one has been set.
-    if (registration_secret) {
+    if (user_registration) {
         secret_input = `<div class="mb-3"><label class="form-label">Secret</label>
                                 <div class="input-group input-group-flat">
-                                    <input type="text" class="form-control" autocomplete="off" name="secret">
+                                    <input type="text" class="form-control" autocomplete="off" name="registration_secret">
                                 </div>
                             </div>`}
 
-    // If there are no users, or a registration secret has not been set, display the registration page.
-    if ((await User.count() == 0) || (registration_secret == '')) {
+    // If there are no users, or registration has been enabled, display the registration page.
+    if ((await User.count() == 0) || (user_registration.value == true)) {
         res.render("register",{ 
             "error": "",
             "reg_secret": secret_input,
@@ -37,7 +39,7 @@ export const submitRegister = async function(req,res){
     const { name, username, password, confirm, secret } = req.body;
     let email = req.body.email.toLowerCase();
 
-    let registration_secret = await ServerSettings.findOne({ where: { key: 'registration' }}).value;
+    let registration_secret = await ServerSettings.findOne({ where: { key: 'registration_secret' }}).value;
 
     let error = '';
     if (!name || !username || !email || !password || !confirm) { error = "All fields are required"; } 
@@ -72,6 +74,7 @@ export const submitRegister = async function(req,res){
     let match = await bcrypt.compare(password, user.password);
     if (match) {
         console.log(`User ${username} created`);
+
         req.session.username = user.username;
         req.session.userID = user.userID;
         req.session.role = user.role;

+ 136 - 21
controllers/settings.js

@@ -3,48 +3,163 @@ import { Alert, getLanguage, Navbar } from '../utils/system.js';
 
 export const Settings = async function(req,res){
 
-    let container_links = await ServerSettings.findOne({ where: {key: 'container_links'}});
+    let custom_link = await ServerSettings.findOne({ where: {key: 'custom_link'}});
+    let link_url = await ServerSettings.findOne({ where: {key: 'link_url'}});
+
     let user_registration = await ServerSettings.findOne({ where: {key: 'user_registration'}});
+    let registration_secret = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
+
+    let authentication = await ServerSettings.findOne({ where: {key: 'authentication'}});
+    
+    let custom_link_enabled = '';
+    try { if (custom_link.value == true) { custom_link_enabled = 'checked'; } } catch { console.log('Custom Link: No Value Set'); }
+
+    let user_registration_enabled = '';
+    try { if (user_registration.value == true) { user_registration_enabled = 'checked'; } } catch { console.log('User Registration: No Value Set'); }
+
+    let link_url_value = '';
+    try { link_url_value = link_url.value; } catch { console.log('Link URL: No Value Set'); }
+
+    let registration_secret_value = '';
+    try { registration_secret_value = registration_secret.value; } catch { console.log('Registration Secret: No Value Set'); }
 
 
     res.render("settings",{ 
         alert: '',
         username: req.session.username,
         role: req.session.role,
-        user_registration: 'checked',
-        registration_secret: 'some-long-secret',
-        container_links: 'checked',
-        link_url: 'mydomain.com',
+        user_registration: user_registration_enabled,
+        registration_secret: registration_secret_value,
+        custom_link: custom_link_enabled,
+        link_url: link_url_value,
+        authentication: authentication.value,
         navbar: await Navbar(req),
     });
 }
 
 
 
-export const submitSettings = async function(req,res){
+export const updateSettings = async function (req, res) {
 
-    console.log(req.body);
+    let { user_registration, registration_secret, custom_link, link_url, authentication } = req.body;
+    let { host2, tag2, ip2, port2 } = req.body;
+    let { host3, tag3, ip3, port3 } = req.body;
+    let { host4, tag4, ip4, port4 } = 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}`);
-
-
-    // [HTMX Triggered] Changes the update button.
-    if(trigger_id == 'settings'){
-        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>`);
-        return;
-    } else if (trigger_id == 'submit'){
+    // If the trigger is 'submit', return the button
+    if (trigger_id == 'submit'){
         res.send(`<button class="btn btn-primary" id="submit" form="settings">Update</button>`);
         return;
     }
 
-    res.render("settings",{
-        alert: '',
-        username: req.session.username,
-        role: req.session.role,
-        navbar: await Navbar(req),
-    });
+    // Continues on if the trigger is 'settings
+
+    // Custom link
+    if (custom_link) {
+        let exists = await ServerSettings.findOne({ where: {key: 'custom_link'}});
+        if (exists) { await ServerSettings.update({value: true}, {where: {key: 'custom_link'}}); }
+        else { await ServerSettings.create({ key: 'custom_link', value: true}); }
+
+        let exists2 = await ServerSettings.findOne({ where: {key: 'link_url'}});
+        if (exists2) { await ServerSettings.update({value: link_url}, {where: {key: 'link_url'}}); }
+        else { await ServerSettings.create({ key: 'link_url', value: link_url}); }
+
+        console.log('Custom link enabled');
+
+    } else if (!custom_link) {
+        let exists = await ServerSettings.findOne({ where: {key: 'custom_link'}});
+        if (exists) { await ServerSettings.update({value: false}, {where: {key: 'custom_link'}}); }
+        else { await ServerSettings.create({ key: 'custom_link', value: false}); }
+
+        let exists2 = await ServerSettings.findOne({ where: {key: 'link_url'}});
+        if (exists2) { await ServerSettings.update({value: ''}, {where: {key: 'link_url'}}); }
+        else { await ServerSettings.create({ key: 'link_url', value: ''}); }
+
+        console.log('Custom links off');
+    }
+
+    // User registration
+    if (user_registration) {
+        let exists = await ServerSettings.findOne({ where: {key: 'user_registration'}});
+        if (exists) { const setting = await ServerSettings.update({value: true}, {where: {key: 'user_registration'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'user_registration', value: true}); }
+
+        let exists2 = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
+        if (exists2) { await ServerSettings.update({value: registration_secret}, {where: {key: 'registration_secret'}}); }
+        else { await ServerSettings.create({ key: 'registration_secret', value: registration_secret}); }
+
+        console.log('registration on');
+
+    } else if (!user_registration) {
+        let exists = await ServerSettings.findOne({ where: {key: 'user_registration'}});
+        if (exists) { await ServerSettings.update({value: false}, {where: {key: 'user_registration'}}); }
+        else { await ServerSettings.create({ key: 'user_registration', value: false}); }
+
+        let exists2 = await ServerSettings.findOne({ where: {key: 'registration_secret'}});
+        if (exists2) { await ServerSettings.update({value: ''}, {where: {key: 'registration_secret'}}); }
+        else { await ServerSettings.create({ key: 'registration_secret', value: ''}); }
+        
+        console.log('registration off');
+    }
+
+    // Authentication
+    if (authentication) {
+        let exists = await ServerSettings.findOne({ where: {key: 'authentication'}});
+        if (exists) { await ServerSettings.update({value: authentication}, {where: {key: 'authentication'}}); }
+        else { await ServerSettings.create({ key: 'authentication', value: authentication}); }
+        console.log('Authentication on');
+    } else if (!authentication) {
+        let exists = await ServerSettings.findOne({ where: {key: 'authentication'}});
+        if (exists) { await ServerSettings.update({value: 'default'}, {where: {key: 'authentication'}}); }
+        else { await ServerSettings.create({ key: 'authentication', value: 'off'}); }
+        console.log('Authentication off');
+    }
+
+
+
+    // Host 2
+    if (host2) {
+        let exists = await ServerSettings.findOne({ where: {key: 'host2'}});
+        if (exists) { const setting = await ServerSettings.update({value: `${tag2},${ip2},${port2}`}, {where: {key: 'host2'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'host2', value: `${tag2},${ip2},${port2}`}); }   
+        console.log('host2 on');
+    } else if (!host2) {
+        let exists = await ServerSettings.findOne({ where: {key: 'host2'}});
+        if (exists) { const setting = await ServerSettings.update({value: ''}, {where: {key: 'host2'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'host2', value: ''}); }
+        console.log('host2 off');
+    }
+
+    // // Host 3
+    if (host3) {
+        let exists = await ServerSettings.findOne({ where: {key: 'host3'}});
+        if (exists) { const setting = await ServerSettings.update({value: `${tag3},${ip3},${port3}`}, {where: {key: 'host3'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'host3', value: `${tag3},${ip3},${port3}`}); }
+        console.log('host3 on');
+    } else if (!host3) {
+        let exists = await ServerSettings.findOne({ where: {key: 'host3'}});
+        if (exists) { const setting = await ServerSettings.update({value: ''}, {where: {key: 'host3'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'host3', value: ''}); }
+        console.log('host3 off');
+    }
+
+    // Host 4
+    if (host4) {
+        let exists = await ServerSettings.findOne({ where: {key: 'host4'}});
+        if (exists) { const setting = await ServerSettings.update({value: `${tag4},${ip4},${port4}`}, {where: {key: 'host4'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'host4', value: `${tag4},${ip4},${port4}`}); }
+        console.log('host4 on');
+    } else if (!host4) {
+        let exists = await ServerSettings.findOne({ where: {key: 'host4'}});
+        if (exists) { const setting = await ServerSettings.update({value: ''}, {where: {key: 'host4'}}); }
+        else { const newSetting = await ServerSettings.create({ key: 'host4', value: ''}); }
+        console.log('host4 off');
+    }
+
 
+    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>`);
 }

+ 12 - 7
languages/chinese.json

@@ -15,19 +15,24 @@
     "admin": "",
     "user": "",
     "Start": "",
-    "Starting": "",
-    "Running": "",
     "Stop": "",
-    "Stopping": "",
-    "Stopped": "",
     "Pause": "",
-    "Pausing": "",
-    "Paused": "",
     "Restart": "",
+    "Starting": "",
+    "Stopping": "",
+    "Pausing": "",
     "Restarting": "",
+    "Running": "",
+    "Stopped": "",
+    "Paused": "",
     "Details": "",
     "Logs": "",
     "Edit": "",
     "Update": "",
-    "Uninstall": ""
+    "Uninstall": "",
+    "Hide": "",
+    "Reset View": "",
+    "Permissions": "",
+    "Sponsors": "",
+    "Credits": ""
 }

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "dweebui",
-  "version": "0.70.402",
+  "version": "0.70.417",
   "main": "server.js",
   "type": "module",
   "scripts": {
@@ -10,7 +10,7 @@
   "keywords": [],
   "author": "lllllllillllllillll",
   "license": "MIT",
-  "description": "DweebUI is a WebUI for managing your containers. Simple setup, a dynamically updating dashboard, and a multi-user permission system.",
+  "description": "DweebUI is a WebUI for managing your containers. https://dweebui.com",
   "dependencies": {
     "bcrypt": "^5.1.1",
     "connect-session-sequelize": "^7.1.7",

+ 150 - 143
public/css/dweebui.css

@@ -1,156 +1,163 @@
 
+@import url('/fonts/inter.css');
+:root {
+  --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
+}
+body {
+  font-feature-settings: "cv03", "cv04", "cv11";
+}
 
 .meter {
-    box-sizing: content-box;
-    height: 15px;
-    margin-left: auto;
-    margin-right: auto;
-    position: relative;
-    background: #a7a7a752;
-    border-radius: 25px;
-    padding: 3px;
-    box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3);
-  }
+  box-sizing: content-box;
+  height: 15px;
+  margin-left: auto;
+  margin-right: auto;
+  position: relative;
+  background: #a7a7a752;
+  border-radius: 25px;
+  padding: 3px;
+  box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3);
+}
 
-  .meter > span {
-    display: block;
-    height: 100%;
-    border-top-right-radius: 20px;
-    border-bottom-right-radius: 20px;
-    border-top-left-radius: 20px;
-    border-bottom-left-radius: 20px;
-    background-color: rgb(43, 194, 83);
-    background-image: linear-gradient(
-      center bottom,
-      rgb(43, 194, 83) 37%,
-      rgb(84, 240, 84) 69%
-    );
-    box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3),
-      inset 0 -2px 6px rgba(0, 0, 0, 0.4);
-    position: relative;
-    overflow: hidden;
-  }
+.meter > span {
+  display: block;
+  height: 100%;
+  border-top-right-radius: 20px;
+  border-bottom-right-radius: 20px;
+  border-top-left-radius: 20px;
+  border-bottom-left-radius: 20px;
+  background-color: rgb(43, 194, 83);
+  background-image: linear-gradient(
+    center bottom,
+    rgb(43, 194, 83) 37%,
+    rgb(84, 240, 84) 69%
+  );
+  box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3),
+    inset 0 -2px 6px rgba(0, 0, 0, 0.4);
+  position: relative;
+  overflow: hidden;
+}
 
-  .meter > span:after,
-  .animate > span > span {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 0;
-    background-image: linear-gradient(
-      -45deg,
-      rgba(255, 255, 255, 0.2) 25%,
-      transparent 25%,
-      transparent 50%,
-      rgba(255, 255, 255, 0.2) 50%,
-      rgba(255, 255, 255, 0.2) 75%,
-      transparent 75%,
-      transparent
-    );
-    z-index: 1;
-    background-size: 50px 50px;
-    animation: move 2s linear infinite;
-    border-top-right-radius: 8px;
-    border-bottom-right-radius: 8px;
-    border-top-left-radius: 20px;
-    border-bottom-left-radius: 20px;
-    overflow: hidden;
-  }
-  
-  .animate > span:after {
-    display: none;
-  }
-  
-  @keyframes move {
-    0% {
-      background-position: 0 0;
-    }
-    100% {
-      background-position: 50px 50px;
-    }
-  }
-  
-  .orange > span {
-    background-image: linear-gradient(#f1a165, #f36d0a);
-  }
-  
-  .red > span {
-    background-image: linear-gradient(#f0a3a3, #f42323);
-  }
+.meter > span:after,
+.animate > span > span {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background-image: linear-gradient(
+    -45deg,
+    rgba(255, 255, 255, 0.2) 25%,
+    transparent 25%,
+    transparent 50%,
+    rgba(255, 255, 255, 0.2) 50%,
+    rgba(255, 255, 255, 0.2) 75%,
+    transparent 75%,
+    transparent
+  );
+  z-index: 1;
+  background-size: 50px 50px;
+  animation: move 2s linear infinite;
+  border-top-right-radius: 8px;
+  border-bottom-right-radius: 8px;
+  border-top-left-radius: 20px;
+  border-bottom-left-radius: 20px;
+  overflow: hidden;
+}
 
-  .blue > span {
-    background-image: linear-gradient(#2478f5, #22017e);
-  }
+.animate > span:after {
+  display: none;
+}
 
-  .purple > span {
-    background-image: linear-gradient(#bd14d3, #670370);
+@keyframes move {
+  0% {
+    background-position: 0 0;
   }
-  
-  .nostripes > span > span,
-  .nostripes > span::after {
-    background-image: none;
+  100% {
+    background-position: 50px 50px;
   }
+}
 
+.orange > span {
+  background-image: linear-gradient(#f1a165, #f36d0a);
+}
 
+.red > span {
+  background-image: linear-gradient(#f0a3a3, #f42323);
+}
 
-  .container-stamp {
-    --tblr-stamp-size: 8rem;
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    width: calc(var(--tblr-stamp-size) * 1);
-    height: calc(var(--tblr-stamp-size) * 1);
-    max-height: 100%;
-    border-top-right-radius: 4px;
-    opacity: 0.2;
-    overflow: hidden;
-    pointer-events: none;
-  }
-  
-  .container-action {
-    padding: 0;
-    border: 0;
-    color: var(--tblr-secondary);
-    display: inline-flex;
-    width: 1.5rem;
-    height: 1.5rem;
-    align-items: center;
-    justify-content: center;
-    border-radius: var(--tblr-border-radius);
-    background: transparent;
-  }
-  .container-action:after {
-    content: none;
-  }
-  .container-action:focus {
-    outline: none;
-    box-shadow: none;
-  }
-  .container-action:hover, .container-action.show {
-    color: var(--tblr-body-color);
-    background: var(--tblr-active-bg);
-  }
-  .container-action.show {
-    color: var(--tblr-primary);
-  }
-  .container-action .icon {
-    margin: 0;
-    width: 1.25rem;
-    height: 1.25rem;
-    font-size: 1.25rem;
-    stroke-width: 1;
-  }
-  
-  .container-actions {
-    display: flex;
-  }
+.blue > span {
+  background-image: linear-gradient(#2478f5, #22017e);
+}
 
-  .modal-content {
-    border: 1px solid grey;
-  }
-  
-  .accordion-item {
-    border: 1px solid grey;
-  }
+.purple > span {
+  background-image: linear-gradient(#bd14d3, #670370);
+}
+
+.nostripes > span > span,
+.nostripes > span::after {
+  background-image: none;
+}
+
+
+
+.container-stamp {
+  --tblr-stamp-size: 8rem;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: calc(var(--tblr-stamp-size) * 1);
+  height: calc(var(--tblr-stamp-size) * 1);
+  max-height: 100%;
+  border-top-right-radius: 4px;
+  opacity: 0.2;
+  overflow: hidden;
+  pointer-events: none;
+}
+
+.container-action {
+  padding: 0;
+  border: 0;
+  color: var(--tblr-secondary);
+  display: inline-flex;
+  width: 1.5rem;
+  height: 1.5rem;
+  align-items: center;
+  justify-content: center;
+  border-radius: var(--tblr-border-radius);
+  background: transparent;
+}
+.container-action:after {
+  content: none;
+}
+.container-action:focus {
+  outline: none;
+  box-shadow: none;
+}
+.container-action:hover, .container-action.show {
+  color: var(--tblr-body-color);
+  background: var(--tblr-active-bg);
+}
+.container-action.show {
+  color: var(--tblr-primary);
+}
+.container-action .icon {
+  margin: 0;
+  width: 1.25rem;
+  height: 1.25rem;
+  font-size: 1.25rem;
+  stroke-width: 1;
+}
+
+.container-actions {
+  display: flex;
+}
+
+.modal-content {
+  border: 1px solid grey;
+}
+
+.accordion-user {
+  border: 1px solid grey;
+}

+ 17 - 1
public/js/dweebui.js

@@ -24,4 +24,20 @@ function toggleTheme(button) {
     document.body.removeAttribute("data-bs-theme");
     localStorage.setItem(themeStorageKey, 'light');
   }
-}
+}
+
+
+
+function selectAll(group) {
+  
+  let checkboxes = document.getElementsByName(group);
+  if (checkboxes[0].checked == true) {
+    for (var i = 0; i < checkboxes.length; i++) {
+      checkboxes[i].checked = true;
+    }
+  } else {
+    for (var i = 0; i < checkboxes.length; i++) {
+      checkboxes[i].checked = false;
+    }
+  }
+}

+ 10 - 7
router.js

@@ -3,19 +3,17 @@ export const router = express.Router();
 
 import { Login, submitLogin, Logout } from './controllers/login.js';
 import { Register, submitRegister } from './controllers/register.js';
-import { Dashboard, submitDashboard, ServerMetrics } from './controllers/dashboard.js';
-import { Settings, submitSettings } from './controllers/settings.js';
+import { Dashboard, submitDashboard, ContainerAction, ServerMetrics, CardList, SSE } from './controllers/dashboard.js';
+import { Settings, updateSettings } from './controllers/settings.js';
 import { Images, submitImages } from './controllers/images.js';
 import { Volumes, submitVolumes } from './controllers/volumes.js';
 import { Networks, submitNetworks } from './controllers/networks.js';
 import { Users, submitUsers } from './controllers/users.js';
 import { Apps, submitApps } from './controllers/apps.js';
 import { Account } from './controllers/account.js';
-import { containerAction } from './utils/docker.js';
 import { Preferences, submitPreferences } from './controllers/preferences.js';
 
-
-import { sessionCheck, adminOnly, permissionCheck } from './utils/permissions.js';
+import { sessionCheck, adminOnly, permissionCheck, permissionModal } from './utils/permissions.js';
 
 router.get('/login', Login);
 router.post('/login', submitLogin);
@@ -25,8 +23,13 @@ router.post('/register', submitRegister);
 
 router.get("/:host?/dashboard", sessionCheck, Dashboard);
 router.get("/server_metrics", sessionCheck, ServerMetrics);
+router.get("/permission_modal", adminOnly, permissionModal);
+
+
+router.get("/sse", sessionCheck, SSE);
+router.get("/card_list", sessionCheck, CardList);
 
-router.post("/:host?/container/:action", permissionCheck, containerAction);
+router.post("/:host?/container/:action/:containerid?", permissionCheck, ContainerAction);
 
 router.get('/images', adminOnly, Images);
 router.post('/images', adminOnly, submitImages);
@@ -38,7 +41,7 @@ router.get('/networks', adminOnly, Networks);
 router.post('/networks', adminOnly, submitNetworks);
 
 router.get('/settings', adminOnly, Settings);
-router.post('/settings', adminOnly, submitSettings);
+router.post('/settings', adminOnly, updateSettings);
 
 router.get("/apps/:page?/:template?", adminOnly, Apps);
 router.post('/apps', adminOnly, submitApps);

+ 5 - 80
utils/docker.js

@@ -1,24 +1,17 @@
 import Docker from 'dockerode';
-import { Permission } from '../database/config.js';
 
 export var docker = new Docker();
 
-export async function containerList() {
-    let containers = await docker.listContainers({ all: true });
-    containers = containers.map(container => ({ 
-        containerName: container.Names[0].split('/').pop(),
-        containerID: container.Id,
-        containerState: container.State,
-        containerImage: container.Image,
-    }));
-    return containers;
-}
 
 export async function imageList() {
     let images = await docker.listImages({ all: true });
     return images;
 }
 
+export async function getContainer(containerID) {
+    let container = docker.getContainer(containerID);
+    return container;
+}
 
 export async function containerInspect (containerID) {
     // get the container info
@@ -64,72 +57,4 @@ export async function containerInspect (containerID) {
         link: 'localhost',
     }
     return details;
-}
-
-
-export const containerAction = async (req, res) => {
-
-    let container_name = req.header('hx-trigger-name');
-    let container_id = req.header('hx-trigger');
-    let action = req.params.action;
-
-    console.log(`Container: ${container_name} ID: ${container_id} Action: ${action}`);
-
-    // Reset the view
-    if (container_id == 'reset') { 
-        console.log('Resetting view'); 
-        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
-        res.send('ok'); 
-        return;
-    }
-    // Inspect the container
-    let container = docker.getContainer(container_id);
-    let containerInfo = await container.inspect();
-    let state = containerInfo.State.Status;
-    // console.log(`Container: ${container_name} ID: ${container_id} State: ${state} Action: ${action}`);
-    // Displays container state (starting, stopping, restarting, pausing)
-    function status (state) {
-        return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
-                        ${state}
-                </span>`);
-    }
-    // Perform the action
-    if ((action == 'start') && (state == 'exited')) {
-        await container.start();
-        res.send(status('starting'));
-    } else if ((action == 'start') && (state == 'paused')) {
-        await container.unpause();
-        res.send(status('starting'));
-    } else if ((action == 'stop') && (state != 'exited')) {
-        await container.stop();
-        res.send(status('stopping'));
-    } else if ((action == 'pause') && (state == 'paused')) {
-        await container.unpause();
-        res.send(status('starting'));
-    }   else if ((action == 'pause') && (state == 'running')) {
-        await container.pause();
-        res.send(status('pausing'));
-    } else if (action == 'restart') {
-        await container.restart();
-        res.send(status('restarting'));
-    } else if (action == 'hide') {
-        let exists = await Permission.findOne({ where: { containerID: container_id, userID: req.session.userID }});
-        if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: container_id, username: req.session.username, userID: req.session.userID, hide: true }); }
-        else { exists.update({ hide: true }); }
-        // Array of hidden containers
-        hidden = await Permission.findAll({ where: { userID: req.session.userID, hide: true}}, { attributes: ['containerID'] });
-        // Map the container IDs
-        hidden = hidden.map((container) => container.containerID);
-        res.send("ok");
-    }
-}
-
-
-
-
-// Listens for docker events
-docker.getEvents({}, function (err, data) {
-    data.on('data', function () {
-        console.log('Docker event');
-    });
-}); 
+}

+ 41 - 0
utils/permissions.js

@@ -1,4 +1,5 @@
 import { Permission } from "../database/config.js";
+import { readFileSync } from 'fs';
 
 export const adminOnly = async (req, res, next) => {
     if (req.session.role == 'admin') { next(); }
@@ -12,5 +13,45 @@ export const sessionCheck = async (req, res, next) => {
 
 
 export const permissionCheck = async (req, res, next) => {
+    next();
+}
+
+
+
+
+export const permissionModal = async (req, res) => {
 
+    // let title = name.charAt(0).toUpperCase() + name.slice(1);
+    // let permissions_list = '';
+    let permissions_modal = readFileSync('./views/partials/permissions.html', 'utf8');
+    // permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
+    // permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
+    // let users = await User.findAll({ attributes: ['username', 'UUID']});
+    // for (let i = 0; i < users.length; i++) {
+    //     let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
+    //     let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+    //     if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
+    //     let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+    //     if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
+    //     if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
+    //     if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
+    //     if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
+    //     if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
+    //     if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
+    //     if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
+    //     if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
+    //     if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
+    //     user_permissions = user_permissions.replace(/EntryNumber/g, i);
+    //     user_permissions = user_permissions.replace(/EntryNumber/g, i);
+    //     user_permissions = user_permissions.replace(/EntryNumber/g, i);
+    //     user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+    //     user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+    //     user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
+    //     user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+    //     user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+    //     user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+    //     permissions_list += user_permissions;
+    // }
+    // permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
+    res.send(permissions_modal);
 }

+ 21 - 9
utils/system.js

@@ -2,17 +2,20 @@ import { User } from '../database/config.js';
 import { readFileSync } from 'fs';
 
 
+
+
+// Navbar
 export async function Navbar (req) {
 
     let username = req.session.username;
 
-
     let language = await getLanguage(req);
 
-    let user = await User.findOne({ where: { userID: req.session.userID }});
-    let preferences = JSON.parse(user.preferences);
-    if (preferences.hide_profile == true) {
-        username = 'Anonymous';
+    // 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 }});
+        let preferences = JSON.parse(user.preferences);
+        if (preferences.hide_profile == true) { username = 'Anon'; }
     }
 
     let navbar = readFileSync('./views/partials/navbar.html', 'utf8');
@@ -39,7 +42,7 @@ export async function Navbar (req) {
     }
 }
 
-
+// Header Alert
 export function Alert (type, message) {
     return `
     <div class="alert alert-${type} alert-dismissible" role="alert" style="margin-bottom: 0;">
@@ -55,10 +58,19 @@ export function Alert (type, message) {
     </div>`;
 }
 
+
 export async function getLanguage (req) {
-    let user = await User.findOne({ where: { userID: req.session.userID }});
-    let preferences = JSON.parse(user.preferences);
-    return preferences.language;
+
+    // No userID if authentication is disabled.
+    if (req.session.userID == '00000000-0000-0000-0000-000000000000') { 
+        let user = await User.findOne({ where: { role: 'admin' }});
+        let preferences = JSON.parse(user.preferences);
+        return preferences.language;
+    } else {
+        let user = await User.findOne({ where: { userID: req.session.userID }});
+        let preferences = JSON.parse(user.preferences);
+        return preferences.language;
+    }
 }
 
 export function Capitalize (string) {

+ 1 - 10
views/account.html

@@ -6,18 +6,9 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
     <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
     <title>Account - DweebUI</title>
-    <!-- CSS files -->
     <link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
     <link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-    <style>
-      @import url('/fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
   </head>
   <body >
     <script src="/js/demo-theme.min.js?1692870487"></script>

+ 1 - 11
views/apps.html

@@ -1,4 +1,5 @@
 <!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
@@ -8,18 +9,7 @@
     <!-- CSS files -->
 		<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
 		<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-
 		<link href="/css/dweebui.css" rel="stylesheet"/>
-
-    <style>
-			@import url('/fonts/inter.css');
-			:root {
-			  --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-			}
-			body {
-			  font-feature-settings: "cv03", "cv04", "cv11";
-			}
-		  </style>
   </head>
   <body >
     <div class="page">

+ 36 - 227
views/dashboard.html

@@ -6,21 +6,9 @@
 		<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
 		<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
 		<title>Dashboard - DweebUI.</title>
-		<!-- CSS files -->
 		<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
 		<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-
 		<link href="/css/dweebui.css" rel="stylesheet"/>
-
-		<style>
-		@import url('/fonts/inter.css');
-		:root {
-			--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-		}
-		body {
-			font-feature-settings: "cv03", "cv04", "cv11";
-		}
-		</style>
   	</head>
 	<body>
 		<script src="/js/demo-theme.min.js?1692870487"></script>
@@ -29,12 +17,10 @@
 				<!-- EJS -->
 				<%- navbar %>
 
-				<div class="page-wrapper">
+				<div class="page-wrapper" hx-ext="sse" sse-connect="/sse">
 					<div class="page-body">
 						<div class="container-xl">
 							<div class="row row-deck row-cards">
-
-
 								<div class="col-12">
 									<div class="row row-cards">
 
@@ -73,10 +59,10 @@
 														<!-- HTMX -->
 														<div class="col" name="RAM" id="blue" data-hx-get="/server_metrics" data-hx-trigger="load, every 2s" hx-swap="innerHTML">
 															<div class="font-weight-medium">
-															<label class="ram-text mb-1" for="ram">RAM 0%</label>
+																<label class="ram-text mb-1" for="ram">RAM 0%</label>
 															</div>
 															<div class="ram-bar meter animate blue">
-															<span style="width:20%"><span></span></span>
+																<span style="width:20%"><span></span></span>
 															</div>
 														</div>
 													</div>
@@ -129,10 +115,20 @@
 
 									</div>
 								</div>
-						
-								<% if(container_list) { %>
-									<%- container_list %>
-								<% } %>
+
+								
+								<div class="col-12">
+									<div class="row row-cards" id="containers">
+									</div>
+								</div>
+					  
+								<!-- HTMX -->
+								<div class="col-12">
+									<div class="row row-cards" hx-get="/card_list" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
+									</div>
+								</div>
+
+
 
 							</div>
 						</div>
@@ -143,226 +139,39 @@
 				</div>
 			</div>
 			
+			<div class="modal modal-blur fade" id="permissions_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">
+						<div class="modal-header">
+							<h5 class="modal-title" id="permissions_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="user_permissions">
+
+								PermissionsList
 
-			
-			<div id="modals-here" class="modal modal-blur fade" tabindex="-1" aria-hidden="true" role="dialog">
-				<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
-				<div class="modal-content">
-					<div class="modal-header">
-						<h5 class="modal-title">Speedtest Permissions</h5>
-					</div>
-					<div class="modal-body">
-						<div class="accordion" id="modal-accordion">
-							<div class="accordion-item mb-3">
-				<h2 class="accordion-header" id="heading-0">
-				  <button class="accordion-button collapsed row" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-0" aria-expanded="false">
-					<span class="avatar avatar-sm bg-green-lt col-3 text-start">JD</span>
-					  <div class="col text-end" style="margin-right: 10px;">JohnDoe</div>
-				  </button>
-				</h2>
-				<div id="collapse-0" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
-				  <div class="accordion-body pt-0">
-			
-			
-					<div class="">
-					  <div class="">
-						<form id="updatePermissions0">
-						  <div class="row mb-3">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  All
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" onclick="selectAll('select0')">
-							  </label>
-							</div>
-						  </div>
-			
-						  <input type="hidden" name="userID" value="c49673fc-9413-40ae-a500-0f1dd2688fe7">
-						  <input type="hidden" name="container" value="speedtest">
-						  <input type="hidden" name="containerID" value="b207a9eebba3e678697bc7c224fdcecf005b5ea733d9608a5a59dca15beaaf5b">
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Uninstall
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="uninstall" data-uninstallcheck="">
-							  </label>
-							</div>
-						  </div>
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Edit
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="edit" data-editcheck="">
-							  </label>
-							</div>
-						  </div>
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Upgrade
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="upgrade" data-upgradecheck="">
-							  </label>
-							</div>
-						  </div>
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Start
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="start" data-startcheck="">
-							  </label>
-							</div>
-						  </div>
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Stop
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="stop" data-stopcheck="">
-							  </label>
-							</div>
-						  </div>
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Pause
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="pause" data-pausecheck="">
-							  </label>
-							</div>
-						  </div>
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Restart
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="restart" data-restartcheck="">
-							  </label>
-							</div>
-						  </div>
-			
-			
-						  <div class="row mb-2">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  Logs
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="logs" data-logscheck="">
-							  </label>
-							</div>
-						  </div>
-			
-			
-						  <div class="row mb-4">
-							<div class="col-9">
-							  <label class="row text-start">
-								<span class="col">
-								  View
-								</span>
-							  </label>
-							</div>
-							<div class="col-3">
-							  <label class="form-check form-check-single form-switch text-end">
-								<input class="form-check-input" type="checkbox" name="select0" value="view" data-viewcheck="">
-							  </label>
 							</div>
-						  </div>
-			
-						  
-			
-						  <div class="row mb-2">
-							<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissions0" hx-swap="outerHTML">Update&nbsp;&nbsp;</button>
-						  </div>
-			
-						</form>
-					  </div>
-					</div>
-			
-				  </div>
-				</div>
-			  </div>
+
 						</div>
-					</div>
-					<div class="modal-footer">
-						<div class="row">
-							<div class="col">
-								<form id="reset_permissions">
-									<input type="hidden" name="containerID" value="b207a9eebba3e678697bc7c224fdcecf005b5ea733d9608a5a59dca15beaaf5b">
-										<button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" value="reset_permissions" id="submit" hx-post="/updatePermissions" hx-trigger="click" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
-								</form>
-							</div>
+						<div class="modal-footer">
+							<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
+							<form id="reset_permissions">
+								<input type="hidden" name="containerID" value="b207a9eebba3e678697bc7c224fdcecf005b5ea733d9608a5a59dca15beaaf5b" id="containerID">
+									<button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" value="reset_permissions" id="submit" hx-post="/updatePermissions" hx-trigger="click" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
+							</form>
 						</div>
-					</div>
+				  	</div>
 				</div>
 			</div>
-			</div>
-
-
 
-			<!-- Libs JS -->
 			<script src="/libs/apexcharts/dist/apexcharts.min.js?1692870487" defer></script>
 
 			<script src="/js/dweebui.js" defer></script>
 			<script src="/js/htmx.min.js"></script>
 			<script src="/js/htmx-sse.js"></script>
 
-			<!-- Tabler Core -->
 			<script src="/js/tabler.min.js?1692870487" defer></script>
 			<script src="/js/demo.min.js?1692870487" defer></script>
 			

+ 1 - 10
views/images.html

@@ -1,4 +1,5 @@
 <!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
@@ -7,17 +8,7 @@
     <title>DweebUI - Images</title>
 		<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
 		<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-
 		<link href="/css/dweebui.css" rel="stylesheet"/>
-    <style>
-      @import url('/fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
   </head>
   <body >
     <div class="page">

+ 1 - 9
views/login.html

@@ -9,15 +9,7 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
     <link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-    <style>
-      @import url('fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
   </head>
   <body  class=" d-flex flex-column">
     <script src="/js/demo-theme.min.js?1692870487"></script>

+ 1 - 11
views/networks.html

@@ -1,4 +1,5 @@
 <!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
@@ -8,18 +9,7 @@
 		<!-- CSS files -->
 		<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
 		<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-
     <link href="/css/dweebui.css" rel="stylesheet"/>
-
-    <style>
-      @import url('/fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
   </head>
   <body >
     <div class="page">

+ 2 - 2
views/partials/app_card.html

@@ -11,10 +11,10 @@
       <div class="d-flex">
         <a href="#" class="card-btn"><!-- Download SVG icon from http://tabler-icons.io/i/mail -->
           <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 me-2 text-muted"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>
-          Email</a>
+          Info</a>
         <a href="#" class="card-btn"><!-- Download SVG icon from http://tabler-icons.io/i/phone -->
           <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 me-2 text-muted"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path></svg>
-          Call</a>
+          Install</a>
       </div>
     </div>
   </div>

+ 14 - 10
views/partials/container_card.html

@@ -1,21 +1,21 @@
-<div class="col-sm-6 col-lg-3">
+<div class="col-sm-6 col-lg-3" hx-post="/container/update/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 width="120px" 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>
+            <img width="110px" 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>
         </div>
         <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" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
+                <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">
                     <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" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
+                <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">
                     <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" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
+                <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">
                     <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" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
+                <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">
                     <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,7 +23,7 @@
                     <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="AppName" id="details" data-hx-post="/container/details" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
+                    <button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-post="/container/details/" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
                     <button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-post="/container/logs" hx-swap="innerHTML" hx-trigger="mousedown" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
                     <button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
                     <button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
@@ -34,19 +34,23 @@
                     <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" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="AppID" value="hide">Hide</button>
-                    <button class="dropdown-item text-secondary" data-hx-post="/dashboard/permissions" name="AppName"  data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
+                    <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" data-hx-get="/permission_modal" name="AppName" hx-target="#user_permissions"  hx-swap="innerHTML" data-bs-toggle="modal" data-bs-target="#permissions_modal">Permissions</button>
                 </div>
                 
             </div>
         </div>
+
         <div class="d-flex align-items-center">
             <label style="font-size: x-large; font-weight: bold;">AppName</label>
-            <div class="text-StateColor d-inline-flex align-items-center lh-1 ms-auto">
+
+            <div class="text-StateColor 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>AppState</strong>
             </div>
+
         </div>
+
         <div id="chart-new-clients" class="chart-sm"></div>
     </div>
 </div>

+ 23 - 8
views/partials/navbar.html

@@ -237,17 +237,32 @@
                 </li>
                 
               </ul>
+
+
               <div class="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">
-                <form action="./" method="get" autocomplete="off" novalidate>
-                  <div class="input-icon">
-                    <span class="input-icon-addon">
-                      <!-- Download SVG icon from http://tabler-icons.io/i/search -->
-                      <svg xmlns="http://www.w3.org/2000/svg" class="icon" 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 d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
-                    </span>
-                    <input type="text" value="" class="form-control" placeholder="Search…" aria-label="Search in website" name="search" hx-post="/search" hx-trigger="input changed delay:500ms, search">
+                <div class="card-actions btn-actions">
+                  <input type="search" class="form-control mx-2" placeholder="Search..." aria-label="search" name="search" hx-post="/search" hx-trigger="input changed delay:500ms, search" hx-swap="none">
+                  <button class="btn-action mx-1" title="Grid View" data-hx-post="/dashboard/view" data-hx-trigger="mousedown" data-hx-target="#" name="grid" id="AppState">
+                    <svg  xmlns="http://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="1.5"  stroke-linecap="round"  stroke-linejoin="round"  class="icon-tabler icons-tabler-outline icon-tabler-layout-grid"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /></svg>
+                  </button>
+                  <button class="btn-action mx-1" title="List View" data-hx-post="/dashboard/view" data-hx-trigger="mousedown" data-hx-target="#" name="list" id="AppState">
+                    <svg  xmlns="http://www.w3.org/2000/svg"  width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="1.5"  stroke-linecap="round"  stroke-linejoin="round"  class="icon-tabler icons-tabler-outline icon-tabler-layout-grid"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l11 0" /><path d="M9 12l11 0" /><path d="M9 18l11 0" /><path d="M5 6l0 .01" /><path d="M5 12l0 .01" /><path d="M5 18l0 .01" /></svg>
+                  </button>
+                  <div class="dropdown">
+                    <a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                      <svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
+                    </a>
+                    <div class="dropdown-menu dropdown-menu-end">
+                      <!-- <button class="dropdown-item text-secondary" data-hx-post="/container/reset/000" data-hx-trigger="mousedown" data-hx-swap="none" name="reset" id="reset" value="reset">Reset View</button> -->
+                       <form action="/container/reset/000" method="post">
+                        <button class="dropdown-item text-secondary" name="reset" id="reset" value="reset">Reset View</button>
+                      </form>
+                    </div>
                   </div>
-                </form>
+                </div>
               </div>
+
+
             </div>
           </div>
         </div>

+ 180 - 0
views/partials/permissions.html

@@ -0,0 +1,180 @@
+<div class="accordion-user mb-3">
+    <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">
+            <span class="avatar avatar-sm bg-green-lt col-3 text-start">JD</span>
+            <div class="col text-end" style="margin-right: 10px;">JohnDoe</div>
+        </button>
+    </h2>
+
+    <div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#accordion-example">
+        <div class="accordion-body pt-0">
+
+            <form id="updatePermissions0">
+                <div class="col">
+
+                    <input type="hidden" name="userID" value="c49673fc-9413-40ae-a500-0f1dd2688fe7">
+                    <input type="hidden" name="container" value="speedtest">
+                    <input type="hidden" name="containerID" value="b207a9eebba3e678697bc7c224fdcecf005b5ea733d9608a5a59dca15beaaf5b">
+
+                    <div class="row mb-3">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                    All
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" onclick="selectAll('select0')">
+                            </label>
+                        </div>
+                    </div>
+
+                    <div class="row mb-2">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                Uninstall
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" value="uninstall" data-uninstallcheck="">
+                            </label>
+                        </div>
+                    </div>
+
+
+                    <div class="row mb-2">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                Edit
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" value="edit" data-editcheck="">
+                            </label>
+                        </div>
+                    </div>
+
+                    <div class="row mb-2">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                Upgrade
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" value="upgrade" data-upgradecheck="">
+                            </label>
+                        </div>
+                    </div>
+    
+                    <div class="row mb-2">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                Start
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" value="start" data-startcheck="">
+                            </label>
+                        </div>
+                    </div>
+        
+                    <div class="row mb-2">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                Stop
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" value="stop" data-stopcheck="">
+                            </label>
+                        </div>
+                    </div>
+        
+                    <div class="row mb-2">
+                        <div class="col-9">
+                            <label class="row text-start">
+                                <span class="col">
+                                Pause
+                                </span>
+                            </label>
+                        </div>
+                        <div class="col-3">
+                            <label class="form-check form-check-single form-switch text-end">
+                                <input class="form-check-input" type="checkbox" name="select0" value="pause" data-pausecheck="">
+                            </label>
+                        </div>
+                    </div>
+        
+                    <div class="row mb-2">
+                        <div class="col-9">
+                        <label class="row text-start">
+                            <span class="col">
+                            Restart
+                            </span>
+                        </label>
+                        </div>
+                        <div class="col-3">
+                        <label class="form-check form-check-single form-switch text-end">
+                            <input class="form-check-input" type="checkbox" name="select0" value="restart" data-restartcheck="">
+                        </label>
+                        </div>
+                    </div>
+        
+        
+                    <div class="row mb-2">
+                        <div class="col-9">
+                        <label class="row text-start">
+                            <span class="col">
+                            Logs
+                            </span>
+                        </label>
+                        </div>
+                        <div class="col-3">
+                        <label class="form-check form-check-single form-switch text-end">
+                            <input class="form-check-input" type="checkbox" name="select0" value="logs" data-logscheck="">
+                        </label>
+                        </div>
+                    </div>
+        
+        
+                    <div class="row mb-4">
+                        <div class="col-9">
+                        <label class="row text-start">
+                            <span class="col">
+                            View
+                            </span>
+                        </label>
+                        </div>
+                        <div class="col-3">
+                        <label class="form-check form-check-single form-switch text-end">
+                            <input class="form-check-input" type="checkbox" name="select0" value="view" data-viewcheck="">
+                        </label>
+                        </div>
+                    </div>
+
+                    <div class="row mb-2">
+                        <button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissions0" hx-swap="outerHTML">Update&nbsp;&nbsp;</button>
+                    </div>
+
+                </div>
+            </form>
+        </div>
+    </div>
+</div>

+ 1 - 9
views/preferences.html

@@ -9,15 +9,7 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
     <link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-    <style>
-      @import url('/fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
   </head>
   <body >
     <script src="/js/demo-theme.min.js?1692870487"></script>

+ 1 - 9
views/register.html

@@ -9,15 +9,7 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
     <link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-    <style>
-      @import url('fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
   </head>
   <body class="d-flex flex-column">
     <script src="/js/demo-theme.min.js?1692870487"></script>

+ 43 - 13
views/settings.html

@@ -9,15 +9,7 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
     <link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-    <style>
-      @import url('/fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
   </head>
   <body >
     <script src="/js/demo-theme.min.js?1692870487"></script>
@@ -48,7 +40,7 @@
                   
 
                     <h3 class="mt-5">User Registration</h3>
-                    <label class="text-muted mb-2">Allow other users to register.</label>
+                    <label class="text-muted mb-2">Enable registration and choose a secret.</label>
                     <div class="row align-items-center">
                       <div class="col-auto">
                         <label class="form-check form-switch form-switch-lg">
@@ -71,7 +63,7 @@
                     <div class="row align-items-center">
                       <div class="col-auto">
                         <label class="form-check form-switch form-switch-lg">
-                          <input class="form-check-input" type="checkbox" name="link_mode" <%= container_links %>>
+                          <input class="form-check-input" type="checkbox" name="custom_link" <%= custom_link %>>
                           <span class="form-check-label form-check-label-on text-warning">
                             Custom
                           </span>
@@ -81,11 +73,49 @@
                         </label>
                       </div>
                       <div class="col-5">
-                        <input type="text" class="form-control" name="link" placeholder="IP Address or Domain" value="<%= link_url %>">
+                        <input type="text" class="form-control" name="link_url" placeholder="IP Address or Domain" value="<%= link_url %>">
+                      </div>
+                    </div>
+
+                    <h3 class="mt-5">Authentication</h3>
+                    <label class="text-muted mb-2">Change authentication settings. Only the default, Username and Password, supports multiple users.</label>
+                    <div class="row align-items-center">
+                      <div class="col-auto">
+                        <select class="form-select" name="authentication">
+                          <option value="default">Username and Password - Default</option>
+                          <option value="localhost">Localhost</option>
+                          <option value="no_auth">Disabled - No Authentication</option>
+                        </select>
+                      
+                      </div>
+                    </div>
+
+                    <h3 class="mt-5">Hosts</h3>
+
+                    <label class="text-muted mb-2">Host #1</label>
+                    <div class="row align-items-center">
+                      <div class="col-auto">
+                        <label class="form-check form-switch form-switch-lg">
+                          <input class="form-check-input" type="checkbox" name="host1" checked disabled>
+                          <span class="form-check-label form-check-label-on text-success">
+                            Enabled
+                          </span>
+                          <span class="form-check-label form-check-label-off text-danger">
+                            Disabled
+                          </span>
+                        </label>
+                      </div>
+                      <div class="col-2">
+                        <input type="text" class="form-control" placeholder="Host 1" readonly>
+                      </div>
+                      <div class="col-4">
+                        <input type="text" class="form-control" placeholder="/var/run/docker.sock" readonly>
+                      </div>
+                      <div class="col-2">
+                        <input type="text" class="form-control" readonly>
                       </div>
                     </div>
 
-                    <h3 class="mt-5">Remote Hosts</h3>
                     <label class="text-muted mb-2">Host #2</label>
                     <div class="row align-items-center">
                       <div class="col-auto">

+ 61 - 66
views/supporters.html

@@ -1,87 +1,82 @@
-	<!doctype html>
-	<html lang="en">
-	<head>
-		<meta charset="utf-8"/>
-		<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
-		<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
-		<title>DweebUI - Settings</title>
-		<!-- CSS files -->
-		<link href="/css/tabler.min.css" rel="stylesheet"/>
-		<link href="/css/demo.min.css" rel="stylesheet"/>
-		<script src="/js/htmx.min.js"></script>
-		<style>
-			@import url('/fonts/inter.css');
-			:root {
-			  --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-			}
-			body {
-			  font-feature-settings: "cv03", "cv04", "cv11";
-			}
-		  </style>
-	</head>
-	<body >
-	<div class="page">
+<!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 
-		<!-- EJS -->
-		<%- navbar %>
+<html lang="en">
+<head>
+	<meta charset="utf-8"/>
+	<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
+	<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
+	<title>DweebUI - Settings</title>
+	<!-- CSS files -->
+	<link href="/css/tabler.min.css" rel="stylesheet"/>
+	<link href="/css/demo.min.css" rel="stylesheet"/>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
 
-		<div class="page-wrapper">
-			<!-- Page header -->
-			<div class="page-header d-print-none">
-				<div class="container-xl">
-					<div class="row g-2 align-items-center">
-						<div class="col">
-							<h2 class="page-title">
-								Settings
-							</h2>
-						</div>
+</head>
+<body >
+<div class="page">
+
+	<!-- EJS -->
+	<%- navbar %>
+
+	<div class="page-wrapper">
+		<!-- Page header -->
+		<div class="page-header d-print-none">
+			<div class="container-xl">
+				<div class="row g-2 align-items-center">
+					<div class="col">
+						<h2 class="page-title">
+							Settings
+						</h2>
 					</div>
 				</div>
 			</div>
-			<!-- Page body -->
-			<div class="page-body">
-				<div class="container-xl">
-					<div class="card">
-						<div class="row g-0">
-							<%- include('partials/sidebar.html') %>
-							<div class="col d-flex flex-column">
-					
-								<div class="card-body">
-									<h2 class="mb-2">Supporters</h2>
-									<p class="text-muted mb-4">[Click to Thank]</p>
-									<div class="row align-items-center">
-										<div class="col">
+		</div>
+		<!-- Page body -->
+		<div class="page-body">
+			<div class="container-xl">
+				<div class="card">
+					<div class="row g-0">
+						<%- include('partials/sidebar.html') %>
+						<div class="col d-flex flex-column">
+				
+							<div class="card-body">
+								<h2 class="mb-2">Supporters</h2>
+								<p class="text-muted mb-4">[Click to Thank]</p>
+								<div class="row align-items-center">
+									<div class="col">
 
-											<span type="button" class="avatar avatar-md bg-green-lt" hx-trigger="load, click" hx-post="/thank" hx-target="#count" name="MM"  title="MM" style="margin-right: 5px;">mm</span>
+										<span type="button" class="avatar avatar-md bg-green-lt" hx-trigger="load, click" hx-post="/thank" hx-target="#count" name="MM"  title="MM" style="margin-right: 5px;">mm</span>
 
-											<span type="button" class="avatar avatar-md bg-cyan-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="PD" title="PD" style="margin-right: 5px;">pd</span>
+										<span type="button" class="avatar avatar-md bg-cyan-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="PD" title="PD" style="margin-right: 5px;">pd</span>
 
 
-										</div>
 									</div>
 								</div>
-								<div class="card-body">
-									<p class="text-muted mb-4">Thanks counter:</p>
-									<div class="row align-items-center">
-										<div class="col">
+							</div>
+							<div class="card-body">
+								<p class="text-muted mb-4">Thanks counter:</p>
+								<div class="row align-items-center">
+									<div class="col">
 
-											<span class="avatar avatar-md bg-yellow-lt" id="count" style="margin-right: 5px;">0</span>
+										<span class="avatar avatar-md bg-yellow-lt" id="count" style="margin-right: 5px;">0</span>
 
-										</div>
 									</div>
 								</div>
 							</div>
 						</div>
-				  	</div>
+					</div>
 				</div>
 			</div>
-
-				<%- include('partials/footer.html') %>
 		</div>
+
+			<%- include('partials/footer.html') %>
 	</div>
-		<!-- Libs JS -->
-		<!-- Tabler Core -->
-		<script src="/js/tabler.min.js" defer></script>
-		<script src="/js/demo.min.js" defer></script>
-	</body>
-	</html>
+</div>
+	<!-- Libs JS -->
+	<!-- Tabler Core -->
+	<script src="/js/htmx.min.js"></script>
+	<script src="/js/tabler.min.js" defer></script>
+	<script src="/js/demo.min.js" defer></script>
+</body>
+</html>

+ 5 - 10
views/syslogs.html

@@ -1,4 +1,5 @@
 <!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
@@ -7,16 +8,7 @@
     <title>DweebUI - Syslogs</title>
     <link href="/css/tabler.min.css" rel="stylesheet"/>
     <link href="/css/demo.min.css" rel="stylesheet"/>
-    <script src="/js/htmx.min.js"></script>
-    <style>
-			@import url('/fonts/inter.css');
-			:root {
-			  --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-			}
-			body {
-			  font-feature-settings: "cv03", "cv04", "cv11";
-			}
-		  </style>
+    <link href="/css/dweebui.css" rel="stylesheet"/>
   </head>
   <body >
     <div class="page">
@@ -74,6 +66,9 @@
     </div>
     <!-- Libs JS -->
     <script src="/libs/list.js/dist/list.min.js" defer></script>
+
+    <script src="/js/htmx.min.js"></script>
+
     <!-- Tabler Core -->
     <script src="/js/tabler.min.js" defer></script>
     <script src="/js/demo.min.js" defer></script>

+ 1 - 11
views/users.html

@@ -1,24 +1,14 @@
 <!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
     <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
     <title>DweebUI - Users</title>
-		<!-- CSS files -->
 		<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
 		<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-
 		<link href="/css/dweebui.css" rel="stylesheet"/>
-    <style>
-			@import url('/fonts/inter.css');
-			:root {
-			  --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-			}
-			body {
-			  font-feature-settings: "cv03", "cv04", "cv11";
-			}
-		  </style>
   </head>
   <body >
     <div class="page">

+ 1 - 11
views/volumes.html

@@ -1,24 +1,14 @@
 <!doctype html>
+<!--Tabler - version 1.0.0-beta20 - Copyright 2018-2023 The Tabler Authors - Copyright 2018-2023 codecalm.net Paweł Kuna - Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)-->
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
     <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
     <title>DweebUI - Volumes</title>
-    <!-- CSS files -->
 		<link href="/css/tabler.min.css?1692870487" rel="stylesheet"/>
 		<link href="/css/demo.min.css?1692870487" rel="stylesheet"/>
-
 		<link href="/css/dweebui.css" rel="stylesheet"/>
-    <style>
-      @import url('/fonts/inter.css');
-      :root {
-        --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
-      }
-      body {
-        font-feature-settings: "cv03", "cv04", "cv11";
-      }
-    </style>
   </head>
   <body >
     <div class="page">