aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/html/index.html
blob: f8f599c7e2a08475adee595d1d405941965d5a89 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
               
                              


                                                                                          
                    
                                                        
         

                                         
                            
                         
                                                                                                        
                 
                                                 

                                                                                                           
                 
                                                                                          


                                                                                                           
                                                                                                                                                
                  




                         
              
                                                                                                                                                                                                                                                                                                                                    
                                                       
                                                                       

                
                                                                                                                                                                                                                                  
                                                     


                                                                

                
                                                                                                                                                                                                                                                                 
                                                                 
                                                                                 

                
                                                                                                                                                                                                                                                        
                                                              
                                                                                   

                
                                                                                                                                                                                                                                                               
                                                            
                                                                                 

                
                                                                                                                                                                                                                                                              
                                                             
                                                                                  

                

              

                      
                                                                                                                                                    


                                      
                                                                                              










                                                                                                 































                                                                                                                                      
          

                                                                                  
                                                                                                                                                                            

             
                                            
                                                      
            






























                                                     




                                                                             





                                                                

 
                                       











                                                


                             


                                      

 
                   
                            

 
                     
                              

 


                              

 










                                                      

                                                                         




                                                                   
                                                                                


                            

                         

 
                        
                               
                                  


       






                                                                                    



                                                                                         

















                                                              



                                                             





















                                                                  

                                                                                











                                        

                                                                                          




                                                     


                                            



                                                          
                                                                                       
















                                                   
                           


                                   








                                                     


                                                   



               



                                            
                                                       








                                        
                                                            


                                                                   

                                                                        


                            



                               



                                                                     



                                                                                       


                                                        













                                                           


                                                         


                          








                                        





                                              
                                  
                                
                                       
                                     




                                   
                                         
 
                        
                            

                                               
                                                                        


                 
                  
                     

                  







              
<!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">&times;</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">&times;</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">&times;</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> &mdash; simple Linux server monitoring by <a href="https://tensin.name/">Egor&nbsp;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) + '&deg;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);
}

// Container's State.Status field values are described in the API reference:
//
//     https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerInspect
//
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') {
        let since = new Date(info.started_at);
        result = `Up ${format_duration(Date.now() - since)}`;
        if (info.health == 'unhealthy')
            result = `${result} (unhealthy)`;
    }

    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('&#10004;');
    let failure_mark = $('<span/>', {'class': 'text-danger'}).html('&#10008;');
    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>