Almost a complete rewrite, part 5. Almost ready.

This commit is contained in:
lllllllillllllillll 2024-10-27 17:55:40 -07:00
parent ec4746ebd3
commit 4bdf1e3148
44 changed files with 2066 additions and 930 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

54
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "fs";
import { execSync } from "child_process";
import { Syslog } from "../database/config.js";
import { docker } from "../utils/docker.js";
import { Syslog } from "../db/config.js";
import { docker } from "./docker.js";
import DockerodeCompose from "dockerode-compose";
import yaml from 'js-yaml';
import { Alert } from "./system.js";
export const Install = async (req, res) => {
@ -28,40 +28,50 @@ export const Install = async (req, res) => {
let containers = await docker.listContainers({ all: true });
for (let i = 0; i < containers.length; i++) {
if (containers[i].Names[0].includes(name)) {
// addAlert(req.session, 'danger', `App '${name}' already exists. Please choose a different name.`);
console.log(`App '${name}' already exists. Please choose a different name.`);
res.redirect('/');
let alert = Alert('danger', `App '${name}' already exists. Please choose a different name.`);
res.send(alert);
return;
}
}
async function composeInstall (name, compose, req) {
try {
await compose.pull().then(() => {
compose.up();
Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
// async function composeInstall (name, compose, req) {
// console.log('[composeInstall]');
// // try {
// // await compose.pull().then(() => {
// // compose.up();
// // Syslog.create({
// // user: req.session.user,
// // email: null,
// // event: "App Installation",
// // message: `${name} installed successfully`,
// // ip: req.socket.remoteAddress
// // });
// // });
// // } catch (err) {
// // await Syslog.create({
// // user: req.session.user,
// // email: null,
// // event: "App Installation",
// // message: `${name} installation failed: ${err}`,
// // ip: req.socket.remoteAddress
// // });
// // }
// await compose.pull();
// await compose.up();
// console.log('compose.up');
// }
});
} catch (err) {
await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
}
// addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
console.log(`Installing ${name}. It should appear on the dashboard shortly.`);
// Compose file installation
if (req.body.compose) {
@ -114,6 +124,7 @@ export const Install = async (req, res) => {
}
}
for (let i = 0; i < volumes.length; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
@ -185,14 +196,32 @@ export const Install = async (req, res) => {
}
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/compose.yaml`, compose_file, function (err) { console.log(err) });
console.log(`Installing ${name}. It should appear on the dashboard shortly.`);
// composeInstall(name, compose, req);
var compose = new DockerodeCompose(docker, `./appdata/${name}/compose.yaml`, `${name}`);
composeInstall(name, compose, req);
(async () => {
console.log('Pulling image');
await compose.pull();
console.log('Starting container');
await compose.up();
})();
res.redirect('/');
let alert = Alert('success', `Installing ${name}. It should appear on the dashboard shortly.`);
res.send(alert);
}
// im just going to leave this old stackfile snippet here for now
// if (image.startsWith('https://')){

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,17 @@
<div class="modal-header">
<h5 class="modal-title">Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="/install" id="install" method="POST">
<form id="details">
<div class="row mb-3 align-items-end">
<div class="col-lg-5">
<label class="form-label">Container Name: </label>
@ -219,7 +225,7 @@
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_0_check" type="checkbox" Vol0Check>
<input class="form-check-input" name="volume0" type="checkbox" Vol0Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_bind" value="Vol0Source"/>
@ -238,7 +244,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_1_check" type="checkbox" Vol1Check>
<input class="form-check-input" name="volume1" type="checkbox" Vol1Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_bind" value="Vol1Source"/>
@ -257,7 +263,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_2_check" type="checkbox" Vol2Check>
<input class="form-check-input" name="volume2" type="checkbox" Vol2Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_bind" value="Vol2Source"/>
@ -276,7 +282,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_3_check" type="checkbox" Vol3Check>
<input class="form-check-input" name="volume3" type="checkbox" Vol3Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_bind" value="Vol3Source"/>
@ -295,7 +301,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_4_check" type="checkbox" Vol4Check>
<input class="form-check-input" name="volume4" type="checkbox" Vol4Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_bind" value="Vol4Source"/>
@ -314,7 +320,7 @@
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume_5_check" type="checkbox" Vol5Check>
<input class="form-check-input" name="volume5" type="checkbox" Vol5Check>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_bind" value="Vol5Source"/>
@ -824,10 +830,13 @@
</div>
</form>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" form="install" value="Install">Install</button>
<button type="submit" class="btn btn-primary" form="install_info">Install</button>
</div>

View file

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

842
views/partials/install.html Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

84
views/partials/user.html Normal file
View file

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

View file

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

View file

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

View file

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

View file

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