cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
root / app / templates / stats.html

stats.html (6791B)


      1 {% extends "base.html" %}
      2 {% set page_title = 'Stats' %}
      3 {% set page_description = 'What is on the CDN: file counts, sizes and types.' %}
      4 {% block page_class %}page-stats{% endblock %}
      5 
      6 {% block content %}
      7   <h1>CDN stats</h1>
      8   <p class="muted" id="sub">Loading…</p>
      9   <div id="out"></div>
     10 {% endblock %}
     11 
     12 {% block scripts %}
     13 <script>
     14 (function () {
     15   var C = ['--c0','--c1','--c2','--c3','--c4','--c5','--c6','--c8'], OTHER = 'var(--c7)';
     16   var $ = function (id) { return document.getElementById(id); };
     17   var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]; }); };
     18   var nf = new Intl.NumberFormat();
     19   function B(n) { var u = ['B','KB','MB','GB','TB'], i = 0; while (n >= 1024 && i < 4) { n /= 1024; i++; } return (i ? n.toFixed(n < 10 ? 1 : 0) : n) + ' ' + u[i]; }
     20   function pct(a, b) { return b ? Math.round(a / b * 100) : 0; }
     21   function color(i, key) { return key === 'other' ? OTHER : 'var(' + C[i % C.length] + ')'; }
     22 
     23   function donut(cats, mode, t) {
     24     var total = mode === 'size' ? t.bytes : t.files, acc = 0, segs = '';
     25     cats.forEach(function (c, i) {
     26       var v = mode === 'size' ? c.bytes : c.files, p = total ? v / total * 100 : 0;
     27       if (p > 0) segs += '<circle cx="21" cy="21" r="15.9155" stroke="' + color(i, c.key) + '" stroke-dasharray="' + p.toFixed(3) + ' ' + (100 - p).toFixed(3) + '" stroke-dashoffset="' + (-acc).toFixed(3) + '"/>';
     28       acc += p;
     29     });
     30     var legend = cats.map(function (c, i) {
     31       var v = mode === 'size' ? c.bytes : c.files;
     32       return '<li><i style="background:' + color(i, c.key) + '"></i>' + esc(c.label) + '<span class="n">' + (mode === 'size' ? B(v) : nf.format(v)) + ' · ' + pct(v, total) + '%</span></li>';
     33     }).join('');
     34     return '<div class="donut-wrap"><div class="donut" role="img" aria-label="File types by ' + (mode === 'size' ? 'size' : 'count') + '"><svg viewBox="0 0 42 42">' +
     35       '<circle cx="21" cy="21" r="15.9155" stroke="var(--surface-3)"/>' + segs + '</svg><div class="hole"><b>' + (mode === 'size' ? B(total) : nf.format(total)) +
     36       '</b><span class="muted">' + (mode === 'size' ? 'total' : 'files') + '</span></div></div><ul class="legend">' + legend + '</ul></div>';
     37   }
     38   function bars(rows, label, val, num, sub) {
     39     var max = Math.max.apply(null, rows.map(num).concat(1));
     40     return '<div class="bars">' + rows.map(function (r) {
     41       return '<div class="bar"><div class="top"><span>' + esc(label(r)) + '</span><span>' + val(r) + '</span></div><div class="track"><i style="width:' + Math.max(1, num(r) / max * 100).toFixed(1) + '%"></i></div></div>';
     42     }).join('') + '</div>';
     43   }
     44   function cols(rows, label, num, fmt, tip) {
     45     var max = Math.max.apply(null, rows.map(num).concat(1));
     46     return '<div class="cols">' + rows.map(function (r) {
     47       return '<div class="col" title="' + esc(tip(r)) + '"><span class="v">' + fmt(num(r)) + '</span><i style="height:' + Math.max(1, num(r) / max * 100).toFixed(1) + '%"></i></div>';
     48     }).join('') + '</div><div class="labels">' + rows.map(function (r) { return '<span>' + esc(label(r)) + '</span>'; }).join('') + '</div>';
     49   }
     50   function short(n) { return n >= 10000 ? (n / 1000).toFixed(0) + 'k' : n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
     51   function mon(m) { var p = m.split('-'); return new Date(Date.UTC(+p[0], +p[1] - 1, 1)).toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }) + (p[1] === '01' ? ' ’' + p[0].slice(2) : ''); }
     52 
     53   function render(d) {
     54     if (d.empty) { $('sub').textContent = ''; $('out').innerHTML = '<div class="note">No stats yet - the file list could not be read, or the CDN is empty.<br><button onclick="location.reload()">Try again</button></div>'; return; }
     55     var t = d.totals, mode = 'files';
     56     $('sub').textContent = 'Updated ' + new Date(d.generated * 1000).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }) + ' · refreshed every few minutes';
     57     var h = '<div class="tiles">' +
     58       [[nf.format(t.files), 'files'], [B(t.bytes), 'total size'], [B(t.median), 'median file'], [nf.format(t.types), 'file types']].map(function (x) { return '<div class="tile"><b>' + x[0] + '</b><span>' + x[1] + '</span></div>'; }).join('') +
     59       '</div><div class="grid">' +
     60       '<section class="card span2"><header><h2>What\'s on the CDN</h2><div class="seg" id="seg"><button aria-pressed="true" data-m="files">Files</button><button aria-pressed="false" data-m="size">Size</button></div></header><div id="donut"></div></section>' +
     61       '<section class="card"><header><h2>Top file types</h2></header>' + bars(d.extensions, function (r) { return r.ext === '(none)' ? '(no extension)' : '.' + r.ext; }, function (r) { return nf.format(r.files) + ' · ' + B(r.bytes); }, function (r) { return r.files; }) +
     62         (d.extensions_more ? '<p class="muted" style="margin:12px 0 0">+ ' + nf.format(d.extensions_more) + ' more files in rarer types</p>' : '') + '</section>' +
     63       '<section class="card"><header><h2>File sizes</h2></header>' + cols(d.sizes, function (r) { return r.label; }, function (r) { return r.files; }, short, function (r) { return r.label + ': ' + nf.format(r.files) + ' files'; }) + '</section>' +
     64       '<section class="card"><header><h2>Biggest folders</h2></header>' + bars(d.folders, function (r) { return r.name; }, function (r) { return B(r.bytes) + ' · ' + nf.format(r.files) + ' files'; }, function (r) { return r.bytes; }) + '</section>' +
     65       (d.months.length > 1 ? '<section class="card"><header><h2>Files added per month</h2></header>' + cols(d.months, function (r) { return mon(r.m); }, function (r) { return r.files; }, short, function (r) { return r.m + ': ' + nf.format(r.files) + ' files, ' + B(r.bytes); }) + '</section>' : '') +
     66       '<section class="card span2"><header><h2>Largest files</h2></header><ul class="files">' + d.largest.map(function (f) { return '<li><a href="/' + encodeURI(f.path) + '" title="' + esc(f.path) + '">' + esc(f.name) + '</a><span>' + B(f.size) + '</span></li>'; }).join('') + '</ul></section></div>';
     67     $('out').innerHTML = h;
     68     function draw() { $('donut').innerHTML = donut(d.categories, mode, t); }
     69     draw();
     70     $('seg').addEventListener('click', function (e) {
     71       var b = e.target.closest('button'); if (!b) return; mode = b.dataset.m;
     72       [].forEach.call($('seg').children, function (x) { x.setAttribute('aria-pressed', x === b); }); draw();
     73     });
     74   }
     75   fetch('/api/stats').then(function (r) { if (!r.ok) throw 0; return r.json(); }).then(render)
     76     .catch(function () { $('sub').textContent = ''; $('out').innerHTML = '<div class="note">Could not load the stats.<br><button onclick="location.reload()">Retry</button></div>'; });
     77 })();
     78 </script>
     79 {% endblock %}