Convert 90+ LuCI view files from legacy cbi-button-* classes to KissTheme kiss-btn-* classes for consistent dark theme styling. Pattern conversions applied: - cbi-button-positive → kiss-btn-green - cbi-button-negative/remove → kiss-btn-red - cbi-button-apply → kiss-btn-cyan - cbi-button-action → kiss-btn-blue - cbi-button (plain) → kiss-btn Also replaced hardcoded colors (#080, #c00, #888, etc.) with CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.) for proper dark theme compatibility. Apps updated include: ai-gateway, auth-guardian, bandwidth-manager, cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure, glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager, mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer, metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status, secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats, service-registry, simplex, streamlit, system-hub, tor-shield, traffic-shaper, vhost-manager, vortex-dns, vortex-firewall, webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
307 lines
11 KiB
JavaScript
307 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require secubox-p2p/api as P2PAPI';
|
|
'require secubox/kiss-theme';
|
|
|
|
/**
|
|
* SecuBox Development Status Widget
|
|
* Generative and dynamic dashboard showing:
|
|
* - Gitea commit activity
|
|
* - MirrorBox App Store stats
|
|
* - Progress toward v1.0 (0-100%)
|
|
*/
|
|
|
|
return view.extend({
|
|
// State
|
|
commits: [],
|
|
repos: [],
|
|
packages: { local: 0, peer: 0, unique: 0, sources: 0 },
|
|
giteaConnected: false,
|
|
loading: true,
|
|
refreshInterval: 30000,
|
|
|
|
// v1.0 Milestones - features required for 1.0 release
|
|
milestones: [
|
|
{ id: 'core', name: 'Core Framework', target: 15, current: 15, complete: true },
|
|
{ id: 'security', name: 'Security Suite', target: 10, current: 10, complete: true },
|
|
{ id: 'network', name: 'Network Tools', target: 8, current: 8, complete: true },
|
|
{ id: 'p2p', name: 'P2P Mesh', target: 12, current: 12, complete: true },
|
|
{ id: 'apps', name: 'App Ecosystem', target: 25, current: 20, complete: false },
|
|
{ id: 'media', name: 'Media Services', target: 5, current: 4, complete: false },
|
|
{ id: 'remote', name: 'Remote Access', target: 3, current: 2, complete: false },
|
|
{ id: 'docs', name: 'Documentation', target: 10, current: 7, complete: false }
|
|
],
|
|
|
|
load: function() {
|
|
var self = this;
|
|
self.loading = true;
|
|
|
|
return Promise.all([
|
|
P2PAPI.getGiteaCommits(20).catch(function() { return { commits: [] }; }),
|
|
P2PAPI.listGiteaRepos().catch(function() { return { repos: [] }; }),
|
|
P2PAPI.getAllPackages().catch(function() { return { sources: [] }; })
|
|
]).then(function(results) {
|
|
// Gitea commits
|
|
var commitResult = results[0] || {};
|
|
self.commits = commitResult.commits || [];
|
|
self.giteaConnected = commitResult.success !== false && self.commits.length > 0;
|
|
|
|
// Gitea repos
|
|
var repoResult = results[1] || {};
|
|
self.repos = repoResult.repos || [];
|
|
|
|
// Package stats
|
|
var pkgResult = results[2] || {};
|
|
var sources = pkgResult.sources || [];
|
|
var localCount = 0, peerCount = 0, uniqueNames = new Set();
|
|
|
|
sources.forEach(function(source) {
|
|
var pkgs = source.packages || [];
|
|
pkgs.forEach(function(p) { uniqueNames.add(p.name); });
|
|
if (source.type === 'local') {
|
|
localCount = source.package_count || pkgs.length;
|
|
} else {
|
|
peerCount += source.package_count || pkgs.length;
|
|
}
|
|
});
|
|
|
|
self.packages = {
|
|
local: localCount,
|
|
peer: peerCount,
|
|
unique: uniqueNames.size,
|
|
sources: sources.length
|
|
};
|
|
|
|
// Update milestones based on real data
|
|
self.updateMilestones();
|
|
|
|
self.loading = false;
|
|
});
|
|
},
|
|
|
|
updateMilestones: function() {
|
|
// Dynamically update milestones based on actual data
|
|
var pkgCount = this.packages.local;
|
|
|
|
// Apps milestone: based on package count
|
|
var appsMilestone = this.milestones.find(function(m) { return m.id === 'apps'; });
|
|
if (appsMilestone) {
|
|
appsMilestone.current = Math.min(pkgCount, appsMilestone.target);
|
|
appsMilestone.complete = appsMilestone.current >= appsMilestone.target;
|
|
}
|
|
},
|
|
|
|
calculateProgress: function() {
|
|
var totalTarget = 0, totalCurrent = 0;
|
|
this.milestones.forEach(function(m) {
|
|
totalTarget += m.target;
|
|
totalCurrent += Math.min(m.current, m.target);
|
|
});
|
|
return totalTarget > 0 ? Math.round((totalCurrent / totalTarget) * 100) : 0;
|
|
},
|
|
|
|
formatTimeAgo: function(dateStr) {
|
|
if (!dateStr) return 'Unknown';
|
|
var date = new Date(dateStr);
|
|
var now = new Date();
|
|
var diff = Math.floor((now - date) / 1000);
|
|
|
|
if (diff < 60) return diff + 's ago';
|
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
|
|
return date.toLocaleDateString();
|
|
},
|
|
|
|
renderProgressBar: function(percent, color) {
|
|
return E('div', { 'style': 'background: var(--kiss-bg2); border-radius: 10px; height: 20px; overflow: hidden; position: relative;' }, [
|
|
E('div', {
|
|
'style': 'background: ' + color + '; height: 100%; width: ' + percent + '%; transition: width 0.5s ease;'
|
|
}),
|
|
E('span', {
|
|
'style': 'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-weight: bold; font-size: 12px; color: var(--kiss-text);'
|
|
}, percent + '%')
|
|
]);
|
|
},
|
|
|
|
renderStats: function() {
|
|
var c = KissTheme.colors;
|
|
return [
|
|
KissTheme.stat(this.packages.local, 'Local Packages', c.green),
|
|
KissTheme.stat(this.packages.peer, 'Peer Packages', c.blue),
|
|
KissTheme.stat(this.repos.length, 'Gitea Repos', c.purple),
|
|
KissTheme.stat(this.commits.length, 'Recent Commits', c.orange)
|
|
];
|
|
},
|
|
|
|
renderMilestoneProgress: function() {
|
|
var self = this;
|
|
var progress = this.calculateProgress();
|
|
var c = KissTheme.colors;
|
|
|
|
var progressColor = progress < 50 ? c.red : (progress < 80 ? c.orange : c.green);
|
|
|
|
return KissTheme.card(
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 12px;' }, [
|
|
E('span', {}, 'Progress to v1.0'),
|
|
KissTheme.badge(progress + '%', progress < 50 ? 'red' : (progress < 80 ? 'orange' : 'green'))
|
|
]),
|
|
E('div', {}, [
|
|
this.renderProgressBar(progress, progressColor),
|
|
E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-top: 16px;' },
|
|
this.milestones.map(function(m) {
|
|
var pct = m.target > 0 ? Math.round((Math.min(m.current, m.target) / m.target) * 100) : 0;
|
|
var color = m.complete ? c.green : (pct > 50 ? c.orange : c.blue);
|
|
return E('div', {
|
|
'style': 'background: var(--kiss-bg2); padding: 12px; border-radius: 8px; border-left: 4px solid ' + color + ';'
|
|
}, [
|
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
|
|
E('span', { 'style': 'font-weight: 500; color: var(--kiss-text);' }, m.name),
|
|
m.complete ?
|
|
E('span', { 'style': 'color: ' + c.green + '; font-size: 14px;' }, '✓') :
|
|
E('span', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, m.current + '/' + m.target)
|
|
]),
|
|
E('div', { 'style': 'margin-top: 8px;' }, self.renderProgressBar(pct, color))
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
);
|
|
},
|
|
|
|
renderCommitLog: function() {
|
|
var self = this;
|
|
var c = KissTheme.colors;
|
|
|
|
if (!this.giteaConnected) {
|
|
return E('div', { 'style': 'text-align: center; padding: 32px; color: var(--kiss-muted);' }, [
|
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 8px;' }, '🔌'),
|
|
E('p', {}, _('Gitea not connected')),
|
|
E('p', { 'style': 'font-size: 12px;' }, _('Configure Gitea in MirrorBox > Hub settings'))
|
|
]);
|
|
}
|
|
|
|
if (this.commits.length === 0) {
|
|
return E('p', { 'style': 'color: var(--kiss-muted); text-align: center;' }, _('No recent commits'));
|
|
}
|
|
|
|
return E('div', { 'style': 'max-height: 400px; overflow-y: auto;' },
|
|
this.commits.slice(0, 15).map(function(commit) {
|
|
var msg = commit.message || commit.commit_message || '';
|
|
var shortMsg = msg.split('\n')[0];
|
|
if (shortMsg.length > 60) shortMsg = shortMsg.substring(0, 57) + '...';
|
|
|
|
var author = commit.author || commit.committer || {};
|
|
var authorName = author.name || author.login || 'Unknown';
|
|
var date = commit.created || commit.committed_date || commit.date;
|
|
|
|
return E('div', {
|
|
'style': 'padding: 12px; border-bottom: 1px solid var(--kiss-line); display: flex; gap: 12px;'
|
|
}, [
|
|
E('div', {
|
|
'style': 'width: 40px; height: 40px; background: ' + c.blue + '; border-radius: 50%; display: flex; ' +
|
|
'align-items: center; justify-content: center; color: white; font-weight: bold; flex-shrink: 0;'
|
|
}, authorName.charAt(0).toUpperCase()),
|
|
E('div', { 'style': 'flex: 1; min-width: 0;' }, [
|
|
E('div', { 'style': 'font-weight: 500; margin-bottom: 4px; color: var(--kiss-text);' }, shortMsg),
|
|
E('div', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, [
|
|
E('span', {}, authorName),
|
|
E('span', { 'style': 'margin: 0 8px;' }, '•'),
|
|
E('span', {}, self.formatTimeAgo(date))
|
|
])
|
|
])
|
|
]);
|
|
})
|
|
);
|
|
},
|
|
|
|
renderRepoList: function() {
|
|
if (this.repos.length === 0) {
|
|
return E('p', { 'style': 'color: var(--kiss-muted); text-align: center;' }, _('No repositories'));
|
|
}
|
|
|
|
return E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 8px;' },
|
|
this.repos.slice(0, 10).map(function(repo) {
|
|
var name = repo.name || repo.full_name || 'Unknown';
|
|
return E('span', {
|
|
'style': 'background: var(--kiss-bg2); padding: 6px 12px; border-radius: 16px; font-size: 13px; color: var(--kiss-text);'
|
|
}, name);
|
|
})
|
|
);
|
|
},
|
|
|
|
render: function() {
|
|
var self = this;
|
|
|
|
if (this.loading) {
|
|
var content = [
|
|
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Development Status'),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'Loading development data...')
|
|
]),
|
|
E('div', { 'style': 'text-align: center; padding: 60px;' }, [
|
|
E('p', { 'class': 'spinning' }, _('Loading...'))
|
|
])
|
|
];
|
|
return KissTheme.wrap(content, 'admin/secubox/p2p/devstatus');
|
|
}
|
|
|
|
// Start polling
|
|
poll.add(function() {
|
|
return self.load().then(function() {
|
|
dom.content(document.getElementById('devstatus-content'), self.renderContent());
|
|
});
|
|
}, this.refreshInterval);
|
|
|
|
var content = [
|
|
// Header
|
|
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
|
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
|
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Development Status'),
|
|
KissTheme.badge('SecuBox v0.17.0 → v1.0', 'blue')
|
|
]),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
|
|
'Real-time development progress from Gitea and MirrorBox App Store')
|
|
]),
|
|
|
|
E('div', { 'id': 'devstatus-content' }, this.renderContent())
|
|
];
|
|
|
|
return KissTheme.wrap(content, 'admin/secubox/p2p/devstatus');
|
|
},
|
|
|
|
renderContent: function() {
|
|
return E('div', {}, [
|
|
// Stats
|
|
E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom: 20px;' },
|
|
this.renderStats()),
|
|
|
|
// Milestone Progress
|
|
this.renderMilestoneProgress(),
|
|
|
|
// Two column layout
|
|
E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'margin-top: 20px;' }, [
|
|
KissTheme.card('Recent Commits', this.renderCommitLog()),
|
|
KissTheme.card(
|
|
'Repositories',
|
|
E('div', {}, [
|
|
this.renderRepoList(),
|
|
E('hr', { 'style': 'margin: 16px 0; border: none; border-top: 1px solid var(--kiss-line);' }),
|
|
E('h4', { 'style': 'margin: 0 0 8px 0; color: var(--kiss-text);' }, _('Feed Sources')),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' },
|
|
this.packages.sources + ' sources providing ' + this.packages.unique + ' unique packages'
|
|
)
|
|
])
|
|
)
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|