Antonio SJ Musumeci 1 day ago
parent
commit
b8440a3492
  1. 259
      index.html
  2. 85
      src/mergerfs_webui.cpp

259
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;
}
</style>
</head>
<body>
@ -1077,6 +1145,9 @@
<button class="tab-button active" role="tab" aria-selected="true" aria-controls="branches-panel" id="branches-tab" tabindex="0">
Branches
</button>
<button class="tab-button" role="tab" aria-selected="false" aria-controls="branch-details-panel" id="branch-details-tab" tabindex="-1">
Branch Details
</button>
<button class="tab-button" role="tab" aria-selected="false" aria-controls="policies-panel" id="policies-tab" tabindex="-1">
Policies
</button>
@ -1128,6 +1199,46 @@
</div>
</div>
<!-- Branch Details Tab -->
<div class="tab-content" id="branch-details-panel" role="tabpanel" aria-labelledby="branch-details-tab">
<div class="controls-section">
<div class="controls-row">
<div class="form-group">
<label for="mount-select-branch-details">Mount Point</label>
<select id="mount-select-branch-details">
<option value="">Loading...</option>
</select>
</div>
<div class="action-buttons">
<button class="button secondary" id="refresh-branch-details-btn">
<span>🔄</span> Refresh
</button>
</div>
</div>
</div>
<div class="table-container">
<table id="branch-details-table" class="branch-details-table">
<thead>
<tr>
<th>Path</th>
<th>Mode</th>
<th>Total Space</th>
<th>Used Space</th>
<th>Available Space</th>
<th>Usage %</th>
<th>Min Free Space</th>
</tr>
</thead>
<tbody id="branch-details-tbody">
<tr>
<td colspan="7" class="empty-state">Loading branch details...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Policies Tab -->
<div class="tab-content" id="policies-panel" role="tabpanel" aria-labelledby="policies-tab">
<div class="controls-section">
@ -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 = '<tr><td colspan="7" class="empty-state">No branches configured</td></tr>';
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 = `<tr><td colspan="7" class="empty-state">${error.message || 'Failed to load branch details'}</td></tr>`;
}
}
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'));

85
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<std::string> 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,

Loading…
Cancel
Save