%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /backups/router/usr/local/opnsense/mvc/app/views/OPNsense/Core/
Upload File :
Create Path :
Current File : //backups/router/usr/local/opnsense/mvc/app/views/OPNsense/Core/firmware.volt

{#
 # Copyright (c) 2015-2023 Franco Fichtner <franco@opnsense.org>
 # Copyright (c) 2015-2018 Deciso B.V.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without modification,
 # are permitted provided that the following conditions are met:
 #
 # 1.  Redistributions of source code must retain the above copyright notice,
 # this list of conditions and the following disclaimer.
 #
 # 2.  Redistributions in binary form must reproduce the above copyright notice,
 # this list of conditions and the following disclaimer in the documentation
 # and/or other materials provided with the distribution.
 #
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 #}

<style>
    /* "dropdown in responsive table" issue workaround: display dummy road so dropdowns fits the table */
    @media screen and (max-width: 767px) {
        .dropdown_helper {
            display: block !important;
        }
    }
    .dropdown_helper {
        display: none;
    }
</style>

<script>

    function generic_search(that, entries) {
        let search = $(that).val().toLowerCase();
        $('.' + entries).each(function () {
            let row_by_col = $(this).find('td').map(function () {
                return $(this).text();
            }).get();
            let name = row_by_col.join(',').toLowerCase();
            if (search.length != 0 && name.indexOf(search) == -1) {
                $(this).hide();
            } else {
                $(this).show();
            }
        });
    }

    function cancel_update() {
        $('#updatelist').hide();
        $('#update_status_container').show();
    }

    /**
     * retrieve update status from backend
     */
    function updateStatus() {
        $('#upgrade_maj').hide();
        $('#upgrade').hide();

        ajaxGet('/api/core/firmware/status', {}, function (data, status){
            if (data['status'] == "update") {
                let show_log = '';

                $.status_reboot = data['status_reboot'];

                // show upgrade list
                $('#upgrade').show();
                $('#updatestatus').html(data['status_msg']);
                $('#updatelist > tbody').empty();
                $('#updatetab > a').tab('show');
                $.each(data['all_packages'], function (index, row) {
                    $('#updatelist > tbody').append('<tr><td>'+row['name']+'</td>' +
                    '<td>'+row['old']+'</td><td>'+row['new']+'</td><td>' +
                    row['reason']+'</td><td>'+row['repository'] + '</td></tr>');

                    if (row['name'] == data['product_target'] && row['new'] != 'N/A') {
                        show_log = row['new'].replace(/[_-].*/, '');
                    }
                });
                $('#update_status_container').hide();
                $('#updatelist').show();

                // display the current changelog if one was found
                if (show_log != '') {
                    changelog(show_log);
                }

                packagesInfo(false);
            } else if (data['status'] == "upgrade") {
                $.status_reboot = data['status_reboot'];

                if (data['upgrade_major_message'] != '') {
                    /* we trust this data, it was signed by us and secured by csrf */
                    stdDialogInform(
                        '{{ lang._('Upgrade instructions') }}',
                        htmlDecode(data['upgrade_major_message']),
                        "{{ lang._('OK') }}",
                        function () { show_upgrade(data) },
                        'warning'
                    );
                } else {
                    show_upgrade(data);
                }

                packagesInfo(false);
            } else if (data['status'] == "error") {
                stdDialogInform('{{ lang._('Firmware status') }}', data['status_msg'], "{{ lang._('Close') }}", undefined, 'danger');
                packagesInfo(true);
            } else if (data['status'] == "none") {
                stdDialogInform('{{ lang._('Firmware status') }}', data['status_msg'], "{{ lang._('Close') }}", undefined, 'success');
                packagesInfo(true);
            } else {
                // in case new responses are added
                console.log('Unknown check response');
                console.log(data);
            }
        });
    }

    /**
     * perform backend action and install poller to update status
     */
    function backend(type) {
        $.upgrade_check = type == 'check'

        $('#update_status').html('');
        $('#updatelist').hide();
        $('#update_status_container').show();
        $('#updatetab > a').tab('show');
        $('#updatetab_progress').addClass("fa fa-spinner fa-pulse");

        ajaxCall('/api/core/firmware/' + type, {}, function () {
            setTimeout(trackStatus, 500);
        });
    }

    /**
     * read package details from backend
     */
    function details(package)
    {
        ajaxCall('/api/core/firmware/details/' + package, {}, function (data, status) {
            var details = "{{ lang._('Sorry, plugin details are currently not available.') }}";
            if (data['details'] != undefined) {
                details = data['details'];
            }
            stdDialogInform("{{ lang._('Plugin details') }}", details, "{{ lang._('Close') }}");
        });
    }

    /**
     * read license from backend
     */
    function license(package)
    {
        ajaxCall('/api/core/firmware/license/' + package, {}, function (data, status) {
            var license = "{{ lang._('Sorry, the package does not have an associated license file.') }}";
            if (data['license'] != undefined) {
                license = data['license'];
            }
            stdDialogInform("{{ lang._('License details') }}", license, "{{ lang._('Close') }}");
        });
    }

    /**
     * read changelog from backend
     */
    function changelog(version)
    {
        ajaxCall('/api/core/firmware/changelog/' + version, {}, function (data, status) {
            if (data['html'] != undefined) {
                /* we trust this data, it was signed by us and secured by csrf */
                stdDialogInform(version, htmlDecode(data['html']), "{{ lang._('Close') }}", undefined, 'primary');
            }
        });
    }

    /**
     * perform package action that requires reboot confirmation
     */
    function action_may_reboot(pkg_act, pkg_name)
    {
        if (pkg_act == 'reinstall' && (pkg_name == 'kernel' || pkg_name == 'base')) {
            // reboot required, inform the user.
            BootstrapDialog.show({
                type:BootstrapDialog.TYPE_WARNING,
                title: "{{ lang._('Reboot required') }}",
                message: "{{ lang._('The firewall will reboot directly after this set reinstall.') }}",
                buttons: [{
                    label: "{{ lang._('OK') }}",
                    cssClass: 'btn-warning',
                    action: function(dialogRef){
                        dialogRef.close();
                        backend(pkg_act + "/" + pkg_name);
                    }
                },{
                    label: "{{ lang._('Cancel') }}",
                    action: function(dialogRef){
                        dialogRef.close();
                    }
                }]
            });
        } else {
            backend(pkg_act + "/" + pkg_name);
        }
    }

    function show_upgrade(data) {
        $('#upgrade_maj').show();
        $('#updatestatus').html(data['status_msg']);
        $('#updatelist > tbody').empty();
        $('#updatetab > a').tab('show');
        $.each(data['all_sets'], function (index, row) {
            $('#updatelist > tbody').append('<tr><td>'+row['name']+'</td>' +
            '<td>'+row['old']+'</td><td>'+row['new']+'</td><td>' +
            row['reason']+'</td><td>'+row['repository'] + '</td></tr>');
        });
        $('#update_status_container').hide();
        $('#updatelist').show();
        changelog(data['upgrade_major_version']);
    }

    /**
     *  check if a reboot is required, warn user or just upgrade
     */
    function upgrade_ui(major)
    {
        if (major !== true && $.status_reboot != "1") {
            backend('update');
        } else {
            let reboot_msg = "{{ lang._('The firewall will reboot directly after this firmware update.') }}";
            if (major === true) {
                reboot_msg = "{{ lang._('The firewall will download all firmware sets and reboot multiple times for this upgrade. All operating system files and packages will be reinstalled as a consequence. This may take several minutes to complete.') }}";
            }
            // reboot required, inform the user.
            BootstrapDialog.show({
                type:BootstrapDialog.TYPE_WARNING,
                title: "{{ lang._('Reboot required') }}",
                message: reboot_msg,
                buttons: [{
                    label: "{{ lang._('OK') }}",
                    cssClass: 'btn-warning',
                    action: function(dialogRef){
                        dialogRef.close();
                        backend(major === true ? 'upgrade' : 'update');
                    }
                },{
                    label: "{{ lang._('Cancel') }}",
                    action: function(dialogRef){
                        dialogRef.close();
                    }
                }]
            });
        }
    }

    function rebootWait() {
        $.ajax({
            url: '/',
            timeout: 2500
        }).fail(function () {
            setTimeout(rebootWait, 2500);
        }).done(function () {
            $(location).attr('href', '/');
        });
    }

    function trackStatus() {
        ajaxGet('/api/core/firmware/upgradestatus', { v: Date.now() }, function(data, status) {
            if (status != 'success') {
                // recover from temporary errors
                setTimeout(trackStatus, 1000);
                return;
            }
            if (data['log'] != undefined && data['log'] != '') {
                var autoscroll = $('#update_status')[0].scrollTop +
                    $('#update_status')[0].clientHeight ===
                    $('#update_status')[0].scrollHeight;
                $('#update_status').html(data['log']);
                if (autoscroll) {
                    $('#update_status').scrollTop($('#update_status')[0].scrollHeight);
                }
            }
            if (data['status'] == 'done') {
                $('#updatetab_progress').removeClass("fa fa-spinner fa-pulse");
                if ($.upgrade_check === true) {
                    updateStatus();
                } else {
                    packagesInfo(true);
                }
            } else if (data['status'] == 'reboot') {
                BootstrapDialog.show({
                    type:BootstrapDialog.TYPE_INFO,
                    title: "{{ lang._('Your device is rebooting') }}",
                    closable: false,
                    message: "{{ lang._('The upgrade has finished and your device is being rebooted at the moment, please wait...') }}" +
                        ' <i class="fa fa-cog fa-spin"></i>',
                    onshow: function (dialogRef) {
                        setTimeout(rebootWait, 45000);
                    },
                });
            } else {
                // schedule next poll
                setTimeout(trackStatus, 500);
            }
        });
    }

    /**
     * show package info
     */
    function packagesInfo(reset) {
        $("#statustab_progress").addClass("fa fa-spinner fa-pulse");
        ajaxGet('/api/core/firmware/info', {}, function (data, status) {
            $('#packageslist > tbody').empty();
            $('#pluginlist > tbody').empty();
            var installed = {};

            $.each(data['product'], function(key, value) {
                if (key == 'product_check') {
                    if (value != null) {
                        $('#product_time_check').text(value['last_check']);
                    } else {
                        $('#product_time_check').text("{{ lang._('N/A') }}");
                    }
                } else {
                    $('#' + key).text(value);
                }
            });

            if (data['product']['product_license'] != undefined) {
                $.each(data['product']['product_license'], function(key, value) {
                    $('#product_license_' + key).text(value).closest('tr').show();
                });
                if (!Object.keys(data['product']['product_license']).length) {
                    $('[id^=product_license_]').closest('tr').hide();
                }
            }

            $("#statustab_progress").removeClass("fa fa-spinner fa-pulse");

            if (reset === true) {
                ajaxGet('/api/core/firmware/upgradestatus', { v: Date.now() }, function(data, status) {
                    if (data['log'] != undefined && data['log'] != '') {
                        $('#update_status').html(data['log']);
                    } else {
                        $('#update_status').html('{{ lang._('No previous action log found.') }}');
                    }
                    $('#update_status').scrollTop($('#update_status')[0].scrollHeight);
                });

                $('#updatelist').hide();
                $('#update_status_container').show();
            }

            var local_count = 0;
            var plugin_count = 0;
            var misconfigured_plugins = 0;
            var missing_plugins = 0;
            var changelog_count = 0;
            var changelog_max = 15;
            if ($.changelog_keep_full != undefined) {
                changelog_max = 9999;
            }

            $.each(data['package'], function(index, row) {
                if (row['installed'] == "1") {
                    local_count += 1;
                } else {
                    return 1;
                }
                $('#packageslist > tbody').append(
                    '<tr class="package_entry">' +
                    '<td>' + row['name'] + '</td>' +
                    '<td>' + row['version'] + '</td>' +
                    '<td>' + row['flatsize'] + '</td>' +
                    '<td>' + row['repository'] + '</td>' +
                    '<td>' + row['license'] + '</td>' +
                    '<td>' + row['comment'] + '</td>' +
                    '<td style="white-space:nowrap;vertical-align:middle;"><div class="input-group">' +
                    '<button class="btn btn-default btn-xs act_license" data-package="' + row['name'] + '" ' +
                    '  data-toggle="tooltip" title="{{ lang._('License') }}">' +
                    '<i class="fa fa-balance-scale fa-fw"></i></button> ' +
                    '<button class="btn btn-default btn-xs act_reinstall" data-package="' + row['name'] + '" ' +
                    '  data-toggle="tooltip" title="{{ lang._('Reinstall') }}">' +
                    '<i class="fa fa-recycle fa-fw"></i></button> ' + (row['locked'] === '1' ?
                        '<button data-toggle="tooltip" title="{{ lang._('Unlock') }}" class="btn btn-default btn-xs act_unlock" data-package="' + row['name'] + '">' +
                        '<i class="fa fa-lock fa-fw">' +
                        '</i></button>' :
                        '<button class="btn btn-default btn-xs act_lock" data-package="' + row['name'] + '" ' +
                        '  data-toggle="tooltip" title="{{ lang._('Lock') }}" >' +
                        '<i class="fa fa-unlock fa-fw"></i></button>'
                    ) + '</div></td>' +
                    '</tr>'
                );
            });

            if (local_count == 0) {
                $('#packageslist > tbody').append(
                    '<tr><td colspan=6>{{ lang._('No packages were found on your system. Please call for help.') }}</td></tr>'
                );
            }

            $.each(data['plugin'], function(index, row) {
                if (row['provided'] == "1") {
                    plugin_count += 1;
                }
                let status_text = '';
                let bold_on = '';
                let bold_off = '';
                if (row['installed'] == "1" && row['configured'] == "0") {
                    status_text = ' ({{ lang._('misconfigured') }})';
                    bold_on = '<b>';
                    bold_off = '</b>';
                    misconfigured_plugins = 1;
                } else if (row['installed'] == "0" && row['configured'] == "1") {
                    status_text = ' ({{ lang._('missing') }})';
                    bold_on = '<span class="text-danger"><b>';
                    bold_off = '</b></span>';
                    missing_plugins = 1;
                } else if (row['installed'] == "1") {
                    if (row['provided'] == "0") {
                        status_text = ' ({{ lang._('orphaned') }})';
                    } else {
                        status_text = ' ({{ lang._('installed') }})';
                    }
                    bold_on = '<b>';
                    bold_off = '</b>';
                }
                $('#pluginlist > tbody').append(
                    '<tr class="plugin_entry">' + '<td>' + bold_on + row['name'] + status_text + bold_off + '</td>' +
                    '<td>' + bold_on + row['version'] + bold_off + '</td>' +
                    '<td>' + bold_on + row['flatsize'] + bold_off + '</td>' +
                    '<td>' + bold_on + row['tier'] + bold_off + '</td>' +
                    '<td>' + bold_on + row['repository'] + bold_off + '</td>' +
                    '<td>' + bold_on + row['comment'] + bold_off + '</td>' +
                    '<td style="white-space:nowrap;vertical-align:middle;"><div class="input-group">' +
                    '<button class="btn btn-default btn-xs act_details" data-package="' + row['name'] + '" ' +
                        ' data-toggle="tooltip" title="{{ lang._('Info') }}">' +
                        '<i class="fa fa-info-circle fa-fw"></i></button>' +
                        (row['installed'] == "1" ?
                        '<button class="btn btn-default btn-xs act_remove" data-package="' + row['name'] + '" '+
                        '  data-toggle="tooltip" title="{{ lang._('Remove') }}">' +
                        '<i class="fa fa-trash fa-fw">' +
                        '</i></button>' :
                        '<button class="btn btn-default btn-xs act_install" data-package="' + row['name'] + '" ' +
                        'data-repository="'+row['repository']+'" data-toggle="tooltip" title="{{ lang._('Install') }}">' +
                        '<i class="fa fa-plus fa-fw">' +
                        '</i></button>'
                    ) + '</div></td>' + '</tr>'
                );
            });

            if (plugin_count == 0) {
                $('#pluginlist > tbody').append(
                    '<tr><td colspan=5>{{ lang._('Check for updates to view available plugins.') }}</td></tr>'
                );
            }

            if (data['product']['product_log']) {
                $('#audit_upgrade').parent().show();
            } else {
                $('#audit_upgrade').parent().hide();
            }
            $('#audit_actions').show();
            $("#plugin_search").keyup();
            $("#package_search").keyup();

            if (misconfigured_plugins || missing_plugins) {
                if (!missing_plugins) {
                    $("#plugin_get").parent().hide();
                } else {
                    $("#plugin_get").parent().show();
                }
                $('#plugin_actions').show();
            } else {
                $('#plugin_actions').hide();
            }

            $("#changeloglist > tbody").empty();
            $("#changeloglist > thead").html("<tr><th>{{ lang._('Version') }}</th>" +
            "<th>{{ lang._('Date') }}</th><th></th></tr>");

            const installed_version = data['product_version'].replace(/[_-].*/, '');

            $.each(data['changelog'], function(index, row) {
                changelog_count += 1;

                let status_text = '';
                let bold_on = '';
                let bold_off = '';

                if (installed_version == row['version']) {
                    status_text = ' ({{ lang._('installed') }})';
                    bold_on = '<b>';
                    bold_off = '</b>';
                }

                $('#changeloglist > tbody').append(
                    '<tr' + (changelog_count > changelog_max ? ' class="changelog-hidden" style="display: none;" ' : '' ) +
                    '><td>' + bold_on + row['version'] + status_text + bold_off + '</td><td>' + bold_on + row['date'] + bold_off + '</td>' +
                    '<td><button class="btn btn-default btn-xs act_changelog" data-version="' + row['version'] + '" ' +
                    'data-toggle="tooltip" title="{{ lang._('View') }}">' +
                    '<i class="fa fa-book fa-fw"></i></button></td></tr>'
                );
            });

            if (!data['changelog'].length) {
                $('#changeloglist > tbody').append(
                    '<tr><td colspan=3>{{ lang._('Check for updates to view changelog history.') }}</td></tr>'
                );
            }

            if (changelog_count > changelog_max) {
                $('#changeloglist > tbody').append(
                    '<tr class= "changelog-full"><td colspan=3><a id="changelog-act" href="#">{{ lang._('Click to view full changelog history.') }}</a></td></tr>'
                );
                $("#changelog-act").click(function(event) {
                    event.preventDefault();
                    $(".changelog-hidden").attr('style', '');
                    $(".changelog-full").attr('style', 'display: none;');
                    $.changelog_keep_full = 1;
                });
            }

            // link buttons to actions
            $(".act_reinstall").click(function(event) {
                event.preventDefault();
                action_may_reboot('reinstall', $(this).data('package'));
            });
            $(".act_unlock").click(function(event) {
                event.preventDefault();
                backend('unlock/' + $(this).data('package'));
            });
            $(".act_lock").click(function(event) {
                event.preventDefault();
                backend('lock/' + $(this).data('package'));
            });
            $(".act_remove").click(function(event) {
                event.preventDefault();
                let plugin_name = $(this).data('package');
                BootstrapDialog.show({
                    type:BootstrapDialog.TYPE_WARNING,
                    title: "{{ lang._('Confirm removal') }}",
                    message: "{{ lang._('Do you really want to remove this plugin?') }}" + " <strong>" + plugin_name + "</strong>",
                    buttons: [{
                        label: "{{ lang._('OK') }}",
                        cssClass: 'btn-warning',
                        action: function (dialogRef) {
                            dialogRef.close();
                            backend('remove/' + plugin_name);
                        }
                    },{
                        label: "{{ lang._('Cancel') }}",
                        action: function(dialogRef){
                            dialogRef.close();
                        }
                    }]
                });
            });
            $(".act_details").click(function(event) {
                event.preventDefault();
                details($(this).data('package'));
            });
            $(".act_install").click(function(event) {
                event.preventDefault();
                let package_name = $(this).data('package');
                /* XXX temporary placeholder to inform the user that he/she is installing from a different (external) source */
                if ($(this).data('repository') !== 'OPNsense') {
                    BootstrapDialog.show({
                        type:BootstrapDialog.TYPE_INFO,
                        title: "{{ lang._('Third party software') }}",
                        message: "{{ lang._('This software package is provided by an external vendor, for more information contact the author')}}",
                        buttons: [{
                            label: "{{ lang._('Install') }}",
                            action: function(dialogRef){
                                dialogRef.close();
                                backend('install/' + package_name);
                            }
                        }, {
                            label: "{{ lang._('Cancel') }}",
                            action: function(dialogRef){
                                dialogRef.close();
                            }
                        }]
                    });
                } else {
                    backend('install/' + package_name);
                }
            });
            $(".act_changelog").click(function(event) {
                event.preventDefault();
                changelog($(this).data('version'));
            });
            $(".act_license").click(function(event) {
                event.preventDefault();
                license($(this).data('package'));
            });
            // attach tooltip to generated buttons
            $('[data-toggle="tooltip"]').tooltip();
        });
    }

    $( document ).ready(function() {
        // link event handlers
        $('#checkupdate').click(function () { backend('check'); });
        $('#upgrade').click(function () { upgrade_ui(false); });
        $('#upgrade_maj').click(function () { upgrade_ui(true); });
        $('#upgrade_cancel').click(cancel_update);
        $("#plugin_see").click(function () { $('#plugintab > a').tab('show'); });
        $("#plugin_get").click(function () { backend('syncPlugins'); });
        $("#plugin_set").click(function () { backend('resyncPlugins'); });
        $('#audit_security').click(function () { backend('audit'); });
        $('#audit_connection').click(function () { backend('connection'); });
        $('#audit_health').click(function () { backend('health'); });
        $('#audit_upgrade').click(function () {
            ajaxCall('/api/core/firmware/log/0', {}, function (data, status) {
                if (data['log'] != undefined) {
                    stdDialogConfirm("{{ lang._('Upgrade log') }}", data['log'],
                      "{{ lang._('Clear') }}", "{{ lang._('Close') }}", function () {
                        ajaxCall('/api/core/firmware/log/1');
                        $('#audit_upgrade').parent().hide();
                      }, 'primary');
                }
            });
        });

        // populate package information
        packagesInfo(true);

        $("#plugin_search").keyup(function () { generic_search(this, 'plugin_entry'); });
        $("#package_search").keyup(function () { generic_search(this, 'package_entry'); });

        ajaxGet('/api/core/firmware/running', {}, function(data, status) {
            if (data['status'] == 'busy') {
                // if action is already running reattach now...
                backend('audit');
            } else if (window.location.hash == '#checkupdate') {
                // dashboard link: run check automatically after delay
                setTimeout(function () { backend('check'); }, 2000);
            }
        });

        // handle firmware config options
        function fillOptions() {
            ajaxGet('/api/core/firmware/get_options', {}, function(firmwareoptions, status) {
                ajaxGet('/api/core/firmware/get', {}, function(config, status) {
                    var firmwareconfig = config['firmware'];
                    var custom_selected = true;

                    $("#firmware_mirror").find('option').remove();
                    $("#firmware_type").find('option').remove();
                    $("#firmware_flavour").find('option').remove();
                    $("#firmware_reboot").prop('checked', firmwareconfig['reboot'] !== '');

                    $.each(firmwareoptions.mirrors, function(key, value) {
                        var selected = false;
                        if (key == firmwareconfig['mirror']) {
                            selected = true;
                            custom_selected = false;
                        }
                        $("#firmware_mirror").append($("<option/>")
                                .attr("value",key)
                                .text(value)
                                .prop('selected', selected)
                        );
                    });
                    if (firmwareoptions['mirrors_allow_custom']) {
                        $("#firmware_mirror :first-child").after($("<option/>")
                            .attr("value", firmwareconfig['mirror'])
                            .text("(custom)")
                            .data("custom", 1)
                            .prop('selected', custom_selected)
                        );
                    }

                    $("#firmware_subscription").val(firmwareconfig['subscription']);

                    $("#firmware_mirror").selectpicker('refresh');
                    $("#firmware_mirror").change();

                    custom_selected = true;
                    $.each(firmwareoptions.flavours, function(key, value) {
                        var selected = false;
                        if (key == firmwareconfig['flavour']) {
                            selected = true;
                            custom_selected = false;
                        }
                        $("#firmware_flavour").append($("<option/>")
                                .attr("value",key)
                                .text(value)
                                .prop('selected', selected)
                        );
                    });
                    if (firmwareoptions['flavours_allow_custom']) {
                        $("#firmware_flavour :first-child").after($("<option/>")
                            .attr("value", firmwareconfig['flavour'])
                            .text("(custom)")
                            .data("custom", 1)
                            .prop('selected', custom_selected)
                        );
                    }
                    $("#firmware_flavour").selectpicker('refresh');
                    $("#firmware_flavour").change();
                    if (firmwareconfig['flavour'] !== '' || firmwareconfig['reboot'] !== '') {
                        $("i.fa-toggle-off#show_advanced_firmware").click();
                    }

                    $.each(firmwareoptions.families, function(key, value) {
                        var selected = false;
                        if (key == firmwareconfig['type']) {
                            selected = true;
                        }
                        $("#firmware_type").append($("<option/>")
                                .attr("value",key)
                                .text(value)
                                .prop('selected', selected)
                        );
                    });
                    $("#firmware_type").selectpicker('refresh');
                    $("#firmware_type").change();
                });
            });
        }
        fillOptions();
        $("#reset_mirror").click(fillOptions);

        $("#firmware_mirror").change(function(){
            $("#firmware_mirror_value").val($(this).val());
            if ($(this).find(':selected').data("custom") == 1) {
                $("#firmware_mirror_custom").show();
            } else {
                $("#firmware_mirror_custom").hide();
            }
        });
        $("#firmware_flavour").change(function() {
            $("#firmware_flavour_value").val($(this).val());
            if ($(this).find(':selected').data("custom") == 1) {
                $("#firmware_flavour_custom").show();
            } else {
                $("#firmware_flavour_custom").hide();
            }
        });

        $("#change_mirror").click(function(){
            $("#settingstab_progress").addClass("fa fa-spinner fa-pulse");
            var confopt = {};
            confopt.mirror = $("#firmware_mirror_value").val();
            confopt.flavour = $("#firmware_flavour_value").val();
            confopt.type = $("#firmware_type").val();
            confopt.reboot = $("#firmware_reboot").is(":checked");
            confopt.subscription = $("#firmware_subscription").val();
            ajaxCall('/api/core/firmware/set', { 'firmware': confopt }, function (data, status) {
                $("#settingstab_progress").removeClass("fa fa-spinner fa-pulse");
                if (data['status'] == 'ok') {
                    packagesInfo(true);
                } else {
                    let validation_msgs = '<ul>';
                    for (i = 0; i < data['status_msg'].length; i++) {
                        validation_msgs += '<li>' + $("<textarea/>").html(data['status_msg'][i]).text() + '</li>';
                    }
                    validation_msgs += '</ul>';

                    stdDialogInform('{{ lang._('Firmware status') }}', htmlDecode(validation_msgs), "{{ lang._('Close') }}", undefined, 'danger');
                }
            });
        });

        $("#update_status_copy").click(function () {
            $("#update_status").select();
            document.execCommand("copy");
            document.getSelection().removeAllRanges();
            setTimeout(function () { $("#update_status").blur(); }, 100);
        });

        // update history on tab state and implement navigation
        if(window.location.hash != "") {
            $('a[href="' + window.location.hash + '"]').click()
        }
        $('.nav-tabs a').on('shown.bs.tab', function (e) {
            history.pushState(null, null, e.target.hash);
        });
        $(window).on('hashchange', function(e) {
            $('a[href="' + window.location.hash + '"]').click()
        });
    });
</script>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-12" id="content">
            <ul class="nav nav-tabs" data-tabs="tabs">
                <li id="statustab" class="active"><a data-toggle="tab" href="#status">{{ lang._('Status') }} <i id="statustab_progress"></i></a></li>
                <li id="settingstab"><a data-toggle="tab" href="#settings">{{ lang._('Settings') }} <i id="settingstab_progress"></i></a></li>
                <li id="changelogtab"><a data-toggle="tab" href="#changelog">{{ lang._('Changelog') }}</a></li>
                <li id="updatetab"><a data-toggle="tab" href="#updates">{{ lang._('Updates') }} <i id="updatetab_progress"></i></a></li>
                <li id="plugintab"><a data-toggle="tab" href="#plugins">{{ lang._('Plugins') }}</a></li>
                <li id="packagestab"><a data-toggle="tab" href="#packages">{{ lang._('Packages') }}</a></li>
            </ul>
            <div class="tab-content content-box">
                <div id="updates" class="tab-pane table-responsive">
                    <table class="table table-striped table-condensed" id="updatelist" style="display: none;">
                        <thead>
                            <tr>
                              <th style="width:20%">{{ lang._('Package name') }}</th>
                              <th style="width:20%">{{ lang._('Current version') }}</th>
                              <th style="width:20%">{{ lang._('New version') }}</th>
                              <th style="width:20%">{{ lang._('Required action') }}</th>
                              <th style="width:20%">{{ lang._('Repository') }}</th>
                            </tr>
                        </thead>
                        <tbody></tbody>
                        <tfoot>
                            <tr>
                                <td></td>
                                <td style="white-space:nowrap;vertical-align:middle;">
                                    <button class="btn btn-info" id="upgrade"><i class="fa fa-check"></i> {{ lang._('Update') }}</button>
                                    <button class='btn btn-warning' id="upgrade_maj"><i class="fa fa-check"></i> {{ lang._('Upgrade') }}</button>
                                    <button class="btn btn-default" id="upgrade_cancel"><i class="fa fa-times"></i> {{ lang._('Cancel') }}</button>
                                </td>
                                <td colspan="2" style="vertical-align:middle">
                                    <strong><div id="updatestatus"></div></strong>
                                </td>
                                <td></td>
                            </tr>
                        </tfoot>
                    </table>
                    <div id="update_status_container" class="table-responsive">
                       <textarea name="output" id="update_status" class="form-control" rows="20" wrap="hard" readonly="readonly" style="max-width:100%; font-family: monospace;"></textarea>
                      <table class="table table-striped table-condensed">
                        <tbody>
                          <tr>
                            <td>
                              {{ lang._('Output shown here for diagnostic purposes. There is no general need for manual system intervention.') }}
                              <a id="update_status_copy">{{ lang._('Click here to copy to clipboard.') }}</a>
                            </td>
                          </tr>
                        </tbody>
                      </table>
                    </div>
                </div>
                <div id="status" class="tab-pane active table-responsive">
                    <table class="table table-striped table-condensed">
                        <tbody>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Type') }}</td>
                                <td id="product_id"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Version') }}</td>
                                <td id="product_version"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Architecture') }}</td>
                                <td id="product_arch"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Commit') }}</td>
                                <td id="product_hash"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Mirror') }}</td>
                                <td id="product_mirror"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Repositories') }}</td>
                                <td id="product_repos"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Updated on') }}</td>
                                <td id="product_time"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;">{{ lang._('Checked on') }}</td>
                                <td id="product_time_check"></td>
                                <td></td>
                            </tr>
                            <tr style='display:none'>
                                <td style="width: 150px;">{{ lang._('Licensed until') }}</td>
                                <td id="product_license_valid_to"></td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;"></td>
                                <td style="min-width: 500px;">
                                    <button class="btn btn-primary" id="checkupdate"><i class="fa fa-refresh"></i> {{ lang._('Check for updates') }}</button>
                                    <div class="btn-group dropdown" id="audit_actions" style="display:none;">
                                        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                                            <i class="fa fa-lock"></i> {{ lang._('Run an audit') }} <i class="caret"></i>
                                        </button>
                                        <ul class="dropdown-menu" role="menu">
                                            <li><a id="audit_connection" href="#">{{ lang._('Connectivity') }}</a></li>
                                            <li><a id="audit_health" href="#">{{ lang._('Health') }}</a></li>
                                            <li><a id="audit_security" href="#">{{ lang._('Security') }}</a></li>
                                            <li><a id="audit_upgrade" href="#">{{ lang._('Upgrade') }}</a></li>
                                        </ul>
                                    </div>
                                    <div class="btn-group dropdown" id="plugin_actions" style="display:none;">
                                        <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                                            <i class="fa fa-exclamation-triangle"></i> {{ lang._('Resolve plugin conflicts') }} <i class="caret"></i>
                                        </button>
                                        <ul class="dropdown-menu" role="menu">
                                            <li><a id="plugin_see" href="#">{{ lang._('View and edit local conflicts') }}</a></li>
                                            <li><a id="plugin_get" href="#">{{ lang._('Run the automatic resolver') }}</a></li>
                                            <li><a id="plugin_set" href="#">{{ lang._('Reset all local conflicts') }}</a></li>
                                        </ul>
                                    </div>
                                </td>
                                <td></td>
                            </tr>
                            <tr class="dropdown_helper" style="padding-bottom: 120px;"></tr>
                        </tbody>
                    </table>
                </div>
                <div id="plugins" class="tab-pane table-responsive">
                    <table class="table table-striped table-condensed" id="pluginlist">
                        <thead>
                            <tr>
                                <th style="vertical-align:middle"><input type="text" class="input-sm" autocomplete="off" id="plugin_search" placeholder="{{ lang._('Name') }}"></th>
                                <th style="vertical-align:middle">{{ lang._('Version') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Size') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Tier') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Repository') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Comment') }}</th>
                                <th style="vertical-align:middle"></th>
                            </tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                </div>
                <div id="packages" class="tab-pane table-responsive">
                    <table class="table table-striped table-condensed" id="packageslist">
                        <thead>
                            <tr>
                                <th style="vertical-align:middle"><input type="text" class="input-sm" autocomplete="off" id="package_search" placeholder="{{ lang._('Name') }}"></th>
                                <th style="vertical-align:middle">{{ lang._('Version') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Size') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Repository') }}</th>
                                <th style="vertical-align:middle">{{ lang._('License') }}</th>
                                <th style="vertical-align:middle">{{ lang._('Comment') }}</th>
                                <th style="vertical-align:middle"></th>
                            </tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                </div>
                <div id="changelog" class="tab-pane table-responsive">
                    <table class="table table-striped table-condensed" id="changeloglist">
                        <thead></thead>
                        <tbody></tbody>
                    </table>
                </div>
                <div id="settings" class="tab-pane table-responsive">
                    <table class="table table-striped table-condensed">
                        <tbody>
                            <tr>
                                <td style="text-align:left"><i class="fa fa-toggle-off text-danger" id="show_advanced_firmware"></i></a> <small>{{ lang._('advanced mode') }}</small></td>
                                <td colspan="2" style="text-align:right">
                                    <small>{{ lang._('full help') }}</small> <a href="#"><i class="fa fa-toggle-off text-danger" id="show_all_help_firmware"></i></a>
                                </td>
                            </tr>
                            <tr>
                                <td style="width: 150px;"><a id="help_for_mirror" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> {{ lang._('Mirror') }}</td>
                                <td>
                                    <select class="selectpicker" id="firmware_mirror"  data-size="5" data-live-search="true">
                                    </select>
                                    <div style="display:none;" id="firmware_mirror_custom">
                                        <input type="text" id="firmware_mirror_value">
                                    </div>
                                    <div class="hidden" data-for="help_for_mirror">
                                        {{ lang._('Select an alternate firmware mirror.') }}
                                    </div>
                                </td>
                                <td></td>
                            </tr>
                            <tr data-advanced="true">
                                <td><a id="help_for_flavour" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> {{ lang._('Flavour') }}</td>
                                <td>
                                    <select class="selectpicker" id="firmware_flavour">
                                    </select>
                                    <div style="display:none;" id="firmware_flavour_custom">
                                        <input type="text" id="firmware_flavour_value">
                                    </div>
                                    <div class="hidden" data-for="help_for_flavour">
                                        {{ lang._('Select an alternate firmware flavour.') }}
                                    </div>
                                </td>
                                <td></td>
                            </tr>
                            <tr>
                                <td><a id="help_for_type" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> {{ lang._('Type') }}</td>
                                <td>
                                    <select class="selectpicker" id="firmware_type">
                                    </select>
                                    <div class="hidden" data-for="help_for_type">
                                        {{ lang._('Select the release type.') }}
                                    </div>
                                </td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;"><a id="help_for_subscription" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> {{ lang._('Subscription') }}</td>
                                <td>
                                    <input type="text" id="firmware_subscription">
                                    <div class="hidden" data-for="help_for_subscription">
                                        {{ lang._('Provide subscription key.') }}
                                    </div>
                                </td>
                                <td></td>
                            </tr>
                            <tr data-advanced="true">
                                <td style="width: 150px;"><i class="fa fa-info-circle text-muted"></i> {{ lang._('Reboot') }}</td>
                                <td>
                                    <input type="checkbox" id="firmware_reboot">
                                    {{ lang._('Always reboot after a successful update') }}
                                </td>
                                <td></td>
                            </tr>
                            <tr>
                                <td style="width: 150px;"><i class="fa fa-info-circle text-muted"></i> {{ lang._('Usage') }}</td>
                                <td>
                                    {{ lang._('In order to apply these settings a firmware update must be performed after save, which can include a reboot of the system.') }}
                                </td>
                                <td></td>
                            </tr>
                            <tr>
                                <td></td>
                                <td>
                                    <button class="btn btn-primary" id="change_mirror" type="button"><i class="fa fa-floppy-o"></i> {{ lang._('Save') }}</button>
                                    <button class="btn btn-default" id="reset_mirror" type="button"><i class="fa fa-times"></i> {{ lang._('Cancel') }}</button>
                                </td>
                                <td></td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            &nbsp;
        </div>
    </div>
</div>

Zerion Mini Shell 1.0