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:
CyberMind-FR 2026-02-11 12:00:00 +01:00
parent 1ab19cb778
commit 03e90bb4af
3 changed files with 643 additions and 149 deletions

View File

@ -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"
;;

View File

@ -9,174 +9,221 @@ var callGetStatus = rpc.declare({
expect: {}
});
var PILLAR_ICONS = {
wpad: '&#x1F310;', // Globe for WPAD
mitm: '&#x1F6E1;', // Shield for mitmproxy
cdn_cache: '&#x1F4BE;', // Disk for CDN Cache
cookie_tracker: '&#x1F36A;', // Cookie for Cookie Tracker
api_failover: '&#x26A1;' // 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

View File

@ -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;