feat(luci-theme-secubox): Add CRT P31 phosphor green terminal theme

Complete LuCI theme implementation with:
- CRT P31 phosphor green color scheme with scanline effects
- Header, footer, and sysauth login page templates
- Cascading CSS with CRT glow and phosphor effects
- CRT engine JS for terminal-style animations
- CRT components JS for reusable UI components
- UCI defaults to set as default LuCI theme
- Updated Makefile for proper asset installation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-26 07:18:23 +01:00
parent 54668158c8
commit 1140221f4a
10 changed files with 2423 additions and 46 deletions

View File

@ -1,16 +1,45 @@
# SPDX-License-Identifier: MIT
# SecuBox CRT P31 Phosphor Theme for OpenWrt LuCI
# CyberMind — SecuBox — 2026
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-secubox
PKG_VERSION:=0.4.8
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_VERSION:=1.0.0
PKG_RELEASE:=2
PKG_MAINTAINER:=Gerald KERMA <devel@cybermind.fr>
PKG_LICENSE:=MIT
LUCI_TITLE:=LuCI - SecuBox CyberMood Theme
LUCI_DESCRIPTION:=Global CyberMood design system (CSS/JS/i18n) shared by all SecuBox dashboards.
LUCI_TITLE:=SecuBox CRT P31 Phosphor Theme
LUCI_DEPENDS:=+luci-base
LUCI_PKGARCH:=all
LUCI_DESCRIPTION:=CRT P31 phosphor green terminal theme for SecuBox mesh network appliance. Features scanlines, phosphor glow effects, multiple theme variants including CRT-P31, dark, cyberpunk, and more.
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot
define Package/luci-theme-secubox/install
# Main theme directory (CRT P31 base)
$(INSTALL_DIR) $(1)/www/luci-static/secubox
$(CP) ./htdocs/luci-static/secubox/* $(1)/www/luci-static/secubox/
# Theme system resources
$(INSTALL_DIR) $(1)/www/luci-static/resources/secubox-theme
$(CP) ./htdocs/luci-static/resources/secubox-theme/* $(1)/www/luci-static/resources/secubox-theme/
# KISS theme helper for individual modules
$(INSTALL_DIR) $(1)/www/luci-static/resources/secubox
$(CP) ./htdocs/luci-static/resources/secubox/* $(1)/www/luci-static/resources/secubox/
# Lua view templates
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/themes/secubox
$(CP) ./luasrc/luci/view/themes/secubox/* $(1)/usr/lib/lua/luci/view/themes/secubox/
# UCI defaults
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./root/etc/uci-defaults/90-luci-theme-secubox $(1)/etc/uci-defaults/
endef
define Package/luci-theme-secubox/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
uci set luci.main.mediaurlbase='/luci-static/secubox'
uci commit luci
}
endef
$(eval $(call BuildPackage,luci-theme-secubox))

View File

@ -1,47 +1,123 @@
# luci-theme-secubox
# SecuBox CRT P31 Phosphor Theme
SecuBox's CyberMood design system packaged as a LuCI theme. Ships shared CSS variables, reusable components, responsive layouts, translations, and the browser-side theme controller used by every SecuBox module (`'require secubox-theme/theme as Theme'`). Install alongside SecuBox modules to ensure consistent styling, language switching, and light/dark/cyberpunk variants.
A retro-futuristic CRT terminal theme for OpenWrt LuCI, designed for the SecuBox mesh security appliance.
```
./feeds/luci/luci-theme-secubox/
├── Makefile
└── htdocs/luci-static/resources/secubox-theme/
├── core/ # Variables, reset, typography, animations, utilities
├── components/ # Buttons, cards, forms, tables, badges, alerts, etc.
├── layouts/ # Dashboard/grid/responsive helpers
├── themes/ # Dark (default), light, cyberpunk variants
├── i18n/ # en/fr/de/es JSON dictionaries
├── secubox-theme.css
├── secubox-theme.min.css
└── theme.js # Theme controller (init/apply/t/Theme.create*)
## Features
- **P31 Phosphor Green** color palette - authentic CRT terminal aesthetic
- **Scanlines overlay** - subtle CRT screen effect
- **Phosphor glow effects** - text and button bloom on hover/focus
- **Boot sequence animation** - terminal-style startup on first visit
- **Responsive design** - works on desktop and mobile
- **Full LuCI coverage** - all pages, forms, tables, and widgets styled
## Color Palette
| Variable | Color | Usage |
|----------|-------|-------|
| `--p31-peak` | `#33ff66` | Maximum brightness - headers, active elements |
| `--p31-hot` | `#66ffaa` | High brightness - hover states |
| `--p31-mid` | `#22cc44` | Medium brightness - body text |
| `--p31-dim` | `#0f8822` | Low brightness - secondary text, borders |
| `--p31-ghost` | `#052210` | Ghosting/afterglow - subtle backgrounds |
| `--p31-decay` | `#ffb347` | Phosphor decay amber - warnings |
| `--tube-black` | `#050803` | Deep CRT black - page background |
| `--tube-deep` | `#080d05` | Card/panel backgrounds |
## Installation
```bash
opkg update
opkg install luci-theme-secubox
```
## Usage
The theme will automatically be set as the default. To manually switch:
```bash
uci set luci.main.mediaurlbase='/luci-static/secubox'
uci commit luci
```
## JavaScript Components
### CRT Engine (`crt-engine.js`)
Provides CRT visual effects:
```javascript
'use strict';
'require secubox-theme/theme as Theme';
return view.extend({
load: function() {
return Theme.init();
},
render: function() {
Theme.apply('dark'); // dark, light, cyberpunk
Theme.setLanguage('en'); // en, fr, de, es
return Theme.createPage({
title: Theme.t('dashboard.title'),
cards: [
Theme.createCard({
title: Theme.t('dashboard.overview'),
icon: '🚀',
content: this.renderOverview()
})
]
});
}
// Configure CRT effects
SecuBoxCRT.configure({
enableScanlines: true, // Scanlines overlay
enableFlicker: false, // Screen flicker (disabled by default)
enableGlow: true, // Phosphor glow on hover
enableBootSequence: true // Terminal boot animation
});
// Trigger boot sequence manually
SecuBoxCRT.bootSequence();
// Typewriter effect on element
SecuBoxCRT.typewriterEffect(element, 50);
```
See `DOCS/GLOBAL_THEME_SYSTEM.md` for the full design reference. This package only contains the shared assets; each module is still responsible for importing `secubox-theme.css` (or `.min.css`) and using the exported helper methods.
### CRT Components (`crt-components.js`)
Reusable UI components:
```javascript
// Create a status widget
var widget = CRTComponents.createWidget({
title: 'CPU Load',
value: '45',
unit: '%',
status: 'normal' // or 'warning', 'danger'
});
// Create a status badge
var badge = CRTComponents.createBadge('Online', 'success');
// Create a progress bar
var progress = CRTComponents.createProgressBar(75, 100, '75%');
// Show toast notification
CRTComponents.toast('Settings saved', 'success', 3000);
```
## File Structure
```
luci-theme-secubox/
├── Makefile # OpenWrt package makefile
├── htdocs/luci-static/secubox/
│ ├── cascade.css # Main theme stylesheet
│ ├── crt-engine.js # CRT visual effects engine
│ └── crt-components.js # Reusable UI components
├── luasrc/luci/view/themes/secubox/
│ ├── header.htm # Page header template
│ ├── footer.htm # Page footer template
│ └── sysauth.htm # Login page template
└── root/etc/uci-defaults/
└── 90-luci-theme-secubox # Default theme configuration
```
## CSS Customization
Override CSS variables in your module's stylesheet:
```css
/* Custom module styling */
.my-module {
background: var(--tube-deep);
border: 1px solid var(--p31-ghost);
color: var(--p31-mid);
}
.my-module .title {
color: var(--p31-peak);
text-shadow: var(--bloom-text);
}
```
## License
MIT License - CyberMind 2026

View File

@ -0,0 +1,411 @@
/**
* SecuBox CRT P31 Phosphor Green Theme
* File: themes/crt-p31.css
* Version: 1.0.0
* SPDX-License-Identifier: MIT
*
* Authentic CRT terminal aesthetic with P31 phosphor green
* Features scanlines, phosphor glow, and retro-futuristic styling
*/
body[data-secubox-theme="crt-p31"] {
/* P31 Phosphor Green Scale */
--cyber-p31-peak: #33ff66; /* Maximum brightness */
--cyber-p31-hot: #66ffaa; /* High brightness */
--cyber-p31-mid: #22cc44; /* Medium brightness */
--cyber-p31-dim: #0f8822; /* Low brightness */
--cyber-p31-ghost: #052210; /* Ghosting/afterglow */
/* Phosphor Decay (amber for warnings) */
--cyber-p31-decay: #ffb347;
--cyber-p31-decay-dim: #cc7722;
/* CRT Tube Colors */
--cyber-tube-black: #050803;
--cyber-tube-deep: #080d05;
--cyber-tube-bezel: #0d1208;
/* Map to cyber design system */
--cyber-bg-primary: var(--cyber-tube-black);
--cyber-bg-secondary: var(--cyber-tube-deep);
--cyber-bg-tertiary: var(--cyber-tube-bezel);
--cyber-surface: #0a1207;
--cyber-surface-light: #0f1a0c;
--cyber-overlay: rgba(5, 8, 3, 0.9);
/* Text colors */
--cyber-text-primary: var(--cyber-p31-mid);
--cyber-text-secondary: var(--cyber-p31-dim);
--cyber-text-muted: var(--cyber-p31-ghost);
--cyber-text-inverse: var(--cyber-tube-black);
/* Accent - all green */
--cyber-accent-primary: var(--cyber-p31-mid);
--cyber-accent-primary-end: var(--cyber-p31-peak);
--cyber-accent-secondary: var(--cyber-p31-hot);
--cyber-accent-tertiary: var(--cyber-p31-dim);
/* Semantic colors */
--cyber-success: var(--cyber-p31-peak);
--cyber-success-soft: rgba(51, 255, 102, 0.1);
--cyber-warning: var(--cyber-p31-decay);
--cyber-warning-soft: rgba(255, 179, 71, 0.1);
--cyber-danger: #ff6688;
--cyber-danger-soft: rgba(255, 68, 102, 0.1);
--cyber-info: var(--cyber-p31-mid);
--cyber-info-soft: rgba(51, 255, 102, 0.05);
/* Fonts - monospace only */
--cyber-font-display: 'Courier Prime', 'IBM Plex Mono', 'Fira Code', 'Courier New', monospace;
--cyber-font-body: 'Courier Prime', 'IBM Plex Mono', 'Fira Code', 'Courier New', monospace;
--cyber-font-mono: 'Courier Prime', 'IBM Plex Mono', 'Fira Code', 'Courier New', monospace;
/* Shadows & borders - green glow */
--cyber-shadow: 0 0 20px rgba(51, 255, 102, 0.15);
--cyber-shadow-soft: 0 0 10px rgba(51, 255, 102, 0.1);
--cyber-border: 1px solid var(--cyber-p31-ghost);
--cyber-border-strong: 1px solid var(--cyber-p31-dim);
/* Phosphor glow effects */
--cyber-bloom-text: 0 0 2px var(--cyber-p31-peak), 0 0 6px var(--cyber-p31-peak), 0 0 14px rgba(51,255,102,0.5);
--cyber-bloom-soft: 0 0 6px var(--cyber-p31-peak), 0 0 14px rgba(51,255,102,0.5);
--cyber-bloom-box: 0 0 8px rgba(51,255,102,0.3), inset 0 0 4px rgba(51,255,102,0.1);
/* Glass effects - darker for CRT */
--cyber-glass-bg: rgba(5, 34, 16, 0.3);
--cyber-glass-border: var(--cyber-p31-ghost);
--cyber-glass-blur: 5px;
--cyber-glass-shadow: 0 0 20px rgba(51, 255, 102, 0.1);
/* Gradients - green phosphor */
--cyber-gradient-primary: linear-gradient(135deg, var(--cyber-p31-dim) 0%, var(--cyber-p31-mid) 100%);
--cyber-gradient-steel: linear-gradient(135deg, var(--cyber-tube-bezel) 0%, var(--cyber-tube-black) 100%);
--cyber-gradient-chrome: linear-gradient(135deg, var(--cyber-p31-dim) 0%, var(--cyber-tube-deep) 100%);
--cyber-gradient-cyber: linear-gradient(135deg, var(--cyber-p31-mid) 0%, var(--cyber-p31-peak) 100%);
/* Base styling */
background: var(--cyber-tube-black);
color: var(--cyber-p31-mid);
font-family: var(--cyber-font-body);
letter-spacing: 0.02em;
}
/* Scanlines overlay */
body[data-secubox-theme="crt-p31"]::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
z-index: 9999;
}
/* CRT screen curvature effect */
body[data-secubox-theme="crt-p31"] {
border-radius: 8px;
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.5), inset 0 0 10px rgba(51, 255, 102, 0.1);
}
/* Headers with glow */
body[data-secubox-theme="crt-p31"] h1,
body[data-secubox-theme="crt-p31"] h2,
body[data-secubox-theme="crt-p31"] h3,
body[data-secubox-theme="crt-p31"] .sh-page-title,
body[data-secubox-theme="crt-p31"] .card-title {
color: var(--cyber-p31-peak);
text-shadow: var(--cyber-bloom-soft);
text-transform: uppercase;
letter-spacing: 2px;
}
/* Links */
body[data-secubox-theme="crt-p31"] a {
color: var(--cyber-p31-mid);
text-decoration: none;
transition: all 0.2s ease;
}
body[data-secubox-theme="crt-p31"] a:hover {
color: var(--cyber-p31-peak);
text-shadow: var(--cyber-bloom-soft);
}
/* Buttons */
body[data-secubox-theme="crt-p31"] button,
body[data-secubox-theme="crt-p31"] .btn,
body[data-secubox-theme="crt-p31"] .cbi-button {
background: transparent;
border: 1px solid var(--cyber-p31-dim);
color: var(--cyber-p31-mid);
font-family: inherit;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.2s ease;
}
body[data-secubox-theme="crt-p31"] button:hover,
body[data-secubox-theme="crt-p31"] .btn:hover,
body[data-secubox-theme="crt-p31"] .cbi-button:hover {
color: var(--cyber-p31-peak);
border-color: var(--cyber-p31-mid);
text-shadow: var(--cyber-bloom-soft);
}
body[data-secubox-theme="crt-p31"] .btn-primary,
body[data-secubox-theme="crt-p31"] .cbi-button-save,
body[data-secubox-theme="crt-p31"] .cbi-button-apply {
border-color: var(--cyber-p31-mid);
color: var(--cyber-p31-peak);
}
body[data-secubox-theme="crt-p31"] .btn-primary:hover,
body[data-secubox-theme="crt-p31"] .cbi-button-save:hover,
body[data-secubox-theme="crt-p31"] .cbi-button-apply:hover {
background: var(--cyber-p31-mid);
color: var(--cyber-tube-black);
text-shadow: none;
}
body[data-secubox-theme="crt-p31"] .btn-danger,
body[data-secubox-theme="crt-p31"] .cbi-button-remove {
border-color: var(--cyber-p31-decay-dim);
color: var(--cyber-p31-decay);
}
body[data-secubox-theme="crt-p31"] .btn-danger:hover,
body[data-secubox-theme="crt-p31"] .cbi-button-remove:hover {
background: var(--cyber-p31-decay);
color: var(--cyber-tube-black);
text-shadow: none;
}
/* Form inputs */
body[data-secubox-theme="crt-p31"] input[type="text"],
body[data-secubox-theme="crt-p31"] input[type="password"],
body[data-secubox-theme="crt-p31"] input[type="number"],
body[data-secubox-theme="crt-p31"] input[type="email"],
body[data-secubox-theme="crt-p31"] textarea,
body[data-secubox-theme="crt-p31"] select {
background: var(--cyber-tube-black);
border: 1px solid var(--cyber-p31-ghost);
color: var(--cyber-p31-mid);
font-family: inherit;
}
body[data-secubox-theme="crt-p31"] input:focus,
body[data-secubox-theme="crt-p31"] select:focus,
body[data-secubox-theme="crt-p31"] textarea:focus {
outline: none;
border-color: var(--cyber-p31-mid);
box-shadow: var(--cyber-bloom-box);
}
/* Cards */
body[data-secubox-theme="crt-p31"] .card,
body[data-secubox-theme="crt-p31"] .panel,
body[data-secubox-theme="crt-p31"] .cbi-section,
body[data-secubox-theme="crt-p31"] .cbi-map {
background: var(--cyber-tube-deep);
border: 1px solid var(--cyber-p31-ghost);
}
/* Tables */
body[data-secubox-theme="crt-p31"] table th {
background: var(--cyber-tube-black);
color: var(--cyber-p31-dim);
text-transform: uppercase;
letter-spacing: 1px;
font-weight: normal;
border-bottom: 1px solid var(--cyber-p31-ghost);
}
body[data-secubox-theme="crt-p31"] table td {
border-bottom: 1px solid var(--cyber-p31-ghost);
color: var(--cyber-p31-mid);
}
body[data-secubox-theme="crt-p31"] table tr:hover td {
background: rgba(51, 255, 102, 0.03);
}
/* Status indicators */
body[data-secubox-theme="crt-p31"] .status-online,
body[data-secubox-theme="crt-p31"] .online,
body[data-secubox-theme="crt-p31"] .running {
color: var(--cyber-p31-peak);
text-shadow: 0 0 6px var(--cyber-p31-peak);
}
body[data-secubox-theme="crt-p31"] .status-offline,
body[data-secubox-theme="crt-p31"] .offline,
body[data-secubox-theme="crt-p31"] .stopped {
color: #ff6688;
text-shadow: 0 0 6px #ff4466;
}
body[data-secubox-theme="crt-p31"] .status-warning,
body[data-secubox-theme="crt-p31"] .warning {
color: var(--cyber-p31-decay);
text-shadow: 0 0 6px var(--cyber-p31-decay);
}
/* Badges */
body[data-secubox-theme="crt-p31"] .badge,
body[data-secubox-theme="crt-p31"] .label {
border-radius: 2px;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.7rem;
}
body[data-secubox-theme="crt-p31"] .badge-success {
background: rgba(51, 255, 102, 0.1);
border: 1px solid var(--cyber-p31-mid);
color: var(--cyber-p31-peak);
}
body[data-secubox-theme="crt-p31"] .badge-warning {
background: rgba(255, 179, 71, 0.1);
border: 1px solid var(--cyber-p31-decay-dim);
color: var(--cyber-p31-decay);
}
body[data-secubox-theme="crt-p31"] .badge-danger {
background: rgba(255, 68, 102, 0.1);
border: 1px solid #ff4466;
color: #ff6688;
}
/* Progress bars */
body[data-secubox-theme="crt-p31"] .progress,
body[data-secubox-theme="crt-p31"] .cbi-progressbar {
background: var(--cyber-tube-black);
border: 1px solid var(--cyber-p31-ghost);
}
body[data-secubox-theme="crt-p31"] .progress-bar,
body[data-secubox-theme="crt-p31"] .cbi-progressbar > div {
background: linear-gradient(90deg, var(--cyber-p31-dim), var(--cyber-p31-mid));
box-shadow: 0 0 10px var(--cyber-p31-mid);
}
/* Navigation */
body[data-secubox-theme="crt-p31"] nav a,
body[data-secubox-theme="crt-p31"] .main-left a {
color: var(--cyber-p31-dim);
text-transform: uppercase;
letter-spacing: 1px;
border-left: 2px solid transparent;
}
body[data-secubox-theme="crt-p31"] nav a:hover,
body[data-secubox-theme="crt-p31"] .main-left a:hover {
color: var(--cyber-p31-mid);
background: rgba(51, 255, 102, 0.05);
border-left-color: var(--cyber-p31-dim);
}
body[data-secubox-theme="crt-p31"] nav a.active,
body[data-secubox-theme="crt-p31"] .main-left .active a {
color: var(--cyber-p31-peak);
background: rgba(51, 255, 102, 0.1);
border-left-color: var(--cyber-p31-peak);
text-shadow: var(--cyber-bloom-soft);
}
/* Alerts */
body[data-secubox-theme="crt-p31"] .alert-success {
background: rgba(51, 255, 102, 0.1);
border: 1px solid var(--cyber-p31-dim);
color: var(--cyber-p31-mid);
}
body[data-secubox-theme="crt-p31"] .alert-warning {
background: rgba(255, 179, 71, 0.1);
border: 1px solid var(--cyber-p31-decay-dim);
color: var(--cyber-p31-decay);
}
body[data-secubox-theme="crt-p31"] .alert-danger,
body[data-secubox-theme="crt-p31"] .alert-error {
background: rgba(255, 68, 102, 0.1);
border: 1px solid #ff4466;
color: #ff6688;
}
/* Scrollbars */
body[data-secubox-theme="crt-p31"]::-webkit-scrollbar {
width: 8px;
height: 8px;
}
body[data-secubox-theme="crt-p31"]::-webkit-scrollbar-track {
background: var(--cyber-tube-black);
}
body[data-secubox-theme="crt-p31"]::-webkit-scrollbar-thumb {
background: var(--cyber-p31-ghost);
border-radius: 4px;
}
body[data-secubox-theme="crt-p31"]::-webkit-scrollbar-thumb:hover {
background: var(--cyber-p31-dim);
}
/* Selection */
body[data-secubox-theme="crt-p31"] ::selection {
background: var(--cyber-p31-dim);
color: var(--cyber-tube-black);
}
/* Widgets */
body[data-secubox-theme="crt-p31"] .widget-value,
body[data-secubox-theme="crt-p31"] .stat-value {
color: var(--cyber-p31-peak);
text-shadow: var(--cyber-bloom-text);
font-weight: bold;
}
body[data-secubox-theme="crt-p31"] .widget-label,
body[data-secubox-theme="crt-p31"] .stat-label {
color: var(--cyber-p31-dim);
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.7rem;
}
/* Topology visualization */
body[data-secubox-theme="crt-p31"] .topology-node circle {
fill: var(--cyber-p31-dim);
stroke: var(--cyber-p31-mid);
}
body[data-secubox-theme="crt-p31"] .topology-node.active circle {
fill: var(--cyber-p31-mid);
stroke: var(--cyber-p31-peak);
}
body[data-secubox-theme="crt-p31"] .topology-edge {
stroke: var(--cyber-p31-ghost);
}
body[data-secubox-theme="crt-p31"] .topology-edge.active {
stroke: var(--cyber-p31-dim);
}
/* Code blocks */
body[data-secubox-theme="crt-p31"] code,
body[data-secubox-theme="crt-p31"] pre {
background: var(--cyber-tube-black);
border: 1px solid var(--cyber-p31-ghost);
color: var(--cyber-p31-mid);
}

View File

@ -0,0 +1,796 @@
/* ============================================
SecuBox CRT P31 Theme for OpenWrt LuCI
CyberMind SecuBox 2026
============================================ */
/* === CSS Variables === */
:root {
/* P31 Phosphor Green Scale */
--p31-peak: #33ff66; /* Maximum brightness - headers, active elements */
--p31-hot: #66ffaa; /* High brightness - hover states */
--p31-mid: #22cc44; /* Medium brightness - body text */
--p31-dim: #0f8822; /* Low brightness - secondary text, borders */
--p31-ghost: #052210; /* Ghosting/afterglow - subtle backgrounds */
/* Phosphor Decay (amber for warnings/errors) */
--p31-decay: #ffb347; /* Warning/caution */
--p31-decay-dim: #cc7722; /* Muted warning */
/* CRT Tube Colors */
--tube-black: #050803; /* Deep CRT black */
--tube-deep: #080d05; /* Card backgrounds */
--tube-bezel: #0d1208; /* Panel borders */
/* Semantic Aliases */
--bg-dark: var(--tube-black);
--bg-card: var(--tube-deep);
--border: var(--p31-ghost);
--text: var(--p31-mid);
--text-bright: var(--p31-peak);
--text-dim: var(--p31-dim);
/* Glow Effects */
--bloom-text: 0 0 2px var(--p31-peak), 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
--bloom-soft: 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
--bloom-box: 0 0 8px rgba(51,255,102,0.3), inset 0 0 4px rgba(51,255,102,0.1);
}
/* === Reset & Base === */
* {
box-sizing: border-box;
}
html, body {
background: var(--tube-black) !important;
color: var(--p31-mid) !important;
font-family: 'Courier Prime', 'IBM Plex Mono', 'Fira Code', 'Courier New', 'Lucida Console', monospace !important;
font-size: 14px;
line-height: 1.5;
letter-spacing: 0.02em;
min-height: 100vh;
margin: 0;
padding: 0;
}
/* === Scanlines Overlay === */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
z-index: 9999;
}
/* === Text Flicker Animation === */
@keyframes textFlicker {
0%, 100% { opacity: 1; }
50% { opacity: 0.98; }
}
.crt-text {
text-shadow: var(--bloom-text);
animation: textFlicker 0.1s infinite;
}
/* === Header === */
header, .main-head, #maincontent > header, .container > header {
background: var(--tube-deep) !important;
border-bottom: 1px solid var(--p31-ghost) !important;
padding: 0.75rem 1.5rem !important;
}
header h1, .main-head h1, #maincontent > header h1 {
font-size: 1.2rem !important;
color: var(--p31-peak) !important;
text-shadow: var(--bloom-text) !important;
font-weight: bold !important;
letter-spacing: 2px !important;
margin: 0 !important;
}
header h1::before {
content: '[ ';
color: var(--p31-dim);
}
header h1::after {
content: ' ]';
color: var(--p31-dim);
}
/* === Navigation Sidebar === */
.main-left, #mainmenu, nav.main, .luci-nav {
background: var(--tube-deep) !important;
border-right: 1px solid var(--p31-ghost) !important;
}
.main-left ul, #mainmenu ul, nav.main ul {
list-style: none !important;
margin: 0 !important;
padding: 0 !important;
}
.main-left li a, #mainmenu li a, nav.main li a,
.main-left li > span, #mainmenu li > span {
display: block !important;
padding: 0.6rem 1.2rem !important;
color: var(--p31-dim) !important;
text-decoration: none !important;
font-size: 0.85rem !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
border-left: 2px solid transparent !important;
transition: all 0.2s ease !important;
background: transparent !important;
}
.main-left li a:hover, #mainmenu li a:hover, nav.main li a:hover {
color: var(--p31-mid) !important;
background: rgba(51, 255, 102, 0.05) !important;
border-left-color: var(--p31-dim) !important;
}
.main-left li.active > a, .main-left li.selected > a,
#mainmenu li.active > a, #mainmenu li.selected > a,
nav.main li.active a {
color: var(--p31-peak) !important;
background: rgba(51, 255, 102, 0.1) !important;
border-left-color: var(--p31-peak) !important;
text-shadow: var(--bloom-soft) !important;
}
/* Category Headers */
.main-left li.slide, #mainmenu li.slide {
padding: 0.8rem 1.2rem 0.4rem !important;
color: var(--p31-ghost) !important;
font-size: 0.7rem !important;
text-transform: uppercase !important;
letter-spacing: 2px !important;
border-bottom: 1px solid var(--p31-ghost) !important;
margin-top: 0.5rem !important;
}
/* === Main Content === */
.main-right, #maincontent, main, .container {
flex: 1 !important;
padding: 1.5rem !important;
background: var(--tube-black) !important;
}
/* === Cards/Panels === */
.cbi-map, .cbi-section, fieldset, .panel, .card,
div.cbi-value, .cbi-section-node {
background: var(--tube-deep) !important;
border: 1px solid var(--p31-ghost) !important;
border-radius: 4px !important;
padding: 1rem !important;
margin-bottom: 1rem !important;
}
.cbi-section-node {
background: transparent !important;
border: none !important;
padding: 0 !important;
}
/* Section Headers */
.cbi-section h2, .cbi-section h3, .cbi-section-title,
legend, .panel-title, .card-title {
color: var(--p31-peak) !important;
font-size: 0.9rem !important;
text-transform: uppercase !important;
letter-spacing: 2px !important;
padding-bottom: 0.5rem !important;
margin-bottom: 1rem !important;
border-bottom: 1px solid var(--p31-ghost) !important;
text-shadow: var(--bloom-soft) !important;
background: transparent !important;
}
/* === Tables === */
table, .table {
width: 100% !important;
border-collapse: collapse !important;
font-size: 0.85rem !important;
background: var(--tube-deep) !important;
}
th, thead td, .table-titles td {
color: var(--p31-dim) !important;
font-weight: normal !important;
text-transform: uppercase !important;
font-size: 0.75rem !important;
letter-spacing: 1px !important;
padding: 0.75rem !important;
text-align: left !important;
border-bottom: 1px solid var(--p31-ghost) !important;
background: var(--tube-black) !important;
}
td, .table td, .cbi-section-table td {
padding: 0.75rem !important;
border-bottom: 1px solid var(--p31-ghost) !important;
color: var(--p31-mid) !important;
background: transparent !important;
}
tr:hover td, .tr:hover .td {
background: rgba(51, 255, 102, 0.03) !important;
}
/* === Forms === */
input[type="text"],
input[type="password"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="tel"],
textarea,
select,
.cbi-input-text,
.cbi-input-password,
.cbi-input-select {
background: var(--tube-black) !important;
border: 1px solid var(--p31-ghost) !important;
color: var(--p31-mid) !important;
padding: 0.5rem 0.75rem !important;
font-family: inherit !important;
font-size: 0.9rem !important;
border-radius: 3px !important;
}
input:focus, select:focus, textarea:focus {
outline: none !important;
border-color: var(--p31-mid) !important;
box-shadow: var(--bloom-box) !important;
}
input::placeholder, textarea::placeholder {
color: var(--p31-ghost) !important;
}
/* Checkboxes & Radio */
input[type="checkbox"],
input[type="radio"] {
accent-color: var(--p31-peak) !important;
}
/* === Buttons === */
.cbi-button, button, input[type="submit"],
input[type="button"], .btn {
background: transparent !important;
border: 1px solid var(--p31-dim) !important;
color: var(--p31-mid) !important;
padding: 0.5rem 1rem !important;
font-family: inherit !important;
font-size: 0.8rem !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
cursor: pointer !important;
border-radius: 3px !important;
transition: all 0.2s ease !important;
text-decoration: none !important;
}
.cbi-button:hover, button:hover, .btn:hover {
color: var(--p31-peak) !important;
border-color: var(--p31-mid) !important;
text-shadow: var(--bloom-soft) !important;
background: transparent !important;
}
.cbi-button-save, .cbi-button-apply, .btn-primary,
.cbi-button-action, .cbi-button-positive {
border-color: var(--p31-mid) !important;
color: var(--p31-peak) !important;
}
.cbi-button-save:hover, .btn-primary:hover,
.cbi-button-apply:hover, .cbi-button-action:hover {
background: var(--p31-mid) !important;
color: var(--tube-black) !important;
text-shadow: none !important;
}
.cbi-button-remove, .cbi-button-reset, .btn-danger,
.cbi-button-negative {
border-color: var(--p31-decay-dim) !important;
color: var(--p31-decay) !important;
}
.cbi-button-remove:hover, .btn-danger:hover,
.cbi-button-reset:hover, .cbi-button-negative:hover {
background: var(--p31-decay) !important;
color: var(--tube-black) !important;
text-shadow: none !important;
}
/* === Status Badges === */
.badge, .label, .cbi-value-field .ifacebadge,
span.zonebadge {
display: inline-block !important;
padding: 0.15rem 0.5rem !important;
font-size: 0.7rem !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
border-radius: 2px !important;
border: 1px solid !important;
}
.badge-success, .label-success, .badge.success {
border-color: var(--p31-mid) !important;
color: var(--p31-peak) !important;
background: rgba(51, 255, 102, 0.1) !important;
}
.badge-warning, .label-warning, .badge.warning {
border-color: var(--p31-decay-dim) !important;
color: var(--p31-decay) !important;
background: rgba(255, 179, 71, 0.1) !important;
}
.badge-danger, .label-danger, .badge.danger, .badge.error {
border-color: #ff4466 !important;
color: #ff6688 !important;
background: rgba(255, 68, 102, 0.1) !important;
}
.badge-info, .label-info, .badge.info {
border-color: var(--p31-dim) !important;
color: var(--p31-mid) !important;
background: rgba(51, 255, 102, 0.05) !important;
}
/* === Alerts/Notifications === */
.alert, .notice, .cbi-section-error, .errorbox,
.cbi-section-warning, .warningbox {
padding: 1rem !important;
border-radius: 4px !important;
margin-bottom: 1rem !important;
border: 1px solid !important;
}
.alert-success, .notice.success, .successbox {
background: rgba(51, 255, 102, 0.1) !important;
border-color: var(--p31-dim) !important;
color: var(--p31-mid) !important;
}
.alert-warning, .notice.warning, .warningbox,
.cbi-section-warning {
background: rgba(255, 179, 71, 0.1) !important;
border-color: var(--p31-decay-dim) !important;
color: var(--p31-decay) !important;
}
.alert-error, .notice.error, .errorbox,
.cbi-section-error, .alert-danger {
background: rgba(255, 68, 102, 0.1) !important;
border-color: #ff4466 !important;
color: #ff6688 !important;
}
/* === Progress Bars === */
.cbi-progressbar, .progress {
background: var(--tube-black) !important;
border: 1px solid var(--p31-ghost) !important;
border-radius: 2px !important;
height: 20px !important;
overflow: hidden !important;
}
.cbi-progressbar > div, .progress-bar {
background: linear-gradient(90deg, var(--p31-dim), var(--p31-mid)) !important;
height: 100% !important;
box-shadow: 0 0 10px var(--p31-mid) !important;
transition: width 0.3s ease !important;
}
/* === Tabs === */
.cbi-tabmenu, .tabs, ul.cbi-tabmenu {
display: flex !important;
border-bottom: 1px solid var(--p31-ghost) !important;
margin-bottom: 1rem !important;
list-style: none !important;
padding: 0 !important;
background: transparent !important;
}
.cbi-tabmenu li, .tabs .tab {
list-style: none !important;
}
.cbi-tabmenu li a, .tabs .tab, .cbi-tabmenu li > a {
display: block !important;
padding: 0.6rem 1rem !important;
color: var(--p31-dim) !important;
text-decoration: none !important;
font-size: 0.8rem !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
border: 1px solid transparent !important;
border-bottom: none !important;
margin-bottom: -1px !important;
cursor: pointer !important;
background: transparent !important;
}
.cbi-tabmenu li.cbi-tab a, .tabs .tab.active,
.cbi-tabmenu li.cbi-tab > a {
color: var(--p31-peak) !important;
border-color: var(--p31-ghost) !important;
background: var(--tube-deep) !important;
text-shadow: var(--bloom-soft) !important;
}
/* === Tooltips === */
[data-tooltip] {
position: relative !important;
}
[data-tooltip]::after {
content: attr(data-tooltip) !important;
position: absolute !important;
bottom: 100% !important;
left: 50% !important;
transform: translateX(-50%) !important;
background: var(--tube-deep) !important;
border: 1px solid var(--p31-dim) !important;
color: var(--p31-mid) !important;
padding: 0.4rem 0.6rem !important;
font-size: 0.75rem !important;
white-space: nowrap !important;
border-radius: 3px !important;
opacity: 0 !important;
visibility: hidden !important;
transition: all 0.2s ease !important;
z-index: 1000 !important;
}
[data-tooltip]:hover::after {
opacity: 1 !important;
visibility: visible !important;
}
/* === Footer === */
footer, .main-foot, #mainfooter {
background: var(--tube-deep) !important;
border-top: 1px solid var(--p31-ghost) !important;
padding: 0.5rem 1.5rem !important;
font-size: 0.75rem !important;
color: var(--p31-dim) !important;
text-align: center !important;
}
/* === Links === */
a, .a {
color: var(--p31-mid) !important;
text-decoration: none !important;
transition: all 0.2s ease !important;
}
a:hover, .a:hover {
color: var(--p31-peak) !important;
text-shadow: var(--bloom-soft) !important;
}
/* === Code/Pre === */
code, pre, .code {
background: var(--tube-black) !important;
border: 1px solid var(--p31-ghost) !important;
color: var(--p31-mid) !important;
font-family: inherit !important;
padding: 0.2rem 0.4rem !important;
border-radius: 3px !important;
}
pre, pre.code {
padding: 1rem !important;
overflow-x: auto !important;
}
/* === Modals/Dialogs === */
.modal, .dialog, .cbi-modal {
background: var(--tube-deep) !important;
border: 1px solid var(--p31-ghost) !important;
border-radius: 6px !important;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8), var(--bloom-box) !important;
}
.modal-overlay, .cbi-modal-overlay {
background: rgba(0, 0, 0, 0.85) !important;
}
/* === Login Page === */
.login-container, #login, form[action*="login"],
body.login {
background: var(--tube-black) !important;
}
.login-container, #login > div, .login-box {
max-width: 400px !important;
margin: 10vh auto !important;
padding: 2rem !important;
background: var(--tube-deep) !important;
border: 1px solid var(--p31-ghost) !important;
border-radius: 6px !important;
text-align: center !important;
}
.login-container h1, #login h1, .login-box h1 {
color: var(--p31-peak) !important;
text-shadow: var(--bloom-text) !important;
margin-bottom: 2rem !important;
letter-spacing: 3px !important;
}
.login-container input, #login input, .login-box input[type="text"],
.login-box input[type="password"] {
width: 100% !important;
max-width: none !important;
margin-bottom: 1rem !important;
}
.login-container button, #login button, .login-box button,
.login-box input[type="submit"] {
width: 100% !important;
padding: 0.75rem !important;
}
/* === Dashboard Widgets === */
.widget, .dashboard-widget {
background: var(--tube-deep) !important;
border: 1px solid var(--p31-ghost) !important;
border-radius: 4px !important;
padding: 1rem !important;
}
.widget-header, .dashboard-widget-header {
color: var(--p31-peak) !important;
font-size: 0.8rem !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
margin-bottom: 0.75rem !important;
text-shadow: var(--bloom-soft) !important;
}
.widget-value, .dashboard-widget-value {
font-size: 2rem !important;
color: var(--p31-peak) !important;
text-shadow: var(--bloom-text) !important;
font-weight: bold !important;
}
.widget-value.warning {
color: var(--p31-decay) !important;
text-shadow: 0 0 6px var(--p31-decay) !important;
}
.widget-value.danger {
color: #ff6688 !important;
text-shadow: 0 0 6px #ff4466 !important;
}
.widget-label {
font-size: 0.7rem !important;
color: var(--p31-dim) !important;
text-transform: uppercase !important;
}
/* === Dashboard Grid === */
.dashboard-grid {
display: grid !important;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important;
gap: 1rem !important;
margin-bottom: 1.5rem !important;
}
/* === Topology Visualization === */
.topology-container {
background: var(--tube-black) !important;
border: 1px solid var(--p31-ghost) !important;
border-radius: 4px !important;
padding: 1rem !important;
}
.topology-container svg {
width: 100% !important;
height: 400px !important;
}
.topology-node circle {
fill: var(--p31-dim) !important;
stroke: var(--p31-mid) !important;
stroke-width: 2 !important;
}
.topology-node.relay circle {
fill: var(--p31-decay-dim) !important;
stroke: var(--p31-decay) !important;
}
.topology-node.active circle {
fill: var(--p31-mid) !important;
stroke: var(--p31-peak) !important;
}
.topology-edge {
stroke: var(--p31-ghost) !important;
stroke-width: 1 !important;
}
.topology-edge.active {
stroke: var(--p31-dim) !important;
stroke-width: 2 !important;
animation: edgePulse 2s infinite !important;
}
@keyframes edgePulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* === Status Indicators === */
.status-online, .online, .running {
color: var(--p31-peak) !important;
}
.status-offline, .offline, .stopped {
color: #ff6688 !important;
}
.status-warning, .warning {
color: var(--p31-decay) !important;
}
/* Glowing dot indicators */
.status-dot {
display: inline-block !important;
width: 8px !important;
height: 8px !important;
border-radius: 50% !important;
margin-right: 0.5rem !important;
}
.status-dot.online {
background: var(--p31-peak) !important;
box-shadow: 0 0 6px var(--p31-peak) !important;
}
.status-dot.offline {
background: #ff4466 !important;
box-shadow: 0 0 6px #ff4466 !important;
}
.status-dot.warning {
background: var(--p31-decay) !important;
box-shadow: 0 0 6px var(--p31-decay) !important;
}
/* === Scrollbars === */
::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
::-webkit-scrollbar-track {
background: var(--tube-black) !important;
}
::-webkit-scrollbar-thumb {
background: var(--p31-ghost) !important;
border-radius: 4px !important;
}
::-webkit-scrollbar-thumb:hover {
background: var(--p31-dim) !important;
}
/* === Selection === */
::selection {
background: var(--p31-dim) !important;
color: var(--tube-black) !important;
}
/* === Responsive === */
@media (max-width: 768px) {
.main-left, #mainmenu, nav.main {
width: 100% !important;
min-height: auto !important;
border-right: none !important;
border-bottom: 1px solid var(--p31-ghost) !important;
}
.main-right, #maincontent, main {
padding: 1rem !important;
}
.dashboard-grid {
grid-template-columns: 1fr !important;
}
}
/* === Spinner/Loading === */
.spinning, .cbi-button-waiting {
animation: spin 1s linear infinite !important;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* === CRT Screen Curvature (subtle) === */
body {
border-radius: 8px !important;
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.5), inset 0 0 10px rgba(51, 255, 102, 0.1) !important;
}
/* === LuCI Specific Overrides === */
.cbi-value-title {
color: var(--p31-dim) !important;
font-weight: normal !important;
}
.cbi-value-description {
color: var(--p31-ghost) !important;
font-size: 0.8rem !important;
}
.cbi-section-descr {
color: var(--p31-dim) !important;
font-size: 0.85rem !important;
margin-bottom: 1rem !important;
}
/* Interface badges */
.ifacebadge {
background: var(--tube-black) !important;
border: 1px solid var(--p31-ghost) !important;
color: var(--p31-mid) !important;
}
/* Zone badges */
.zonebadge {
background: rgba(51, 255, 102, 0.1) !important;
border-color: var(--p31-dim) !important;
color: var(--p31-mid) !important;
}
.zonebadge.zone-lan {
border-color: var(--p31-mid) !important;
background: rgba(51, 255, 102, 0.15) !important;
}
.zonebadge.zone-wan {
border-color: var(--p31-decay-dim) !important;
background: rgba(255, 179, 71, 0.1) !important;
color: var(--p31-decay) !important;
}
/* Hide default theme backgrounds */
.main, .main-left, .main-right, .container,
#maincontent, #maincontainer, header, footer {
background-image: none !important;
}
/* Force dark background everywhere */
div, section, article, aside, main, nav, header, footer {
background-color: transparent;
}
/* Override any white/light backgrounds */
.white, .bg-white, .bg-light {
background: var(--tube-deep) !important;
color: var(--p31-mid) !important;
}

View File

@ -0,0 +1,433 @@
/**
* SecuBox CRT P31 Reusable UI Components
* CyberMind SecuBox 2026
*
* Provides CRT-styled UI components for SecuBox dashboards
*/
(function() {
'use strict';
var CRTComponents = {
/**
* Create a status widget with glowing value
*/
createWidget: function(options) {
var defaults = {
title: 'Widget',
value: '0',
unit: '',
status: 'normal', // normal, warning, danger
icon: null
};
var opts = Object.assign({}, defaults, options);
var statusClass = opts.status === 'warning' ? 'warning' :
opts.status === 'danger' ? 'danger' : '';
var widget = document.createElement('div');
widget.className = 'crt-widget';
widget.innerHTML = [
'<div class="crt-widget-header">' + this.escapeHtml(opts.title) + '</div>',
'<div class="crt-widget-value ' + statusClass + '">' +
this.escapeHtml(opts.value) +
(opts.unit ? '<span class="unit">' + this.escapeHtml(opts.unit) + '</span>' : '') +
'</div>'
].join('');
return widget;
},
/**
* Create a status badge
*/
createBadge: function(text, type) {
type = type || 'info';
var badge = document.createElement('span');
badge.className = 'crt-badge crt-badge-' + type;
badge.textContent = text;
return badge;
},
/**
* Create a status dot indicator
*/
createStatusDot: function(status) {
var dot = document.createElement('span');
dot.className = 'crt-status-dot ' + (status || 'offline');
return dot;
},
/**
* Create a progress bar
*/
createProgressBar: function(value, max, label) {
max = max || 100;
var percent = Math.min(100, Math.max(0, (value / max) * 100));
var container = document.createElement('div');
container.className = 'crt-progress';
var bar = document.createElement('div');
bar.className = 'crt-progress-bar';
bar.style.width = percent + '%';
if (label) {
var labelEl = document.createElement('span');
labelEl.className = 'crt-progress-label';
labelEl.textContent = label;
container.appendChild(labelEl);
}
container.appendChild(bar);
return container;
},
/**
* Create a terminal-style log viewer
*/
createLogViewer: function(lines, maxLines) {
maxLines = maxLines || 20;
lines = lines || [];
var container = document.createElement('div');
container.className = 'crt-log-viewer';
var displayLines = lines.slice(-maxLines);
displayLines.forEach(function(line) {
var lineEl = document.createElement('div');
lineEl.className = 'crt-log-line';
lineEl.textContent = line;
container.appendChild(lineEl);
});
// Auto-scroll to bottom
container.scrollTop = container.scrollHeight;
return container;
},
/**
* Create a topology node for SVG visualization
*/
createTopologyNode: function(svg, x, y, label, status, nodeType) {
var ns = 'http://www.w3.org/2000/svg';
var group = document.createElementNS(ns, 'g');
group.setAttribute('class', 'topology-node ' + (status || '') + ' ' + (nodeType || ''));
group.setAttribute('transform', 'translate(' + x + ',' + y + ')');
var circle = document.createElementNS(ns, 'circle');
circle.setAttribute('r', '20');
circle.setAttribute('cx', '0');
circle.setAttribute('cy', '0');
var text = document.createElementNS(ns, 'text');
text.setAttribute('y', '35');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('class', 'topology-label');
text.textContent = label || '';
group.appendChild(circle);
group.appendChild(text);
return group;
},
/**
* Create a topology edge for SVG visualization
*/
createTopologyEdge: function(svg, x1, y1, x2, y2, active) {
var ns = 'http://www.w3.org/2000/svg';
var line = document.createElementNS(ns, 'line');
line.setAttribute('class', 'topology-edge ' + (active ? 'active' : ''));
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
return line;
},
/**
* Create an alert/notification box
*/
createAlert: function(message, type) {
type = type || 'info';
var alert = document.createElement('div');
alert.className = 'crt-alert crt-alert-' + type;
alert.textContent = message;
var close = document.createElement('button');
close.className = 'crt-alert-close';
close.innerHTML = '&times;';
close.onclick = function() {
alert.remove();
};
alert.appendChild(close);
return alert;
},
/**
* Show a toast notification
*/
toast: function(message, type, duration) {
type = type || 'info';
duration = duration || 3000;
var toast = document.createElement('div');
toast.className = 'crt-toast crt-toast-' + type;
toast.textContent = message;
// Position in top-right
toast.style.cssText = [
'position: fixed',
'top: 1rem',
'right: 1rem',
'padding: 0.75rem 1.5rem',
'background: #080d05',
'border: 1px solid',
'border-radius: 4px',
'z-index: 10001',
'animation: toastIn 0.3s ease'
].join(';');
if (type === 'success') {
toast.style.borderColor = '#22cc44';
toast.style.color = '#33ff66';
} else if (type === 'error') {
toast.style.borderColor = '#ff4466';
toast.style.color = '#ff6688';
} else if (type === 'warning') {
toast.style.borderColor = '#cc7722';
toast.style.color = '#ffb347';
} else {
toast.style.borderColor = '#0f8822';
toast.style.color = '#22cc44';
}
document.body.appendChild(toast);
setTimeout(function() {
toast.style.animation = 'toastOut 0.3s ease';
setTimeout(function() {
toast.remove();
}, 300);
}, duration);
},
/**
* Create a data table with CRT styling
*/
createTable: function(headers, rows) {
var table = document.createElement('table');
table.className = 'crt-table';
// Header
var thead = document.createElement('thead');
var headerRow = document.createElement('tr');
headers.forEach(function(header) {
var th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Body
var tbody = document.createElement('tbody');
rows.forEach(function(row) {
var tr = document.createElement('tr');
row.forEach(function(cell) {
var td = document.createElement('td');
if (typeof cell === 'object' && cell.html) {
td.innerHTML = cell.html;
} else {
td.textContent = cell;
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
},
/**
* Escape HTML entities
*/
escapeHtml: function(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Add component styles
var styles = document.createElement('style');
styles.textContent = [
'.crt-widget {',
' background: #080d05;',
' border: 1px solid #052210;',
' border-radius: 4px;',
' padding: 1rem;',
' text-align: center;',
'}',
'.crt-widget-header {',
' color: #0f8822;',
' font-size: 0.75rem;',
' text-transform: uppercase;',
' letter-spacing: 1px;',
' margin-bottom: 0.5rem;',
'}',
'.crt-widget-value {',
' color: #33ff66;',
' font-size: 2rem;',
' font-weight: bold;',
' text-shadow: 0 0 6px #33ff66;',
'}',
'.crt-widget-value.warning {',
' color: #ffb347;',
' text-shadow: 0 0 6px #ffb347;',
'}',
'.crt-widget-value.danger {',
' color: #ff6688;',
' text-shadow: 0 0 6px #ff4466;',
'}',
'.crt-widget-value .unit {',
' font-size: 0.9rem;',
' margin-left: 0.25rem;',
'}',
'',
'.crt-badge {',
' display: inline-block;',
' padding: 0.15rem 0.5rem;',
' font-size: 0.7rem;',
' text-transform: uppercase;',
' letter-spacing: 1px;',
' border-radius: 2px;',
' border: 1px solid;',
'}',
'.crt-badge-success { border-color: #22cc44; color: #33ff66; background: rgba(51,255,102,0.1); }',
'.crt-badge-warning { border-color: #cc7722; color: #ffb347; background: rgba(255,179,71,0.1); }',
'.crt-badge-danger { border-color: #ff4466; color: #ff6688; background: rgba(255,68,102,0.1); }',
'.crt-badge-info { border-color: #0f8822; color: #22cc44; background: rgba(51,255,102,0.05); }',
'',
'.crt-status-dot {',
' display: inline-block;',
' width: 8px;',
' height: 8px;',
' border-radius: 50%;',
' margin-right: 0.5rem;',
'}',
'.crt-status-dot.online { background: #33ff66; box-shadow: 0 0 6px #33ff66; }',
'.crt-status-dot.offline { background: #ff4466; box-shadow: 0 0 6px #ff4466; }',
'.crt-status-dot.warning { background: #ffb347; box-shadow: 0 0 6px #ffb347; }',
'',
'.crt-progress {',
' background: #050803;',
' border: 1px solid #052210;',
' border-radius: 2px;',
' height: 20px;',
' position: relative;',
' overflow: hidden;',
'}',
'.crt-progress-bar {',
' background: linear-gradient(90deg, #0f8822, #22cc44);',
' height: 100%;',
' box-shadow: 0 0 10px #22cc44;',
' transition: width 0.3s ease;',
'}',
'.crt-progress-label {',
' position: absolute;',
' top: 50%;',
' left: 50%;',
' transform: translate(-50%, -50%);',
' font-size: 0.7rem;',
' color: #22cc44;',
'}',
'',
'.crt-log-viewer {',
' background: #050803;',
' border: 1px solid #052210;',
' border-radius: 4px;',
' padding: 0.5rem;',
' font-size: 0.8rem;',
' max-height: 300px;',
' overflow-y: auto;',
'}',
'.crt-log-line {',
' color: #22cc44;',
' padding: 0.1rem 0;',
' border-bottom: 1px solid #052210;',
'}',
'.crt-log-line:last-child { border-bottom: none; }',
'',
'.crt-alert {',
' padding: 1rem;',
' border-radius: 4px;',
' margin-bottom: 1rem;',
' border: 1px solid;',
' position: relative;',
'}',
'.crt-alert-success { background: rgba(51,255,102,0.1); border-color: #0f8822; color: #22cc44; }',
'.crt-alert-warning { background: rgba(255,179,71,0.1); border-color: #cc7722; color: #ffb347; }',
'.crt-alert-error { background: rgba(255,68,102,0.1); border-color: #ff4466; color: #ff6688; }',
'.crt-alert-info { background: rgba(51,255,102,0.05); border-color: #052210; color: #22cc44; }',
'.crt-alert-close {',
' position: absolute;',
' top: 0.5rem;',
' right: 0.5rem;',
' background: none;',
' border: none;',
' color: inherit;',
' cursor: pointer;',
' font-size: 1.2rem;',
' opacity: 0.6;',
'}',
'.crt-alert-close:hover { opacity: 1; }',
'',
'.crt-table {',
' width: 100%;',
' border-collapse: collapse;',
' font-size: 0.85rem;',
'}',
'.crt-table th {',
' color: #0f8822;',
' font-weight: normal;',
' text-transform: uppercase;',
' font-size: 0.75rem;',
' letter-spacing: 1px;',
' padding: 0.75rem;',
' text-align: left;',
' border-bottom: 1px solid #052210;',
' background: #050803;',
'}',
'.crt-table td {',
' padding: 0.75rem;',
' border-bottom: 1px solid #052210;',
' color: #22cc44;',
'}',
'.crt-table tr:hover td { background: rgba(51,255,102,0.03); }',
'',
'.topology-label {',
' fill: #22cc44;',
' font-size: 0.75rem;',
'}',
'',
'@keyframes toastIn {',
' from { opacity: 0; transform: translateX(100%); }',
' to { opacity: 1; transform: translateX(0); }',
'}',
'@keyframes toastOut {',
' from { opacity: 1; transform: translateX(0); }',
' to { opacity: 0; transform: translateX(100%); }',
'}'
].join('\n');
document.head.appendChild(styles);
// Export to global scope
window.CRTComponents = CRTComponents;
})();

View File

@ -0,0 +1,264 @@
/**
* SecuBox CRT P31 Effects Engine
* CyberMind SecuBox 2026
*
* Provides CRT terminal effects for the SecuBox LuCI theme
*/
(function() {
'use strict';
var SecuBoxCRT = {
config: {
enableScanlines: true,
enableFlicker: false, // Disabled by default (can be distracting)
enableGlow: true,
enableBootSequence: true,
bootSequenceOnce: true
},
/**
* Add scanlines overlay to the page
*/
addScanlines: function() {
if (!this.config.enableScanlines) return;
if (document.querySelector('.crt-scanlines')) return;
var scanlines = document.createElement('div');
scanlines.className = 'crt-scanlines';
scanlines.style.cssText = [
'position: fixed',
'top: 0',
'left: 0',
'width: 100%',
'height: 100%',
'pointer-events: none',
'background: repeating-linear-gradient(0deg, rgba(0,0,0,0.15), rgba(0,0,0,0.15) 1px, transparent 1px, transparent 2px)',
'z-index: 9999'
].join(';');
document.body.appendChild(scanlines);
},
/**
* Enable subtle screen flicker effect
*/
enableFlicker: function() {
if (!this.config.enableFlicker) return;
document.body.classList.add('crt-flicker');
// Add flicker keyframes if not present
if (!document.querySelector('#crt-flicker-style')) {
var style = document.createElement('style');
style.id = 'crt-flicker-style';
style.textContent = [
'@keyframes crtFlicker {',
' 0%, 100% { opacity: 1; }',
' 92% { opacity: 0.98; }',
' 93% { opacity: 1; }',
' 94% { opacity: 0.97; }',
'}',
'.crt-flicker { animation: crtFlicker 0.15s infinite; }'
].join('\n');
document.head.appendChild(style);
}
},
/**
* Add phosphor glow effect on hover for interactive elements
*/
addPhosphorGlow: function() {
if (!this.config.enableGlow) return;
var glowElements = document.querySelectorAll('button, .cbi-button, a, input[type="submit"]');
var bloomSoft = '0 0 6px #33ff66, 0 0 14px rgba(51,255,102,0.5)';
glowElements.forEach(function(el) {
el.addEventListener('mouseenter', function() {
this.style.textShadow = bloomSoft;
});
el.addEventListener('mouseleave', function() {
this.style.textShadow = '';
});
});
},
/**
* Add terminal cursor effect to text inputs
*/
addTerminalCursor: function() {
var inputs = document.querySelectorAll('input[type="text"], input[type="password"], input[type="email"], input[type="url"], textarea');
inputs.forEach(function(input) {
input.addEventListener('focus', function() {
this.style.caretColor = '#33ff66';
});
});
},
/**
* Typewriter effect for an element
*/
typewriterEffect: function(element, speed) {
speed = speed || 50;
var text = element.textContent;
element.textContent = '';
element.style.visibility = 'visible';
var i = 0;
function type() {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
setTimeout(type, speed);
}
}
type();
},
/**
* Boot sequence animation
*/
bootSequence: function() {
if (!this.config.enableBootSequence) return;
if (this.config.bootSequenceOnce && sessionStorage.getItem('crt-booted')) return;
var bootText = [
'[ SECUBOX MESH DAEMON ]',
'Initializing P31 phosphor display...',
'Loading security modules...',
'mDNS discovery: ACTIVE',
'Mesh topology: ONLINE',
'System ready.'
];
var container = document.createElement('div');
container.id = 'crt-boot-sequence';
container.style.cssText = [
'position: fixed',
'top: 0',
'left: 0',
'width: 100%',
'height: 100%',
'background: #050803',
'z-index: 10000',
'display: flex',
'flex-direction: column',
'justify-content: center',
'align-items: center',
'font-family: "Courier Prime", "Courier New", monospace',
'color: #22cc44'
].join(';');
document.body.appendChild(container);
var lineIndex = 0;
function showLine() {
if (lineIndex < bootText.length) {
var line = document.createElement('div');
var isHeader = lineIndex === 0;
line.style.cssText = [
'opacity: 0',
'margin: 0.3rem 0',
isHeader ? 'font-size: 1.2rem; color: #33ff66; text-shadow: 0 0 6px #33ff66;' : ''
].join(';');
line.textContent = bootText[lineIndex];
container.appendChild(line);
setTimeout(function() {
line.style.transition = 'opacity 0.3s';
line.style.opacity = '1';
}, 50);
lineIndex++;
setTimeout(showLine, 300);
} else {
setTimeout(function() {
container.style.transition = 'opacity 0.5s';
container.style.opacity = '0';
setTimeout(function() {
container.remove();
}, 500);
}, 800);
}
}
showLine();
sessionStorage.setItem('crt-booted', 'true');
},
/**
* Apply status dot glow effects
*/
applyStatusGlow: function() {
// Find status indicators and add appropriate glow
var onlineElements = document.querySelectorAll('.online, .running, .status-online');
var offlineElements = document.querySelectorAll('.offline, .stopped, .status-offline');
var warningElements = document.querySelectorAll('.warning, .status-warning');
onlineElements.forEach(function(el) {
el.style.textShadow = '0 0 6px #33ff66';
});
offlineElements.forEach(function(el) {
el.style.textShadow = '0 0 6px #ff4466';
});
warningElements.forEach(function(el) {
el.style.textShadow = '0 0 6px #ffb347';
});
},
/**
* Initialize all CRT effects
*/
init: function() {
var self = this;
this.addScanlines();
this.enableFlicker();
this.addPhosphorGlow();
this.addTerminalCursor();
this.applyStatusGlow();
// Boot sequence on first visit
if (this.config.enableBootSequence) {
this.bootSequence();
}
// Re-apply effects on dynamic content changes
var observer = new MutationObserver(function(mutations) {
self.addPhosphorGlow();
self.addTerminalCursor();
self.applyStatusGlow();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
},
/**
* Configure CRT effects
*/
configure: function(options) {
for (var key in options) {
if (this.config.hasOwnProperty(key)) {
this.config[key] = options[key];
}
}
}
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
SecuBoxCRT.init();
});
} else {
SecuBoxCRT.init();
}
// Export to global scope
window.SecuBoxCRT = SecuBoxCRT;
})();

View File

@ -0,0 +1,37 @@
<%#
SecuBox CRT P31 Theme - Footer Template
CyberMind — SecuBox — 2026
-%>
</main>
</div>
<footer class="main-foot" id="mainfooter">
<span>SecuBox Mesh v<%=luci.version.distversion%></span>
<span class="separator">|</span>
<span>Powered by <a href="https://openwrt.org" target="_blank">OpenWrt</a></span>
<span class="separator">|</span>
<span>&copy; 2026 CyberMind</span>
</footer>
<%
local webroot = luci.http.getenv("DOCUMENT_ROOT") or ""
local resource = luci.dispatcher.build_url("admin/translations", luci.i18n.context.lang)
-%>
<script>
(function() {
// Initialize SecuBox CRT effects
if (window.SecuBoxCRT) {
window.SecuBoxCRT.configure({
enableScanlines: true,
enableFlicker: false,
enableGlow: true,
enableBootSequence: true
});
}
})();
</script>
</body>
</html>

View File

@ -0,0 +1,106 @@
<%#
SecuBox CRT P31 Theme - Header Template
CyberMind — SecuBox — 2026
-%>
<%
local sys = require "luci.sys"
local http = require "luci.http"
local disp = require "luci.dispatcher"
local util = require "luci.util"
local fs = require "nixio.fs"
local request = disp.context.path
local request2 = disp.context.request
local category = request[1]
local cattree = category and disp.node(category)
local node = token and disp.node(unpack(request)) or nil
local title = node and node.title or "SecuBox"
local hostname = fs.readfile("/proc/sys/kernel/hostname") or ""
hostname = util.trim(hostname)
-%>
<!DOCTYPE html>
<html lang="<%=luci.i18n.context.lang%>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><%=pcdata(hostname)%> - <%=striptags(title)%> - SecuBox</title>
<link rel="stylesheet" href="<%=media%>/cascade.css?v=<%=luci.version.revision%>">
<script src="<%=media%>/crt-engine.js?v=<%=luci.version.revision%>"></script>
<% if node and node.css then %>
<link rel="stylesheet" href="<%=resource%>/<%=node.css%>">
<% end -%>
<% if css then %>
<style title="text/css"><%= css %></style>
<% end -%>
</head>
<body class="lang_<%=luci.i18n.context.lang%>">
<header class="main-head">
<div class="brand">SECUBOX</div>
<div class="status">
<span class="hostname"><%=pcdata(hostname)%></span>
</div>
</header>
<div id="maincontainer" class="main-container">
<nav class="main-left" id="mainmenu">
<ul class="mainmenu">
<%
local function submenu(prefix, node, level)
local childs = disp.node_childs(node)
if #childs > 0 then
for i, r in ipairs(childs) do
local nnode = node.nodes[r]
local href = controller .. prefix .. r
local title = pcdata(striptags(nnode.title))
local active = (request[level] == r)
if nnode.hidden then
-- skip hidden nodes
else
%>
<li class="<%=active and "active " or ""%><%=nnode.nodes and "slide" or ""%>">
<a href="<%=href%>"><%=title%></a>
<%
if active and nnode.nodes then
submenu(prefix .. r .. "/", nnode, level + 1)
end
%>
</li>
<%
end
end
end
end
for i, r in ipairs(disp.node_childs(cattree)) do
local nnode = cattree.nodes[r]
local href = controller .. "/" .. category .. "/" .. r
local title = pcdata(striptags(nnode.title))
local active = (request[2] == r)
if not nnode.hidden then
%>
<li class="<%=active and "active " or ""%><%=nnode.nodes and "slide" or ""%>">
<a href="<%=href%>"><%=title%></a>
<%
if active and nnode.nodes then
submenu("/" .. category .. "/" .. r .. "/", nnode, 3)
end
%>
</li>
<%
end
end
%>
</ul>
</nav>
<main class="main-right" id="maincontent">
<% if title then %>
<h2><%=pcdata(striptags(title))%></h2>
<% end %>

View File

@ -0,0 +1,215 @@
<%#
SecuBox CRT P31 Theme - Login Page Template
CyberMind — SecuBox — 2026
-%>
<%
local sys = require "luci.sys"
local fs = require "nixio.fs"
local util = require "luci.util"
local hostname = fs.readfile("/proc/sys/kernel/hostname") or "SecuBox"
hostname = util.trim(hostname)
-%>
<!DOCTYPE html>
<html lang="<%=luci.i18n.context.lang%>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><%=pcdata(hostname)%> - Login - SecuBox</title>
<link rel="stylesheet" href="<%=media%>/cascade.css?v=<%=luci.version.revision%>">
<script src="<%=media%>/crt-engine.js?v=<%=luci.version.revision%>"></script>
<style>
body.login {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 1rem;
}
.login-container {
width: 100%;
max-width: 400px;
background: #080d05;
border: 1px solid #052210;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8), 0 0 8px rgba(51,255,102,0.1);
}
.login-logo {
text-align: center;
margin-bottom: 2rem;
}
.login-logo h1 {
color: #33ff66;
font-size: 2rem;
letter-spacing: 4px;
text-shadow: 0 0 6px #33ff66, 0 0 14px rgba(51,255,102,0.5);
margin: 0;
font-weight: bold;
}
.login-logo h1::before {
content: '[ ';
color: #0f8822;
}
.login-logo h1::after {
content: ' ]';
color: #0f8822;
}
.login-logo .tagline {
color: #0f8822;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 2px;
margin-top: 0.5rem;
}
.login-hostname {
color: #22cc44;
font-size: 0.85rem;
text-align: center;
margin-bottom: 1.5rem;
padding: 0.5rem;
border: 1px solid #052210;
border-radius: 3px;
background: #050803;
}
.login-form label {
display: block;
color: #0f8822;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.3rem;
}
.login-form input[type="text"],
.login-form input[type="password"] {
width: 100%;
background: #050803;
border: 1px solid #052210;
color: #22cc44;
padding: 0.75rem;
font-family: inherit;
font-size: 1rem;
border-radius: 3px;
margin-bottom: 1rem;
}
.login-form input:focus {
outline: none;
border-color: #22cc44;
box-shadow: 0 0 8px rgba(51,255,102,0.3), inset 0 0 4px rgba(51,255,102,0.1);
}
.login-form button {
width: 100%;
background: transparent;
border: 1px solid #22cc44;
color: #33ff66;
padding: 0.75rem;
font-family: inherit;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 2px;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s ease;
}
.login-form button:hover {
background: #22cc44;
color: #050803;
text-shadow: none;
}
.login-error {
background: rgba(255, 68, 102, 0.1);
border: 1px solid #ff4466;
color: #ff6688;
padding: 0.75rem;
border-radius: 3px;
margin-bottom: 1rem;
font-size: 0.85rem;
text-align: center;
}
.login-footer {
text-align: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #052210;
color: #052210;
font-size: 0.7rem;
}
.login-footer a {
color: #0f8822;
}
</style>
</head>
<body class="login">
<div class="login-container">
<div class="login-logo">
<h1>SECUBOX</h1>
<div class="tagline">Mesh Security Appliance</div>
</div>
<div class="login-hostname">
<%=pcdata(hostname)%>
</div>
<% if fuser then %>
<div class="login-error">
Invalid username or password
</div>
<% end %>
<form class="login-form" method="post" action="<%=pcdata(duser and url("admin/uci/changes") or REQUEST_URI)%>">
<div class="form-group">
<label for="luci_username"><%:Username%></label>
<input type="text" id="luci_username" name="luci_username"
value="<%=pcdata(duser)%>" placeholder="root"
autocomplete="username" autofocus>
</div>
<div class="form-group">
<label for="luci_password"><%:Password%></label>
<input type="password" id="luci_password" name="luci_password"
placeholder="********" autocomplete="current-password">
</div>
<button type="submit"><%:Login%></button>
</form>
<div class="login-footer">
<span>SecuBox v<%=luci.version.distversion%></span>
<span>|</span>
<a href="https://openwrt.org" target="_blank">OpenWrt</a>
</div>
</div>
<script>
// Focus password field if username is pre-filled
document.addEventListener('DOMContentLoaded', function() {
var username = document.getElementById('luci_username');
var password = document.getElementById('luci_password');
if (username && username.value && password) {
password.focus();
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,10 @@
#!/bin/sh
# SecuBox CRT P31 Theme - UCI Defaults
# Set SecuBox theme as default LuCI theme
uci -q batch <<-EOF
set luci.main.mediaurlbase='/luci-static/secubox'
commit luci
EOF
exit 0