diff --git a/index.html b/index.html
index 4a89b5c3..dacee14f 100644
--- a/index.html
+++ b/index.html
@@ -1054,6 +1054,74 @@
transition-duration: 0.01ms !important;
}
}
+
+ /* Branch Details Table Styles */
+ .branch-details-table {
+ width: 100%;
+ }
+
+ .branch-details-table th,
+ .branch-details-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ .usage-bar-container {
+ width: 100%;
+ max-width: 150px;
+ height: 8px;
+ background-color: var(--bg-input);
+ border-radius: 4px;
+ overflow: hidden;
+ position: relative;
+ }
+
+ .usage-bar {
+ height: 100%;
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ }
+
+ .usage-bar.low {
+ background-color: var(--success-color);
+ }
+
+ .usage-bar.medium {
+ background-color: var(--warning-color);
+ }
+
+ .usage-bar.high {
+ background-color: var(--danger-color);
+ }
+
+ .usage-text {
+ font-size: 12px;
+ font-weight: 500;
+ min-width: 50px;
+ text-align: right;
+ }
+
+ .readonly-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ background-color: var(--warning-color);
+ color: white;
+ font-size: 11px;
+ font-weight: 500;
+ border-radius: 3px;
+ margin-left: 8px;
+ }
+
+ .branch-error {
+ color: var(--danger-color);
+ font-size: 11px;
+ }
+
+ .space-value {
+ font-family: monospace;
+ font-size: 13px;
+ }
@@ -1077,6 +1145,9 @@
@@ -1372,6 +1483,11 @@
async getAllMounts() {
const response = await this.request('/mounts');
return await response.json();
+ },
+
+ async getBranchesInfo(mount) {
+ const response = await this.request(`/branches-info?mount=${encodeURIComponent(mount)}`);
+ return await response.json();
}
};
@@ -1713,7 +1829,7 @@
}
function populateMountSelects() {
- const selects = ['mount-select-branches', 'mount-select-policies', 'mount-select-config', 'mount-select-commands'];
+ const selects = ['mount-select-branches', 'mount-select-branch-details', 'mount-select-policies', 'mount-select-config', 'mount-select-commands'];
selects.forEach(selectId => {
const select = document.getElementById(selectId);
@@ -1755,21 +1871,24 @@
try {
const activeTab = document.querySelector('.tab-button[aria-selected="true"]').getAttribute('aria-controls');
- switch(activeTab) {
-
- case 'branches-panel':
- await loadBranches(mount);
- break;
- case 'policies-panel':
- await loadPolicies(mount);
- break;
- case 'config-panel':
- await loadConfig(mount);
- break;
- case 'commands-panel':
- // Commands don't need data loading
- break;
- }
+ switch(activeTab) {
+
+ case 'branches-panel':
+ await loadBranches(mount);
+ break;
+ case 'branch-details-panel':
+ await loadBranchDetails(mount);
+ break;
+ case 'policies-panel':
+ await loadPolicies(mount);
+ break;
+ case 'config-panel':
+ await loadConfig(mount);
+ break;
+ case 'commands-panel':
+ // Commands don't need data loading
+ break;
+ }
} catch (error) {
console.error('Failed to load mount data:', error);
showToast('Failed to load configuration', 'error');
@@ -1827,6 +1946,101 @@
}
}
+ // Branch details management
+ async function loadBranchDetails(mount) {
+ const tbody = document.getElementById('branch-details-tbody');
+ UI.showLoading(tbody);
+
+ try {
+ const branches = await API.getBranchesInfo(mount);
+ tbody.innerHTML = '';
+
+ if (!branches || branches.length === 0) {
+ tbody.innerHTML = '
| No branches configured |
';
+ return;
+ }
+
+ branches.forEach(branch => {
+ const row = document.createElement('tr');
+
+ const totalSpace = branch.total_space || 0;
+ const usedSpace = branch.used_space || 0;
+ const availableSpace = branch.available_space || 0;
+ const usagePercent = totalSpace > 0 ? (usedSpace / totalSpace) * 100 : 0;
+
+ const usageClass = usagePercent < 70 ? 'low' : (usagePercent < 90 ? 'medium' : 'high');
+
+ const pathCell = document.createElement('td');
+ const pathSpan = document.createElement('span');
+ pathSpan.className = 'space-value';
+ pathSpan.textContent = Utils.escapeHtml(branch.path);
+ pathCell.appendChild(pathSpan);
+ if (branch.readonly) {
+ const badge = document.createElement('span');
+ badge.className = 'readonly-badge';
+ badge.textContent = 'RO';
+ pathCell.appendChild(badge);
+ }
+ if (branch.error) {
+ const errorDiv = document.createElement('div');
+ errorDiv.className = 'branch-error';
+ errorDiv.textContent = Utils.escapeHtml(branch.error);
+ pathCell.appendChild(errorDiv);
+ }
+
+ const modeCell = document.createElement('td');
+ modeCell.textContent = Utils.escapeHtml(branch.mode || 'RW');
+
+ const totalCell = document.createElement('td');
+ totalCell.className = 'space-value';
+ totalCell.textContent = Utils.formatBytes(totalSpace);
+
+ const usedCell = document.createElement('td');
+ usedCell.className = 'space-value';
+ usedCell.textContent = Utils.formatBytes(usedSpace);
+
+ const availableCell = document.createElement('td');
+ availableCell.className = 'space-value';
+ availableCell.textContent = Utils.formatBytes(availableSpace);
+
+ const usageCell = document.createElement('td');
+ usageCell.style.textAlign = 'center';
+
+ const usageBarContainer = document.createElement('div');
+ usageBarContainer.className = 'usage-bar-container';
+
+ const usageBar = document.createElement('div');
+ usageBar.className = `usage-bar ${usageClass}`;
+ usageBar.style.width = `${usagePercent}%`;
+
+ usageBarContainer.appendChild(usageBar);
+ usageCell.appendChild(usageBarContainer);
+
+ const usageText = document.createElement('div');
+ usageText.className = 'usage-text';
+ usageText.textContent = `${usagePercent.toFixed(1)}%`;
+ usageCell.appendChild(usageText);
+
+ const minfreespaceCell = document.createElement('td');
+ minfreespaceCell.className = 'space-value';
+ minfreespaceCell.textContent = Utils.escapeHtml(branch.minfreespace || '-');
+
+ row.appendChild(pathCell);
+ row.appendChild(modeCell);
+ row.appendChild(totalCell);
+ row.appendChild(usedCell);
+ row.appendChild(availableCell);
+ row.appendChild(usageCell);
+ row.appendChild(minfreespaceCell);
+
+ tbody.appendChild(row);
+ });
+ } catch (error) {
+ console.error('Failed to load branch details:', error);
+ tbody.innerHTML = `| ${error.message || 'Failed to load branch details'} |
`;
+ }
+ }
+
async function saveBranches() {
if (!AppState.currentMount) {
showToast('No mount selected', 'error');
@@ -2397,12 +2611,13 @@
// Set up event listeners
document.getElementById('add-branch-btn').addEventListener('click', addBranchEntry);
- document.getElementById('save-branches-btn').addEventListener('click', saveBranches);
- document.getElementById('reset-branches-btn').addEventListener('click', () => loadBranches(AppState.currentMount));
- document.getElementById('reset-policies-btn').addEventListener('click', resetPolicies);
- document.getElementById('export-config-btn').addEventListener('click', exportConfig);
- document.getElementById('import-config-btn').addEventListener('click', importConfig);
- document.getElementById('refresh-config-btn').addEventListener('click', () => loadConfig(AppState.currentMount));
+ document.getElementById('save-branches-btn').addEventListener('click', saveBranches);
+ document.getElementById('reset-branches-btn').addEventListener('click', () => loadBranches(AppState.currentMount));
+ document.getElementById('refresh-branch-details-btn').addEventListener('click', () => loadBranchDetails(AppState.currentMount));
+ document.getElementById('reset-policies-btn').addEventListener('click', resetPolicies);
+ document.getElementById('export-config-btn').addEventListener('click', exportConfig);
+ document.getElementById('import-config-btn').addEventListener('click', importConfig);
+ document.getElementById('refresh-config-btn').addEventListener('click', () => loadConfig(AppState.currentMount));
// Command buttons
document.getElementById('cmd-gc').addEventListener('click', () => executeCommand('gc'));
diff --git a/src/mergerfs_webui.cpp b/src/mergerfs_webui.cpp
index 1e03051c..e036c450 100644
--- a/src/mergerfs_webui.cpp
+++ b/src/mergerfs_webui.cpp
@@ -1,9 +1,11 @@
#include "mergerfs_webui.hpp"
#include "fs_exists.hpp"
+#include "fs_info.hpp"
#include "fs_mounts.hpp"
#include "mergerfs_api.hpp"
#include "str.hpp"
+#include "strvec.hpp"
#include "CLI11.hpp"
#include "fmt/core.h"
@@ -540,6 +542,88 @@ _post_auth_verify(const httplib::Request &req_,
}
}
+static
+void
+_get_branches_info(const httplib::Request &req_,
+ httplib::Response &res_)
+{
+ if(not req_.has_param("mount"))
+ {
+ res_.status = 400;
+ res_.set_content("{\"error\":\"mount param not set\"}",
+ "application/json");
+ return;
+ }
+
+ fs::path mount;
+ std::string branches_str;
+ std::vector branches;
+
+ mount = req_.get_param_value("mount");
+
+ int rv = mergerfs::api::get_kv(mount,"branches",&branches_str);
+ if(rv < 0)
+ {
+ res_.status = 404;
+ res_.set_content("{\"error\":\"failed to get branches\"}",
+ "application/json");
+ return;
+ }
+
+ str::split(branches_str,':',&branches);
+
+ json json_array = json::array();
+
+ for(const auto &branch_full : branches)
+ {
+ fs::info_t info;
+ std::string path;
+ std::string mode;
+ std::string minfreespace;
+
+ str::splitkv(branch_full,'=',&path,&mode);
+
+ if(!mode.empty())
+ {
+ std::string::size_type pos = mode.find(',');
+ if(pos != std::string::npos)
+ {
+ minfreespace = mode.substr(pos + 1);
+ mode = mode.substr(0,pos);
+ }
+ }
+
+ json branch_obj;
+ branch_obj["path"] = path;
+ branch_obj["mode"] = mode;
+ branch_obj["minfreespace"] = minfreespace;
+
+ rv = fs::info(path,&info);
+ if(rv == 0)
+ {
+ uint64_t total = info.spaceavail + info.spaceused;
+
+ branch_obj["total_space"] = total;
+ branch_obj["used_space"] = info.spaceused;
+ branch_obj["available_space"] = info.spaceavail;
+ branch_obj["readonly"] = info.readonly;
+ }
+ else
+ {
+ branch_obj["total_space"] = 0;
+ branch_obj["used_space"] = 0;
+ branch_obj["available_space"] = 0;
+ branch_obj["readonly"] = false;
+ branch_obj["error"] = "Failed to get disk info";
+ }
+
+ json_array.push_back(branch_obj);
+ }
+
+ res_.set_content(json_array.dump(),
+ "application/json");
+}
+
int
mergerfs::webui::main(const int argc_,
char **argv_)
@@ -593,6 +677,7 @@ mergerfs::webui::main(const int argc_,
http_server.Get("/kvs",::_get_kvs);
http_server.Get("/kvs/:key",::_get_kvs_key);
http_server.Post("/kvs/:key",::_post_kvs_key);
+ http_server.Get("/branches-info",::_get_branches_info);
fmt::print("host:port = http://{}:{}\n",
host,