Skip to content

Commit 9b6ade5

Browse files
committed
Better repquota
1 parent 701dcb6 commit 9b6ade5

1 file changed

Lines changed: 83 additions & 135 deletions

File tree

public/admin/repquota.html

Lines changed: 83 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -2,108 +2,81 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<title>repquota -a Parser with Vue</title>
5+
<title>Storage Usage Analyzer - Cumulative</title>
66
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
77
<style>
8-
body {
9-
font-family: sans-serif;
10-
padding: 20px;
11-
}
12-
button {
13-
padding: 8px 16px;
14-
font-size: 16px;
15-
margin-bottom: 20px;
16-
}
17-
table {
18-
width: 100%;
19-
border-collapse: collapse;
20-
margin-top: 20px;
21-
}
22-
th, td {
23-
border: 1px solid #ccc;
24-
padding: 6px 10px;
25-
text-align: right;
26-
}
27-
th:first-child, td:first-child {
28-
text-align: left;
29-
}
30-
th {
31-
cursor: pointer;
32-
background-color: #f2f2f2;
33-
}
8+
body { font-family: 'Inter', system-ui, sans-serif; padding: 20px; background-color: #f4f4f9; color: #2d3748; }
9+
button { padding: 10px 18px; cursor: pointer; border-radius: 6px; border: 1px solid #cbd5e0; background: #fff; font-weight: 600; transition: 0.2s; }
10+
button:hover { background: #edf2f7; }
11+
12+
.summary-card { background: #fff; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); display: inline-block; border-left: 4px solid #4299e1; }
13+
14+
table { width: 100%; border-collapse: collapse; background: white; box-shadow: 0 4px 12px rgba(0,0,0,0.08); border-radius: 8px; table-layout: auto; }
15+
th, td { padding: 12px 15px; text-align: right; border-bottom: 1px solid #edf2f7; white-space: nowrap; }
16+
17+
/* Give the name column just enough space */
18+
th:first-child, td:first-child { text-align: left; width: 1%; }
19+
20+
/* Make the cumulative column wide to accommodate a long progress bar */
21+
.col-cumulative { text-align: left; width: auto; }
3422

35-
/* Modal styles */
36-
.modal {
37-
display: block;
38-
position: fixed;
39-
z-index: 999;
40-
left: 0;
41-
top: 0;
42-
width: 100%;
43-
height: 100%;
44-
overflow: auto;
45-
background-color: rgba(0,0,0,0.4);
46-
}
47-
.modal-content {
48-
background-color: #fff;
49-
margin: 10% auto;
50-
padding: 20px;
51-
border: 1px solid #888;
52-
width: 80%;
53-
max-width: 600px;
54-
}
55-
.close {
56-
color: #aaa;
57-
float: right;
58-
font-size: 24px;
59-
font-weight: bold;
60-
cursor: pointer;
61-
}
62-
textarea {
63-
width: 100%;
64-
height: 200px;
65-
margin-top: 10px;
66-
font-family: monospace;
67-
}
23+
th { background-color: #f8fafc; color: #4a5568; font-size: 11px; text-transform: uppercase; cursor: pointer; letter-spacing: 0.05em; }
24+
25+
/* Wide Cumulative Bar */
26+
.cum-wrapper { display: flex; align-items: center; gap: 15px; width: 100%; }
27+
.cum-container { flex-grow: 1; background: #edf2f7; height: 14px; border-radius: 7px; overflow: hidden; min-width: 200px; }
28+
.cum-bar { height: 100%; background: linear-gradient(90deg, #4299e1 0%, #3182ce 100%); transition: width 0.5s ease-out; }
29+
.cum-text { min-width: 50px; font-weight: bold; font-size: 13px; color: #2b6cb0; }
30+
31+
/* Modal */
32+
.modal { display: block; position: fixed; z-index: 100; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px); }
33+
.modal-content { background: #fff; margin: 5% auto; padding: 25px; border-radius: 12px; width: 80%; max-width: 900px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); }
34+
textarea { width: 100%; height: 350px; font-family: 'Fira Code', monospace; margin: 15px 0; padding: 12px; border: 1px solid #e2e8f0; border-radius: 6px; box-sizing: border-box; font-size: 12px; }
6835
</style>
6936
</head>
7037
<body>
7138
<div id="app">
72-
<button @click="showModal = true">Paste repquota -a Output</button>
39+
<button @click="showModal = true">📂 Load Data</button>
40+
41+
<div class="summary-card" v-if="quotaData.length">
42+
<strong>Total Disk footprint:</strong> {{ formatSize(totalSystemUsed) }}
43+
<small style="color: #718096; margin-left: 10px;">({{ (totalSystemUsed * 1024).toLocaleString() }} Bytes)</small>
44+
</div>
7345

74-
<!-- Modal -->
7546
<div class="modal" v-if="showModal">
7647
<div class="modal-content">
77-
<span class="close" @click="showModal = false">&times;</span>
78-
<h2>Paste Output</h2>
79-
<button @click="getQuota">Get From /status/repquota</button>
80-
<textarea v-model="rawInput" placeholder="Paste repquota -a output here..."></textarea>
81-
82-
<button @click="parseQuota">Parse</button>
48+
<h3>Paste repquota Output</h3>
49+
<textarea v-model="rawInput" placeholder="Paste results here..."></textarea>
50+
<div style="text-align: right;">
51+
<button @click="parseQuota" style="background: #3182ce; color: white; border: none; padding: 12px 24px;">Analyze Storage</button>
52+
</div>
8353
</div>
8454
</div>
8555

86-
<!-- Table -->
8756
<table v-if="quotaData.length">
8857
<thead>
8958
<tr>
90-
<th v-for="(h, index) in headers" :key="index" @click="sortBy(index)">
91-
{{ h }}
92-
</th>
59+
<th>User</th>
60+
<th>Used Bytes</th>
61+
<th>Human Readable</th>
62+
<th>% Share</th>
63+
<th class="col-cumulative">Cumulative % (Downward)</th>
9364
</tr>
9465
</thead>
9566
<tbody>
96-
<tr v-for="(row, rowIndex) in quotaData" :key="rowIndex">
97-
<td>{{ row.user }}</td>
98-
<td>{{ row.flags }}</td>
99-
<td>{{ row.blockUsed }}</td>
100-
<td>{{ row.blockSoft }}</td>
101-
<td>{{ row.blockHard }}</td>
102-
<td>{{ row.blockGrace }}</td>
103-
<td>{{ row.inodeUsed }}</td>
104-
<td>{{ row.inodeSoft }}</td>
105-
<td>{{ row.inodeHard }}</td>
106-
<td>{{ row.inodeGrace }}</td>
67+
<tr v-for="(row, index) in quotaData" :key="row.user">
68+
<td><strong>{{ row.user }}</strong></td>
69+
<td style="font-family: monospace; color: #718096;">{{ (row.blockUsed * 1024).toLocaleString() }}</td>
70+
<td style="font-weight: 500;">{{ formatSize(row.blockUsed) }}</td>
71+
<td>{{ ((row.blockUsed / totalSystemUsed) * 100).toFixed(2) }}%</td>
72+
<td class="col-cumulative">
73+
<div class="cum-wrapper">
74+
<div class="cum-container">
75+
<div class="cum-bar" :style="{ width: row.cumulativePct + '%' }"></div>
76+
</div>
77+
<span class="cum-text">{{ row.cumulativePct.toFixed(1) }}%</span>
78+
</div>
79+
</td>
10780
</tr>
10881
</tbody>
10982
</table>
@@ -115,76 +88,51 @@ <h2>Paste Output</h2>
11588
createApp({
11689
data() {
11790
return {
118-
showModal: false,
91+
showModal: true,
11992
rawInput: '',
12093
quotaData: [],
121-
headers: [
122-
'User', 'Flags', 'Block Used', 'Block Soft', 'Block Hard', 'Block Grace',
123-
'Inode Used', 'Inode Soft', 'Inode Hard', 'Inode Grace'
124-
],
125-
currentSortIndex: null,
126-
sortAsc: true
94+
totalSystemUsed: 0
12795
};
12896
},
12997
methods: {
130-
async getQuota() {
131-
this.rawInput = await (await fetch('/status/repquota')).text();
98+
formatSize(kb) {
99+
if (kb === 0) return '0 KB';
100+
if (kb < 1024) return kb + ' KB';
101+
if (kb < 1024 * 1024) return (kb / 1024).toFixed(2) + ' MB';
102+
if (kb < 1024 * 1024 * 1024) return (kb / (1024 * 1024)).toFixed(2) + ' GB';
103+
return (kb / (1024 * 1024 * 1024)).toFixed(2) + ' TB';
132104
},
133105
parseQuota() {
134-
this.quotaData = [];
135-
const lines = this.rawInput.split('\n').filter(line =>
136-
/^[a-zA-Z0-9]/.test(line.trim())
106+
const lines = this.rawInput.split('\n').filter(line =>
107+
/^[a-zA-Z0-9]/.test(line.trim()) && !line.startsWith('User') && !line.startsWith('***')
137108
);
138109

139-
this.quotaData = lines.map(line => {
110+
let items = lines.map(line => {
140111
const parts = line.trim().split(/\s+/);
141-
const [user, flags, ...rest] = parts;
142112
return {
143-
user,
144-
flags,
145-
blockUsed: +rest[0],
146-
blockSoft: +rest[1],
147-
blockHard: +rest[2],
148-
blockGrace: rest[3] || '',
149-
inodeUsed: +rest[4],
150-
inodeSoft: +rest[5],
151-
inodeHard: +rest[6],
152-
inodeGrace: rest[7] || ''
113+
user: parts[0],
114+
blockUsed: +parts[2] || 0
153115
};
154116
});
155117

156-
this.showModal = false;
157-
},
158-
sortBy(index) {
159-
const keyMap = [
160-
'user', 'flags', 'blockUsed', 'blockSoft', 'blockHard', 'blockGrace',
161-
'inodeUsed', 'inodeSoft', 'inodeHard', 'inodeGrace'
162-
];
163-
const key = keyMap[index];
118+
// Sort by usage descending
119+
items.sort((a, b) => b.blockUsed - a.blockUsed);
164120

165-
if (this.currentSortIndex === index) {
166-
this.sortAsc = !this.sortAsc;
167-
} else {
168-
this.currentSortIndex = index;
169-
this.sortAsc = true;
170-
}
171-
172-
const isNumeric = typeof this.quotaData[0][key] === 'number';
173-
174-
this.quotaData.sort((a, b) => {
175-
const valA = a[key];
176-
const valB = b[key];
177-
if (isNumeric) {
178-
return this.sortAsc ? valA - valB : valB - valA;
179-
} else {
180-
return this.sortAsc
181-
? String(valA).localeCompare(valB)
182-
: String(valB).localeCompare(valA);
183-
}
121+
this.totalSystemUsed = items.reduce((sum, item) => sum + item.blockUsed, 0);
122+
123+
let runningTotal = 0;
124+
this.quotaData = items.map(item => {
125+
runningTotal += item.blockUsed;
126+
return {
127+
...item,
128+
cumulativePct: this.totalSystemUsed > 0 ? (runningTotal / this.totalSystemUsed) * 100 : 0
129+
};
184130
});
131+
132+
this.showModal = false;
185133
}
186134
}
187135
}).mount('#app');
188136
</script>
189137
</body>
190-
</html>
138+
</html>

0 commit comments

Comments
 (0)