feat(ui): Add KISS theme with eye toggle and git status tracking
- Add shared kiss-theme.js module for consistent dark theme across views
- Add eye toggle button (👁️) to switch between KISS and LuCI modes
- Add git repo status methods to luci.gitea RPCD:
- get_repo_status: branch, ahead/behind, staged/modified files
- get_commit_history: recent commits with stats
- get_commit_stats: daily commit counts for graphs
- Update InterceptoR overview with KISS styling and responsive grid
- Fix quick links paths (network-tweaks → admin/network/)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ab19cb778
commit
03e90bb4af
@ -634,6 +634,152 @@ create_repo() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Get git repo status (for local development tracking)
|
||||
get_repo_status() {
|
||||
read -r input
|
||||
local repo_path
|
||||
repo_path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
|
||||
|
||||
# Default to secubox-openwrt if no path specified
|
||||
[ -z "$repo_path" ] && repo_path="/root/secubox-openwrt"
|
||||
|
||||
if [ ! -d "$repo_path/.git" ]; then
|
||||
json_error "Not a git repository: $repo_path"
|
||||
return
|
||||
fi
|
||||
|
||||
cd "$repo_path" || { json_error "Cannot access $repo_path"; return; }
|
||||
|
||||
local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||||
local remote_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "")
|
||||
local commit_hash=$(git rev-parse --short HEAD 2>/dev/null || echo "")
|
||||
local commit_msg=$(git log -1 --pretty=%s 2>/dev/null || echo "")
|
||||
local commit_date=$(git log -1 --pretty=%ci 2>/dev/null || echo "")
|
||||
|
||||
# Commits ahead/behind
|
||||
local ahead=0 behind=0
|
||||
if [ -n "$remote_branch" ]; then
|
||||
ahead=$(git rev-list --count HEAD ^${remote_branch} 2>/dev/null || echo "0")
|
||||
behind=$(git rev-list --count ${remote_branch} ^HEAD 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
# Diff stats
|
||||
local staged=$(git diff --cached --numstat 2>/dev/null | wc -l)
|
||||
local modified=$(git diff --numstat 2>/dev/null | wc -l)
|
||||
local untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l)
|
||||
|
||||
# Lines changed
|
||||
local lines_added=$(git diff --numstat 2>/dev/null | awk '{sum+=$1} END {print sum+0}')
|
||||
local lines_deleted=$(git diff --numstat 2>/dev/null | awk '{sum+=$2} END {print sum+0}')
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "repo_path" "$repo_path"
|
||||
json_add_string "branch" "$branch"
|
||||
json_add_string "remote_branch" "$remote_branch"
|
||||
json_add_string "commit_hash" "$commit_hash"
|
||||
json_add_string "commit_msg" "$commit_msg"
|
||||
json_add_string "commit_date" "$commit_date"
|
||||
json_add_int "ahead" "${ahead:-0}"
|
||||
json_add_int "behind" "${behind:-0}"
|
||||
json_add_int "staged" "${staged:-0}"
|
||||
json_add_int "modified" "${modified:-0}"
|
||||
json_add_int "untracked" "${untracked:-0}"
|
||||
json_add_int "lines_added" "${lines_added:-0}"
|
||||
json_add_int "lines_deleted" "${lines_deleted:-0}"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get recent commits for evolution graph
|
||||
get_commit_history() {
|
||||
read -r input
|
||||
local repo_path limit
|
||||
repo_path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
|
||||
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null)
|
||||
|
||||
[ -z "$repo_path" ] && repo_path="/root/secubox-openwrt"
|
||||
[ -z "$limit" ] && limit=20
|
||||
|
||||
if [ ! -d "$repo_path/.git" ]; then
|
||||
json_error "Not a git repository: $repo_path"
|
||||
return
|
||||
fi
|
||||
|
||||
cd "$repo_path" || { json_error "Cannot access $repo_path"; return; }
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_array "commits"
|
||||
|
||||
git log --pretty=format:'%H|%h|%s|%an|%ae|%ci|%cr' -n "$limit" 2>/dev/null | while IFS='|' read -r hash short_hash msg author email date relative; do
|
||||
# Get stats for this commit
|
||||
local stats=$(git show --stat --format='' "$hash" 2>/dev/null | tail -1)
|
||||
local files_changed=$(echo "$stats" | grep -oP '\d+(?= files? changed)' || echo "0")
|
||||
local insertions=$(echo "$stats" | grep -oP '\d+(?= insertions?)' || echo "0")
|
||||
local deletions=$(echo "$stats" | grep -oP '\d+(?= deletions?)' || echo "0")
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "hash" "$hash"
|
||||
json_add_string "short_hash" "$short_hash"
|
||||
json_add_string "message" "$msg"
|
||||
json_add_string "author" "$author"
|
||||
json_add_string "date" "$date"
|
||||
json_add_string "relative" "$relative"
|
||||
json_add_int "files_changed" "${files_changed:-0}"
|
||||
json_add_int "insertions" "${insertions:-0}"
|
||||
json_add_int "deletions" "${deletions:-0}"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get daily commit stats for graph
|
||||
get_commit_stats() {
|
||||
read -r input
|
||||
local repo_path days
|
||||
repo_path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
|
||||
days=$(echo "$input" | jsonfilter -e '@.days' 2>/dev/null)
|
||||
|
||||
[ -z "$repo_path" ] && repo_path="/root/secubox-openwrt"
|
||||
[ -z "$days" ] && days=30
|
||||
|
||||
if [ ! -d "$repo_path/.git" ]; then
|
||||
json_error "Not a git repository: $repo_path"
|
||||
return
|
||||
fi
|
||||
|
||||
cd "$repo_path" || { json_error "Cannot access $repo_path"; return; }
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_array "daily_stats"
|
||||
|
||||
# Get commits per day for last N days
|
||||
for i in $(seq 0 $((days - 1))); do
|
||||
local date_str=$(date -d "-${i} days" +%Y-%m-%d 2>/dev/null || date -v-${i}d +%Y-%m-%d 2>/dev/null)
|
||||
[ -z "$date_str" ] && continue
|
||||
|
||||
local count=$(git log --after="${date_str} 00:00" --before="${date_str} 23:59" --oneline 2>/dev/null | wc -l)
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "date" "$date_str"
|
||||
json_add_int "commits" "${count:-0}"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
|
||||
# Total stats
|
||||
local total_commits=$(git rev-list --count HEAD 2>/dev/null || echo "0")
|
||||
local total_authors=$(git log --format='%ae' | sort -u | wc -l)
|
||||
|
||||
json_add_int "total_commits" "$total_commits"
|
||||
json_add_int "total_authors" "$total_authors"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Create backup
|
||||
create_backup() {
|
||||
local result
|
||||
@ -789,7 +935,10 @@ case "$1" in
|
||||
"create_backup": {},
|
||||
"list_backups": {},
|
||||
"restore_backup": {"file": "str"},
|
||||
"get_install_progress": {}
|
||||
"get_install_progress": {},
|
||||
"get_repo_status": {"path": "str"},
|
||||
"get_commit_history": {"path": "str", "limit": 20},
|
||||
"get_commit_stats": {"path": "str", "days": 30}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
@ -861,6 +1010,15 @@ case "$1" in
|
||||
get_install_progress)
|
||||
get_install_progress
|
||||
;;
|
||||
get_repo_status)
|
||||
get_repo_status
|
||||
;;
|
||||
get_commit_history)
|
||||
get_commit_history
|
||||
;;
|
||||
get_commit_stats)
|
||||
get_commit_stats
|
||||
;;
|
||||
*)
|
||||
json_error "Unknown method: $2"
|
||||
;;
|
||||
|
||||
@ -9,174 +9,221 @@ var callGetStatus = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var PILLAR_ICONS = {
|
||||
wpad: '🌐', // Globe for WPAD
|
||||
mitm: '🛡', // Shield for mitmproxy
|
||||
cdn_cache: '💾', // Disk for CDN Cache
|
||||
cookie_tracker: '🍪', // Cookie for Cookie Tracker
|
||||
api_failover: '⚡' // Lightning for API Failover
|
||||
};
|
||||
var PILLARS = [
|
||||
{ id: 'wpad', name: 'WPAD', icon: '🌐', desc: 'Auto-proxy discovery' },
|
||||
{ id: 'mitm', name: 'MITM Proxy', icon: '🛡️', desc: 'Traffic inspection' },
|
||||
{ id: 'cdn_cache', name: 'CDN Cache', icon: '💾', desc: 'Content caching' },
|
||||
{ id: 'cookie_tracker', name: 'Cookies', icon: '🍪', desc: 'Tracker detection' },
|
||||
{ id: 'api_failover', name: 'API Failover', icon: '⚡', desc: 'Graceful degradation' }
|
||||
];
|
||||
|
||||
var PILLAR_NAMES = {
|
||||
wpad: 'WPAD Redirector',
|
||||
mitm: 'MITM Proxy',
|
||||
cdn_cache: 'CDN Cache',
|
||||
cookie_tracker: 'Cookie Tracker',
|
||||
api_failover: 'API Failover'
|
||||
};
|
||||
var QUICK_LINKS = [
|
||||
{ name: 'Network Tweaks', path: 'admin/network/network-tweaks', icon: '🌐' },
|
||||
{ name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy/status', icon: '🔍' },
|
||||
{ name: 'CDN Cache', path: 'admin/services/cdn-cache', icon: '💾' },
|
||||
{ name: 'CrowdSec', path: 'admin/secubox/security/crowdsec/overview', icon: '🛡️' }
|
||||
];
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return callGetStatus();
|
||||
},
|
||||
|
||||
renderHealthScore: function(data) {
|
||||
var summary = data.summary || {};
|
||||
var score = summary.health_score || 0;
|
||||
var pillars_active = summary.pillars_active || 0;
|
||||
var pillars_total = summary.pillars_total || 5;
|
||||
|
||||
var scoreColor = score >= 80 ? '#4caf50' : score >= 50 ? '#ff9800' : '#f44336';
|
||||
|
||||
return E('div', { 'class': 'cbi-section', 'style': 'text-align: center; padding: 30px;' }, [
|
||||
E('div', { 'style': 'font-size: 64px; margin-bottom: 10px;' }, [
|
||||
E('span', { 'style': 'color: ' + scoreColor + '; font-weight: bold;' }, score + '%')
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 18px; color: #888;' },
|
||||
'InterceptoR Health Score'),
|
||||
E('div', { 'style': 'font-size: 14px; color: #666; margin-top: 10px;' },
|
||||
pillars_active + ' of ' + pillars_total + ' pillars active')
|
||||
]);
|
||||
},
|
||||
|
||||
renderPillarCard: function(id, data, name, icon) {
|
||||
var pillarData = data[id] || {};
|
||||
var enabled = pillarData.enabled || false;
|
||||
var running = pillarData.running !== undefined ? pillarData.running : enabled;
|
||||
|
||||
var statusColor = running ? '#4caf50' : '#f44336';
|
||||
var statusText = running ? 'Active' : 'Inactive';
|
||||
|
||||
var statsHtml = [];
|
||||
|
||||
// Build stats based on pillar type
|
||||
switch(id) {
|
||||
case 'wpad':
|
||||
if (pillarData.dhcp_configured) {
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'DHCP: Configured'));
|
||||
}
|
||||
if (pillarData.enforce_enabled) {
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #4caf50;' },
|
||||
'Enforcement: ON'));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mitm':
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Threats Today: ' + (pillarData.threats_today || 0)));
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Active: ' + (pillarData.active_connections || 0)));
|
||||
break;
|
||||
|
||||
case 'cdn_cache':
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Hit Ratio: ' + (pillarData.hit_ratio || 0) + '%'));
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Saved: ' + (pillarData.saved_mb || 0) + ' MB'));
|
||||
if (pillarData.offline_mode) {
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #ff9800;' },
|
||||
'OFFLINE MODE'));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cookie_tracker':
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Cookies: ' + (pillarData.total_cookies || 0)));
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #f44336;' },
|
||||
'Trackers: ' + (pillarData.trackers_detected || 0)));
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Blocked: ' + (pillarData.blocked || 0)));
|
||||
break;
|
||||
|
||||
case 'api_failover':
|
||||
statsHtml.push(E('div', { 'style': 'font-size: 12px; color: #888;' },
|
||||
'Stale Serves: ' + (pillarData.stale_serves || 0)));
|
||||
break;
|
||||
}
|
||||
|
||||
return E('div', {
|
||||
'style': 'background: #222; border-radius: 8px; padding: 20px; margin: 10px; ' +
|
||||
'min-width: 200px; flex: 1; text-align: center; ' +
|
||||
'border-left: 4px solid ' + statusColor + ';'
|
||||
}, [
|
||||
E('div', { 'style': 'font-size: 32px; margin-bottom: 10px;' }, icon),
|
||||
E('div', { 'style': 'font-size: 16px; font-weight: bold; margin-bottom: 5px;' }, name),
|
||||
E('div', { 'style': 'font-size: 12px; color: ' + statusColor + '; margin-bottom: 10px;' },
|
||||
statusText),
|
||||
E('div', {}, statsHtml)
|
||||
]);
|
||||
return callGetStatus().catch(function() {
|
||||
return { success: false };
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
|
||||
// Inject KISS CSS
|
||||
this.injectCSS();
|
||||
|
||||
if (!data || !data.success) {
|
||||
return E('div', { 'class': 'alert-message warning' },
|
||||
'Failed to load InterceptoR status');
|
||||
return E('div', { 'class': 'kiss-root' }, [
|
||||
E('div', { 'class': 'kiss-card kiss-panel-red' }, [
|
||||
E('div', { 'class': 'kiss-card-title' }, '⚠️ InterceptoR Status Unavailable'),
|
||||
E('p', { 'style': 'color: var(--kiss-muted);' }, 'Failed to load status. Check if RPCD service is running.')
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
var pillars = [
|
||||
{ id: 'wpad', name: PILLAR_NAMES.wpad, icon: PILLAR_ICONS.wpad },
|
||||
{ id: 'mitm', name: PILLAR_NAMES.mitm, icon: PILLAR_ICONS.mitm },
|
||||
{ id: 'cdn_cache', name: PILLAR_NAMES.cdn_cache, icon: PILLAR_ICONS.cdn_cache },
|
||||
{ id: 'cookie_tracker', name: PILLAR_NAMES.cookie_tracker, icon: PILLAR_ICONS.cookie_tracker },
|
||||
{ id: 'api_failover', name: PILLAR_NAMES.api_failover, icon: PILLAR_ICONS.api_failover }
|
||||
];
|
||||
var summary = data.summary || {};
|
||||
var score = summary.health_score || 0;
|
||||
var pillarsActive = summary.pillars_active || 0;
|
||||
|
||||
var cards = pillars.map(function(p) {
|
||||
return this.renderPillarCard(p.id, data, p.name, p.icon);
|
||||
}, this);
|
||||
return E('div', { 'class': 'kiss-root' }, [
|
||||
// Header
|
||||
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
||||
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0 0 8px 0;' }, '🧙 InterceptoR'),
|
||||
E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' }, 'The Gandalf Proxy — Transparent traffic interception')
|
||||
]),
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', { 'style': 'margin-bottom: 5px;' }, 'SecuBox InterceptoR'),
|
||||
E('p', { 'style': 'color: #888; margin-bottom: 20px;' },
|
||||
'The Gandalf Proxy - Transparent traffic interception and protection'),
|
||||
|
||||
// Health Score
|
||||
this.renderHealthScore(data),
|
||||
// Health Score Card
|
||||
E('div', { 'class': 'kiss-card', 'style': 'text-align: center; padding: 30px; margin-bottom: 20px;' }, [
|
||||
E('div', { 'style': 'font-size: 56px; font-weight: 900; color: ' + this.scoreColor(score) + ';' }, score + '%'),
|
||||
E('div', { 'style': 'font-size: 14px; color: var(--kiss-muted); margin-top: 8px;' }, 'Health Score'),
|
||||
E('div', { 'style': 'font-size: 12px; color: var(--kiss-cyan); margin-top: 4px;' },
|
||||
pillarsActive + ' of 5 pillars active')
|
||||
]),
|
||||
|
||||
// Pillars Grid
|
||||
E('h3', { 'style': 'margin-top: 30px;' }, 'Interception Pillars'),
|
||||
E('div', {
|
||||
'style': 'display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 15px;'
|
||||
}, cards),
|
||||
E('div', { 'class': 'kiss-grid kiss-grid-auto', 'style': 'margin-bottom: 24px;' },
|
||||
PILLARS.map(function(p) {
|
||||
return self.renderPillar(p, data[p.id] || {});
|
||||
})
|
||||
),
|
||||
|
||||
// Quick Links
|
||||
E('h3', { 'style': 'margin-top: 30px;' }, 'Quick Links'),
|
||||
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px;' }, [
|
||||
E('a', {
|
||||
'href': '/cgi-bin/luci/admin/secubox/network-tweaks',
|
||||
'class': 'cbi-button',
|
||||
'style': 'text-decoration: none;'
|
||||
}, 'Network Tweaks (WPAD)'),
|
||||
E('a', {
|
||||
'href': '/cgi-bin/luci/admin/secubox/mitmproxy/overview',
|
||||
'class': 'cbi-button',
|
||||
'style': 'text-decoration: none;'
|
||||
}, 'mitmproxy'),
|
||||
E('a', {
|
||||
'href': '/cgi-bin/luci/admin/secubox/cdn-cache/overview',
|
||||
'class': 'cbi-button',
|
||||
'style': 'text-decoration: none;'
|
||||
}, 'CDN Cache'),
|
||||
E('a', {
|
||||
'href': '/cgi-bin/luci/admin/secubox/crowdsec/overview',
|
||||
'class': 'cbi-button',
|
||||
'style': 'text-decoration: none;'
|
||||
}, 'CrowdSec')
|
||||
E('div', { 'class': 'kiss-card' }, [
|
||||
E('div', { 'class': 'kiss-card-title' }, '🔗 Quick Links'),
|
||||
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px;' },
|
||||
QUICK_LINKS.map(function(link) {
|
||||
return E('a', {
|
||||
'href': '/cgi-bin/luci/' + link.path,
|
||||
'class': 'kiss-btn',
|
||||
'style': 'text-decoration: none;'
|
||||
}, link.icon + ' ' + link.name);
|
||||
})
|
||||
)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderPillar: function(pillar, data) {
|
||||
var enabled = data.enabled || false;
|
||||
var running = data.running !== undefined ? data.running : enabled;
|
||||
var statusColor = running ? 'var(--kiss-green)' : 'var(--kiss-red)';
|
||||
var statusText = running ? 'Active' : 'Inactive';
|
||||
|
||||
var stats = [];
|
||||
switch(pillar.id) {
|
||||
case 'mitm':
|
||||
stats.push('Threats: ' + (data.threats_today || 0));
|
||||
stats.push('Connections: ' + (data.active_connections || 0));
|
||||
break;
|
||||
case 'cdn_cache':
|
||||
stats.push('Hit Ratio: ' + (data.hit_ratio || 0) + '%');
|
||||
if (data.offline_mode) stats.push('⚠️ OFFLINE');
|
||||
break;
|
||||
case 'cookie_tracker':
|
||||
stats.push('Cookies: ' + (data.total_cookies || 0));
|
||||
stats.push('Trackers: ' + (data.trackers_detected || 0));
|
||||
break;
|
||||
case 'wpad':
|
||||
if (data.dhcp_configured) stats.push('DHCP: ✓');
|
||||
if (data.enforce_enabled) stats.push('Enforce: ✓');
|
||||
break;
|
||||
case 'api_failover':
|
||||
stats.push('Stale serves: ' + (data.stale_serves || 0));
|
||||
break;
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'kiss-card', 'style': 'text-align: center; border-left: 3px solid ' + statusColor + ';' }, [
|
||||
E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, pillar.icon),
|
||||
E('div', { 'style': 'font-weight: 700; font-size: 14px;' }, pillar.name),
|
||||
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-bottom: 8px;' }, pillar.desc),
|
||||
E('div', { 'style': 'font-size: 11px; color: ' + statusColor + '; font-weight: 600;' }, statusText),
|
||||
stats.length ? E('div', { 'style': 'font-size: 10px; color: var(--kiss-muted); margin-top: 8px;' },
|
||||
stats.join(' • ')) : null
|
||||
]);
|
||||
},
|
||||
|
||||
scoreColor: function(score) {
|
||||
if (score >= 80) return 'var(--kiss-green)';
|
||||
if (score >= 50) return 'var(--kiss-yellow)';
|
||||
return 'var(--kiss-red)';
|
||||
},
|
||||
|
||||
kissMode: true,
|
||||
|
||||
injectCSS: function() {
|
||||
var self = this;
|
||||
if (document.querySelector('#kiss-interceptor-css')) return;
|
||||
|
||||
var css = `
|
||||
:root {
|
||||
--kiss-bg: #0a0e17; --kiss-bg2: #111827; --kiss-card: #161e2e;
|
||||
--kiss-line: #1e293b; --kiss-text: #e2e8f0; --kiss-muted: #94a3b8;
|
||||
--kiss-green: #00C853; --kiss-red: #FF1744; --kiss-blue: #2979FF;
|
||||
--kiss-cyan: #22d3ee; --kiss-yellow: #fbbf24;
|
||||
}
|
||||
.kiss-root {
|
||||
background: var(--kiss-bg); color: var(--kiss-text);
|
||||
font-family: 'Segoe UI', sans-serif; min-height: 100vh; padding: 20px;
|
||||
}
|
||||
.kiss-card {
|
||||
background: var(--kiss-card); border: 1px solid var(--kiss-line);
|
||||
border-radius: 12px; padding: 20px; margin-bottom: 16px;
|
||||
}
|
||||
.kiss-card-title { font-weight: 700; font-size: 16px; margin-bottom: 12px; }
|
||||
.kiss-grid { display: grid; gap: 16px; }
|
||||
.kiss-grid-auto { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
|
||||
.kiss-btn {
|
||||
padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 600;
|
||||
cursor: pointer; border: 1px solid var(--kiss-line); background: var(--kiss-bg2);
|
||||
color: var(--kiss-text); transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.kiss-btn:hover { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.05); }
|
||||
.kiss-panel-red { border-left: 3px solid var(--kiss-red); }
|
||||
#kiss-toggle {
|
||||
position: fixed; top: 10px; right: 10px; z-index: 99999;
|
||||
font-size: 32px; cursor: pointer; opacity: 0.7; transition: all 0.3s;
|
||||
background: rgba(0,0,0,0.5); border-radius: 50%; width: 50px; height: 50px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
#kiss-toggle:hover { opacity: 1; transform: scale(1.1); }
|
||||
@media (max-width: 600px) { .kiss-grid-auto { grid-template-columns: 1fr 1fr; } }
|
||||
`;
|
||||
var style = document.createElement('style');
|
||||
style.id = 'kiss-interceptor-css';
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Add toggle button
|
||||
if (!document.querySelector('#kiss-toggle')) {
|
||||
var toggle = document.createElement('div');
|
||||
toggle.id = 'kiss-toggle';
|
||||
toggle.innerHTML = '👁️';
|
||||
toggle.title = 'Toggle KISS/LuCI mode';
|
||||
toggle.onclick = function() { self.toggleMode(); };
|
||||
document.body.appendChild(toggle);
|
||||
}
|
||||
|
||||
// Hide LuCI chrome
|
||||
this.hideChrome();
|
||||
},
|
||||
|
||||
hideChrome: function() {
|
||||
document.body.style.background = 'var(--kiss-bg)';
|
||||
['#mainmenu', '.main-left', 'header', 'footer', '#topmenu', '#tabmenu'].forEach(function(sel) {
|
||||
var el = document.querySelector(sel);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
var main = document.querySelector('.main-right') || document.querySelector('#maincontent');
|
||||
if (main) { main.style.marginLeft = '0'; main.style.padding = '0'; main.style.maxWidth = 'none'; }
|
||||
},
|
||||
|
||||
showChrome: function() {
|
||||
document.body.style.background = '';
|
||||
['#mainmenu', '.main-left', 'header', 'footer', '#topmenu', '#tabmenu'].forEach(function(sel) {
|
||||
var el = document.querySelector(sel);
|
||||
if (el) el.style.display = '';
|
||||
});
|
||||
var main = document.querySelector('.main-right') || document.querySelector('#maincontent');
|
||||
if (main) { main.style.marginLeft = ''; main.style.padding = ''; main.style.maxWidth = ''; }
|
||||
},
|
||||
|
||||
toggleMode: function() {
|
||||
this.kissMode = !this.kissMode;
|
||||
var toggle = document.getElementById('kiss-toggle');
|
||||
if (this.kissMode) {
|
||||
toggle.innerHTML = '👁️';
|
||||
this.hideChrome();
|
||||
} else {
|
||||
toggle.innerHTML = '👁️🗨️';
|
||||
this.showChrome();
|
||||
}
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
|
||||
@ -0,0 +1,289 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* SecuBox KISS Theme - Shared styling for all LuCI dashboards
|
||||
*
|
||||
* Usage in any LuCI view:
|
||||
* 'require secubox/kiss-theme';
|
||||
* return view.extend({
|
||||
* render: function() {
|
||||
* KissTheme.apply(); // Injects CSS and hides LuCI chrome
|
||||
* return E('div', { 'class': 'kiss-root' }, [...]);
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
window.KissTheme = window.KissTheme || {
|
||||
// Core palette
|
||||
colors: {
|
||||
bg: '#0a0e17',
|
||||
bg2: '#111827',
|
||||
card: '#161e2e',
|
||||
cardHover: '#1c2640',
|
||||
line: '#1e293b',
|
||||
text: '#e2e8f0',
|
||||
muted: '#94a3b8',
|
||||
green: '#00C853',
|
||||
greenGlow: '#00E676',
|
||||
red: '#FF1744',
|
||||
blue: '#2979FF',
|
||||
blueGlow: '#448AFF',
|
||||
cyan: '#22d3ee',
|
||||
purple: '#a78bfa',
|
||||
orange: '#fb923c',
|
||||
pink: '#f472b6',
|
||||
yellow: '#fbbf24'
|
||||
},
|
||||
|
||||
// CSS generation
|
||||
generateCSS: function() {
|
||||
var c = this.colors;
|
||||
return `
|
||||
/* SecuBox KISS Theme */
|
||||
:root {
|
||||
--kiss-bg: ${c.bg}; --kiss-bg2: ${c.bg2}; --kiss-card: ${c.card};
|
||||
--kiss-line: ${c.line}; --kiss-text: ${c.text}; --kiss-muted: ${c.muted};
|
||||
--kiss-green: ${c.green}; --kiss-red: ${c.red}; --kiss-blue: ${c.blue};
|
||||
--kiss-cyan: ${c.cyan}; --kiss-purple: ${c.purple}; --kiss-orange: ${c.orange};
|
||||
--kiss-yellow: ${c.yellow};
|
||||
}
|
||||
.kiss-root {
|
||||
background: var(--kiss-bg); color: var(--kiss-text);
|
||||
font-family: 'Outfit', 'Segoe UI', sans-serif;
|
||||
min-height: 100vh; padding: 20px;
|
||||
}
|
||||
.kiss-root * { box-sizing: border-box; }
|
||||
/* Cards */
|
||||
.kiss-card {
|
||||
background: var(--kiss-card); border: 1px solid var(--kiss-line);
|
||||
border-radius: 12px; padding: 20px; margin-bottom: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.kiss-card:hover { border-color: rgba(0,200,83,0.2); }
|
||||
.kiss-card-title {
|
||||
font-weight: 700; font-size: 16px; margin-bottom: 12px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
/* Grid */
|
||||
.kiss-grid { display: grid; gap: 16px; }
|
||||
.kiss-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.kiss-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.kiss-grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
.kiss-grid-auto { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||
/* Stats */
|
||||
.kiss-stat {
|
||||
background: var(--kiss-card); border: 1px solid var(--kiss-line);
|
||||
border-radius: 10px; padding: 16px; text-align: center;
|
||||
}
|
||||
.kiss-stat-value {
|
||||
font-family: 'Orbitron', monospace; font-weight: 700;
|
||||
font-size: 28px; color: var(--kiss-green);
|
||||
}
|
||||
.kiss-stat-label {
|
||||
font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;
|
||||
letter-spacing: 1px; margin-top: 4px;
|
||||
}
|
||||
/* Buttons */
|
||||
.kiss-btn {
|
||||
padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 600;
|
||||
cursor: pointer; border: 1px solid var(--kiss-line); background: var(--kiss-bg2);
|
||||
color: var(--kiss-text); transition: all 0.2s; display: inline-flex;
|
||||
align-items: center; gap: 6px; text-decoration: none;
|
||||
}
|
||||
.kiss-btn:hover { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.05); }
|
||||
.kiss-btn-green { border-color: var(--kiss-green); color: var(--kiss-green); }
|
||||
.kiss-btn-red { border-color: var(--kiss-red); color: var(--kiss-red); }
|
||||
.kiss-btn-blue { border-color: var(--kiss-blue); color: var(--kiss-blue); }
|
||||
/* Status badges */
|
||||
.kiss-badge {
|
||||
font-family: monospace; font-size: 10px; letter-spacing: 1px;
|
||||
padding: 4px 10px; border-radius: 4px; display: inline-block;
|
||||
}
|
||||
.kiss-badge-green { color: var(--kiss-green); background: rgba(0,200,83,0.1); border: 1px solid rgba(0,200,83,0.25); }
|
||||
.kiss-badge-red { color: var(--kiss-red); background: rgba(255,23,68,0.1); border: 1px solid rgba(255,23,68,0.25); }
|
||||
.kiss-badge-blue { color: var(--kiss-blue); background: rgba(41,121,255,0.1); border: 1px solid rgba(41,121,255,0.25); }
|
||||
.kiss-badge-yellow { color: var(--kiss-yellow); background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.25); }
|
||||
/* Tables */
|
||||
.kiss-table { width: 100%; border-collapse: separate; border-spacing: 0; }
|
||||
.kiss-table th {
|
||||
text-align: left; padding: 10px 16px; font-size: 11px; letter-spacing: 1px;
|
||||
text-transform: uppercase; color: var(--kiss-muted); font-family: monospace;
|
||||
border-bottom: 1px solid var(--kiss-line);
|
||||
}
|
||||
.kiss-table td { padding: 10px 16px; font-size: 14px; border-bottom: 1px solid rgba(255,255,255,0.03); }
|
||||
.kiss-table tr:hover td { background: rgba(255,255,255,0.02); }
|
||||
/* Progress bars */
|
||||
.kiss-progress { height: 8px; background: rgba(255,255,255,0.06); border-radius: 4px; overflow: hidden; }
|
||||
.kiss-progress-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--kiss-green), var(--kiss-cyan)); }
|
||||
/* Panels with border accent */
|
||||
.kiss-panel-green { border-left: 3px solid var(--kiss-green); }
|
||||
.kiss-panel-red { border-left: 3px solid var(--kiss-red); }
|
||||
.kiss-panel-blue { border-left: 3px solid var(--kiss-blue); }
|
||||
.kiss-panel-orange { border-left: 3px solid var(--kiss-orange); }
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.kiss-grid-2, .kiss-grid-3, .kiss-grid-4 { grid-template-columns: 1fr; }
|
||||
}
|
||||
`;
|
||||
},
|
||||
|
||||
// State
|
||||
isKissMode: true,
|
||||
|
||||
// Apply theme to page
|
||||
apply: function(options) {
|
||||
options = options || {};
|
||||
|
||||
// Inject CSS if not already done
|
||||
if (!document.querySelector('#kiss-theme-css')) {
|
||||
var style = document.createElement('style');
|
||||
style.id = 'kiss-theme-css';
|
||||
style.textContent = this.generateCSS();
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Add toggle button
|
||||
this.addToggle();
|
||||
|
||||
// Hide LuCI chrome if requested (default: true)
|
||||
if (options.hideChrome !== false) {
|
||||
this.hideChrome();
|
||||
}
|
||||
},
|
||||
|
||||
// Add eye toggle button
|
||||
addToggle: function() {
|
||||
var self = this;
|
||||
if (document.querySelector('#kiss-toggle')) return;
|
||||
|
||||
var toggle = document.createElement('div');
|
||||
toggle.id = 'kiss-toggle';
|
||||
toggle.innerHTML = '👁️';
|
||||
toggle.title = 'Toggle KISS/LuCI mode';
|
||||
toggle.style.cssText = 'position:fixed;top:10px;right:10px;z-index:99999;' +
|
||||
'font-size:32px;cursor:pointer;opacity:0.7;transition:all 0.3s;' +
|
||||
'background:rgba(0,0,0,0.5);border-radius:50%;width:50px;height:50px;' +
|
||||
'display:flex;align-items:center;justify-content:center;';
|
||||
|
||||
toggle.onmouseover = function() { this.style.opacity = '1'; this.style.transform = 'scale(1.1)'; };
|
||||
toggle.onmouseout = function() { this.style.opacity = '0.7'; this.style.transform = 'scale(1)'; };
|
||||
toggle.onclick = function() { self.toggleMode(); };
|
||||
|
||||
document.body.appendChild(toggle);
|
||||
},
|
||||
|
||||
// Toggle between KISS and LuCI mode
|
||||
toggleMode: function() {
|
||||
this.isKissMode = !this.isKissMode;
|
||||
var toggle = document.getElementById('kiss-toggle');
|
||||
|
||||
if (this.isKissMode) {
|
||||
// KISS mode - hide chrome
|
||||
toggle.innerHTML = '👁️';
|
||||
this.hideChrome();
|
||||
document.body.classList.add('kiss-mode');
|
||||
} else {
|
||||
// LuCI mode - show chrome
|
||||
toggle.innerHTML = '👁️🗨️';
|
||||
this.showChrome();
|
||||
document.body.classList.remove('kiss-mode');
|
||||
}
|
||||
},
|
||||
|
||||
// Show LuCI chrome
|
||||
showChrome: function() {
|
||||
[
|
||||
'#mainmenu', '.main-left', 'header.main-header', 'header',
|
||||
'nav[role="navigation"]', 'aside', 'footer', '.container > header',
|
||||
'.pull-right', '#indicators', '.brand', '#topmenu', '#tabmenu'
|
||||
].forEach(function(sel) {
|
||||
var el = document.querySelector(sel);
|
||||
if (el) el.style.display = '';
|
||||
});
|
||||
var main = document.querySelector('.main-right') || document.querySelector('#maincontent') || document.querySelector('.container');
|
||||
if (main) {
|
||||
main.style.marginLeft = '';
|
||||
main.style.marginTop = '';
|
||||
main.style.width = '';
|
||||
main.style.padding = '';
|
||||
main.style.maxWidth = '';
|
||||
}
|
||||
document.body.style.padding = '';
|
||||
document.body.style.margin = '';
|
||||
},
|
||||
|
||||
// Hide LuCI navigation chrome
|
||||
hideChrome: function() {
|
||||
document.body.classList.add('kiss-mode');
|
||||
[
|
||||
'#mainmenu', '.main-left', 'header.main-header', 'header',
|
||||
'nav[role="navigation"]', 'aside', 'footer', '.container > header',
|
||||
'.pull-right', '#indicators', '.brand', '#topmenu', '#tabmenu'
|
||||
].forEach(function(sel) {
|
||||
var el = document.querySelector(sel);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
var main = document.querySelector('.main-right') || document.querySelector('#maincontent') || document.querySelector('.container');
|
||||
if (main) {
|
||||
main.style.marginLeft = '0';
|
||||
main.style.marginTop = '0';
|
||||
main.style.width = '100%';
|
||||
main.style.padding = '0';
|
||||
main.style.maxWidth = 'none';
|
||||
}
|
||||
document.body.style.padding = '0';
|
||||
document.body.style.margin = '0';
|
||||
},
|
||||
|
||||
// Helper: Create element with KISS classes
|
||||
E: function(tag, attrs, children) {
|
||||
var el = document.createElement(tag);
|
||||
if (attrs) {
|
||||
for (var k in attrs) {
|
||||
if (k === 'class') {
|
||||
el.className = attrs[k];
|
||||
} else if (k.startsWith('on') && typeof attrs[k] === 'function') {
|
||||
el.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
|
||||
} else {
|
||||
el.setAttribute(k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (children) {
|
||||
(Array.isArray(children) ? children : [children]).forEach(function(c) {
|
||||
if (c == null) return;
|
||||
el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
||||
});
|
||||
}
|
||||
return el;
|
||||
},
|
||||
|
||||
// Component helpers
|
||||
card: function(title, content) {
|
||||
return this.E('div', { 'class': 'kiss-card' }, [
|
||||
title ? this.E('div', { 'class': 'kiss-card-title' }, title) : null,
|
||||
this.E('div', {}, content)
|
||||
]);
|
||||
},
|
||||
|
||||
stat: function(value, label, color) {
|
||||
var style = color ? 'color:' + color : '';
|
||||
return this.E('div', { 'class': 'kiss-stat' }, [
|
||||
this.E('div', { 'class': 'kiss-stat-value', 'style': style }, String(value)),
|
||||
this.E('div', { 'class': 'kiss-stat-label' }, label)
|
||||
]);
|
||||
},
|
||||
|
||||
badge: function(text, type) {
|
||||
return this.E('span', { 'class': 'kiss-badge kiss-badge-' + (type || 'green') }, text);
|
||||
},
|
||||
|
||||
btn: function(label, onClick, type) {
|
||||
return this.E('button', {
|
||||
'class': 'kiss-btn' + (type ? ' kiss-btn-' + type : ''),
|
||||
'onClick': onClick
|
||||
}, label);
|
||||
}
|
||||
};
|
||||
|
||||
return window.KissTheme;
|
||||
Loading…
Reference in New Issue
Block a user