secubox-openwrt/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js
2025-12-29 09:03:49 +01:00

208 lines
7.3 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require secubox-theme/theme as Theme';
'require cdn-cache/nav as CdnNav';
var callStatus = rpc.declare({
object: 'luci.cdn-cache',
method: 'status',
expect: { }
});
var callStats = rpc.declare({
object: 'luci.cdn-cache',
method: 'stats',
expect: { }
});
var callCacheSize = rpc.declare({
object: 'luci.cdn-cache',
method: 'cache_size',
expect: { }
});
var callTopDomains = rpc.declare({
object: 'luci.cdn-cache',
method: 'top_domains',
expect: { domains: [] }
});
function formatBytes(bytes) {
if (!bytes)
return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
function formatUptime(seconds) {
if (!seconds)
return '0s';
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
if (days)
return days + 'd ' + hours + 'h';
if (hours)
return hours + 'h ' + minutes + 'm';
return minutes + 'm ' + (seconds % 60) + 's';
}
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
return view.extend({
load: function() {
return Promise.all([
callStatus(),
callStats(),
callCacheSize(),
callTopDomains()
]);
},
render: function(data) {
var status = data[0] || {};
var stats = data[1] || {};
var cacheSize = data[2] || {};
var topDomains = (data[3] && data[3].domains) || [];
return E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
CdnNav.renderTabs('overview'),
this.renderHeader(status),
this.renderMetricGrid(stats, cacheSize),
this.renderSections(stats, cacheSize, topDomains)
]);
},
renderHeader: function(status) {
var stats = [
{ icon: '🏷️', label: _('Version'), value: status.version || _('Unknown') },
{ icon: '🟢', label: _('Service'), value: status.running ? _('Running') : _('Stopped'), tone: status.running ? 'success' : 'danger' },
{ icon: '⏱', label: _('Uptime'), value: formatUptime(status.uptime || 0) },
{ icon: '📁', label: _('Cache files'), value: (status.cache_files || 0).toLocaleString() }
];
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '📦'),
_('CDN Cache Control')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Edge caching for media, firmware, and downloads'))
]),
E('div', { 'class': 'sh-header-meta' }, stats.map(this.renderHeaderChip, this))
]);
},
renderHeaderChip: function(stat) {
return E('div', { 'class': 'sh-header-chip' + (stat.tone ? ' ' + stat.tone : '') }, [
E('span', { 'class': 'sh-chip-icon' }, stat.icon || '•'),
E('div', { 'class': 'sh-chip-text' }, [
E('span', { 'class': 'sh-chip-label' }, stat.label),
E('strong', {}, stat.value)
])
]);
},
renderMetricGrid: function(stats, cacheSize) {
return E('section', { 'class': 'cdn-metric-grid' }, [
this.renderMetricCard('🎯', _('Hit Ratio'), (stats.hit_ratio || 0) + '%', (stats.hits || 0).toLocaleString() + ' hits / ' + (stats.misses || 0).toLocaleString() + ' misses'),
this.renderMetricCard('💾', _('Cache Used'), formatBytes((cacheSize.used_kb || 0) * 1024), (cacheSize.usage_percent || 0) + '% of ' + formatBytes((cacheSize.max_kb || 0) * 1024)),
this.renderMetricCard('📊', _('Requests'), (stats.requests || 0).toLocaleString(), _('Objects cached: ') + (stats.unique_objects || 0)),
this.renderMetricCard('⚡', _('Bandwidth Saved'), formatBytes(stats.bandwidth_saved_bytes || 0), _('Local delivery savings'))
]);
},
renderMetricCard: function(icon, label, value, sub) {
return E('div', { 'class': 'cdn-metric-card' }, [
E('div', { 'class': 'cdn-card-icon' }, icon),
E('div', { 'class': 'cdn-metric-label' }, label),
E('div', { 'class': 'cdn-metric-value' }, value),
E('div', { 'class': 'cdn-metric-sub' }, sub)
]);
},
renderSections: function(stats, cacheSize, topDomains) {
return E('div', { 'class': 'cdn-sections-grid' }, [
E('div', { 'class': 'cdn-section' }, [
E('div', { 'class': 'cdn-section-header' }, [
E('div', { 'class': 'cdn-section-title' }, ['🎯', _('Hit Ratio')]),
E('span', { 'class': 'sb-badge sb-badge-ghost' }, _('Real-time'))
]),
this.renderRatioGauge(stats.hit_ratio || 0),
E('div', { 'class': 'cdn-domain-stats' }, [
_('Hits: ') + (stats.hits || 0).toLocaleString(),
_('Misses: ') + (stats.misses || 0).toLocaleString(),
_('Requests: ') + (stats.requests || 0).toLocaleString()
])
]),
E('div', { 'class': 'cdn-section' }, [
E('div', { 'class': 'cdn-section-header' }, [
E('div', { 'class': 'cdn-section-title' }, ['🌐', _('Top Domains Served')]),
E('span', { 'class': 'sb-badge sb-badge-ghost' }, _('Last hour'))
]),
E('ul', { 'class': 'cdn-domain-list' },
topDomains.slice(0, 6).map(function(domain) {
return E('li', { 'class': 'cdn-domain-item' }, [
E('div', { 'class': 'cdn-domain-name' }, domain.name || _('Unknown')),
E('div', { 'class': 'cdn-domain-stats' }, [
_('Hits: ') + (domain.hits || 0).toLocaleString(),
_('Traffic: ') + formatBytes(domain.bytes || 0)
])
]);
}))
]),
E('div', { 'class': 'cdn-section' }, [
E('div', { 'class': 'cdn-section-header' }, [
E('div', { 'class': 'cdn-section-title' }, ['💾', _('Cache Utilisation')])
]),
this.renderProgress(_('Usage'), cacheSize.usage_percent || 0, formatBytes((cacheSize.used_kb || 0) * 1024) + ' / ' + formatBytes((cacheSize.max_kb || 0) * 1024)),
E('div', { 'class': 'cdn-savings-card' }, [
E('div', { 'class': 'cdn-savings-value' }, formatBytes(stats.bandwidth_saved_bytes || 0)),
E('div', { 'class': 'cdn-savings-label' }, _('Bandwidth saved this week'))
])
])
]);
},
renderRatioGauge: function(ratio) {
var radius = 60;
var circumference = 2 * Math.PI * radius;
var normalized = Math.min(100, Math.max(0, ratio));
var offset = circumference - normalized / 100 * circumference;
return E('div', { 'class': 'cdn-ratio-circle' }, [
E('svg', { 'width': '140', 'height': '140', 'class': 'cdn-ratio-spark' }, [
E('circle', { 'class': 'bg', 'cx': '70', 'cy': '70', 'r': String(radius) }),
E('circle', {
'class': 'fg',
'cx': '70',
'cy': '70',
'r': String(radius),
'style': 'stroke-dasharray:' + circumference + ';stroke-dashoffset:' + offset + ';'
})
]),
E('div', { 'class': 'cdn-ratio-value' }, normalized + '%')
]);
},
renderProgress: function(label, percent, meta) {
return E('div', { 'class': 'cdn-progress-stack' }, [
E('div', { 'class': 'secubox-stat-details' }, [
E('div', { 'class': 'secubox-stat-label' }, label),
E('div', { 'class': 'secubox-stat-value' }, percent + '%')
]),
E('div', { 'class': 'cdn-progress-bar' }, [
E('div', { 'class': 'cdn-progress-fill', 'style': 'width:' + Math.min(100, Math.max(0, percent)) + '%;' })
]),
E('div', { 'class': 'secubox-stat-label' }, meta)
]);
}
});