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 @@ + @@ -1128,6 +1199,46 @@ + +
+
+
+
+ + +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + +
PathModeTotal SpaceUsed SpaceAvailable SpaceUsage %Min Free Space
Loading branch details...
+
+
+
@@ -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,