dashboard.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
  2. import { docker, containerInfo, containerLogs, containerStats, GetContainerLists } from '../utils/docker.js';
  3. import { readFileSync } from 'fs';
  4. import { User, Permission, ServerSettings, ContainerLists, Container } from '../database/config.js';
  5. import { Alert, Navbar, Footer, Capitalize } from '../utils/system.js';
  6. import { Op } from 'sequelize';
  7. let [ hidden, alert, stats ] = [ '', '', '', {} ];
  8. let container_link = 'http://localhost';
  9. // Dashboard
  10. export const Dashboard = async function (req, res) {
  11. let host = req.params.host || 1;
  12. req.session.host = `${host}`;
  13. // Create the lists needed for the dashboard
  14. const [list, created] = await ContainerLists.findOrCreate({
  15. where: { userID: req.session.userID },
  16. defaults: {
  17. userID: req.session.userID,
  18. username: req.session.username,
  19. containers: '[]',
  20. new: '[]',
  21. updates: '[]',
  22. sent: '[]',
  23. },
  24. });
  25. if (created) { console.log(`New entry created in ContainerLists for ${req.session.username}`); }
  26. res.render("dashboard",{
  27. alert: '',
  28. username: req.session.username,
  29. role: req.session.role,
  30. navbar: await Navbar(req),
  31. footer: await Footer(req),
  32. });
  33. }
  34. // Dashboard search
  35. export const searchDashboard = async function (req, res) {
  36. console.log(`[Search] ${req.body.search}`);
  37. res.send('ok');
  38. return;
  39. }
  40. // Server metrics (CPU, RAM, TX, RX, DISK)
  41. export const ServerMetrics = async (req, res) => {
  42. let name = req.header('hx-trigger-name');
  43. let color = req.header('hx-trigger');
  44. let value = 0;
  45. switch (name) {
  46. case 'CPU':
  47. await currentLoad().then(data => { value = Math.round(data.currentLoad); });
  48. break;
  49. case 'RAM':
  50. await mem().then(data => { value = Math.round((data.active / data.total) * 100); });
  51. break;
  52. case 'NET':
  53. let [down, up, percent] = [0, 0, 0];
  54. await networkStats().then(data => { down = Math.round(data[0].rx_bytes / (1024 * 1024)); up = Math.round(data[0].tx_bytes / (1024 * 1024)); percent = Math.round((down / 1000) * 100); });
  55. let net = `<div class="font-weight-medium"><label class="cpu-text mb-1">Down:${down}MB Up:${up}MB</label></div>
  56. <div class="cpu-bar meter animate ${color}"><span style="width:20%"><span></span></span></div>`;
  57. res.send(net);
  58. return;
  59. case 'DISK':
  60. await fsSize().then(data => { value = data[0].use; });
  61. break;
  62. }
  63. let info = `<div class="font-weight-medium"> <label class="cpu-text mb-1">${name} ${value}%</label></div>
  64. <div class="cpu-bar meter animate ${color}"><span style="width:${value}%"><span></span></span></div>`;
  65. res.send(info);
  66. }
  67. async function userCards (req) {
  68. let container_list = [];
  69. // Check what containers the user has hidden.
  70. let hidden = await Permission.findAll({ where: {userID: req.session.userID, hide: true}}, { attributes: ['containerID'] });
  71. hidden = hidden.map((container) => container.containerID);
  72. // Check what containers the user has permission for.
  73. let visable = await Permission.findAll({ where: { userID: req.session.userID, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] }, attributes: ['containerID'] });
  74. visable = visable.map((container) => container.containerID);
  75. let containers = await GetContainerLists(req.session.host);
  76. for (let i = 0; i < containers.length; i++) {
  77. let container_name = containers[i].Names[0].split('/').pop();
  78. // Skip if the ID is found in the hidden list.
  79. if (hidden.includes(containers[i].Id)) { continue; }
  80. // Skip if the state is 'created'.
  81. if (containers[i].State == 'created') { continue; }
  82. // Admin can see all containers that they don't have hidden.
  83. if (req.session.role == 'admin') { container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); }
  84. // User can see any containers that they have any permissions for.
  85. else if (visable.includes(containers[i].Id)){ container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); }
  86. }
  87. return container_list;
  88. }
  89. // Container actions (start, stop, pause, restart, hide)
  90. export const ContainerAction = async (req, res) => {
  91. let container_name = req.header('hx-trigger-name');
  92. let containerID = req.params.containerid;
  93. let action = req.params.action;
  94. console.log(`[Action] ${action} ${container_name} ${containerID}`);
  95. if (action == 'reset') {
  96. console.log('Resetting view');
  97. await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
  98. res.redirect('/dashboard');
  99. return;
  100. }
  101. else if (action == 'logs') {
  102. let logs = await containerLogs(containerID);
  103. let modal = readFileSync('./views/partials/logs.html', 'utf8');
  104. modal = modal.replace(/AppName/g, container_name);
  105. modal = modal.replace(/ContainerID/g, containerID);
  106. modal = modal.replace(/ContainerLogs/g, logs);
  107. res.send(modal);
  108. return;
  109. }
  110. else if (action == 'details') {
  111. let container = await containerInfo(containerID);
  112. let modal = readFileSync('./views/partials/details.html', 'utf8');
  113. modal = modal.replace(/AppName/g, container.containerName);
  114. modal = modal.replace(/AppImage/g, container.containerImage);
  115. res.send(modal);
  116. return;
  117. }
  118. else if (action == 'uninstall') {
  119. let modal = readFileSync('./views/partials/uninstall.html', 'utf8');
  120. modal = modal.replace(/AppName/g, container_name);
  121. modal = modal.replace(/ContainerID/g, containerID);
  122. res.send(modal);
  123. return;
  124. }
  125. else if (action == 'link_modal') {
  126. const [container, created] = await Container.findOrCreate({ where: { containerID: containerID }, defaults: { containerName: container_name, containerID: containerID, link: '' } });
  127. let modal = readFileSync('./views/partials/link.html', 'utf8');
  128. modal = modal.replace(/AppName/g, container_name);
  129. modal = modal.replace(/ContainerID/g, containerID);
  130. modal = modal.replace(/AppLink/g, container.link);
  131. res.send(modal);
  132. return;
  133. } else if (action == 'update_link') {
  134. let url = req.body.url;
  135. console.log(url);
  136. // find the container entry with the containerID and userID
  137. let container = await Container.findOne({ where: { containerID: containerID } });
  138. container.update({ link: url });
  139. res.send('ok');
  140. return;
  141. }
  142. // Inspect the container
  143. let info = docker.getContainer(containerID);
  144. let container = await info.inspect();
  145. let containerState = container.State.Status;
  146. // Displays container state (starting, stopping, restarting, pausing)
  147. function status (state) {
  148. return(`<div class="text-yellow d-inline-flex align-items-center lh-1 ms-auto" id="AltIDState">
  149. <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>
  150. <strong>${state}</strong>
  151. </div>`);
  152. }
  153. // Perform the action
  154. if ((action == 'start') && (containerState == 'exited')) {
  155. info.start();
  156. res.send(status('starting'));
  157. } else if ((action == 'start') && (containerState == 'paused')) {
  158. info.unpause();
  159. res.send(status('starting'));
  160. } else if ((action == 'stop') && (containerState != 'exited')) {
  161. info.stop();
  162. res.send(status('stopping'));
  163. } else if ((action == 'pause') && (containerState == 'paused')) {
  164. info.unpause();
  165. res.send(status('starting'));
  166. } else if ((action == 'pause') && (containerState == 'running')) {
  167. info.pause();
  168. res.send(status('pausing'));
  169. } else if (action == 'restart') {
  170. info.restart();
  171. res.send(status('restarting'));
  172. } else if (action == 'hide') {
  173. let exists = await Permission.findOne({ where: { containerID: containerID, userID: req.session.userID }});
  174. if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: containerID, username: req.session.username, userID: req.session.userID, hide: true }); }
  175. else { exists.update({ hide: true }); }
  176. res.send('ok');
  177. }
  178. }
  179. async function createCard (details) {
  180. // let trigger = 'data-hx-trigger="load, every 3s"';
  181. let containerName = details.containerName;
  182. if (containerName.length > 13) { containerName = containerName.substring(0, 13) + '...'; }
  183. let containerTitle = Capitalize(containerName);
  184. let container_link = '';
  185. let container = await Container.findOne({ where: { containerID: details.containerID } });
  186. container_link = container.link || '#';
  187. let titleLink = `<a href="${container_link}" class="nav-link" target="_blank">${containerTitle}</a>`;
  188. let containerID = details.containerID;
  189. let containerState = details.containerState;
  190. let containerService = details.containerService;
  191. let containerStateColor = '';
  192. if (containerState == 'running') { containerStateColor = 'green'; }
  193. else if (containerState == 'exited') { containerStateColor = 'red'; containerState = 'stopped'; }
  194. else if (containerState == 'paused') { containerStateColor = 'orange'; }
  195. else { containerStateColor = 'blue'; }
  196. let container_card = readFileSync('./views/partials/container_card.html', 'utf8');
  197. container_card = container_card.replace(/ContainerID/g, containerID);
  198. container_card = container_card.replace(/AltID/g, 'a' + containerID);
  199. container_card = container_card.replace(/TitleLink/g, titleLink);
  200. container_card = container_card.replace(/AppName/g, containerName);
  201. container_card = container_card.replace(/AppTitle/g, containerTitle);
  202. container_card = container_card.replace(/AppService/g, containerService);
  203. container_card = container_card.replace(/AppState/g, containerState);
  204. container_card = container_card.replace(/StateColor/g, containerStateColor);
  205. if (details.external_port == 0 && details.internal_port == 0) {
  206. container_card = container_card.replace(/AppPorts/g, ``);
  207. } else {
  208. 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>`);
  209. }
  210. return container_card;
  211. }
  212. export const UpdateCard = async function (req, res) {
  213. let containerID = req.params.containerid;
  214. let lists = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['containers'] });
  215. let container_list = JSON.parse(lists.containers);
  216. let found = container_list.find(c => c.containerID === containerID);
  217. if (!found) { res.send(''); return; }
  218. let details = await containerInfo(containerID);
  219. let card = await createCard(details);
  220. res.send(card);
  221. }
  222. export const CardList = async function (req, res) {
  223. let cards_list = '';
  224. // Check if there are any new cards in queue.
  225. let new_cards = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['new'] });
  226. let new_list = JSON.parse(new_cards.new);
  227. // Check what containers the user should see.
  228. let containers = await userCards(req);
  229. // Create the cards.
  230. if (new_list.length > 0) {
  231. for (let i = 0; i < new_list.length; i++) {
  232. let details = await containerInfo(new_list[i]);
  233. let card = await createCard(details);
  234. cards_list += card;
  235. }
  236. } else {
  237. for (let i = 0; i < containers.length; i++) {
  238. let details = await containerInfo(containers[i].containerID);
  239. let card = await createCard(details);
  240. cards_list += card;
  241. }
  242. }
  243. // Update lists, clear the queue, and send the cards.
  244. await ContainerLists.update({ containers: JSON.stringify(containers), sent: JSON.stringify(containers), new: '[]' }, { where: { userID: req.session.userID } });
  245. res.send(cards_list);
  246. }
  247. // HTMX - Server-side events
  248. export const SSE = async (req, res) => {
  249. res.writeHead(200, {
  250. 'Content-Type': 'text/event-stream',
  251. 'Cache-Control': 'no-cache',
  252. 'Connection': 'keep-alive'
  253. });
  254. async function eventCheck () {
  255. let list = await ContainerLists.findOne({ where: { userID: req.session.userID }, attributes: ['sent'] });
  256. let container_list = await userCards(req);
  257. let new_cards = [];
  258. let update_list = [];
  259. let sent_cards = [];
  260. sent_cards = JSON.parse(list.sent);
  261. if (JSON.stringify(container_list) == list.sent) { return; }
  262. console.log(`Update for ${req.session.username}`);
  263. // loop through the containers list to see if any new containers have been added or changed
  264. container_list.forEach(container => {
  265. let { containerName, containerID, containerState } = container;
  266. if (list.sent) { sent_cards = JSON.parse(list.sent); }
  267. let found = sent_cards.find(c => c.containerID === containerID);
  268. if (!found) { new_cards.push(containerID); }
  269. else if (found.containerState !== containerState) { update_list.push(containerID); }
  270. });
  271. // loop through the sent list to see if any containers have been removed
  272. sent_cards.forEach(container => {
  273. let { containerName, containerID, containerState } = container;
  274. let found = container_list.find(c => c.containerID === containerID);
  275. if (!found) { update_list.push(containerID); }
  276. });
  277. await ContainerLists.update({ new: JSON.stringify(new_cards), sent: JSON.stringify(container_list), containers: JSON.stringify(container_list) }, { where: { userID: req.session.userID } });
  278. if (update_list.length > 0 ) {
  279. for (let i = 0; i < update_list.length; i++) {
  280. res.write(`event: ${update_list[i]}\n`);
  281. res.write(`data: 'update cards'\n\n`);
  282. }
  283. }
  284. if (new_cards.length > 0) {
  285. res.write(`event: update\n`);
  286. res.write(`data: 'card updates'\n\n`);
  287. }
  288. }
  289. docker.getEvents({}, async function (err, data) {
  290. data.on('data', async function () {
  291. console.log(`[Docker Event]`);
  292. await eventCheck();
  293. });
  294. });
  295. req.on('close', async () => {
  296. // Nothing
  297. });
  298. }