diff --git a/plist b/plist index 1f299b5cc..4fba63431 100644 --- a/plist +++ b/plist @@ -1930,6 +1930,7 @@ /usr/local/opnsense/www/js/widgets/BaseWidget.js /usr/local/opnsense/www/js/widgets/Cpu.js /usr/local/opnsense/www/js/widgets/Interfaces.js +/usr/local/opnsense/www/js/widgets/SystemInformation.js /usr/local/opnsense/www/themes/opnsense/LICENSE /usr/local/opnsense/www/themes/opnsense/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot /usr/local/opnsense/www/themes/opnsense/assets/fonts/SourceSansPro-Bold/SourceSansPro-Bold.otf diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php index 71d456bae..931e429af 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php @@ -46,6 +46,15 @@ class DashboardController extends ApiControllerBase ], 'interfaces' => [ 'title' => gettext('Interfaces'), + ], + 'systeminformation' => [ + 'title' => gettext('System Information'), + 'name' => gettext('Name'), + 'versions' => gettext('Versions'), + 'updates' => gettext('Updates'), + 'datetime' => gettext('Current date/time'), + 'uptime' => gettext('Uptime'), + 'config' => gettext('Last configuration change') ] ]; } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php index e1681b629..8c78c8da4 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php @@ -33,6 +33,7 @@ namespace OPNsense\Core\Api; use OPNsense\Base\ApiControllerBase; use OPNsense\Core\ACL; use OPNsense\Core\Backend; +use OPNsense\Core\Config; /** * Class SystemController @@ -40,6 +41,24 @@ use OPNsense\Core\Backend; */ class SystemController extends ApiControllerBase { + private function formatUptime($uptime) + { + $days = floor($uptime / (3600 * 24)); + $hours = floor(($uptime % (3600 * 24)) / 3600); + $minutes = floor(($uptime % 3600) / 60); + $seconds = $uptime % 60; + + if ($days > 0) { + $plural = $days > 1 ? gettext("days") : gettext("day"); + return sprintf( + "%d %s, %02d:%02d:%02d", + $days, $plural, $hours, $minutes, $seconds + ); + } else { + return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds); + } + } + public function haltAction() { if ($this->request->isPost()) { @@ -185,4 +204,56 @@ class SystemController extends ApiControllerBase return ["status" => "failed"]; } + + public function systemInformationAction() + { + $config = Config::getInstance()->object(); + $backend = new Backend(); + + $product = json_decode($backend->configdRun('firmware product'), true); + $current = explode('_', $product['product_version'])[0]; + /* information from changelog, more accurate for production release */ + $from_changelog = strpos($product['product_id'], '-devel') === false && + !empty($product['product_latest']) && + $product['product_latest'] != $current; + + /* update status from last check, also includes major releases */ + $from_check = !empty($product['product_check']['upgrade_sets']) || + !empty($product['product_check']['downgrade_packages']) || + !empty($product['product_check']['new_packages']) || + !empty($product['product_check']['reinstall_packages']) || + !empty($product['product_check']['remove_packages']) || + !empty($product['product_check']['upgrade_packages']); + + $response = [ + 'name' => $config->system->hostname . '.' . $config->system->domain, + 'versions' => [ + sprintf('%s %s-%s', $product['product_name'], $product['product_version'], $product['product_arch']), + php_uname('s') . ' ' . php_uname('r'), + trim($backend->configdRun('system openssl version')) + ], + 'updates' => ($from_changelog || $from_check) + ? gettext('Click to view pending updates.') + : gettext('Click to check for updates.'), + ]; + + return json_encode($response); + } + + public function systemTimeAction() + { + $config = Config::getInstance()->object(); + $boottime = json_decode((new Backend())->configdRun('system sysctl values kern.boottime'), true); + preg_match("/sec = (\d+)/", $boottime['kern.boottime'], $matches); + + $last_change = date("D M j G:i:s T Y", !empty($config->revision->time) ? intval($config->revision->time) : 0); + + $response = [ + 'uptime' => $this->formatUptime(time() - $matches[1]), + 'datetime' => date("D M j G:i:s T Y"), + 'config' => $last_change, + ]; + + return json_encode($response); + } } diff --git a/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt b/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt index 0ead897c7..b5f24f772 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt @@ -32,6 +32,8 @@ + + diff --git a/src/opnsense/service/conf/actions.d/actions_system.conf b/src/opnsense/service/conf/actions.d/actions_system.conf index 67197aa95..61b3c4fb0 100644 --- a/src/opnsense/service/conf/actions.d/actions_system.conf +++ b/src/opnsense/service/conf/actions.d/actions_system.conf @@ -114,3 +114,9 @@ command:/usr/local/opnsense/scripts/system/cpu.py parameters:--interval %s type:stream_output message:Stream CPU stats + +[openssl.version] +command:/usr/local/bin/openssl version | cut -f -2 -d ' ' +parameters: +type:script_output +message:Show OpenSSL version diff --git a/src/opnsense/www/js/widgets/BaseTableWidget.js b/src/opnsense/www/js/widgets/BaseTableWidget.js index fd4e85909..61b6a4cef 100644 --- a/src/opnsense/www/js/widgets/BaseTableWidget.js +++ b/src/opnsense/www/js/widgets/BaseTableWidget.js @@ -20,6 +20,7 @@ export default class BaseTableWidget extends BaseWidget { '.flextable-row': {'padding': '0.5em 0.5em'}, '.header .flex-row': {'border-bottom': ''}, '.flex-row': {'width': this._calculateColumnWidth.bind(this)}, + '.column .flex-row': {'width': '100%'}, '.column': {'width': ''}, '.flex-cell': {'width': ''}, } @@ -47,11 +48,26 @@ export default class BaseTableWidget extends BaseWidget { return ''; } + _constructTable() { + if (this.options === null) { + console.error('No table options set'); + return null; + } + + this.$flextable = $(`
`) + + if (this.options.headerPosition === 'top') { + this.$headerContainer = $(``); + this.$flextable.append(this.$headerContainer); + } + } + setTableOptions(options = {}) { /** * headerPosition: top, left or none. * top: headers are on top of the table. Data layout: [{header1: value1}, {header1: value2}, ...] - * left: headers are on the left of the table (key-value). Data layout: [{header1: value1, header2: value2}, ...] + * left: headers are on the left of the table (key-value). Data layout: [{header1: value1}, {header2: value2}, ...]. + * Supports nested columns (e.g. {header1: [value1, value2...]}) * none: no headers. Data layout: [[value1, value2], ...] */ this.options = { @@ -60,15 +76,12 @@ export default class BaseTableWidget extends BaseWidget { } } - updateTable(data = [], clear = true) { + updateTable(data = []) { let $table = $(`#${this.flextableId}`); - if (clear) { - $table.children('.flextable-row').remove(); - } + $table.children('.flextable-row').remove(); for (const row of data) { - this.data.push(row); let rowType = Array.isArray(row) && row !== null ? 'flat' : 'nested'; if (rowType === 'flat' && this.options.headerPosition !== 'none') { console.error('Flat data is not supported with headers'); @@ -80,16 +93,17 @@ export default class BaseTableWidget extends BaseWidget { return null; } - if (rowType === 'flat') { - let $row = $(``) - for (const item of row) { - $row.append($(` -