-
-
used of
-
-
( ${i18n('storage_puter_used')})
+
`;
},
init: ($el_window) => {
- const sanitize_id = id => (''+id).replace(/[^A-Za-z0-9-]/g, '');
- $.ajax({
- url: window.api_origin + "/drivers/usage",
- type: 'GET',
- async: true,
- contentType: "application/json",
- headers: {
- "Authorization": "Bearer " + window.auth_token
- },
- statusCode: {
- 401: function () {
- window.logout();
- },
- },
- success: function (res) {
- let h = ''; // Initialize HTML string for driver usage bars
+ puter.auth.getMonthlyUsage().then(res => {
+ let monthlyAllowance = res.allowanceInfo?.monthUsageAllowance;
+ let remaining = res.allowanceInfo?.remaining;
+ let totalUsage = monthlyAllowance - remaining;
+ let totalUsagePercentage = (totalUsage / monthlyAllowance * 100).toFixed(0);
- // Usages provided by arbitrary services
- res.usages.forEach(entry => {
- if ( ! entry.usage_percentage ) {
- entry.usage_percentage = (entry.used / entry.available * 100).toFixed(0);
- }
-
- // Skip the 'ai-chat (complete)' entry since we've made it infinite for now
- if(entry.name === 'ai-chat (complete)')
- return;
-
- if(entry.name.startsWith('es:subdomain'))
- return;
-
- if(entry.name.startsWith('es:app'))
- return;
-
- let name = entry.name;
-
- if(name === 'convert-api (convert)')
- name = `File Conversions`;
-
- h += `
-
-
${html_encode(name)}:
-
${i18n('used_of', {
- ...entry,
- used: window.format_credits(entry.used),
- available: window.format_credits(entry.available),
- })}
-
-
${Number(entry.usage_percentage)}%
-
-
- `;
- });
-
- const divContent = $el_window.find('.settings-content[data-settings="usage"]');
-
- // Append driver usage bars to the container
- divContent.append(`
${h}
`);
-
- const update_usage = event => {
- if ( ! event.usage_percentage ) {
- event.usage_percentage = (event.used / event.available * 100).toFixed(0);
- }
- const el_divContent = divContent[0];
- el_divContent
- .querySelector(`[data-id=${sanitize_id(event.id)}] .usage-progbar`)
- .style.width = '' + Number(event.usage_percentage) + '%';
- el_divContent
- .querySelector(`[data-id=${sanitize_id(event.id)}] .usage-progbar span`)
- .innerText = '' + Number(event.usage_percentage) + '%';
- const used_of_str = i18n('used_of', {
- ...event,
- used: window.format_credits(event.used),
- available: window.format_credits(event.available),
- });
- el_divContent
- .querySelector(`[data-id=${sanitize_id(event.id)}] > span`)
- .innerText = used_of_str;
- };
-
-
- const interval = setInterval(async () => {
- const resp = await fetch(`${window.api_origin}/drivers/usage`, {
- headers: {
- "Authorization": "Bearer " + window.auth_token
- },
- })
- const usages = (await resp.json()).usages;
- for ( const usage of usages ) {
- if ( ! usage.id ) continue;
- update_usage(usage);
- }
- }, 2000);
-
- divContent.on('remove', () => {
- socket.off('usage.update', update_usage);
- clearInterval(interval);
- });
- socket.on('usage.update', update_usage);
- }
+ $('#total-usage').html(window.number_format(totalUsage / 100_000_000, { decimals: 2, prefix: '$' }));
+ $('#total-capacity').html(window.number_format(monthlyAllowance / 100_000_000, { decimals: 2, prefix: '$' }));
+ $('.usage-progbar-percent').html(totalUsagePercentage + '%');
+ $('.usage-progbar').css('width', totalUsagePercentage + '%');
+
});
- // df
- $.ajax({
- url: window.api_origin + "/df",
- type: 'GET',
- async: true,
- contentType: "application/json",
- headers: {
- "Authorization": "Bearer " + window.auth_token
- },
- statusCode: {
- 401: function () {
- window.logout();
- },
- },
- success: function (res) {
- let usage_percentage = (res.used / res.capacity * 100).toFixed(0);
- usage_percentage = usage_percentage > 100 ? 100 : usage_percentage;
+ puter.fs.space().then(res => {
+ let usage_percentage = (res.used / res.capacity * 100).toFixed(0);
+ usage_percentage = usage_percentage > 100 ? 100 : usage_percentage;
- let general_used = res.used;
+ let general_used = res.used;
- let host_usage_percentage = 0;
- if ( res.host_used ) {
- $('#storage-puter-used').html(window.byte_format(res.used));
- $('#storage-puter-used-w').show();
+ let host_usage_percentage = 0;
+ if ( res.host_used ) {
+ $('#storage-puter-used').html(window.byte_format(res.used));
+ $('#storage-puter-used-w').show();
- general_used = res.host_used;
- host_usage_percentage = ((res.host_used - res.used) / res.capacity * 100).toFixed(0);
- }
+ general_used = res.host_used;
+ host_usage_percentage = ((res.host_used - res.used) / res.capacity * 100).toFixed(0);
+ }
- $('#storage-used').html(window.byte_format(general_used));
- $('#storage-capacity').html(window.byte_format(res.capacity));
- $('#storage-used-percent').html(
- usage_percentage + '%' +
- (host_usage_percentage > 0
- ? ' / ' + host_usage_percentage + '%' : '')
- );
- $('#storage-bar').css('width', usage_percentage + '%');
- $('#storage-bar-host').css('width', host_usage_percentage + '%');
- if (usage_percentage >= 100) {
- $('#storage-bar').css({
- 'border-top-right-radius': '3px',
- 'border-bottom-right-radius': '3px',
- });
- }
+ $('#storage-used').html(window.byte_format(general_used));
+ $('#storage-capacity').html(window.byte_format(res.capacity));
+ $('#storage-used-percent').html(
+ usage_percentage + '%' +
+ (host_usage_percentage > 0
+ ? ' / ' + host_usage_percentage + '%' : '')
+ );
+ $('#storage-bar').css('width', usage_percentage + '%');
+ $('#storage-bar-host').css('width', host_usage_percentage + '%');
+ if (usage_percentage >= 100) {
+ $('#storage-bar').css({
+ 'border-top-right-radius': '3px',
+ 'border-bottom-right-radius': '3px',
+ });
}
});
},
};
+
+$(document).on('click', '.driver-usage-details', function() {
+ $('.driver-usage-details-content').toggleClass('active');
+ $('.driver-usage-details').toggleClass('active');
+
+ // change the text of the driver-usage-details-text depending on the class
+ if($('.driver-usage-details').hasClass('active')){
+ $('.driver-usage-details-text').text('Hide usage details');
+ }else{
+ $('.driver-usage-details-text').text('View usage details');
+ }
+
+ puter.auth.getMonthlyUsage().then(res => {
+ let h = '
';
+
+ h += `
+
+ | Resource |
+ Units |
+ Cost |
+
+ `;
+
+ h += ``;
+ for(let key in res.usage){
+ // value must be object
+ if(typeof res.usage[key] !== 'object')
+ continue;
+
+ h += `
+
+ | ${key} |
+ ${window.format_credits(res.usage[key].units)} |
+ ${window.number_format(res.usage[key].cost / 100_000_000, { decimals: 2, prefix: '$' })} |
+
`;
+ }
+ h += ``;
+ h += '
';
+
+ $('.driver-usage-details-content').html(h);
+ });
+});
\ No newline at end of file
diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css
index a09c6af5d..e6b55ff1a 100644
--- a/src/gui/src/css/style.css
+++ b/src/gui/src/css/style.css
@@ -4334,7 +4334,9 @@ fieldset[name=number-code] {
}
.settings-content.active {
- display: block;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
.settings-content .about-container {
@@ -4451,8 +4453,13 @@ fieldset[name=number-code] {
box-sizing: border-box;
color: #3c4963;
height: 85px;
+ display: flex;
+ flex-direction: column;
}
+.driver-usage-container .driver-usage{
+ flex-grow: 1;
+}
.credits {
padding: 0;
border: 1px solid #bfbfbf;
@@ -4486,12 +4493,18 @@ fieldset[name=number-code] {
margin-bottom: 10px;
}
+.driver-usage-header{
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 5px;
+}
+
#storage-bar-wrapper {
width: 100%;
height: 20px;
- border: 1px solid #8a9096;
+ border: 1px solid #dddddd;
border-radius: 3px;
- background-color: #fff;
+ background-color: #fbfbfb;
position: relative;
display: flex;
align-items: center;
@@ -4526,9 +4539,9 @@ fieldset[name=number-code] {
.usage-progbar-wrapper {
width: 100%;
height: 20px;
- border: 1px solid #8a9096;
+ border: 1px solid #dddddd;
border-radius: 3px;
- background-color: #fff;
+ background-color: #fbfbfb;
position: relative;
display: flex;
align-items: center;
@@ -4552,7 +4565,78 @@ fieldset[name=number-code] {
font-size: 13px;
line-height: 20px;
}
+.driver-usage-container{
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ margin-top: 20px;
+}
+.driver-usage-details-text{
+ cursor: pointer !important;
+}
+.driver-usage-details-content {
+ display: none;
+ margin-top: 10px;
+ border-radius: 4px;
+}
+.driver-usage-details-content.active {
+ display: block !important;
+ flex-grow: 1;
+ overflow-y: scroll;
+}
+
+.driver-usage-details{
+ display: inline-block;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+}
+
+.driver-usage-details .caret {
+ transition: transform 0.1s ease-in-out;
+ margin-right: 3px;
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ opacity: 0.7;
+}
+
+.driver-usage-details.active .caret {
+ transform: rotate(90deg);
+ margin-top: -2px;
+}
+
+.driver-usage-details-content-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.driver-usage-details-content-table thead {
+ background-color: #f0f0f0;
+}
+
+.driver-usage-details-content-table thead th {
+ padding: 5px;
+ border: 1px solid #e0e0e0;
+ text-align: left;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.driver-usage-details-content-table td {
+ padding: 5px;
+ border: 1px solid #e0e0e0;
+ max-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: 14px;
+}
+
+.driver-usage-details-content-table td:first-child {
+ width: 50%;
+}
.version {
font-size: 9px;
color: #343c4f;
diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js
index fb2b46edd..9b95ca859 100644
--- a/src/gui/src/helpers.js
+++ b/src/gui/src/helpers.js
@@ -3064,6 +3064,108 @@ window.format_credits = (num) => {
return window.format_with_units(num, { mulUnits })
};
+/**
+ * General-purpose number formatting function with support for decimal places,
+ * thousand separators, and various formatting options.
+ *
+ * @param {number} num - The number to format
+ * @param {Object} options - Formatting options
+ * @param {number} options.decimals - Number of decimal places (default: 0)
+ * @param {string} options.decimalSeparator - Decimal separator character (default: '.')
+ * @param {string} options.thousandSeparator - Thousand separator character (default: ',')
+ * @param {string} options.prefix - String to prepend (e.g., '$' for currency)
+ * @param {string} options.suffix - String to append (e.g., '%' for percentage)
+ * @param {boolean} options.stripInsignificantZeros - Remove trailing zeros after decimal (default: false)
+ * @param {string} options.negativeFormat - Format for negative numbers: 'sign' (default), 'parentheses', or 'accounting'
+ * @param {boolean} options.forceSign - Always show sign for positive numbers (default: false)
+ *
+ * @returns {string} Formatted number string
+ *
+ * @example
+ * number_format(1234.5) // "1,234"
+ * number_format(1234.5, { decimals: 2 }) // "1,234.50"
+ * number_format(1234.5678, { decimals: 2 }) // "1,234.57"
+ * number_format(1234567.89, { decimals: 2, prefix: '$' }) // "$1,234,567.89"
+ * number_format(0.5, { decimals: 1, suffix: '%' }) // "0.5%"
+ * number_format(-1234.5, { decimals: 2 }) // "-1,234.50"
+ * number_format(-1234.5, { decimals: 2, negativeFormat: 'parentheses' }) // "(1,234.50)"
+ * number_format(1234.5, { decimals: 2, thousandSeparator: ' ' }) // "1 234.50"
+ * number_format(1234.5, { decimals: 2, decimalSeparator: ',' }) // "1.234,50"
+ */
+window.number_format = (num, options = {}) => {
+ // Default options
+ const {
+ decimals = 0,
+ decimalSeparator = '.',
+ thousandSeparator = ',',
+ prefix = '',
+ suffix = '',
+ stripInsignificantZeros = false,
+ negativeFormat = 'sign', // 'sign', 'parentheses', 'accounting'
+ forceSign = false,
+ } = options;
+
+ // Handle non-numeric values
+ if (num === null || num === undefined || isNaN(num)) {
+ return prefix + '0' + suffix;
+ }
+
+ // Handle infinity
+ if (!isFinite(num)) {
+ return num > 0 ? prefix + '∞' + suffix : prefix + '-∞' + suffix;
+ }
+
+ const isNegative = num < 0;
+ const absNum = Math.abs(num);
+
+ // Round to specified decimal places
+ const multiplier = Math.pow(10, decimals);
+ const rounded = Math.round(absNum * multiplier) / multiplier;
+
+ // Split into integer and decimal parts
+ let [intPart, decPart] = rounded.toFixed(decimals).split('.');
+
+ // Add thousand separators to integer part
+ if (thousandSeparator) {
+ intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);
+ }
+
+ // Build the number string
+ let numStr = intPart;
+ if (decimals > 0) {
+ // Handle stripInsignificantZeros
+ if (stripInsignificantZeros && decPart) {
+ decPart = decPart.replace(/0+$/, '');
+ }
+ if (decPart && decPart.length > 0) {
+ numStr += decimalSeparator + decPart;
+ } else if (!stripInsignificantZeros) {
+ numStr += decimalSeparator + decPart;
+ }
+ }
+
+ // Handle negative formatting
+ let sign = '';
+ let wrapper = { start: '', end: '' };
+
+ if (isNegative) {
+ if (negativeFormat === 'parentheses') {
+ wrapper = { start: '(', end: ')' };
+ } else if (negativeFormat === 'accounting') {
+ // Accounting format: negative in parentheses with red color context
+ wrapper = { start: '(', end: ')' };
+ } else {
+ // Default: sign format
+ sign = '-';
+ }
+ } else if (forceSign && num > 0) {
+ sign = '+';
+ }
+
+ // Assemble final string
+ return wrapper.start + sign + prefix + numStr + suffix + wrapper.end;
+};
+
/**
* This function will call the provided action function in a try...catch
* and handle the 'item_with_same_name_exists' error by re-calling the