<!DOCTYPE HTML>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>-</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body class="d-flex flex-column h-100">
<div class="container flex-shrink-0">
<div class="row mt-3">
<div class="col">
<div style="display: flex; justify-content: space-between; align-items: flex-end; gap: .5em;">
<div>
<h1 class="h2" id="hostname">-</h1>
<p class="mb-0 small">refreshed every <span id="status_refresh_interval">-</span> seconds</p>
</div>
<div>
<div class="btn-group" role="group" style="width: 100%;" id="power_buttons">
<button type="button" class="btn btn-sm btn-warning" onclick="reboot();">Reboot</button>
<button type="button" class="btn btn-sm btn-danger" onclick="poweroff();">Shutdown</button>
</div>
<div title="Thermal sensor readings from /sys/class/thermal. Only the first few are shown." class="small mt-1" id="thermal"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<hr>
<p><button type="button" class="btn btn-outline-primary btn-sm mr-2" data-toggle="collapse" data-target="#collapse_top">+</button><a href="#collapse_top" data-toggle="collapse"><code>top</code></a> <span class="float-right"><small>refreshed every <span id="top_refresh_interval">-</span> seconds</small></span></p>
<div class="collapse show" id="collapse_top">
<pre class="pre-scrollable" id="top">Data not loaded.</pre>
</div>
<hr>
<p><button type="button" class="btn btn-outline-primary btn-sm mr-2" data-toggle="collapse" data-target="#collapse_docker">+</button><a href="#collapse_docker" data-toggle="collapse"><code>docker ps -a</code></a></p>
<div class="collapse" id="collapse_docker">
<div id="docker">
<pre class="pre-scrollable">Data not loaded.</pre>
</div>
</div>
<hr>
<p><button type="button" class="btn btn-outline-primary btn-sm mr-2" data-toggle="collapse" data-target="#collapse_failed_system">+</button><a href="#collapse_failed_system" data-toggle="collapse"><code>systemctl list-units --failed</code></a></p>
<div class="collapse show" id="collapse_failed_system">
<pre class="pre-scrollable" id="failed_system">Data not loaded.</pre>
</div>
<hr>
<p><button type="button" class="btn btn-outline-primary btn-sm mr-2" data-toggle="collapse" data-target="#collapse_overview_system">+</button><a href="#collapse_overview_system" data-toggle="collapse"><code>systemctl status</code></a></p>
<div class="collapse" id="collapse_overview_system">
<pre class="pre-scrollable" id="overview_system">Data not loaded.</pre>
</div>
<hr>
<p><button type="button" class="btn btn-outline-primary btn-sm mr-2" data-toggle="collapse" data-target="#collapse_timers_system">+</button><a href="#collapse_timers_system" data-toggle="collapse"><code>systemctl list-timers --all</code></a></p>
<div class="collapse" id="collapse_timers_system">
<pre class="pre-scrollable" id="timers_system">Data not loaded.</pre>
</div>
<hr>
<p><button type="button" class="btn btn-outline-primary btn-sm mr-2" data-toggle="collapse" data-target="#collapse_journal_system">+</button><a href="#collapse_journal_system" data-toggle="collapse"><code>journalctl -b --lines=20</code></a></p>
<div class="collapse" id="collapse_journal_system">
<pre class="pre-scrollable" id="journal_system">Data not loaded.</pre>
</div>
<hr>
</div>
</div>
<div id="users">
</div>
<div class="modal" id="power_request_modal_forbidden" tabindex="-1" aria-labelledby="power_request_modal_forbidden_header" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="power_request_modal_forbidden_header">Forbidden</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Sorry, power management is turned off on this instance.</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" id="reboot_modal_success" tabindex="-1" aria-labelledby="reboot_modal_success_header" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="reboot_modal_success_header">Reboot</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Done! Please give some time for the machine to come back up.</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<div class="modal" id="poweroff_modal_success" tabindex="-1" aria-labelledby="poweroff_modal_success_header" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="poweroff_modal_success_header">Shutdown</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Done! Please give some time for the machine to shut down gracefully.</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
</div>
<footer class="mt-auto py-3 border-top bg-light text-center text-muted small">
<div class="container">
<a href="https://github.com/egor-tensin/linux-status">linux-status</a> — simple Linux server monitoring by <a href="https://egort.name/">Egor Tensin</a>
</div>
</footer>
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script>
function format_duration(duration) {
let MSECS_IN_MIN = 60 * 1000;
let MSECS_IN_HOUR = 60 * MSECS_IN_MIN;
let MSECS_IN_DAY = 24 * MSECS_IN_HOUR;
let days = Math.floor(duration / MSECS_IN_DAY);
duration -= days * MSECS_IN_DAY;
let hours = Math.floor(duration / MSECS_IN_HOUR);
duration -= hours * MSECS_IN_HOUR;
let mins = Math.floor(duration / MSECS_IN_MIN);
duration -= mins * MSECS_IN_MIN;
if (days > 0) {
let result = `${days}d`;
if (days == 1 && hours > 0)
result += ` ${hours}h`;
return result;
}
if (hours > 0) {
let result = `${hours}h`;
if (hours == 1 && mins > 0)
result += ` ${mins}m`;
return result;
}
return ` ${mins}m`;
}
function dump_fail(data) {
console.log('Response code was: ' + data.status + ' ' + data.statusText);
console.log('Response was:\n' + data.responseText);
}
function get(url, success_callback, fail_callback = dump_fail) {
$.get(url, success_callback).fail(fail_callback);
}
function on_power_request_success(url) {
$('#' + url + '_modal_success').modal();
}
function on_power_request_forbidden() {
$('#power_request_modal_forbidden').modal();
}
function on_power_request_fail(data) {
switch (data.status) {
case 403:
on_power_request_forbidden();
break;
default:
dump_fail(data);
break;
}
}
function power_request(url) {
get(url, function() {
on_power_request_success(url);
}, on_power_request_fail);
}
function reboot() {
power_request('reboot');
}
function poweroff() {
power_request('poweroff');
}
function set_hostname(data) {
$('#hostname').text(data);
$('title').text(data);
}
var thermal_info_max_rows = 2;
function set_thermal(data) {
data = data.slice(0, thermal_info_max_rows);
let body = $('<tbody/>');
data.forEach(function(info) {
let type = info['type'];
let temp = info['temp'].toFixed(2) + '°C';
let row = $('<tr/>')
.append($('<td/>', {'class': 'pr-1'}).text(type))
.append($('<td/>', {'class': 'pl-1 text-right'}).html(temp));
body.append(row);
});
$('#thermal').empty();
$('#thermal').append($('<div/>', {'class': 'table-responsive'})
.append($('<table/>', {'class': 'text-nowrap', 'style': 'width: 100%;'})
.append(body)));
}
function set_top(data) {
$('#top').text(data);
}
function refresh_top() {
get('top', function(data) {
set_top(JSON.parse(data));
});
}
var top_refresh_interval_seconds = 5;
function loop_top() {
setInterval(function() { refresh_top(); }, top_refresh_interval_seconds * 1000);
$('#top_refresh_interval').text(top_refresh_interval_seconds);
}
function docker_container_is_ok(info) {
if (info.status == 'restarting' || info.status == 'dead')
return false;
if (info.status == 'exited' && info.exit_code != 0)
return false;
if (info.health == 'unhealthy')
return false;
return true;
}
function docker_container_format_status(info) {
let result = info.status;
result = result.charAt(0).toUpperCase() + result.slice(1);
if (info.status == 'exited' && info.exit_code != 0)
result += ` (${info.exit_code})`;
if (info.status == 'running') {
if (info.health == 'unhealthy') {
result = 'Up (unhealthy)';
} else {
let since = new Date(info.started_at);
result = `Up ${format_duration(Date.now() - since)}`;
}
}
return result;
}
function docker_fill_data(data) {
data.forEach(function(info) {
info.ok = docker_container_is_ok(info);
info.pretty_status = docker_container_format_status(info);
});
}
function make_docker_table_header() {
return $('<thead/>')
.append($('<tr/>')
.append($('<th/>'))
.append($('<th/>').text('Container'))
.append($('<th/>').text('Image'))
.append($('<th/>').text('Status')));
}
function make_docker_table_row(info) {
let success_mark = $('<span/>', {'class': 'text-success'}).html('✔');
let failure_mark = $('<span/>', {'class': 'text-danger'}).html('✘');
let success_class = 'table-light';
let failure_class = 'table-warning';
let mark = success_mark;
let _class = success_class;
if (!info.ok) {
mark = failure_mark;
_class = failure_class;
}
return $('<tr/>', {'class': _class})
.append($('<td/>').html(mark))
.append($('<td/>').append($('<code/>', {'class': 'text-reset'}).text(info.name)))
.append($('<td/>').append($('<code/>', {'class': 'text-reset'}).text(info.image)))
.append($('<td/>').text(info.pretty_status));
}
function make_docker_table(data) {
let body = $('<tbody/>');
data.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
data.forEach(function(info) {
body.append(make_docker_table_row(info));
});
let table = $('<div/>', {'class': 'table-responsive'})
.append($('<table/>', {'class': 'table table-hover table-sm text-nowrap mb-0'})
.append(make_docker_table_header())
.append(body));
return table;
}
function set_docker(data) {
docker_fill_data(data);
$('#docker').empty();
$('#docker').append(make_docker_table(data));
data.forEach(function(info) {
if (!info.ok)
$('#collapse_docker').addClass('show');
});
}
function set_system(data) {
if ('docker' in data) {
set_docker(data['docker']);
}
if ('failed' in data) {
$('#failed_system').text(data['failed']);
}
if ('overview' in data) {
$('#overview_system').text(data['overview']);
}
if ('timers' in data) {
$('#timers_system').text(data['timers']);
}
if ('journal' in data) {
$('#journal_system').text(data['journal']);
}
}
var users = [];
function create_user_block(name, lbl, cmd) {
let pre_id = `${lbl}_user_${name}`;
let collapse_id = `collapse_${pre_id}`;
let button_params = {
'class': 'btn btn-outline-primary btn-sm mr-2',
'data-toggle': 'collapse',
'data-target': '#' + collapse_id
};
let a_params = {
href: '#' + collapse_id,
'data-toggle': 'collapse'
};
return $('<div/>')
.append($('<p/>')
.append($('<button/>', button_params).text('+'))
.append($('<a/>', a_params)
.append($('<code/>').text(cmd))))
.append($('<div/>', {'class': 'collapse', id: collapse_id})
.append($('<pre/>', {'class': 'pre-scrollable', id: pre_id})
.text('Data not loaded.')))
.append($('<hr/>'));
}
function add_user(name) {
if (users.includes(name)) {
return;
}
let container = $('<div/>', {'class': 'row', id: 'user_' + name})
.append($('<div/>', {'class': 'col'})
.append($('<h2/>').text(name))
.append($('<hr/>'))
.append(create_user_block(name, 'failed', 'systemctl list-units --failed'))
.append(create_user_block(name, 'overview', 'systemctl status'))
.append(create_user_block(name, 'timers', 'systemctl list-timers --all'))
.append(create_user_block(name, 'journal', 'journalctl -b --lines=20')));
$('#users').append(container);
$('#collapse_failed_user_' + name).addClass('show');
users.push(name);
}
function set_user(name, data) {
add_user(name);
if ('failed' in data) {
$('#failed_user_' + name).text(data['failed']);
}
if ('overview' in data) {
$('#overview_user_' + name).text(data['overview']);
}
if ('timers' in data) {
$('#timers_user_' + name).text(data['timers']);
}
if ('journal' in data) {
$('#journal_user_' + name).text(data['journal']);
}
}
function set_users(data) {
users.forEach(function(name) {
if (!(name in data)) {
$('#user_' + name).remove();
let i = users.indexOf(name);
if (i > -1) {
users.splice(i, 1);
}
}
});
Object.keys(data).forEach(function(name) {
set_user(name, data[name]);
});
}
function refresh_status() {
get('status', function(data) {
data = JSON.parse(data);
set_hostname(data['hostname']);
set_thermal(data['thermal']);
set_system(data['system']);
set_users(data['user']);
});
}
var status_refresh_interval_seconds = 30;
function loop_status() {
setInterval(function() {
refresh_status();
}, status_refresh_interval_seconds * 1000);
$('#status_refresh_interval').text(status_refresh_interval_seconds);
}
function main() {
refresh_top();
refresh_status();
loop_top();
loop_status();
}
$(function() {
main();
});
</script>
</body>
</html>