From bbe6f9dc2702da7436d77f08021d411c7eab25a5 Mon Sep 17 00:00:00 2001 From: Lui Duarte <94099281+llpingll@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:29:02 +0000 Subject: [PATCH] Feat: Add system info (Client + Server metrics) (#2311) * Add ststem info to user options extensions - Add UIWindowSystemInfo, add ui sections for client and server, add basic getClientinfo function * Fix typo * Replace accidentally deleted es.js file * Refactor client information to be consistant with project standard * Complete Client information in ststem information window * Remove console logs * Add basic api functionality for getting server system information * Structure return data from system server information endpoint | Add copyright to UIWindowSystemInfo * Add function to format server system api data | Add loading element to server container while waiting for data | Complete System Information * fix: disallow non admin for backend + move to extensions --------- Co-authored-by: Daniel Salazar --- extensions/serverInfo/config.json | 5 + extensions/serverInfo/index.ts | 63 ++++ extensions/serverInfo/package.json | 11 + extensions/serverInfo/tsconfig.json | 28 ++ extensions/serverInfo/types.ts | 1 + src/gui/src/UI/UIWindowSystemInfo.js | 295 ++++++++++++++++++ src/gui/src/css/style.css | 85 ++++- .../extensions/modify-user-options-menu.js | 19 ++ src/gui/src/i18n/translations/en.js | 14 + src/gui/src/icons/system-info-browser.svg | 17 + src/gui/src/icons/system-info-color.svg | 10 + src/gui/src/icons/system-info-cpu.svg | 24 ++ src/gui/src/icons/system-info-os.svg | 6 + src/gui/src/icons/system-info-pixel.svg | 2 + src/gui/src/icons/system-info-ram.svg | 92 ++++++ src/gui/src/icons/system-info-screen.svg | 4 + src/gui/src/icons/system-info-storage.svg | 2 + src/gui/src/icons/system-info-time.svg | 6 + 18 files changed, 683 insertions(+), 1 deletion(-) create mode 100644 extensions/serverInfo/config.json create mode 100644 extensions/serverInfo/index.ts create mode 100644 extensions/serverInfo/package.json create mode 100644 extensions/serverInfo/tsconfig.json create mode 100644 extensions/serverInfo/types.ts create mode 100644 src/gui/src/UI/UIWindowSystemInfo.js create mode 100644 src/gui/src/icons/system-info-browser.svg create mode 100644 src/gui/src/icons/system-info-color.svg create mode 100644 src/gui/src/icons/system-info-cpu.svg create mode 100644 src/gui/src/icons/system-info-os.svg create mode 100644 src/gui/src/icons/system-info-pixel.svg create mode 100644 src/gui/src/icons/system-info-ram.svg create mode 100644 src/gui/src/icons/system-info-screen.svg create mode 100644 src/gui/src/icons/system-info-storage.svg create mode 100644 src/gui/src/icons/system-info-time.svg diff --git a/extensions/serverInfo/config.json b/extensions/serverInfo/config.json new file mode 100644 index 000000000..77cf15d65 --- /dev/null +++ b/extensions/serverInfo/config.json @@ -0,0 +1,5 @@ +{ + "allowedUsernames": [ + "puter" +] +} \ No newline at end of file diff --git a/extensions/serverInfo/index.ts b/extensions/serverInfo/index.ts new file mode 100644 index 000000000..ab7a017e2 --- /dev/null +++ b/extensions/serverInfo/index.ts @@ -0,0 +1,63 @@ +import fs from 'fs/promises'; +import os from 'os'; +const { Controller, Get, ExtensionController } = extension.import('extensionController'); + +@Controller('/serverInfo', [...config.allowedUsernames]) +class ServerInfoController extends ExtensionController { + @Get('', { subdomain: 'api' }) + async getServerInfo (req, res) { + const osData = { + platform: os.platform(), + type: os.type(), + release: os.release(), + pretty: `${os.type()} ${os.release()}`, + }; + + const cpus = os.cpus(); + const cpuData = { + model: cpus[0]?.model || 'Unknown', + cores: cpus.length, + }; + + const ramData = { + total: os.totalmem(), + free: os.freemem(), + totalGB: (os.totalmem() / 1073741824).toFixed(2), + freeGB: (os.freemem() / 1073741824).toFixed(2), + }; + + const uptimeSeconds = os.uptime(); + const uptimeData = { + seconds: uptimeSeconds, + days: Math.floor(uptimeSeconds / 86400), + hours: Math.floor((uptimeSeconds % 86400) / 3600), + minutes: Math.floor((uptimeSeconds % 3600) / 60), + pretty: `${Math.floor(uptimeSeconds / 86400)}d ${Math.floor((uptimeSeconds % 86400) / 3600)}h ${Math.floor((uptimeSeconds % 3600) / 60)}m`, + }; + + let diskData = { total: 'N/A', free: 'N/A', used: 'N/A' }; + try { + const stats = await fs.statfs('/'); + const totalGB = (stats.blocks * stats.bsize / 1073741824); + const freeGB = (stats.bfree * stats.bsize / 1073741824); + const usedGB = (totalGB - freeGB).toFixed(2); + diskData = { total: totalGB.toFixed(2), free: freeGB.toFixed(2), used: usedGB }; + } catch ( err ) { + console.error('Disk stats error:', err); + } + + const response = { + os: osData, + cpu: cpuData, + ram: ramData, + uptime: uptimeData, + disk: diskData, + loadavg: os.loadavg(), + hostname: os.hostname(), + }; + + res.json(response); + } +} + +(new ServerInfoController()).registerRoutes(); \ No newline at end of file diff --git a/extensions/serverInfo/package.json b/extensions/serverInfo/package.json new file mode 100644 index 000000000..56157804f --- /dev/null +++ b/extensions/serverInfo/package.json @@ -0,0 +1,11 @@ +{ + "name": "@heyputer/server-info-extension", + "main": "index.js", + "type": "module", + "scripts": { + "postinstall": "tsc --noCheck" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/extensions/serverInfo/tsconfig.json b/extensions/serverInfo/tsconfig.json new file mode 100644 index 000000000..3e9daf662 --- /dev/null +++ b/extensions/serverInfo/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true, + "noEmitOnError": true, + "noImplicitAny": false, + "allowJs": true, + "checkJs": false, + }, + "include": [ + "./**/*.ts", + "./**/*.d.ts" + ], + "exclude": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/test/**", + "**/tests/**", + "node_modules", + "dist", + "*.js" + ] +} diff --git a/extensions/serverInfo/types.ts b/extensions/serverInfo/types.ts new file mode 100644 index 000000000..69a5a7cfd --- /dev/null +++ b/extensions/serverInfo/types.ts @@ -0,0 +1 @@ +import '../api.js'; diff --git a/src/gui/src/UI/UIWindowSystemInfo.js b/src/gui/src/UI/UIWindowSystemInfo.js new file mode 100644 index 000000000..a91c784d0 --- /dev/null +++ b/src/gui/src/UI/UIWindowSystemInfo.js @@ -0,0 +1,295 @@ +/** + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import UIWindow from './UIWindow.js'; +import * as utils from '../../../puter-js/src/lib/utils.js'; + +const triggerRefreshBtnAnimation = ($btn) => { + const $icon = $btn.find('.update-usage-details-icon'); + + const icon = $icon[0]; + const clone = icon.cloneNode(true); + // Cloned node required to get animation to play on refresh + icon.parentNode.replaceChild(clone, icon); +}; + +// Leverage User-Agent Client Hints API to request user browser information +async function getClientInfo () { + let clientInfo = []; + + // Get browser & OS info + if ( navigator.userAgentData ) { + const uaData = await navigator.userAgentData.getHighEntropyValues([ + 'platform', 'platformVersion', 'model', 'fullVersionList', + ]); + + const browser = uaData.brands?.[0]?.brand || 'Unknown'; + const browserVersion = uaData.brands?.[0]?.version || 'Unknown'; + const os = uaData.platform || 'Unknown'; + const osVersion = uaData.platformVersion || 'Unknown'; + + clientInfo.push ({ + key: 'browser', + icon: 'system-info-browser.svg', + i18n_key: 'browser', + title: i18n('browser'), + value: `${browser} ${browserVersion}`, + }, + { + key: 'os', + icon: 'system-info-os.svg', + i18n_key: 'system-info-os', + title: i18n('os'), + value: `${os} ${osVersion}`, + }); + } else { + // Fallback for older browsers + const userAgent = navigator.userAgent; + let os = 'Unknown'; + if ( /Win/.test(userAgent) ) os = 'Windows'; + else if ( /Mac/.test(userAgent) ) os = 'macOS'; + else if ( /Linux/.test(userAgent) ) os = 'Linux'; + else if ( /Android/.test(userAgent) ) os = 'Android'; + else if ( /iPhone|iPad|iPod/.test(userAgent) ) os = 'iOS'; + + clientInfo.push({ + key: 'os', + icon: 'system-info-os.svg', + i18n_key: 'os', + title: i18n('os'), + value: os, + }); + } + + // Get hardware info + const cpuCores = navigator.hardwareConcurrency || 'Unknown'; + const ram = navigator.deviceMemory ? `${navigator.deviceMemory} GB (approx)` : 'Unknown'; + + clientInfo.push({ + key: 'cpu_cores', + icon: 'system-info-cpu.svg', + i18n_key: 'cpu_cores', + title: i18n('cpu_cores'), + value: `${cpuCores} cores`, + }, + { + key: 'ram', + icon: 'system-info-ram.svg', + i18n_key: 'ram', + title: i18n('ram'), + value: ram, + }); + + // Get screen info + const screenResolution = `${window.screen.width}x${window.screen.height}`; + const pixelRatio = window.devicePixelRatio; + const colorDepth = window.screen.colorDepth; + + clientInfo.push({ + key: 'screen_resolution', + icon: 'system-info-screen.svg', + i18n_key: 'screen_resolution', + title: i18n('screen_resolution'), + value: screenResolution, + }, + { + key: 'pixel_ratio', + icon: 'system-info-pixel.svg', + i18n_key: 'pixel_ratio', + title: i18n('pixel_ratio'), + value: `${pixelRatio}x`, + }, + { + key: 'color_depth', + icon: 'system-info-color.svg', + i18n_key: 'color_depth', + title: i18n('color_depth'), + value: `${colorDepth} bits`, + }); + + return clientInfo; +} + +async function getServerInfo (options = {}) { + const APIOrigin = window.puter?.APIOrigin; + const authToken = window.puter?.authToken; + return new Promise((resolve, reject) => { + const xhr = utils.initXhr('/serverInfo', APIOrigin, authToken, 'get'); + utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject); + xhr.send(); + }); +} + +async function getServerInfoFormatted () { + try { + const rawServerData = await getServerInfo(); + + // Map raw data to render-ready array + return [ + { + key: 'os', + icon: 'system-info-os.svg', + i18n_key: 'os', + title: i18n('os'), + value: rawServerData.os?.pretty || `${rawServerData.os?.type || 'Unknown'} ${rawServerData.os?.release || ''}`, + }, + { + key: 'cpu', + icon: 'system-info-cpu.svg', + i18n_key: 'cpu', + title: i18n('cpu'), + value: `${rawServerData.cpu?.model || 'Unknown'} (${rawServerData.cpu?.cores || 0} cores)`, + }, + { + key: 'ram', + icon: 'system-info-ram.svg', + i18n_key: 'ram', + title: i18n('ram'), + value: `${rawServerData.ram?.freeGB || 0} Free / ${rawServerData.ram?.totalGB || 0} GB`, + }, + { + key: 'disk_storage', + icon: 'system-info-storage.svg', + i18n_key: 'disk_storage', + title: i18n('disk_storage'), + value: `${rawServerData.disk?.used || 0} Used / ${rawServerData.disk?.total || 0} GB`, + }, + { + key: 'uptime', + icon: 'system-info-time.svg', + i18n_key: 'uptime', + title: i18n('uptime'), + value: rawServerData.uptime?.pretty || 'N/A', + }, + ]; + } catch ( err ) { + console.error('Failed to fetch server info:', err); + return []; + } +} + +function renderSystemInfo ( information ) { + let html = ''; + for ( const info of information ) { + html += `
+

${info.title}

+
+ ${info.i18n} image + ${info.value} +
+
`; + } + return html; +} + +async function UIWindowSystemInfo (options) { + return new Promise(async (resolve) => { + // Build client & Server containers & headers + const h = `
+
+

${i18n('client_information')} + +

+
+
+
+

${i18n('server_information')} + +

+
+
+
`; + + const el_window = await UIWindow({ + title: 'System Information', + app: 'System Information', + single_instance: true, + icon: null, + uid: null, + is_dir: false, + body_content: h, + has_head: true, + selectable_body: false, + allow_context_menu: false, + is_resizable: true, + is_droppable: false, + init_center: true, + allow_native_ctxmenu: true, + allow_user_select: true, + backdrop: false, + width: 560, + height: 540, + dominant: true, + show_in_taskbar: true, + draggable_body: false, + body_css: { + width: 'initial', + height: 'calc(100% - 30px)', + overflow: 'auto', + }, + ...options?.window_options ?? {}, + }); + + // Scope jQuery to this window + const $win = $(el_window); + + // Inject client info on launch + const clientInfo = await getClientInfo(); + const clientInfohtml = renderSystemInfo(clientInfo); + $win.find('.clientinfo-content').html(clientInfohtml); + // Inject server info on launch + $win.find('.serverinfo-content').html('

Loading server info...

'); + const serverInfo = await getServerInfoFormatted(); + const serverInfohtml = renderSystemInfo(serverInfo); + $win.find('.serverinfo-content').html(serverInfohtml); + + // Spin both reset buttons once on launch + const $icons = $win.find('.update-usage-details-icon'); + $icons.addClass('spin-once'); + + // Refresh button onclick event + $win.on('click', '.update-usage-details', async function () { + if ( $(this).hasClass('client-btn') ) { + triggerRefreshBtnAnimation($(this)); + const clientInfo = await getClientInfo(); + const clientInfohtml = renderSystemInfo(clientInfo); + $win.find('.clientinfo-content').html(clientInfohtml); + } else { + triggerRefreshBtnAnimation($(this)); + const serverInfo = await getServerInfoFormatted(); + const serverInfohtml = renderSystemInfo(serverInfo); + $win.find('.serverinfo-content').html(serverInfohtml); + } + }); + + resolve(el_window); + }); +} + +export default UIWindowSystemInfo; \ No newline at end of file diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css index b411bfa97..0c2758bb1 100644 --- a/src/gui/src/css/style.css +++ b/src/gui/src/css/style.css @@ -1200,7 +1200,7 @@ span.header-sort-icon img { .window-body { width: 100%; - height: calc(100% - 77px); + height: 100%; background-color: white; overflow: auto; } @@ -2952,6 +2952,89 @@ label { } +/***************************************************** + * System Information + *****************************************************/ + +.systeminfo-container { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; + background-color: #f9f9f9; +} + +.serverinfo-container, +.clientinfo-container { + padding: 20px; + background-color: #ffffff; + border: 1px solid #cccccc8f; + border-radius: 4px; +} + +.serverinfo-container h1, +.clientinfo-container h1 { + font-size: 24px; + margin-bottom: 20px; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 10px; + padding-left: 5px; + font-weight: 500; +} + +.update-usage-details-icon { + transform-origin: center; + transform-box: fill-box; +} + +/* For refresh button animation */ +.spin-once { animation: spin-once 1s linear; } + +@keyframes spin-once { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.clientinfo-content, +.serverinfo-content { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.systeminfo-item { + flex: 1 1 45%; /* Grow, shrink, min width 45% */ + min-width: 150px; /* Prevents items from getting too small */ + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 5px +} + +.systeminfo-value { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 10px; +} + +.systeminfo-title { + font-weight: 500; + font-size: 14px; + color: #3C4963; + margin: 0; +} + +.systeminfo-value { + color: #3C4963; + font-size: 13px; +} + +.systeminfo-icon { + width: 20px; + height: 20px; +} + /***************************************************** * Tooltip *****************************************************/ diff --git a/src/gui/src/extensions/modify-user-options-menu.js b/src/gui/src/extensions/modify-user-options-menu.js index 9fd4b82b3..882c03412 100644 --- a/src/gui/src/extensions/modify-user-options-menu.js +++ b/src/gui/src/extensions/modify-user-options-menu.js @@ -17,10 +17,29 @@ * along with this program. If not, see . */ +import UIWindowSystemInfo from '../UI/UIWindowSystemInfo.js'; + +console.debug('[puter] modify-user-options-menu loaded'); + $(window).on('ctxmenu-will-open', (event) => { if ( event.detail.options?.id === 'user-options-menu' ) { // Define array of new menu items const newMenuItems = [ + // System Information window + { + id: 'system_information', + html: 'System Information', + html_active: 'System Information', + action: async function () { + try { + console.debug('[puter] System Information click'); + await UIWindowSystemInfo(); + console.debug('[puter] System Information opened'); + } catch (e) { + console.error('[puter] System Information failed', e); + } + }, + }, // Separator '-', // 'Developer', opens developer site in new tab diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index f747014a2..77bda3853 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -36,6 +36,8 @@ const en = { auto_arrange: 'Auto Arrange', background: 'Background', browse: 'Browse', + browser: 'Browser', + browser_version: 'Browser Version', captcha_required: 'Please complete the CAPTCHA verification', cancel: 'Cancel', center: 'Center', @@ -47,6 +49,7 @@ const en = { change_password: 'Change Password', change_ui_colors: 'Change UI Colors', change_username: 'Change Username', + color_depth: 'Color Depth', clock_visibility: 'Clock Visibility', close: 'Close', close_all_windows: 'Close All Windows', @@ -83,6 +86,8 @@ const en = { copying: 'Copying', copying_file: 'Copying %%', cover: 'Cover', + cpu_cores: 'CPU Cores', + cpu: 'CPU', create_account: 'Create Account', create_free_account: 'Create Free Account', create_desktop_shortcut: 'Create Shortcut (Desktop)', @@ -92,6 +97,7 @@ const en = { credits: 'Credits', current_password: 'Current Password', cut: 'Cut', + client_information: 'Client Information', clock: 'Clock', clock_visible_hide: 'Hide - Always hidden', clock_visible_show: 'Show - Always visible', @@ -114,6 +120,7 @@ const en = { disable_2fa_confirm: 'Are you sure you want to disable 2FA?', disable_2fa_instructions: 'Enter your password to disable 2FA.', disassociate_dir: 'Disassociate Directory', + disk_storage: 'Disk Storage', documents: 'Documents', dont_allow: 'Don\'t Allow', download: 'Download', @@ -203,7 +210,9 @@ const en = { open_with: 'Open With', original_name: 'Original Name', original_path: 'Original Path', + os: 'Operating System', oss_code_and_content: 'Open Source Software and Content', + os_version: 'OS Version', password: 'Password', password_changed: 'Password changed.', password_recovery_rate_limit: "You've reached our rate-limit; please wait a few minutes. To prevent this in the future, avoid reloading the page too many times.", @@ -220,6 +229,7 @@ const en = { pick_name_for_worker: 'Pick a name for your worker:', picture: 'Picture', pictures: 'Pictures', + pixel_ratio: 'Pixel Ratio', plural_suffix: 's', powered_by_puter_js: 'Powered by {{link=docs}}Puter.js{{/link}}', preparing: 'Preparing...', @@ -239,6 +249,7 @@ const en = { publish_as_website: 'Publish as website', publish_as_serverless_worker: 'Publish as Worker', puter_description: 'Puter is a privacy-first personal cloud to keep all your files, apps, and games in one secure place, accessible from anywhere at any time.', + ram: 'RAM', reading: 'Reading %strong%', writing: 'Writing %strong%', recent: 'Recent', @@ -268,6 +279,7 @@ const en = { scan_qr_c2a: 'Scan the code below\nto log into this session from other devices', scan_qr_2fa: 'Scan the QR code with your authenticator app', scan_qr_generic: 'Scan this QR code using your phone or another device', + screen_resolution: 'Screen Resolution', search: 'Search', seconds: 'seconds', security: 'Security', @@ -277,6 +289,7 @@ const en = { sessions: 'Sessions', send: 'Send', send_password_recovery_email: 'Send Password Recovery Email', + server_information: 'Server Information', session_saved: 'Thank you for creating an account. This session has been saved.', settings: 'Settings', set_new_password: 'Set New Password', @@ -333,6 +346,7 @@ const en = { uploading: 'Uploading', uploading_file: 'Uploading %%', upload_here: 'Upload here', + uptime: 'Uptime', used_of: '{{used}} used of {{available}}', usage: 'Usage', username: 'Username', diff --git a/src/gui/src/icons/system-info-browser.svg b/src/gui/src/icons/system-info-browser.svg new file mode 100644 index 000000000..e222eeaeb --- /dev/null +++ b/src/gui/src/icons/system-info-browser.svg @@ -0,0 +1,17 @@ + + + + + browser + Created with Sketch Beta. + + + + + + + + + + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-color.svg b/src/gui/src/icons/system-info-color.svg new file mode 100644 index 000000000..35b50abbd --- /dev/null +++ b/src/gui/src/icons/system-info-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-cpu.svg b/src/gui/src/icons/system-info-cpu.svg new file mode 100644 index 000000000..148fd4dd8 --- /dev/null +++ b/src/gui/src/icons/system-info-cpu.svg @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-os.svg b/src/gui/src/icons/system-info-os.svg new file mode 100644 index 000000000..98377cf74 --- /dev/null +++ b/src/gui/src/icons/system-info-os.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-pixel.svg b/src/gui/src/icons/system-info-pixel.svg new file mode 100644 index 000000000..877d61044 --- /dev/null +++ b/src/gui/src/icons/system-info-pixel.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-ram.svg b/src/gui/src/icons/system-info-ram.svg new file mode 100644 index 000000000..de43340e4 --- /dev/null +++ b/src/gui/src/icons/system-info-ram.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-screen.svg b/src/gui/src/icons/system-info-screen.svg new file mode 100644 index 000000000..43d596128 --- /dev/null +++ b/src/gui/src/icons/system-info-screen.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-storage.svg b/src/gui/src/icons/system-info-storage.svg new file mode 100644 index 000000000..44dcfe673 --- /dev/null +++ b/src/gui/src/icons/system-info-storage.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/gui/src/icons/system-info-time.svg b/src/gui/src/icons/system-info-time.svg new file mode 100644 index 000000000..4b8d54aee --- /dev/null +++ b/src/gui/src/icons/system-info-time.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file