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.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 @@
+
+
+
\ 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