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 "> ×</ 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 - z A - Z 0 - 9 ] / . test ( line . trim ( ) )
106+ const lines = this . rawInput . split ( '\n' ) . filter ( line =>
107+ / ^ [ a - z A - Z 0 - 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